https://github.com/neurosynq/parse-stack-next
Ruby SDK for Parse Server with extended features: vector & Atlas Search, agent ACL scopes, GraphQL, MCP, and ORM tooling
https://github.com/neurosynq/parse-stack-next
activemodel agent ai-agent atlas-search cloud-code graphql live-query mcp model-context-protocol mongodb orm parse-platform parse-server rails ruby ruby-sdk vector-search webhooks
Last synced: about 2 hours ago
JSON representation
Ruby SDK for Parse Server with extended features: vector & Atlas Search, agent ACL scopes, GraphQL, MCP, and ORM tooling
- Host: GitHub
- URL: https://github.com/neurosynq/parse-stack-next
- Owner: neurosynq
- License: mit
- Created: 2026-05-23T18:11:37.000Z (7 days ago)
- Default Branch: main
- Last Pushed: 2026-05-28T13:57:30.000Z (1 day ago)
- Last Synced: 2026-05-29T04:24:08.471Z (1 day ago)
- Topics: activemodel, agent, ai-agent, atlas-search, cloud-code, graphql, live-query, mcp, model-context-protocol, mongodb, orm, parse-platform, parse-server, rails, ruby, ruby-sdk, vector-search, webhooks
- Language: Ruby
- Homepage: https://neurosynq.github.io/parse-stack-next/
- Size: 2.91 MB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Security: SECURITY.md
Awesome Lists containing this project
README

# Parse Stack Next
A full-featured Ruby client SDK for [Parse Server](http://parseplatform.org/). [parse-stack-next](https://github.com/neurosynq/parse-stack-next) is a Ruby client SDK, REST client, and Active Model ORM for [Parse Server](http://parseplatform.org/), combining a low-level API client, a query engine, an object-relational mapper (ORM), and a Cloud Code Webhooks rack application in a single gem.
### What's new in 5.0
- **RAG foundation** — `:vector` property type, `Parse::Embeddings` provider registry shipping built-in adapters for OpenAI, Cohere (v3 + v4.0 Matryoshka text-mode), Voyage (incl. open-weight `voyage-4-nano` and `voyage-multimodal-3` text-mode), Jina v3/v4/v5/code, Qwen 3 (DashScope), and a generic `LocalHTTP` client for Ollama / LM Studio / vLLM / TEI. `Klass.find_similar(vector:/text:, k:)` over Atlas `$vectorSearch`, and an `embed` class macro that digest-tracks source fields so vectors only recompute when content changes
- **`Parse::Cache::Redis`** — Moneta-compatible Redis cache wrapper with a built-in `ConnectionPool`, optional `cache_namespace:` for multi-tenant Redis sharing, and graceful degrade on pool saturation
- **`ActiveSupport::Notifications` instrumentation** — `parse.cache.*`, `parse.mongodb.aggregate`, `parse.mongodb.find`, and `parse.embeddings.embed` events with stable, PII-safe payload schemas; in-core slow-query log via `Parse.slow_query_threshold_ms`
- **MCP transport hardening** — Streamable HTTP `Mcp-Session-Id` header (renamed from `X-MCP-Session-Id`, **breaking**), `MCP-Protocol-Version` validation, `DELETE /` session termination, structured-content (`outputSchema`) on built-in tools, optional `health_path:` liveness probe
- **`Parse::GraphQL::TypeGenerator`** — generate `graphql-ruby` types directly from your `Parse::Object` subclasses (no Parse Server round-trip), with `:vector` columns surfaced as `[Float]` and association registries (`has_one_associations`, `has_many_associations`) populated at DSL time
- **LiveQuery promoted to stable** — the experimental warning is removed; `Parse.live_query_enabled = true` is retained as a network-egress safety toggle, not a stability gate
- **Server-version deprecation warning** — one-shot warning when connecting to Parse Server below the supported floor (currently 7.0.0); silence with `Parse.suppress_server_version_warning = true`
- **`mongo_relation_index :field, dedup: true`** — register a compound `{owningId, relatedId}` UNIQUE on relation join collections to prevent duplicate-pair subscriptions without breaking `has_many` semantics
See [CHANGELOG.md](./CHANGELOG.md) for the full 5.0 entry, including security-hardening notes and Ruby 3.x cleanup.
### Core capabilities
- MongoDB Aggregation Framework support
- **MongoDB Atlas Search** — full-text search, autocomplete, faceted search with direct MongoDB access
- **Direct MongoDB Queries** — bypass Parse Server's REST surface for high-performance reads, with SDK-side ACL/CLP/`protectedFields` enforcement for scoped agents
- **Schema Introspection & Migration** — compare local models with server schema and generate migrations
- **Enhanced Role Management** — helper methods for role hierarchies, user membership, and subscription queries
- **Read Preference Support** — route reads to MongoDB secondary replicas
- **Class-Level Permissions (CLP)** — define and filter protected fields based on roles and user ownership
- Advanced ACL query constraints (`readable_by`, `writable_by`)
- **Owner-aware default ACL policy** (`acl_policy :owner_else_private`) — per-class defaults granting read/write only to the record's owner, with a secure or public fallback for server-context creates
- Full transaction support with automatic retry
- Comprehensive integration testing with Docker
- Enhanced change tracking and webhooks
- Request idempotency with `Retry-After` header support
- Timezone support for date operations
- Partial fetch with smart autofetch and serialization control
- Multi-Factor Authentication (MFA/2FA) support
- LiveQuery real-time subscriptions with TLS/SSL, circuit breaker, and health monitoring
- AI/LLM agent integration (MCP-spec compliant) with security hardening — rate limiting, injection protection, agent ACL scopes
Below is a [quick start guide](#overview). See also the [Usage Guide](./docs/usage_guide.md) for practical examples covering queries, aggregation, ACLs, and more.
> **Note:** API reference docs are published at [neurosynq.github.io/parse-stack-next](https://neurosynq.github.io/parse-stack-next/index.html). Generated via YARD from the current source; covers the full 5.x surface.
### Credits
This project (`parse-stack-next`) is a continuation of the [Parse Stack framework](https://github.com/modernistik/parse-stack) originally created by [Modernistik](https://www.modernistik.com). We are grateful for their foundational work and continue to build upon it under the [neurosynq](https://github.com/neurosynq) organization.
### Code Status
[](https://rubygems.org/gems/parse-stack-next)
[](https://rubygems.org/gems/parse-stack-next)
[](https://github.com/neurosynq/parse-stack-next/releases)
#### Tutorial Videos
The following videos were recorded for the original parse-stack gem. The model, query, and association surface they cover is unchanged in parse-stack-next, so they remain a useful introduction; see the [Usage Guide](./docs/usage_guide.md) for v5.x-specific features (vector search, Redis cache, agent tools).
1. Getting Started: https://youtu.be/zoYSGmciDlQ
2. Custom Classes and Relations: https://youtu.be/tfSesotfU7w
3. Working with Existing Schemas: https://youtu.be/EJGPT7YWyXA
Any other questions, please post them on StackOverflow with the proper parse-stack / parse-server / ruby tags.
## Installation
Add this line to your application's `Gemfile`:
```ruby
gem 'parse-stack-next'
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install parse-stack-next
```
> **Note:** The Ruby require path and module namespace are unchanged. You still `require 'parse/stack'` and reference classes under `Parse::Object`, `Parse::Query`, etc. Only the gem name on RubyGems has changed.
### Rack / Sinatra
Parse-Stack API, models and webhooks easily integrate in your existing Rack/Sinatra based applications.
### Rails
Parse-Stack comes with support for Rails by adding additional rake tasks and generators. After adding `parse-stack-next` as a gem dependency in your Gemfile and running `bundle`, you should run the install script:
```bash
$ rails g parse_stack:install
```
### Interactive Command Line Playground
You can also used the bundled `parse-console` command line to connect and interact with your Parse Server and its data in an IRB-like console. This is useful for trying concepts and debugging as it will automatically connect to your Parse Server, and if provided the master key, automatically generate all the models entities.
```bash
$ parse-console -h # see all options
$ parse-console -v -a myAppId -m myMasterKey http://localhost:2337/parse
Server : http://localhost:2337/parse
App Id : myAppId
Master : true
2.4.0 > Parse::User.first
```
## Overview
Parse-Stack is a full stack framework that utilizes several ideas behind [DataMapper](http://datamapper.org/docs/find.html) and [ActiveModel](https://github.com/rails/rails/tree/master/activemodel) to manage and maintain larger scale ruby applications and tools that utilize the [Parse Server Platform](http://parseplatform.org/). If you are familiar with these technologies, the framework should feel familiar to you.
```ruby
require 'parse/stack'
Parse.setup server_url: 'http://localhost:2337/parse',
app_id: APP_ID,
api_key: REST_API_KEY,
master_key: YOUR_MASTER_KEY # optional
# Automatically build models based on your Parse application schemas.
Parse.auto_generate_models!
# or define custom Subclasses (Highly Recommended)
class Song < Parse::Object
property :name
property :play, :integer
property :audio_file, :file
property :tags, :array
property :released, :date
belongs_to :artist
# `like` is a Parse Relation to User class
has_many :likes, as: :user, through: :relation
# deny public write to Song records by default
set_default_acl :public, read: true, write: false
end
class Artist < Parse::Object
property :name
property :genres, :array
has_many :fans, as: :user
has_one :manager, as: :user
scope :recent, ->(x) { query(:created_at.after => x) }
end
# updates schemas for your Parse app based on your models (non-destructive)
Parse.auto_upgrade!
# login
user = Parse::User.login(username, passwd)
artist = Artist.new(name: "Frank Sinatra", genres: ["swing", "jazz"])
artist.fans << user
artist.save
# Query
artist = Artist.first(:name.like => /Sinatra/, :genres.in => ['swing'])
# more examples
song = Song.new name: "Fly Me to the Moon"
song.artist = artist
# Parse files - upload a file and attach to object
song.audio_file = Parse::File.create("http://path_to.mp3")
# relations - find a User matching username and add it to relation.
song.likes.add Parse::User.first(username: "persaud")
# saves both attributes and relations
song.save
# find songs
songs = Song.all(artist: artist, :plays.gt => 100, :released.on_or_after => 30.days.ago)
songs.each { |s| s.tags.add "awesome" }
# batch saves
songs.save
# Call Cloud Code functions
result = Parse.call_function :myFunctionName, {param: value}
```
## Release History
**Current version: 5.0.1** | **Ruby 3.2+ required**
The 5.0 highlights (vector search / RAG, pooled Redis cache, AS::N instrumentation, MCP transport hardening, GraphQL type generation) are summarized in the [What's new in 5.0](#whats-new-in-50) section above. Earlier releases are recorded below.
Per-version detail lives in [CHANGELOG.md](./CHANGELOG.md) and on the [Releases page](https://github.com/neurosynq/parse-stack-next/releases). The compact summary below is the major-line view.
### 4.x — MongoDB index management, agent ACL scope, CLP enforced on mongo-direct, and `parse-stack-next` debut
- **`mongo_index` DSL** (`mongo_index`, `mongo_geo_index`, `mongo_relation_index`) with class-load validation (pointer auto-rewrite, parallel-array rejection, `_id` guard, 64-per-collection cap). `parse_reference` fields auto-register a unique-sparse index.
- **Index migration tooling** — `Parse::MongoDB.configure_writer` (separate write connection, triple-gated), `Parse::Schema::IndexMigrator` (plan / apply with optional orphan drop), and `rake parse:mongo:indexes:plan` / `:apply`.
- **`Model.describe`** — operator introspection aggregator (local declarations + optional server fetch covering schema, CLP, default ACLs, Atlas Search, MongoDB indexes).
- **CLP + `protectedFields` enforced on mongo-direct** — `Parse::CLPScope` gates `Parse::MongoDB.aggregate` for scoped agents (`session_token:` / `acl_user:` / `acl_role:`) and strips protected fields from result rows. This is the only first-class enforcement surface for ACL + CLP + protectedFields on scoped reads; Parse Server's REST aggregate enforces neither.
- **`Parse::Agent.new(acl_user:|acl_role:)`** — declared agent identity without a session token; built-in tools auto-promote to mongo-direct. Sub-agent identity must be a subset of the parent's reach.
- **Pipeline correctness** — schema-aware `$author` → `$_p_author` rewriter respects pipeline-local aliases; forward-pass field tracking through `$group`/`$addFields`/`$set`/`$lookup.as`; pointer `query_hint:` surfaced in `get_schema`.
- **`Parse.strict_pointer_shapes`** — opt-in flag that converts unresolvable pointer-shape constraints into a `PointerShapeError` raise (recommended for test/CI and LLM-driven workloads).
- **`first_or_create!` / `create_lock` accept `Parse::Operation` keys** in `synchronize:`, fixing filter-lock fingerprint collisions on inequality/range constraints.
- **Security & modernization** — Ruby 3.2 floor, Rails/ActiveSupport 6.1 floor, CI on Ruby 3.2–3.5. LiveQuery TLS hostname verification. Webhook endpoint fails closed when no key is configured. `Parse::Error.new(code, message)` two-argument constructor. `include`d pointer fields auto-added to `keys` when an allowlist is set.
- **4.5.0 — first release of this gem.** `parse-stack-next` debuts on RubyGems under the [neurosynq](https://github.com/neurosynq) organization, continuing from the upstream `parse-stack` 4.4.x line. The Ruby require path (`require 'parse/stack'`) and the `Parse::*` namespace are unchanged from upstream — only the gem name on RubyGems is new.
### 3.x — Atlas Search, MongoDB-direct, CLP, AI agent, push, MFA, LiveQuery
- **MongoDB Atlas Search** — full-text search, autocomplete, faceted search.
- **Direct MongoDB queries** — `results_direct`, `first_direct`, `count_direct` bypassing Parse Server's REST surface.
- **Schema tools** — `Parse::Schema.diff`, `Parse::Schema.migration`, plus `read_pref(:secondary)` for replica reads.
- **Role management** — `find_or_create`, `add_users`, `add_child_role`, `all_users` (with hierarchy walks).
- **Class-Level Permissions (CLP)** declared in models — `set_clp :find, public: true`, `protect_fields "*", [:internal_notes]`.
- **AI/LLM agent** — `Parse::Agent` with natural-language queries over a tool interface.
- **Push builder API** — `to_channel`, `with_alert`, `silent!`, `send!`; installation channels (`subscribe`, `unsubscribe`).
- **Session lifecycle** — `expired?`, `time_remaining`, `logout_all!`.
- **MFA** — TOTP and SMS two-factor authentication.
- **LiveQuery** — real-time WebSocket subscriptions (promoted to stable in 5.0).
- **Ruby 3.1 floor** (3.0 reached EOL March 2024).
### 2.x — aggregation, transactions, idempotency, ACL constraints
- **Transactions** — `Parse::Object.transaction` with automatic retry.
- **MongoDB aggregation** — `group_by`, `count_distinct`, custom pipelines.
- **ACL query constraints** — `readable_by`, `writable_by`, `publicly_readable`.
- **Request idempotency** — automatic duplicate prevention, enabled by default.
- **Change tracking** — works correctly in `after_save` hooks.
- **Breaking from 1.x** — Ruby 3.0 floor, Faraday 2.x (no `faraday_middleware`), `distinct` returns object IDs by default (pass `return_pointers: true` for pointers), `constaint` → `constraint` typo fix.
### 1.x — initial Parse Server SDK
The 1.x line is the original [`modernistik/parse-stack`](https://github.com/modernistik/parse-stack) — Active Model ORM, REST client, query DSL, associations, and Cloud Code webhooks for Parse Server. `parse-stack-next` is a continuation of that work; the first release published under the new gem name is **4.5.0** (above), on RubyGems as [`parse-stack-next`](https://rubygems.org/gems/parse-stack-next).
## Table of Contents
- [Architecture](#architecture)
- [Parse::Client](#parseclient)
- [Parse::Query](#parsequery)
- [Parse::Object](#parseobject)
- [Parse::Webhooks](#parsewebhooks)
- [Field Naming Conventions](#field-naming-conventions)
- [Connection Setup](#connection-setup)
- [Connection Options](#connection-options)
- [Working With Existing Schemas](#working-with-existing-schemas)
- [Parse Config](#parse-config)
- [Core Classes](#core-classes)
- [Parse::Pointer](#parsepointer)
- [Parse::File](#parsefile)
- [Parse::Date](#parsedate)
- [Parse::GeoPoint](#parsegeopoint)
- [Calculating Distances between locations](#calculating-distances-between-locations)
- [Parse::Bytes](#parsebytes)
- [Parse::TimeZone](#parsetimezone)
- [Parse::ACL](#parseacl)
- [Parse::CLP (Class-Level Permissions)](#parseclp-class-level-permissions)
- [Defining CLPs in Models](#defining-clps-in-models)
- [Filtering Data for Webhook Responses](#filtering-data-for-webhook-responses)
- [Protected Fields Intersection Logic](#protected-fields-intersection-logic)
- [Push CLPs to Parse Server](#push-clps-to-parse-server)
- [Fetch and Inspect CLPs](#fetch-and-inspect-clps)
- [Owner-Based Access with userField](#owner-based-access-with-userfield)
- [Parse::Session](#parsesession)
- [Parse::Installation](#parseinstallation)
- [Parse::Product](#parseproduct)
- [Parse::Role](#parserole)
- [Parse::JobStatus](#parsejobstatus)
- [Parse::JobSchedule](#parsejobschedule)
- [Parse::User](#parseuser)
- [Signup](#signup)
- [Third-Party Services](#third-party-services)
- [Login and Sessions](#login-and-sessions)
- [Linking and Unlinking](#linking-and-unlinking)
- [Request Password Reset](#request-password-reset)
- [Modeling and Subclassing](#modeling-and-subclassing)
- [Defining Properties](#defining-properties)
- [Accessor Aliasing](#accessor-aliasing)
- [Property Options](#property-options)
- [`:required`](#required)
- [`:field`](#field)
- [`:default`](#default)
- [`:alias`](#alias)
- [`:symbolize`](#symbolize)
- [`:enum`](#enum)
- [`:scope`](#scope)
- [Associations](#associations)
- [Belongs To](#belongs-to)
- [Options](#options)
- [`:required`](#required-1)
- [`:as`](#as)
- [`:field`](#field-1)
- [Has One](#has-one)
- [Has Many](#has-many)
- [Query](#query)
- [Array](#array)
- [Parse Relation](#parse-relation)
- [Options](#options-1)
- [`:through`](#through)
- [`:scope_only`](#scope_only)
- [Creating, Saving and Deleting Records](#creating-saving-and-deleting-records)
- [Create](#create)
- [Upsert Operations](#upsert-operations)
- [first_or_create](#first_or_create)
- [first_or_create!](#first_or_create_bang)
- [create_or_update!](#create_or_update_bang)
- [Saving](#saving)
- [Saving applying User ACLs](#saving-applying-user-acls)
- [Raising an exception when save fails](#raising-an-exception-when-save-fails)
- [Enhanced Object Fetching](#enhanced-object-fetching)
- [Modifying Associations](#modifying-associations)
- [Batch Requests](#batch-requests)
- [Atomic Transactions](#atomic-transactions)
- [Magic `save_all`](#magic-save_all)
- [Deleting](#deleting)
- [Fetching, Finding and Counting Records](#fetching-finding-and-counting-records)
- [Auto-Fetching Associations](#auto-fetching-associations)
- [Advanced Querying](#advanced-querying)
- [Results Caching](#results-caching)
- [Counting](#counting)
- [Count Distinct](#count-distinct)
- [Aggregation Functions](#aggregation-functions)
- [Group By Operations](#group-by-operations)
- [Distinct Aggregation](#distinct-aggregation)
- [Query Expressions](#query-expressions)
- [:order](#order)
- [:keys](#keys)
- [:includes](#includes)
- [:limit](#limit)
- [:skip](#skip)
- [Cursor-Based Pagination](#cursor-based-pagination)
- [:cache](#cache)
- [:use_master_key](#use_master_key)
- [:session](#session)
- [:where](#where)
- [Query Constraints](#query-constraints)
- [Equals](#equals)
- [Less Than](#less-than)
- [Less Than or Equal To](#less-than-or-equal-to)
- [Greater Than](#greater-than)
- [Greater Than or Equal](#greater-than-or-equal)
- [Not Equal To](#not-equal-to)
- [Nullability Check](#nullability-check)
- [Exists](#exists)
- [Contained In](#contained-in)
- [Not Contained In](#not-contained-in)
- [Contains All](#contains-all)
- [Regex Matching](#regex-matching)
- [Select](#select)
- [Reject](#reject)
- [Matches Query](#matches-query)
- [Excludes Query](#excludes-query)
- [Matches Object Id](#matches-object-id)
- [Geo Queries](#geo-queries)
- [Max Distance Constraint](#max-distance-constraint)
- [Bounding Box Constraint](#bounding-box-constraint)
- [Polygon Area Constraint](#polygon-area-constraint)
- [Full Text Search Constraint](#full-text-search-constraint)
- [Relational Queries](#relational-queries)
- [Compound Queries](#compound-queries)
- [Query Scopes](#query-scopes)
- [Calling Cloud Code Functions](#calling-cloud-code-functions)
- [Calling Background Jobs](#calling-background-jobs)
- [Active Model Callbacks](#active-model-callbacks)
- [Schema Upgrades and Migrations](#schema-upgrades-and-migrations)
- [Push Notifications](#push-notifications)
- [Builder Pattern API](#builder-pattern-api)
- [Silent Push](#silent-push-ios-background-notifications)
- [Rich Push](#rich-push-ios-notification-extensions)
- [Localization](#localization)
- [Badge Management](#badge-management)
- [Saved Audiences](#saved-audiences)
- [Push Status Tracking](#push-status-tracking)
- [Installation Channel Management](#installation-channel-management)
- [Analytics](#analytics)
- [Cloud Code Webhooks](#cloud-code-webhooks)
- [Cloud Code Functions](#cloud-code-functions)
- [Cloud Code Triggers](#cloud-code-triggers)
- [Mounting Webhooks Application](#mounting-webhooks-application)
- [Register Webhooks](#register-webhooks)
- [Parse REST API Client](#parse-rest-api-client)
- [Request Caching](#request-caching)
- [Atlas Search](#atlas-search)
- [Setup](#setup)
- [Full-Text Search](#full-text-search)
- [Autocomplete](#autocomplete-search-as-you-type)
- [Faceted Search](#faceted-search)
- [Search Builder](#search-builder-advanced)
- [Query Integration](#query-integration)
- [Index Management](#index-management)
- [Creating Search Indexes](#creating-search-indexes)
- [Contributing](#contributing)
- [Testing](#testing)
- [Docker Integration Tests](#docker-integration-tests)
- [Unit Tests](#unit-tests)
- [Contributing Tests](#contributing-tests)
- [License](#license)
## Architecture
The architecture of `Parse::Stack` is broken into four main components.
### [Parse::Client](https://neurosynq.github.io/parse-stack-next/Parse/Client.html)
This class is the core and low level API for the Parse Server REST interface that is used by the other components. It can manage multiple sessions, which means you can have multiple client instances pointing to different Parse Server applications at the same time. It handles sending raw requests as well as providing Request/Response objects for all API handlers. The connection engine is Faraday, which means it is open to add any additional middleware for features you'd like to implement.
### [Parse::Query](https://neurosynq.github.io/parse-stack-next/Parse/Query.html)
This class implements the [Parse REST Querying](http://docs.parseplatform.org/rest/guide/#queries) interface in the [DataMapper finder syntax style](http://datamapper.org/docs/find.html). It compiles a set of query constraints and utilizes `Parse::Client` to send the request and provide the raw results. This class can be used without the need to define models.
### [Parse::Object](https://neurosynq.github.io/parse-stack-next/Parse/Object.html)
This component is main class for all object relational mapping subclasses for your application. It provides features in order to map your remote Parse records to a local ruby object. It implements the Active::Model interface to provide a lot of additional features, CRUD operations, querying, including dirty tracking, JSON serialization, save/destroy callbacks and others. While we are overlooking some functionality, for simplicity, you will mainly be working with Parse::Object as your superclass. While not required, it is highly recommended that you define a model (Parse::Object subclass) for all the Parse classes in your application.
### [Parse::Webhooks](https://neurosynq.github.io/parse-stack-next/Parse/Webhooks.html)
Parse provides a feature called [Cloud Code Webhooks](http://blog.parse.com/announcements/introducing-cloud-code-webhooks/). For most applications, save/delete triggers and cloud functions tend to be implemented by Parse's own hosted Javascript solution called Cloud Code. However, Parse provides the ability to have these hooks utilize your hosted solution instead of their own, since their environment is limited in terms of resources and tools.
## Field Naming Conventions
By convention in Ruby (see [Style Guide](https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars)), symbols and variables are expressed in lower_snake_case form. Parse, however, prefers column names in **lower-first camel case** (ex. `objectId`, `createdAt` and `updatedAt`). To keep in line with the style guides between the languages, we do the automatic conversion of the field names when compiling the query. As an additional exception to this rule, the field key of `id` will automatically be converted to the `objectId` field when used. If you do not want this to happen, you can turn off or change the value `Parse::Query.field_formatter` as shown below. Though we recommend leaving the default `:columnize` if possible.
```ruby
# default uses :columnize
query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
query.compile_where # {"fieldOne"=>1, "fieldTwo"=>2, "fieldThree"=>3}
# turn off
Parse::Query.field_formatter = nil
query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
query.compile_where # {"field_one"=>1, "FieldTwo"=>2, "Field_Three"=>3}
# force everything camel case
Parse::Query.field_formatter = :camelize
query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3
query.compile_where # {"FieldOne"=>1, "FieldTwo"=>2, "FieldThree"=>3}
```
## Connection Setup
To connect to a Parse server, you will need a minimum of an `application_id`, an `api_key` and a `server_url`. To connect to the server endpoint, you use the `Parse.setup()` method below.
```ruby
Parse.setup app_id: "YOUR_APP_ID",
api_key: "YOUR_API_KEY",
master_key: "YOUR_MASTER_KEY", # optional
server_url: 'https://localhost:2337/parse' #default
```
If you wish to add additional connection middleware to the stack, you may do so by utilizing passing a block to the setup method.
```ruby
Parse.setup( ... ) do |conn|
# conn is a Faraday connection object
conn.use Your::Middleware
conn.response :logger
# ....
end
```
Calling `setup` will create the default `Parse::Client` session object that will be used for all models and requests in the stack. You may retrive this client by calling the class `client` method. It is possible to create different client connections and have different models point to different Parse applications and endpoints at the same time.
```ruby
default_client = Parse.client
# alias Parse::Client.client(:default)
```
### Connection Options
There are additional connection options that you may pass the setup method when creating a `Parse::Client`.
#### `:server_url`
The server url of your Parse Server if you are not using the hosted Parse service. By default it will use `PARSE_SERVER_URL` environment variable available or fall back to `https://localhost:2337/parse` if not specified.
#### `:app_id`
The Parse application id. By default it will use `PARSE_SERVER_APPLICATION_ID` environment variable if not specified.
#### `:api_key`
The Parse REST API Key. By default it will use `PARSE_SERVER_REST_API_KEY` environment variable if not specified.
#### `:master_key` _(optional)_
The Parse application master key. If this key is set, it will be sent on every request sent by the client and your models. By default it will use `PARSE_SERVER_MASTER_KEY` environment variable if not specified.
#### `:logging`
Controls request/response logging. Accepts:
- `true` - Enable logging at `:info` level (logs method, URL, status, timing)
- `:debug` - Enable verbose logging with headers and body content
- `:warn` - Only log errors and warnings
- `false` or `nil` - Disable logging (default)
```ruby
Parse.setup(logging: true, ...) # info level
Parse.setup(logging: :debug, ...) # verbose with body content
```
#### `:logger`
A custom Logger instance for request/response logging. Defaults to `Logger.new(STDOUT)`.
```ruby
Parse.setup(logging: true, logger: Rails.logger, ...)
```
You can also configure logging programmatically after setup:
```ruby
Parse.logging_enabled = true # Enable/disable
Parse.log_level = :debug # :info, :debug, or :warn
Parse.logger = Rails.logger # Custom logger
Parse.log_max_body_length = 1000 # Truncate body after N chars (default: 500)
```
#### `:adapter`
The HTTP connection adapter. By default, Parse Stack uses `:net_http_persistent` for connection pooling, which significantly improves performance by reusing HTTP connections. Set `connection_pooling: false` to use the standard `Net::HTTP` adapter instead.
```ruby
# Use a custom adapter (overrides connection_pooling setting)
Parse.setup(adapter: :excon, ...)
```
#### `:connection_pooling`
Controls HTTP connection pooling for improved performance. Enabled by default using the `net_http_persistent` adapter.
**Benefits:**
- 30-70% latency reduction by eliminating TCP/SSL handshakes per request
- Reduced server load through connection reuse
- Better performance for high-throughput applications
```ruby
# Default: connection pooling enabled
Parse.setup(server_url: "...", app_id: "...", api_key: "...")
# Disable connection pooling
Parse.setup(connection_pooling: false, ...)
# Custom pool configuration
Parse.setup(
connection_pooling: {
pool_size: 5, # Connections per thread (default: 1)
idle_timeout: 60, # Seconds before closing idle connections (default: 5)
keep_alive: 60 # HTTP Keep-Alive timeout in seconds
},
...
)
```
**Configuration Options:**
| Option | Default | Description |
|--------|---------|-------------|
| `pool_size` | 1 | Number of connections per thread. Increase if making parallel requests within a thread. |
| `idle_timeout` | 5 | Seconds before closing idle connections. Set higher (30-60s) for frequently-used servers. |
| `keep_alive` | - | HTTP Keep-Alive timeout. Should be less than your Parse Server's `keepAliveTimeout`. |
**Recommended settings for Heroku:**
```ruby
Parse.setup(
connection_pooling: { pool_size: 2, idle_timeout: 60, keep_alive: 60 },
...
)
```
If `faraday-net_http_persistent` is not available, Parse Stack automatically falls back to the standard adapter with a warning.
#### `:cache`
A caching adapter of type `Moneta::Transformer`. Caching queries and object fetches can help improve the performance of your application, even if it is for a few seconds. Only successful `GET` object fetches and queries (non-empty) will be cached. You may set the default expiration time with the `expires` option. See related: [Moneta](https://github.com/minad/moneta). At any point in time you may clear the cache by calling the `clear_cache!` method on the client connection.
```ruby
store = Moneta.new :Redis, url: 'redis://localhost:6379'
# use a Redis cache store with an automatic expire of 10 seconds.
Parse.setup(cache: store, expires: 10, ...)
```
As a shortcut, if you are planning on using REDIS and have configured the use of `redis` in your `Gemfile`, you can just pass the REDIS connection string directly to the cache option.
```ruby
Parse.setup(cache: 'redis://localhost:6379', ...)
```
Redis is the recommended cache backend for multi-process / multi-dyno deployments: an in-memory `Moneta.new(:Memory)` store is local to a single Ruby process, so two Puma workers (or two web dynos) each hold their own cache and a write through one will not invalidate the other. A shared Redis backend gives every process the same view, and the existing PUT/POST/DELETE invalidation in `Parse::Middleware::Caching` runs against the shared store. Cache reads degrade gracefully on `Redis::CannotConnectError` / `Redis::TimeoutError` — the middleware disables caching for the failing request and lets the underlying GET pass through to Parse Server.
The cache surface is opt-in at two layers. Object fetches (`Model.find(id)`, `obj.reload!` in non-write-only mode) cache by default once a store is configured. Query results do **not** cache by default — pass `cache: true` per call (e.g. `Song.all(limit: 500, cache: true)`) or set `Parse.default_query_cache = true` for opt-out behavior. Both layers honor `cache: false` / `Cache-Control: no-cache` to skip the cache for an individual request.
#### `:expires`
Sets the default cache expiration time (in seconds) for successful non-empty `GET` requests when using the caching middleware. The default value is 3 seconds. If `:expires` is set to 0, caching will be disabled. You can always clear the current state of the cache using the `clear_cache!` method on your `Parse::Client` instance.
#### `:faraday`
You may pass a hash of options that will be passed to the `Faraday` constructor.
### Global Settings
#### `Parse.warn_on_query_issues`
Controls whether query validation warnings are displayed. When enabled (default: `true`), Parse-Stack will print helpful warnings about common query mistakes:
- Warning when including non-pointer fields (e.g., including a string field that doesn't need `include`)
- Warning when including a pointer AND specifying subfield keys (redundant - the full object makes the subfield keys unnecessary)
```ruby
# Disable query validation warnings globally
Parse.warn_on_query_issues = false
# Example warnings that may be shown when enabled:
# [Parse::Query] Warning: 'filename' is a string field, not a pointer/relation - it does not need to be included
# [Parse::Query] Warning: including 'project' returns the full object - keys ["project.name"] are unnecessary
```
#### N+1 Query Detection
Parse Stack can detect N+1 query patterns - a common performance issue where accessing associations in a loop triggers separate queries for each item.
**Enable Detection:**
```ruby
# Warning mode (logs warnings)
Parse.n_plus_one_mode = :warn
# Or use the legacy API
Parse.warn_on_n_plus_one = true
```
**Example:**
```ruby
Parse.n_plus_one_mode = :warn
songs = Song.all(limit: 100)
songs.each do |song|
song.artist.name # Warning: N+1 query detected!
end
# Output:
# [Parse::N+1] Warning: N+1 query detected on Song.artist (3 separate fetches for Artist)
# Location: app/controllers/songs_controller.rb:42 in `index`
# Suggestion: Use `.includes(:artist)` to eager-load this association
```
**Fix with Includes:**
```ruby
# Eager-load associations to avoid N+1
songs = Song.all(limit: 100, includes: [:artist])
songs.each do |song|
song.artist.name # No warning - already loaded
end
```
**Available Modes:**
| Mode | Behavior |
|------|----------|
| `:ignore` | Detection disabled (default) |
| `:warn` | Log warnings when N+1 detected |
| `:raise` | Raise `Parse::NPlusOneQueryError` - ideal for CI/tests |
**Strict Mode for CI/Tests:**
```ruby
# In test_helper.rb or rails_helper.rb
Parse.n_plus_one_mode = :raise
# Now N+1 queries will fail your tests!
```
**Custom Callbacks:**
```ruby
# Track N+1 patterns in your metrics
Parse.on_n_plus_one do |source_class, association, target_class, count, location|
StatsD.increment("n_plus_one.#{source_class}.#{association}")
end
```
**Configuration:**
```ruby
Parse.configure_n_plus_one do |config|
config.detection_window = 5.0 # Seconds to track related fetches (default: 2.0)
config.fetch_threshold = 5 # Fetches to trigger warning (default: 3)
end
```
## Working With Existing Schemas
If you already have a Parse application with defined schemas and collections, you can have Parse-Stack automatically generate the ruby Parse::Object subclasses instead of writing them on your own. Through this process, the framework will download all the defined schemas of all your collections, and infer the properties and associations defined. While this method is useful for getting started with the framework with an existing app, we highly recommend defining your own models. This would allow you to customize and utilize all the features available in Parse Stack.
```ruby
# after you have called Parse.setup
# Assume you have a Song and Artist collections defined remotely
Parse.auto_generate_models!
# You can now use them as if you defined them
artist = Artist.first
Song.all(artist: artist)
```
You can always combine both approaches by defining special attributes before you auto generate your models:
```ruby
# create a Song class, but only create the artist array pointer association.
class Song < Parse::Object
has_many :artists, through: :array
end
# Now let Parse Stack generate the rest of the properties and associations
# based on your remote schema. Assume there is a `title` field for the `Song`
# collection.
Parse.auto_generate_models!
song = Song.first
song.artists # created with our definition above
song.title # auto-generated property
```
## [Parse Config](https://neurosynq.github.io/parse-stack-next/Parse/API/Config.html)
Getting your configuration variables once you have a default client setup can be done with `Parse.config`. The first time this method is called, Parse-Stack will get the configuration from Parse Server, and cache it. To force a reload of the config, use `config!`. You
```ruby
Parse.setup( ... )
val = Parse.config["myKey"]
val = Parse.config["myKey"] # cached
# update a config with Parse
Parse.set_config "myKey", "someValue"
# batch update several
Parse.update_config({fieldEnabled: true, searchMiles: 50})
# Force fetch of config!
val = Parse.config!["myKey"]
```
## Core Classes
While some native data types are similar to the ones supported by Ruby natively, other ones are more complex and require their dedicated classes.
### [Parse::Pointer](https://neurosynq.github.io/parse-stack-next/Parse/Pointer.html)
An important concept is the `Parse::Pointer` class. This is the superclass of `Parse::Object` and represents the pointer type in Parse. A `Parse::Pointer` only contains data about the specific Parse class and the `id` for the object. Therefore, creating an instance of any Parse::Object subclass with only the `:id` field set will be considered in "pointer" state even though its specific class is not `Parse::Pointer` type. The only case that you may have a Parse::Pointer is in the case where an object was received for one of your classes and the framework has no registered class handler for it. Using the example above, assume you have the tables `Post`, `Comment` and `Author` defined in your remote Parse application, but have only defined `Post` and `Commentary` locally.
```ruby
# assume the following
class Post < Parse::Object
end
class Commentary < Parse::Object
parse_class "Comment"
belongs_to :post
#'Author' class not defined locally
belongs_to :author
end
comment = Commentary.first
comment.post? # true because it is non-nil
comment.artist? # true because it is non-nil
# both are true because they are in a Pointer state
comment.post.pointer? # true
comment.author.pointer? # true
# we have defined a Post class handler
comment.post #
# we have not defined an Author class handler
comment.author #
comment.post.fetch # fetch the relation
comment.post.pointer? # false, it is now a full object.
```
#### Auto-fetch on Property Access
When you have a `Parse::Pointer` for a registered model class, you can access properties directly and the object will be automatically fetched:
```ruby
# Create a pointer (not yet fetched)
pointer = Post.pointer("abc123")
pointer.pointer? # true - no data yet
# Accessing a property auto-fetches and returns the value
pointer.title # Fetches the object, returns "My Post Title"
# Subsequent accesses use the cached fetched object (no additional network request)
pointer.content # Returns content without another fetch
pointer.author # Returns author without another fetch
# The pointer remembers the fetched object
pointer.pointer? # false - now has data
```
This auto-fetch behavior respects the `Parse.autofetch_raise_on_missing_keys` setting:
```ruby
Parse.autofetch_raise_on_missing_keys = true
pointer = Post.pointer("abc123")
pointer.title # Raises Parse::AutofetchTriggeredError instead of fetching
```
The effect is that for any unknown classes that the framework encounters, it will generate Parse::Pointer instances until you define those classes with valid properties and associations. While this might be ok for some classes you do not use, we still recommend defining all your Parse classes locally in the framework.
### [Parse::File](https://neurosynq.github.io/parse-stack-next/Parse/File.html)
This class represents a Parse file pointer. `Parse::File` has helper methods to upload Parse files directly to Parse and manage file associations with your classes. Using our Song class example:
```ruby
song = Song.first
file = song.audio_file # Parse::File
file.url # URL in the Parse file storage system
file = File.open("file_path.jpg")
contents = file.read
file = Parse::File.new("myimage.jpg", contents , "image/jpeg")
file.saved? # false. Hasn't been uploaded to Parse
file.save # uploads to Parse.
file.url # https://files.parsetfss.com/....
# or create and upload a remote file (auto-detected mime type)
file = Parse::File.create(some_url)
song.file = file
song.save
```
The default MIME type for all files is `image/jpeg`. This can be default can be changed by setting a value to `Parse::File.default_mime_type`. Other ways of creating a `Parse::File` are provided below. The created Parse::File is not uploaded until you call `save`.
```ruby
# urls
file = Parse::File.new "http://example.com/image.jpg"
file.name # image.jpg
# file objects
file = Parse::File.new File.open("myimage.jpg")
# non-image files work too
file = Parse::File.new "http://www.example.com/something.pdf"
file.mime_type = "application/octet-stream" #set the mime-type!
# or another Parse::File object
file = Parse::File.new parse_file
```
If you are using displaying these files on a secure site and want to make sure that urls returned by a call to `url` are `https`, you can set `Parse::File.force_ssl` to true.
```ruby
# Assume file is a Parse::File
file.url # => http://www.example.com/file.png
Parse::File.force_ssl = true # make all urls be https
file.url # => https://www.example.com/file.png
```
### [Parse::Date](https://neurosynq.github.io/parse-stack-next/Parse/Date.html)
This class manages dates in the special JSON format it requires for properties of type `:date`. `Parse::Date` subclasses `DateTime`, which allows you to use any features or methods available to `DateTime` with `Parse::Date`. While the conversion between `Time` and `DateTime` objects to a `Parse::Date` object is done implicitly for you, you can use the added special methods, `DateTime#parse_date` and `Time#parse_date`, for special occasions.
```ruby
song = Song.first
song.released = DateTime.now # converted to Parse::Date
song.save # ok
```
### [Parse::GeoPoint](https://neurosynq.github.io/parse-stack-next/Parse/GeoPoint.html)
This class manages the GeoPoint data type that Parse provides to support geo-queries. To define a GeoPoint property, use the `:geopoint` data type. Please note that latitudes should not be between -90.0 and 90.0, and longitudes should be between -180.0 and 180.0.
```ruby
class PlaceObject < Parse::Object
property :location, :geopoint
end
san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
los_angeles = Parse::GeoPoint.new [34.0192341, -118.970792]
san_diego == los_angeles # false
place = PlaceObject.new
place.location = san_diego
place.save
```
#### Calculating Distances between locations
We include helper methods to calculate distances between GeoPoints: `distance_in_miles` and `distance_in_km`.
```ruby
san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
los_angeles = Parse::GeoPoint.new [34.0192341, -118.970792]
# Haversine calculations
san_diego.distance_in_miles(los_angeles)
# ~112.33 miles
san_diego.distance_in_km(los_angeles)
# ~180.793 km
```
### [Parse::Bytes](https://neurosynq.github.io/parse-stack-next/Parse/Bytes.html)
The `Bytes` data type represents the storage format for binary content in a Parse column. The content is needs to be encoded into a base64 string.
```ruby
bytes = Parse::Bytes.new( base64_string )
# or use helper method
bytes = Parse::Bytes.new
bytes.encode( content ) # same as Base64.encode64
decoded = bytes.decoded # same as Base64.decode64
```
### [Parse::TimeZone](https://neurosynq.github.io/parse-stack-next/Parse/TimeZone.html)
While Parse does not provide a native time zone data type, Parse-Stack provides a class to make it easier to manage time zone attributes, usually stored IANA string identifiers, with your ruby code. This is done by utilizing the features provided by [`ActiveSupport::TimeZone`](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html). In addition to setting a column as a time zone field, we also add special validations to verify it is of the right IANA identifier.
```ruby
class Event < Parse::Object
# an event occurs in a time zone.
property :time_zone, :timezone, default: 'America/Los_Angeles'
end
event = Event.new
event.time_zone.name # => 'America/Los_Angeles'
event.time_zone.valid? # => true
event.time_zone.zone # => ActiveSupport::TimeZone
event.time_zone.formatted_offset # => "-08:00"
event.time_zone = 'Europe/Paris'
event.time_zone.formatted_offset # => +01:00"
event.time_zone = 'Galaxy/Andromeda'
event.time_zone.valid? # => false
```
### [Parse::ACL](https://neurosynq.github.io/parse-stack-next/Parse/ACL.html)
The `ACL` class represents the access control lists for each record. An ACL is represented by a JSON object with the keys being `Parse::User` object ids or the special key of `*`, which indicates the public access permissions.
The value of each key in the hash is a [`Parse::ACL::Permission`](https://neurosynq.github.io/parse-stack-next/Parse/ACL/Permission.html) object which defines the boolean permission state for `read` and `write`.
The example below illustrates a Parse ACL JSON object where there is a public read permission, but public write is prevented. In addition, the user with id `3KmCvT7Zsb` and the `Admins` role, are allowed to both read and write on this record.
```json
{
"*": { "read": true },
"3KmCvT7Zsb": { "read": true, "write": true },
"role:Admins": { "read": true, "write": true }
}
```
All `Parse::Object` subclasses have an `acl` property by default. With this property, you can apply and delete permissions for this particular Parse object record.
```ruby
user = Parse::User.first
artist = Artist.first
artist.acl # "*": { "read": true, "write": true }
# apply public read, but no public write
artist.acl.everyone true, false
# allow user to have read and write access
artist.acl.apply user.id, true, true
# remove all permissions for this user id
artist.acl.delete user.id
# allow the 'Admins' role read and write
artist.acl.apply_role "Admins", true, true
# remove write from all attached privileges
artist.acl.no_write!
# remove all attached privileges
artist.acl.master_key_only!
artist.save
```
You may also set default ACLs for newly created instances of your subclasses using `set_default_acl`:
```ruby
class AdminData < Parse::Object
# Disable public read and write
set_default_acl :public, read: false, write: false
# but allow members of the Admin role to read and write
set_default_acl 'Admin', role: true, read: true, write: true
end
data = AdminData.new
data.acl # => ACL({"role:Admin"=>{"read"=>true, "write"=>true}})
```
#### Declarative ACL Policy (`acl_policy`)
For owner-aware defaults — where the record's ACL should grant read/write to a specific user pointer at save time — declare an `acl_policy` instead of (or in addition to) `set_default_acl`. The policy is resolved by a `before_save` callback that walks `as: user` → owner-field pointer → policy fallback, and stamps the resolved ACL onto the record. Any explicit `obj.acl = …` change by the caller is always respected.
There are four policies:
| Policy | When an owner is resolvable | When no owner is resolvable |
|---|---|---|
| `:public` | public read + write | public read + write |
| `:public_read` | public read, master-key write | public read, master-key write |
| `:private` | master-key only | master-key only |
| `:owner_else_public` | owner read + write only | public read + write |
| `:owner_else_private` | owner read + write only | master-key only |
| `:owner_but_public_read` | owner read + write *and* public read | public read, master-key write |
`:public_read` (v5.0+) stamps `{"*": {"read": true}}` — anyone can read the row, but no client can mutate it through ACL (only the master key can write). Useful for catalog / lookup / reference data.
`:owner_but_public_read` (v5.0+) is the "single-author public post" case: the resolved owner gets full R/W and the rest of the world gets read-only access in the same ACL — `{"*": {"read": true}, "": {"read": true, "write": true}}`. When no owner resolves at save (no `as:` and no resolvable `owner:` field), it degrades to `:public_read` semantics rather than the all-or-nothing fallback used by the `:owner_else_*` family.
```ruby
class Post < Parse::Object
property :title, :string
belongs_to :author, as: :user
# Posts grant read/write to their author; server-side creates with no
# author resolvable fall back to master-key-only.
acl_policy :owner_else_private, owner: :author
end
# Owner resolved from the belongs_to pointer:
Post.create!(title: "draft", author: current_user)
# => ACL { "": { read: true, write: true } }
# Or pass the owner explicitly with `as:`:
Post.create!(title: "draft", as: current_user)
# Server-side, no owner: master-key-only fallback.
Post.create!(title: "system note")
# => ACL { } (only the master key can read or write)
```
Resolution order at save (only when the caller has not set the ACL):
1. `obj.acl = …` or in-place mutation of `obj.acl` by the caller — always wins
2. `as: user` passed at construction
3. Owner pointer from the property named by `owner:`
4. The "else" half of the policy — public R/W or master-key-only
The `:as` key may be a `Parse::User`, a `Parse::Pointer` to a user, or a raw `objectId` string. It is popped from the opts hash before attributes are applied, so it never reaches `apply_attributes!` and never appears as a property.
Subclasses inherit the parent's policy and owner field. Classes that already call `set_default_acl` are detected automatically and opt out of the policy resolver, so legacy callers retain pre-4.1 behavior without changes.
Owner resolution is strictly type-gated. The `as:` kwarg and any `owner:` pointer accept a `Parse::User` instance, a `Parse::Pointer` whose `parse_class == "_User"`, or a raw `objectId` `String`. Pointers to non-User classes and arbitrary objects responding to `#id` are silently rejected and the policy falls through to its else-half, so a stray pointer to a non-user record cannot accidentally grant ACL access to a user record that happens to share the same `objectId`.
You may not combine `acl_policy` with `set_default_acl` on the same class — the two APIs have ambiguous interactions at save time. Calling the second one raises `ArgumentError`. Pick one configuration approach per class.
#### Self-Owned Users (`owner: :self`)
`Parse::User` records are special: the record IS the owner. The SDK provides `owner: :self` as a Parse::User-only shorthand for "this user owns themselves." The save-time resolver pre-generates a Parse-compatible `objectId` client-side (via the same helper that backs `parse_reference precompute: true`) when none is set, then stamps the ACL as `{ : { read: true, write: true } }`. The signup body then carries both the `objectId` and the `ACL` in a single POST.
```ruby
class Parse::User
# New users: only the user can read or write their own profile.
acl_policy :owner_else_private, owner: :self
end
new_user = Parse::User.new(username: "alice", password: "secret")
new_user.save
# Single roundtrip. After save, new_user.id is a 10-char Parse id and
# the persisted record's ACL is { "": { read: true, write: true } }.
# Other clients (including unauthenticated) cannot see this user.
```
`owner: :self` is rejected at class-definition time on any non-User class — there's no sensible interpretation when the record's `objectId` is not a user id.
The signup request body normally has `objectId` and `ACL` stripped (a security mitigation against client-planted permissive ACLs). When `owner: :self` is declared, those two fields are allowed through only when they match the narrow self-only ownership pattern: `objectId` is the 10-char Parse format, and `ACL` has exactly one entry granting `read+write` to that same `objectId`. Any deviation — multiple keys, a `*` (public) entry, a `role:` entry, half-permissions, mismatched id — still triggers the full strip and Parse Server applies its own default.
`acl_policy ..., owner: :self` is orthogonal to `parse_reference precompute: true`. Both reuse `Parse::Core::ParseReference.generate_object_id` for client-side id generation; neither installs the other's side effects. Declare both if you want both the ACL self-ownership AND the canonical reference column.
#### Breaking Change in v4.1: Secure-by-Default ACL Policy
Starting with v4.1, the gem-wide default ACL policy for `Parse::Object` subclasses is `:owner_else_private`. Records created with no resolvable owner (no `as:` kwarg, no `owner:` field) and no class-level `acl_policy` or `set_default_acl` declaration are saved with an empty ACL — readable and writable only with the master key.
**This is a behavioral change.** Pre-4.1, the same class would have produced records with public read + public write. Applications that depend on the historical default for client-side reads of unowned records will see those reads return empty result sets until they update their model declarations.
Migration recipes:
```ruby
# A class whose records should remain publicly readable + writable:
class PublicNotice < Parse::Object
property :body, :string
acl_policy :public
end
# A class whose records belong to a user:
class JournalEntry < Parse::Object
property :text, :string
belongs_to :author, as: :user
acl_policy :owner_else_private, owner: :author
end
# A class whose records are written client-side but readable by anyone:
class Post < Parse::Object
property :title, :string
belongs_to :author, as: :user
acl_policy :owner_else_public, owner: :author
end
```
When a class explicitly opts into a permissive policy (`:public` or `:owner_else_public`), a one-time per-class warning is emitted on first instance creation to make the choice visible in logs:
```
[Parse::Stack security] PublicNotice uses permissive default ACL policy
`public`. New records can be modified by anyone unless an owner is
resolved at save. Call `acl_policy :owner_else_private` or `:private`
in the class to silence this warning.
```
The warning fires once per class per process and is automatically suppressed for the SDK's own built-in classes (`Parse::User`, `Parse::Installation`, `Parse::Session`, `Parse::Role`, `Parse::Product`, `Parse::PushStatus`, `Parse::Audience`, `Parse::JobStatus`, `Parse::JobSchedule`). To silence it globally — for example in test suites or in applications that have reviewed and accepted permissive defaults — set either:
```ruby
Parse::Object.suppress_permissive_acl_warning = true
# or, via the environment:
ENV["PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING"] = "1"
```
For more information about Parse record ACLs, see the documentation at [Security](http://docs.parseplatform.org/rest/guide/#security)
### Parse::CLP (Class-Level Permissions)
Class-Level Permissions (CLPs) control access at the schema level, determining who can perform operations on a class and which fields are visible to different users/roles. Unlike ACLs (which are per-object), CLPs apply to the entire class.
#### Defining CLPs in Models
Use the `set_clp` and `protect_fields` DSL methods to define CLPs:
```ruby
class Song < Parse::Object
property :title, :string
property :artist, :string
property :internal_notes, :string
property :royalty_data, :string
belongs_to :owner
# Set operation-level permissions
set_clp :find, public: true
set_clp :get, public: true
set_clp :create, public: false, roles: ["Admin", "Editor"]
set_clp :update, public: false, roles: ["Admin", "Editor"]
set_clp :delete, public: false, roles: ["Admin"]
# Protect fields from certain users (use camelCase for JSON field names)
protect_fields "*", [:internalNotes, :royaltyData] # Hidden from everyone
protect_fields "role:Admin", [] # Admins see everything
protect_fields "userField:owner", [] # Owners see their own data
end
```
**Supported Operations:** `:find`, `:get`, `:count`, `:create`, `:update`, `:delete`, `:addField`
**Supported Patterns:**
- `"*"` - Public (everyone)
- `"role:RoleName"` - Users with specific role
- `"userField:fieldName"` - Users referenced in a pointer field
- `"authenticated"` - Any authenticated user
- User objectId string - Specific user
#### Filtering Data for Webhook Responses
When returning data from webhooks, use `filter_for_user` to apply CLP field protection:
```ruby
# In a webhook handler
def after_find(request)
user = request.user
roles = Song.roles_for_user(user)
# Filter each object for the requesting user
filtered_results = request.objects.map do |song|
song.filter_for_user(user, roles: roles)
end
# Or use the class method for arrays
filtered_results = Song.filter_results_for_user(request.objects, user, roles: roles)
{ objects: filtered_results }
end
```
#### Protected Fields Intersection Logic
When a user matches multiple patterns, the protected fields are the **intersection** of all matching patterns. A field is only hidden if it's protected by ALL patterns that apply to the user:
```ruby
protect_fields "*", [:owner, :secret, :internal] # Hide from everyone
protect_fields "role:Admin", [:owner] # Admins: only owner hidden
protect_fields "userField:owner", [] # Owners see everything
# User with Admin role matches "*" and "role:Admin":
# - "*" protects: [owner, secret, internal]
# - "role:Admin" protects: [owner]
# - Intersection: [owner] - only this field is hidden
# - "secret" and "internal" become visible (cleared by role pattern)
# An empty array [] means "no fields protected" (user sees everything)
# If ANY matching pattern has [], the intersection is empty (nothing hidden)
```
#### Push CLPs to Parse Server
CLPs are automatically included when upgrading schemas:
```ruby
# Include CLPs in schema upgrade (default)
Song.auto_upgrade!
# Skip CLPs during schema upgrade
Song.auto_upgrade!(include_clp: false)
# Update only CLPs (no schema changes)
Song.update_clp!
```
#### Fetch and Inspect CLPs
```ruby
# Fetch current CLPs from server
clp = Song.fetch_clp
# Check operation permissions
clp.find_allowed?("*") # => true (public find allowed)
clp.create_allowed?("*") # => false (public create denied)
clp.role_allowed?(:create, "Admin") # => true
clp.requires_authentication?(:update) # => false
# Get protected fields for a pattern
clp.protected_fields_for("*") # => ["internalNotes", "royaltyData"]
clp.protected_fields_for("role:Admin") # => []
# Use fetched CLP for filtering
filtered = song.filter_for_user(user, roles: roles, clp: clp)
```
#### Owner-Based Access with userField
The `userField:fieldName` pattern allows owners (users referenced in a pointer field) to have different visibility:
```ruby
class Document < Parse::Object
property :content, :string
property :secret, :string
belongs_to :owner
# Hide secret and owner from everyone
protect_fields "*", [:secret, :owner]
# But owners of the document can see everything
protect_fields "userField:owner", []
end
# When filtering:
doc_data = {
"content" => "Public content",
"secret" => "Private data",
"owner" => { "objectId" => "user123", "__type" => "Pointer" }
}
clp = Document.class_permissions
# Owner sees everything
clp.filter_fields(doc_data, user: "user123")
# => { "content" => "...", "secret" => "...", "owner" => {...} }
# Non-owner has protected fields hidden
clp.filter_fields(doc_data, user: "other_user")
# => { "content" => "..." }
```
This also works with arrays of pointers (e.g., `owners: [user1, user2]`).
### [Parse::Session](https://neurosynq.github.io/parse-stack-next/Parse/Session.html)
This class represents the data and columns contained in the standard Parse `_Session` collection. You may add additional properties and methods to this class. See [Session API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Session.html). You may call `Parse.use_shortnames!` to use `Session` in addition to `Parse::Session`.
You can get a specific `Parse::Session` given a session_token by using the `session` method. You can also find the user tied to a specific Parse session or session token with `Parse::User.session`.
```ruby
session = Parse::Session.session(token)
session.user # the Parse user for this session
# or fetch user with a session token
user = Parse::User.session(token)
# save an object with the privileges (ACLs) of this user
some_object.save( session: user.session_token )
# delete an object with the privileges of this user
some_object.destroy( session: user.session_token )
```
### [Parse::Installation](https://neurosynq.github.io/parse-stack-next/Parse/Installation.html)
This class represents the data and columns contained in the standard Parse `_Installation` collection. You may add additional properties and methods to this class. See [Installation API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Installation.html). You may call `Parse.use_shortnames!` to use `Installation` in addition to `Parse::Installation`.
### [Parse::Product](https://neurosynq.github.io/parse-stack-next/Parse/Product.html)
This class represents the data and columns contained in the standard Parse `_Product` collection. You may add additional properties and methods to this class. See [Product API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Product.html). You may call `Parse.use_shortnames!` to use `Product` in addition to `Parse::Product`.
The `_Product` collection backs the original Parse iOS SDK's `PFProduct` downloadable-content in-app-purchase flow. That feature was tied to hosted Parse and is not actively used by modern Parse Server deployments — most apps now verify in-app purchase receipts directly against the Apple App Store or Google Play. The class is retained for backwards compatibility with legacy applications that still read or write product metadata. It is also marked `agent_hidden` by default so it does not surface through MCP / agent tooling; applications that genuinely need agent access can call `Parse::Product.agent_unhidden` at boot.
### [Parse::Role](https://neurosynq.github.io/parse-stack-next/Parse/Role.html)
This class represents the data and columns contained in the standard Parse `_Role` collection. You may add additional properties and methods to this class. See [Roles API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Role.html). You may call `Parse.use_shortnames!` to use `Role` in addition to `Parse::Role`.
#### Default ACL (master-only)
Parse Server requires every `_Role` row to ship with an ACL — the requirement is hard-coded in `SchemaController.requiredColumns` and cannot be disabled by config. `Parse::Role` declares `acl_policy :private`, so every role saved without an explicit ACL is stamped with `{}` (master-key only). This is intentional: anonymous and authenticated-but-non-master clients cannot enumerate role names, read subscription, or walk the role hierarchy. Parse Server's internal role-subscription expansion (used during ACL evaluation) runs with master context, so the master-only default does not break permission checks on other classes.
To opt into broader access, pass an explicit ACL:
```ruby
acl = Parse::ACL.new
acl.everyone(true, false) # public read, no public write
admin = Parse::Role.find_or_create("Admin", acl: acl)
# or on an instance:
role = Parse::Role.new(name: "Editor")
role.acl = acl
role.save
```
The explicit ACL bypasses the policy resolver — caller-supplied ACLs are never overwritten.
#### Role Management Helpers
Parse::Role provides convenient methods for managing users and role hierarchies:
```ruby
# Find or create roles
admin = Parse::Role.find_by_name("Admin")
moderator = Parse::Role.find_or_create("Moderator")
# Manage users
admin.add_user(user).save
admin.add_users(user1, user2, user3).save
admin.remove_user(user).save
admin.has_user?(user) # => true
# Role hierarchy (Admins inherit Moderator permissions)
admin.add_child_role(moderator).save
admin.has_child_role?(moderator) # => true
admin.all_child_roles # => All child roles recursively
admin.all_users # => Users from this role AND child roles
# Counts
admin.users_count # Direct users
admin.child_roles_count # Direct child roles
admin.total_users_count # All users including child roles
```
### [Parse::JobStatus](https://neurosynq.github.io/parse-stack-next/Parse/JobStatus.html)
This class represents the data and columns contained in the standard Parse `_JobStatus` collection. Parse Server writes a row here every time a background job — registered server-side via `Parse.Cloud.job(...)` — runs, recording its outcome and any status/message updates emitted via `request.message(...)`.
This Ruby SDK cannot *define* a job (cloud-code registrations live in server-side JavaScript), but you can read from `_JobStatus` to display the most recent run of a job, count failed runs, or sweep old history rows. `Parse::JobStatus` is marked `agent_hidden` by default — `_JobStatus` carries operational signal (job names, error traces, scheduler parameters) that an LLM-driven agent should not enumerate unsolicited. Applications that need agent visibility can call `Parse::JobStatus.agent_unhidden` at boot.
```ruby
# Did the nightly cleanup run today? What's the latest state?
latest = Parse::JobStatus.latest_for("nightlyCleanup")
puts "#{latest.status} at #{latest.created_at}"
puts "Duration: #{latest.duration}s" if latest.finished?
# Find failed jobs in the last 24h
yesterday = Time.now - 86_400
Parse::JobStatus.failed.where(:created_at.gt => yesterday).all
# Status scopes
Parse::JobStatus.running # => Parse::Query
Parse::JobStatus.succeeded # => Parse::Query
Parse::JobStatus.failed # => Parse::Query
Parse::JobStatus.recent(limit: 50)
# Instance predicates
js.running? # status == "running"
js.succeeded? # status == "succeeded"
js.failed? # status == "failed"
js.finished? # finished_at present OR status terminal
js.duration # finished_at - created_at, or nil while in-flight
```
#### Cleanup helper
Parse Server does not garbage-collect `_JobStatus` rows on its own; long-running deployments accumulate run history indefinitely. `Parse::JobStatus.cleanup_older_than!` mirrors `Parse::Installation.cleanup_stale_tokens!` for this case:
```ruby
# Default: only destroy rows in a terminal state (succeeded/failed)
# and older than 30 days. Orphaned `status == "running"` rows (from a
# crashed worker) are PRESERVED so an in-flight job is never reaped
# mid-execution.
deleted_count = Parse::JobStatus.cleanup_older_than!(days: 30)
# Explicit orphan cleanup: drop the status guard.
Parse::JobStatus.cleanup_older_than!(days: 7, terminal_only: false)
```
The helper requires master-key access (Parse Server's default `_JobStatus` CLP). Run from a periodic cron or scheduled job to keep `_JobStatus` from growing unboundedly.
### [Parse::JobSchedule](https://neurosynq.github.io/parse-stack-next/Parse/JobSchedule.html)
This class represents the data and columns contained in the standard Parse `_JobSchedule` collection. Rows here define recurring runs for background jobs registered via `Parse.Cloud.job(...)`. The collection is populated by the Parse Dashboard's "Schedule a Job" UI.
**Note:** Parse Server itself does not auto-trigger jobs from `_JobSchedule` rows. The actual dispatch is performed by external scheduling tooling (e.g. `parse-server-scheduler`, dashboard-driven cron wrappers, or a sidecar process) that reads `_JobSchedule` and fires `POST /parse/jobs/` at the appropriate times. Run-status rows then appear in `Parse::JobStatus`.
`Parse::JobSchedule` is marked `agent_hidden` by default because `params` may carry credentials or destination configuration written by external schedulers.
```ruby
schedule = Parse::JobSchedule.for_job("nightlyCleanup").first
schedule.time_of_day # => "03:00:00"
schedule.days_of_week # => ["mon","tue","wed","thu","fri"]
schedule.parsed_params # => { "dryRun" => false } — JSON-decoded
```
`params` is stored on the wire as a JSON-encoded **string** per Parse Server's canonical schema (Object columns reject `$` and `.` in nested keys, which would otherwise break common payload shapes). Use `#parsed_params` to decode; it returns `nil` for blank or invalid JSON instead of raising. `last_run` is a raw `Number` whose unit is scheduler-defined — most external schedulers write `Date.now()` milliseconds, but the canonical schema does not pin a unit.
### [Parse::User](https://neurosynq.github.io/parse-stack-next/Parse/User.html)
This class represents the data and columns contained in the standard Parse `_User` collection. You may add additional properties and methods to this class. See [User API Reference](https://neurosynq.github.io/parse-stack-next/Parse/User.html). You may call `Parse.use_shortnames!` to use `User` in addition to `Parse::User`.
#### Signup
You can signup new users in two ways. You can either use a class method `Parse::User.signup` to create a new user with the minimum fields of username, password and email, or create a `Parse::User` object can call the `signup!` method. If signup fails, it will raise the corresponding exception.
```ruby
user = Parse::User.signup(username, password, email)
#or
user = Parse::User.new username: "user", password: "s3cret"
user.signup!
```
##### Third-Party Services
You can signup users using third-party services like Facebook and Twitter as described in: [Signing Up and Logging In](http://docs.parseplatform.org/rest/guide/#signing-up). To do this with Parse-Stack, you can call the `Parse::User.autologin_service` method by passing the service name and the corresponding authentication hash data. For a listing of supported third-party authentication services, see [OAuth](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication).
```ruby
fb_auth = {}
fb_auth[:id] = "123456789"
fb_auth[:access_token] = "SaMpLeAAiZBLR995wxBvSGNoTrEaL"
fb_auth[:expiration_date] = "2025-02-21T23:49:36.353Z"
# signup or login a user with this auth data.
user = Parse::User.autologin_service(:facebook, fb_auth)
```
You may also combine both approaches of signing up a new user with a third-party service and set additional custom fields. For this, use the method `Parse::User.create`.
```ruby
# or to signup a user with additional data, but linked to Facebook
data = {
username: "johnsmith",
name: "John",
email: "user@example.com",
authData: { facebook: fb_auth }
}
user = Parse::User.create data
```
#### Login and Sessions
With the `Parse::User` class, you can also perform login and logout functionality. The class special accessors for `session_token` and `session` to manage its authentication state. This will allow you to authenticate users as well as perform Parse queries as a specific user using their session token. To login a user, use the `Parse::User.login` method by supplying the corresponding username and password, or if you already have a user record, use `login!` with the proper password.
```ruby
user = Parse::User.login(username,password)
user.session_token # session token from a Parse::Session
user.session # Parse::Session tied to the token
# You can login user records
user = Parse::User.first
user.session_token # nil
passwd = 'p_n7!-e8' # corresponding password
user.login!(passwd) # true
user.session_token # 'r:pnktnjyb996sj4p156gjtp4im'
# logout to delete the session
user.logout
```
If you happen to already have a valid session token, you can use it to retrieve the corresponding Parse::User.
```ruby
# finds user with session token
user = Parse::User.session(session_token)
user.logout # deletes the corresponding session
```
#### Linking and Unlinking
You can link or unlink user accounts with third-party services like Facebook and Twitter as described in: [Linking and Unlinking Users](http://docs.parseplatform.org/rest/guide/#linking-users). To do this, you must first get the corresponding authentication data for the specific service, and then apply it to the user using the linking and unlinking methods. Each method returns true or false if the action was successful. For a listing of supported third-party authentication services, see [OAuth](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication).
```ruby
user = Parse::User.first
fb_auth = { ... } # Facebook auth data
# Link this user's Facebook account with Parse
user.link_auth_data! :facebook, fb_auth
# Unlinks this user's Facebook account from Parse
user.unlink_auth_data! :facebook
```
#### Request Password Reset
You can reset a user's password using the `Parse::User.request_password_reset` method.
```ruby
user = Parse::User.first
# pass a user object
Parse::User.request_password_reset user
# or email
Parse::User.request_password_reset("user@example.com")
```
#### Multi-Factor Authentication (MFA)
Parse-Stack provides comprehensive MFA support that integrates with Parse Server's built-in MFA adapter. This enables TOTP (Time-based One-Time Password) authentication with apps like Google Authenticator, Authy, or 1Password.
**Prerequisites:**
- Parse Server must have the MFA adapter enabled
- Add optional gems to your Gemfile: `gem 'rotp'` and `gem 'rqrcode'`
**Parse Server Configuration:**
```javascript
{
auth: {
mfa: {
enabled: true,
options: ["TOTP"],
digits: 6,
period: 30,
algorithm: "SHA1"
}
}
}
```
**Setting Up MFA:**
```ruby
# Configure the issuer name shown in authenticator apps
Parse::MFA.configure do |config|
config[:issuer] = "MyApp"
end
# Step 1: Generate a TOTP secret
secret = Parse::MFA.generate_secret
# Step 2: Display QR code to the user
qr_svg = user.mfa_qr_code(secret, issuer: "MyApp")
# Render in HTML: <%= raw qr_svg %>
# Step 3: User scans QR and enters code from their authenticator
recovery_codes = user.setup_mfa!(secret: secret, token: "123456")
# IMPORTANT: Display recovery codes to user - they can only see them once!
```
**Logging In with MFA:**
```ruby
# Login with username, password, and MFA token
user = Parse::User.login_with_mfa("username", "password", "123456")
# Check if MFA is required before login
if Parse::User.mfa_required?("username")
# Prompt for MFA token
end
```
**Managing MFA:**
```ruby
# Check MFA status
user.mfa_enabled? # => true
user.mfa_status # => :enabled, :disabled, or :unknown
# Disable MFA (requires current token)
user.disable_mfa!(current_token: "123456")
# Admin reset (requires master key)
user.disable_mfa_admin!
```
**SMS MFA (requires Parse Server SMS callback):**
```ruby
# Initiate SMS setup
user.setup_sms_mfa!(mobile: "+1234567890")
# Confirm with received code
user.confirm_sms_mfa!(mobile: "+1234567890", token: "123456")
```
**Error Handling:**
```ruby
begin
user = Parse::User.login_with_mfa(username, password, token)
rescue Parse::MFA::RequiredError
# MFA token was not provided but is required
rescue Parse::MFA::VerificationError
# Invalid MFA token
end
```
## Modeling and Subclassing
For the general case, your Parse classes should inherit from `Parse::Object`. `Parse::Object` utilizes features from `ActiveModel` to add several features to each instance of your subclass. These include `Dirty`, `Conversion`, `Callbacks`, `Naming` and `Serializers::JSON`.
To get started use the `property` and `has_many` methods to setup declarations for your fields. Properties define literal values that are columns in your Parse class. These can be any of the base Parse data types. You will not need to define classes for the basic Parse class types - this includes "\_User", "\_Installation", "\_Session" and "\_Role". These are mapped to `Parse::User`, `Parse::Installation`, `Parse::Session` and `Parse::Role` respectively.
To get started, you define your classes based on `Parse::Object`. By default, the name of the class is used as the name of the remote Parse class. For a class `Post`, we will assume there is a remote camel-cased Parse table called `Post`. If you need to map the local class name to a different remote class, use the `parse_class` method.
```ruby
class Post < Parse::Object
# assumes Parse class "Post"
end
class Commentary < Parse::Object
# set remote class "Comment"
parse_class "Comment"
end
```
### Defining Properties
Properties are considered a literal-type of association. This means that a defined local property maps directly to a column name for that remote Parse class which contain the value. **All properties are implicitly formatted to map to a lower-first camelcase version in Parse (remote).** Therefore a local property defined as `like_count`, would be mapped to the remote column of `likeCount` automatically. The only special behavior to this rule is the `:id` property which maps to `objectId` in Parse. This implicit conversion mapping is the default behavior, but can be changed on a per-property basis. All Parse data types are supported and all Parse::Object subclasses already provide definitions for `:id` (objectId), `:created_at` (createdAt), `:updated_at` (updatedAt) and `:acl` (ACL) properties.
- **:string** (_default_) - a generic string. Can be used as an enum field, see [Enum](#enum).
- **:integer** (alias **:int**) - basic number. Will also generate atomic `_increment!` helper method.
- **:float** - a floating numeric value. Will also generate atomic `_increment!` helper method.
- **:boolean** (alias **:bool**) - true/false value. This will also generate a class scope helper. See [Query Scopes](#query-scopes).
- **:date** - a Parse date type. See [Parse::Date](#parsedate).
- **:timezone** - a time zone object. See [Parse::TimeZone](#parsetimezone).
- **:array** - a heterogeneous list with dirty tracking. See [Parse::CollectionProxy](https://github.com/modernistik/parse-stack/blob/master/lib/parse/model/associations/collection_proxy.rb).
- **:file** - a Parse file type. See [Parse::File](#parsefile).
- **:geopoint** - a GeoPoint type. See [Parse::GeoPoint](#parsegeopoint).
- **:bytes** - a Parse bytes data type managed as base64. See [Parse::Bytes](#parsebytes).
- **:object** - an object "hash" data type. See [ActiveSupport::HashWithIndifferentAccess](http://apidock.com/rails/ActiveSupport/HashWithIndifferentAccess).
For completeness, the `:id` and `:acl` data types are also defined in order to handle the Parse `objectId` field and the `ACL` object. Those are special and should not be used in your class (unless you know what you are doing). New data types can be implemented through the internal `typecast` interface. **TODO: discuss `typecast` interface in the future**
When declaring a `:boolean` data type, it will also create a special method that uses the `?` convention. As an example, if you have a property named `approved`, the normal getter `obj.approved` can return true, false or nil based on the value in Parse. However with the `obj.approved?` method, it will return true if it set to true, false for any other value.
When declaring an `:integer` or `:float` type, it will also create a special method that performs
an atomic increment of that field through the `_increment!` and `_decrement!` methods. If you have
defined a property named `like_count` for one of these numeric types, which would create the normal getter/setter `obj.like_count`; you can now also call `obj.like_count_increment!` or `obj.like_count_decrement!` to perform the atomic operations (done server side) on this field. You may also pass an amount as an argument to these helper methods such as `obj.like_count_increment!(3)`.
Using the example above, we can add the base properties to our classes.
```ruby
class Post < Parse::Object
property :title
property :content, :string # explicit
# treat the values of this field as symbols instead of strings.
property :category, :string, symbolize: true
# maybe a count of comments.
property :comment_count, :integer, default: 0
# use lambda to access the instance object.
# Set draft_date to the created_at date if empty.
property :draft_date, :date, default: lambda { |x| x.created_at }
# the published date. Maps to "publishDate"
property :publish_date, :date, default: lambda { |x| DateTime.now }
# maybe whether it is currently visible
property :visible, :boolean
# a list using
property :tags, :array
# string column as enumerated type. see :enum
property :status, enum: [:active, :archived]
# Maps to "featuredImage" column representing a File.
property :featured_image, :file
property :location, :geopoint
# Support bytes
property :data, :bytes
# A field that contains time zone information (ex. 'America/Los_Angeles')
property :time_zone, :timezone
# store SEO information. Make sure we map it to the column
# "SEO", otherwise it would have implicitly used "seo"
# as the remote column name
property :seo, :object, field: "SEO"
end
```
After properties are defined, you can use appropriate getter and setter methods to modify the values. As properties become modified, the model will keep track of the changes using the [dirty tracking feature of ActiveModel](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html). If an attribute is modified in-place then make use of **[attribute_name]_will_change!** to mark that the attribute is changing. Otherwise ActiveModel can't track changes to in-place attributes.
To support dirty tracking on properties of data type of `:array`, we utilize a proxy class called `Parse::CollectionProxy`. This class has special functionality which allows lazy loading of content as well and keeping track of the changes that are made. While you are able to access the internal array on the collection through the `#collection` method, it is important not to make in-place edits to the object. You should use the preferred methods of `#add` and `#remove` to modify the contents of the collection. When `#save` is called on the object, the changes will be committed to Parse.
```ruby
post = Post.first
post.tags.each do |tag|
puts tag
end
post.tags.empty? # false
post.tags.count # 3
array = post.tags.to_a # get array
# Add
post.tags.add "music", "tech"
post.tags.remove "stuff"
post.save # commit changes
```
#### Accessor Aliasing
To enable easy conversion between incoming Parse attributes, which may be different than the locally labeled attribute, we make use of aliasing accessors with their remote field names. As an example, for a `Post` instance and its `publish_date` property, it would have an accessor defined for both `publish_date` and `publishDate` (or whatever value you passed as the `:field` option) that map to the same attribute. We highly discourage turning off this feature, but if you need to, you can pass the value of `false` to the `:alias` option when defining the property.
```ruby
# These are equivalent
post.publish_date = DateTime.now
post.publishDate = DateTime.now
post.publish_date == post.publishDate
post.seo # ok
post.SEO # the alias method since 'field: "SEO"'
```
#### Property Options
These are the supported options when defining properties. Parse::Objects are backed by `ActiveModel`, which means you can add additional validations and features supported by that library.
##### `:required`
A boolean property. This option provides information to the property builder that it is a required property. The requirement is not strongly enforced for a save, which means even though the value for the property may not be present, saves and updates can be successfully performed. However, the setting `required` to true, it will set some ActiveModel validations on the property to be used when calling `valid?`. By default it will add a `validates_presence_of` for the property key. If the data type of the property is either `:integer` or `:float`, it will also add a `validates_numericality_of` validation. Default `false`.
##### `:field`
This option allows you to set the name of the remote column for the Parse table. Using this will explicitly set the remote property name to the value of this option. The value provided for this option will affect the name of the alias method that is generated when `alias` option is used. **By default, the name of the remote column is the lower-first camelcase version of the property name. As an example, for a property with key `:my_property_name`, the framework will implicitly assume that the remote column is `myPropertyName`.**
##### `:default`
This option provides you to set a default value for a specific property when the getter accessor method is used and the internal value of the instance object's property is nil. It can either take a literal value or a Proc/lambda.
```ruby
class SomeClass < Parse::Object
# default value
property :category, default: "myValue"
# default value Proc style
property :date, default: lambda { |x| DateTime.now }
end
```
##### `:alias`
A boolean property. It is highly recommended that this is set to true, which is the default. This option allows for the generation of the additional accessors with the value of `:field`. By allowing two accessors methods, aliased to each other, allows for easier importing and automatic object instantiation based on Parse object JSON data into the Parse::Object subclass.
##### `:symbolize`
A boolean property. This option is only available for fields with data type of `:string`. This allows you to utilize the values for this property as symbols instead of the literal strings, which is Parse's storage format. This feature is useful if a particular property represents a set of enumerable states described in string form. As an example, if you have a `Post` object which has a set of publish states stored in Parse as "draft","scheduled", and "published" - we can use ruby symbols to make our code easier.
```ruby
class Post < Parse::Object
property :state, :string, symbolize: true
end
post = Post.first
# the value returned is auto-symbolized
if post.state == :draft
# will be converted to string when updated in Parse
post.state = :published
post.save
end
```
##### `:enum`
The enum option allows you to define an array of possible values that the particular `:string` property should hold. This feature has similarities in the methods and accessors generated for you as described in [ActiveRecord::Enum](http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html). Using the example in that documentation:
```ruby
class Conversation < Parse::Object
property :status, enum: [ :active, :archived ]
end
Conversation.statuses # => [ :active, :archived ]
# named scopes
Conversation.active # where status: :active
Conversation.archived(limit: 10) # where status: :archived, limit 10
conversation.active! # sets status to active!
conversation.active? # => true
conversation.status # => :active
conversation.archived!
conversation.archived? # => true
conversation.status # => :archived
# equivalent
conversation.status = "archived"
conversation.status = :archived
# allowed by the setter
conversation.status = :banana
conversation.status_valid? # => false
```
Similar to [ActiveRecord::Enum](http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html), you can use the `:_prefix` or `:_suffix` options when you need to define multiple enums with same values. If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
```ruby
class Conversation < Parse::Object
property :status, enum: [:active, :archived], _suffix: true
property :comments_status, enum: [:active, :inactive], _prefix: :comments
# combined
property :discussion, enum: [:casual, :business], _prefix: :talk, _suffix: true
end
Conversation.statuses # => [:active, :archived]
Conversation.comments # => [:active, :inactive]
Conversation.talks # => [:casual, :business]
# affects scopes names
Conversation.archived_status
Conversation.comments_inactive
Conversation.business_talk
conversation.active_status!
conversation.archived_status? # => false
conversation.status = :banana
conversation.valid_status? # => false
conversation.comments_inactive!
conversation.comments_active? # => false
conversation.casual_talk!
conversation.business_talk? # => false
```
##### `:scope`
A boolean property. For some data types like `:boolean` and enums, some [query scopes](#query-scopes) are generated to more easily query data. To prevent generating these scopes for a particular property, set this value to `false`.
### Associations
Parse supports a three main types of relational associations. One type of relation is the `One-to-One` association. This is implemented through a specific column in Parse with a Pointer data type. This pointer column, contains a local value that refers to a different record in a separate Parse table. This association is implemented using the `:belongs_to` feature. The second association is of `One-to-Many`. This is implemented is in Parse as a Array type column that contains a list of of Parse pointer objects. It is recommended by Parse that this array does not exceed 100 items for performance reasons. This feature is implemented using the `:has_many` operation with the plural name of the local Parse class. The last association type is a Parse Relation. These can be used to implement a large `Many-to-Many` association without requiring an explicit intermediary Parse table or class. This feature is also implemented using the `:has_many` method but passing the option of `:relation`.
#### Belongs To
This association creates a one-to-one association with another Parse model. This association says that this class contains a foreign pointer column which references a different class. Utilizing the `belongs_to` method in defining a property in a Parse::Object subclass sets up an association between the local table and a foreign table. Specifying the `belongs_to` in the class, tells the framework that the Parse table contains a local column in its schema that has a reference to a record in a foreign table. The argument to `belongs_to` should be the singularized version of the foreign Parse::Object class. you should specify the foreign table as the snake_case singularized version of the foreign table class. It is important to note that the reverse relationship is not generated automatically.
```ruby
class Author < Parse::Object
property :name
end
class Comment < Parse::Object
belongs_to :user # Parse::User
end
class Post < Parse::Object
belongs_to :author
end
post = Post.first
# Follow the author pointer and get name
post.author.name
other_author = Author.first
# change author by setting new pointer
post.author = other_author
post.save
```
##### Options
You can override some of the default functionality when creating both `belongs_to`, `has_one` and `has_many` associations.
###### `:required`
A boolean property. Setting the requirement, automatically creates an ActiveModel validation of `validates_presence_of` for the association. This will not prevent the save, but affects the validation check when `valid?` is called on an instance. Default is false.
###### `:as`
This option allows you to override the foreign Parse class that this association refers while allowing you to have a different accessor name. As an example, you may have a class `Band` which has a `manager` who is of type `Parse::User` and a set of band members, represented by the class `Artist`. You can override the default casting class as follows:
```ruby
# represents a member of a band or group
class Artist < Parse::Object
end
class Band < Parse::Object
belongs_to :manager, as: :user
belongs_to :lead_singer, as: :artist
belongs_to :drummer, as: :artist
end
band = Band.first
band.manager # Parse::User object
band.lead_singer # Artist object
band.drummer # Artist object
```
###### `:field`
This option allows you to set the name of the remote Parse column for this property. Using this will explicitly set the remote property name to the value of this option. The value provided for this option will affect the name of the alias method that is generated when `alias` option is used. **By default, the name of the remote column is the lower-first camel case version of the property name. As an example, for a property with key `:my_property_name`, the framework will implicitly assume that the remote column is `myPropertyName`.**
#### [Has One](https://neurosynq.github.io/parse-stack-next/Parse/Associations/HasOne.html)
The `has_one` creates a one-to-one association with another Parse class. This association says that the other class in the association contains a foreign pointer column which references instances of this class. If your model contains a column that is a Parse pointer to another class, you should use `belongs_to` for that association instead.
Defining a `has_one` property generates a helper query method to fetch a particular record from a foreign class. This is useful for setting up the inverse relationship accessors of a `belongs_to`. In the case of the `has_one` relationship, the `:field` option represents the name of the column of the foreign class where the Parse pointer is stored. By default, the lower-first camel case version of the Parse class name is used.
In the example below, a `Band` has a local column named `manager` which has a pointer to a `Parse::User` record. This setups up the accessor for `Band` objects to access the band's manager.
```ruby
# every band has a manager
class Band < Parse::Object
belongs_to :manager, as: :user
end
band = Band.first id: '12345'
# the user represented by this manager
user = band.manger
```
Since we know there is a column named `manager` in the `Band` class that points to a single `Parse::User`, you can setup the inverse association read accessor in the `Parse::User` class. Note, that to change the association, you need to modify the `manager` property on the band instance since it contains the `belongs_to` property.
```ruby
# every user manages a band
class Parse::User
# inverse relationship to `Band.belongs_to :manager`
has_one :band, field: :manager
end
user = Parse::User.first
# use the generated has_one accessor `band`.
user.band # similar to query: Band.first(:manager => user)
```
You may optionally use `has_one` with scopes, in order to fine tune the query result. Using the example above, you can customize the query with a scope that only fetches the association if the band is approved. If the association cannot be fetched, `nil` is returned.
```ruby
# adding to previous example
class Band < Parse::Object
property :approved, :boolean
property :approved_date, :date
end
# every user manages a band
class Parse::User
has_one :recently_approved, ->{ where(order: :approved_date.desc) }, field: :manager, as: :band
has_one :band_by_status, ->(status) { where(approved: status) }, field: :manager, as: :band
end
# gets the band most recently approved
user.recently_approved
# equivalent: Band.first(manager: user, order: :approved_date.desc)
# fetch the managed band that is not approved
user.band_by_status(false)
# equivalent: Band.first(manager: user, approved: false)
```
#### [Has Many](https://neurosynq.github.io/parse-stack-next/Parse/Associations/HasMany.html)
Parse has many ways to implement one-to-many and many-to-many associations: `Array`, `Parse Relation` or through a `Query`. How you decide to implement your associations, will affect how `has_many` works in Parse-Stack. Parse natively supports one-to-many and many-to-many relationships using `Array` and `Relations`, as described in [Relational Data](http://docs.parseplatform.org/js/guide/#relational-data). Both of these methods require you define a specific column type in your Parse table that will be used to store information about the association.
In addition to `Array` and `Relation`, Parse-Stack also implements the standard `has_many` behavior prevalent in other frameworks through a query where the associated class contains a foreign pointer to the local class, usually the inverse of a `belongs_to`. This requires that the associated class has a defined column
that contains a pointer the refers to the defining class.
##### Query
In this implementation, a `has_many` association for a Parse class requires that another Parse class will have a foreign pointer that refers to instances of this class. This is the standard way that `has_many` relationships work in most databases systems. This is usually the case when you have a class that has a `belongs_to` relationship to instances of the local class.
In the example below, many songs belong to a specific artist. We set this association by setting `:belongs_to` relationship from `Song` to `Artist`. Knowing there is a column in `Song` that points to instances of an `Artist`, we can setup a `has_many` association to `Song` instances in the `Artist` class. Doing so will generate a helper query method on the `Artist` instance objects.
```ruby
class Song < Parse::Object
property :released, :date
# this class will have a pointer column to an Artist
belongs_to :artist
end
class Artist < Parse::Object
has_many :songs
end
artist = Artist.first
artist.songs # => [all songs belonging to artist]
# equivalent: Song.all(artist: artist)
# filter also by release date
artist.songs(:released.after => 1.year.ago)
# equivalent: Song.all(artist: artist, :released.after => 1.year.ago)
```
In order to modify the associated objects (ex. `songs`), you must modify their corresponding `belongs_to` field (in this case `song.artist`), to another record and save it.
Options for `has_many` using this approach are `:as` and `:field`. The `:as` option behaves similarly to the `:belongs_to` counterpart. The `:field` option can be used to override the derived column name located in the foreign class. The default value for `:field` is the columnized version of the Parse subclass `parse_class` method.
```ruby
class Parse::User
# since the foreign column name is :agent
has_many :artists, field: :agent
end
class Artist < Parse::Object
belongs_to :manager, as: :user, field: :agent
end
artist.manager # => Parse::User object
user.artists # => [artists where :agent colum