parse-stack-next 4.5.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.env.sample +17 -3
- data/.github/workflows/codeql.yml +44 -0
- data/.github/workflows/docs.yml +39 -0
- data/.github/workflows/release.yml +32 -0
- data/.github/workflows/ruby.yml +8 -6
- data/.gitignore +4 -0
- data/.vscode/settings.json +3 -0
- data/CHANGELOG.md +305 -72
- data/Gemfile.lock +10 -3
- data/LICENSE.txt +1 -1
- data/README.md +190 -219
- data/Rakefile +1 -1
- data/SECURITY.md +30 -0
- data/assets/parse-stack-next-avatar.png +0 -0
- data/assets/parse-stack-next-avatar.svg +37 -0
- data/assets/parse-stack-next-banner.png +0 -0
- data/assets/parse-stack-next-banner.svg +45 -0
- data/assets/parse-stack-next-social-preview.png +0 -0
- data/docs/atlas_vector_search_guide.md +511 -0
- data/docs/client_sdk_guide.md +1320 -0
- data/docs/mcp_guide.md +225 -104
- data/docs/mongodb_direct_guide.md +21 -4
- data/docs/usage_guide.md +585 -0
- data/examples/transaction_example.rb +28 -28
- data/lib/parse/acl_scope.rb +2 -2
- data/lib/parse/agent/mcp_rack_app.rb +184 -16
- data/lib/parse/agent/metadata_dsl.rb +16 -16
- data/lib/parse/agent/pipeline_validator.rb +28 -1
- data/lib/parse/agent/prompts.rb +5 -5
- data/lib/parse/agent/tools.rb +287 -14
- data/lib/parse/agent.rb +209 -12
- data/lib/parse/api/analytics.rb +27 -5
- data/lib/parse/api/files.rb +6 -2
- data/lib/parse/api/push.rb +21 -4
- data/lib/parse/api/server.rb +59 -0
- data/lib/parse/api/users.rb +26 -2
- data/lib/parse/atlas_search/index_manager.rb +84 -0
- data/lib/parse/atlas_search.rb +37 -9
- data/lib/parse/cache/pool.rb +88 -0
- data/lib/parse/cache/redis.rb +249 -0
- data/lib/parse/client/body_builder.rb +94 -0
- data/lib/parse/client/caching.rb +109 -9
- data/lib/parse/client/response.rb +27 -0
- data/lib/parse/client.rb +74 -3
- data/lib/parse/console.rb +203 -0
- data/lib/parse/embeddings/cohere.rb +484 -0
- data/lib/parse/embeddings/fixture.rb +130 -0
- data/lib/parse/embeddings/jina.rb +454 -0
- data/lib/parse/embeddings/local_http.rb +492 -0
- data/lib/parse/embeddings/openai.rb +520 -0
- data/lib/parse/embeddings/provider.rb +264 -0
- data/lib/parse/embeddings/qwen.rb +431 -0
- data/lib/parse/embeddings/voyage.rb +550 -0
- data/lib/parse/embeddings.rb +225 -0
- data/lib/parse/graphql/scalars.rb +53 -0
- data/lib/parse/graphql/type_generator.rb +264 -0
- data/lib/parse/graphql.rb +48 -0
- data/lib/parse/live_query/client.rb +24 -5
- data/lib/parse/live_query/subscription.rb +17 -6
- data/lib/parse/live_query.rb +9 -4
- data/lib/parse/model/associations/collection_proxy.rb +2 -2
- data/lib/parse/model/associations/has_many.rb +32 -1
- data/lib/parse/model/associations/has_one.rb +17 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
- data/lib/parse/model/classes/user.rb +307 -11
- data/lib/parse/model/clp.rb +1 -1
- data/lib/parse/model/core/create_lock.rb +14 -2
- data/lib/parse/model/core/embed_managed.rb +296 -0
- data/lib/parse/model/core/fetching.rb +4 -4
- data/lib/parse/model/core/indexing.rb +53 -14
- data/lib/parse/model/core/parse_reference.rb +3 -3
- data/lib/parse/model/core/properties.rb +70 -1
- data/lib/parse/model/core/querying.rb +57 -1
- data/lib/parse/model/core/vector_searchable.rb +285 -0
- data/lib/parse/model/file.rb +16 -4
- data/lib/parse/model/model.rb +26 -10
- data/lib/parse/model/object.rb +63 -6
- data/lib/parse/model/pointer.rb +16 -2
- data/lib/parse/model/shortnames.rb +2 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
- data/lib/parse/model/vector.rb +102 -0
- data/lib/parse/mongodb.rb +90 -8
- data/lib/parse/pipeline_security.rb +59 -2
- data/lib/parse/query/constraints.rb +16 -14
- data/lib/parse/query/ordering.rb +1 -1
- data/lib/parse/query.rb +137 -64
- data/lib/parse/stack/generators/templates/model.erb +2 -2
- data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
- data/lib/parse/stack/generators/templates/model_role.rb +1 -1
- data/lib/parse/stack/generators/templates/model_session.rb +1 -1
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +375 -73
- data/lib/parse/two_factor_auth/user_extension.rb +5 -2
- data/lib/parse/vector_search.rb +341 -0
- data/parse-stack-next.gemspec +10 -9
- data/scripts/docker/docker-compose.test.yml +18 -0
- data/scripts/start-parse.sh +6 -0
- data/scripts/vector_prototype/create_vector_index.js +105 -0
- data/scripts/vector_prototype/fetch_embeddings.py +241 -0
- data/scripts/vector_prototype/fixture_manifest.json +9 -0
- data/scripts/vector_prototype/query_prototype.rb +84 -0
- data/scripts/vector_prototype/run.sh +34 -0
- metadata +77 -5
- data/parse-stack.png +0 -0
data/lib/parse/query.rb
CHANGED
|
@@ -458,7 +458,15 @@ module Parse
|
|
|
458
458
|
@skip = 0
|
|
459
459
|
@table = table
|
|
460
460
|
@cache = Parse.default_query_cache
|
|
461
|
-
|
|
461
|
+
# Tri-state: `nil` means "no caller preference" — the request layer
|
|
462
|
+
# then applies the master-key default, the `Parse.client_mode` flag,
|
|
463
|
+
# and the `Parse.with_session` ambient as configured. Explicit
|
|
464
|
+
# `true` / `false` (set via `use_master_key=` or the `use_master_key:`
|
|
465
|
+
# constraint key) wins over both. A `true` default here would
|
|
466
|
+
# silently smuggle the master-key header past every client-mode
|
|
467
|
+
# query, so we deliberately leave the decision to the request layer
|
|
468
|
+
# unless the caller said otherwise.
|
|
469
|
+
@use_master_key = nil
|
|
462
470
|
@verbose_aggregate = false
|
|
463
471
|
conditions constraints
|
|
464
472
|
end # initialize
|
|
@@ -588,15 +596,15 @@ module Parse
|
|
|
588
596
|
# @return [Array] an array of field values from all matching objects.
|
|
589
597
|
# @example
|
|
590
598
|
# # Get all asset names
|
|
591
|
-
#
|
|
599
|
+
# Document.query.pluck(:name)
|
|
592
600
|
# # => ["video1.mp4", "image1.jpg", "audio1.mp3"]
|
|
593
601
|
#
|
|
594
|
-
# # Get all author
|
|
595
|
-
#
|
|
596
|
-
# # => [{"__type"=>"Pointer", "className"=>"
|
|
602
|
+
# # Get all author workspace IDs
|
|
603
|
+
# Document.query.pluck(:author_workspace)
|
|
604
|
+
# # => [{"__type"=>"Pointer", "className"=>"Workspace", "objectId"=>"abc123"}, ...]
|
|
597
605
|
#
|
|
598
606
|
# # Get created dates
|
|
599
|
-
#
|
|
607
|
+
# Document.query.pluck(:created_at)
|
|
600
608
|
# # => [2024-11-24 10:30:00 UTC, 2024-11-25 14:20:00 UTC, ...]
|
|
601
609
|
def pluck(field)
|
|
602
610
|
if field.nil? || !field.respond_to?(:to_s)
|
|
@@ -835,15 +843,60 @@ module Parse
|
|
|
835
843
|
unless opts[:filter] == false
|
|
836
844
|
constraint.operand = Query.format_field(constraint.operand)
|
|
837
845
|
end
|
|
846
|
+
reject_vector_constraint!(constraint)
|
|
838
847
|
@where.push constraint
|
|
839
848
|
@results = nil
|
|
840
849
|
self #chaining
|
|
841
850
|
end
|
|
842
851
|
|
|
852
|
+
# @!visibility private
|
|
853
|
+
# Raise {Parse::VectorSearch::ConstraintNotSupported} when a
|
|
854
|
+
# constraint targets a declared `:vector` property with an operator
|
|
855
|
+
# other than the narrow allow-list. Silent-no-op when the query's
|
|
856
|
+
# `@table` doesn't map to a registered Parse::Object subclass, when
|
|
857
|
+
# the subclass declares no `:vector` properties, or when the
|
|
858
|
+
# operand doesn't match a declared vector field on the resolved
|
|
859
|
+
# class.
|
|
860
|
+
#
|
|
861
|
+
# Allow-list: `$exists` (the constraint key for both `:exists` and
|
|
862
|
+
# `:null`), and that's it. Backfill queries like
|
|
863
|
+
# `Doc.query(:body_embedding.null => true)` are useful. Equality,
|
|
864
|
+
# range, $in, $nin, $all, etc. on a 1536-float array are at best
|
|
865
|
+
# surprising and at worst wrong.
|
|
866
|
+
def reject_vector_constraint!(constraint)
|
|
867
|
+
return unless @table
|
|
868
|
+
klass = Parse::Model.find_class(@table)
|
|
869
|
+
return unless klass.respond_to?(:vector_properties)
|
|
870
|
+
vec_fields = klass.vector_properties
|
|
871
|
+
return if vec_fields.nil? || vec_fields.empty?
|
|
872
|
+
# `constraint.operand` may be either the local symbol (e.g.
|
|
873
|
+
# `:body_embedding`) or the camel-cased remote field (e.g.
|
|
874
|
+
# `:bodyEmbedding`) depending on whether Query.format_field has
|
|
875
|
+
# already run. Resolve both shapes against the local set.
|
|
876
|
+
operand_sym = constraint.operand.to_sym
|
|
877
|
+
local_field =
|
|
878
|
+
if vec_fields.key?(operand_sym)
|
|
879
|
+
operand_sym
|
|
880
|
+
elsif klass.respond_to?(:field_map)
|
|
881
|
+
klass.field_map.find { |_local, remote| remote.to_sym == operand_sym }&.first
|
|
882
|
+
end
|
|
883
|
+
return unless local_field && vec_fields.key?(local_field)
|
|
884
|
+
# `$exists` is the only constraint key that makes semantic sense
|
|
885
|
+
# on a dense numeric array — "do you have an embedding yet?" is a
|
|
886
|
+
# legitimate backfill query.
|
|
887
|
+
return if constraint.class.key == :$exists
|
|
888
|
+
op_keyword = constraint.class.key || :eq
|
|
889
|
+
raise Parse::VectorSearch::ConstraintNotSupported,
|
|
890
|
+
"#{klass}.#{local_field} is a :vector property; constraint `#{op_keyword}` " \
|
|
891
|
+
"is not supported on vector fields. Vector queries must use " \
|
|
892
|
+
"#{klass}.find_similar(vector:/text:) (which routes through Atlas " \
|
|
893
|
+
"$vectorSearch); only :exists / :null are accepted in Parse::Query."
|
|
894
|
+
end
|
|
895
|
+
|
|
843
896
|
# @param raw [Boolean] whether to return the hash form of the constraints.
|
|
844
897
|
# @return [Array<Parse::Constraint>] if raw is false, an array of constraints
|
|
845
898
|
# composing the :where clause for this query.
|
|
846
|
-
# @return [Hash] if raw
|
|
899
|
+
# @return [Hash] if raw is true, a hash representing the constraints.
|
|
847
900
|
def constraints(raw = false)
|
|
848
901
|
raw ? where_constraints : @where
|
|
849
902
|
end
|
|
@@ -1364,8 +1417,13 @@ module Parse
|
|
|
1364
1417
|
def _opts
|
|
1365
1418
|
opts = {}
|
|
1366
1419
|
opts[:cache] = self.cache || false
|
|
1367
|
-
|
|
1368
|
-
|
|
1420
|
+
# Only forward `use_master_key` when the caller actually set it.
|
|
1421
|
+
# Forwarding the default (`nil`) would make `opts.key?(:use_master_key)`
|
|
1422
|
+
# true in the request layer and short-circuit the
|
|
1423
|
+
# `Parse.client_mode` / ambient-session resolution paths. See the
|
|
1424
|
+
# init-block comment on `@use_master_key`.
|
|
1425
|
+
opts[:use_master_key] = self.use_master_key unless self.use_master_key.nil?
|
|
1426
|
+
opts[:session_token] = self.session_token unless self.session_token.nil?
|
|
1369
1427
|
# for now, don't cache requests where we disable master_key or provide session token
|
|
1370
1428
|
# if opts[:use_master_key] == false || opts[:session_token].present?
|
|
1371
1429
|
# opts[:cache] = false
|
|
@@ -1663,7 +1721,22 @@ module Parse
|
|
|
1663
1721
|
# @!visibility private
|
|
1664
1722
|
def assert_mongo_direct_routable!
|
|
1665
1723
|
has_session = @session_token.is_a?(String) && !@session_token.empty?
|
|
1666
|
-
|
|
1724
|
+
# Mirror the request-layer auth resolution in Parse::Client#request:
|
|
1725
|
+
# when the process is in "server mode" — Parse.client_mode == false
|
|
1726
|
+
# AND the resolved Parse::Client has a master_key — and the caller
|
|
1727
|
+
# hasn't explicitly opted out via `use_master_key = false`, the
|
|
1728
|
+
# configured master key is the ambient credential. A mongo-direct
|
|
1729
|
+
# query in that posture is authorized by the same key the REST
|
|
1730
|
+
# path would have sent; the SDK should not force callers to repeat
|
|
1731
|
+
# `use_master_key: true` on every direct query.
|
|
1732
|
+
client_has_master_key = begin
|
|
1733
|
+
c = client
|
|
1734
|
+
c.respond_to?(:master_key) && !c.master_key.to_s.empty?
|
|
1735
|
+
rescue StandardError
|
|
1736
|
+
false
|
|
1737
|
+
end
|
|
1738
|
+
server_mode_master = (use_master_key != false) && !Parse.client_mode && client_has_master_key
|
|
1739
|
+
unless use_master_key || server_mode_master || @acl_user || @acl_role || has_session
|
|
1667
1740
|
raise MongoDirectRequired,
|
|
1668
1741
|
"[Parse::Query] This query uses a constraint that can only run " \
|
|
1669
1742
|
"via mongo-direct. Mongo-direct bypasses Parse Server's enforcement, " \
|
|
@@ -2977,15 +3050,15 @@ module Parse
|
|
|
2977
3050
|
# { "$match" => { "status" => "active" } },
|
|
2978
3051
|
# { "$group" => { "_id" => "$category", "count" => { "$sum" => 1 } } }
|
|
2979
3052
|
# ]
|
|
2980
|
-
# aggregation =
|
|
3053
|
+
# aggregation = Document.query.aggregate(pipeline)
|
|
2981
3054
|
# results = aggregation.results
|
|
2982
3055
|
# raw_results = aggregation.raw
|
|
2983
3056
|
# pointer_results = aggregation.result_pointers
|
|
2984
3057
|
#
|
|
2985
3058
|
# # With verbose output
|
|
2986
|
-
# aggregation =
|
|
3059
|
+
# aggregation = Document.query.aggregate(pipeline, verbose: true)
|
|
2987
3060
|
# # With MongoDB direct (required for $inQuery constraints in aggregation)
|
|
2988
|
-
# aggregation =
|
|
3061
|
+
# aggregation = Document.query.aggregate(pipeline, mongo_direct: true)
|
|
2989
3062
|
# Pipeline stages that are blocked to prevent data exfiltration or destructive operations.
|
|
2990
3063
|
# @deprecated Retained for backwards compatibility. The canonical list now lives in
|
|
2991
3064
|
# {Parse::PipelineSecurity::DENIED_OPERATORS} and is enforced recursively, not only
|
|
@@ -3949,23 +4022,23 @@ module Parse
|
|
|
3949
4022
|
# @param return_pointers [Boolean] if true, converts Parse pointer group keys to Parse::Pointer objects.
|
|
3950
4023
|
# @return [GroupBy, SortableGroupBy] an object that supports chaining aggregation methods.
|
|
3951
4024
|
# @example
|
|
3952
|
-
#
|
|
3953
|
-
#
|
|
3954
|
-
#
|
|
4025
|
+
# Document.group_by(:category).count
|
|
4026
|
+
# Document.where(:status => "active").group_by(:project).sum(:file_size)
|
|
4027
|
+
# Document.group_by(:media_format).average(:duration)
|
|
3955
4028
|
#
|
|
3956
4029
|
# # Array flattening example:
|
|
3957
4030
|
# # Record 1: tags = ["a", "b"]
|
|
3958
4031
|
# # Record 2: tags = ["b", "c"]
|
|
3959
|
-
#
|
|
4032
|
+
# Document.group_by(:tags, flatten_arrays: true).count
|
|
3960
4033
|
# # => {"a" => 1, "b" => 2, "c" => 1}
|
|
3961
4034
|
#
|
|
3962
4035
|
# # Sortable results:
|
|
3963
|
-
#
|
|
4036
|
+
# Document.group_by(:category, sortable: true).count.sort_by_value_desc
|
|
3964
4037
|
# # => [["video", 45], ["image", 23], ["audio", 12]]
|
|
3965
4038
|
#
|
|
3966
4039
|
# # Return Parse::Pointer objects for pointer fields:
|
|
3967
|
-
#
|
|
3968
|
-
# # => {#<Parse::Pointer @parse_class="
|
|
4040
|
+
# Document.group_by(:author_workspace, return_pointers: true).count
|
|
4041
|
+
# # => {#<Parse::Pointer @parse_class="Workspace" @id="team1"> => 5, ...}
|
|
3969
4042
|
# @param mongo_direct [Boolean] if true, queries MongoDB directly bypassing Parse Server.
|
|
3970
4043
|
# Requires Parse::MongoDB to be configured. Default: false.
|
|
3971
4044
|
def group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false)
|
|
@@ -3987,16 +4060,16 @@ module Parse
|
|
|
3987
4060
|
# @param return_pointers [Boolean] if true, returns Parse::Pointer objects instead of full objects.
|
|
3988
4061
|
# @return [Hash] a hash with field values as keys and arrays of Parse objects as values.
|
|
3989
4062
|
# @example
|
|
3990
|
-
# # Get arrays of actual
|
|
3991
|
-
#
|
|
4063
|
+
# # Get arrays of actual Document objects grouped by category
|
|
4064
|
+
# Document.query.group_objects_by(:category)
|
|
3992
4065
|
# # => {
|
|
3993
|
-
# # "video" => [#<
|
|
3994
|
-
# # "image" => [#<
|
|
3995
|
-
# # "audio" => [#<
|
|
4066
|
+
# # "video" => [#<Document:video1>, #<Document:video2>, ...],
|
|
4067
|
+
# # "image" => [#<Document:image1>, #<Document:image2>, ...],
|
|
4068
|
+
# # "audio" => [#<Document:audio1>, ...]
|
|
3996
4069
|
# # }
|
|
3997
4070
|
#
|
|
3998
4071
|
# # Get Parse::Pointer objects instead (memory efficient)
|
|
3999
|
-
#
|
|
4072
|
+
# Document.query.group_objects_by(:category, return_pointers: true)
|
|
4000
4073
|
# # => {
|
|
4001
4074
|
# # "video" => [#<Parse::Pointer>, #<Parse::Pointer>, ...],
|
|
4002
4075
|
# # "image" => [#<Parse::Pointer>, ...],
|
|
@@ -4048,7 +4121,7 @@ module Parse
|
|
|
4048
4121
|
|
|
4049
4122
|
# Convert query results to a formatted table display.
|
|
4050
4123
|
# @param columns [Array<Symbol, String, Hash>] column definitions. Can be:
|
|
4051
|
-
# - Symbol/String: field name (e.g., :object_id, :name) or dot notation (e.g., "project.
|
|
4124
|
+
# - Symbol/String: field name (e.g., :object_id, :name) or dot notation (e.g., "project.workspace.name")
|
|
4052
4125
|
# - Hash: { field: :custom_name, header: "Custom Header" }
|
|
4053
4126
|
# - Hash: { block: ->(obj) { obj.some_calculation }, header: "Calculated" }
|
|
4054
4127
|
# @param format [Symbol] output format (:ascii, :csv, :json)
|
|
@@ -4059,17 +4132,17 @@ module Parse
|
|
|
4059
4132
|
# Project.query.to_table([:object_id, :name, :address])
|
|
4060
4133
|
#
|
|
4061
4134
|
# # With dot notation for related objects
|
|
4062
|
-
#
|
|
4135
|
+
# Document.query.to_table([
|
|
4063
4136
|
# :object_id,
|
|
4064
4137
|
# "project.name", # Access project name through relationship
|
|
4065
|
-
# "project.
|
|
4138
|
+
# "project.workspace.name", # Access workspace name through project->workspace relationship
|
|
4066
4139
|
# :file_size
|
|
4067
4140
|
# ])
|
|
4068
4141
|
#
|
|
4069
4142
|
# # With custom headers and calculated columns
|
|
4070
4143
|
# Project.query.to_table([
|
|
4071
4144
|
# { field: :object_id, header: "ID" },
|
|
4072
|
-
# { field: "
|
|
4145
|
+
# { field: "workspace.name", header: "Workspace Name" },
|
|
4073
4146
|
# { field: :address, header: "Project Address" },
|
|
4074
4147
|
# { block: ->(proj) { proj.notes.count }, header: "Note Count" }
|
|
4075
4148
|
# ])
|
|
@@ -4119,12 +4192,12 @@ module Parse
|
|
|
4119
4192
|
# Note: This is primarily for consistency - date groupings typically use formatted date strings as keys.
|
|
4120
4193
|
# @return [GroupByDate, SortableGroupByDate] an object that supports chaining aggregation methods.
|
|
4121
4194
|
# @example
|
|
4122
|
-
#
|
|
4123
|
-
#
|
|
4124
|
-
#
|
|
4195
|
+
# Post.group_by_date(:created_at, :day).count
|
|
4196
|
+
# Document.group_by_date(:created_at, :month).sum(:file_size)
|
|
4197
|
+
# Post.where(:project => project_id).group_by_date(:created_at, :week).average(:duration)
|
|
4125
4198
|
#
|
|
4126
4199
|
# # Sortable date results:
|
|
4127
|
-
#
|
|
4200
|
+
# Document.group_by_date(:created_at, :day, sortable: true).count.sort_by_value_desc
|
|
4128
4201
|
# # => [["2024-11-25", 45], ["2024-11-24", 23], ...]
|
|
4129
4202
|
# @param mongo_direct [Boolean] if true, queries MongoDB directly bypassing Parse Server.
|
|
4130
4203
|
# Requires Parse::MongoDB to be configured. Default: false.
|
|
@@ -4150,12 +4223,12 @@ module Parse
|
|
|
4150
4223
|
# @return [Array] array of distinct values, with Parse pointers populated as full objects.
|
|
4151
4224
|
# @example
|
|
4152
4225
|
# # Basic usage (returns raw values for non-pointer fields)
|
|
4153
|
-
#
|
|
4226
|
+
# Document.query.distinct_objects(:media_format)
|
|
4154
4227
|
# # => ["video", "audio", "photo"]
|
|
4155
4228
|
#
|
|
4156
4229
|
# # Auto-populate Parse pointer objects (much faster than manual conversion)
|
|
4157
|
-
#
|
|
4158
|
-
# # => [#<
|
|
4230
|
+
# Document.query.distinct_objects(:author_workspace)
|
|
4231
|
+
# # => [#<Workspace:0x123 @attributes={"name"=>"Workspace A", ...}>, ...]
|
|
4159
4232
|
def distinct_objects(field, return_pointers: false)
|
|
4160
4233
|
if field.nil? || !field.respond_to?(:to_s)
|
|
4161
4234
|
raise ArgumentError, "Invalid field name passed to `distinct_objects`."
|
|
@@ -4244,7 +4317,7 @@ module Parse
|
|
|
4244
4317
|
end
|
|
4245
4318
|
|
|
4246
4319
|
# Extract field value from object (similar to pluck logic).
|
|
4247
|
-
# Supports dot notation for nested attributes (e.g., "project.
|
|
4320
|
+
# Supports dot notation for nested attributes (e.g., "project.workspace.name").
|
|
4248
4321
|
# @param obj [Object] object to extract from
|
|
4249
4322
|
# @param field [Symbol, String] field name or dot-notation path
|
|
4250
4323
|
# @return [Object] field value
|
|
@@ -4584,8 +4657,8 @@ module Parse
|
|
|
4584
4657
|
|
|
4585
4658
|
# Check if a field is a pointer field by looking at the Parse class definition
|
|
4586
4659
|
# @param parse_class [Class] the Parse::Object subclass
|
|
4587
|
-
# @param field [Symbol, String] the original field name (e.g., :
|
|
4588
|
-
# @param formatted_field [String] the formatted field name (e.g., "
|
|
4660
|
+
# @param field [Symbol, String] the original field name (e.g., :author_workspace)
|
|
4661
|
+
# @param formatted_field [String] the formatted field name (e.g., "authorWorkspace")
|
|
4589
4662
|
# @return [Boolean] true if the field is a pointer field
|
|
4590
4663
|
def is_pointer_field?(parse_class, field, formatted_field)
|
|
4591
4664
|
return false unless parse_class.respond_to?(:fields)
|
|
@@ -4836,7 +4909,7 @@ module Parse
|
|
|
4836
4909
|
end
|
|
4837
4910
|
end
|
|
4838
4911
|
|
|
4839
|
-
# Convert constraint field names to aggregation format (e.g.,
|
|
4912
|
+
# Convert constraint field names to aggregation format (e.g., authorWorkspace -> _p_authorWorkspace for pointers)
|
|
4840
4913
|
# @param constraints [Hash] the constraints hash to convert
|
|
4841
4914
|
# @return [Hash] the converted constraints with aggregation-compatible field names
|
|
4842
4915
|
def convert_constraints_for_aggregation(constraints)
|
|
@@ -4847,8 +4920,8 @@ module Parse
|
|
|
4847
4920
|
# Skip special Parse operators, but recurse into the boolean
|
|
4848
4921
|
# combinators so a pointer-field rewrite is not bypassed when
|
|
4849
4922
|
# the LLM (or any caller) wraps the constraint in $or/$and/$nor.
|
|
4850
|
-
# Without this, `{ "$or" => [{ "
|
|
4851
|
-
# would ship to MongoDB with `
|
|
4923
|
+
# Without this, `{ "$or" => [{ "workspace" => { "$in" => ["bare"] } }] }`
|
|
4924
|
+
# would ship to MongoDB with `workspace` un-rewritten to `_p_workspace` —
|
|
4852
4925
|
# the canonical silent-zero pattern.
|
|
4853
4926
|
if field.to_s.start_with?("$")
|
|
4854
4927
|
if value.is_a?(Array) && %w[$and $or $nor].include?(field.to_s)
|
|
@@ -5607,11 +5680,11 @@ module Parse
|
|
|
5607
5680
|
# Ruby's `Hash#sort` default of sorting by key.
|
|
5608
5681
|
# @return [self]
|
|
5609
5682
|
# @example Biggest groups first
|
|
5610
|
-
#
|
|
5683
|
+
# Document.group_by(:category).order(value: :desc).count
|
|
5611
5684
|
# @example Alphabetical group keys
|
|
5612
|
-
#
|
|
5685
|
+
# Document.group_by(:category).order(key: :asc).count
|
|
5613
5686
|
# @example Groups with the most members first
|
|
5614
|
-
#
|
|
5687
|
+
# Document.group_by(:category).order(size: :desc).list
|
|
5615
5688
|
def order(spec)
|
|
5616
5689
|
target, direction =
|
|
5617
5690
|
case spec
|
|
@@ -5652,8 +5725,8 @@ module Parse
|
|
|
5652
5725
|
# @param direction [Symbol] `:asc` (default) or `:desc`
|
|
5653
5726
|
# @return [self]
|
|
5654
5727
|
# @example
|
|
5655
|
-
#
|
|
5656
|
-
#
|
|
5728
|
+
# Document.group_by(:category).sort.count # group keys ascending
|
|
5729
|
+
# Document.group_by(:category).sort(:desc).count # group keys descending
|
|
5657
5730
|
def sort(direction = :asc)
|
|
5658
5731
|
order(direction)
|
|
5659
5732
|
end
|
|
@@ -5662,8 +5735,8 @@ module Parse
|
|
|
5662
5735
|
# This is useful for debugging and understanding the generated pipeline.
|
|
5663
5736
|
# @return [Array<Hash>] the MongoDB aggregation pipeline
|
|
5664
5737
|
# @example
|
|
5665
|
-
#
|
|
5666
|
-
# # => [{"$match"=>{"
|
|
5738
|
+
# Post.where(:author_workspace.eq => workspace).group_by(:last_action).pipeline
|
|
5739
|
+
# # => [{"$match"=>{"authorWorkspace"=>"Workspace$abc123"}}, {"$group"=>{"_id"=>"$lastAction", "count"=>{"$sum"=>1}}}, {"$project"=>{"_id"=>0, "objectId"=>"$_id", "count"=>1}}]
|
|
5667
5740
|
def pipeline
|
|
5668
5741
|
# This introspection builds the same shape as the count execution
|
|
5669
5742
|
# path (`$sum: 1`), so reject order/aggregation combinations that
|
|
@@ -5774,7 +5847,7 @@ module Parse
|
|
|
5774
5847
|
# Count the number of items in each group.
|
|
5775
5848
|
# @return [Hash] a hash with group values as keys and counts as values.
|
|
5776
5849
|
# @example
|
|
5777
|
-
#
|
|
5850
|
+
# Document.group_by(:category).count
|
|
5778
5851
|
# # => {"image" => 45, "video" => 23, "audio" => 12}
|
|
5779
5852
|
def count
|
|
5780
5853
|
execute_group_aggregation("count", { "$sum" => 1 })
|
|
@@ -5784,7 +5857,7 @@ module Parse
|
|
|
5784
5857
|
# @param field [Symbol, String] the field to sum within each group.
|
|
5785
5858
|
# @return [Hash] a hash with group values as keys and sums as values.
|
|
5786
5859
|
# @example
|
|
5787
|
-
#
|
|
5860
|
+
# Document.group_by(:project).sum(:file_size)
|
|
5788
5861
|
# # => {"Project1" => 1024000, "Project2" => 512000}
|
|
5789
5862
|
def sum(field)
|
|
5790
5863
|
if field.nil? || !field.respond_to?(:to_s)
|
|
@@ -5799,7 +5872,7 @@ module Parse
|
|
|
5799
5872
|
# @param field [Symbol, String] the field to average within each group.
|
|
5800
5873
|
# @return [Hash] a hash with group values as keys and averages as values.
|
|
5801
5874
|
# @example
|
|
5802
|
-
#
|
|
5875
|
+
# Document.group_by(:category).average(:duration)
|
|
5803
5876
|
# # => {"video" => 120.5, "audio" => 45.2}
|
|
5804
5877
|
def average(field)
|
|
5805
5878
|
if field.nil? || !field.respond_to?(:to_s)
|
|
@@ -5848,10 +5921,10 @@ module Parse
|
|
|
5848
5921
|
# @return [Hash{Object => Array<Parse::Object>}] mapping of group key to
|
|
5849
5922
|
# the Parse::Object instances in that group.
|
|
5850
5923
|
# @example
|
|
5851
|
-
#
|
|
5852
|
-
# # => {"image" => [<
|
|
5924
|
+
# Document.where(:status => "active").group_by(:category).list
|
|
5925
|
+
# # => {"image" => [<Document:...>, <Document:...>], "video" => [<Document:...>]}
|
|
5853
5926
|
# @example Largest groups first
|
|
5854
|
-
#
|
|
5927
|
+
# Document.group_by(:category).order(size: :desc).list
|
|
5855
5928
|
# @note On the Parse REST `/aggregate` path there is no ACL/CLP/protectedFields
|
|
5856
5929
|
# enforcement — that endpoint is master-key-only. On the mongo-direct path
|
|
5857
5930
|
# the SDK's ACL `$match` runs before `$group`, and both ACL redaction and
|
|
@@ -6232,8 +6305,8 @@ module Parse
|
|
|
6232
6305
|
# @param headers [Array<String>] custom headers (default: ["Group", "Count"])
|
|
6233
6306
|
# @return [String] formatted table
|
|
6234
6307
|
# @example
|
|
6235
|
-
#
|
|
6236
|
-
#
|
|
6308
|
+
# Document.group_by(:category, sortable: true).count.to_table
|
|
6309
|
+
# Document.group_by(:category).sum(:file_size).to_table(headers: ["Category", "Total Size"])
|
|
6237
6310
|
def to_table(format: :ascii, headers: ["Group", "Count"])
|
|
6238
6311
|
pairs = @results.to_a
|
|
6239
6312
|
|
|
@@ -6425,9 +6498,9 @@ module Parse
|
|
|
6425
6498
|
# - `:asc`/`:desc` shorthand for `{ key: direction }`
|
|
6426
6499
|
# @return [self]
|
|
6427
6500
|
# @example Newest periods first
|
|
6428
|
-
#
|
|
6501
|
+
# Post.group_by_date(:created_at, :day).order(key: :desc).count
|
|
6429
6502
|
# @example Busiest day first
|
|
6430
|
-
#
|
|
6503
|
+
# Post.group_by_date(:created_at, :day).order(value: :desc).count
|
|
6431
6504
|
def order(spec)
|
|
6432
6505
|
target, direction =
|
|
6433
6506
|
case spec
|
|
@@ -6466,8 +6539,8 @@ module Parse
|
|
|
6466
6539
|
# This is useful for debugging and understanding the generated pipeline.
|
|
6467
6540
|
# @return [Array<Hash>] the MongoDB aggregation pipeline
|
|
6468
6541
|
# @example
|
|
6469
|
-
#
|
|
6470
|
-
# # => [{"$match"=>{"
|
|
6542
|
+
# Post.where(:author_workspace.eq => workspace).group_by_date(:created_at, :month).pipeline
|
|
6543
|
+
# # => [{"$match"=>{"authorWorkspace"=>"Workspace$abc123"}}, {"$group"=>{"_id"=>{"year"=>{"$year"=>"$createdAt"}, "month"=>{"$month"=>"$createdAt"}}, "count"=>{"$sum"=>1}}}, {"$project"=>{"_id"=>0, "objectId"=>"$_id", "count"=>1}}]
|
|
6471
6544
|
def pipeline
|
|
6472
6545
|
# Format the date field name
|
|
6473
6546
|
formatted_date_field = @query.send(:format_aggregation_field, @date_field)
|
|
@@ -6511,7 +6584,7 @@ module Parse
|
|
|
6511
6584
|
# Count the number of items in each time period.
|
|
6512
6585
|
# @return [Hash] a hash with formatted date strings as keys and counts as values.
|
|
6513
6586
|
# @example
|
|
6514
|
-
#
|
|
6587
|
+
# Post.group_by_date(:created_at, :day).count
|
|
6515
6588
|
# # => {"2024-11-24" => 45, "2024-11-25" => 23}
|
|
6516
6589
|
def count
|
|
6517
6590
|
execute_date_aggregation("count", { "$sum" => 1 })
|
|
@@ -6521,7 +6594,7 @@ module Parse
|
|
|
6521
6594
|
# @param field [Symbol, String] the field to sum within each time period.
|
|
6522
6595
|
# @return [Hash] a hash with formatted date strings as keys and sums as values.
|
|
6523
6596
|
# @example
|
|
6524
|
-
#
|
|
6597
|
+
# Document.group_by_date(:created_at, :month).sum(:file_size)
|
|
6525
6598
|
# # => {"2024-11" => 1024000, "2024-12" => 512000}
|
|
6526
6599
|
def sum(field)
|
|
6527
6600
|
if field.nil? || !field.respond_to?(:to_s)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
class <%= class_name %> < Parse::Object
|
|
3
|
-
# See: https://github.com/
|
|
3
|
+
# See: https://github.com/neurosynq/parse-stack-next#defining-properties
|
|
4
4
|
|
|
5
5
|
# You can change the inferred Parse table/collection name below
|
|
6
6
|
# parse_class "<%= class_name.to_s.to_parse_class %>"
|
|
@@ -13,7 +13,7 @@ class <%= class_name %> < Parse::Object
|
|
|
13
13
|
property :<%= attr.name %>, :<%= parse_type -%>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
|
-
# See: https://github.com/
|
|
16
|
+
# See: https://github.com/neurosynq/parse-stack-next#cloud-code-webhooks
|
|
17
17
|
# define a before save webhook for <%= class_name %>
|
|
18
18
|
webhook :before_save do
|
|
19
19
|
<%= class_name.to_s.underscore %> = parse_object
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "parse/stack"
|
|
2
2
|
|
|
3
3
|
# Set your specific Parse keys in your ENV. For all connection options, see
|
|
4
|
-
# https://github.com/
|
|
4
|
+
# https://github.com/neurosynq/parse-stack-next#connection-setup
|
|
5
5
|
Parse.setup app_id: ENV["PARSE_SERVER_APPLICATION_ID"],
|
|
6
6
|
api_key: ENV["PARSE_SERVER_REST_API_KEY"],
|
|
7
7
|
master_key: ENV["PARSE_SERVER_MASTER_KEY"], # optional
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# See: https://github.com/
|
|
1
|
+
# See: https://github.com/neurosynq/parse-stack-next#cloud-code-webhooks
|
|
2
2
|
Parse::Webhooks.route(:function, :helloWorld) do
|
|
3
3
|
# use the Parse::Payload instance methods in this block
|
|
4
4
|
name = params["name"].to_s #function params
|