├── .codeclimate.yml ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── postgrest.rb └── postgrest │ ├── builders │ ├── base_builder.rb │ ├── filter_builder.rb │ └── query_builder.rb │ ├── client.rb │ ├── http.rb │ ├── responses │ ├── base_response.rb │ ├── delete_response.rb │ ├── get_response.rb │ ├── patch_response.rb │ └── post_response.rb │ └── version.rb ├── postgrest.gemspec └── spec ├── postgrest ├── builders │ ├── base_builder_spec.rb │ ├── filter_builder_spec.rb │ └── query_builder_spec.rb ├── client_spec.rb ├── http_spec.rb └── responses │ ├── base_response_spec.rb │ ├── delete_response_spec.rb │ ├── get_response_spec.rb │ ├── patch_response_spec.rb │ └── post_response_spec.rb ├── postgrest_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | rubocop: 4 | enabled: true 5 | channel: rubocop-0-60 6 | config: 7 | file: ".rubocop.yml" 8 | exclude_patterns: 9 | - "spec/" -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | ruby-version: ['2.6', '2.7', '3.0'] 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Ruby 27 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 28 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 29 | # uses: ruby/setup-ruby@v1 30 | uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e 31 | with: 32 | ruby-version: ${{ matrix.ruby-version }} 33 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 34 | - name: Run tests 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | bin/private_console 14 | 15 | *.gem -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_mode: 2 | merge: 3 | - Exclude 4 | AllCops: 5 | SuggestExtensions: false 6 | Exclude: 7 | - 'spec/**/*.rb' 8 | - 'bin/**/*' 9 | Style/Documentation: 10 | Enabled: false 11 | Style/MissingRespondToMissing: 12 | Enabled: false 13 | Naming/MethodParameterName: 14 | Enabled: false 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CC_TEST_REPORTER_ID=$CODE_CLIMATE_TOKEN 4 | language: ruby 5 | rvm: 6 | - 2.6.6 7 | before_script: 8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 9 | - chmod +x ./cc-test-reporter 10 | - ./cc-test-reporter before-build 11 | - gem install bundler:2.1.4 12 | script: 13 | - bundle exec rubocop 14 | - bundle exec rspec 15 | after_script: 16 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.3] 2 | 3 | ### First full query working 4 | 5 | - Change how queries were build 6 | - Added Responses classes 7 | - Introduced #before_hooks that execute some action before execute the HTTP request 8 | - Added #method_missing in order to build the relationship queries 9 | - Added #order 10 | - Added #limit 11 | - Added #offset 12 | 13 | ## [0.0.2] 14 | 15 | ### Added first working select 16 | 17 | - Added #from and #select methods 18 | - Added #eq method 19 | - Introduced the #execute method 20 | 21 | ## [0.0.1] 22 | 23 | ### Everything should start from somewhere 24 | 25 | - Create the project 26 | - Added HTTP requests 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in postgrest.gemspec 6 | gemspec 7 | 8 | gem 'rake', '~> 12.0' 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | postgrest (0.3.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | coderay (1.1.3) 11 | diff-lcs (1.4.4) 12 | docile (1.3.5) 13 | method_source (1.0.0) 14 | parallel (1.20.1) 15 | parser (3.0.1.0) 16 | ast (~> 2.4.1) 17 | pry (0.14.1) 18 | coderay (~> 1.1) 19 | method_source (~> 1.0) 20 | rainbow (3.0.0) 21 | rake (12.3.3) 22 | regexp_parser (2.1.1) 23 | rexml (3.2.5) 24 | rspec (3.10.0) 25 | rspec-core (~> 3.10.0) 26 | rspec-expectations (~> 3.10.0) 27 | rspec-mocks (~> 3.10.0) 28 | rspec-core (3.10.1) 29 | rspec-support (~> 3.10.0) 30 | rspec-expectations (3.10.1) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.10.0) 33 | rspec-mocks (3.10.2) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.10.0) 36 | rspec-support (3.10.2) 37 | rubocop (1.13.0) 38 | parallel (~> 1.10) 39 | parser (>= 3.0.0.0) 40 | rainbow (>= 2.2.2, < 4.0) 41 | regexp_parser (>= 1.8, < 3.0) 42 | rexml 43 | rubocop-ast (>= 1.2.0, < 2.0) 44 | ruby-progressbar (~> 1.7) 45 | unicode-display_width (>= 1.4.0, < 3.0) 46 | rubocop-ast (1.4.1) 47 | parser (>= 2.7.1.5) 48 | ruby-progressbar (1.11.0) 49 | simplecov (0.21.2) 50 | docile (~> 1.1) 51 | simplecov-html (~> 0.11) 52 | simplecov_json_formatter (~> 0.1) 53 | simplecov-html (0.12.3) 54 | simplecov_json_formatter (0.1.2) 55 | unicode-display_width (2.0.0) 56 | 57 | PLATFORMS 58 | ruby 59 | 60 | DEPENDENCIES 61 | postgrest! 62 | pry 63 | rake (~> 12.0) 64 | rspec (~> 3.5) 65 | rubocop (~> 1.13.0) 66 | simplecov (~> 0.21.2) 67 | 68 | BUNDLED WITH 69 | 2.1.4 70 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Marcelo Barreto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgREST 2 | 3 | ## Status 4 | 5 | [![Build Status](https://api.travis-ci.com/marcelobarreto/postgrest-rb.svg?branch=master)](https://travis-ci.com/marcelobarreto/postgrest-rb) 6 | [![Gem Version](https://badge.fury.io/rb/postgrest.svg)](https://badge.fury.io/rb/postgrest) 7 | [![Code Climate](https://codeclimate.com/github/marcelobarreto/postgrest-rb.svg)](https://codeclimate.com/github/marcelobarreto/postgrest-rb) 8 | [![Code Climate](https://codeclimate.com/github/marcelobarreto/postgrest-rb/coverage.svg)](https://codeclimate.com/github/marcelobarreto/postgrest-rb) 9 | [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop) 10 | [![RubyGems](https://img.shields.io/gem/dt/postgrest.svg?style=flat)](https://rubygems.org/gems/postgrest) 11 | 12 | Ruby client for [PostgREST](https://postgrest.org/) 13 | 14 | This gem is under development, any help are welcome :muscle: 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'postgrest' 22 | ``` 23 | 24 | And then execute: 25 | 26 | `$ bundle install` 27 | 28 | Or install it yourself as: 29 | 30 | `$ gem install postgrest` 31 | 32 | ## Usage 33 | 34 | ### Configuration 35 | 36 | ```ruby 37 | db = Postgrest::Client.new(url: url, headers: headers, schema: schema) 38 | ``` 39 | 40 | ### Selecting 41 | 42 | ```ruby 43 | # Basic select 44 | 45 | db.from('todos').select('*').execute 46 | # or just db.from('todos').select 47 | 48 | #1, "title"=>"foo", "completed"=>false}, {"id"=>2, "title"=>"foo", "completed"=>false}]> 49 | 50 | # Selecting just one or more fields 51 | db.from('todos').select(:title).execute 52 | 53 | #"foo"}, {"title"=>"foo"}]> 54 | 55 | ``` 56 | 57 | #### Renaming a column name 58 | 59 | You have the ability to alias the name of the column you want doing as follows: 60 | 61 | ```ruby 62 | db.from('todos').select('name:title').eq(id: 112).execute 63 | #"Go to the gym"}]> 64 | ``` 65 | 66 | ### Querying 67 | 68 | ```ruby 69 | db.from('todos').select('*').eq(id: 100).execute 70 | # 100, "title"=>"foo", "completed" => true}}]> 71 | 72 | 73 | db.from('todos').select('*').neq(id: 100).execute 74 | # 101, "title"=>"foo", "completed" => true}}]> 75 | ``` 76 | 77 | ### Ordering 78 | 79 | TODO 80 | 81 | ### Relationships 82 | 83 | TODO 84 | 85 | ### Full query example 86 | 87 | ```ruby 88 | db.from('todos').select(:id, :title).owners(:name, as: :owner).workers(:name, as: :worker).in(id: [112, 113]).order(id: :asc).execute 89 | 90 | #112, "title"=>"Eat something", "owner"=>{"name"=>"Marcelo"}, "worker"=>{"name"=>"Marcelo"}}, {"id"=>113, "title"=>"Go to the gym", "owner"=>{"name"=>"Marcelo"}, "worker"=>nil}]> 91 | ``` 92 | 93 | ### Inserting 94 | 95 | ```ruby 96 | db.from('todos').insert(title: 'Go to the gym', completed: false).execute 97 | 98 | #1, "title"=>"Go to the gym", "completed"=>false}]> 99 | 100 | db.from('todos').upsert(id: 1, title: 'Ok, I wont go to the gym', completed: true).execute 101 | 102 | #1, "title"=>"Ok, I wont go to the gym", "completed"=>true}]> 103 | 104 | # Inserting multiple rows at once 105 | db.from('todos').insert([ 106 | { title: 'Go to the gym', completed: false }, 107 | { title: 'Walk in the park', completed: true }, 108 | ]).execute 109 | 110 | #110, "title"=>"Go to the gym", "completed"=>false}, {"id"=>111, "title"=>"Walk in the park", "completed"=>true}]> 111 | 112 | ``` 113 | 114 | ### Updating 115 | 116 | ```ruby 117 | 118 | # Query before update 119 | 120 | db.from('todos').update(title: 'foobar').eq(id: 109).execute 121 | 122 | #106, "title"=>"foo", "completed"=>false}]> 123 | 124 | # Update all rows 125 | 126 | db.from('todos').update(title: 'foobar').execute 127 | 128 | #107, "title"=>"foobar", "completed"=>false}, {"id"=>1, "title"=>"foobar", "completed"=>true}, {"id"=>110, "title"=>"foobar", "completed"=>false}, {"id"=>111, "title"=>"foobar", "completed"=>true}, {"id"=>106, "title"=>"foobar", "completed"=>false}]> 129 | ``` 130 | 131 | ### Deleting 132 | 133 | ```ruby 134 | # Querying before delete 135 | 136 | db.from('todos').delete.eq(id: 109).execute 137 | #109, "title"=>"Go to the gym", "completed"=>false}]> 138 | 139 | # OR deleting everything 140 | 141 | db.from('todos').delete.execute 142 | 143 | #110, "title"=>"Go to the gym", "completed"=>false}, {"id"=>111, "title"=>"Go to the gym", "completed"=>false}]> 144 | ``` 145 | 146 | ## Development 147 | 148 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 149 | 150 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 151 | 152 | ## Contributing 153 | 154 | Bug reports and pull requests are welcome on GitHub at https://github.com/marcelobarreto/postgrest. 155 | 156 | ## License 157 | 158 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 159 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'postgrest' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/postgrest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'net/http' 4 | require 'json' 5 | require 'cgi' 6 | 7 | require 'postgrest/version' 8 | 9 | # Builders 10 | require 'postgrest/builders/base_builder' 11 | require 'postgrest/builders/query_builder' 12 | require 'postgrest/builders/filter_builder' 13 | 14 | # Responses 15 | require 'postgrest/responses/base_response' 16 | require 'postgrest/responses/get_response' 17 | require 'postgrest/responses/post_response' 18 | require 'postgrest/responses/patch_response' 19 | require 'postgrest/responses/delete_response' 20 | 21 | require 'postgrest/http' 22 | require 'postgrest/client' 23 | 24 | module Postgrest 25 | class MissingTableError < StandardError; end 26 | 27 | class InvalidHTTPMethod < ArgumentError; end 28 | 29 | class RequestError < StandardError; end 30 | end 31 | -------------------------------------------------------------------------------- /lib/postgrest/builders/base_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Builders 5 | class BaseBuilder 6 | def self.before_execute_hooks 7 | @before_execute ||= [] 8 | @before_execute 9 | end 10 | 11 | def self.before_execute(*values) 12 | @before_execute = values 13 | end 14 | 15 | attr_reader :http 16 | 17 | def initialize(http) 18 | @http = http 19 | end 20 | 21 | def call 22 | self.class.before_execute_hooks.each { |method_name| send(method_name) } 23 | 24 | http.call 25 | end 26 | alias execute call 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/postgrest/builders/filter_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Builders 5 | class FilterBuilder < BaseBuilder 6 | before_execute :update_http_instance 7 | 8 | SIMPLE_MATCHERS = %i[eq neq gt gte lt lte like is ilike fts plfts phfts wfts].freeze 9 | RANGE_MATCHERS = %i[sl sr nxr nxl adj].freeze 10 | 11 | def initialize(http) 12 | super 13 | @inverse_next = false 14 | end 15 | 16 | SIMPLE_MATCHERS.each do |method_name| 17 | define_method(method_name) do |values| 18 | transform_params(method_name: method_name, values: values) 19 | 20 | self 21 | end 22 | end 23 | 24 | RANGE_MATCHERS.each do |method_name| 25 | define_method(method_name) do |values| 26 | transform_params(method_name: method_name, values: values) do |key, value| 27 | [key, "range=#{method_key(method_name)}.(#{value.join(',')})"] 28 | end 29 | 30 | self 31 | end 32 | end 33 | 34 | def in(values = []) 35 | transform_params(method_name: __method__, values: values) do |key, value| 36 | [key, "#{method_key(__method__)}.(#{value.join(',')})"] 37 | end 38 | 39 | self 40 | end 41 | 42 | def order(values) 43 | transform_params(method_name: __method__, values: values) do |key, value| 44 | asc = value.to_sym != :desc 45 | asc = !asc if should_invert? 46 | 47 | [__method__, "#{key}.#{asc ? 'asc' : 'desc'}"] 48 | end 49 | 50 | self 51 | end 52 | 53 | def method_missing(method_name, *columns, as: nil) 54 | decoded_query['select'] += as ? ",#{as}:#{method_name}" : ",#{method_name}" 55 | decoded_query['select'] += columns.empty? ? '(*)' : "(#{columns.join(',')})" 56 | 57 | self 58 | end 59 | 60 | def query 61 | http.uri.query 62 | end 63 | 64 | def decoded_query 65 | @decoded_query ||= URI.decode_www_form(query).to_h 66 | end 67 | 68 | def limit(number = 0) 69 | decoded_query['limit'] = number 70 | self 71 | end 72 | 73 | def offset(number = 0) 74 | decoded_query['offset'] = number 75 | self 76 | end 77 | 78 | def not 79 | @inverse_next = !@inverse_next 80 | self 81 | end 82 | 83 | private 84 | 85 | def should_invert? 86 | @inverse_next 87 | end 88 | 89 | def transform_params(values:, method_name:) 90 | values.each do |k, v| 91 | key, value = yield(k, v) if block_given? 92 | key ||= k 93 | value ||= "#{method_key(method_name)}.#{v}" 94 | 95 | decoded_query[key.to_s] = value 96 | end 97 | 98 | @inverse_next = false 99 | end 100 | 101 | def method_key(name) 102 | should_invert? ? "not.#{name}" : name 103 | end 104 | 105 | # Before execute callback 106 | def update_http_instance 107 | http.update_query_params(decoded_query) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/postgrest/builders/query_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Builders 5 | class QueryBuilder 6 | attr_reader :uri, :headers, :schema 7 | 8 | def initialize(url:, headers:, schema:) 9 | @uri = URI(url) 10 | @headers = headers 11 | @schema = schema 12 | end 13 | 14 | def select(*columns, extra_headers: {}) 15 | columns.compact! 16 | columns = ['*'] if columns.length.zero? 17 | request = HTTP.new(uri: uri, query: { select: columns.join(',') }, headers: headers.merge(extra_headers)) 18 | 19 | FilterBuilder.new(request) 20 | end 21 | 22 | def insert(values) 23 | upsert(values, extra_headers: { Prefer: 'return=representation' }) 24 | end 25 | 26 | def upsert(values, extra_headers: {}) 27 | extra_headers[:Prefer] ||= 'return=representation,resolution=merge-duplicates' 28 | request = HTTP.new(uri: uri, body: values, http_method: :post, headers: headers.merge(extra_headers)) 29 | 30 | BaseBuilder.new(request) 31 | end 32 | 33 | def update(values, extra_headers: {}) 34 | extra_headers[:Prefer] ||= 'return=representation' 35 | request = HTTP.new(uri: uri, body: values, http_method: :patch, headers: headers.merge(extra_headers)) 36 | 37 | FilterBuilder.new(request) 38 | end 39 | 40 | def delete(extra_headers: {}) 41 | extra_headers[:Prefer] ||= 'return=representation' 42 | request = HTTP.new(uri: uri, http_method: :delete, headers: headers.merge(extra_headers)) 43 | 44 | FilterBuilder.new(request) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/postgrest/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | class Client 5 | DEFAULT_SCHEMA = 'public' 6 | 7 | attr_reader :url, :headers, :schema 8 | 9 | def initialize(url:, headers: {}, schema: DEFAULT_SCHEMA) 10 | @url = URI(url) 11 | @headers = headers 12 | @schema = schema 13 | end 14 | 15 | def from(table) 16 | raise MissingTableError if table.nil? || table.empty? 17 | 18 | Builders::QueryBuilder.new(url: "#{url}/#{table}", headers: headers, schema: schema) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/postgrest/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | class HTTP 5 | METHODS = { 6 | get: Net::HTTP::Get, 7 | post: Net::HTTP::Post, 8 | patch: Net::HTTP::Patch, 9 | put: Net::HTTP::Patch, 10 | delete: Net::HTTP::Delete 11 | }.freeze 12 | 13 | RESPONSES = { 14 | get: Responses::GetResponse, 15 | post: Responses::PostResponse, 16 | put: Responses::PatchResponse, 17 | patch: Responses::PatchResponse, 18 | delete: Responses::DeleteResponse, 19 | options: Responses::GetResponse # ? 20 | }.freeze 21 | 22 | USER_AGENT = 'PostgREST Ruby Client' 23 | 24 | attr_reader :request, :response, :query, :body, :headers, :http_method, :uri 25 | 26 | def initialize(uri:, query: {}, body: {}, headers: {}, http_method: :get) 27 | @uri = uri 28 | @body = body 29 | @headers = headers 30 | @http_method = http_method.to_sym 31 | @response = nil 32 | @request = nil 33 | uri.query = decode_query_params(query) 34 | end 35 | 36 | def update_query_params(new_value = {}) 37 | @uri.query = decode_query_params(new_value) 38 | rescue NoMethodError 39 | @uri.query 40 | end 41 | 42 | def call 43 | raise InvalidHTTPMethod unless valid_http_method? 44 | 45 | @response = Net::HTTP.start(uri.host, uri.port, use_ssl: use_ssl?) do |http| 46 | @request = create_request 47 | http.request(request) 48 | end 49 | 50 | RESPONSES[http_method].new(request, response) 51 | end 52 | alias execute call 53 | 54 | private 55 | 56 | def decode_query_params(query_params) 57 | CGI.unescape(URI.encode_www_form(query_params)) 58 | end 59 | 60 | def create_request 61 | request = METHODS[http_method].new(uri) 62 | request.body = body.to_json 63 | request.content_type = 'application/json' 64 | add_headers(request) 65 | 66 | request 67 | end 68 | 69 | def use_ssl? 70 | uri.scheme == 'https' 71 | end 72 | 73 | def add_headers(request) 74 | headers.each { |key, value| request[key] = value } 75 | request['User-Agent'] = USER_AGENT 76 | 77 | nil 78 | end 79 | 80 | def valid_http_method? 81 | METHODS.keys.include?(http_method) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/postgrest/responses/base_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Responses 5 | class BaseResponse 6 | attr_reader :request, :response 7 | 8 | def initialize(request, response) 9 | @request = request 10 | @response = response 11 | @data = data 12 | end 13 | 14 | def inspect 15 | "\#<#{self.class} #{request.method} #{response.message} data=#{@data}>" 16 | end 17 | 18 | def error 19 | !response.is_a?(Net::HTTPSuccess) 20 | end 21 | 22 | def count 23 | data.count 24 | end 25 | 26 | def status 27 | response.code.to_i 28 | end 29 | 30 | def status_text 31 | response.message 32 | end 33 | 34 | def data 35 | return [] if error 36 | 37 | body = response.body 38 | body = decompress(body) if compressed_body? 39 | 40 | safe_json_parse(body) 41 | end 42 | alias as_json data 43 | 44 | def params 45 | { 46 | query: request.uri.query, 47 | body: safe_json_parse(request.body) 48 | } 49 | end 50 | 51 | private 52 | 53 | def safe_json_parse(json) 54 | JSON.parse(json) 55 | rescue TypeError, JSON::ParserError 56 | {} 57 | end 58 | 59 | def decompress(body) 60 | gz = Zlib::GzipReader.new(StringIO.new(body)) 61 | gz.read 62 | end 63 | 64 | def compressed_body? 65 | # https://ruby-doc.org/3.2.1/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Compression 66 | response['content-encoding'] == 'gzip' && response['content-range'] 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/postgrest/responses/delete_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Responses 5 | class DeleteResponse < BaseResponse 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/postgrest/responses/get_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Responses 5 | class GetResponse < BaseResponse 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/postgrest/responses/patch_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Responses 5 | class PatchResponse < BaseResponse 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/postgrest/responses/post_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | module Responses 5 | class PostResponse < BaseResponse 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/postgrest/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Postgrest 4 | VERSION = '0.3.1' 5 | end 6 | -------------------------------------------------------------------------------- /postgrest.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/postgrest/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'postgrest' 7 | spec.version = Postgrest::VERSION 8 | spec.authors = ['Marcelo Barreto'] 9 | spec.email = ['marcelobarretojunior@gmail.com'] 10 | 11 | spec.summary = 'Ruby client for PostgREST' 12 | spec.description = 'Ruby client for PostgREST' 13 | spec.homepage = 'https://github.com/marcelobarreto/postgrest-rb' 14 | spec.license = 'MIT' 15 | spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0') 16 | 17 | spec.metadata['homepage_uri'] = spec.homepage 18 | spec.metadata['source_code_uri'] = 'https://github.com/marcelobarreto/postgrest-rb' 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 23 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 24 | end 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ['lib'] 28 | 29 | spec.add_development_dependency 'pry' 30 | spec.add_development_dependency 'rspec', '~> 3.5' 31 | spec.add_development_dependency 'rubocop', '~> 1.13.0' 32 | spec.add_development_dependency 'simplecov', '~> 0.21.2' 33 | end 34 | -------------------------------------------------------------------------------- /spec/postgrest/builders/base_builder_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Builders::BaseBuilder do 3 | describe 'call/execute' do 4 | let(:http_instance) { double(Postgrest::HTTP) } 5 | before { allow(http_instance).to receive(:call) } 6 | 7 | subject { described_class.new(http_instance) } 8 | 9 | it { expect(subject).to respond_to(:call) } 10 | it { expect(subject).to respond_to(:execute) } 11 | 12 | context 'when trigger' do 13 | it 'calls the #call method' do 14 | subject.call 15 | 16 | expect(http_instance).to have_received(:call) 17 | end 18 | 19 | it 'executes the #call method' do 20 | subject.execute 21 | 22 | expect(http_instance).to have_received(:call) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/postgrest/builders/filter_builder_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Builders::FilterBuilder do 3 | let(:uri) { URI('https://postgrest_server.com') } 4 | let(:http_instance) { Postgrest::HTTP.new(uri: uri, query: { select: '*' }) } 5 | subject { described_class.new(http_instance) } 6 | 7 | describe 'constants' do 8 | it { expect(described_class.const_defined?('SIMPLE_MATCHERS')).to eq(true) } 9 | it { expect(described_class.const_defined?('RANGE_MATCHERS')).to eq(true) } 10 | end 11 | 12 | describe 'before execute hooks defined' do 13 | it { expect(described_class.before_execute_hooks).to eq([:update_http_instance])} 14 | end 15 | 16 | describe 'attributes' do 17 | it 'is expected to assing inverse_next' do 18 | expect(subject.instance_variable_get(:@inverse_next)).to be_falsy 19 | end 20 | 21 | it 'is expected to assing http instance' do 22 | expect(subject.instance_variable_get(:@http)).to eq(http_instance) 23 | end 24 | end 25 | 26 | describe 'methods' do 27 | describe '#eq' do 28 | context 'when call not before' do 29 | it 'updates the query params' do 30 | expect(subject.not.eq(id: 1).decoded_query).to eq({ "id" => "not.eq.1", "select" => "*" }) 31 | end 32 | end 33 | 34 | context 'when call it directly' do 35 | it 'updates the query params' do 36 | expect(subject.eq(id: 1).decoded_query).to eq({ "id" => "eq.1", "select" => "*" }) 37 | end 38 | end 39 | end 40 | 41 | describe '#neq' do 42 | context 'when call not before' do 43 | it 'updates the query params' do 44 | expect(subject.not.neq(id: 1).decoded_query).to eq({ "id" => "not.neq.1", "select" => "*" }) 45 | end 46 | end 47 | 48 | context 'when call it directly' do 49 | it 'updates the query params' do 50 | expect(subject.neq(id: 1).decoded_query).to eq({ "id" => "neq.1", "select" => "*" }) 51 | end 52 | end 53 | end 54 | 55 | describe '#gt' do 56 | context 'when call not before' do 57 | it 'updates the query params' do 58 | expect(subject.not.gt(id: 1).decoded_query).to eq({ "id" => "not.gt.1", "select" => "*" }) 59 | end 60 | end 61 | 62 | context 'when call it directly' do 63 | it 'updates the query params' do 64 | expect(subject.gt(id: 1).decoded_query).to eq({ "id" => "gt.1", "select" => "*" }) 65 | end 66 | end 67 | end 68 | 69 | describe '#gte' do 70 | context 'when call not before' do 71 | it 'updates the query params' do 72 | expect(subject.not.gte(id: 1).decoded_query).to eq({ "id" => "not.gte.1", "select" => "*" }) 73 | end 74 | end 75 | 76 | context 'when call it directly' do 77 | it 'updates the query params' do 78 | expect(subject.gte(id: 1).decoded_query).to eq({ "id" => "gte.1", "select" => "*" }) 79 | end 80 | end 81 | end 82 | 83 | describe '#lt' do 84 | context 'when call not before' do 85 | it 'updates the query params' do 86 | expect(subject.not.lt(id: 1).decoded_query).to eq({ "id" => "not.lt.1", "select" => "*" }) 87 | end 88 | end 89 | 90 | context 'when call it directly' do 91 | it 'updates the query params' do 92 | expect(subject.lt(id: 1).decoded_query).to eq({ "id" => "lt.1", "select" => "*" }) 93 | end 94 | end 95 | end 96 | 97 | describe '#lte' do 98 | context 'when call not before' do 99 | it 'updates the query params' do 100 | expect(subject.not.lte(id: 1).decoded_query).to eq({ "id" => "not.lte.1", "select" => "*" }) 101 | end 102 | end 103 | 104 | context 'when call it directly' do 105 | it 'updates the query params' do 106 | expect(subject.lte(id: 1).decoded_query).to eq({ "id" => "lte.1", "select" => "*" }) 107 | end 108 | end 109 | end 110 | 111 | describe '#like' do 112 | context 'when call not before' do 113 | it 'updates the query params' do 114 | expect(subject.not.like(id: 1).decoded_query).to eq({ "id" => "not.like.1", "select" => "*" }) 115 | end 116 | end 117 | 118 | context 'when call it directly' do 119 | it 'updates the query params' do 120 | expect(subject.like(id: 1).decoded_query).to eq({ "id" => "like.1", "select" => "*" }) 121 | end 122 | end 123 | end 124 | 125 | describe '#ilike' do 126 | context 'when call not before' do 127 | it 'updates the query params' do 128 | expect(subject.not.ilike(id: 1).decoded_query).to eq({ "id" => "not.ilike.1", "select" => "*" }) 129 | end 130 | end 131 | 132 | context 'when call it directly' do 133 | it 'updates the query params' do 134 | expect(subject.ilike(id: 1).decoded_query).to eq({ "id" => "ilike.1", "select" => "*" }) 135 | end 136 | end 137 | end 138 | 139 | describe '#fts' do 140 | context 'when call not before' do 141 | it 'updates the query params' do 142 | expect(subject.not.fts(id: 1).decoded_query).to eq({ "id" => "not.fts.1", "select" => "*" }) 143 | end 144 | end 145 | 146 | context 'when call it directly' do 147 | it 'updates the query params' do 148 | expect(subject.fts(id: 1).decoded_query).to eq({ "id" => "fts.1", "select" => "*" }) 149 | end 150 | end 151 | end 152 | 153 | describe '#plfts' do 154 | context 'when call not before' do 155 | it 'updates the query params' do 156 | expect(subject.not.plfts(id: 1).decoded_query).to eq({ "id" => "not.plfts.1", "select" => "*" }) 157 | end 158 | end 159 | 160 | context 'when call it directly' do 161 | it 'updates the query params' do 162 | expect(subject.plfts(id: 1).decoded_query).to eq({ "id" => "plfts.1", "select" => "*" }) 163 | end 164 | end 165 | end 166 | 167 | describe '#phfts' do 168 | context 'when call not before' do 169 | it 'updates the query params' do 170 | expect(subject.not.phfts(id: 1).decoded_query).to eq({ "id" => "not.phfts.1", "select" => "*" }) 171 | end 172 | end 173 | 174 | context 'when call it directly' do 175 | it 'updates the query params' do 176 | expect(subject.phfts(id: 1).decoded_query).to eq({ "id" => "phfts.1", "select" => "*" }) 177 | end 178 | end 179 | end 180 | 181 | describe '#wfts' do 182 | context 'when call not before' do 183 | it 'updates the query params' do 184 | expect(subject.not.wfts(id: 1).decoded_query).to eq({ "id" => "not.wfts.1", "select" => "*" }) 185 | end 186 | end 187 | 188 | context 'when call it directly' do 189 | it 'updates the query params' do 190 | expect(subject.wfts(id: 1).decoded_query).to eq({ "id" => "wfts.1", "select" => "*" }) 191 | end 192 | end 193 | end 194 | 195 | describe '#in' do 196 | context 'when call not before' do 197 | it 'updates the query params' do 198 | expect(subject.not.in(id: [1, 2]).decoded_query).to eq({ "id" => "not.in.(1,2)", "select" => "*" }) 199 | end 200 | end 201 | 202 | context 'when call it directly' do 203 | it 'updates the query params' do 204 | expect(subject.in(id: [1, 2]).decoded_query).to eq({ "id" => "in.(1,2)", "select" => "*" }) 205 | end 206 | end 207 | end 208 | 209 | describe '#sl' do 210 | context 'when call not before' do 211 | it 'updates the query params' do 212 | expect(subject.not.sl(id: [1, 2]).decoded_query).to eq({ "id" => "range=not.sl.(1,2)", "select" => "*" }) 213 | end 214 | end 215 | 216 | context 'when call it directly' do 217 | it 'updates the query params' do 218 | expect(subject.sl(id: [1, 2]).decoded_query).to eq({ "id" => "range=sl.(1,2)", "select" => "*" }) 219 | end 220 | end 221 | end 222 | 223 | describe '#sr' do 224 | context 'when call not before' do 225 | it 'updates the query params' do 226 | expect(subject.not.sr(id: [1, 2]).decoded_query).to eq({ "id" => "range=not.sr.(1,2)", "select" => "*" }) 227 | end 228 | end 229 | 230 | context 'when call it directly' do 231 | it 'updates the query params' do 232 | expect(subject.sr(id: [1, 2]).decoded_query).to eq({ "id" => "range=sr.(1,2)", "select" => "*" }) 233 | end 234 | end 235 | end 236 | 237 | describe '#nxr' do 238 | context 'when call not before' do 239 | it 'updates the query params' do 240 | expect(subject.not.nxr(id: [1, 2]).decoded_query).to eq({ "id" => "range=not.nxr.(1,2)", "select" => "*" }) 241 | end 242 | end 243 | 244 | context 'when call it directly' do 245 | it 'updates the query params' do 246 | expect(subject.nxr(id: [1, 2]).decoded_query).to eq({ "id" => "range=nxr.(1,2)", "select" => "*" }) 247 | end 248 | end 249 | end 250 | 251 | describe '#nxl' do 252 | context 'when call not before' do 253 | it 'updates the query params' do 254 | expect(subject.not.nxl(id: [1, 2]).decoded_query).to eq({ "id" => "range=not.nxl.(1,2)", "select" => "*" }) 255 | end 256 | end 257 | 258 | context 'when call it directly' do 259 | it 'updates the query params' do 260 | expect(subject.nxl(id: [1, 2]).decoded_query).to eq({ "id" => "range=nxl.(1,2)", "select" => "*" }) 261 | end 262 | end 263 | end 264 | 265 | describe '#adj' do 266 | context 'when call not before' do 267 | it 'updates the query params' do 268 | expect(subject.not.adj(id: [1, 2]).decoded_query).to eq({ "id" => "range=not.adj.(1,2)", "select" => "*" }) 269 | end 270 | end 271 | 272 | context 'when call it directly' do 273 | it 'updates the query params' do 274 | expect(subject.adj(id: [1, 2]).decoded_query).to eq({ "id" => "range=adj.(1,2)", "select" => "*" }) 275 | end 276 | end 277 | end 278 | 279 | describe '#not' do 280 | context 'when previous command was not a #not' do 281 | it 'changes the @inverse_next instance variable' do 282 | subject.not 283 | expect(subject.instance_variable_get(:@inverse_next)).to eq(true) 284 | end 285 | end 286 | 287 | context 'when previous command was a #not' do 288 | it 'changes the @inverse_next instance variable' do 289 | subject.not.not 290 | expect(subject.instance_variable_get(:@inverse_next)).to eq(false) 291 | end 292 | end 293 | end 294 | 295 | describe '#order' do 296 | context 'when call not before' do 297 | it 'updates the query params' do 298 | expect(subject.not.order(name: :asc).decoded_query).to eq({ "order" => "name.desc", "select" => "*" }) 299 | end 300 | end 301 | 302 | context 'when call it directly' do 303 | it 'updates the query params' do 304 | expect(subject.order(name: :asc).decoded_query).to eq({ "order" => "name.asc", "select" => "*" }) 305 | end 306 | end 307 | end 308 | 309 | describe '#limit' do 310 | context 'when call it directly' do 311 | it 'it sets the limit instance variable' do 312 | subject.limit(10) 313 | expect(subject.decoded_query["limit"]).to eq(10) 314 | end 315 | end 316 | end 317 | 318 | describe '#offset' do 319 | context 'when call it directly' do 320 | it 'it sets the offset instance variable' do 321 | subject.offset(10) 322 | expect(subject.decoded_query["offset"]).to eq(10) 323 | end 324 | end 325 | end 326 | 327 | skip '#before_execute hooks' do; end 328 | 329 | describe 'scenarios' do 330 | context 'when combining limit + offset' do 331 | it 'sets both instance variables' do 332 | subject.limit(10).offset(10) 333 | expect(subject.decoded_query["limit"]).to eq(10) 334 | expect(subject.decoded_query["offset"]).to eq(10) 335 | end 336 | end 337 | 338 | context 'full query scenario' do 339 | it 'builds the query successfully' do 340 | subject.owners(:name, as: :owner) 341 | .workers(:name, as: :worker) 342 | .in(id: [112, 113]) 343 | .order(id: :asc) 344 | .limit(10) 345 | .offset(5) 346 | 347 | expect(subject.decoded_query).to eq({ 348 | "id" => "in.(112,113)", 349 | "limit" => 10, 350 | "offset" => 5, 351 | "order" => "id.asc", 352 | "select" => "*,owner:owners(name),worker:workers(name)" 353 | }) 354 | end 355 | end 356 | end 357 | 358 | describe '#query' do 359 | context 'delegates the method to the http instance' do 360 | it 'changes the @inverse_next instance variable' do 361 | expect(subject.query).to eq(http_instance.uri.query) 362 | end 363 | end 364 | end 365 | end 366 | end 367 | end 368 | -------------------------------------------------------------------------------- /spec/postgrest/builders/query_builder_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Builders::QueryBuilder do 3 | let(:url) { 'https://postgrest_server.com' } 4 | subject { described_class.new(url: url, headers: {}, schema: 'public') } 5 | 6 | describe 'attributes' do 7 | it { is_expected.to respond_to(:uri) } 8 | it { is_expected.to respond_to(:headers) } 9 | it { is_expected.to respond_to(:schema) } 10 | end 11 | 12 | describe '#select' do 13 | context 'when passing extra headers' do 14 | it 'merges the headers' do 15 | instance = subject.select(extra_headers: { foo: 123 }) 16 | expect(instance.http.headers).to eq({ foo: 123 }) 17 | end 18 | end 19 | 20 | context 'when not passing extra headers' do 21 | context 'when passing some custom values' do 22 | it 'returns an FilterBuilder instance' do 23 | instance = subject.select(:name, :age) 24 | expect(instance).to be_a(Builders::FilterBuilder) 25 | expect(instance.query).to eq('select=name,age') 26 | end 27 | end 28 | 29 | it 'returns an FilterBuilder instance' do 30 | instance = subject.select 31 | expect(instance).to be_a(Builders::FilterBuilder) 32 | expect(instance.query).to eq('select=*') 33 | end 34 | end 35 | end 36 | 37 | describe '#insert' do 38 | context 'common scenario' do 39 | let(:instance) { subject.insert({ name: 'John', age: 32 }) } 40 | 41 | it { expect(instance).to be_a(Builders::BaseBuilder) } 42 | it { expect(instance.http.body).to eq({ name: 'John', age: 32 }) } 43 | it { expect(instance.http.headers).to eq({ Prefer: 'return=representation' }) } 44 | end 45 | end 46 | 47 | describe '#upsert' do 48 | context 'common scenario' do 49 | let(:instance) { subject.upsert({ name: 'John', age: 32 }) } 50 | 51 | it { expect(instance).to be_a(Builders::BaseBuilder) } 52 | it { expect(instance.http.body).to eq({ name: 'John', age: 32 }) } 53 | it { expect(instance.http.headers).to eq({ Prefer: 'return=representation,resolution=merge-duplicates' }) } 54 | end 55 | end 56 | 57 | describe '#update' do 58 | context 'common scenario' do 59 | let(:instance) { subject.update({ name: 'John', age: 32 }) } 60 | 61 | it { expect(instance).to be_a(Builders::FilterBuilder) } 62 | it { expect(instance.http.body).to eq({ name: 'John', age: 32 }) } 63 | it { expect(instance.http.headers).to eq({ Prefer: 'return=representation' }) } 64 | end 65 | end 66 | 67 | describe '#delete' do 68 | context 'with custom headers' do 69 | describe 'sets the headers' do 70 | let(:instance) { subject.delete(extra_headers: { foo: 123 }) } 71 | 72 | it { expect(instance).to be_a(Builders::FilterBuilder) } 73 | it { expect(instance.http.headers).to eq({ Prefer: 'return=representation', foo: 123 }) } 74 | end 75 | end 76 | 77 | context 'common scenario' do 78 | let(:instance) { subject.delete } 79 | 80 | it { expect(instance).to be_a(Builders::FilterBuilder) } 81 | it { expect(instance.http.headers).to eq({ Prefer: 'return=representation' }) } 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/postgrest/client_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Client do 3 | describe 'constants' do 4 | it { expect(described_class.const_defined?('DEFAULT_SCHEMA')).to eq(true) } 5 | end 6 | 7 | describe 'attributes' do 8 | subject { described_class.new(url: '') } 9 | 10 | it { is_expected.to respond_to(:url) } 11 | it { is_expected.to respond_to(:headers) } 12 | it { is_expected.to respond_to(:schema) } 13 | end 14 | 15 | describe '#from' do 16 | subject { described_class.new(url: '') } 17 | 18 | context 'when no table provided' do 19 | it 'raises MissingTableError' do 20 | expect { subject.from(nil) }.to raise_error(Postgrest::MissingTableError) 21 | end 22 | end 23 | 24 | context 'when table is an empty string' do 25 | it 'raises MissingTableError' do 26 | expect { subject.from('') }.to raise_error(Postgrest::MissingTableError) 27 | end 28 | end 29 | 30 | context 'when table was provided' do 31 | it 'returns a Builders::QueryBuilder instance' do 32 | expect(subject.from('todos')).to be_a(Postgrest::Builders::QueryBuilder) 33 | end 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /spec/postgrest/http_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe HTTP do 3 | let(:uri) { URI('http://google.com') } 4 | 5 | describe 'constants' do 6 | it { expect(described_class.const_defined?('USER_AGENT')).to eq(true) } 7 | it { expect(described_class.const_defined?('METHODS')).to eq(true) } 8 | it { expect(described_class.const_defined?('RESPONSES')).to eq(true) } 9 | end 10 | 11 | describe 'attributes' do 12 | subject { described_class.new(uri: uri)} 13 | 14 | it { expect(subject).to respond_to(:uri)} 15 | it { expect(subject).to respond_to(:query)} 16 | it { expect(subject).to respond_to(:body)} 17 | it { expect(subject).to respond_to(:headers)} 18 | it { expect(subject).to respond_to(:http_method)} 19 | it { expect(subject).to respond_to(:response)} 20 | it { expect(subject).to respond_to(:request)} 21 | end 22 | 23 | describe '#create_request' do 24 | subject { described_class.new(uri: uri, http_method: http_method, headers: { foo: 123 })} 25 | 26 | context 'when request is a GET' do 27 | let(:http_method) { :get } 28 | let(:result) { subject.send(:create_request) } 29 | 30 | it { expect(result).to be_a(Net::HTTP::Get) } 31 | it { expect(result.uri).to eq(uri) } 32 | it { expect(result['foo']).to eq('123') } 33 | end 34 | 35 | context 'when request is a POST' do 36 | let(:http_method) { :post } 37 | let(:result) { subject.send(:create_request) } 38 | 39 | it { expect(result).to be_a(Net::HTTP::Post) } 40 | it { expect(result.uri).to eq(uri) } 41 | it { expect(result['foo']).to eq('123') } 42 | end 43 | 44 | context 'when request is a PUT' do 45 | let(:http_method) { :patch } 46 | let(:result) { subject.send(:create_request) } 47 | 48 | it { expect(result).to be_a(Net::HTTP::Patch) } 49 | it { expect(result.uri).to eq(uri) } 50 | it { expect(result['foo']).to eq('123') } 51 | end 52 | 53 | context 'when request is a DELETE' do 54 | let(:http_method) { :delete } 55 | let(:result) { subject.send(:create_request) } 56 | 57 | it { expect(result).to be_a(Net::HTTP::Delete) } 58 | it { expect(result.uri).to eq(uri) } 59 | it { expect(result['foo']).to eq('123') } 60 | end 61 | end 62 | 63 | describe '#update_query_params' do 64 | subject { described_class.new(uri: uri, query: { foo: 123 })} 65 | 66 | context 'when param is a Hash' do 67 | it "change the request's query params" do 68 | subject.update_query_params(foo: 321, bar: 321) 69 | 70 | expect(subject.uri.query).to eq('foo=321&bar=321') 71 | end 72 | end 73 | 74 | context 'when params is an Array' do 75 | it "change the request's query params" do 76 | subject.update_query_params([:foo, :bar]) 77 | 78 | expect(subject.uri.query).to eq('foo&bar') 79 | end 80 | end 81 | 82 | context 'when params is a String' do 83 | it "change the request's query params" do 84 | subject.update_query_params('foo') 85 | 86 | expect(subject.uri.query).to eq('foo=123') 87 | end 88 | end 89 | end 90 | 91 | xdescribe '#call' do 92 | end 93 | 94 | describe '#add_headers' do 95 | subject { described_class.new(uri: uri, http_method: :get, headers: { foo: 123 })} 96 | 97 | before do 98 | request = subject.instance_variable_set(:@request, described_class::METHODS[:get].new(uri)) 99 | subject.send(:add_headers, request) 100 | end 101 | 102 | it { expect(subject.request['foo']).to eq('123') } 103 | it { expect(subject.request['user-agent']).to eq(described_class::USER_AGENT) } 104 | end 105 | 106 | describe '#use_ssl?' do 107 | context 'when URI use HTTP' do 108 | subject { described_class.new(uri: uri, http_method: :get)} 109 | 110 | it { expect(subject.send(:use_ssl?)).to eq(false) } 111 | end 112 | 113 | context 'when URI use HTTPS' do 114 | let(:uri) { URI('https://google.com') } 115 | subject { described_class.new(uri: uri, http_method: :get)} 116 | 117 | it { expect(subject.send(:use_ssl?)).to eq(true) } 118 | end 119 | end 120 | 121 | describe '#valid_http_method?' do 122 | context 'when method is valid' do 123 | subject { described_class.new(uri: uri, http_method: :get)} 124 | 125 | it { expect(subject.send(:valid_http_method?)).to eq(true) } 126 | end 127 | 128 | context 'when method is invalid' do 129 | subject { described_class.new(uri: uri, http_method: :foo)} 130 | 131 | it { expect(subject.send(:valid_http_method?)).to eq(false) } 132 | end 133 | end 134 | end 135 | end -------------------------------------------------------------------------------- /spec/postgrest/responses/base_response_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Responses::BaseResponse do 3 | let(:uri) { URI('https://google.com?eq=123') } 4 | let(:request) { Net::HTTP::Get.new(uri) } 5 | let(:response_klass) { Net::HTTPSuccess } 6 | let(:response) { double(response_klass) } 7 | let(:response_body) { { success: true }.to_json } 8 | 9 | subject { described_class.new(request, response) } 10 | 11 | before do 12 | allow(response).to receive(:is_a?).and_return(true) 13 | allow(response).to receive(:body).and_return(response_body) 14 | allow(response).to receive(:[]).and_return({}) 15 | end 16 | 17 | describe '#error' do 18 | context 'when response was a failure' do 19 | let(:response_klass) { Net::HTTPUnprocessableEntity } 20 | before { allow(response).to receive(:is_a?).and_return(false) } 21 | 22 | it { expect(subject.error).to eq(true) } 23 | end 24 | 25 | context 'when response was success' do 26 | subject { described_class.new(request, response) } 27 | 28 | it { expect(subject.error).to eq(false) } 29 | end 30 | end 31 | 32 | describe '#count' do 33 | context 'when response was success' do 34 | subject { described_class.new(request, response) } 35 | let(:response_body) { [{ a: 1 }, { a: 2 }].to_json } 36 | 37 | it { expect(subject.count).to eq(2) } 38 | end 39 | end 40 | 41 | describe '#status' do 42 | context 'when response was success' do 43 | subject { described_class.new(request, response) } 44 | before { allow(response).to receive(:code).and_return('200') } 45 | 46 | it { expect(subject.status).to eq(200) } 47 | end 48 | 49 | context 'when response was a failure' do 50 | before { allow(response).to receive(:code).and_return('422') } 51 | 52 | it { expect(subject.status).to eq(422) } 53 | end 54 | end 55 | 56 | describe '#status_text' do 57 | context 'when response was success' do 58 | subject { described_class.new(request, response) } 59 | before { allow(response).to receive(:message).and_return('OK') } 60 | 61 | it { expect(subject.status_text).to eq('OK') } 62 | end 63 | 64 | context 'when response was a failure' do 65 | before { allow(response).to receive(:message).and_return('Bad Request') } 66 | 67 | it { expect(subject.status_text).to eq('Bad Request') } 68 | end 69 | end 70 | 71 | describe '#data' do 72 | context 'when response was success' do 73 | subject { described_class.new(request, response) } 74 | let(:response_body) { { success: true, foo: :bar }.to_json } 75 | it { expect(subject.data).to eq({ 'success' => true, 'foo' => 'bar' }) } 76 | end 77 | 78 | context 'when response was a failure' do 79 | let(:response_body) { '' } 80 | subject { described_class.new(request, response) } 81 | 82 | it { expect(subject.data).to eq({}) } 83 | end 84 | 85 | context 'when response body was compressed' do 86 | subject { described_class.new(request, response) } 87 | 88 | let(:response_body) do 89 | data = { success: true, foo: :bar }.to_json 90 | gz = Zlib::GzipWriter.new(StringIO.new) 91 | gz << data 92 | gz.close.string 93 | end 94 | 95 | before do 96 | allow(response).to receive(:[]).with('content-encoding').and_return('gzip') 97 | allow(response).to receive(:[]).with('content-range').and_return('0-999/*') 98 | end 99 | 100 | it 'decompresses the body' do 101 | expect(subject.data).to eq({ 'success' => true, 'foo' => 'bar' }) 102 | end 103 | end 104 | end 105 | 106 | describe '#params' do 107 | context 'when request was success' do 108 | subject { described_class.new(request, response) } 109 | 110 | it { expect(subject.params).to eq({ body: {}, query: 'eq=123' }) } 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/postgrest/responses/delete_response_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Responses::DeleteResponse do 3 | end 4 | end -------------------------------------------------------------------------------- /spec/postgrest/responses/get_response_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Responses::GetResponse do 3 | end 4 | end -------------------------------------------------------------------------------- /spec/postgrest/responses/patch_response_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Responses::PatchResponse do 3 | end 4 | end -------------------------------------------------------------------------------- /spec/postgrest/responses/post_response_spec.rb: -------------------------------------------------------------------------------- 1 | module Postgrest 2 | RSpec.describe Responses::PostResponse do 3 | end 4 | end -------------------------------------------------------------------------------- /spec/postgrest_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Postgrest do 2 | it 'has a version number' do 3 | expect(Postgrest::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'postgrest' 3 | require 'pry' 4 | require 'simplecov' 5 | SimpleCov.start 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = ".rspec_status" 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | --------------------------------------------------------------------------------