├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .solargraph.yml ├── .travis.yml ├── .yardopts ├── Changes.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── _config.yml ├── bin ├── console ├── parse-console ├── server └── setup ├── lib ├── parse-stack.rb └── parse │ ├── api │ ├── aggregate.rb │ ├── all.rb │ ├── analytics.rb │ ├── batch.rb │ ├── cloud_functions.rb │ ├── config.rb │ ├── files.rb │ ├── hooks.rb │ ├── objects.rb │ ├── push.rb │ ├── schema.rb │ ├── server.rb │ ├── sessions.rb │ └── users.rb │ ├── client.rb │ ├── client │ ├── authentication.rb │ ├── batch.rb │ ├── body_builder.rb │ ├── caching.rb │ ├── protocol.rb │ ├── request.rb │ └── response.rb │ ├── model │ ├── acl.rb │ ├── associations │ │ ├── belongs_to.rb │ │ ├── collection_proxy.rb │ │ ├── has_many.rb │ │ ├── has_one.rb │ │ ├── pointer_collection_proxy.rb │ │ └── relation_collection_proxy.rb │ ├── bytes.rb │ ├── classes │ │ ├── installation.rb │ │ ├── product.rb │ │ ├── role.rb │ │ ├── session.rb │ │ └── user.rb │ ├── core │ │ ├── actions.rb │ │ ├── builder.rb │ │ ├── errors.rb │ │ ├── fetching.rb │ │ ├── properties.rb │ │ ├── querying.rb │ │ └── schema.rb │ ├── date.rb │ ├── file.rb │ ├── geopoint.rb │ ├── model.rb │ ├── object.rb │ ├── pointer.rb │ ├── push.rb │ ├── shortnames.rb │ └── time_zone.rb │ ├── query.rb │ ├── query │ ├── constraint.rb │ ├── constraints.rb │ ├── operation.rb │ └── ordering.rb │ ├── stack.rb │ ├── stack │ ├── generators │ │ ├── rails.rb │ │ └── templates │ │ │ ├── model.erb │ │ │ ├── model_installation.rb │ │ │ ├── model_role.rb │ │ │ ├── model_session.rb │ │ │ ├── model_user.rb │ │ │ ├── parse.rb │ │ │ └── webhooks.rb │ ├── railtie.rb │ ├── tasks.rb │ └── version.rb │ ├── webhooks.rb │ └── webhooks │ ├── payload.rb │ └── registration.rb ├── parse-stack.gemspec ├── parse-stack.png └── test ├── lib └── parse │ ├── cache_test.rb │ ├── models │ ├── acl_permissions_test.rb │ ├── acl_test.rb │ ├── collection_proxy_test.rb │ ├── geopoint_test.rb │ ├── installation_test.rb │ ├── pointer_test.rb │ ├── product_test.rb │ ├── property_test.rb │ ├── role_test.rb │ ├── session_test.rb │ ├── subclass_test.rb │ ├── timezone_test.rb │ └── user_test.rb │ ├── query │ ├── basic_test.rb │ ├── constraints │ │ ├── base_test.rb │ │ ├── contained_in_test.rb │ │ ├── contains_all_test.rb │ │ ├── equality_test.rb │ │ ├── exists_test.rb │ │ ├── geobox_test.rb │ │ ├── greater_than_or_equal_test.rb │ │ ├── greater_than_test.rb │ │ ├── id_test.rb │ │ ├── in_query_test.rb │ │ ├── inequality_test.rb │ │ ├── less_than_or_equal_test.rb │ │ ├── less_than_test.rb │ │ ├── near_sphere_test.rb │ │ ├── not_contained_in_test.rb │ │ ├── not_in_query_test.rb │ │ ├── nullability_test.rb │ │ ├── polygon_test.rb │ │ └── text_search_test.rb │ ├── core_query_test.rb │ └── expression_test.rb │ └── version_test.rb └── test_helper.rb /.github/workflows/ruby.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: Gem Tests 9 | 10 | on: 11 | push: 12 | branches: [master] 13 | pull_request: 14 | branches: [master] 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, macos-latest] 22 | ruby: [2.5, 2.6, 2.7] 23 | runs-on: ${{ matrix.os }} 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@ec106b438a1ff6ff109590de34ddc62c540232e0 31 | with: 32 | ruby-version: ${{ matrix.ruby }} 33 | - name: Install dependencies 34 | run: bundle install 35 | - name: Run tests 36 | run: bundle exec rake 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /spec/examples.txt 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | 14 | ## Specific to RubyMotion: 15 | .dat* 16 | .repl_history 17 | build/ 18 | 19 | ## Documentation cache and generated files: 20 | /.yardoc/ 21 | /_yardoc/ 22 | /doc/ 23 | /rdoc/ 24 | 25 | ## Environment normalization: 26 | /.bundle/ 27 | /vendor/bundle 28 | /lib/bundler/man/ 29 | 30 | # for a library or gem, you might want to ignore these files since the code is 31 | # intended to run in multiple environments; otherwise, check them in: 32 | # Gemfile.lock 33 | # .ruby-version 34 | # .ruby-gemset 35 | 36 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 37 | .rvmrc 38 | bin/.env 39 | bin/config.json 40 | .byebug_history 41 | /.env 42 | /node_modules 43 | /bin/parse-dashboard-config.json 44 | logs 45 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - "**/*.rb" 4 | exclude: 5 | - spec/**/* 6 | - test/**/* 7 | - vendor/**/* 8 | - ".bundle/**/*" 9 | domains: [] 10 | reporters: 11 | - rubocop 12 | - require_not_found 13 | require_paths: [] 14 | max_files: 5000 15 | require: 16 | - activemodel 17 | - faraday 18 | - faraday_middleware 19 | - moneta 20 | - activesupport 21 | - rack 22 | - active_model_serializers 23 | - parallel 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.6 4 | - 2.7 5 | - 3.1.2 6 | before_install: 7 | - yes | gem update --system --force 8 | - gem install bundler 9 | - bundle update --bundler 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in parse-stack.gemspec 4 | gemspec 5 | 6 | group :test, :development do 7 | gem "dotenv" 8 | gem "redis" 9 | gem "rake" 10 | gem "byebug" 11 | gem "minitest" 12 | gem 'minitest-reporters' 13 | gem "pry" 14 | gem "pry-stack_explorer" 15 | gem "pry-nav" 16 | gem "yard", ">= 0.9.11" 17 | gem "redcarpet" 18 | gem "rufo" 19 | gem "thin" # for yard server 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | parse-stack (1.9.1) 5 | active_model_serializers (>= 0.9, < 1) 6 | activemodel (>= 5, < 7) 7 | activesupport (>= 5, < 7) 8 | faraday (< 1) 9 | faraday_middleware (>= 0.9, < 2) 10 | moneta (< 2) 11 | parallel (>= 1.6, < 2) 12 | rack (>= 2.0.6, < 3) 13 | 14 | GEM 15 | remote: https://rubygems.org/ 16 | specs: 17 | actionpack (6.1.7.1) 18 | actionview (= 6.1.7.1) 19 | activesupport (= 6.1.7.1) 20 | rack (~> 2.0, >= 2.0.9) 21 | rack-test (>= 0.6.3) 22 | rails-dom-testing (~> 2.0) 23 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 24 | actionview (6.1.7.1) 25 | activesupport (= 6.1.7.1) 26 | builder (~> 3.1) 27 | erubi (~> 1.4) 28 | rails-dom-testing (~> 2.0) 29 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 30 | active_model_serializers (0.10.13) 31 | actionpack (>= 4.1, < 7.1) 32 | activemodel (>= 4.1, < 7.1) 33 | case_transform (>= 0.2) 34 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3) 35 | activemodel (6.1.7.1) 36 | activesupport (= 6.1.7.1) 37 | activesupport (6.1.7.1) 38 | concurrent-ruby (~> 1.0, >= 1.0.2) 39 | i18n (>= 1.6, < 2) 40 | minitest (>= 5.1) 41 | tzinfo (~> 2.0) 42 | zeitwerk (~> 2.3) 43 | ansi (1.5.0) 44 | binding_of_caller (1.0.0) 45 | debug_inspector (>= 0.0.1) 46 | builder (3.2.4) 47 | byebug (11.1.3) 48 | case_transform (0.2) 49 | activesupport 50 | coderay (1.1.3) 51 | concurrent-ruby (1.1.10) 52 | connection_pool (2.3.0) 53 | crass (1.0.6) 54 | daemons (1.4.1) 55 | debug_inspector (1.1.0) 56 | dotenv (2.8.1) 57 | erubi (1.12.0) 58 | eventmachine (1.2.7) 59 | faraday (0.17.6) 60 | multipart-post (>= 1.2, < 3) 61 | faraday_middleware (0.14.0) 62 | faraday (>= 0.7.4, < 1.0) 63 | i18n (1.12.0) 64 | concurrent-ruby (~> 1.0) 65 | jsonapi-renderer (0.2.2) 66 | loofah (2.19.1) 67 | crass (~> 1.0.2) 68 | nokogiri (>= 1.5.9) 69 | method_source (1.0.0) 70 | mini_portile2 (2.8.1) 71 | minitest (5.17.0) 72 | minitest-reporters (1.5.0) 73 | ansi 74 | builder 75 | minitest (>= 5.0) 76 | ruby-progressbar 77 | moneta (1.5.2) 78 | multipart-post (2.2.3) 79 | nokogiri (1.13.10) 80 | mini_portile2 (~> 2.8.0) 81 | racc (~> 1.4) 82 | parallel (1.22.1) 83 | pry (0.14.2) 84 | coderay (~> 1.1) 85 | method_source (~> 1.0) 86 | pry-nav (1.0.0) 87 | pry (>= 0.9.10, < 0.15) 88 | pry-stack_explorer (0.6.1) 89 | binding_of_caller (~> 1.0) 90 | pry (~> 0.13) 91 | racc (1.6.2) 92 | rack (2.2.6.2) 93 | rack-test (2.0.2) 94 | rack (>= 1.3) 95 | rails-dom-testing (2.0.3) 96 | activesupport (>= 4.2.0) 97 | nokogiri (>= 1.6) 98 | rails-html-sanitizer (1.5.0) 99 | loofah (~> 2.19, >= 2.19.1) 100 | rake (13.0.6) 101 | redcarpet (3.5.1) 102 | redis (5.0.6) 103 | redis-client (>= 0.9.0) 104 | redis-client (0.12.1) 105 | connection_pool 106 | ruby-progressbar (1.11.0) 107 | rufo (0.13.0) 108 | thin (1.8.1) 109 | daemons (~> 1.0, >= 1.0.9) 110 | eventmachine (~> 1.0, >= 1.0.4) 111 | rack (>= 1, < 3) 112 | tzinfo (2.0.5) 113 | concurrent-ruby (~> 1.0) 114 | webrick (1.7.0) 115 | yard (0.9.28) 116 | webrick (~> 1.7.0) 117 | zeitwerk (2.6.6) 118 | 119 | PLATFORMS 120 | ruby 121 | 122 | DEPENDENCIES 123 | byebug 124 | dotenv 125 | minitest 126 | minitest-reporters 127 | parse-stack! 128 | pry 129 | pry-nav 130 | pry-stack_explorer 131 | rake 132 | redcarpet 133 | redis 134 | rufo 135 | thin 136 | yard (>= 0.9.11) 137 | 138 | BUNDLED WITH 139 | 2.3.19 140 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Anthony Persaud, Modernistik LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "yard" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new do |t| 7 | t.libs << "lib/parse/stack" 8 | t.test_files = FileList["test/lib/**/*_test.rb"] 9 | t.warning = false 10 | t.verbose = true 11 | end 12 | 13 | task :default => :test 14 | 15 | task :console do 16 | exec "./bin/console" 17 | end 18 | task :c => :console 19 | 20 | desc "List undocumented methods" 21 | task "yard:stats" do 22 | exec "yard stats --list-undoc" 23 | end 24 | 25 | desc "Start the yard server" 26 | task "docs" do 27 | exec "rm -rf ./yard && yard server --reload" 28 | end 29 | 30 | YARD::Rake::YardocTask.new do |t| 31 | t.files = ["lib/**/*.rb"] # optional 32 | t.options = ["-o", "doc/parse-stack"] # optional 33 | t.stats_options = ["--list-undoc"] # optional 34 | end 35 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require 'byebug' 5 | require 'dotenv' 6 | require "parse/stack" 7 | 8 | Dotenv.load 9 | 10 | def setup 11 | Parse.setup # cache: 'redis://localhost:6379' 12 | 13 | puts "[ParseServerURL] #{Parse.client.server_url}" 14 | puts "[ParseAppID] #{Parse.client.app_id}" 15 | 16 | if Parse.client.master_key.present? 17 | Parse.auto_generate_models!.each do |model| 18 | puts "Generated #{model}" 19 | end 20 | end 21 | 22 | end 23 | puts "Type 'setup' to connect to Parse-Server" 24 | 25 | # Create shortnames 26 | Parse.use_shortnames! 27 | 28 | # You can add fixtures and/or initialization code here to make experimenting 29 | # with your gem easier. You can also use a different console, if you like. 30 | 31 | # (If you use this, don't forget to add pry to your Gemfile!) 32 | require "pry" 33 | Pry.start 34 | 35 | # 36 | # require "irb" 37 | # IRB.start 38 | #Rack::Server.start :app => HelloWorldApp 39 | -------------------------------------------------------------------------------- /bin/parse-console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'json' 5 | require 'open-uri' 6 | require 'active_support' 7 | require 'active_support/core_ext' 8 | 9 | DEFAULT_CONFIG_FILE = 'config.json' 10 | DEFAULT_CONFIG_CONTENTS = { 11 | "apps": [{ 12 | "serverURL": "http://localhost:1337/parse", 13 | "appId": "myAppId", 14 | "masterKey": "myMasterKey", 15 | "restAPIKey": "myRestAPIKey", 16 | "javascriptKey": "myJavascriptKey", 17 | "appName": "Parse Server App" 18 | }] 19 | }.freeze 20 | 21 | opts = { verbose: false, pry: false } 22 | opt_parser = OptionParser.new do |o| 23 | 24 | o.banner = "Usage: #{File.basename($0)} [options] SERVER_URL" 25 | o.separator "" 26 | o.separator "Example:" 27 | o.separator "#{File.basename($0)} -a APP_ID -m MASTER_KEY https://your-parse-server-url" 28 | o.separator "#{File.basename($0)} -c ./config.json" 29 | o.separator "" 30 | o.separator "If you use the -m option, parse-console will automatically" 31 | o.separator "import your schema as ruby models." 32 | o.separator "" 33 | o.separator "Options" 34 | o.on('-a APP_ID', '--appId APP_ID', 'Parse App ID (required)') { |a| opts[:app_id] = a } 35 | o.on('-k REST_API_KEY', '--restAPIKey REST_API_KEY', 'Parse REST API Key') { |a| opts[:api_key] = a } 36 | o.on('-m MASTER_KEY', '--masterKey MASTER_KEY', 'Parse Master Key') { |a| opts[:master_key] = a } 37 | o.on('-s SERVER_URL', '--serverURL SERVER_URL', 'The Parse server url.', 'Defaults to http://localhost:1337/parse') { |a| opts[:server_url] = a } 38 | o.on('-v','--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] ||= v } 39 | o.on('--pry', 'Use Pry instead of IRB') { |v| opts[:pry] = v } 40 | o.on('--version', 'Parse Stack version') do |v| 41 | require 'parse/stack/version' 42 | puts "Parse Stack : #{Parse::Stack::VERSION}" 43 | exit 1 44 | end 45 | o.on('--config-sample', "Create a sample config file to use. See --config.") do 46 | contents = JSON.pretty_generate(DEFAULT_CONFIG_CONTENTS) 47 | File.open(DEFAULT_CONFIG_FILE, 'w') { |f| f.write(contents) } 48 | puts "Created #{DEFAULT_CONFIG_FILE} : \n" 49 | puts contents 50 | puts "Edit #{DEFAULT_CONFIG_FILE} with your information and run: \n" 51 | puts "\n\tparse-console -c ./#{DEFAULT_CONFIG_FILE}\n\n" 52 | exit 1 53 | end 54 | o.on('-c','--config CONFIG_JSON', "Load config from a parse-dashboard.json compatible file.") do |filepath| 55 | unless File.exists?(filepath) 56 | $stderr.puts "File #{filepath} does not exist." 57 | exit 1 58 | end 59 | 60 | begin 61 | puts "Loading config: #{filepath}" 62 | file = File.read(filepath) 63 | config = JSON.parse file 64 | app = config["apps"].is_a?(Array) ? config["apps"].first : config["apps"] 65 | app = config if app.nil? # uses parse-server config.json 66 | opts[:server_url] ||= app["serverURL"] 67 | opts[:app_id] ||= app["appId"] 68 | opts[:api_key] ||= app["restAPIKey"] 69 | opts[:master_key] ||= app["masterKey"] 70 | rescue Exception => e 71 | $stderr.puts "Error: Incompatible JSON format for #{filepath} (#{e})" 72 | exit 1 73 | end 74 | 75 | end 76 | o.on('--url URL', 'Load the env config from a url.') do |url| 77 | begin 78 | puts "Loading config: #{url}" 79 | json = JSON.load open(url) 80 | raise "Contents not a JSON hash." unless json.is_a?(Hash) 81 | json.each { |k,v| ENV[k.upcase] = v } 82 | opts[:server_url] ||= ENV['PARSE_SERVER_URL'] 83 | opts[:app_id] ||= ENV['PARSE_SERVER_APPLICATION_ID'] || ENV['PARSE_APP_ID'] 84 | opts[:api_key] ||= ENV['PARSE_SERVER_REST_API_KEY'] || ENV['PARSE_API_KEY'] 85 | opts[:master_key] ||= ENV['PARSE_SERVER_MASTER_KEY'] || ENV['PARSE_MASTER_KEY'] 86 | rescue Exception => e 87 | $stderr.puts "Error: Invalid JSON format for #{url} (#{e})" 88 | exit 1 89 | end 90 | end 91 | end 92 | opt_parser.parse! 93 | 94 | opts[:server_url] ||= ARGV.shift || 'http://localhost:1337/parse' 95 | 96 | if opts[:app_id].nil? 97 | $stderr.puts "Error: Option --app_id missing\n" 98 | $stderr.puts opt_parser 99 | exit 1 100 | end 101 | 102 | if opts[:api_key].nil? && opts[:master_key].nil? 103 | $stderr.puts "Error: You must supply either --api_key (REST API Key) or --master_key (Parse Master key).\n" 104 | $stderr.puts opt_parser 105 | exit 1 106 | end 107 | 108 | # lazy loading 109 | require "parse/stack" 110 | Parse.setup server_url: opts[:server_url], 111 | app_id: opts[:app_id], 112 | api_key: opts[:api_key], 113 | master_key: opts[:master_key] 114 | Parse.logging = true if opts[:verbose] 115 | puts "Server : #{Parse.client.server_url}" 116 | puts "App Id : #{Parse.client.app_id}" 117 | puts "Master : #{Parse.client.master_key.present?}" 118 | 119 | if Parse.client.master_key.present? 120 | puts "Schema : imported" 121 | Parse.auto_generate_models!.each do |model| 122 | puts "Generated #{model}" if opts[:verbose] 123 | end 124 | else 125 | puts "Schema : skipped (requires master key)" 126 | end 127 | # Create shortnames 128 | Parse.use_shortnames! 129 | 130 | if opts[:pry] 131 | require "pry" 132 | Pry.start 133 | else 134 | require "irb" 135 | IRB.start 136 | end 137 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "parse/stack" 5 | 6 | require "rack" 7 | require "rack/server" 8 | require "puma" 9 | Rack::Handler::WEBrick = Rack::Handler.get(:puma) 10 | Rack::Server.start :app => Parse::Webhooks 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/parse-stack.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | # Useful for some users that require 'parse-stack' manually 5 | require_relative "./parse/stack.rb" 6 | -------------------------------------------------------------------------------- /lib/parse/api/aggregate.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/core_ext" 6 | require_relative "./objects" 7 | 8 | module Parse 9 | module API 10 | # REST API methods for fetching CRUD operations on Parse objects. 11 | module Aggregate 12 | # The class prefix for fetching objects. 13 | # @!visibility private 14 | PATH_PREFIX = "aggregate/" 15 | 16 | # @!visibility private 17 | PREFIX_MAP = Parse::API::Objects::PREFIX_MAP 18 | 19 | # @!visibility private 20 | def self.included(base) 21 | base.extend(ClassMethods) 22 | end 23 | 24 | # Class methods to be applied to {Parse::Client} 25 | module ClassMethods 26 | # Get the aggregate API path for this class. 27 | # @param className [String] the name of the Parse collection. 28 | # @return [String] the API uri path 29 | def aggregate_uri_path(className) 30 | if className.is_a?(Parse::Pointer) 31 | id = className.id 32 | className = className.parse_class 33 | end 34 | "#{PATH_PREFIX}#{className}" 35 | end 36 | end 37 | 38 | # Get the API path for this class. 39 | # @param className [String] the name of the Parse collection. 40 | # @return [String] the API uri path 41 | def aggregate_uri_path(className) 42 | self.class.aggregate_uri_path(className) 43 | end 44 | 45 | # Aggregate a set of matching objects for a query. 46 | # @param className [String] the name of the Parse collection. 47 | # @param query [Hash] The set of query constraints. 48 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 49 | # @param headers [Hash] additional HTTP headers to send with the request. 50 | # @return [Parse::Response] 51 | # @see Parse::Query 52 | def aggregate_objects(className, query = {}, headers: {}, **opts) 53 | response = request :get, aggregate_uri_path(className), query: query, headers: headers, opts: opts 54 | response.parse_class = className if response.present? 55 | response 56 | end 57 | end #Aggregate 58 | end #API 59 | end 60 | -------------------------------------------------------------------------------- /lib/parse/api/all.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "../client" 5 | require_relative "analytics" 6 | require_relative "aggregate" 7 | require_relative "batch" 8 | require_relative "config" 9 | require_relative "files" 10 | require_relative "cloud_functions" 11 | require_relative "hooks" 12 | require_relative "objects" 13 | require_relative "push" 14 | require_relative "schema" 15 | require_relative "server" 16 | require_relative "sessions" 17 | require_relative "users" 18 | 19 | module Parse 20 | # The module containing most of the REST API requests supported by Parse Server. 21 | # Defines all the Parse REST API endpoints. 22 | module API 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/parse/api/analytics.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Analytics interface for the Parse REST API 7 | module Analytics 8 | 9 | # Send analytics data. 10 | # @param event_name [String] the name of the event. 11 | # @param metrics [Hash] the metrics to attach to event. 12 | # @see http://docs.parseplatform.org/rest/guide/#analytics Parse Analytics 13 | def send_analytics(event_name, metrics = {}) 14 | request :post, "events/#{event_name}", body: metrics 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/parse/api/batch.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "parallel" 5 | require "active_support" 6 | require "active_support/core_ext" 7 | 8 | module Parse 9 | module API 10 | # Defines the Batch interface for the Parse REST API 11 | # @see Parse::BatchOperation 12 | # @see Array.destroy 13 | # @see Array.save 14 | module Batch 15 | # @note You cannot use batch_requests with {Parse::User} instances that need to 16 | # be created. 17 | # @overload batch_request(requests) 18 | # Perform a set of {Parse::Request} instances as a batch operation. 19 | # @param requests [Array] the set of requests to batch. 20 | # @overload batch_request(operation) 21 | # Submit a batch operation. 22 | # @param operation [Parse::BatchOperation] the batch operation. 23 | # @return [Array] if successful, a set of responses for each operation in the batch. 24 | # @return [Parse::Response] if an error occurred, the error response. 25 | def batch_request(batch_operations) 26 | unless batch_operations.is_a?(Parse::BatchOperation) 27 | batch_operations = Parse::BatchOperation.new batch_operations 28 | end 29 | response = request(:post, "batch", body: batch_operations.as_json) 30 | response.success? && response.batch? ? response.batch_responses : response 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/parse/api/cloud_functions.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the CloudCode interface for the Parse REST API 7 | module CloudFunctions 8 | 9 | # Call a cloud function. 10 | # @param name [String] the name of the cloud function. 11 | # @param body [Hash] the parameters to forward to the function. 12 | # @return [Parse::Response] 13 | def call_function(name, body = {}) 14 | request :post, "functions/#{name}", body: body 15 | end 16 | 17 | # Trigger a job. 18 | # @param name [String] the name of the job to trigger. 19 | # @param body [Hash] the parameters to forward to the job. 20 | # @return [Parse::Response] 21 | def trigger_job(name, body = {}) 22 | request :post, "jobs/#{name}", body: body 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/parse/api/config.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Config interface for the Parse REST API 7 | module Config 8 | 9 | # @!attribute config 10 | # @return [Hash] the cached config hash for the client. 11 | attr_accessor :config 12 | 13 | # @!visibility private 14 | CONFIG_PATH = "config" 15 | 16 | # @return [Hash] force fetch the application configuration hash. 17 | def config! 18 | @config = nil 19 | self.config 20 | end 21 | 22 | # Return the configuration hash for the configured application for this client. 23 | # This method caches the configuration after the first time it is fetched. 24 | # @return [Hash] force fetch the application configuration hash. 25 | def config 26 | if @config.nil? 27 | response = request :get, CONFIG_PATH 28 | unless response.error? 29 | @config = response.result["params"] 30 | end 31 | end 32 | @config 33 | end 34 | 35 | # Update the application configuration 36 | # @param params [Hash] the hash of key value pairs. 37 | # @return [Boolean] true if the configuration was successfully updated. 38 | def update_config(params) 39 | body = { params: params } 40 | response = request :put, CONFIG_PATH, body: body 41 | return false if response.error? 42 | result = response.result["result"] 43 | @config.merge!(params) if result && @config.present? 44 | result 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/parse/api/files.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/core_ext" 6 | 7 | module Parse 8 | module API 9 | # Defines the Parse Files interface for the Parse REST API 10 | module Files 11 | # @!visibility private 12 | FILES_PATH = "files" 13 | 14 | # Upload and create a Parse file. 15 | # @param fileName [String] the basename of the file. 16 | # @param data [Hash] the data related to this file. 17 | # @param content_type [String] the mime-type of the file. 18 | # @return [Parse::Response] 19 | def create_file(fileName, data = {}, content_type = nil) 20 | headers = {} 21 | headers.merge!({ Parse::Protocol::CONTENT_TYPE => content_type.to_s }) if content_type.present? 22 | response = request :post, "#{FILES_PATH}/#{fileName}", body: data, headers: headers 23 | response.parse_class = Parse::Model::TYPE_FILE 24 | response 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/parse/api/hooks.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Parse webhooks interface for the Parse REST API 7 | module Hooks 8 | # @!visibility private 9 | HOOKS_PREFIX = "hooks/" 10 | # The allowed set of Parse triggers. 11 | TRIGGER_NAMES = [:afterDelete, :afterFind, :afterSave, :beforeDelete, :beforeFind, :beforeSave].freeze 12 | # @!visibility private 13 | TRIGGER_NAMES_LOCAL = [:after_delete, :after_find, :after_save, :before_delete, :before_find, :before_save].freeze 14 | # @!visibility private 15 | def _verify_trigger(triggerName) 16 | triggerName = triggerName.to_s.camelize(:lower).to_sym 17 | raise ArgumentError, "Invalid trigger name #{triggerName}" unless TRIGGER_NAMES.include?(triggerName) 18 | triggerName 19 | end 20 | 21 | # Fetch all defined cloud code functions. 22 | # @return [Parse::Response] 23 | def functions 24 | opts = { cache: false } 25 | request :get, "#{HOOKS_PREFIX}functions", opts: opts 26 | end 27 | 28 | # Fetch information about a specific registered cloud function. 29 | # @param functionName [String] the name of the cloud code function. 30 | # @return [Parse::Response] 31 | def fetch_function(functionName) 32 | request :get, "#{HOOKS_PREFIX}functions/#{functionName}" 33 | end 34 | 35 | # Register a cloud code webhook function pointing to a endpoint url. 36 | # @param functionName [String] the name of the cloud code function. 37 | # @param url [String] the url endpoint for this cloud code function. 38 | # @return [Parse::Response] 39 | def create_function(functionName, url) 40 | request :post, "#{HOOKS_PREFIX}functions", body: { functionName: functionName, url: url } 41 | end 42 | 43 | # Updated the endpoint url for a registered cloud code webhook function. 44 | # @param functionName [String] the name of the cloud code function. 45 | # @param url [String] the new url endpoint for this cloud code function. 46 | # @return [Parse::Response] 47 | def update_function(functionName, url) 48 | # If you add _method => "PUT" to the JSON body, 49 | # and send it as a POST request and parse will accept it as a PUT. 50 | request :put, "#{HOOKS_PREFIX}functions/#{functionName}", body: { url: url } 51 | end 52 | 53 | # Remove a registered cloud code webhook function. 54 | # @param functionName [String] the name of the cloud code function. 55 | # @return [Parse::Response] 56 | def delete_function(functionName) 57 | request :put, "#{HOOKS_PREFIX}functions/#{functionName}", body: { __op: "Delete" } 58 | end 59 | 60 | # Get the set of registered triggers. 61 | # @return [Parse::Response] 62 | def triggers 63 | opts = { cache: false } 64 | request :get, "#{HOOKS_PREFIX}triggers", opts: opts 65 | end 66 | 67 | # Fetch information about a registered webhook trigger. 68 | # @param triggerName [String] the name of the trigger. (ex. beforeSave, afterSave) 69 | # @param className [String] the name of the Parse collection for the trigger. 70 | # @return [Parse::Response] 71 | # @see TRIGGER_NAMES 72 | def fetch_trigger(triggerName, className) 73 | triggerName = _verify_trigger(triggerName) 74 | request :get, "#{HOOKS_PREFIX}triggers/#{className}/#{triggerName}" 75 | end 76 | 77 | # Register a new cloud code webhook trigger with an endpoint url. 78 | # @param triggerName [String] the name of the trigger. (ex. beforeSave, afterSave) 79 | # @param className [String] the name of the Parse collection for the trigger. 80 | # @param url [String] the url endpoint for this webhook trigger. 81 | # @return [Parse::Response] 82 | # @see Parse::API::Hooks::TRIGGER_NAMES 83 | def create_trigger(triggerName, className, url) 84 | triggerName = _verify_trigger(triggerName) 85 | body = { className: className, triggerName: triggerName, url: url } 86 | request :post, "#{HOOKS_PREFIX}triggers", body: body 87 | end 88 | 89 | # Updated the registered endpoint for this cloud code webhook trigger. 90 | # @param triggerName [String] the name of the trigger. (ex. beforeSave, afterSave) 91 | # @param className [String] the name of the Parse collection for the trigger. 92 | # @param url [String] the new url endpoint for this webhook trigger. 93 | # @return [Parse::Response] 94 | # @see Parse::API::Hooks::TRIGGER_NAMES 95 | def update_trigger(triggerName, className, url) 96 | triggerName = _verify_trigger(triggerName) 97 | request :put, "#{HOOKS_PREFIX}triggers/#{className}/#{triggerName}", body: { url: url } 98 | end 99 | 100 | # Remove a registered cloud code webhook trigger. 101 | # @param triggerName [String] the name of the trigger. (ex. beforeSave, afterSave) 102 | # @param className [String] the name of the Parse collection for the trigger. 103 | # @return [Parse::Response] 104 | # @see Parse::API::Hooks::TRIGGER_NAMES 105 | def delete_trigger(triggerName, className) 106 | triggerName = _verify_trigger(triggerName) 107 | request :put, "#{HOOKS_PREFIX}triggers/#{className}/#{triggerName}", body: { __op: "Delete" } 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/parse/api/objects.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/core_ext" 6 | 7 | module Parse 8 | module API 9 | # REST API methods for fetching CRUD operations on Parse objects. 10 | module Objects 11 | # The class prefix for fetching objects. 12 | # @!visibility private 13 | CLASS_PATH_PREFIX = "classes/" 14 | 15 | # @!visibility private 16 | PREFIX_MAP = { installation: "installations", _installation: "installations", 17 | user: "users", _user: "users", 18 | role: "roles", _role: "roles", 19 | session: "sessions", _session: "sessions" }.freeze 20 | 21 | # @!visibility private 22 | def self.included(base) 23 | base.extend(ClassMethods) 24 | end 25 | 26 | # Class methods to be applied to {Parse::Client} 27 | module ClassMethods 28 | # Get the API path for this class. 29 | # @param className [String] the name of the Parse collection. 30 | # @param id [String] optional objectId to add at the end of the path. 31 | # @return [String] the API uri path 32 | def uri_path(className, id = nil) 33 | if className.is_a?(Parse::Pointer) 34 | id = className.id 35 | className = className.parse_class 36 | end 37 | uri = "#{CLASS_PATH_PREFIX}#{className}" 38 | class_prefix = className.downcase.to_sym 39 | if PREFIX_MAP.has_key?(class_prefix) 40 | uri = PREFIX_MAP[class_prefix] 41 | end 42 | id.present? ? "#{uri}/#{id}" : "#{uri}/" 43 | end 44 | end 45 | 46 | # Get the API path for this class. 47 | # @param className [String] the name of the Parse collection. 48 | # @param id [String] optional objectId to add at the end of the path. 49 | # @return [String] the API uri path 50 | def uri_path(className, id = nil) 51 | self.class.uri_path(className, id) 52 | end 53 | 54 | # Create an object in a collection. 55 | # @param className [String] the name of the Parse collection. 56 | # @param body [Hash] the body of the request. 57 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 58 | # @param headers [Hash] additional HTTP headers to send with the request. 59 | # @return [Parse::Response] 60 | def create_object(className, body = {}, headers: {}, **opts) 61 | response = request :post, uri_path(className), body: body, headers: headers, opts: opts 62 | response.parse_class = className if response.present? 63 | response 64 | end 65 | 66 | # Delete an object in a collection. 67 | # @param className [String] the name of the Parse collection. 68 | # @param id [String] The objectId of the record in the collection. 69 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 70 | # @param headers [Hash] additional HTTP headers to send with the request. 71 | # @return [Parse::Response] 72 | def delete_object(className, id, headers: {}, **opts) 73 | response = request :delete, uri_path(className, id), headers: headers, opts: opts 74 | response.parse_class = className if response.present? 75 | response 76 | end 77 | 78 | # Fetch a specific object from a collection. 79 | # @param className [String] the name of the Parse collection. 80 | # @param id [String] The objectId of the record in the collection. 81 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 82 | # @param headers [Hash] additional HTTP headers to send with the request. 83 | # @return [Parse::Response] 84 | def fetch_object(className, id, headers: {}, **opts) 85 | response = request :get, uri_path(className, id), headers: headers, opts: opts 86 | response.parse_class = className if response.present? 87 | response 88 | end 89 | 90 | # Fetch a set of matching objects for a query. 91 | # @param className [String] the name of the Parse collection. 92 | # @param query [Hash] The set of query constraints. 93 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 94 | # @param headers [Hash] additional HTTP headers to send with the request. 95 | # @return [Parse::Response] 96 | # @see Parse::Query 97 | def find_objects(className, query = {}, headers: {}, **opts) 98 | response = request :get, uri_path(className), query: query, headers: headers, opts: opts 99 | response.parse_class = className if response.present? 100 | response 101 | end 102 | 103 | # Update an object in a collection. 104 | # @param className [String] the name of the Parse collection. 105 | # @param id [String] The objectId of the record in the collection. 106 | # @param body [Hash] The key value pairs to update. 107 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 108 | # @param headers [Hash] additional HTTP headers to send with the request. 109 | # @return [Parse::Response] 110 | def update_object(className, id, body = {}, headers: {}, **opts) 111 | response = request :put, uri_path(className, id), body: body, headers: headers, opts: opts 112 | response.parse_class = className if response.present? 113 | response 114 | end 115 | end #Objects 116 | end #API 117 | end 118 | -------------------------------------------------------------------------------- /lib/parse/api/push.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Parse Push notification service interface for the Parse REST API 7 | module Push 8 | # @!visibility private 9 | PUSH_PATH = "push" 10 | 11 | # Update the schema for a collection. 12 | # @param payload [Hash] the paylod for the Push notification. 13 | # @return [Parse::Response] 14 | # @see http://docs.parseplatform.org/rest/guide/#sending-pushes Sending Pushes 15 | def push(payload = {}) 16 | request :post, PUSH_PATH, body: payload.as_json 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/parse/api/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Schema interface for the Parse REST API 7 | module Schema 8 | # @!visibility private 9 | SCHEMAS_PATH = "schemas" 10 | 11 | # Get all the schemas for the application. 12 | # @return [Parse::Response] 13 | def schemas 14 | opts = { cache: false } 15 | request :get, SCHEMAS_PATH, opts: opts 16 | end 17 | 18 | # Get the schema for a collection. 19 | # @param className [String] the name of the remote Parse collection. 20 | # @return [Parse::Response] 21 | def schema(className) 22 | opts = { cache: false } 23 | request :get, "#{SCHEMAS_PATH}/#{className}", opts: opts 24 | end 25 | 26 | # Create a new collection with the specific schema. 27 | # @param className [String] the name of the remote Parse collection. 28 | # @param schema [Hash] the schema hash. This is a specific format specified by 29 | # Parse. 30 | # @return [Parse::Response] 31 | def create_schema(className, schema) 32 | request :post, "#{SCHEMAS_PATH}/#{className}", body: schema 33 | end 34 | 35 | # Update the schema for a collection. 36 | # @param className [String] the name of the remote Parse collection. 37 | # @param schema [Hash] the schema hash. This is a specific format specified by 38 | # Parse. 39 | # @return [Parse::Response] 40 | def update_schema(className, schema) 41 | request :put, "#{SCHEMAS_PATH}/#{className}", body: schema 42 | end 43 | end #Schema 44 | end #API 45 | end 46 | -------------------------------------------------------------------------------- /lib/parse/api/server.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # APIs related to the open source Parse Server. 7 | module Server 8 | 9 | # @!attribute server_info 10 | # @return [Hash] the information about the server. 11 | attr_accessor :server_info 12 | 13 | # @!visibility private 14 | SERVER_INFO_PATH = "serverInfo" 15 | # @!visibility private 16 | SERVER_HEALTH_PATH = "health" 17 | # Fetch and cache information about the Parse server configuration. This 18 | # hash contains information specifically to the configuration of the running 19 | # parse server. 20 | # @return (see #server_info!) 21 | def server_info 22 | return @server_info if @server_info.present? 23 | response = request :get, SERVER_INFO_PATH 24 | @server_info = response.error? ? nil : 25 | response.result.with_indifferent_access 26 | end 27 | 28 | # Fetches the status of the server based on the health check. 29 | # @return [Boolean] whether the server is 'OK'. 30 | def server_health 31 | opts = { cache: false } 32 | response = request :get, SERVER_HEALTH_PATH, opts: opts 33 | response.success? 34 | end 35 | 36 | # Force fetches the server information. 37 | # @return [Hash] a hash containing server configuration if available. 38 | def server_info! 39 | @server_info = nil 40 | server_info 41 | end 42 | 43 | # Returns the version of the Parse server the client is connected to. 44 | # @return [String] a version string (ex. '2.2.25') if available. 45 | def server_version 46 | server_info.present? ? @server_info[:parseServerVersion] : nil 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/parse/api/sessions.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module API 6 | # Defines the Session class interface for the Parse REST API 7 | module Sessions 8 | # @!visibility private 9 | SESSION_PATH_PREFIX = "sessions" 10 | 11 | # Fetch a session record for a given session token. 12 | # @param session_token [String] an active session token. 13 | # @param opts [Hash] additional options to pass to the {Parse::Client} request. 14 | # @return [Parse::Response] 15 | def fetch_session(session_token, **opts) 16 | opts.merge!({ use_master_key: false, cache: false }) 17 | headers = { Parse::Protocol::SESSION_TOKEN => session_token } 18 | response = request :get, "#{SESSION_PATH_PREFIX}/me", headers: headers, opts: opts 19 | response.parse_class = Parse::Model::CLASS_SESSION 20 | response 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/parse/client/authentication.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "faraday" 5 | require "faraday_middleware" 6 | require "active_support" 7 | require "active_support/core_ext" 8 | 9 | require_relative "protocol" 10 | 11 | module Parse 12 | module Middleware 13 | # This middleware handles sending the proper authentication headers to the 14 | # Parse REST API endpoint. 15 | class Authentication < Faraday::Middleware 16 | include Parse::Protocol 17 | # @!visibility private 18 | DISABLE_MASTER_KEY = "X-Disable-Parse-Master-Key".freeze 19 | # @return [String] the application id for this Parse endpoint. 20 | attr_accessor :application_id 21 | # @return [String] the REST API Key for this Parse endpoint. 22 | attr_accessor :api_key 23 | # The Master key API Key for this Parse endpoint. This is optional. If 24 | # provided, it will be sent in every request. 25 | # @return [String] 26 | attr_accessor :master_key 27 | 28 | # 29 | # @param adapter [Faraday::Adapter] An instance of the Faraday adapter 30 | # used for the connection. Defaults Faraday::Adapter::NetHttp. 31 | # @param options [Hash] the options containing Parse authentication data. 32 | # @option options [String] :application_id the application id. 33 | # @option options [String] :api_key the REST API key. 34 | # @option options [String] :master_key the Master Key for this application. 35 | # If it is set, it will be sent on every request unless this middleware sees 36 | # {DISABLE_MASTER_KEY} as an entry in the headers section. 37 | # @option options [String] :content_type the content type format header. Defaults to 38 | # {Parse::Protocol::CONTENT_TYPE_FORMAT}. 39 | def initialize(adapter, options = {}) 40 | super(adapter) 41 | @application_id = options[:application_id] 42 | @api_key = options[:api_key] 43 | @master_key = options[:master_key] 44 | @content_type = options[:content_type] || CONTENT_TYPE_FORMAT 45 | end 46 | 47 | # Thread-safety 48 | # @!visibility private 49 | def call(env) 50 | dup.call!(env) 51 | end 52 | 53 | # @!visibility private 54 | def call!(env) 55 | # We add the main Parse protocol headers 56 | headers = {} 57 | raise ArgumentError, "No Parse Application Id specified for authentication." unless @application_id.present? 58 | headers[APP_ID] = @application_id 59 | headers[API_KEY] = @api_key unless @api_key.blank? 60 | unless @master_key.blank? || env[:request_headers][DISABLE_MASTER_KEY].present? 61 | headers[MASTER_KEY] = @master_key 62 | end 63 | 64 | env[:request_headers].delete(DISABLE_MASTER_KEY) 65 | 66 | # delete the use of master key if we are using session token. 67 | if env[:request_headers].key?(Parse::Protocol::SESSION_TOKEN) 68 | headers.delete(MASTER_KEY) 69 | end 70 | # merge the headers with the current provided headers 71 | env[:request_headers].merge! headers 72 | # set the content type of the request if it was not provided already. 73 | env[:request_headers][CONTENT_TYPE] ||= @content_type 74 | # only modify header 75 | 76 | @app.call(env).on_complete do |response_env| 77 | # check for return code raise an error when authentication was a failure 78 | # if response_env[:status] == 401 79 | # warn "Unauthorized Parse API Credentials for Application Id: #{@application_id}" 80 | # end 81 | 82 | end 83 | end 84 | end # Authenticator 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/parse/client/body_builder.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "faraday" 5 | require "faraday_middleware" 6 | require_relative "response" 7 | require_relative "protocol" 8 | require "active_support" 9 | require "active_support/core_ext" 10 | require "active_model_serializers" 11 | 12 | module Parse 13 | 14 | # @!attribute self.logging 15 | # Sets {Parse::Middleware::BodyBuilder} logging. 16 | # You may specify `:debug` for additional verbosity. 17 | # @return (see Parse::Middleware::BodyBuilder.logging) 18 | def self.logging 19 | Parse::Middleware::BodyBuilder.logging 20 | end 21 | # @!visibility private 22 | def self.logging=(value) 23 | Parse::Middleware::BodyBuilder.logging = value 24 | end 25 | 26 | # Namespace for Parse-Stack related middleware. 27 | module Middleware 28 | # This middleware takes an incoming Parse response, after an outgoing request, 29 | # and creates a Parse::Response object. 30 | class BodyBuilder < Faraday::Middleware 31 | include Parse::Protocol 32 | # Header sent when a GET requests exceeds the limit. 33 | HTTP_METHOD_OVERRIDE = "X-Http-Method-Override" 34 | # Maximum url length for most server requests before HTTP Method Override is used. 35 | MAX_URL_LENGTH = 2_000.freeze 36 | class << self 37 | # Allows logging. Set to `true` to enable logging, `false` to disable. 38 | # You may specify `:debug` for additional verbosity. 39 | # @return [Boolean] 40 | attr_accessor :logging 41 | end 42 | 43 | # Thread-safety 44 | # @!visibility private 45 | def call(env) 46 | dup.call!(env) 47 | end 48 | 49 | # @!visibility private 50 | def call!(env) 51 | # the maximum url size is ~2KB, so if we request a Parse API url greater than this 52 | # (which is most likely a very complicated query), we need to override the request method 53 | # to be POST instead of GET and send the query parameters in the body of the POST request. 54 | # The standard maximum POST request (which is a server setting), is usually set to 20MBs 55 | if env[:method] == :get && env[:url].to_s.length >= MAX_URL_LENGTH 56 | env[:request_headers][HTTP_METHOD_OVERRIDE] = "GET" 57 | env[:request_headers][CONTENT_TYPE] = "application/x-www-form-urlencoded" 58 | # parse-sever looks for method overrides in the body under the `_method` param. 59 | # so we will add it to the query string, which will now go into the body. 60 | env[:body] = "_method=GET&" + env[:url].query 61 | env[:url].query = nil 62 | #override 63 | env[:method] = :post 64 | # else if not a get, always make sure the request is JSON encoded if the content type matches 65 | elsif env[:request_headers][CONTENT_TYPE] == CONTENT_TYPE_FORMAT && 66 | (env[:body].is_a?(Hash) || env[:body].is_a?(Array)) 67 | env[:body] = env[:body].to_json 68 | end 69 | 70 | if self.class.logging 71 | puts "[Request #{env.method.upcase}] #{env[:url]}" 72 | env[:request_headers].each do |k, v| 73 | next if k == Parse::Protocol::MASTER_KEY 74 | puts "[Header] #{k} : #{v}" 75 | end 76 | 77 | puts "[Request Body] #{env[:body]}" 78 | end 79 | @app.call(env).on_complete do |response_env| 80 | # on a response, create a new Parse::Response and replace the :body 81 | # of the env 82 | # @todo CHECK FOR HTTP STATUS CODES 83 | if self.class.logging 84 | puts "[[Response #{response_env[:status]}]] ----------------------------------" 85 | puts response_env.body 86 | puts "[[Response]] --------------------------------------\n" 87 | end 88 | 89 | begin 90 | r = Parse::Response.new(response_env.body) 91 | rescue => e 92 | r = Parse::Response.new 93 | r.code = response_env.status 94 | r.error = "Invalid response for #{env[:method]} #{env[:url]}: #{e}" 95 | end 96 | r.http_status = response_env[:status] 97 | r.code ||= response_env[:status] if r.error.present? 98 | response_env[:body] = r 99 | end 100 | end 101 | end 102 | end #Middleware 103 | end 104 | -------------------------------------------------------------------------------- /lib/parse/client/protocol.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | # Set of Parse protocol constants. 6 | module Protocol 7 | # The default server url, based on the hosted Parse platform. 8 | SERVER_URL = "http://localhost:1337/parse".freeze 9 | # The request header field to send the application Id. 10 | APP_ID = "X-Parse-Application-Id" 11 | # The request header field to send the REST API key. 12 | API_KEY = "X-Parse-REST-API-Key" 13 | # The request header field to send the Master key. 14 | MASTER_KEY = "X-Parse-Master-Key" 15 | # The request header field to send the revocable Session key. 16 | SESSION_TOKEN = "X-Parse-Session-Token" 17 | # The request header field to request a revocable session token. 18 | REVOCABLE_SESSION = "X-Parse-Revocable-Session" 19 | # The request header field to send the installation id. 20 | INSTALLATION_ID = "Parse-Installation-Id" 21 | # The request header field to send an email when authenticating with Parse hosted platform. 22 | EMAIL = "X-Parse-Email" 23 | # The request header field to send the password when authenticating with the Parse hosted platform. 24 | PASSWORD = "X-Parse-Password" 25 | # The request header field for the Content type. 26 | CONTENT_TYPE = "Content-Type" 27 | # The default content type format for sending API requests. 28 | CONTENT_TYPE_FORMAT = "application/json; charset=utf-8" 29 | end 30 | 31 | # All Parse error codes. 32 | # @todo Implement all error codes as StandardError 33 | # 34 | # List of error codes. 35 | # OtherCause -1 Error code indicating that an unknown error or an error unrelated to Parse occurred. 36 | # InternalServerError 1 Error code indicating that something has gone wrong with the server. If you get this error code, it is Parse's fault. Please report the bug to https://parse.com/help. 37 | # ConnectionFailed 100 Error code indicating the connection to the Parse servers failed. 38 | # ObjectNotFound 101 Error code indicating the specified object doesn't exist. 39 | # InvalidQuery 102 Error code indicating you tried to query with a datatype that doesn't support it, like exact matching an array or object. 40 | # InvalidClassName 103 Error code indicating a missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters. 41 | # MissingObjectId 104 Error code indicating an unspecified object id. 42 | # InvalidKeyName 105 Error code indicating an invalid key name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters. 43 | # InvalidPointer 106 Error code indicating a malformed pointer. You should not see this unless you have been mucking about changing internal Parse code. 44 | # InvalidJSON 107 Error code indicating that badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly. 45 | # CommandUnavailable 108 Error code indicating that the feature you tried to access is only available internally for testing purposes. 46 | # NotInitialized 109 You must call Parse.initialize before using the Parse library. 47 | # IncorrectType 111 Error code indicating that a field was set to an inconsistent type. 48 | # InvalidChannelName 112 Error code indicating an invalid channel name. A channel name is either an empty string (the broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter. 49 | # PushMisconfigured 115 Error code indicating that push is misconfigured. 50 | # ObjectTooLarge 116 Error code indicating that the object is too large. 51 | # OperationForbidden 119 Error code indicating that the operation isn't allowed for clients. 52 | # CacheMiss 120 Error code indicating the result was not found in the cache. 53 | # InvalidNestedKey 121 Error code indicating that an invalid key was used in a nested JSONObject. 54 | # InvalidFileName 122 Error code indicating that an invalid filename was used for ParseFile. A valid file name contains only a-zA-Z0-9_. characters and is between 1 and 128 characters. 55 | # InvalidACL 123 Error code indicating an invalid ACL was provided. 56 | # Timeout 124 Error code indicating that the request timed out on the server. Typically this indicates that the request is too expensive to run. 57 | # InvalidEmailAddress 125 Error code indicating that the email address was invalid. 58 | # DuplicateValue 137 Error code indicating that a unique field was given a value that is already taken. 59 | # InvalidRoleName 139 Error code indicating that a role's name is invalid. 60 | # ExceededQuota 140 Error code indicating that an application quota was exceeded. Upgrade to resolve. 61 | # ScriptFailed 141 Error code indicating that a Cloud Code script failed. 62 | # ValidationFailed 142 Error code indicating that a Cloud Code validation failed. 63 | # FileDeleteFailed 153 Error code indicating that deleting a file failed. 64 | # RequestLimitExceeded 155 Error code indicating that the application has exceeded its request limit. 65 | # InvalidEventName 160 Error code indicating that the provided event name is invalid. 66 | # UsernameMissing 200 Error code indicating that the username is missing or empty. 67 | # PasswordMissing 201 Error code indicating that the password is missing or empty. 68 | # UsernameTaken 202 Error code indicating that the username has already been taken. 69 | # EmailTaken 203 Error code indicating that the email has already been taken. 70 | # EmailMissing 204 Error code indicating that the email is missing, but must be specified. 71 | # EmailNotFound 205 Error code indicating that a user with the specified email was not found. 72 | # SessionMissing 206 Error code indicating that a user object without a valid session could not be altered. 73 | # MustCreateUserThroughSignup 207 Error code indicating that a user can only be created through signup. 74 | # AccountAlreadyLinked 208 Error code indicating that an an account being linked is already linked to another user. 75 | # InvalidSessionToken 209 Error code indicating that the current session token is invalid. 76 | # LinkedIdMissing 250 Error code indicating that a user cannot be linked to an account because that account's id could not be found. 77 | # InvalidLinkedSession 251 Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session. 78 | # UnsupportedService 252 Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported. 79 | # module ErrorCodes 80 | # 81 | # end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /lib/parse/client/request.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/json" 6 | 7 | module Parse 8 | #This class represents a Parse request. 9 | class Request 10 | # @!attribute [rw] method 11 | # @return [String] the HTTP method used for this request. 12 | 13 | # @!attribute [rw] path 14 | # @return [String] the uri path. 15 | 16 | # @!attribute [rw] body 17 | # @return [Hash] the body of this request. 18 | 19 | # TODO: Document opts and cache options. 20 | 21 | # @!attribute [rw] opts 22 | # @return [Hash] a set of options for this request. 23 | # @!attribute [rw] cache 24 | # @return [Boolean] 25 | attr_accessor :method, :path, :body, :headers, :opts, :cache 26 | 27 | # @!visibility private 28 | # Used to correlate batching requests with their responses. 29 | attr_accessor :tag 30 | 31 | # Creates a new request 32 | # @param method [String] the HTTP method 33 | # @param uri [String] the API path of the request (without the host) 34 | # @param body [Hash] the body (or parameters) of this request. 35 | # @param headers [Hash] additional headers to send in this request. 36 | # @param opts [Hash] additional optional parameters. 37 | def initialize(method, uri, body: nil, headers: nil, opts: {}) 38 | @tag = 0 39 | method = method.downcase.to_sym 40 | unless method == :get || method == :put || method == :post || method == :delete 41 | raise ArgumentError, "Invalid method #{method} for request : '#{uri}'" 42 | end 43 | self.method = method 44 | self.path = uri 45 | self.body = body 46 | self.headers = headers || {} 47 | self.opts = opts || {} 48 | end 49 | 50 | # The parameters of this request if the HTTP method is GET. 51 | # @return [Hash] 52 | def query 53 | body if @method == :get 54 | end 55 | 56 | # @return [Hash] JSON encoded hash 57 | def as_json 58 | signature.as_json 59 | end 60 | 61 | # @return [Boolean] 62 | def ==(r) 63 | return false unless r.is_a?(Request) 64 | @method == r.method && @path == r.uri && @body == r.body && @headers == r.headers 65 | end 66 | 67 | # Signature provies a way for us to compare different requests objects. 68 | # Two requests objects are the same if they have the same signature. 69 | # @return [Hash] A hash representing this request. 70 | def signature 71 | { method: @method.upcase, path: @path, body: @body } 72 | end 73 | 74 | # @!visibility private 75 | def inspect 76 | "#<#{self.class} @method=#{@method} @path='#{@path}'>" 77 | end 78 | 79 | # @return [String] 80 | def to_s 81 | "#{@method.to_s.upcase} #{@path}" 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/parse/client/response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/json" 6 | 7 | module Parse 8 | 9 | # Represents a response from Parse server. A response can also 10 | # be a set of responses (from a Batch response). 11 | class Response 12 | include Enumerable 13 | 14 | # Code for an unknown error. 15 | ERROR_INTERNAL = 1 16 | # Code when the server returns a 500 or is non-responsive. 17 | ERROR_SERVICE_UNAVAILABLE = 2 18 | # Code when the request times out. 19 | ERROR_TIMEOUT = 124 20 | # Code when the requests per second limit as been exceeded. 21 | ERROR_EXCEEDED_BURST_LIMIT = 155 22 | # Code when a requested record is not found. 23 | ERROR_OBJECT_NOT_FOUND = 101 24 | # Code when the username is missing in request. 25 | ERROR_USERNAME_MISSING = 200 26 | # Code when the password is missing in request. 27 | ERROR_PASSWORD_MISSING = 201 28 | # Code when the username is already in the system. 29 | ERROR_USERNAME_TAKEN = 202 30 | # Code when the email is already in the system. 31 | ERROR_EMAIL_TAKEN = 203 32 | # Code when the email is not found 33 | ERROR_EMAIL_NOT_FOUND = 205 34 | # Code when the email is invalid 35 | ERROR_EMAIL_INVALID = 125 36 | 37 | # The field name for the error. 38 | ERROR = "error".freeze 39 | # The field name for the success. 40 | SUCCESS = "success".freeze 41 | # The field name for the error code. 42 | CODE = "code".freeze 43 | # The field name for the results of the request. 44 | RESULTS = "results".freeze 45 | # The field name for the count result in a count response. 46 | COUNT = "count".freeze 47 | 48 | # @!attribute [rw] parse_class 49 | # @return [String] the Parse class for this request 50 | # @!attribute [rw] code 51 | # @return [Integer] the error code 52 | # @!attribute [rw] error 53 | # @return [Integer] the error message 54 | # @!attribute [rw] result 55 | # @return [Hash] the body of the response result. 56 | # @!attribute [rw] http_status 57 | # @return [Integer] the HTTP status code from the response. 58 | # @!attribute [rw] request 59 | # @return [Integer] the Parse::Request that generated this response. 60 | # @see Parse::Request 61 | attr_accessor :parse_class, :code, :error, :result, :http_status, 62 | :request 63 | # You can query Parse for counting objects, which may not actually have 64 | # results. 65 | # @return [Integer] the count result from a count query request. 66 | attr_reader :count 67 | 68 | # Create an instance with a Parse response JSON hash. 69 | # @param res [Hash] the JSON hash 70 | def initialize(res = {}) 71 | @http_status = 0 72 | @count = 0 73 | @batch_response = false # by default, not a batch response 74 | @result = nil 75 | # If a string is used for initializing, treat it as JSON 76 | # check for string to not be 'OK' since that is the health check API response 77 | res = JSON.parse(res) if res.is_a?(String) && res != "OK".freeze 78 | # If it is a hash (or parsed JSON), then parse the result. 79 | parse_result!(res) if res.is_a?(Hash) 80 | # if the result is an Array, then most likely it is a set of responses 81 | # from using a Batch API. 82 | if res.is_a?(Array) 83 | @batch_response = true 84 | @result = res || [] 85 | @count = @result.count 86 | end 87 | #if none match, set pure result 88 | @result = res if @result.nil? 89 | end 90 | 91 | # true if this was a batch response. 92 | def batch? 93 | @batch_response 94 | end 95 | 96 | # If it is a batch respnose, we'll create an array of Response objects for each 97 | # of the ones in the batch. 98 | # @return [Array] an array of Response objects. 99 | def batch_responses 100 | return [@result] unless @batch_response 101 | # if batch response, generate array based on the response hash. 102 | @result.map do |r| 103 | next r unless r.is_a?(Hash) 104 | hash = r[SUCCESS] || r[ERROR] 105 | Parse::Response.new hash 106 | end 107 | end 108 | 109 | # This method takes the result hash and determines if it is a regular 110 | # parse query result, object result or a count result. The response should 111 | # be a hash either containing the result data or the error. 112 | def parse_result!(h) 113 | @result = {} 114 | return unless h.is_a?(Hash) 115 | @code = h[CODE] 116 | @error = h[ERROR] 117 | if h[RESULTS].is_a?(Array) 118 | @result = h[RESULTS] 119 | @count = h[COUNT] || @result.count 120 | else 121 | @result = h 122 | @count = 1 123 | end 124 | end 125 | 126 | alias_method :parse_results!, :parse_result! 127 | 128 | # true if the response is successful. 129 | # @see #error? 130 | def success? 131 | @code.nil? && @error.nil? 132 | end 133 | 134 | # true if the response has an error code. 135 | # @see #success? 136 | def error? 137 | !success? 138 | end 139 | 140 | # true if the response has an error code of 'object not found' 141 | # @see ERROR_OBJECT_NOT_FOUND 142 | def object_not_found? 143 | @code == ERROR_OBJECT_NOT_FOUND 144 | end 145 | 146 | # @return [Array] the result data from the response. 147 | def results 148 | return [] if @result.nil? 149 | @result.is_a?(Array) ? @result : [@result] 150 | end 151 | 152 | # @return [Object] the first thing in the result array. 153 | def first 154 | @result.is_a?(Array) ? @result.first : @result 155 | end 156 | 157 | # Iterate through each result item. 158 | # @yieldparam [Object] a result entry. 159 | def each 160 | return enum_for(:each) unless block_given? 161 | results.each(&Proc.new) 162 | self 163 | end 164 | 165 | # @!visibility private 166 | def inspect 167 | if error? 168 | "#<#{self.class} @code=#{code} @error='#{error}'>" 169 | else 170 | "#<#{self.class} @result='#{@result}'>" 171 | end 172 | end 173 | 174 | # @return [String] JSON encoded object, or an error string. 175 | def to_s 176 | return "[E-#{@code}] #{@request} : #{@error} (#{@http_status})" if error? 177 | @result.to_json 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/parse/model/associations/has_one.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "../pointer" 5 | require_relative "collection_proxy" 6 | require_relative "pointer_collection_proxy" 7 | require_relative "relation_collection_proxy" 8 | 9 | module Parse 10 | module Associations 11 | # The `has_one` creates a one-to-one association with another Parse class. 12 | # This association says that the other class in the association contains a 13 | # foreign pointer column which references instances of this class. If your 14 | # model contains a column that is a Parse pointer to another class, you should 15 | # use `belongs_to` for that association instead. 16 | # 17 | # Defining a `has_one` property generates a helper query method to fetch a 18 | # particular record from a foreign class. This is useful for setting up the 19 | # inverse relationship accessors of a `belongs_to`. In the case of the 20 | # `has_one` relationship, the `:field` option represents the name of the 21 | # column of the foreign class where the Parse pointer is stored. By default, 22 | # the lower-first camel case version of the Parse class name is used. 23 | # 24 | # In the example below, a `Band` has a local column named `manager` which has 25 | # a pointer to a `Parse::User` (_:user_) record. This setups up the accessor for `Band` 26 | # objects to access the band's manager. 27 | # 28 | # Since we know there is a column named `manager` in the `Band` class that 29 | # points to a single `Parse::User`, you can setup the inverse association 30 | # read accessor in the `Parse::User` class. Note, that to change the 31 | # association, you need to modify the `manager` property on the band instance 32 | # since it contains the `belongs_to` property. 33 | # 34 | # # every band has a manager 35 | # class Band < Parse::Object 36 | # belongs_to :manager, as: :user 37 | # end 38 | # 39 | # band = Band.first id: '12345' 40 | # # the user represented by this manager 41 | # user = band.manger 42 | # 43 | # # every user manages a band 44 | # class Parse::User 45 | # # inverse relationship to `Band.belongs_to :manager` 46 | # has_one :band, field: :manager 47 | # end 48 | # 49 | # user = Parse::User.first 50 | # 51 | # user.band # similar to performing: Band.first(:manager => user) 52 | # 53 | # 54 | # You may optionally use `has_one` with scopes, in order to fine tune the 55 | # query result. Using the example above, you can customize the query with 56 | # a scope that only fetches the association if the band is approved. If 57 | # the association cannot be fetched, `nil` is returned. 58 | # 59 | # # adding to previous example 60 | # class Band < Parse::Object 61 | # property :approved, :boolean 62 | # property :approved_date, :date 63 | # end 64 | # 65 | # # every user manages a band 66 | # class Parse::User 67 | # has_one :recently_approved, ->{ where(order: :approved_date.desc) }, field: :manager, as: :band 68 | # has_one :band_by_status, ->(status) { where(approved: status) }, field: :manager, as: :band 69 | # end 70 | # 71 | # # gets the band most recently approved 72 | # user.recently_approved 73 | # # equivalent: Band.first(manager: user, order: :approved_date.desc) 74 | # 75 | # # fetch the managed band that is not approved 76 | # user.band_by_status(false) 77 | # # equivalent: Band.first(manager: user, approved: false) 78 | # 79 | # @see Parse::Associations::BelongsTo 80 | # @see Parse::Associations::HasMany 81 | module HasOne 82 | 83 | # @!method self.has_one(key, scope = nil, opts = {}) 84 | # Creates a one-to-one association with another Parse model. 85 | # @param [Symbol] key The singularized version of the foreign class and the name of the 86 | # *foreign* column in the foreign Parse table where the pointer is stored. 87 | # @param [Proc] scope A proc that can customize the query by applying 88 | # additional constraints when fetching the associated record. Works similarly as 89 | # ActiveModel associations described in section {http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html Customizing the Query} 90 | # @option opts [Symbol] :field override the name of the remote column 91 | # where the pointer is stored. By default this is inferred as 92 | # the columnized of the key parameter. 93 | # @option opts [Symbol] :as override the inferred Parse::Object subclass. 94 | # By default this is inferred as the singularized camel case version of 95 | # the key parameter. This option allows you to override the Parse model used 96 | # to perform the query for the association, while allowing you to have a 97 | # different accessor name. 98 | # @option opts [Boolean] scope_only Setting this option to `true`, 99 | # makes the association fetch based only on the scope provided and does 100 | # not use the local instance object as a foreign pointer in the query. 101 | # This allows for cases where another property of the local class, is 102 | # needed to match the resulting records in the association. 103 | # @see String#columnize 104 | # @see Associations::HasMany.has_many 105 | # @return [Parse::Object] a Parse::Object subclass when using the accessor 106 | # when fetching the association. 107 | 108 | # @!visibility private 109 | def self.included(base) 110 | base.extend(ClassMethods) 111 | end 112 | 113 | # @!visibility private 114 | module ClassMethods 115 | 116 | # has one are not property but instance scope methods 117 | def has_one(key, scope = nil, **opts) 118 | opts.reverse_merge!({ as: key, field: parse_class.columnize, scope_only: false }) 119 | klassName = opts[:as].to_parse_class 120 | foreign_field = opts[:field].to_sym 121 | ivar = :"@_has_one_#{key}" 122 | 123 | if self.method_defined?(key) 124 | warn "Creating has_one :#{key} association. Will overwrite existing method #{self}##{key}." 125 | end 126 | 127 | define_method(key) do |*args, &block| 128 | return nil if @id.nil? 129 | query = Parse::Query.new(klassName, limit: 1) 130 | query.where(foreign_field => self) unless opts[:scope_only] == true 131 | 132 | if scope.is_a?(Proc) 133 | # any method not part of Query, gets delegated to the instance object 134 | instance = self 135 | query.define_singleton_method(:method_missing) { |m, *args, &block| instance.send(m, *args, &block) } 136 | query.define_singleton_method(:i) { instance } 137 | 138 | if scope.arity.zero? 139 | query.instance_exec(&scope) 140 | query.conditions(*args) if args.present? 141 | else 142 | query.instance_exec(*args, &scope) 143 | end 144 | instance = nil # help clean up ruby gc 145 | elsif args.present? 146 | query.conditions(*args) 147 | end 148 | # query.define_singleton_method(:method_missing) do |m, *args, &block| 149 | # self.first.send(m, *args, &block) 150 | # end 151 | return query.first if block.nil? 152 | block.call(query.first) 153 | end 154 | end 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/parse/model/associations/pointer_collection_proxy.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_model" 5 | require "active_support" 6 | require "active_support/inflector" 7 | require "active_support/core_ext/object" 8 | require_relative "collection_proxy" 9 | 10 | module Parse 11 | # A PointerCollectionProxy is a collection proxy that only allows Parse Pointers (Objects) 12 | # to be part of the collection. This is done by typecasting the collection to a particular 13 | # Parse class. Ex. An Artist may have several Song objects. Therefore an Artist could have a 14 | # column :songs, that is an array (collection) of Song (Parse::Object subclass) objects. 15 | class PointerCollectionProxy < CollectionProxy 16 | 17 | # @!attribute [rw] collection 18 | # The internal backing store of the collection. 19 | # @note If you modify this directly, it is highly recommended that you 20 | # call {CollectionProxy#notify_will_change!} to notify the dirty tracking system. 21 | # @return [Array] 22 | # @see CollectionProxy#collection 23 | def collection=(c) 24 | notify_will_change! 25 | @collection = c 26 | end 27 | 28 | # Add Parse::Objects to the collection. 29 | # @overload add(parse_object) 30 | # Add a Parse::Object or Parse::Pointer to this collection. 31 | # @param parse_object [Parse::Object,Parse::Pointer] the object to add 32 | # @overload add(parse_objects) 33 | # Add an array of Parse::Objects or Parse::Pointers to this collection. 34 | # @param parse_objects [Array] the array to append. 35 | # @return [Array] the collection 36 | def add(*items) 37 | notify_will_change! if items.count > 0 38 | items.flatten.parse_objects.each do |item| 39 | collection.push(item) if item.is_a?(Parse::Pointer) 40 | end 41 | @collection 42 | end 43 | 44 | # Removes Parse::Objects from the collection. 45 | # @overload remove(parse_object) 46 | # Remove a Parse::Object or Parse::Pointer to this collection. 47 | # @param parse_object [Parse::Object,Parse::Pointer] the object to remove 48 | # @overload remove(parse_objects) 49 | # Remove an array of Parse::Objects or Parse::Pointers from this collection. 50 | # @param parse_objects [Array] the array of objects to remove. 51 | # @return [Array] the collection 52 | def remove(*items) 53 | notify_will_change! if items.count > 0 54 | items.flatten.parse_objects.each do |item| 55 | collection.delete item 56 | end 57 | @collection 58 | end 59 | 60 | # Atomically add a set of Parse::Objects to this collection. 61 | # This is done by making the API request directly with Parse server; the 62 | # local object is not updated with changes. 63 | # @see CollectionProxy#add! 64 | # @see #add_unique! 65 | def add!(*items) 66 | super(items.flatten.parse_pointers) 67 | end 68 | 69 | # Atomically add a set of Parse::Objects to this collection for those not already 70 | # in the collection. 71 | # This is done by making the API request directly with Parse server; the 72 | # local object is not updated with changes. 73 | # @see CollectionProxy#add_unique! 74 | # @see #add! 75 | def add_unique!(*items) 76 | super(items.flatten.parse_pointers) 77 | end 78 | 79 | # Atomically remove a set of Parse::Objects to this collection. 80 | # This is done by making the API request directly with Parse server; the 81 | # local object is not updated with changes. 82 | # @see CollectionProxy#remove! 83 | def remove!(*items) 84 | super(items.flatten.parse_pointers) 85 | end 86 | 87 | # Force fetch the set of pointer objects in this collection. 88 | # @see Array.fetch_objects! 89 | def fetch! 90 | collection.fetch_objects! 91 | end 92 | 93 | # Fetch the set of pointer objects in this collection. 94 | # @see Array.fetch_objects 95 | def fetch 96 | collection.fetch_objects 97 | end 98 | 99 | # Encode the collection as a JSON object of Parse::Pointers. 100 | def as_json(opts = nil) 101 | parse_pointers.as_json(opts) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/parse/model/associations/relation_collection_proxy.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/inflector" 6 | require "active_support/core_ext/object" 7 | require_relative "pointer_collection_proxy" 8 | 9 | module Parse 10 | # The RelationCollectionProxy is similar to a PointerCollectionProxy except that 11 | # there is no actual "array" object in Parse. Parse treats relation through an 12 | # intermediary table (a.k.a. join table). Whenever a developer wants the 13 | # contents of a collection, the foreign table needs to be queried instead. 14 | # In this scenario, the parse_class: initializer argument should be passed in order to 15 | # know which remote table needs to be queried in order to fetch the items of the collection. 16 | # 17 | # Unlike managing an array of Pointers, relations in Parse are done throug atomic operations, 18 | # which have a specific API. The design of this proxy is to maintain two sets of lists, 19 | # items to be added to the relation and a separate list of items to be removed from the 20 | # relation. 21 | # 22 | # Because this relationship is based on queryable Parse table, we are also able to 23 | # not just get all the items in a collection, but also provide additional constraints to 24 | # get matching items within the relation collection. 25 | # 26 | # When creating a Relation proxy, all the delegate methods defined in the superclasses 27 | # need to be implemented, in addition to a few others with the key parameter: 28 | # _relation_query and _commit_relation_updates . :'key'_relation_query should return a 29 | # Parse::Query object that is properly tied to the foreign table class related to this object column. 30 | # Example, if an Artist has many Song objects, then the query to be returned by this method 31 | # should be a Parse::Query for the class 'Song'. 32 | # Because relation changes are separate from object changes, you can call save on a 33 | # relation collection to save the current add and remove operations. Because the delegate needs 34 | # to be informed of the changes being committed, it will be notified 35 | # through :'key'_commit_relation_updates message. The delegate is also in charge of 36 | # clearing out the change information for the collection if saved successfully. 37 | # @see PointerCollectionProxy 38 | class RelationCollectionProxy < PointerCollectionProxy 39 | define_attribute_methods :additions, :removals 40 | # @!attribute [r] removals 41 | # The objects that have been newly removed to this collection 42 | # @return [Array] 43 | # @!attribute [r] additions 44 | # The objects that have been newly added to this collection 45 | # @return [Array] 46 | attr_reader :additions, :removals 47 | 48 | def initialize(collection = nil, delegate: nil, key: nil, parse_class: nil) 49 | super 50 | @additions = [] 51 | @removals = [] 52 | end 53 | 54 | # You can get items within the collection relation filtered by a specific set 55 | # of query constraints. 56 | def all(constraints = {}) 57 | q = query({ limit: :max }.merge(constraints)) 58 | if block_given? 59 | # if we have a query, then use the Proc with it (more efficient) 60 | return q.present? ? q.results(&Proc.new) : collection.each(&Proc.new) 61 | end 62 | # if no block given, get all the results 63 | q.present? ? q.results : collection 64 | end 65 | 66 | # Ask the delegate to return a query for this collection type 67 | def query(constraints = {}) 68 | q = forward :"#{@key}_relation_query" 69 | end 70 | 71 | # Add Parse::Objects to the relation. 72 | # @overload add(parse_object) 73 | # Add a Parse::Object or Parse::Pointer to this relation. 74 | # @param parse_object [Parse::Object,Parse::Pointer] the object to add 75 | # @overload add(parse_objects) 76 | # Add an array of Parse::Objects or Parse::Pointers to this relation. 77 | # @param parse_objects [Array] the array to append. 78 | # @return [Array] the collection 79 | def add(*items) 80 | items = items.flatten.parse_objects 81 | return @collection if items.empty? 82 | 83 | notify_will_change! 84 | additions_will_change! 85 | removals_will_change! 86 | # take all the items 87 | items.each do |item| 88 | @additions.push item 89 | @collection.push item 90 | #cleanup 91 | @removals.delete item 92 | end 93 | @collection 94 | end 95 | 96 | # Removes Parse::Objects from the relation. 97 | # @overload remove(parse_object) 98 | # Remove a Parse::Object or Parse::Pointer to this relation. 99 | # @param parse_object [Parse::Object,Parse::Pointer] the object to remove 100 | # @overload remove(parse_objects) 101 | # Remove an array of Parse::Objects or Parse::Pointers from this relation. 102 | # @param parse_objects [Array] the array of objects to remove. 103 | # @return [Array] the collection 104 | def remove(*items) 105 | items = items.flatten.parse_objects 106 | return @collection if items.empty? 107 | notify_will_change! 108 | additions_will_change! 109 | removals_will_change! 110 | items.each do |item| 111 | @removals.push item 112 | @collection.delete item 113 | # remove it from any add operations 114 | @additions.delete item 115 | end 116 | @collection 117 | end 118 | 119 | # Atomically add a set of Parse::Objects to this relation. 120 | # This is done by making the API request directly with Parse server; the 121 | # local object is not updated with changes. 122 | def add!(*items) 123 | return false unless @delegate.respond_to?(:op_add_relation!) 124 | items = items.flatten.parse_pointers 125 | @delegate.send :op_add_relation!, @key, items 126 | end 127 | 128 | # Atomically add a set of Parse::Objects to this relation. 129 | # This is done by making the API request directly with Parse server; the 130 | # local object is not updated with changes. 131 | def add_unique!(*items) 132 | return false unless @delegate.respond_to?(:op_add_relation!) 133 | items = items.flatten.parse_pointers 134 | @delegate.send :op_add_relation!, @key, items 135 | end 136 | 137 | # Atomically remove a set of Parse::Objects to this relation. 138 | # This is done by making the API request directly with Parse server; the 139 | # local object is not updated with changes. 140 | def remove!(*items) 141 | return false unless @delegate.respond_to?(:op_remove_relation!) 142 | items = items.flatten.parse_pointers 143 | @delegate.send :op_remove_relation!, @key, items 144 | end 145 | 146 | # Save the changes to the relation 147 | def save 148 | unless @removals.empty? && @additions.empty? 149 | forward :"#{@key}_commit_relation_updates" 150 | end 151 | end 152 | 153 | # @see #add 154 | def <<(*list) 155 | list.each { |d| add(d) } 156 | @collection 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/parse/model/bytes.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/core_ext/object" 6 | require_relative "model" 7 | require "base64" 8 | 9 | module Parse 10 | 11 | # Support for the Bytes type in Parse 12 | class Bytes < Model 13 | # The default attributes in a Parse Bytes hash. 14 | ATTRIBUTES = { __type: :string, base64: :string }.freeze 15 | # @return [String] the base64 string representing the content 16 | attr_accessor :base64 17 | # @return [TYPE_BYTES] 18 | def self.parse_class; TYPE_BYTES; end 19 | # @return [TYPE_BYTES] 20 | def parse_class; self.class.parse_class; end 21 | 22 | alias_method :__type, :parse_class 23 | 24 | # initialize with a base64 string or a Bytes object 25 | # @param bytes [String] The content as base64 string. 26 | def initialize(bytes = "") 27 | @base64 = (bytes.is_a?(Bytes) ? bytes.base64 : bytes).dup 28 | end 29 | 30 | # @!attribute attributes 31 | # Supports for mass assignment of values and encoding to JSON. 32 | # @return [ATTRIBUTES] 33 | def attributes 34 | ATTRIBUTES 35 | end 36 | 37 | # Base64 encode and set the instance contents 38 | # @param str the string to encode 39 | def encode(str) 40 | @base64 = Base64.encode64(str) 41 | end 42 | 43 | # Get the content as decoded base64 bytes 44 | def decoded 45 | Base64.decode64(@base64 || "") 46 | end 47 | 48 | def attributes=(a) 49 | if a.is_a?(String) 50 | @bytes = a 51 | elsif a.is_a?(Hash) 52 | @bytes = a["base64"] || @bytes 53 | end 54 | end 55 | 56 | # Two Parse::Bytes objects are equal if they have the same base64 signature 57 | def ==(u) 58 | return false unless u.is_a?(self.class) 59 | @base64 == u.base64 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/parse/model/classes/installation.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | require_relative "../object" 4 | 5 | module Parse 6 | # This class represents the data and columns contained in the standard Parse 7 | # `_Installation` collection. This class is also responsible for managing the 8 | # device tokens for mobile devices in order to use push notifications. All queries done 9 | # to send pushes using Parse::Push are performed against the Installation collection. 10 | # An installation object represents an instance of your app being installed 11 | # on a device. These objects are used to store subscription data for 12 | # installations which have subscribed to one or more push notification channels. 13 | # 14 | # The default schema for {Installation} is as follows: 15 | # 16 | # class Parse::Installation < Parse::Object 17 | # # See Parse::Object for inherited properties... 18 | # 19 | # property :gcm_sender_id, field: :GCMSenderId 20 | # property :app_identifier 21 | # property :app_name 22 | # property :app_version 23 | # property :badge, :integer 24 | # property :channels, :array 25 | # property :device_token 26 | # property :device_token_last_modified, :integer 27 | # property :device_type, enum: [:ios, :android, :winrt, :winphone, :dotnet] 28 | # property :installation_id 29 | # property :locale_identifier 30 | # property :parse_version 31 | # property :push_type 32 | # property :time_zone, :timezone 33 | # 34 | # has_one :session, ->{ where(installation_id: i.installation_id) }, scope_only: true 35 | # end 36 | # @see Push 37 | # @see Parse::Object 38 | class Installation < Parse::Object 39 | parse_class Parse::Model::CLASS_INSTALLATION 40 | # @!attribute gcm_sender_id 41 | # This field only has meaning for Android installations that use the GCM 42 | # push type. It is reserved for directing Parse to send pushes to this 43 | # installation with an alternate GCM sender ID. This field should generally 44 | # not be set unless you are uploading installation data from another push 45 | # provider. If you set this field, then you must set the GCM API key 46 | # corresponding to this GCM sender ID in your Parse application’s push settings. 47 | # @return [String] 48 | property :gcm_sender_id, field: :GCMSenderId 49 | 50 | # @!attribute app_identifier 51 | # A unique identifier for this installation’s client application. In iOS, this is the Bundle Identifier. 52 | # @return [String] 53 | property :app_identifier 54 | 55 | # @!attribute app_name 56 | # The display name of the client application to which this installation belongs. 57 | # @return [String] 58 | property :app_name 59 | 60 | # @!attribute app_version 61 | # The version string of the client application to which this installation belongs. 62 | # @return [String] 63 | property :app_version 64 | 65 | # @!attribute badge 66 | # A number field representing the last known application badge for iOS installations. 67 | # @return [Integer] 68 | property :badge, :integer 69 | 70 | # @!attribute channels 71 | # An array of the channels to which a device is currently subscribed. 72 | # Note that **channelUris** (the Microsoft-generated push URIs for Windows devices) is 73 | # not supported at this time. 74 | # @return [Array] 75 | property :channels, :array 76 | 77 | # @!attribute device_token 78 | # The Apple or Google generated token used to deliver messages to the APNs 79 | # or GCM push networks respectively. 80 | # @return [String] 81 | property :device_token 82 | 83 | # @!attribute device_token_last_modified 84 | # @return [Integer] number of seconds since token modified 85 | property :device_token_last_modified, :integer 86 | 87 | # @!attribute device_type 88 | # The type of device, “ios”, “android”, “winrt”, “winphone”, or “dotnet” (readonly). 89 | # This property is implemented as a Parse::Stack enumeration. 90 | # @return [String] 91 | property :device_type, enum: [:ios, :android, :winrt, :winphone, :dotnet] 92 | 93 | # @!attribute installation_id 94 | # Universally Unique Identifier (UUID) for the device used by Parse. It 95 | # must be unique across all of an app’s installations. (readonly). 96 | # @return [String] 97 | property :installation_id 98 | 99 | # @!attribute locale_identifier 100 | # The locale for this device. 101 | # @return [String] 102 | property :locale_identifier 103 | 104 | # @!attribute parse_version 105 | # The version of the Parse SDK which this installation uses. 106 | # @return [String] 107 | property :parse_version 108 | 109 | # @!attribute push_type 110 | # This field is reserved for directing Parse to the push delivery network 111 | # to be used. If the device is registered to receive pushes via GCM, this 112 | # field will be marked “gcm”. If this device is not using GCM, and is 113 | # using Parse’s push notification service, it will be blank (readonly). 114 | # @return [String] 115 | property :push_type 116 | 117 | # @!attribute time_zone 118 | # The current time zone where the target device is located. This should be an IANA time zone identifier 119 | # or a {Parse::TimeZone} instance. 120 | # @return [Parse::TimeZone] 121 | property :time_zone, :timezone 122 | 123 | # @!attribute session 124 | # Returns the corresponding {Parse::Session} associated with this installation, if any exists. 125 | # This is implemented as a has_one association to the Session class using the {installation_id}. 126 | # @version 1.7.1 127 | # @return [Parse::Session] The associated {Parse::Session} that might be tied to this installation 128 | has_one :session, -> { where(installation_id: i.installation_id) }, scope_only: true 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/parse/model/classes/product.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | require_relative "../object" 4 | require_relative "user" 5 | 6 | module Parse 7 | # This class represents the data and columns contained in the standard Parse `_Product` collection. 8 | # These records are usually used when implementing in-app purchases in mobile applications. 9 | # 10 | # The default schema for {Product} is as follows: 11 | # 12 | # class Parse::Product < Parse::Object 13 | # # See Parse::Object for inherited properties... 14 | # 15 | # property :download, :file 16 | # property :icon, :file, required: true 17 | # property :order, :integer, required: true 18 | # property :subtitle, required: true 19 | # property :title, required: true 20 | # property :product_identifier, required: true 21 | # 22 | # end 23 | # @see Parse::Object 24 | class Product < Parse::Object 25 | parse_class Parse::Model::CLASS_PRODUCT 26 | # @!attribute download 27 | # @return [String] the file payload for this product download. 28 | property :download, :file 29 | 30 | # @!attribute download_name 31 | # @return [String] the name of this download. 32 | property :download_name 33 | 34 | # @!attribute icon 35 | # An icon file representing this download. This field is required by Parse. 36 | # @return [String] 37 | property :icon, :file, required: true 38 | 39 | # @!attribute order 40 | # The product order number. This field is required by Parse. 41 | # @return [String] 42 | property :order, :integer, required: true 43 | 44 | # @!attribute product_identifier 45 | # The product identifier. This field is required by Parse. 46 | # @return [String] 47 | property :product_identifier, required: true 48 | 49 | # @!attribute subtitle 50 | # The subtitle description for this product. This field is required by Parse. 51 | # @return [String] 52 | property :subtitle, required: true 53 | 54 | # @!attribute title 55 | # The title for this product. This field is required by Parse. 56 | # @return [String] the title for this product. 57 | property :title, required: true 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/parse/model/classes/role.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | require_relative "../object" 4 | require_relative "user" 5 | 6 | module Parse 7 | # This class represents the data and columns contained in the standard Parse `_Role` collection. 8 | # Roles allow the an application to group a set of {Parse::User} records with the same set of 9 | # permissions, so that specific records in the database can have {Parse::ACL}s related to a role 10 | # than trying to add all the users in a group. 11 | # 12 | # The default schema for {Role} is as follows: 13 | # 14 | # class Parse::Role < Parse::Object 15 | # # See Parse::Object for inherited properties... 16 | # 17 | # property :name 18 | # 19 | # # A role may have child roles. 20 | # has_many :roles, through: :relation 21 | # 22 | # # The set of users who belong to this role. 23 | # has_many :users, through: :relation 24 | # end 25 | # @see Parse::Object 26 | class Role < Parse::Object 27 | parse_class Parse::Model::CLASS_ROLE 28 | # @!attribute name 29 | # @return [String] the name of this role. 30 | property :name 31 | # This attribute is mapped as a `has_many` Parse relation association with the {Parse::Role} class, 32 | # as roles can be associated with multiple child roles to support role inheritance. 33 | # The roles Parse relation provides a mechanism to create a hierarchical inheritable types of permissions 34 | # by assigning child roles. 35 | # @return [RelationCollectionProxy] a collection of Roles. 36 | has_many :roles, through: :relation 37 | # This attribute is mapped as a `has_many` Parse relation association with the {Parse::User} class. 38 | # @return [RelationCollectionProxy] a Parse relation of users belonging to this role. 39 | has_many :users, through: :relation 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/parse/model/classes/session.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | require_relative "../object" 4 | 5 | module Parse 6 | # This class represents the data and columns contained in the standard Parse 7 | # `_Session` collection. The Session class maintains per-device (or website) authentication 8 | # information for a particular user. Whenever a User object is logged in, a new Session record, with 9 | # a session token is generated. You may use a known active session token to find the corresponding 10 | # user for that session. Deleting a Session record (and session token), effectively logs out the user, when making Parse requests 11 | # on behalf of the user using the session token. 12 | # 13 | # The default schema for the {Session} class is as follows: 14 | # class Parse::Session < Parse::Object 15 | # # See Parse::Object for inherited properties... 16 | # 17 | # property :session_token 18 | # property :created_with, :object 19 | # property :expires_at, :date 20 | # property :installation_id 21 | # property :restricted, :boolean 22 | # 23 | # belongs_to :user 24 | # 25 | # # Installation where the installation_id matches. 26 | # has_one :installation, ->{ where(installation_id: i.installation_id) }, scope_only: true 27 | # end 28 | # 29 | # @see Parse::Object 30 | class Session < Parse::Object 31 | parse_class Parse::Model::CLASS_SESSION 32 | 33 | # @!attribute created_with 34 | # @return [Hash] data on how this Session was created. 35 | property :created_with, :object 36 | 37 | # @!attribute expires_at 38 | # @return [Parse::Date] when the session token expires. 39 | property :expires_at, :date 40 | 41 | # @!attribute installation_id 42 | # @return [String] The installation id from the Installation table. 43 | # @see Installation#installation_id 44 | property :installation_id 45 | 46 | # @!attribute [r] restricted 47 | # @return [Boolean] whether this session token is restricted. 48 | property :restricted, :boolean 49 | 50 | # @!attribute [r] session_token 51 | # @return [String] the session token for this installation and user pair. 52 | property :session_token 53 | # @!attribute [r] user 54 | # This property is mapped as a `belongs_to` association with the {Parse::User} 55 | # class. Every session instance is tied to a specific logged in user. 56 | # @return [User] the user corresponding to this session. 57 | # @see User 58 | belongs_to :user 59 | 60 | # @!attribute [r] installation 61 | # Returns the {Parse::Installation} where the sessions installation_id field matches the installation_id field 62 | # in the {Parse::Installation} collection. This is implemented as a has_one scope. 63 | # @version 1.7.1 64 | # @return [Parse::Installation] The associated {Parse::Installation} tied to this session 65 | has_one :installation, -> { where(installation_id: i.installation_id) }, scope_only: true 66 | 67 | # Return the Session record for this session token. 68 | # @param token [String] the session token 69 | # @return [Session] the session for this token, otherwise nil. 70 | def self.session(token, **opts) 71 | response = client.fetch_session(token, opts) 72 | if response.success? 73 | return Parse::Session.build response.result 74 | end 75 | nil 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/parse/model/core/builder.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/inflector" 6 | require "active_support/core_ext" 7 | require_relative "../object" 8 | 9 | module Parse 10 | # Create all Parse::Object subclasses, including their properties and inferred 11 | # associations by importing the schema for the remote collections in a Parse 12 | # application. Uses the default configured client. 13 | # @return [Array] an array of created Parse::Object subclasses. 14 | # @see Parse::Model::Builder.build! 15 | def self.auto_generate_models! 16 | Parse.schemas.map do |schema| 17 | Parse::Model::Builder.build!(schema) 18 | end 19 | end 20 | 21 | class Model 22 | # This class provides a method to automatically generate Parse::Object subclasses, including 23 | # their properties and inferred associations by importing the schema for the remote collections 24 | # in a Parse application. 25 | class Builder 26 | 27 | # Builds a ruby Parse::Object subclass with the provided schema information. 28 | # @param schema [Hash] the Parse-formatted hash schema for a collection. This hash 29 | # should two keys: 30 | # * className: Contains the name of the collection. 31 | # * field: A hash containg the column fields and their type. 32 | # @raise ArgumentError when the className could not be inferred from the schema. 33 | # @return [Array] an array of Parse::Object subclass constants. 34 | def self.build!(schema) 35 | unless schema.is_a?(Hash) 36 | raise ArgumentError, "Schema parameter should be a Parse schema hash object." 37 | end 38 | schema = schema.with_indifferent_access 39 | fields = schema[:fields] || {} 40 | className = schema[:className] 41 | 42 | if className.blank? 43 | raise ArgumentError, "No valid className provided for schema hash" 44 | end 45 | 46 | begin 47 | klass = Parse::Model.find_class className 48 | klass = ::Object.const_get(className.to_parse_class) if klass.nil? 49 | rescue => e 50 | klass = ::Class.new(Parse::Object) 51 | ::Object.const_set(className, klass) 52 | end 53 | 54 | base_fields = Parse::Properties::BASE.keys 55 | class_fields = klass.field_map.values + [:className] 56 | fields.each do |field, type| 57 | field = field.to_sym 58 | key = field.to_s.underscore.to_sym 59 | next if base_fields.include?(field) || class_fields.include?(field) 60 | 61 | data_type = type[:type].downcase.to_sym 62 | if data_type == :pointer 63 | klass.belongs_to key, as: type[:targetClass], field: field 64 | elsif data_type == :relation 65 | klass.has_many key, through: :relation, as: type[:targetClass], field: field 66 | else 67 | klass.property key, data_type, field: field 68 | end 69 | class_fields.push(field) 70 | end 71 | klass 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/parse/model/core/errors.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | # The set of all Parse errors. 5 | module Parse 6 | # An abstract parent class for all Parse::Error types. 7 | class Error < StandardError; end 8 | end 9 | -------------------------------------------------------------------------------- /lib/parse/model/core/fetching.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "time" 5 | require "parallel" 6 | 7 | module Parse 8 | # Combines a set of core functionality for {Parse::Object} and its subclasses. 9 | module Core 10 | # Defines the record fetching interface for instances of Parse::Object. 11 | module Fetching 12 | 13 | # Force fetches and updates the current object with the data contained in the Parse collection. 14 | # The changes applied to the object are not dirty tracked. 15 | # @param opts [Hash] a set of options to pass to the client request. 16 | # @return [self] the current object, useful for chaining. 17 | def fetch!(opts = {}) 18 | response = client.fetch_object(parse_class, id, opts) 19 | if response.error? 20 | puts "[Fetch Error] #{response.code}: #{response.error}" 21 | end 22 | # take the result hash and apply it to the attributes. 23 | apply_attributes!(response.result, dirty_track: false) 24 | clear_changes! 25 | self 26 | end 27 | 28 | # Fetches the object from the Parse data store if the object is in a Pointer 29 | # state. This is similar to the `fetchIfNeeded` action in the standard Parse client SDK. 30 | # @return [self] the current object. 31 | def fetch 32 | # if it is a pointer, then let's go fetch the rest of the content 33 | pointer? ? fetch! : self 34 | end 35 | 36 | # Autofetches the object based on a key that is not part {Parse::Properties::BASE_KEYS}. 37 | # If the key is not a Parse standard key, and the current object is in a 38 | # Pointer state, then fetch the data related to this record from the Parse 39 | # data store. 40 | # @param key [String] the name of the attribute being accessed. 41 | # @return [Boolean] 42 | def autofetch!(key) 43 | key = key.to_sym 44 | @fetch_lock ||= false 45 | if @fetch_lock != true && pointer? && key != :acl && Parse::Properties::BASE_KEYS.include?(key) == false && respond_to?(:fetch) 46 | #puts "AutoFetching Triggerd by: #{self.class}.#{key} (#{id})" 47 | @fetch_lock = true 48 | send :fetch 49 | @fetch_lock = false 50 | end 51 | end 52 | end 53 | end 54 | end 55 | 56 | class Array 57 | 58 | # Perform a threaded each iteration on a set of array items. 59 | # @param threads [Integer] the maximum number of threads to spawn/ 60 | # @yield the block for the each iteration. 61 | # @return [self] 62 | # @see Array#each 63 | # @see https://github.com/grosser/parallel Parallel 64 | def threaded_each(threads = 2, &block) 65 | Parallel.each(self, { in_threads: threads }, &block) 66 | end 67 | 68 | # Perform a threaded map operation on a set of array items. 69 | # @param threads [Integer] the maximum number of threads to spawn 70 | # @yield the block for the map iteration. 71 | # @return [Array] the resultant array from the map. 72 | # @see Array#map 73 | # @see https://github.com/grosser/parallel Parallel 74 | def threaded_map(threads = 2, &block) 75 | Parallel.map(self, { in_threads: threads }, &block) 76 | end 77 | 78 | # Fetches all the objects in the array even if they are not in a Pointer state. 79 | # @param lookup [Symbol] The methodology to use for HTTP requests. Use :parallel 80 | # to fetch all objects in parallel HTTP requests. Set to anything else to 81 | # perform requests serially. 82 | # @return [Array] an array of fetched Parse::Objects. 83 | # @see Array#fetch_objects 84 | def fetch_objects!(lookup = :parallel) 85 | # this gets all valid parse objects from the array 86 | items = valid_parse_objects 87 | lookup == :parallel ? items.threaded_each(2, &:fetch!) : items.each(&:fetch!) 88 | #self.replace items 89 | self #return for chaining. 90 | end 91 | 92 | # Fetches all the objects in the array that are in Pointer state. 93 | # @param lookup [Symbol] The methodology to use for HTTP requests. Use :parallel 94 | # to fetch all objects in parallel HTTP requests. Set to anything else to 95 | # perform requests serially. 96 | # @return [Array] an array of fetched Parse::Objects. 97 | # @see Array#fetch_objects! 98 | def fetch_objects(lookup = :parallel) 99 | items = valid_parse_objects 100 | lookup == :parallel ? items.threaded_each(2, &:fetch) : items.each(&:fetch) 101 | #self.replace items 102 | self 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/parse/model/core/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "properties" 5 | 6 | module Parse 7 | module Core 8 | # Defines the Schema methods applied to a Parse::Object. 9 | module Schema 10 | 11 | # Generate a Parse-server compatible schema hash for performing changes to the 12 | # structure of the remote collection. 13 | # @return [Hash] the schema for this Parse::Object subclass. 14 | def schema 15 | sch = { className: parse_class, fields: {} } 16 | #first go through all the attributes 17 | attributes.each do |k, v| 18 | # don't include the base Parse fields 19 | next if Parse::Properties::BASE.include?(k) 20 | next if v.nil? 21 | result = { type: v.to_s.camelize } 22 | # if it is a basic column property, find the right datatype 23 | case v 24 | when :integer, :float 25 | result[:type] = Parse::Model::TYPE_NUMBER 26 | when :geopoint, :geo_point 27 | result[:type] = Parse::Model::TYPE_GEOPOINT 28 | when :pointer 29 | result = { type: Parse::Model::TYPE_POINTER, targetClass: references[k] } 30 | when :acl 31 | result[:type] = Parse::Model::ACL 32 | when :timezone, :time_zone 33 | result[:type] = "String" # no TimeZone native in Parse 34 | else 35 | result[:type] = v.to_s.camelize 36 | end 37 | 38 | sch[:fields][k] = result 39 | end 40 | #then add all the relational column attributes 41 | relations.each do |k, v| 42 | sch[:fields][k] = { type: Parse::Model::TYPE_RELATION, targetClass: relations[k] } 43 | end 44 | sch 45 | end 46 | 47 | # Update the remote schema for this Parse collection. 48 | # @param schema_updates [Hash] the changes to be made to the schema. 49 | # @return [Parse::Response] 50 | def update_schema(schema_updates = nil) 51 | schema_updates ||= schema 52 | client.update_schema parse_class, schema_updates 53 | end 54 | 55 | # Create a new collection for this model with the schema defined by the local 56 | # model. 57 | # @return [Parse::Response] 58 | # @see Schema.schema 59 | def create_schema 60 | client.create_schema parse_class, schema 61 | end 62 | 63 | # Fetche the current schema for this collection from Parse server. 64 | # @return [Parse::Response] 65 | def fetch_schema 66 | client.schema parse_class 67 | end 68 | 69 | # A class method for non-destructive auto upgrading a remote schema based 70 | # on the properties and relations you have defined in your local model. If 71 | # the collection doesn't exist, we create the schema. If the collection already 72 | # exists, the current schema is fetched, and only add the additional fields 73 | # that are missing. 74 | # @note This feature requires use of the master_key. No columns or fields are removed, this is a safe non-destructive upgrade. 75 | # @return [Parse::Response] if the remote schema was modified. 76 | # @return [Boolean] if no changes were made to the schema, it returns true. 77 | def auto_upgrade! 78 | unless client.master_key.present? 79 | warn "[Parse] Schema changes for #{parse_class} is only available with the master key!" 80 | return false 81 | end 82 | # fetch the current schema (requires master key) 83 | response = fetch_schema 84 | 85 | # if it's a core class that doesn't exist, then create the collection without any fields, 86 | # since parse-server will automatically create the collection with the set of core fields. 87 | # then fetch the schema again, to add the missing fields. 88 | if response.error? && self.to_s.start_with?("Parse::") #is it a core class? 89 | client.create_schema parse_class, {} 90 | response = fetch_schema 91 | # if it still wasn't able to be created, raise an error. 92 | if response.error? 93 | warn "[Parse] Schema error: unable to create class #{parse_class}" 94 | return response 95 | end 96 | end 97 | 98 | if response.success? 99 | #let's figure out the diff fields 100 | remote_fields = response.result["fields"] 101 | current_schema = schema 102 | current_schema[:fields] = current_schema[:fields].reduce({}) do |h, (k, v)| 103 | #if the field does not exist in Parse, then add it to the update list 104 | h[k] = v if remote_fields[k.to_s].nil? 105 | h 106 | end 107 | return true if current_schema[:fields].empty? 108 | return update_schema(current_schema) 109 | end 110 | create_schema 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/parse/model/date.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "time" 5 | require "date" 6 | require "active_model" 7 | require "active_support" 8 | require "active_support/inflector" 9 | require "active_support/core_ext/object" 10 | require "active_support/core_ext/date/calculations" 11 | require "active_support/core_ext/date_time/calculations" 12 | require "active_support/core_ext/time/calculations" 13 | require "active_model_serializers" 14 | require_relative "model" 15 | 16 | module Parse 17 | # This class manages dates in the special JSON format it requires for 18 | # properties of type _:date_. 19 | class Date < ::DateTime 20 | # The default attributes in a Parse Date hash. 21 | ATTRIBUTES = { __type: :string, iso: :string }.freeze 22 | include ::ActiveModel::Model 23 | include ::ActiveModel::Serializers::JSON 24 | 25 | # @return [Parse::Model::TYPE_DATE] 26 | def self.parse_class; Parse::Model::TYPE_DATE; end 27 | # @return [Parse::Model::TYPE_DATE] 28 | def parse_class; self.class.parse_class; end 29 | 30 | alias_method :__type, :parse_class 31 | 32 | # @return [Hash] 33 | def attributes 34 | ATTRIBUTES 35 | end 36 | 37 | # @return [String] the ISO8601 time string including milliseconds 38 | def iso 39 | to_time.utc.iso8601(3) 40 | end 41 | 42 | # @return (see #iso) 43 | def to_s(*args) 44 | args.empty? ? iso : super(*args) 45 | end 46 | end 47 | end 48 | 49 | # Adds extensions to Time class to be compatible with {Parse::Date}. 50 | class Time 51 | # @return [Parse::Date] Converts object to Parse::Date 52 | def parse_date 53 | Parse::Date.parse iso8601(3) 54 | end 55 | end 56 | 57 | # Adds extensions to DateTime class to be compatible with {Parse::Date}. 58 | class DateTime 59 | # @return [Parse::Date] Converts object to Parse::Date 60 | def parse_date 61 | Parse::Date.parse iso8601(3) 62 | end 63 | end 64 | 65 | # Adds extensions to ActiveSupport class to be compatible with {Parse::Date}. 66 | module ActiveSupport 67 | # Adds extensions to ActiveSupport::TimeWithZone class to be compatible with {Parse::Date}. 68 | class TimeWithZone 69 | # @return [Parse::Date] Converts object to Parse::Date 70 | def parse_date 71 | Parse::Date.parse iso8601(3) 72 | end 73 | end 74 | end 75 | 76 | # Adds extensions to Date class to be compatible with {Parse::Date}. 77 | class Date 78 | # @return [Parse::Date] Converts object to Parse::Date 79 | def parse_date 80 | Parse::Date.parse iso8601 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/parse/model/push.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "../query.rb" 5 | require_relative "../client.rb" 6 | require "active_model_serializers" 7 | 8 | module Parse 9 | # This class represents the API to send push notification to devices that are 10 | # available in the Installation table. Push notifications are implemented 11 | # through the `Parse::Push` class. To send push notifications through the 12 | # REST API, you must enable `REST push enabled?` option in the `Push 13 | # Notification Settings` section of the `Settings` page in your Parse 14 | # application. Push notifications targeting uses the Installation Parse 15 | # class to determine which devices receive the notification. You can provide 16 | # any query constraint, similar to using `Parse::Query`, in order to target 17 | # the specific set of devices you want given the columns you have configured 18 | # in your `Installation` class. The `Parse::Push` class supports many other 19 | # options not listed here. 20 | # @example 21 | # 22 | # push = Parse::Push.new 23 | # push.send( "Hello World!") # to everyone 24 | # 25 | # # simple channel push 26 | # push = Parse::Push.new 27 | # push.channels = ["addicted2salsa"] 28 | # push.send "You are subscribed to Addicted2Salsa!" 29 | # 30 | # # advanced targeting 31 | # push = Parse::Push.new( {..where query constraints..} ) 32 | # # or use `where()` 33 | # push.where :device_type.in => ['ios','android'], :location.near => some_geopoint 34 | # push.alert = "Hello World!" 35 | # push.sound = "soundfile.caf" 36 | # 37 | # # additional payload data 38 | # push.data = { uri: "app://deep_link_path" } 39 | # 40 | # # Send the push 41 | # push.send 42 | # 43 | # 44 | class Push 45 | include Client::Connectable 46 | 47 | # @!attribute [rw] query 48 | # Sending a push notification is done by performing a query against the Installation 49 | # collection with a Parse::Query. This query contains the constraints that will be 50 | # sent to Parse with the push payload. 51 | # @return [Parse::Query] the query containing Installation constraints. 52 | 53 | # @!attribute [rw] alert 54 | # @return [String] 55 | # @!attribute [rw] badge 56 | # @return [Integer] 57 | # @!attribute [rw] sound 58 | # @return [String] the name of the sound file 59 | # @!attribute [rw] title 60 | # @return [String] 61 | # @!attribute [rw] data 62 | # @return [Hash] specific payload data. 63 | # @!attribute [rw] expiration_time 64 | # @return [Parse::Date] 65 | # @!attribute [rw] expiration_interval 66 | # @return [Integer] 67 | # @!attribute [rw] push_time 68 | # @return [Parse::Date] 69 | # @!attribute [rw] channels 70 | # @return [Array] an array of strings for subscribed channels. 71 | attr_accessor :query, :alert, :badge, :sound, :title, :data, 72 | :expiration_time, :expiration_interval, :push_time, :channels 73 | 74 | alias_method :message, :alert 75 | alias_method :message=, :alert= 76 | 77 | # Send a push notification using a push notification hash 78 | # @param payload [Hash] a push notification hash payload 79 | def self.send(payload) 80 | client.push payload.as_json 81 | end 82 | 83 | # Initialize a new push notification request. 84 | # @param constraints [Hash] a set of query constraints 85 | def initialize(constraints = {}) 86 | self.where constraints 87 | end 88 | 89 | def query 90 | @query ||= Parse::Query.new(Parse::Model::CLASS_INSTALLATION) 91 | end 92 | 93 | # Set a hash of conditions for this push query. 94 | # @return [Parse::Query] 95 | def where=(where_clausees) 96 | query.where where_clauses 97 | end 98 | 99 | # Apply a set of constraints. 100 | # @param constraints [Hash] the set of {Parse::Query} cosntraints 101 | # @return [Hash] if no constraints were passed, returns a compiled query. 102 | # @return [Parse::Query] if constraints were passed, returns the chainable query. 103 | def where(constraints = nil) 104 | return query.compile_where unless constraints.is_a?(Hash) 105 | query.where constraints 106 | query 107 | end 108 | 109 | def channels=(list) 110 | @channels = Array.wrap(list) 111 | end 112 | 113 | def data=(h) 114 | if h.is_a?(String) 115 | @alert = h 116 | else 117 | @data = h.symbolize_keys 118 | end 119 | end 120 | 121 | # @return [Hash] a JSON encoded hash. 122 | def as_json(*args) 123 | payload.as_json 124 | end 125 | 126 | # @return [String] a JSON encoded string. 127 | def to_json(*args) 128 | as_json.to_json 129 | end 130 | 131 | # This method takes all the parameters of the instance and creates a proper 132 | # hash structure, required by Parse, in order to process the push notification. 133 | # @return [Hash] the prepared push payload to be used in the request. 134 | def payload 135 | msg = { 136 | data: { 137 | alert: alert, 138 | badge: badge || "Increment", 139 | }, 140 | } 141 | msg[:data][:sound] = sound if sound.present? 142 | msg[:data][:title] = title if title.present? 143 | msg[:data].merge! @data if @data.is_a?(Hash) 144 | 145 | if @expiration_time.present? 146 | msg[:expiration_time] = @expiration_time.respond_to?(:iso8601) ? @expiration_time.iso8601(3) : @expiration_time 147 | end 148 | if @push_time.present? 149 | msg[:push_time] = @push_time.respond_to?(:iso8601) ? @push_time.iso8601(3) : @push_time 150 | end 151 | 152 | if @expiration_interval.is_a?(Numeric) 153 | msg[:expiration_interval] = @expiration_interval.to_i 154 | end 155 | 156 | if query.where.present? 157 | q = @query.dup 158 | if @channels.is_a?(Array) && @channels.empty? == false 159 | q.where :channels.in => @channels 160 | end 161 | msg[:where] = q.compile_where unless q.where.empty? 162 | elsif @channels.is_a?(Array) && @channels.empty? == false 163 | msg[:channels] = @channels 164 | end 165 | msg 166 | end 167 | 168 | # helper method to send a message 169 | # @param message [String] the message to send 170 | def send(message = nil) 171 | @alert = message if message.is_a?(String) 172 | @data = message if message.is_a?(Hash) 173 | client.push(payload.as_json) 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/parse/model/shortnames.rb: -------------------------------------------------------------------------------- 1 | require_relative "object" 2 | 3 | # Simple include to use short verion of core class names 4 | ::Installation = Parse::Installation unless defined?(::Installation) 5 | ::Role = Parse::Role unless defined?(::Role) 6 | ::Product = Parse::Product unless defined?(::Product) 7 | ::Session = Parse::Session unless defined?(::Session) 8 | ::User = Parse::User unless defined?(::User) 9 | -------------------------------------------------------------------------------- /lib/parse/model/time_zone.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/values/time_zone" 6 | require_relative "model" 7 | 8 | module Parse 9 | # This class a wrapper around ActiveSupport::TimeZone when using Parse columns that 10 | # store IANA time zone identifiers (ex. Installation collection). Parse does not have a 11 | # native time zone data type, but this class is provided to manage and perform timezone-like 12 | # operation on those properties which you have marked as type _:timezone_. 13 | # 14 | # When declaring a property of type :timezone, you may also define a default just like 15 | # any other property. In addition, the framework will automatically add a validation 16 | # to make sure that your property is either nil or one of the valid IANA time zone identifiers. 17 | # 18 | # Each instance of {Parse::TimeZone} has a {Parse::TimeZone#zone} attribute that provides access to 19 | # the underlying {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html ActiveSupport::TimeZone} 20 | # instance, which you can use to perform time zone operations. 21 | # @example 22 | # class Event < Parse::Object 23 | # # an event occurs in a time zone. 24 | # property :time_zone, :timezone, default: 'America/Los_Angeles' 25 | # end 26 | # 27 | # event = Event.new 28 | # event.time_zone.name # => 'America/Los_Angeles' 29 | # event.time_zone.valid? # => true 30 | # 31 | # event.time_zone.zone # => ActiveSupport::TimeZone 32 | # event.time_zone.formatted_offset # => "-08:00" 33 | # 34 | # event.time_zone = 'Europe/Paris' 35 | # event.time_zone.formatted_offset # => +01:00" 36 | # 37 | # event.time_zone = 'Galaxy/Andromeda' 38 | # event.time_zone.valid? # => false 39 | # @version 1.7.1 40 | class TimeZone 41 | # The mapping of TimeZones 42 | MAPPING = ActiveSupport::TimeZone::MAPPING 43 | 44 | # Create methods based on the allowable public methods on ActiveSupport::TimeZone. 45 | # Basically sets up sending forwarding calls to the `zone` object for a Parse::TimeZone object. 46 | (ActiveSupport::TimeZone.public_instance_methods(false) - [:to_s, :name, :as_json]).each do |meth| 47 | Parse::TimeZone.class_eval do 48 | define_method meth do |*args| 49 | zone.send meth, *args 50 | end 51 | end 52 | end 53 | 54 | # Creates a new instance given the IANA identifier (ex. America/Los_Angeles) 55 | # @overload new(iana) 56 | # @param iana [String] the IANA identifier (ex. America/Los_Angeles) 57 | # @return [Parse::TimeZone] 58 | # @overload new(timezone) 59 | # You can instantiate a new instance with either a {Parse::TimeZone} or an {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html ActiveSupport::TimeZone} 60 | # object. 61 | # @param timezone [Parse::TimeZone|ActiveSupport::TimeZone] an instance of either timezone class. 62 | # @return [Parse::TimeZone] 63 | def initialize(iana) 64 | if iana.is_a?(String) 65 | @name = iana 66 | @zone = nil 67 | elsif iana.is_a?(::Parse::TimeZone) 68 | @zone = iana.zone 69 | @name = nil 70 | elsif iana.is_a?(::ActiveSupport::TimeZone) 71 | @zone = iana 72 | @name = nil 73 | end 74 | end 75 | 76 | # @!attribute [rw] name 77 | # @raise ArgumentError if value is not a string type. 78 | # @return [String] the IANA identifier for this time zone. 79 | def name 80 | @zone.present? ? zone.name : @name 81 | end 82 | 83 | def name=(iana) 84 | unless iana.nil? || iana.is_a?(String) 85 | raise ArgumentError, "Parse::TimeZone#name should be an IANA time zone identifier." 86 | end 87 | @name = iana 88 | @zone = nil 89 | end 90 | 91 | # @!attribute [rw] zone 92 | # Returns an instance of {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html ActiveSupport::TimeZone} 93 | # based on the IANA identifier. The setter may allow usign an IANA string identifier, 94 | # a {Parse::TimeZone} or an 95 | # {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html ActiveSupport::TimeZone} 96 | # object. 97 | # @see #name 98 | # @raise ArgumentError 99 | # @return [ActiveSupport::TimeZone] 100 | def zone 101 | # lazy load the TimeZone object only when the user requests it, otherwise 102 | # just keep the name of the string around. Makes encoding/decoding faster. 103 | if @zone.nil? && @name.present? 104 | @zone = ::ActiveSupport::TimeZone.new(@name) 105 | @name = nil # clear out the cache 106 | end 107 | @zone 108 | end 109 | 110 | def zone=(timezone) 111 | if timezone.is_a?(::ActiveSupport::TimeZone) 112 | @zone = timezone 113 | @name = nil 114 | elsif timezone.is_a?(Parse::TimeZone) 115 | @name = timezone.name 116 | @zone = nil 117 | elsif timezone.nil? || timezone.is_a?(String) 118 | @name = timezone 119 | @zone = nil 120 | else 121 | raise ArgumentError, "Invalid value passed to Parse::TimeZone#zone." 122 | end 123 | end 124 | 125 | # (see #to_s) 126 | def as_json(*args) 127 | name 128 | end 129 | 130 | # @return [String] the IANA identifier for this timezone or nil. 131 | def to_s 132 | name 133 | end 134 | 135 | # Returns true or false whether the time zone exists in the {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html ActiveSupport::TimeZone} mapping. 136 | # @return [Bool] true if it contains a valid time zone 137 | def valid? 138 | ActiveSupport::TimeZone[to_s].present? 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/parse/query/constraint.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "operation" 5 | require "time" 6 | require "date" 7 | 8 | module Parse 9 | # Constraints are the heart of the Parse::Query system. 10 | # Each constraint is made up of an Operation and a value (the right side 11 | # of an operator). Constraints are responsible for making their specific 12 | # Parse hash format required when sending Queries to Parse. All constraints can 13 | # be combined by merging different constraints (since they are multiple hashes) 14 | # and some constraints may have higher precedence than others (ex. equality is higher 15 | # precedence than an "in" query). 16 | # 17 | # All constraints should inherit from Parse::Constraint and should 18 | # register their specific Operation method (ex. :eq or :lte) 19 | # For more information about the query design pattern from DataMapper 20 | # that inspired this, see http://datamapper.org/docs/find.html 21 | class Constraint 22 | 23 | # @!attribute operation 24 | # The operation tied to this constraint. 25 | # @return [Parse::Operation] 26 | 27 | # @!attribute value 28 | # The value to be applied to this constraint. 29 | # @return [Parse::Operation] 30 | 31 | attr_accessor :operation, :value 32 | 33 | # Create a new constraint. 34 | # @param operation [Parse::Operation] the operation for this constraint. 35 | # @param value [Object] the value to attach to this constraint. 36 | # @yield You may also pass a block to modify the operation or value. 37 | def initialize(operation, value) 38 | # if the first parameter is not an Operation, but it is a symbol 39 | # it most likely is just the field name, so let's assume they want 40 | # the default equality operation. 41 | if operation.is_a?(Operation) == false && operation.respond_to?(:to_sym) 42 | operation = Operation.new(operation.to_sym, self.class.operand) 43 | end 44 | @operation = operation 45 | @value = value 46 | yield(self) if block_given? 47 | end 48 | 49 | class << self 50 | # @!attribute key 51 | # The class attributes keep track of the Parse key (special Parse 52 | # text symbol representing this operation. Ex. local method could be called 53 | # .ex, where the Parse Query operation that should be sent out is "$exists") 54 | # in this case, key should be set to "$exists" 55 | # @return [Symbol] 56 | attr_accessor :key 57 | 58 | # @!attribute precedence 59 | # Precedence defines the priority of this operation when merging. 60 | # The higher the more priority it will receive. 61 | # @return [Integer] 62 | attr_accessor :precedence 63 | 64 | # @!attribute operand 65 | # @return [Symbol] the operand for this constraint. 66 | attr_accessor :operand 67 | 68 | # Creates a new constraint given an operation and value. 69 | def create(operation, value) 70 | #default to a generic equality constraint if not passed an operation 71 | unless operation.is_a?(Parse::Operation) && operation.valid? 72 | return self.new(operation, value) 73 | end 74 | operation.constraint(value) 75 | end 76 | 77 | # Set the keyword for this Constaint. Subclasses should use this method. 78 | # @param keyword [Symbol] 79 | # @return (see key) 80 | def contraint_keyword(keyword) 81 | @key = keyword 82 | end 83 | 84 | # Set the default precedence for this constraint. 85 | # @param priority [Integer] a higher priority has higher precedence 86 | # @return [Integer] 87 | def precedence(priority = nil) 88 | @precedence = 0 if @precedence.nil? 89 | @precedence = priority unless priority.nil? 90 | @precedence 91 | end 92 | 93 | # Register the given operand for this Parse::Constraint subclass. 94 | # @note All subclasses should register their operation and themselves. 95 | # @param op [Symbol] the operand 96 | # @param klass [Parse::Constraint] a subclass of Parse::Constraint 97 | # @return (see Parse::Operation.register) 98 | def register(op, klass = self) 99 | self.operand ||= op 100 | Operation.register op, klass 101 | end 102 | 103 | # @return [Object] a formatted value based on the data type. 104 | def formatted_value(value) 105 | d = value 106 | d = { __type: Parse::Model::TYPE_DATE, iso: d.utc.iso8601(3) } if d.respond_to?(:utc) 107 | # if it responds to parse_date (most likely a time/date object), then call the conversion 108 | d = d.parse_date if d.respond_to?(:parse_date) 109 | # if it's not a Parse::Date, but still responds to iso8601, then do it manually 110 | if d.is_a?(Parse::Date) == false && d.respond_to?(:iso8601) 111 | d = { __type: Parse::Model::TYPE_DATE, iso: d.iso8601(3) } 112 | end 113 | d = d.pointer if d.respond_to?(:pointer) #simplified query object 114 | d = d.to_s if d.is_a?(Regexp) 115 | # d = d.pointer if d.is_a?(Parse::Object) #simplified query object 116 | # d = d.compile 117 | if d.is_a?(Parse::Query) 118 | compiled = d.compile(encode: false, includeClassName: true) 119 | # compiled["className"] = d.table 120 | d = compiled 121 | end 122 | d 123 | end 124 | end 125 | 126 | # @return [Integer] the precedence of this constraint 127 | def precedence 128 | self.class.precedence 129 | end 130 | 131 | # @return [Symbol] the Parse keyword for this constraint. 132 | def key 133 | self.class.key 134 | end 135 | 136 | # @!attribute operand 137 | # @return [Symbol] the operand for the operation. 138 | def operand 139 | @operation.operand unless @operation.nil? 140 | end 141 | 142 | def operand=(o) 143 | @operation.operand = o unless @operation.nil? 144 | end 145 | 146 | # @!attribute operator 147 | # @return [Symbol] the operator for the operation. 148 | def operator 149 | @operation.operator unless @operation.nil? 150 | end 151 | 152 | def operator=(o) 153 | @operation.operator = o unless @operation.nil? 154 | end 155 | 156 | # @!visibility private 157 | def inspect 158 | "<#{self.class} #{operator.to_s}(#{operand.inspect}, `#{value}`)>" 159 | end 160 | 161 | # Calls build internally 162 | # @return [Hash] 163 | def as_json(*args) 164 | build 165 | end 166 | 167 | # Builds the JSON hash representation of this constraint for a Parse query. 168 | # This method should be overriden by subclasses. The default implementation 169 | # implements buildling the equality constraint. 170 | # @raise ArgumentError if the constraint could be be build due to a bad parameter. 171 | # This will be different depending on the constraint subclass. 172 | # @return [Hash] 173 | def build 174 | return { @operation.operand => formatted_value } if @operation.operator == :eq || key.nil? 175 | { @operation.operand => { key => formatted_value } } 176 | end 177 | 178 | # @return [String] string representation of this constraint. 179 | def to_s 180 | inspect 181 | end 182 | 183 | # @return [Object] formatted value based on the specific data type. 184 | def formatted_value 185 | self.class.formatted_value(@value) 186 | end 187 | 188 | # Registers the default constraint of equality 189 | register :eq, Constraint 190 | precedence 100 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/parse/query/operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/inflector" 6 | 7 | module Parse 8 | 9 | # An operation is the core part of {Parse::Constraint} when performing 10 | # queries. It contains an operand (the Parse field) and an operator (the Parse 11 | # operation). These combined with a value, provide you with a constraint. 12 | # 13 | # All operation registrations add methods to the Symbol class. 14 | class Operation 15 | 16 | # @!attribute operand 17 | # The field in Parse for this operation. 18 | # @return [Symbol] 19 | attr_accessor :operand 20 | 21 | # @!attribute operator 22 | # The type of Parse operation. 23 | # @return [Symbol] 24 | attr_accessor :operator 25 | 26 | class << self 27 | # @return [Hash] a hash containing all supported Parse operations mapped 28 | # to their {Parse::Constraint} subclass. 29 | attr_accessor :operators 30 | 31 | def operators 32 | @operators ||= {} 33 | end 34 | end 35 | 36 | # Whether this operation is defined properly. 37 | def valid? 38 | !(@operand.nil? || @operator.nil? || handler.nil?) 39 | end 40 | 41 | # @return [Parse::Constraint] the constraint class designed to handle 42 | # this operator. 43 | def handler 44 | Operation.operators[@operator] unless @operator.nil? 45 | end 46 | 47 | # Create a new operation. 48 | # @param field [Symbol] the name of the Parse field 49 | # @param op [Symbol] the operator name (ex. :eq, :lt) 50 | def initialize(field, op) 51 | self.operand = field.to_sym 52 | self.operand = :objectId if operand == :id 53 | self.operator = op.to_sym 54 | end 55 | 56 | # @!visibility private 57 | def inspect 58 | "#{operator.inspect}(#{operand.inspect})" 59 | end 60 | 61 | # Create a new constraint based on the handler that had 62 | # been registered with this operation. 63 | # @param value [Object] a value to pass to the constraint subclass. 64 | # @return [Parse::Constraint] a constraint with this operation and value. 65 | def constraint(value = nil) 66 | handler.new(self, value) 67 | end 68 | 69 | # Register a new symbol operator method mapped to a specific {Parse::Constraint}. 70 | def self.register(op, klass) 71 | Operation.operators[op.to_sym] = klass 72 | Symbol.send :define_method, op do |value = nil| 73 | operation = Operation.new self, op 74 | value.nil? ? operation : operation.constraint(value) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/parse/query/ordering.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | # This class adds support for describing ordering for Parse queries. You can 6 | # either order by ascending (asc) or descending (desc) order. 7 | # 8 | # Ordering is implemented similarly to constraints in which we add 9 | # special methods to the Symbol class. The developer can then pass one 10 | # or an array of fields (as symbols) and call the particular ordering 11 | # polarity (ex. _:name.asc_ would create a Parse::Order where we want 12 | # things to be sortd by the name field in ascending order) 13 | # For more information about the query design pattern from DataMapper 14 | # that inspired this, see http://datamapper.org/docs/find.html' 15 | # @example 16 | # :name.asc # => Parse::Order by ascending :name 17 | # :like_count.desc # => Parse::Order by descending :like_count 18 | # 19 | class Order 20 | # The Parse operators to indicate ordering direction. 21 | ORDERING = { asc: "", desc: "-" }.freeze 22 | 23 | # @!attribute [rw] field 24 | # @return [Symbol] the name of the field 25 | attr_accessor :field 26 | 27 | # @!attribute [rw] direction 28 | # The direction of the sorting. This is either `:asc` or `:desc`. 29 | # @return [Symbol] 30 | attr_accessor :direction 31 | 32 | def initialize(field, order = :asc) 33 | @field = field.to_sym || :objectId 34 | @direction = order 35 | end 36 | 37 | def field=(f) 38 | @field = f.to_sym 39 | end 40 | 41 | # @return [String] the sort direction 42 | def polarity 43 | ORDERING[@direction] || ORDERING[:asc] 44 | end # polarity 45 | 46 | # @return [String] the ordering as a string 47 | def to_s 48 | "" if @field.nil? 49 | polarity + @field.to_s 50 | end 51 | 52 | # @!visibility private 53 | def inspect 54 | "#{@direction.to_s}(#{@field.inspect})" 55 | end 56 | end # Order 57 | end 58 | 59 | # Extension to add all the operator instance methods to the Symbol classe 60 | class Symbol 61 | Parse::Order::ORDERING.keys.each do |sym| 62 | define_method(sym) do 63 | Parse::Order.new self, sym 64 | end 65 | end # each 66 | end 67 | -------------------------------------------------------------------------------- /lib/parse/stack.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "stack/version" 5 | require_relative "client" 6 | require_relative "query" 7 | require_relative "model/object" 8 | require_relative "webhooks" 9 | 10 | module Parse 11 | class Error < StandardError; end 12 | 13 | module Stack 14 | end 15 | 16 | # Special class to support Modernistik Hyperdrive server. 17 | class Hyperdrive 18 | # Applies a remote JSON hash containing the ENV keys and values from a remote 19 | # URL. Values from the JSON hash are only applied to the current ENV hash ONLY if 20 | # it does not already have a value. Therefore local ENV values will take precedence 21 | # over remote ones. By default, it uses the url in environment value in 'CONFIG_URL' or 'HYPERDRIVE_URL'. 22 | # @param url [String] the remote url that responds with the JSON body. 23 | # @return [Boolean] true if the JSON hash was found and applied successfully. 24 | def self.config!(url = nil) 25 | url ||= ENV["HYPERDRIVE_URL"] || ENV["CONFIG_URL"] 26 | if url.present? 27 | begin 28 | remote_config = JSON.load open(url) 29 | remote_config.each do |key, value| 30 | k = key.upcase 31 | next unless ENV[k].nil? 32 | ENV[k] ||= value.to_s 33 | end 34 | return true 35 | rescue => e 36 | warn "[Parse::Stack] Error loading config: #{url} (#{e})" 37 | end 38 | end 39 | false 40 | end 41 | end 42 | end 43 | 44 | require_relative "stack/railtie" if defined?(::Rails) 45 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/rails.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "parse/stack" 5 | require "parse/stack/tasks" 6 | require "rails/generators" 7 | require "rails/generators/named_base" 8 | 9 | # Module namespace to show up in the generators list for Rails. 10 | module ParseStack 11 | # Adds support for rails when installing Parse::Stack to a Rails project. 12 | class InstallGenerator < Rails::Generators::Base 13 | source_root File.expand_path("../templates", __FILE__) 14 | 15 | desc "This generator creates an initializer file at config/initializers" 16 | # @!visibility private 17 | def generate_initializer 18 | copy_file "parse.rb", "config/initializers/parse.rb" 19 | copy_file "model_user.rb", File.join("app/models", "user.rb") 20 | copy_file "model_role.rb", File.join("app/models", "role.rb") 21 | copy_file "model_session.rb", File.join("app/models", "session.rb") 22 | copy_file "model_installation.rb", File.join("app/models", "installation.rb") 23 | copy_file "webhooks.rb", File.join("app/models", "webhooks.rb") 24 | end 25 | end 26 | 27 | # @!visibility private 28 | class ModelGenerator < Rails::Generators::NamedBase 29 | source_root File.expand_path(__dir__ + "/templates") 30 | desc "Creates a Parse::Object model subclass." 31 | argument :attributes, type: :array, default: [], banner: "field:type field:type" 32 | check_class_collision 33 | 34 | # @!visibility private 35 | def create_model_file 36 | @allowed_types = Parse::Properties::TYPES - [:acl, :id, :relation] 37 | template "model.erb", File.join("app/models", class_path, "#{file_name}.rb") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/model.erb: -------------------------------------------------------------------------------- 1 | 2 | class <%= class_name %> < Parse::Object 3 | # See: https://github.com/modernistik/parse-stack#defining-properties 4 | 5 | # You can change the inferred Parse table/collection name below 6 | # parse_class "<%= class_name.to_s.to_parse_class %>" 7 | <% attributes.each do |attr| 8 | parse_type = attr.type.to_s.downcase.to_sym 9 | unless @allowed_types.include?(parse_type) 10 | puts "\n[Warning] Skipping property `#{attr.name}` with type `#{parse_type}`. Type should be one of #{@allowed_types}." 11 | next 12 | end %> 13 | property :<%= attr.name %>, :<%= parse_type -%> 14 | <% end %> 15 | 16 | # See: https://github.com/modernistik/parse-stack#cloud-code-webhooks 17 | # define a before save webhook for <%= class_name %> 18 | webhook :before_save do 19 | <%= class_name.to_s.underscore %> = parse_object 20 | # perform any validations with <%= class_name.to_s.underscore %> 21 | # use `error!(msg)` to fail the save 22 | # ... 23 | <%= class_name.to_s.underscore %> 24 | end 25 | 26 | ## define an after save webhook for <%= class_name %> 27 | # 28 | # webhook :after_save do 29 | # <%= class_name.to_s.underscore %> = parse_object 30 | # 31 | # end 32 | 33 | ## define a before delete webhook for <%= class_name %> 34 | # webhook :before_delete do 35 | # <%= class_name.to_s.underscore %> = parse_object 36 | # # use `error!(msg)` to fail the delete 37 | # true # allow the deletion 38 | # end 39 | 40 | ## define an after delete webhook for <%= class_name %> 41 | # webhook :after_delete do 42 | # <%= class_name.to_s.underscore %> = parse_object 43 | # end 44 | 45 | ## Example of a CloudCode Webhook function 46 | ## define a `helloWorld` Parse CloudCode function 47 | # webhook :function, :helloWorld do 48 | # "Hello!" 49 | # end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/model_installation.rb: -------------------------------------------------------------------------------- 1 | class Parse::Installation < Parse::Object 2 | # See: https://github.com/modernistik/parse-stack#parseinstallation 3 | # add additional properties here 4 | end 5 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/model_role.rb: -------------------------------------------------------------------------------- 1 | class Parse::Role < Parse::Object 2 | # See: https://github.com/modernistik/parse-stack#parserole 3 | # add additional properties here 4 | end 5 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/model_session.rb: -------------------------------------------------------------------------------- 1 | class Parse::Session < Parse::Object 2 | # See: https://github.com/modernistik/parse-stack#parsesession 3 | # add additional properties here 4 | end 5 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/model_user.rb: -------------------------------------------------------------------------------- 1 | class Parse::User < Parse::Object 2 | # add additional properties 3 | 4 | # define a before save webhook for Parse::User 5 | # webhook :before_save do 6 | # obj = parse_object # Parse::User 7 | # # make changes to record.... 8 | # obj # will send the proper changelist back to Parse-Server 9 | # end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/parse.rb: -------------------------------------------------------------------------------- 1 | require "parse/stack" 2 | 3 | # Set your specific Parse keys in your ENV. For all connection options, see 4 | # https://github.com/modernistik/parse-stack#connection-setup 5 | Parse.setup app_id: ENV["PARSE_SERVER_APPLICATION_ID"], 6 | api_key: ENV["PARSE_SERVER_REST_API_KEY"], 7 | master_key: ENV["PARSE_SERVER_MASTER_KEY"], # optional 8 | server_url: "https://localhost:1337/parse" 9 | # optional 10 | # logging: false, 11 | # cache: Moneta.new(:File, dir: 'tmp/cache'), 12 | # expires: 1 # cache ttl 1 second 13 | -------------------------------------------------------------------------------- /lib/parse/stack/generators/templates/webhooks.rb: -------------------------------------------------------------------------------- 1 | # See: https://github.com/modernistik/parse-stack#cloud-code-webhooks 2 | Parse::Webhooks.route(:function, :helloWorld) do 3 | # use the Parse::Payload instance methods in this block 4 | name = params["name"].to_s #function params 5 | 6 | # will return proper error response 7 | # error!("Missing argument 'name'.") unless name.present? 8 | 9 | name.present? ? "Hello #{name}!" : "Hello World!" 10 | end 11 | -------------------------------------------------------------------------------- /lib/parse/stack/railtie.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | module Stack 6 | # Support for adding rake tasks to a Rails project. 7 | class Railtie < ::Rails::Railtie 8 | rake_tasks do 9 | require_relative "tasks" 10 | Parse::Stack.load_tasks 11 | end 12 | 13 | generators do 14 | require_relative "generators/rails" 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/parse/stack/tasks.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require_relative "../stack.rb" 5 | require "active_support" 6 | require "active_support/inflector" 7 | require "active_support/core_ext" 8 | require "rake" 9 | require "rake/dsl_definition" 10 | 11 | module Parse 12 | module Stack 13 | # Loads and installs all Parse::Stack related tasks in a rake file. 14 | def self.load_tasks 15 | Parse::Stack::Tasks.new.install_tasks 16 | end 17 | 18 | # Defines all the related Rails tasks for Parse. 19 | class Tasks 20 | include Rake::DSL if defined? Rake::DSL 21 | 22 | # Installs the rake tasks. 23 | def install_tasks 24 | if defined?(::Rails) 25 | unless Rake::Task.task_defined?("db:seed") || Rails.root.blank? 26 | namespace :db do 27 | desc "Seeds your database with by loading db/seeds.rb" 28 | task :seed => "parse:env" do 29 | load Rails.root.join("db", "seeds.rb") 30 | end 31 | end 32 | end 33 | end 34 | 35 | namespace :parse do 36 | task :env do 37 | if Rake::Task.task_defined?("environment") 38 | Rake::Task["environment"].invoke 39 | if defined?(::Rails) 40 | Rails.application.eager_load! if Rails.application.present? 41 | end 42 | end 43 | end 44 | 45 | task :verify_env => :env do 46 | unless Parse::Client.client? 47 | raise "Please make sure you have setup the Parse.setup configuration before invoking task. Usually done in the :environment task." 48 | end 49 | 50 | endpoint = ENV["HOOKS_URL"] || "" 51 | unless endpoint.starts_with?("http://") || endpoint.starts_with?("https://") 52 | raise "The ENV variable HOOKS_URL must be a url : '#{endpoint}'. Ex. https://12345678.ngrok.io/webhooks" 53 | end 54 | end 55 | 56 | desc "Run auto_upgrade on all of your Parse models." 57 | task :upgrade => :env do 58 | puts "Auto Upgrading Parse schemas..." 59 | Parse.auto_upgrade! do |k| 60 | puts "[+] #{k}" 61 | end 62 | end 63 | 64 | namespace :webhooks do 65 | desc "Register local webhooks with Parse server" 66 | task :register => :verify_env do 67 | endpoint = ENV["HOOKS_URL"] 68 | puts "Registering Parse Webhooks @ #{endpoint}" 69 | Rake::Task["parse:webhooks:register:functions"].invoke 70 | Rake::Task["parse:webhooks:register:triggers"].invoke 71 | end 72 | 73 | desc "List all webhooks and triggers registered with the Parse Server" 74 | task :list => :verify_env do 75 | Rake::Task["parse:webhooks:list:functions"].invoke 76 | Rake::Task["parse:webhooks:list:triggers"].invoke 77 | end 78 | 79 | desc "Remove all locally registered webhooks from the Parse Application." 80 | task :remove => :verify_env do 81 | Rake::Task["parse:webhooks:remove:functions"].invoke 82 | Rake::Task["parse:webhooks:remove:triggers"].invoke 83 | end 84 | 85 | namespace :list do 86 | task :functions => :verify_env do 87 | endpoint = ENV["HOOKS_URL"] || "-" 88 | Parse.client.functions.each do |r| 89 | name = r["functionName"] 90 | url = r["url"] 91 | star = url.starts_with?(endpoint) ? "*" : " " 92 | puts "[#{star}] #{name} -> #{url}" 93 | end 94 | end 95 | 96 | task :triggers => :verify_env do 97 | endpoint = ENV["HOOKS_URL"] || "-" 98 | triggers = Parse.client.triggers.results 99 | triggers.sort! { |x, y| [x["className"], x["triggerName"]] <=> [y["className"], y["triggerName"]] } 100 | triggers.each do |r| 101 | name = r["className"] 102 | trigger = r["triggerName"] 103 | url = r["url"] 104 | star = url.starts_with?(endpoint) ? "*" : " " 105 | puts "[#{star}] #{name}.#{trigger} -> #{url}" 106 | end 107 | end 108 | end 109 | 110 | namespace :register do 111 | task :functions => :verify_env do 112 | endpoint = ENV["HOOKS_URL"] 113 | Parse::Webhooks.register_functions!(endpoint) do |name| 114 | puts "[+] function - #{name}" 115 | end 116 | end 117 | 118 | task :triggers => :verify_env do 119 | endpoint = ENV["HOOKS_URL"] 120 | Parse::Webhooks.register_triggers!(endpoint, { include_wildcard: true }) do |trigger, name| 121 | puts "[+] #{trigger.to_s.ljust(12, " ")} - #{name}" 122 | end 123 | end 124 | end 125 | 126 | namespace :remove do 127 | task :functions => :verify_env do 128 | Parse::Webhooks.remove_all_functions! do |name| 129 | puts "[-] function - #{name}" 130 | end 131 | end 132 | 133 | task :triggers => :verify_env do 134 | Parse::Webhooks.remove_all_triggers! do |trigger, name| 135 | puts "[-] #{trigger.to_s.ljust(12, " ")} - #{name}" 136 | end 137 | end 138 | end 139 | end # webhooks 140 | end # webhooks namespace 141 | end 142 | end # Tasks 143 | end # Webhooks 144 | end # Parse 145 | -------------------------------------------------------------------------------- /lib/parse/stack/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | module Parse 5 | # @author Anthony Persaud 6 | # The Parse Server SDK for Ruby 7 | module Stack 8 | # The current version. 9 | VERSION = "1.9.1" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/parse/webhooks/registration.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | require "active_support" 5 | require "active_support/inflector" 6 | require "active_support/core_ext/object" 7 | require "active_support/core_ext/string" 8 | require "active_support/core_ext" 9 | 10 | module Parse 11 | # Interface to the CloudCode webhooks API. 12 | class Webhooks 13 | # Module to support registering Parse CloudCode webhooks. 14 | module Registration 15 | # The set of allowed trigger types. 16 | ALLOWED_HOOKS = Parse::API::Hooks::TRIGGER_NAMES + [:function] 17 | 18 | # removes all registered webhook functions with Parse Server. 19 | def remove_all_functions! 20 | client.functions.results.sort_by { |f| f["functionName"] }.each do |f| 21 | next unless f["url"].present? 22 | client.delete_function f["functionName"] 23 | yield(f["functionName"]) if block_given? 24 | end 25 | end 26 | 27 | # removes all registered webhook triggers with Parse Server. 28 | def remove_all_triggers! 29 | client.triggers.results.sort_by { |f| [f["triggerName"], f["className"]] }.each do |f| 30 | next unless f["url"].present? 31 | triggerName = f["triggerName"] 32 | className = f[Parse::Model::KEY_CLASS_NAME] 33 | client.delete_trigger triggerName, className 34 | yield(f["triggerName"], f[Parse::Model::KEY_CLASS_NAME]) if block_given? 35 | end 36 | end 37 | 38 | # Registers all webhook functions registered with Parse::Stack with Parse server. 39 | # @param endpoint [String] a https url that points to the webhook server. 40 | def register_functions!(endpoint) 41 | unless endpoint.present? && (endpoint.starts_with?("http://") || endpoint.starts_with?("https://")) 42 | raise ArgumentError, "The HOOKS_URL must be http/s: '#{endpoint}''" 43 | end 44 | endpoint += "/" unless endpoint.ends_with?("/") 45 | functionsMap = {} 46 | client.functions.results.each do |f| 47 | next unless f["url"].present? 48 | functionsMap[f["functionName"]] = f["url"] 49 | end 50 | 51 | routes.function.keys.sort.each do |functionName| 52 | url = endpoint + functionName 53 | if functionsMap[functionName].present? #you may need to update 54 | next if functionsMap[functionName] == url 55 | client.update_function(functionName, url) 56 | else 57 | client.create_function(functionName, url) 58 | end 59 | yield(functionName) if block_given? 60 | end 61 | end 62 | 63 | # Registers all webhook triggers registered with Parse::Stack with Parse server. 64 | # @param endpoint [String] a https url that points to the webhook server. 65 | # @param include_wildcard [Boolean] Allow wildcard registrations 66 | def register_triggers!(endpoint, include_wildcard: false) 67 | unless endpoint.present? && (endpoint.starts_with?("http://") || endpoint.starts_with?("https://")) 68 | raise ArgumentError, "The HOOKS_URL must be http/s: '#{endpoint}''" 69 | end 70 | endpoint += "/" unless endpoint.ends_with?("/") 71 | all_triggers = Parse::API::Hooks::TRIGGER_NAMES_LOCAL 72 | 73 | current_triggers = {} 74 | all_triggers.each { |t| current_triggers[t] = {} } 75 | 76 | client.triggers.each do |t| 77 | next unless t["url"].present? 78 | trigger_name = t["triggerName"].underscore.to_sym 79 | current_triggers[trigger_name] ||= {} 80 | current_triggers[trigger_name][t["className"]] = t["url"] 81 | end 82 | 83 | all_triggers.each do |trigger| 84 | classNames = routes[trigger].keys.dup 85 | if include_wildcard && classNames.include?("*") #then create the list for all classes 86 | classNames.delete "*" #delete the wildcard before we expand it 87 | classNames = classNames + Parse.registered_classes 88 | classNames.uniq! 89 | end 90 | 91 | classNames.sort.each do |className| 92 | next if className == "*" 93 | url = endpoint + "#{trigger}/#{className}" 94 | if current_triggers[trigger][className].present? #then you may need to update 95 | next if current_triggers[trigger][className] == url 96 | client.update_trigger(trigger, className, url) 97 | else 98 | client.create_trigger(trigger, className, url) 99 | end 100 | yield(trigger.columnize, className) if block_given? 101 | end 102 | end 103 | end 104 | 105 | # Registers a webhook trigger with a given endpoint url. 106 | # @param trigger [Symbol] Trigger type based on Parse::API::Hooks::TRIGGER_NAMES or :function. 107 | # @param name [String] the name of the webhook. 108 | # @param url [String] the https url endpoint that will handle the request. 109 | # @see Parse::API::Hooks::TRIGGER_NAMES 110 | def register_webhook!(trigger, name, url) 111 | trigger = trigger.to_s.camelize(:lower).to_sym 112 | raise ArgumentError, "Invalid hook trigger #{trigger}" unless ALLOWED_HOOKS.include?(trigger) 113 | if trigger == :function 114 | response = client.fetch_function(name) 115 | # if it is either an error (which has no results) or there is a result but 116 | # no registered item with a URL (which implies either none registered or only cloud code registered) 117 | # then create it. 118 | if response.results.none? { |d| d.has_key?("url") } 119 | response = client.create_function(name, url) 120 | else 121 | # update it 122 | response = client.update_function(name, url) 123 | end 124 | warn "Webhook Registration warning: #{response.result["warning"]}" if response.result.has_key?("warning") 125 | warn "Failed to register Cloud function #{name} with #{url}" if response.error? 126 | return response 127 | else # must be trigger 128 | response = client.fetch_trigger(trigger, name) 129 | # if it is either an error (which has no results) or there is a result but 130 | # no registered item with a URL (which implies either none registered or only cloud code registered) 131 | # then create it. 132 | if response.results.none? { |d| d.has_key?("url") } 133 | # create it 134 | response = client.create_trigger(trigger, name, url) 135 | else 136 | # update it 137 | response = client.update_trigger(trigger, name, url) 138 | end 139 | 140 | warn "Webhook Registration warning: #{response.result["warning"]}" if response.result.has_key?("warning") 141 | warn "Webhook Registration error: #{response.error}" if response.error? 142 | return response 143 | end 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /parse-stack.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "parse/stack/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "parse-stack" 8 | spec.version = Parse::Stack::VERSION 9 | spec.authors = ["Anthony Persaud"] 10 | spec.email = ["persaud@modernistik.com"] 11 | 12 | spec.summary = %q{Parse Server Ruby Client SDK} 13 | spec.description = %q{Parse Server Ruby Client. Perform Object-relational mapping between Parse Server and Ruby classes, with authentication, cloud code webhooks, push notifications and more built in.} 14 | spec.homepage = "https://github.com/modernistik/parse-stack" 15 | spec.license = "MIT" 16 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 17 | # delete this section to allow pushing this gem to any host. 18 | # if spec.respond_to?(:metadata) 19 | # spec.metadata['allowed_push_host'] = "http://www.modernistik.com" 20 | # else 21 | # raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 22 | # end 23 | 24 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | spec.bindir = "bin" 26 | spec.executables = ["parse-console"] #spec.files.grep(%r{^bin/pstack/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | spec.required_ruby_version = ">= 2.6" 29 | 30 | spec.add_runtime_dependency "activemodel", [">= 5", "< 7"] 31 | spec.add_runtime_dependency "active_model_serializers", [">= 0.9", "< 1"] 32 | spec.add_runtime_dependency "activesupport", [">= 5", "< 7"] 33 | spec.add_runtime_dependency "parallel", [">= 1.6", "< 2"] 34 | spec.add_runtime_dependency "faraday", "< 1" 35 | spec.add_runtime_dependency "faraday_middleware", [">= 0.9", "< 2"] 36 | spec.add_runtime_dependency "moneta", "< 2" 37 | spec.add_runtime_dependency "rack", ">= 2.0.6", "< 3" 38 | 39 | # spec.post_install_message = < :string, 6 | :created_at => :date, 7 | :updated_at => :date, 8 | :acl => :acl, 9 | :objectId => :string, 10 | :createdAt => :date, 11 | :updatedAt => :date, 12 | :ACL => :acl, 13 | :gcm_sender_id => :string, 14 | :GCMSenderId => :string, 15 | :app_identifier => :string, 16 | :appIdentifier => :string, 17 | :app_name => :string, 18 | :appName => :string, 19 | :app_version => :string, 20 | :appVersion => :string, 21 | :badge => :integer, 22 | :channels => :array, 23 | :device_token => :string, 24 | :deviceToken => :string, 25 | :device_token_last_modified => :integer, 26 | :deviceTokenLastModified => :integer, 27 | :device_type => :string, 28 | :deviceType => :string, 29 | :installation_id => :string, 30 | :installationId => :string, 31 | :locale_identifier => :string, 32 | :localeIdentifier => :string, 33 | :parse_version => :string, 34 | :parseVersion => :string, 35 | :push_type => :string, 36 | :pushType => :string, 37 | :time_zone => :timezone, 38 | :timeZone => :timezone, 39 | }) 40 | 41 | def test_properties 42 | assert Parse::Installation < Parse::Object 43 | assert_equal CORE_FIELDS, Parse::Installation.fields 44 | assert_empty Parse::Installation.references 45 | assert_empty Parse::Installation.relations 46 | assert Parse::Installation.method_defined?(:session) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/lib/parse/models/pointer_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestPointer < Minitest::Test 4 | def setup 5 | @id = "theObjectId" 6 | @theClass = "_User" 7 | @pointer = Parse::Pointer.new(@theClass, @id) 8 | end 9 | 10 | def test_base_fields 11 | pointer = @pointer 12 | assert_equal Parse::Model::TYPE_POINTER, "Pointer" 13 | assert_respond_to pointer, :__type 14 | assert_equal pointer.__type, Parse::Model::TYPE_POINTER 15 | assert_respond_to pointer, :id 16 | assert_respond_to pointer, :objectId 17 | assert_equal pointer.id, @id 18 | assert_equal pointer.id, pointer.objectId 19 | 20 | assert_respond_to pointer, :className 21 | assert_respond_to pointer, :parse_class 22 | assert_equal pointer.parse_class, @theClass 23 | assert_equal pointer.parse_class, pointer.className 24 | assert pointer.pointer? 25 | refute pointer.fetched? 26 | # Create a new pointer from this pointer. They should still be equal. 27 | assert pointer == pointer.pointer 28 | assert pointer.present? 29 | end 30 | 31 | def test_json 32 | assert_equal @pointer.as_json, { :__type => Parse::Model::TYPE_POINTER, className: @theClass, objectId: @id }.as_json 33 | end 34 | 35 | def test_sig 36 | assert_equal @pointer.sig, "#{@theClass}##{@id}" 37 | end 38 | 39 | def test_array_objectIds 40 | assert_equal [@pointer.id], [@pointer].objectIds 41 | assert_equal [@pointer.id], [@pointer, 4, "junk", nil].objectIds 42 | assert_equal [], [4, "junk", nil].objectIds 43 | end 44 | 45 | def test_array_valid_parse_objects 46 | assert_equal [@pointer], [@pointer].valid_parse_objects 47 | assert_equal [@pointer], [@pointer, 4, "junk", nil].valid_parse_objects 48 | assert_equal [], [4, "junk", nil].valid_parse_objects 49 | end 50 | 51 | def test_array_parse_pointers 52 | assert_equal [@pointer], [@pointer].parse_pointers 53 | assert_equal [@pointer, @pointer], [@pointer, { className: "_User", objectId: @id }].parse_pointers 54 | assert_equal [@pointer, @pointer], [@pointer, { "className" => "_User", "objectId" => @id }].parse_pointers 55 | assert_equal [@pointer, @pointer], [nil, 4, "junk", { className: "_User", objectId: @id }, { "className" => "_User", "objectId" => @id }].parse_pointers 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/lib/parse/models/product_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestProduct < Minitest::Test 4 | CORE_FIELDS = Parse::Object.fields.merge({ 5 | :id => :string, 6 | :created_at => :date, 7 | :updated_at => :date, 8 | :acl => :acl, 9 | :objectId => :string, 10 | :createdAt => :date, 11 | :updatedAt => :date, 12 | :ACL => :acl, 13 | :download => :file, 14 | :download_name => :string, 15 | :downloadName => :string, 16 | :icon => :file, 17 | :order => :integer, 18 | :product_identifier => :string, 19 | :productIdentifier => :string, 20 | :subtitle => :string, 21 | :title => :string, 22 | }) 23 | 24 | def test_properties 25 | assert Parse::Product < Parse::Object 26 | assert_equal CORE_FIELDS, Parse::Product.fields 27 | assert_empty Parse::Product.references 28 | assert_empty Parse::Product.relations 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/lib/parse/models/property_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestPropertyTypesClass < Parse::Object; end 4 | 5 | class TestPropertyModule < Minitest::Test 6 | TYPES = [:string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :bytes, :object, :acl, :timezone].freeze 7 | # These are the base mappings of the remote field name types. 8 | BASE = { objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze 9 | # The list of properties that are part of all objects 10 | BASE_KEYS = [:id, :created_at, :updated_at].freeze 11 | # Default hash map of local attribute name to remote column name 12 | BASE_FIELD_MAP = { id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL }.freeze 13 | CORE_FIELD_DEFINITION = { id: :string, created_at: :date, updated_at: :date, acl: :acl, objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze 14 | 15 | def setup 16 | end 17 | 18 | def test_parse_object_definition 19 | assert_equal TYPES, Parse::Object::TYPES 20 | assert_equal BASE, Parse::Object::BASE 21 | assert_equal BASE_KEYS, Parse::Object::BASE_KEYS 22 | assert_equal BASE_FIELD_MAP, Parse::Object::BASE_FIELD_MAP 23 | assert_empty Parse::Object.references, "Parse::Object should not have core references." 24 | assert_empty Parse::Object.relations, "Parse::Object should not have core relations." 25 | assert_equal CORE_FIELD_DEFINITION, Parse::Object.fields, "Parse::Object should have core fields defined." 26 | end 27 | 28 | def test_property_types 29 | assert TestPropertyTypesClass < Parse::Object 30 | assert_equal Parse::Object::fields, TestPropertyTypesClass.fields, "Initial subclass should be same fields as Parse::Object" 31 | CORE_FIELD_DEFINITION.each do |key, type| 32 | assert_equal type, TestPropertyTypesClass.fields[key], "Type for core property '#{key}' should be :#{type}" 33 | end 34 | 35 | TYPES - [:id] 36 | end 37 | 38 | def test_redeclarations 39 | warn_level = $VERBOSE 40 | $VERBOSE = nil 41 | BASE_FIELD_MAP.flatten.each do |field| 42 | refute Parse::Object.property(field), "Should not allow redeclaring property #{field} field" 43 | end 44 | BASE_FIELD_MAP.flatten.each do |field| 45 | key = "f_#{field}" 46 | refute Parse::Object.property(key, field: "#{field}"), "Should not allow redeclaring alias '#{field}' field. (#{key})" 47 | end 48 | $VERBOSE = warn_level 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/lib/parse/models/role_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestRole < Minitest::Test 4 | CORE_FIELDS = Parse::Object.fields.merge({ 5 | :id => :string, 6 | :created_at => :date, 7 | :updated_at => :date, 8 | :acl => :acl, 9 | :objectId => :string, 10 | :createdAt => :date, 11 | :updatedAt => :date, 12 | :ACL => :acl, 13 | :name => :string, 14 | }) 15 | 16 | def test_properties 17 | assert Parse::Role < Parse::Object 18 | assert_equal CORE_FIELDS, Parse::Role.fields 19 | assert_empty Parse::Role.references 20 | assert_equal({ :roles => Parse::Model::CLASS_ROLE, :users => Parse::Model::CLASS_USER }, Parse::Role.relations) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/lib/parse/models/session_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestSession < Minitest::Test 4 | CORE_FIELDS = Parse::Object.fields.merge({ 5 | created_with: :object, 6 | createdWith: :object, 7 | expires_at: :date, 8 | expiresAt: :date, 9 | installation_id: :string, 10 | installationId: :string, 11 | restricted: :boolean, 12 | session_token: :string, 13 | sessionToken: :string, 14 | user: :pointer, 15 | }) 16 | 17 | def test_properties 18 | assert Parse::Session < Parse::Object 19 | assert_equal CORE_FIELDS, Parse::Session.fields 20 | assert_equal({ user: Parse::Model::CLASS_USER }, Parse::Session.references) 21 | assert_empty Parse::Session.relations 22 | # check association methods 23 | assert Parse::Session.method_defined?(:user) 24 | assert Parse::Session.method_defined?(:installation) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/lib/parse/models/subclass_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestSubclassing < Minitest::Test 4 | def setup 5 | Parse.use_shortnames! 6 | end 7 | 8 | def test_inheritance 9 | assert_equal Installation.superclass, Parse::Object 10 | assert_equal Role.superclass, Parse::Object 11 | assert_equal User.superclass, Parse::Object 12 | assert_equal Session.superclass, Parse::Object 13 | assert_equal Product.superclass, Parse::Object 14 | assert_equal Parse::Object.superclass, Parse::Pointer 15 | assert_equal Parse::Pointer.superclass, Parse::Model 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/lib/parse/models/timezone_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class MyTestTimeZone < Parse::Object; end 4 | 5 | class TestTimeZone < Minitest::Test 6 | def setup 7 | @la = "America/Los_Angeles" 8 | @paris = "Europe/Paris" 9 | @chicago = "America/Chicago" 10 | end 11 | 12 | def test_property_definition 13 | assert_nil MyTestTimeZone.fields[:timezone] 14 | MyTestTimeZone.property :time_zone, :timezone 15 | refute_nil MyTestTimeZone.fields[:time_zone] 16 | refute_nil MyTestTimeZone.fields[:timeZone] 17 | assert_equal MyTestTimeZone.fields[:time_zone], :timezone 18 | assert_equal MyTestTimeZone.fields[:timeZone], :timezone 19 | assert_equal MyTestTimeZone.attributes[:timeZone], :timezone 20 | end 21 | 22 | def test_activesupport_method_forwarding 23 | as_methods = ActiveSupport::TimeZone.public_instance_methods(false) 24 | ps_methods = Parse::TimeZone.public_instance_methods(false) 25 | assert_empty(as_methods - ps_methods) 26 | end 27 | 28 | def test_creation 29 | as_tz = ActiveSupport::TimeZone.new @la 30 | tz = Parse::TimeZone.new @la 31 | assert_equal tz.name, @la 32 | assert_equal tz.zone.name, as_tz.name 33 | assert_equal tz.zone, as_tz 34 | 35 | assert_raises(ArgumentError) { tz.name = 234234 } 36 | assert_raises(ArgumentError) { tz.name = DateTime.now } 37 | refute_raises(ArgumentError) { tz.name = @paris } 38 | assert_equal tz.name, @paris 39 | assert_equal tz.zone, ActiveSupport::TimeZone.new(@paris) 40 | refute_raises(ArgumentError) { tz.zone = @chicago } 41 | assert_equal tz.name, @chicago 42 | refute_raises(ArgumentError) { tz.zone = Parse::TimeZone.new(@la) } 43 | assert_equal tz.name, @la 44 | 45 | as_tz = ActiveSupport::TimeZone.new @paris 46 | refute_raises(ArgumentError) { tz.zone = as_tz } 47 | assert_equal tz.formatted_offset, as_tz.formatted_offset 48 | end 49 | 50 | def test_validation 51 | tz = Parse::TimeZone.new @la 52 | assert tz.valid? 53 | tz = Parse::TimeZone.new "Galaxy/Andromeda" 54 | refute tz.valid? 55 | tz.name = @chicago 56 | assert tz.valid? 57 | end 58 | 59 | def test_encoding 60 | tz = Parse::TimeZone.new @la 61 | assert_equal tz.name, tz.as_json 62 | assert_equal tz.name, tz.to_s 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/lib/parse/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestUser < Minitest::Test 4 | CORE_FIELDS = Parse::Object.fields.merge({ 5 | auth_data: :object, 6 | authData: :object, 7 | email: :string, 8 | password: :string, 9 | username: :string, 10 | }) 11 | 12 | def test_properties 13 | assert Parse::User < Parse::Object 14 | assert_equal CORE_FIELDS, Parse::User.fields 15 | assert_empty Parse::User.references 16 | assert_empty Parse::User.relations 17 | end 18 | 19 | def test_password_reset 20 | assert_equal Parse::User.request_password_reset(""), false 21 | assert_equal Parse::User.request_password_reset(" "), false 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/lib/parse/query/basic_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestQueryObject < Parse::Object 4 | parse_class "TestQueryObjectTableName" 5 | end 6 | 7 | class CommentObject < Parse::Object; end 8 | 9 | class TestParseQuery < Minitest::Test 10 | extend Minitest::Spec::DSL 11 | 12 | def setup 13 | @query = Parse::Query.new("Song") 14 | end 15 | 16 | def test_columnize 17 | assert_equal :MyColumnField.columnize, :myColumnField 18 | assert_equal "MyColumnField".columnize, "myColumnField" 19 | assert_equal :My_column_field.columnize, :myColumnField 20 | assert_equal "My_column_field".columnize, "myColumnField" 21 | assert_equal :testField.columnize, :testField 22 | assert_equal "testField".columnize, "testField" 23 | assert_equal :test_field.columnize, :testField 24 | assert_equal "test_field".columnize, "testField" 25 | end 26 | 27 | def test_field_formatter 28 | @query.clear :where 29 | @query.where :fan_count => 0, :playCount => 0, :ShareCount => 0, :' test_name ' => 1 30 | clause = { "fanCount" => 0, "playCount" => 0, "shareCount" => 0, "testName" => 1 } 31 | assert_equal clause, @query.compile_where 32 | Parse::Query.field_formatter = nil 33 | @query.clear :where 34 | @query.where :fan_count => 0, :playCount => 0, :ShareCount => 0, :' test_name ' => 1 35 | clause = { "fan_count" => 0, "playCount" => 0, "ShareCount" => 0, "test_name" => 1 } 36 | assert_equal clause, @query.compile_where 37 | Parse::Query.field_formatter = :camelize 38 | @query.clear :where 39 | @query.where :fan_count => 0, :playCount => 0, :ShareCount => 0, :' test_name ' => 1 40 | clause = { "FanCount" => 0, "PlayCount" => 0, "ShareCount" => 0, "TestName" => 1 } 41 | assert_equal clause, @query.compile_where 42 | Parse::Query.field_formatter = :columnize 43 | end 44 | 45 | def test_table_name 46 | assert_equal @query.table, "Song" 47 | assert_equal Parse::Query.new("MyClass").table, "MyClass" 48 | assert_equal TestQueryObject.query.table, TestQueryObject.parse_class 49 | assert_equal CommentObject.query.table, CommentObject.parse_class 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/base_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestConstraintEquality < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | 6 | def setup 7 | @query = Parse::Query.new("Basic") 8 | end 9 | 10 | def test_formatted_value 11 | value = "value" 12 | constraint = Parse::Constraint.new(:field, value) 13 | assert_equal value, constraint.formatted_value 14 | 15 | # Time tests 16 | value = Time.now 17 | expected_value = { __type: "Date", iso: value.utc.iso8601(3) } 18 | constraint = Parse::Constraint.new(:field, value) 19 | assert_equal expected_value, constraint.formatted_value 20 | 21 | # DateTime tests 22 | value = DateTime.now 23 | expected_value = { __type: "Date", iso: value.utc.iso8601(3) } 24 | constraint = Parse::Constraint.new(:field, value) 25 | assert_equal expected_value, constraint.formatted_value 26 | 27 | # Parse::Date tests 28 | value = Parse::Date.now 29 | expected_value = { __type: "Date", iso: value.utc.iso8601(3) } 30 | constraint = Parse::Constraint.new(:field, value) 31 | assert_equal expected_value, constraint.formatted_value 32 | 33 | # Regex Test 34 | value = /test/i 35 | expected_value = value.to_s 36 | constraint = Parse::Constraint.new(:field, value) 37 | assert_instance_of(Regexp, value) 38 | assert_equal expected_value, constraint.formatted_value 39 | 40 | # Pointer Test 41 | value = Parse::User.new(id: "123456", username: "test") 42 | expected_value = value.pointer 43 | constraint = Parse::Constraint.new(:field, value) 44 | assert_instance_of Parse::Pointer, constraint.formatted_value 45 | assert_equal expected_value, constraint.formatted_value 46 | expected_value = { "field" => { :__type => "Pointer", :className => "_User", :objectId => "123456" } }.as_json 47 | assert_equal expected_value, constraint.build.as_json 48 | 49 | # Parse::Query Test 50 | value = Parse::Query.new("Song", :name => "Song Name", :field => "value") 51 | constraint = Parse::Constraint.new(:field, value) 52 | expected_value = { :where => { "name" => "Song Name", "field" => "value" }, :className => "Song" } 53 | assert_equal expected_value, constraint.formatted_value 54 | end 55 | 56 | def test_build 57 | constraint = Parse::Constraint.new(:field, 1) 58 | assert_nil constraint.key 59 | expected = { :field => 1 } 60 | assert_equal expected, constraint.build 61 | 62 | # if we set a key when calling the base version of build 63 | # then we get a different format. 64 | Parse::Constraint.key = :$test 65 | constraint = Parse::Constraint.new(:field, 1) 66 | assert_equal :eq, constraint.operator 67 | constraint.operator = :test 68 | assert_equal :test, constraint.operator 69 | assert_equal :$test, constraint.key 70 | expected = { :field => { :$test => 1 } } 71 | assert_equal expected, constraint.build 72 | Parse::Constraint.key = nil 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/contained_in_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestContainedInConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::ContainedInConstraint 9 | @key = :$in 10 | @operand = :in 11 | @keys = [:in, :contained_in] 12 | end 13 | 14 | def build(value) 15 | { "field" => { @key.to_s => [Parse::Constraint.formatted_value(value)].flatten.compact } } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/contains_all_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestContainsAllConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::ContainsAllConstraint 9 | @key = :$all 10 | @operand = :all 11 | @keys = [:all, :contains_all] 12 | end 13 | 14 | def build(value) 15 | { "field" => { @key.to_s => [Parse::Constraint.formatted_value(value)].flatten.compact } } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/equality_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestEqualityConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint 9 | @key = nil 10 | @operand = :eq 11 | @keys = [:eq] 12 | end 13 | 14 | def build(value) 15 | { "field" => Parse::Constraint.formatted_value(value) } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/exists_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestExistsConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::ExistsConstraint 9 | @key = :$exists 10 | @operand = :exists 11 | @keys = [:exists] 12 | end 13 | 14 | def build(value) 15 | value = Parse::Constraint.formatted_value(value) 16 | value = value.present? ? true : false 17 | { "field" => { @key => value } } 18 | end 19 | 20 | def test_scalar_values 21 | [true, false].each do |value| 22 | constraint = @klass.new(:field, value) 23 | expected = build(value).as_json 24 | assert_equal expected, constraint.build.as_json 25 | end 26 | 27 | ["true", 1, nil].each do |value| 28 | constraint = @klass.new(:field, value) 29 | assert_raises(ArgumentError) do 30 | constraint.build.as_json 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/geobox_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestWithinGeoBoxQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::WithinGeoBoxQueryConstraint 9 | @key = :$within 10 | @operand = :within_box 11 | @keys = [:within_box] 12 | @skip_scalar_values_test = true 13 | end 14 | 15 | def build(value) 16 | { "field" => { @key => { :$box => value } } } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/greater_than_or_equal_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestGreaterThanOrEqualConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::GreaterThanOrEqualConstraint 9 | @key = :$gte 10 | @operand = :gte 11 | @keys = [:gte, :greater_than_or_equal, :on_or_after] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/greater_than_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestGreaterThanConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::GreaterThanConstraint 9 | @key = :$gt 10 | @operand = :gt 11 | @keys = [:gt, :greater_than, :after] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/id_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class Song < Parse::Object; end 4 | 5 | class OtherSong < Parse::Object 6 | parse_class "MySong" 7 | end 8 | 9 | class TestObjectIdConstraint < Minitest::Test 10 | extend Minitest::Spec::DSL 11 | include ConstraintTests 12 | 13 | def setup 14 | @klass = Parse::Constraint::ObjectIdConstraint 15 | @key = nil 16 | @operand = :id 17 | @keys = [:id] 18 | end 19 | 20 | def test_scalar_values 21 | [10, nil, true, false].each do |value| 22 | constraint = @klass.new(:field, value) 23 | assert_raises(ArgumentError) do 24 | # all should fail 25 | constraint.build.as_json 26 | end 27 | end 28 | 29 | list = ["123456", :myObjectId] 30 | assert_equal "Song", Song.parse_class 31 | assert_equal "MySong", OtherSong.parse_class 32 | 33 | list.each do |value| 34 | # Test against className matching parseClass 35 | constraint = @klass.new(:song, value) 36 | expected = { "song" => Song.pointer(value) }.as_json 37 | constraint.build.as_json 38 | assert_equal expected, constraint.build.as_json 39 | 40 | # Test by safely supporting pointers too 41 | constraint = @klass.new(:song, Song.pointer(value)) 42 | expected = { "song" => Song.pointer(value) }.as_json 43 | constraint.build.as_json 44 | assert_equal expected, constraint.build.as_json 45 | 46 | # Test against a valid parse class name 47 | constraint = @klass.new(:my_song, value) 48 | expected = { "my_song" => OtherSong.pointer(value) }.as_json 49 | assert_equal expected, constraint.build.as_json 50 | 51 | # Test Pointer support 52 | constraint = @klass.new(:my_song, OtherSong.pointer(value)) 53 | expected = { "my_song" => OtherSong.pointer(value) }.as_json 54 | assert_equal expected, constraint.build.as_json 55 | 56 | # Test with parse_class name set to something else 57 | constraint = @klass.new(:other_song, value) 58 | expected = { "other_song" => OtherSong.pointer(value) }.as_json 59 | assert_equal expected, constraint.build.as_json 60 | 61 | # Test with pointers 62 | constraint = @klass.new(:other_song, OtherSong.pointer(value)) 63 | expected = { "other_song" => OtherSong.pointer(value) }.as_json 64 | assert_equal expected, constraint.build.as_json 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/in_query_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestInQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::InQueryConstraint 9 | @key = :$inQuery 10 | @operand = :matches 11 | @keys = [:matches, :in_query] 12 | @skip_scalar_values_test = true 13 | end 14 | 15 | def build(value) 16 | { "field" => { @key.to_s => Parse::Constraint.formatted_value(value) } } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/inequality_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestNotEqualConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::NotEqualConstraint 9 | @key = :$ne 10 | @operand = :not 11 | @keys = [:not, :ne] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/less_than_or_equal_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestLessThanOrEqualConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::LessThanOrEqualConstraint 9 | @key = :$lte 10 | @operand = :lte 11 | @keys = [:lte, :less_than_or_equal, :on_or_before] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/less_than_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestLessThanConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::LessThanConstraint 9 | @key = :$lt 10 | @operand = :lt 11 | @keys = [:lt, :less_than, :before] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/near_sphere_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestNearSphereQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::NearSphereQueryConstraint 9 | @key = :$nearSphere 10 | @operand = :near 11 | @keys = [:near] 12 | @skip_scalar_values_test = true 13 | end 14 | 15 | def build(value) 16 | { "field" => { @key.to_s => value } } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/not_contained_in_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestNotContainedInConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::NotContainedInConstraint 9 | @key = :$nin 10 | @operand = :not_in 11 | @keys = [:not_in, :nin, :not_contained_in] 12 | end 13 | 14 | def build(value) 15 | { "field" => { @key.to_s => [Parse::Constraint.formatted_value(value)].flatten.compact } } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/not_in_query_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestNotInQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::NotInQueryConstraint 9 | @key = :$notInQuery 10 | @operand = :excludes 11 | @keys = [:excludes, :not_in_query] 12 | @skip_scalar_values_test = true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/nullability_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestNullabilityConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::NullabilityConstraint 9 | @key = :$exists 10 | @operand = :null 11 | @keys = [:null] 12 | end 13 | 14 | def build(value) 15 | value = Parse::Constraint.formatted_value(value) 16 | if value == true 17 | { "field" => { @key => false } } 18 | else 19 | { "field" => { Parse::Constraint::NotEqualConstraint.key => nil } } 20 | end 21 | end 22 | 23 | def test_scalar_values 24 | [true, false].each do |value| 25 | constraint = @klass.new(:field, value) 26 | expected = build(value).as_json 27 | assert_equal expected, constraint.build.as_json 28 | end 29 | 30 | ["true", 1, nil].each do |value| 31 | constraint = @klass.new(:field, value) 32 | assert_raises(ArgumentError) do 33 | expected = build(value).as_json 34 | constraint.build.as_json 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/polygon_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestWithinPolygonQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::WithinPolygonQueryConstraint 9 | @key = :$geoWithin 10 | @operand = :within_polygon 11 | @keys = [:within_polygon] 12 | @skip_scalar_values_test = true 13 | 14 | @bermuda = Parse::GeoPoint.new 32.3078000, -64.7504999 # Bermuda 15 | @miami = Parse::GeoPoint.new 25.7823198, -80.2660226 # Miami, FL 16 | @san_juan = Parse::GeoPoint.new 18.3848232, -66.0933608 # San Juan, PR 17 | @san_diego = Parse::GeoPoint.new 32.9201332, -117.1088263 18 | end 19 | 20 | def build(value) 21 | { "field" => { @key => { :$polygon => value } } } 22 | end 23 | 24 | def test_argument_error 25 | triangle = [@bermuda, @miami] # missing one 26 | assert_raises(ArgumentError) { User.query(:location.within_polygon => nil).compile } 27 | assert_raises(ArgumentError) { User.query(:location.within_polygon => []).compile } 28 | assert_raises(ArgumentError) { User.query(:location.within_polygon => [@bermuda, 2343]).compile } 29 | assert_raises(ArgumentError) { User.query(:location.within_polygon => triangle).compile } 30 | triangle.push @san_juan 31 | refute_raises(ArgumentError) { User.query(:location.within_polygon => triangle).compile } 32 | quad = triangle + [@san_diego] 33 | refute_raises(ArgumentError) { User.query(:location.within_polygon => quad).compile } 34 | end 35 | 36 | def test_compiled_query 37 | triangle = [@bermuda, @miami, @san_juan] 38 | compiled_query = { "location" => { "$geoWithin" => { "$polygon" => [ 39 | { :__type => "GeoPoint", :latitude => 32.3078, :longitude => -64.7504999 }, 40 | { :__type => "GeoPoint", :latitude => 25.7823198, :longitude => -80.2660226 }, 41 | { :__type => "GeoPoint", :latitude => 18.3848232, :longitude => -66.0933608 }, 42 | ] } } } 43 | query = User.query(:location.within_polygon => [@bermuda, @miami, @san_juan]) 44 | assert_equal query.compile_where.as_json, compiled_query.as_json 45 | 46 | compiled_query = { "location" => { "$geoWithin" => { "$polygon" => [ 47 | { :__type => "GeoPoint", :latitude => 32.9201332, :longitude => -117.1088263 }, 48 | { :__type => "GeoPoint", :latitude => 25.7823198, :longitude => -80.2660226 }, 49 | { :__type => "GeoPoint", :latitude => 18.3848232, :longitude => -66.0933608 }, 50 | { :__type => "GeoPoint", :latitude => 32.3078, :longitude => -64.7504999 }, 51 | ] } } } 52 | query = User.query(:location.within_polygon => [@san_diego, @miami, @san_juan, @bermuda]) 53 | assert_equal query.compile_where.as_json, compiled_query.as_json 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/lib/parse/query/constraints/text_search_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../../test_helper" 2 | 3 | class TestFullTextSearchQueryConstraint < Minitest::Test 4 | extend Minitest::Spec::DSL 5 | include ConstraintTests 6 | 7 | def setup 8 | @klass = Parse::Constraint::FullTextSearchQueryConstraint 9 | @key = :$text 10 | @operand = :text_search 11 | @keys = [:text_search] 12 | @skip_scalar_values_test = true 13 | end 14 | 15 | def build(value) 16 | { "field" => { @key => { :$search => { :$term => value } } } } 17 | end 18 | 19 | def test_argument_error 20 | assert_raises(ArgumentError) { User.query(:name.text_search => nil).compile } 21 | assert_raises(ArgumentError) { User.query(:name.text_search => []).compile } 22 | assert_raises(ArgumentError) { User.query(:name.text_search => {}).compile } 23 | assert_raises(ArgumentError) { User.query(:name.text_search => { :lang => :en }).compile } 24 | 25 | refute_raises(ArgumentError) { User.query(:name.text_search => "text").compile } 26 | refute_raises(ArgumentError) { User.query(:name.text_search => :text).compile } 27 | refute_raises(ArgumentError) { User.query(:name.text_search => { :$term => "text" }).compile } 28 | refute_raises(ArgumentError) { User.query(:name.text_search => { "$term" => "text" }).compile } 29 | refute_raises(ArgumentError) { User.query(:name.text_search => { "term" => "text" }).compile } 30 | refute_raises(ArgumentError) { User.query(:name.text_search => { :term => "text" }).compile } 31 | end 32 | 33 | def test_compiled_query 34 | params = { "$term" => "text" } 35 | compiled_query = { "name" => { "$text" => { "$search" => params } } } 36 | # Basics 37 | query = User.query(:name.text_search => "text") 38 | assert_equal query.compile_where.as_json, compiled_query 39 | query = User.query(:name.text_search => :text) 40 | assert_equal query.compile_where.as_json, compiled_query 41 | query = User.query(:name.text_search => { term: "text" }) 42 | assert_equal query.compile_where.as_json, compiled_query 43 | query = User.query(:name.text_search => { term: :text }) 44 | assert_equal query.compile_where.as_json, compiled_query 45 | query = User.query(:name.text_search => { :$term => :text }) 46 | assert_equal query.compile_where.as_json, compiled_query 47 | query = User.query(:name.text_search => { "$term" => "text" }) 48 | assert_equal query.compile_where.as_json, compiled_query 49 | query = User.query(:name.text_search => { "$term" => :text }) 50 | assert_equal query.compile_where.as_json, compiled_query 51 | query = User.query(:name.text_search => params) 52 | assert_equal query.compile_where.as_json, compiled_query 53 | 54 | # Advanced 55 | 56 | params["$caseSensitive"] = true 57 | compiled_query = { "name" => { "$text" => { "$search" => params } } } 58 | query = User.query(:name.text_search => params) 59 | assert_equal query.compile_where.as_json, compiled_query 60 | query = User.query(:name.text_search => { term: "text", case_sensitive: true }) 61 | assert_equal query.compile_where.as_json, compiled_query 62 | params["$caseSensitive"] = false 63 | query = User.query(:name.text_search => { term: "text", caseSensitive: false }) 64 | assert_equal query.compile_where.as_json, compiled_query 65 | query = User.query(:name.text_search => { term: "text", :$caseSensitive => false }) 66 | assert_equal query.compile_where.as_json, compiled_query 67 | query = User.query(:name.text_search => { term: "text", "$caseSensitive" => false }) 68 | assert_equal query.compile_where.as_json, compiled_query 69 | 70 | params["$language"] = ["stop", "words"] 71 | compiled_query = { "name" => { "$text" => { "$search" => params } } } 72 | query = User.query(:name.text_search => params) 73 | assert_equal query.compile_where.as_json, compiled_query 74 | query = User.query(:name.text_search => { term: "text", caseSensitive: false, language: ["stop", "words"] }) 75 | assert_equal query.compile_where.as_json, compiled_query 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/lib/parse/query/core_query_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestParseCoreQuery < Minitest::Test 4 | def test_save_all_invalid_constraints 5 | # test passing :updated_at as a constraint 6 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at => 123 } 7 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.on_or_after => DateTime.now } 8 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.after => DateTime.now } 9 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.on_or_before => DateTime.now } 10 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.before => DateTime.now } 11 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.gt => DateTime.now } 12 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.gte => DateTime.now } 13 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.ne => DateTime.now } 14 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.lt => DateTime.now } 15 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.lte => DateTime.now } 16 | assert_raises(ArgumentError) { Parse::User.save_all :updated_at.eq => DateTime.now } 17 | # test passing :updatedAt as a constraint 18 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt => 123 } 19 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.on_or_after => DateTime.now } 20 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.after => DateTime.now } 21 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.on_or_before => DateTime.now } 22 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.before => DateTime.now } 23 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.gt => DateTime.now } 24 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.gte => DateTime.now } 25 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.ne => DateTime.now } 26 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.lt => DateTime.now } 27 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.lte => DateTime.now } 28 | assert_raises(ArgumentError) { Parse::User.save_all :updatedAt.eq => DateTime.now } 29 | end 30 | 31 | def test_each_invalid_constraints 32 | # test passing :created_at as a constraint 33 | assert_raises(ArgumentError) { Parse::User.each :created_at => 123 } 34 | assert_raises(ArgumentError) { Parse::User.each :created_at.on_or_after => DateTime.now } 35 | assert_raises(ArgumentError) { Parse::User.each :created_at.after => DateTime.now } 36 | assert_raises(ArgumentError) { Parse::User.each :created_at.on_or_before => DateTime.now } 37 | assert_raises(ArgumentError) { Parse::User.each :created_at.before => DateTime.now } 38 | assert_raises(ArgumentError) { Parse::User.each :created_at.gt => DateTime.now } 39 | assert_raises(ArgumentError) { Parse::User.each :created_at.gte => DateTime.now } 40 | assert_raises(ArgumentError) { Parse::User.each :created_at.ne => DateTime.now } 41 | assert_raises(ArgumentError) { Parse::User.each :created_at.lt => DateTime.now } 42 | assert_raises(ArgumentError) { Parse::User.each :created_at.lte => DateTime.now } 43 | assert_raises(ArgumentError) { Parse::User.each :created_at.eq => DateTime.now } 44 | # test passing :createdAt as a constraint 45 | assert_raises(ArgumentError) { Parse::User.each :createdAt => 123 } 46 | assert_raises(ArgumentError) { Parse::User.each :createdAt.on_or_after => DateTime.now } 47 | assert_raises(ArgumentError) { Parse::User.each :createdAt.after => DateTime.now } 48 | assert_raises(ArgumentError) { Parse::User.each :createdAt.on_or_before => DateTime.now } 49 | assert_raises(ArgumentError) { Parse::User.each :createdAt.before => DateTime.now } 50 | assert_raises(ArgumentError) { Parse::User.each :createdAt.gt => DateTime.now } 51 | assert_raises(ArgumentError) { Parse::User.each :createdAt.gte => DateTime.now } 52 | assert_raises(ArgumentError) { Parse::User.each :createdAt.ne => DateTime.now } 53 | assert_raises(ArgumentError) { Parse::User.each :createdAt.lt => DateTime.now } 54 | assert_raises(ArgumentError) { Parse::User.each :createdAt.lte => DateTime.now } 55 | assert_raises(ArgumentError) { Parse::User.each :createdAt.eq => DateTime.now } 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/lib/parse/query/expression_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../test_helper" 2 | 3 | class TestParseQueryExpressions < Minitest::Test 4 | def setup 5 | @query = Parse::Query.new("Song") 6 | Parse::Query.field_formatter = :columnize 7 | end 8 | 9 | def test_counting 10 | @query.clear :where 11 | @query.where :fan_count => 0 12 | # interal way of setting count query without executing 13 | @query.instance_variable_set :@count, 1 14 | clause = { :where => { "fanCount" => 0 }, :limit => 0, :count => 1 } 15 | assert_equal clause, @query.prepared 16 | @query.limit 1_000 # should be ignored 17 | assert_equal clause, @query.prepared 18 | end 19 | 20 | def test_exp_order 21 | assert_empty @query.clause(:order) 22 | end 23 | 24 | def test_exp_keys 25 | assert_empty @query.clause(:keys) 26 | simple_query = { "keys" => "test" } 27 | compound_query = { "keys" => "test,field" } 28 | 29 | q = User.query(:key => "test") 30 | assert_equal q.compile.as_json, simple_query 31 | q = User.query(:key => ["test"]) 32 | assert_equal q.compile.as_json, simple_query 33 | q = User.query(:key => ["test", "field"]) 34 | assert_equal q.compile.as_json, compound_query 35 | q = User.query(:key => :test) 36 | assert_equal q.compile.as_json, simple_query 37 | q = User.query(:key => [:test, :field]) 38 | assert_equal q.compile.as_json, compound_query 39 | 40 | q = User.query(:keys => "test") 41 | assert_equal q.compile.as_json, simple_query 42 | q = User.query(:keys => ["test"]) 43 | assert_equal q.compile.as_json, simple_query 44 | q = User.query(:keys => ["test", "field"]) 45 | assert_equal q.compile.as_json, compound_query 46 | q = User.query(:keys => :test) 47 | assert_equal q.compile.as_json, simple_query 48 | q = User.query(:keys => [:test, :field]) 49 | assert_equal q.compile.as_json, compound_query 50 | end 51 | 52 | def test_exp_includes 53 | assert_empty @query.clause(:includes) 54 | @query.includes(:field) 55 | assert_equal @query.compile.as_json, { "include" => "field" } 56 | @query.includes(:field, :name) 57 | assert_equal @query.compile.as_json, { "include" => "field,name" } 58 | @query.where(:field.eq => "text") 59 | assert_equal @query.compile.as_json, { "include" => "field,name", "where" => "{\"field\":\"text\"}" } 60 | end 61 | 62 | def test_exp_skip 63 | assert_equal 0, @query.clause(:skip) 64 | @query.skip 100 65 | assert_equal 100, @query.clause(:skip) 66 | @query.skip 15_000 # allow skips over 10k 67 | assert_equal 15_000, @query.clause(:skip) 68 | end 69 | 70 | def test_exp_limit 71 | assert_nil @query.clause(:limit) 72 | @query.limit 100 73 | assert_equal 100, @query.clause(:limit) 74 | @query.limit 5000 # allow limits over 1k 75 | assert_equal 5000, @query.clause(:limit) 76 | @query.limit :max 77 | assert_equal :max, @query.clause(:limit) 78 | end 79 | 80 | def test_exp_session 81 | assert_nil @query.clause(:session) 82 | assert_nil @query.session_token 83 | 84 | user = Parse::User.new 85 | session = Parse::Session.new 86 | 87 | assert_raises(ArgumentError) { @query.session_token = 123456 } 88 | assert_raises(ArgumentError) { @query.session_token = user } 89 | assert_raises(ArgumentError) { @query.session_token = session } 90 | assert_raises(ArgumentError) { @query.conditions(session: 123456) } 91 | assert_raises(ArgumentError) { @query.conditions(session: user) } 92 | assert_raises(ArgumentError) { @query.conditions(session: session) } 93 | 94 | session.session_token = user.session_token = "r:123456" 95 | 96 | refute_raises(ArgumentError) { @query.session_token = nil } 97 | refute_raises(ArgumentError) { @query.session_token = user } 98 | refute_raises(ArgumentError) { @query.session_token = session } 99 | refute_raises(ArgumentError) { @query.conditions(session: nil) } 100 | refute_raises(ArgumentError) { @query.conditions(session: user) } 101 | refute_raises(ArgumentError) { @query.conditions(session: session) } 102 | end 103 | 104 | def test_exp_options 105 | # cache 106 | # session token 107 | # use_master_key 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/lib/parse/version_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../test_helper" 2 | 3 | describe Parse::Stack do 4 | it "must be defined" do 5 | _(Parse::Stack::VERSION).wont_be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | require "minitest/pride" 3 | require 'minitest/reporters' 4 | Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new) 5 | require_relative "../lib/parse/stack.rb" 6 | require "minitest/autorun" 7 | 8 | Parse.use_shortnames! 9 | 10 | module ConstraintTests 11 | TEST_VALUES = [ 12 | "v", [1, "test", :other, true], 13 | nil, Parse::User.pointer(12345), true, false, 14 | ] 15 | 16 | def build(value) 17 | { "field" => { @key.to_s => Parse::Constraint.formatted_value(value) } } 18 | end 19 | 20 | def test_operator 21 | assert_equal @operand, @klass.operand 22 | # Some constraint classes are macros and do not have operator keys. 23 | # The reason for putting them in this block is because in MT6 (mini-test), 24 | # assert_equal will fail if we are comparing nil. 25 | if @key.nil? # for Parse::Constraint, Parse::Constraint::ObjectIdConstraint 26 | assert_nil @klass.key 27 | else 28 | assert_equal @key, @klass.key 29 | end 30 | 31 | @keys.each do |o| 32 | assert_respond_to(:field, o) 33 | op = :field.send(o) 34 | assert_instance_of(Parse::Operation, op) 35 | assert_instance_of(@klass, op.constraint) 36 | if @key.nil? 37 | assert_nil op.constraint.key 38 | else 39 | assert_equal @key, op.constraint.key 40 | end 41 | value = { "operand" => "field", "operator" => o.to_s } 42 | assert_equal value, op.as_json 43 | end 44 | end 45 | 46 | def test_scalar_values 47 | return if @skip_scalar_values_test.present? 48 | TEST_VALUES.each do |value| 49 | constraint = @klass.new(:field, value) 50 | expected = build(value).as_json 51 | assert_equal expected, constraint.build.as_json 52 | end 53 | end 54 | end 55 | 56 | module MiniTest 57 | module Assertions 58 | def refute_raises(*exp) 59 | msg = "#{exp.pop}.\n" if String === exp.last 60 | 61 | begin 62 | yield 63 | rescue MiniTest::Skip => e 64 | return e if exp.include? MiniTest::Skip 65 | raise e 66 | rescue Exception => e 67 | exp = exp.first if exp.size == 1 68 | flunk "unexpected exception raised: #{e}" 69 | end 70 | end 71 | end 72 | 73 | module Expectations 74 | infect_an_assertion :refute_raises, :wont_raise 75 | end 76 | end 77 | --------------------------------------------------------------------------------