Releases: commandpostsoft/parse-stack
v3.3.4 - Cloud Code Error Handling
Cloud Code Error Handling
This release adds raising variants of Parse.call_function and Parse.trigger_job that surface cloud-code errors instead of silently returning nil, introduces Parse::Error::CloudCodeError, and hardens result extraction against unusual response bodies.
Changes
- NEW:
Parse.call_function!,Parse.call_function_with_session!,Parse.trigger_job!, andParse.trigger_job_with_session!raiseParse::Error::CloudCodeErrorwhen the cloud function or job returns an error response, instead of silently returning nil. The error carriesfunction_name,code,http_status, and the underlyingParse::Responsefor debugging. Use these variants when you want failures to propagate rather than be coerced to nil. - IMPROVED:
Parse.call_functionandParse.trigger_jobnow emit a[Parse:CloudCodeError]warning to stderr when the response indicates an error. Previously both methods coerced any cloud-code error response to a nil return value with no log line, making misconfigured calls (missing session token, failederror!()in cloud code) invisible to callers and tests. The nil return is preserved for backwards compatibility; the warning surfaces the failure. Matches the existing warn-then-raise pattern used by other HTTP error paths inParse::Client#request. - FIXED:
Parse.call_function,Parse.trigger_job, and their!variants no longer raiseTypeErroron unusual successful response bodies. Result extraction now guards against non-Hash response payloads (e.g., a bare string body) by returning the raw result rather than indexing into a non-Hash.
Code Examples
# Raise on error instead of silently returning nil
result = Parse.call_function!(:addUserToTeams, { userId: id }, session_token: token)
# Handle the raise with structured error info
begin
Parse.call_function!(:addUserToTeams, params, session_token: token)
rescue Parse::Error::CloudCodeError => e
Rails.logger.error "Cloud function #{e.function_name} failed: [#{e.code}] HTTP #{e.http_status}"
raise
end
# Non-bang form now warns on error (still returns nil)
Parse.call_function(:broken, {})
# stderr: [Parse:CloudCodeError] broken [141] Missing session token (HTTP 400)
# returns: nilCommit: 7ff4741
Author: Adrian Curtin
Date: April 2026
v3.3.0 - Ruby 3.1 Minimum Version
Ruby 3.1 Minimum Version
This release updates the minimum Ruby version requirement.
Breaking Changes
- BREAKING: Minimum Ruby version is now 3.1 (previously 3.0). Ruby 3.0 reached end-of-life in March 2024.
Changes
- IMPROVED: CI now tests against Ruby 3.1, 3.2, 3.3, and 3.4.
Commit: f4bd663
Author: Adrian Curtin
Date: January 2026
v3.2.2 - Query Methods & JSON Serialization
Query Method Improvements & JSON Serialization
This release improves query methods and adds JSON serialization options.
Improvements
- IMPROVED:
latestandlast_updatedmethods now support alimit:option when passing constraints.
Song.latest(:user.eq => user, limit: 5)
Song.last_updated(status: "active", limit: 10)
query.where(genre: "rock").last_updated(limit: 3)- IMPROVED:
PointerCollectionProxy#as_jsonnow supports thepointers_onlyoption. Setpointers_only: falseto serialize objects with their fetched fields instead of just pointers.
# Default - pointers for storage
capture.assets.as_json
# => [{"__type"=>"Pointer", "className"=>"Asset", "objectId"=>"abc"}, ...]
# Serialize with fetched fields
capture.assets.as_json(pointers_only: false)
# => [{"objectId"=>"abc", "file"=>{...}, "caption"=>"My photo", ...}, ...]- IMPROVED:
Parse::Object#as_jsonwith:onlyoption now automatically includes identification fields (objectId,className,__type,id). Usestrict: trueto disable this.
# Default: identification fields included
song.as_json(only: [:title, :artist])
# => {"objectId"=>"abc", "className"=>"Song", "__type"=>"Object", "title"=>"...", "artist"=>"..."}
# Strict mode: only specified fields
song.as_json(only: [:title, :artist], strict: true)
# => {"title"=>"...", "artist"=>"..."}- NEW: Added
:excludeas an alias for:exceptinas_json.
song.as_json(exclude: [:acl, :created_at])Author: Adrian Curtin
Date: December 2025
v3.2.1 - CLP Default Permissions & User Fields
CLP Default Permissions & User Fields
This release adds new CLP configuration methods and improves field name handling.
New Features
- NEW: Added
set_default_clpmethod to set a default permission for all CLP operations at once.
class Document < Parse::Object
# Set all operations to public by default
set_default_clp public: true
# Or require authentication for all operations
set_default_clp requires_authentication: true
# Or restrict all operations to specific roles
set_default_clp roles: ["Admin", "Editor"]
# Then override specific operations as needed
set_clp :delete, public: false, roles: ["Admin"]
end- NEW: Added
set_read_user_fieldsandset_write_user_fieldsfor pointer-based permissions.
class Document < Parse::Object
belongs_to :owner, as: :user
belongs_to :editor, as: :user
# Owner can read, editor can write
set_read_user_fields [:owner]
set_write_user_fields [:editor]
end- NEW: Added
reset_clp!method to reset CLPs to public defaults.
Improvements
- IMPROVED: CLP methods now automatically convert snake_case Ruby property names to camelCase Parse Server field names.
- IMPROVED: Added
include_defaultsparameter toCLP#as_jsonfor pushing complete CLPs to server.
Bug Fixes
- FIXED:
auto_upgrade!now resets CLPs before applying new ones, preventing old restrictive permissions from persisting. - FIXED:
as_json(include_defaults: true)now properly includes all operations even when no explicitset_default_clpis called.
Author: Adrian Curtin
Date: December 2025
v3.2.0 - Class-Level Permissions & Consolidated Updates
Class-Level Permissions (CLP) Support
This release adds comprehensive Class-Level Permissions support for protecting fields and controlling access at the schema level.
Note: This release consolidates changes from versions 3.1.1 through 3.1.12 (GitHub releases were skipped for those versions). See CHANGELOG.md for detailed version-by-version changes.
New Features: Class-Level Permissions
DSL for Defining CLPs:
class Song < Parse::Object
property :title, :string
property :internal_notes, :string
# Set operation-level permissions
set_clp :find, public: true
set_clp :create, public: false, roles: ["Admin", "Editor"]
set_clp :delete, public: false, roles: ["Admin"]
# Protect fields from certain users
protect_fields "*", [:internal_notes] # Hidden from everyone
protect_fields "role:Admin", [] # Admins see everything
endFilter Data for Webhook Responses:
filtered = song.filter_for_user(current_user, roles: ["Member"])
filtered_results = Song.filter_results_for_user(songs, current_user, roles: user_roles)Push CLPs to Parse Server:
Song.auto_upgrade! # Includes CLPs in schema upgrades
Song.update_clp! # Update only CLPs
Song.fetch_clp # Fetch current CLPs from serverConsolidated Changes from 3.1.1 - 3.1.12
Query & Constraint Improvements (3.1.12, 3.1.4, 3.1.3)
- NEW:
ends_withquery constraint for string suffix matching - NEW: ACL query convenience methods:
publicly_readable,publicly_writable,privately_readable,privately_writable,private_acl,not_publicly_readable,not_publicly_writable - NEW:
not_readable_byandnot_writeable_byconstraints - NEW: ACL queries support
readable_by: userandwritable_by: roleas hash keys - NEW: Role hierarchy expansion - queries automatically include child roles
- NEW:
default_acl_privateclass setting andprivate_acl!convenience method - NEW:
Parse::ACL.privateclass method for empty ACLs
Caching Improvements (3.1.9, 3.1.7, 3.1.5)
- NEW:
fetch_cache!method onParse::Pointerwith caching support - NEW:
cache:parameter forParse::Pointer#fetch - NEW: "Write-only" cache mode (
:write_only) - skip cache read, update cache with fresh data - NEW:
fetch_cache!andfind_cachedconvenience methods - CHANGED: Query caching is now opt-in by default (was opt-out)
- NEW:
Parse.default_query_cacheandParse.cache_write_on_fetchconfiguration options
Performance Improvements (3.1.10)
- IMPROVED: Aggregation pipeline optimization automatically merges consecutive
$matchstages
Validation & Callback Improvements (3.1.2)
- NEW:
save()passes validation context (:createor:update) to validations and callbacks - NEW:
before_validation,after_validation,around_validationsupporton: :create/:updateoption - NEW:
create!class method for Active Model consistency
ACL Dirty Tracking (3.1.3)
- FIXED:
acl_wascorrectly captures ACL state before in-place modifications - NEW:
acl_changed?compares actual ACL content, not just object references
Serialization (3.1.1)
- NEW:
:exclude_keysoption as alias for:exceptinas_json - NEW:
Parse::MongoDB.to_mongodb_datehelper for date conversion
Bug Fixes (Various)
- FIXED:
auto_upgrade!skips read-only system classes (_PushStatus,_SCHEMA) - FIXED: Date property parsing handles empty strings gracefully
- FIXED: Query methods
first,latest,last_updatedproperly accept keyword options - FIXED: Connection pooling
pool_sizeoption works correctly
Code Quality (3.1.6)
- FIXED: Resolved circular require warnings
- FIXED: Resolved method redefinition warnings
- UPDATED:
Parse::Installationdevice_type enum updated for current Parse Server types - NEW: Push notification validation for installation targeting
Author: Adrian Curtin
Date: November 2025
v3.1.0 - Role Management, Schema Tools & Atlas Search
Role Management, Schema Tools & Atlas Search
This release adds enhanced role management, schema introspection tools, MongoDB Atlas Search integration, and direct MongoDB query support.
Note: This release also includes changes from v3.0.2 (GitHub release was skipped for that version).
Enhanced Role Management
New helper methods for managing Parse roles and role hierarchies:
# Find or create a role
admin = Parse::Role.find_or_create("Admin")
# Add/remove users
admin.add_users(user1, user2).save
admin.remove_user(user).save
# Check membership
admin.has_user?(user) # => true
# Role hierarchy
admin.add_child_role(moderator).save
admin.all_child_roles # => [moderator, ...]
admin.all_users # => Users from this role AND child rolesHTTP 429 Retry-After Header Support
The client now respects the Retry-After HTTP header when handling rate limit responses. Supports both integer seconds and HTTP-date formats.
MongoDB Read Preference Support
Direct read queries to secondary replicas for load balancing:
songs = Song.query.read_pref(:secondary).where(genre: "Rock").resultsSchema Introspection and Migration Tools
New Parse::Schema module for inspecting and migrating schemas:
# Fetch and inspect schema
schema = Parse::Schema.fetch("Song")
schema.field_names # => ["objectId", "title", "duration", ...]
schema.field_type(:title) # => :string
# Compare local model with server
diff = Parse::Schema.diff(Song)
diff.summary # => Human-readable diff
# Generate and apply migration
migration = Parse::Schema.migration(Song)
migration.apply!(dry_run: true) # Preview onlyMongoDB Atlas Search Integration
Full-text search, autocomplete, and faceted search via MongoDB Atlas Search:
# Configure
Parse::MongoDB.configure(uri: "mongodb+srv://...", enabled: true)
Parse::AtlasSearch.configure(enabled: true, default_index: "default")
# Full-text search
result = Parse::AtlasSearch.search("Song", "love ballad")
# Autocomplete
result = Parse::AtlasSearch.autocomplete("Song", "Lov", field: :title)
# Faceted search
result = Parse::AtlasSearch.faceted_search("Song", "rock", facets)Direct MongoDB Query Methods
New methods for executing queries directly against MongoDB:
songs = Song.query(:plays.gt => 1000).results_direct
song = Song.query.order(:plays.desc).first_direct
count = Song.query(:plays.gt => 1000).count_directAlso Includes Changes from v3.0.2 (GitHub release skipped)
Push Notification User/Installation Targeting
Parse::Push.to_user(current_user).with_alert("Hello!").send!
Parse::Push.to_users(user1, user2, user3).with_alert("Group message!").send!
Parse::Push.to_installation(device).with_alert("Hello!").send!Bug Fixes from v3.0.2
- FIXED: Array constraint field name formatting for MongoDB aggregation queries
- FIXED: All 13 array constraints now use proper field name conversion (
empty_or_nil,not_empty,set_equals, etc.) - FIXED:
build_aggregation_pipelinenow merges all$matchstages correctly
Author: Adrian Curtin
Date: December 2025
v3.0.1 - Agent Enhancements & LiveQuery
Agent Enhancements & LiveQuery Improvements
This release adds comprehensive agent features and LiveQuery improvements.
Agent Enhancements
Environment Variable Gating for MCP:
# Requires PARSE_MCP_ENABLED=true AND Parse.mcp_server_enabled = true
Parse.mcp_server_enabled = true
Parse::Agent.enable_mcp!(port: 3001)Conversation Support (Multi-turn):
agent = Parse::Agent.new
agent.ask("How many users are there?")
agent.ask_followup("What about admins?")
agent.clear_conversation!Token Usage Tracking:
agent.token_usage # => { prompt_tokens: 450, completion_tokens: 120, total_tokens: 570 }
agent.estimated_cost # => 0.0234Callback/Hooks System:
agent.on_tool_call { |tool, args| puts "Calling: #{tool}" }
agent.on_tool_result { |tool, args, result| log_result(tool, result) }
agent.on_error { |error, context| notify_slack(error) }Streaming Support:
agent.ask_streaming("Analyze user growth trends") { |chunk| print chunk }Export/Import Conversation:
state = agent.export_conversation
new_agent.import_conversation(state)LiveQuery Enhancements
- NEW: Configurable frame read timeout to prevent indefinite socket blocking
- NEW: Automatic cleanup of expired cache entries in
Parse::Audience
Bug Fixes
- FIXED: Arrays containing Parse objects now stored in proper pointer format for
.in/.ninqueries - NEW:
pointers_onlyoption forCollectionProxy#as_json - FIXED:
add!,add_unique!,remove!methods correctly convert Parse objects to pointer format
Author: Adrian Curtin
Date: November 2025
v3.0.0 - Push, Sessions, Agent & LiveQuery
Major Release - Push, Sessions, Agent, LiveQuery & Validation
This major release adds comprehensive push notification features, session management, AI agent integration, LiveQuery client, and model validation utilities.
Push Builder Pattern API
New fluent API for building push notifications:
Parse::Push.new
.to_channel("news")
.with_title("Breaking News")
.with_body("Major event happening now!")
.with_badge(1)
.with_sound("alert.caf")
.with_data(article_id: "12345")
.schedule(Time.now + 3600)
.send!
# Class method shortcuts
Parse::Push.to_channel("news").with_alert("Hello!").send!Silent Push Support (iOS)
Parse::Push.new
.to_channel("sync")
.silent!
.with_data(action: "refresh")
.send!Rich Push Support (iOS)
Parse::Push.new
.to_channel("media")
.with_title("New Photo")
.with_image("https://example.com/photo.jpg")
.with_category("PHOTO_ACTIONS")
.send!Installation Channel Management
installation.subscribe("news", "weather")
installation.unsubscribe("sports")
installation.subscribed_to?("news")
Parse::Installation.all_channels
Parse::Installation.subscribers_count("news")Push Localization & Badge Increment
# Localized alerts
Parse::Push.new
.with_localized_alerts(en: "Hello!", fr: "Bonjour!", es: "Hola!")
.send!
# Badge increment
Parse::Push.new.increment_badge.send!
Parse::Push.new.clear_badge.send!Session Management
session.expired?
session.time_remaining
session.revoke!
Parse::Session.active.all
Parse::Session.for_user(user).all
user.logout_all!
user.active_session_countAI Agent Integration
New Parse::Agent module for AI-powered database interactions:
agent = Parse::Agent.new
result = agent.ask("How many users signed up this week?")
result = agent.ask("Find all songs with more than 1000 plays")
# Available tools for LLM
Parse::Agent.available_toolsMCP Server Support:
Parse.mcp_server_enabled = true
Parse::Agent.enable_mcp!(port: 3001)LiveQuery Client
Real-time subscriptions for Parse objects:
# Configure
Parse::LiveQuery.configure(
server_url: "wss://your-server.com/parse",
application_id: "your-app-id"
)
# Subscribe to changes
subscription = Song.live_query.where(:plays.gt => 1000).subscribe
subscription.on(:create) { |song| puts "New hit: #{song.title}" }
subscription.on(:update) { |song| puts "Updated: #{song.title}" }
subscription.on(:delete) { |song| puts "Removed: #{song.title}" }
# Unsubscribe
subscription.unsubscribeFeatures:
- Circuit breaker for connection resilience
- Health monitoring with automatic reconnection
- Event queue for ordered processing
Email Validation Model
New Parse::Email class for email validation:
email = Parse::Email.new("user@example.com")
email.valid? # => true
email.domain # => "example.com"
email.local_part # => "user"
email.normalized # => "user@example.com"Phone Validation Model
New Parse::Phone class for phone number validation:
phone = Parse::Phone.new("+1 (555) 123-4567")
phone.valid? # => true
phone.e164 # => "+15551234567"
phone.national # => "(555) 123-4567"
phone.country_code # => "US"Saved Audiences & Push Status
# Target saved audiences
Parse::Push.new.to_audience("VIP Users").with_alert("Exclusive!").send!
Parse::Audience.find_by_name("VIP Users")
# Track push status
status = Parse::PushStatus.find(push_id)
status.succeeded?
status.num_sent
status.success_rateAuthor: Adrian Curtin
Date: November 2025
v2.3.0 - Connection Pooling, Cursor Pagination & N+1 Detection
Connection Pooling, Cursor Pagination & N+1 Detection
This release adds HTTP connection pooling, cursor-based pagination, N+1 query detection, and thread safety improvements.
HTTP Connection Pooling (Default)
Parse Stack now uses HTTP persistent connections by default for improved performance.
# Default: connection pooling enabled
Parse.setup(
server_url: "https://your-parse-server.com/parse",
application_id: "your-app-id",
api_key: "your-api-key"
)
# Custom configuration
Parse.setup(
...,
connection_pooling: {
pool_size: 5,
idle_timeout: 60,
keep_alive: 60
}
)
# Disable if needed
Parse.setup(..., connection_pooling: false)Cursor-Based Pagination
New Parse::Cursor for efficiently traversing large datasets:
# Iterate over pages
cursor = Song.cursor(limit: 100, order: :created_at.desc)
cursor.each_page { |page| process(page) }
# Iterate over items
Song.cursor(limit: 50).each { |song| puts song.title }
# Manual control
cursor = User.cursor(limit: 100)
first_page = cursor.next_page
second_page = cursor.next_page
cursor.reset!
# Resumable cursors
state = cursor.serialize
# Later...
cursor = Parse::Cursor.deserialize(state)N+1 Query Detection
New Parse::NPlusOneDetector to detect N+1 query patterns:
# Enable warnings
Parse.warn_on_n_plus_one = true
# Or use mode API
Parse.n_plus_one_mode = :warn
# Strict mode for CI
Parse.n_plus_one_mode = :raise
# Fix with includes
songs = Song.all(limit: 100, includes: [:artist])Available modes: :ignore (default), :warn, :raise
Thread Safety & Marshaling Improvements
- IMPROVED: Thread safety improvements in
Parse::Objectfor concurrent access - IMPROVED: Better marshaling support for Parse objects
Bug Fixes
- IMPROVED: Aggregation pipeline correctly handles
__aggregation_pipelinestages - IMPROVED: Better whitespace formatting in SortableGroupBy pipeline generation
Author: Adrian Curtin
Date: November 2025
v2.2.0 - Validations DSL, Profiling & Consolidated Updates
Validations DSL, Performance Profiling & Partial Fetch System
This release adds Rails-style validations, performance profiling middleware, and comprehensive partial fetch tracking.
Note: This release consolidates changes from versions 2.1.1 through 2.1.10 (GitHub releases were skipped). See CHANGELOG.md for detailed version-by-version changes.
New Features: Validations DSL
Rails-style validations with custom uniqueness validator:
class User < Parse::Object
property :email, :string
property :username, :string
validates :email, uniqueness: true
validates :username, uniqueness: { case_sensitive: false }
validates :employee_id, uniqueness: { scope: :organization }
endValidation Callbacks:
before_validation,after_validation,around_validation
Update Callbacks:
before_update,after_update,around_update
Performance Profiling Middleware
Parse.profiling_enabled = true
Parse.recent_profiles.each do |profile|
puts "#{profile[:method]} #{profile[:url]}: #{profile[:duration_ms]}ms"
end
stats = Parse.profiling_statistics
# => { count: N, avg_ms: X, min_ms: Y, max_ms: Z, by_method: {...} }Query Explain
plan = Song.query(:plays.gt => 1000).explainConsolidated Changes from 2.1.1 - 2.1.10
Partial Fetch Tracking System (2.1.0-2.1.6)
- NEW:
partially_fetched?,fetched_keys,field_was_fetched?(key)methods - NEW: Autofetch triggers automatically when accessing unfetched fields
- NEW:
disable_autofetch!,enable_autofetch!methods - NEW:
Parse::UnfetchedFieldAccessErrorwhen accessing unfetched fields with autofetch disabled - NEW:
Parse.serialize_only_fetched_fieldsconfiguration option - NEW:
fully_fetched?,fetched?methods for checking fetch state
Partial Fetch on Existing Objects (2.1.5)
post.fetch(keys: [:view_count])
post.fetch!(keys: [:title], includes: [:author])
pointer.fetch(keys: [:title, :content])Advanced Array Query Constraints (2.1.9-2.1.10)
- NEW:
:field.size => n- Match arrays with exact size - NEW:
:field.arr_empty => true/false- Match empty arrays - NEW:
:field.eq_array => [values]- Exact match (order matters) - NEW:
:field.set_equals => [values]- Set equality (any order) - NEW:
:field.elem_match => { criteria }- Match array elements - NEW:
:field.any => [values],:field.none => [values] - NEW:
:field.first => value,:field.last => value
Request/Response Logging Middleware (2.1.10)
Parse.setup(..., logging: true)
Parse.log_level = :debugBug Fixes (Various)
- FIXED:
fetch!handles array responses gracefully - FIXED: Transaction objects receive IDs after successful create
- FIXED: Setting fields on pointer objects correctly marks them as dirty
- FIXED:
hashmethod consistent with==for Parse objects - FIXED: Autofetch no longer wipes out nested embedded data
- FIXED:
as_jsonwith:onlyoption works correctly - FIXED: Assignment to unfetched fields no longer triggers autofetch
- FIXED: Partial fetch handles fields with default values correctly
Other Improvements
- REMOVED:
active_model_serializersgem dependency - FIXED: ActiveSupport deprecation warnings from Rails 8.2
- NEW:
Parse.autofetch_raise_on_missing_keysfor debugging - NEW: Query validation warnings for common mistakes
Author: Adrian Curtin
Date: November 2025