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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.env.sample +17 -3
  4. data/.github/workflows/codeql.yml +44 -0
  5. data/.github/workflows/docs.yml +39 -0
  6. data/.github/workflows/release.yml +32 -0
  7. data/.github/workflows/ruby.yml +8 -6
  8. data/.gitignore +4 -0
  9. data/.vscode/settings.json +3 -0
  10. data/CHANGELOG.md +305 -72
  11. data/Gemfile.lock +10 -3
  12. data/LICENSE.txt +1 -1
  13. data/README.md +190 -219
  14. data/Rakefile +1 -1
  15. data/SECURITY.md +30 -0
  16. data/assets/parse-stack-next-avatar.png +0 -0
  17. data/assets/parse-stack-next-avatar.svg +37 -0
  18. data/assets/parse-stack-next-banner.png +0 -0
  19. data/assets/parse-stack-next-banner.svg +45 -0
  20. data/assets/parse-stack-next-social-preview.png +0 -0
  21. data/docs/atlas_vector_search_guide.md +511 -0
  22. data/docs/client_sdk_guide.md +1320 -0
  23. data/docs/mcp_guide.md +225 -104
  24. data/docs/mongodb_direct_guide.md +21 -4
  25. data/docs/usage_guide.md +585 -0
  26. data/examples/transaction_example.rb +28 -28
  27. data/lib/parse/acl_scope.rb +2 -2
  28. data/lib/parse/agent/mcp_rack_app.rb +184 -16
  29. data/lib/parse/agent/metadata_dsl.rb +16 -16
  30. data/lib/parse/agent/pipeline_validator.rb +28 -1
  31. data/lib/parse/agent/prompts.rb +5 -5
  32. data/lib/parse/agent/tools.rb +287 -14
  33. data/lib/parse/agent.rb +209 -12
  34. data/lib/parse/api/analytics.rb +27 -5
  35. data/lib/parse/api/files.rb +6 -2
  36. data/lib/parse/api/push.rb +21 -4
  37. data/lib/parse/api/server.rb +59 -0
  38. data/lib/parse/api/users.rb +26 -2
  39. data/lib/parse/atlas_search/index_manager.rb +84 -0
  40. data/lib/parse/atlas_search.rb +37 -9
  41. data/lib/parse/cache/pool.rb +88 -0
  42. data/lib/parse/cache/redis.rb +249 -0
  43. data/lib/parse/client/body_builder.rb +94 -0
  44. data/lib/parse/client/caching.rb +109 -9
  45. data/lib/parse/client/response.rb +27 -0
  46. data/lib/parse/client.rb +74 -3
  47. data/lib/parse/console.rb +203 -0
  48. data/lib/parse/embeddings/cohere.rb +484 -0
  49. data/lib/parse/embeddings/fixture.rb +130 -0
  50. data/lib/parse/embeddings/jina.rb +454 -0
  51. data/lib/parse/embeddings/local_http.rb +492 -0
  52. data/lib/parse/embeddings/openai.rb +520 -0
  53. data/lib/parse/embeddings/provider.rb +264 -0
  54. data/lib/parse/embeddings/qwen.rb +431 -0
  55. data/lib/parse/embeddings/voyage.rb +550 -0
  56. data/lib/parse/embeddings.rb +225 -0
  57. data/lib/parse/graphql/scalars.rb +53 -0
  58. data/lib/parse/graphql/type_generator.rb +264 -0
  59. data/lib/parse/graphql.rb +48 -0
  60. data/lib/parse/live_query/client.rb +24 -5
  61. data/lib/parse/live_query/subscription.rb +17 -6
  62. data/lib/parse/live_query.rb +9 -4
  63. data/lib/parse/model/associations/collection_proxy.rb +2 -2
  64. data/lib/parse/model/associations/has_many.rb +32 -1
  65. data/lib/parse/model/associations/has_one.rb +17 -0
  66. data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
  67. data/lib/parse/model/classes/user.rb +307 -11
  68. data/lib/parse/model/clp.rb +1 -1
  69. data/lib/parse/model/core/create_lock.rb +14 -2
  70. data/lib/parse/model/core/embed_managed.rb +296 -0
  71. data/lib/parse/model/core/fetching.rb +4 -4
  72. data/lib/parse/model/core/indexing.rb +53 -14
  73. data/lib/parse/model/core/parse_reference.rb +3 -3
  74. data/lib/parse/model/core/properties.rb +70 -1
  75. data/lib/parse/model/core/querying.rb +57 -1
  76. data/lib/parse/model/core/vector_searchable.rb +285 -0
  77. data/lib/parse/model/file.rb +16 -4
  78. data/lib/parse/model/model.rb +26 -10
  79. data/lib/parse/model/object.rb +63 -6
  80. data/lib/parse/model/pointer.rb +16 -2
  81. data/lib/parse/model/shortnames.rb +2 -0
  82. data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
  83. data/lib/parse/model/vector.rb +102 -0
  84. data/lib/parse/mongodb.rb +90 -8
  85. data/lib/parse/pipeline_security.rb +59 -2
  86. data/lib/parse/query/constraints.rb +16 -14
  87. data/lib/parse/query/ordering.rb +1 -1
  88. data/lib/parse/query.rb +137 -64
  89. data/lib/parse/stack/generators/templates/model.erb +2 -2
  90. data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
  91. data/lib/parse/stack/generators/templates/model_role.rb +1 -1
  92. data/lib/parse/stack/generators/templates/model_session.rb +1 -1
  93. data/lib/parse/stack/generators/templates/parse.rb +1 -1
  94. data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
  95. data/lib/parse/stack/version.rb +1 -1
  96. data/lib/parse/stack.rb +375 -73
  97. data/lib/parse/two_factor_auth/user_extension.rb +5 -2
  98. data/lib/parse/vector_search.rb +341 -0
  99. data/parse-stack-next.gemspec +10 -9
  100. data/scripts/docker/docker-compose.test.yml +18 -0
  101. data/scripts/start-parse.sh +6 -0
  102. data/scripts/vector_prototype/create_vector_index.js +105 -0
  103. data/scripts/vector_prototype/fetch_embeddings.py +241 -0
  104. data/scripts/vector_prototype/fixture_manifest.json +9 -0
  105. data/scripts/vector_prototype/query_prototype.rb +84 -0
  106. data/scripts/vector_prototype/run.sh +34 -0
  107. metadata +77 -5
  108. 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
- @use_master_key = true
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
- # Asset.query.pluck(:name)
599
+ # Document.query.pluck(:name)
592
600
  # # => ["video1.mp4", "image1.jpg", "audio1.mp3"]
593
601
  #
594
- # # Get all author team IDs
595
- # Asset.query.pluck(:author_team)
596
- # # => [{"__type"=>"Pointer", "className"=>"Team", "objectId"=>"abc123"}, ...]
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
- # Asset.query.pluck(:created_at)
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 i strue, an hash representing the constraints.
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
- opts[:use_master_key] = self.use_master_key
1368
- opts[:session_token] = self.session_token
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
- unless use_master_key || @acl_user || @acl_role || has_session
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 = Asset.query.aggregate(pipeline)
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 = Asset.query.aggregate(pipeline, verbose: true)
3059
+ # aggregation = Document.query.aggregate(pipeline, verbose: true)
2987
3060
  # # With MongoDB direct (required for $inQuery constraints in aggregation)
2988
- # aggregation = Asset.query.aggregate(pipeline, mongo_direct: true)
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
- # Asset.group_by(:category).count
3953
- # Asset.where(:status => "active").group_by(:project).sum(:file_size)
3954
- # Asset.group_by(:media_format).average(:duration)
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
- # Asset.group_by(:tags, flatten_arrays: true).count
4032
+ # Document.group_by(:tags, flatten_arrays: true).count
3960
4033
  # # => {"a" => 1, "b" => 2, "c" => 1}
3961
4034
  #
3962
4035
  # # Sortable results:
3963
- # Asset.group_by(:category, sortable: true).count.sort_by_value_desc
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
- # Asset.group_by(:author_team, return_pointers: true).count
3968
- # # => {#<Parse::Pointer @parse_class="Team" @id="team1"> => 5, ...}
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 Asset objects grouped by category
3991
- # Asset.query.group_objects_by(:category)
4063
+ # # Get arrays of actual Document objects grouped by category
4064
+ # Document.query.group_objects_by(:category)
3992
4065
  # # => {
3993
- # # "video" => [#<Asset:video1>, #<Asset:video2>, ...],
3994
- # # "image" => [#<Asset:image1>, #<Asset:image2>, ...],
3995
- # # "audio" => [#<Asset:audio1>, ...]
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
- # Asset.query.group_objects_by(:category, return_pointers: true)
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.team.name")
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
- # Asset.query.to_table([
4135
+ # Document.query.to_table([
4063
4136
  # :object_id,
4064
4137
  # "project.name", # Access project name through relationship
4065
- # "project.team.name", # Access team name through project->team relationship
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: "team.name", header: "Team Name" },
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
- # Capture.group_by_date(:created_at, :day).count
4123
- # Asset.group_by_date(:created_at, :month).sum(:file_size)
4124
- # Capture.where(:project => project_id).group_by_date(:created_at, :week).average(:duration)
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
- # Asset.group_by_date(:created_at, :day, sortable: true).count.sort_by_value_desc
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
- # Asset.query.distinct_objects(:media_format)
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
- # Asset.query.distinct_objects(:author_team)
4158
- # # => [#<Team:0x123 @attributes={"name"=>"Team A", ...}>, ...]
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.team.name").
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., :author_team)
4588
- # @param formatted_field [String] the formatted field name (e.g., "authorTeam")
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., authorTeam -> _p_authorTeam for pointers)
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" => [{ "team" => { "$in" => ["bare"] } }] }`
4851
- # would ship to MongoDB with `team` un-rewritten to `_p_team` —
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
- # Asset.group_by(:category).order(value: :desc).count
5683
+ # Document.group_by(:category).order(value: :desc).count
5611
5684
  # @example Alphabetical group keys
5612
- # Asset.group_by(:category).order(key: :asc).count
5685
+ # Document.group_by(:category).order(key: :asc).count
5613
5686
  # @example Groups with the most members first
5614
- # Asset.group_by(:category).order(size: :desc).list
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
- # Asset.group_by(:category).sort.count # group keys ascending
5656
- # Asset.group_by(:category).sort(:desc).count # group keys descending
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
- # Capture.where(:author_team.eq => team).group_by(:last_action).pipeline
5666
- # # => [{"$match"=>{"authorTeam"=>"Team$abc123"}}, {"$group"=>{"_id"=>"$lastAction", "count"=>{"$sum"=>1}}}, {"$project"=>{"_id"=>0, "objectId"=>"$_id", "count"=>1}}]
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
- # Asset.group_by(:category).count
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
- # Asset.group_by(:project).sum(:file_size)
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
- # Asset.group_by(:category).average(:duration)
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
- # Asset.where(:status => "active").group_by(:category).list
5852
- # # => {"image" => [<Asset:...>, <Asset:...>], "video" => [<Asset:...>]}
5924
+ # Document.where(:status => "active").group_by(:category).list
5925
+ # # => {"image" => [<Document:...>, <Document:...>], "video" => [<Document:...>]}
5853
5926
  # @example Largest groups first
5854
- # Asset.group_by(:category).order(size: :desc).list
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
- # Asset.group_by(:category, sortable: true).count.to_table
6236
- # Asset.group_by(:category).sum(:file_size).to_table(headers: ["Category", "Total Size"])
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
- # Capture.group_by_date(:created_at, :day).order(key: :desc).count
6501
+ # Post.group_by_date(:created_at, :day).order(key: :desc).count
6429
6502
  # @example Busiest day first
6430
- # Capture.group_by_date(:created_at, :day).order(value: :desc).count
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
- # Capture.where(:author_team.eq => team).group_by_date(:created_at, :month).pipeline
6470
- # # => [{"$match"=>{"authorTeam"=>"Team$abc123"}}, {"$group"=>{"_id"=>{"year"=>{"$year"=>"$createdAt"}, "month"=>{"$month"=>"$createdAt"}}, "count"=>{"$sum"=>1}}}, {"$project"=>{"_id"=>0, "objectId"=>"$_id", "count"=>1}}]
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
- # Capture.group_by_date(:created_at, :day).count
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
- # Asset.group_by_date(:created_at, :month).sum(:file_size)
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/modernistik/parse-stack#defining-properties
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/modernistik/parse-stack#cloud-code-webhooks
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,4 +1,4 @@
1
1
  class Parse::Installation < Parse::Object
2
- # See: https://github.com/modernistik/parse-stack#parseinstallation
2
+ # See: https://github.com/neurosynq/parse-stack-next#parseinstallation
3
3
  # add additional properties here
4
4
  end
@@ -1,4 +1,4 @@
1
1
  class Parse::Role < Parse::Object
2
- # See: https://github.com/modernistik/parse-stack#parserole
2
+ # See: https://github.com/neurosynq/parse-stack-next#parserole
3
3
  # add additional properties here
4
4
  end
@@ -1,4 +1,4 @@
1
1
  class Parse::Session < Parse::Object
2
- # See: https://github.com/modernistik/parse-stack#parsesession
2
+ # See: https://github.com/neurosynq/parse-stack-next#parsesession
3
3
  # add additional properties here
4
4
  end
@@ -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/modernistik/parse-stack#connection-setup
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/modernistik/parse-stack#cloud-code-webhooks
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
@@ -6,6 +6,6 @@ module Parse
6
6
  # The Parse Server SDK for Ruby
7
7
  module Stack
8
8
  # The current version.
9
- VERSION = "4.5.0"
9
+ VERSION = "5.0.1"
10
10
  end
11
11
  end