parse-stack-next 5.6.0 → 5.7.0

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +7 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/classes/role.rb +227 -28
  75. data/lib/parse/model/classes/user.rb +60 -1
  76. data/lib/parse/model/clp.rb +4 -4
  77. data/lib/parse/model/core/actions.rb +97 -15
  78. data/lib/parse/model/core/create_lock.rb +0 -2
  79. data/lib/parse/model/core/describe.rb +57 -57
  80. data/lib/parse/model/core/embed_managed.rb +21 -23
  81. data/lib/parse/model/core/fetching.rb +2 -0
  82. data/lib/parse/model/core/indexing.rb +14 -14
  83. data/lib/parse/model/core/parse_reference.rb +1 -1
  84. data/lib/parse/model/core/properties.rb +2 -2
  85. data/lib/parse/model/core/querying.rb +1 -1
  86. data/lib/parse/model/core/schema.rb +2 -2
  87. data/lib/parse/model/core/search_indexing.rb +2 -2
  88. data/lib/parse/model/core/vector_searchable.rb +4 -5
  89. data/lib/parse/model/file.rb +21 -15
  90. data/lib/parse/model/geojson.rb +2 -2
  91. data/lib/parse/model/geopoint.rb +1 -0
  92. data/lib/parse/model/object.rb +132 -66
  93. data/lib/parse/model/pointer.rb +2 -0
  94. data/lib/parse/model/polygon.rb +3 -6
  95. data/lib/parse/model/push.rb +2 -2
  96. data/lib/parse/model/vector.rb +3 -1
  97. data/lib/parse/mongodb.rb +512 -282
  98. data/lib/parse/pipeline_security.rb +3 -0
  99. data/lib/parse/query/constraints.rb +46 -46
  100. data/lib/parse/query.rb +146 -59
  101. data/lib/parse/retrieval/agent_tool.rb +34 -20
  102. data/lib/parse/retrieval/chunk.rb +1 -0
  103. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  104. data/lib/parse/retrieval/reranker.rb +3 -4
  105. data/lib/parse/retrieval/retriever.rb +4 -6
  106. data/lib/parse/schema/index_migrator.rb +26 -26
  107. data/lib/parse/schema/search_index_migrator.rb +19 -19
  108. data/lib/parse/stack/tasks.rb +6 -6
  109. data/lib/parse/stack/version.rb +1 -1
  110. data/lib/parse/stack.rb +10 -10
  111. data/lib/parse/vector_search/hybrid.rb +66 -41
  112. data/lib/parse/vector_search.rb +14 -11
  113. data/lib/parse/webhooks/payload.rb +1 -0
  114. data/lib/parse/webhooks/registration.rb +4 -4
  115. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  116. data/lib/parse/webhooks.rb +22 -4
  117. data/parse-stack-next.gemspec +6 -6
  118. metadata +10 -1
@@ -31,10 +31,10 @@ require "parse-stack-next"
31
31
  # ---------------------------------------------------------------------------
32
32
  Parse.setup(
33
33
  server_url: ENV.fetch("PARSE_SERVER_URL", "http://localhost:1337/parse"),
34
- app_id: ENV.fetch("PARSE_APP_ID"),
35
- api_key: ENV.fetch("PARSE_REST_KEY"),
34
+ app_id: ENV.fetch("PARSE_APP_ID"),
35
+ api_key: ENV.fetch("PARSE_REST_KEY"),
36
36
  master_key: nil, # explicit: never set this from env in client builds
37
- logging: false,
37
+ logging: false,
38
38
  )
39
39
 
40
40
  # Belt-and-suspenders: prove the master key really is absent.
@@ -22,8 +22,8 @@ require "parse-stack-next"
22
22
  # ---------------------------------------------------------------------------
23
23
  Parse.setup(
24
24
  server_url: ENV.fetch("PARSE_SERVER_URL", "http://localhost:1337/parse"),
25
- app_id: ENV.fetch("PARSE_APP_ID"),
26
- api_key: ENV.fetch("PARSE_REST_KEY"),
25
+ app_id: ENV.fetch("PARSE_APP_ID"),
26
+ api_key: ENV.fetch("PARSE_REST_KEY"),
27
27
  master_key: ENV.fetch("PARSE_MASTER_KEY"),
28
28
  )
29
29
 
@@ -98,7 +98,7 @@ popular = Song.query(:plays.gt => 1_500)
98
98
  .order(:plays.desc)
99
99
  .limit(10)
100
100
  .results
101
- puts "Popular songs: #{popular.map(&:title).join(', ')}"
101
+ puts "Popular songs: #{popular.map(&:title).join(", ")}"
102
102
 
103
103
  puts "Total songs by #{artist.name}: #{Song.count(artist: artist)}"
104
104
 
@@ -32,17 +32,17 @@ require "parse/live_query"
32
32
  # ---------------------------------------------------------------------------
33
33
  Parse.setup(
34
34
  server_url: ENV.fetch("PARSE_SERVER_URL", "http://localhost:1337/parse"),
35
- app_id: ENV.fetch("PARSE_APP_ID"),
36
- api_key: ENV.fetch("PARSE_REST_KEY"),
35
+ app_id: ENV.fetch("PARSE_APP_ID"),
36
+ api_key: ENV.fetch("PARSE_REST_KEY"),
37
37
  master_key: nil, # a plain client — the session token does the scoping
38
- logging: false,
38
+ logging: false,
39
39
  )
40
40
 
41
41
  Parse.live_query_enabled = true
42
42
  Parse::LiveQuery.configure do |config|
43
- config.url = ENV.fetch("PARSE_LIVE_QUERY_URL", "ws://localhost:1337/parse")
43
+ config.url = ENV.fetch("PARSE_LIVE_QUERY_URL", "ws://localhost:1337/parse")
44
44
  config.application_id = ENV.fetch("PARSE_APP_ID")
45
- config.client_key = ENV.fetch("PARSE_REST_KEY")
45
+ config.client_key = ENV.fetch("PARSE_REST_KEY")
46
46
  end
47
47
 
48
48
  class Post < Parse::Object
@@ -73,13 +73,13 @@ subscription = Post.subscribe(
73
73
  session_token: user.session_token,
74
74
  )
75
75
 
76
- subscription.on(:subscribe) { puts "[#{stamp}] subscribed — waiting for events…" }
77
- subscription.on(:create) { |post| puts "[#{stamp}] CREATE #{post.id} #{post.title.inspect}" }
78
- subscription.on(:update) { |post, _orig| puts "[#{stamp}] UPDATE #{post.id} #{post.title.inspect}" }
79
- subscription.on(:delete) { |post| puts "[#{stamp}] DELETE #{post.id}" }
80
- subscription.on(:enter) { |post, _orig| puts "[#{stamp}] ENTER #{post.id} (now matches query)" }
81
- subscription.on(:leave) { |post, _orig| puts "[#{stamp}] LEAVE #{post.id} (no longer matches)" }
82
- subscription.on(:error) { |err| warn "[#{stamp}] ERROR #{err}" }
76
+ subscription.on(:subscribe) { puts "[#{stamp}] subscribed — waiting for events…" }
77
+ subscription.on(:create) { |post| puts "[#{stamp}] CREATE #{post.id} #{post.title.inspect}" }
78
+ subscription.on(:update) { |post, _orig| puts "[#{stamp}] UPDATE #{post.id} #{post.title.inspect}" }
79
+ subscription.on(:delete) { |post| puts "[#{stamp}] DELETE #{post.id}" }
80
+ subscription.on(:enter) { |post, _orig| puts "[#{stamp}] ENTER #{post.id} (now matches query)" }
81
+ subscription.on(:leave) { |post, _orig| puts "[#{stamp}] LEAVE #{post.id} (no longer matches)" }
82
+ subscription.on(:error) { |err| warn "[#{stamp}] ERROR #{err}" }
83
83
 
84
84
  # ---------------------------------------------------------------------------
85
85
  # 4. Block and print until Ctrl-C
@@ -43,8 +43,8 @@ Parse::Embeddings.register(
43
43
 
44
44
  Parse.setup(
45
45
  server_url: ENV.fetch("PARSE_SERVER_URL", "http://localhost:1337/parse"),
46
- app_id: ENV.fetch("PARSE_APP_ID"),
47
- api_key: ENV.fetch("PARSE_REST_KEY"),
46
+ app_id: ENV.fetch("PARSE_APP_ID"),
47
+ api_key: ENV.fetch("PARSE_REST_KEY"),
48
48
  master_key: ENV.fetch("PARSE_MASTER_KEY"),
49
49
  )
50
50
 
@@ -76,7 +76,7 @@ class KnowledgeArticle < Parse::Object
76
76
  # Declare the Atlas $vectorSearch index that retrieval needs.
77
77
  mongo_search_index "knowledge_embedding",
78
78
  { fields: [{ type: "vector", path: "embedding",
79
- numDimensions: 1536, similarity: "cosine" }] },
79
+ numDimensions: 1536, similarity: "cosine" }] },
80
80
  type: "vectorSearch"
81
81
  end
82
82
 
@@ -101,26 +101,26 @@ module ChatAnswerer
101
101
  # --- OpenAI backend ---
102
102
  def openai(question, chunks, model: "gpt-4o-mini")
103
103
  post("https://api.openai.com/v1/chat/completions",
104
- { "Authorization" => "Bearer #{ENV.fetch('OPENAI_API_KEY')}" },
104
+ { "Authorization" => "Bearer #{ENV.fetch("OPENAI_API_KEY")}" },
105
105
  { model: model,
106
- messages: [
107
- { role: "system", content: PROMPT },
108
- { role: "user",
109
- content: "Context:\n#{context(chunks)}\n\nQuestion: #{question}" },
110
- ] })
106
+ messages: [
107
+ { role: "system", content: PROMPT },
108
+ { role: "user",
109
+ content: "Context:\n#{context(chunks)}\n\nQuestion: #{question}" },
110
+ ] })
111
111
  .dig("choices", 0, "message", "content")
112
112
  end
113
113
 
114
114
  # --- Anthropic backend ---
115
115
  def anthropic(question, chunks, model: "claude-opus-4-8")
116
116
  post("https://api.anthropic.com/v1/messages",
117
- { "x-api-key" => ENV.fetch("ANTHROPIC_API_KEY"),
117
+ { "x-api-key" => ENV.fetch("ANTHROPIC_API_KEY"),
118
118
  "anthropic-version" => "2023-06-01" },
119
119
  { model: model, max_tokens: 1024, system: PROMPT,
120
- messages: [
121
- { role: "user",
122
- content: "Context:\n#{context(chunks)}\n\nQuestion: #{question}" },
123
- ] })
120
+ messages: [
121
+ { role: "user",
122
+ content: "Context:\n#{context(chunks)}\n\nQuestion: #{question}" },
123
+ ] })
124
124
  .dig("content", 0, "text")
125
125
  end
126
126
 
@@ -4,33 +4,33 @@
4
4
  # This example demonstrates how to use transactions to ensure atomic operations
5
5
  # across multiple Parse objects.
6
6
 
7
- require 'parse/stack'
7
+ require "parse/stack"
8
8
 
9
9
  # Configure your Parse application
10
10
  Parse.setup(
11
- app_id: ENV['PARSE_APP_ID'] || 'your-app-id',
12
- api_key: ENV['PARSE_API_KEY'] || 'your-api-key',
13
- server_url: ENV['PARSE_SERVER_URL'] || 'http://localhost:1337/parse'
11
+ app_id: ENV["PARSE_APP_ID"] || "your-app-id",
12
+ api_key: ENV["PARSE_API_KEY"] || "your-api-key",
13
+ server_url: ENV["PARSE_SERVER_URL"] || "http://localhost:1337/parse",
14
14
  )
15
15
 
16
16
  # Define example models
17
17
  class Workspace < Parse::Object
18
18
  property :name
19
- property :owner, :pointer, class_name: 'User'
19
+ property :owner, :pointer, class_name: "User"
20
20
  property :member_count, :integer, default: 0
21
21
  end
22
22
 
23
23
  class Subscription < Parse::Object
24
- property :user, :pointer, class_name: 'User'
25
- property :workspace, :pointer, class_name: 'Workspace'
24
+ property :user, :pointer, class_name: "User"
25
+ property :workspace, :pointer, class_name: "Workspace"
26
26
  property :access_level, :string
27
27
  property :grant, :string
28
28
  end
29
29
 
30
30
  class Project < Parse::Object
31
31
  property :name
32
- property :workspace, :pointer, class_name: 'Workspace'
33
- property :owner, :pointer, class_name: 'User'
32
+ property :workspace, :pointer, class_name: "Workspace"
33
+ property :owner, :pointer, class_name: "User"
34
34
  end
35
35
 
36
36
  # Example 1: Basic transaction with explicit batch operations
@@ -38,45 +38,45 @@ def transfer_project_ownership_basic(project, new_owner)
38
38
  Parse::Object.transaction do |batch|
39
39
  # Get old owner
40
40
  old_owner = project.owner
41
-
41
+
42
42
  # Find or create new owner subscription
43
43
  new_owner_membership = Subscription.first(
44
44
  project: project,
45
- user: new_owner
45
+ user: new_owner,
46
46
  )
47
-
47
+
48
48
  if new_owner_membership.nil?
49
49
  new_owner_membership = Subscription.new(
50
50
  project: project,
51
51
  workspace: project.workspace,
52
52
  user: new_owner,
53
- grant: 'project',
54
- access_level: 'owner'
53
+ grant: "project",
54
+ access_level: "owner",
55
55
  )
56
56
  batch.add(new_owner_membership)
57
57
  else
58
- new_owner_membership.access_level = 'owner'
58
+ new_owner_membership.access_level = "owner"
59
59
  batch.add(new_owner_membership)
60
60
  end
61
-
61
+
62
62
  # Demote old owner if they have a subscription
63
63
  if old_owner.present?
64
64
  old_owner_membership = Subscription.first(
65
65
  project: project,
66
- user: old_owner
66
+ user: old_owner,
67
67
  )
68
-
68
+
69
69
  if old_owner_membership.present?
70
- old_owner_membership.access_level = 'admin'
70
+ old_owner_membership.access_level = "admin"
71
71
  batch.add(old_owner_membership)
72
72
  end
73
73
  end
74
-
74
+
75
75
  # Update project owner
76
76
  project.owner = new_owner
77
77
  batch.add(project)
78
78
  end
79
-
79
+
80
80
  puts "Successfully transferred ownership"
81
81
  rescue Parse::Error => e
82
82
  puts "Transaction failed: #{e.message}"
@@ -88,42 +88,42 @@ def transfer_project_ownership_auto(project, new_owner)
88
88
  results = Parse::Object.transaction do
89
89
  old_owner = project.owner
90
90
  objects_to_save = []
91
-
91
+
92
92
  # Find or create new owner subscription
93
93
  new_owner_membership = Subscription.first(
94
94
  project: project,
95
- user: new_owner
95
+ user: new_owner,
96
96
  ) || Subscription.new(
97
97
  project: project,
98
98
  workspace: project.workspace,
99
99
  user: new_owner,
100
- grant: 'project'
100
+ grant: "project",
101
101
  )
102
-
103
- new_owner_membership.access_level = 'owner'
102
+
103
+ new_owner_membership.access_level = "owner"
104
104
  objects_to_save << new_owner_membership
105
-
105
+
106
106
  # Demote old owner
107
107
  if old_owner.present?
108
108
  old_owner_membership = Subscription.first(
109
109
  project: project,
110
- user: old_owner
110
+ user: old_owner,
111
111
  )
112
-
112
+
113
113
  if old_owner_membership.present?
114
- old_owner_membership.access_level = 'admin'
114
+ old_owner_membership.access_level = "admin"
115
115
  objects_to_save << old_owner_membership
116
116
  end
117
117
  end
118
-
118
+
119
119
  # Update project
120
120
  project.owner = new_owner
121
121
  objects_to_save << project
122
-
122
+
123
123
  # Return array of objects to be saved in transaction
124
124
  objects_to_save
125
125
  end
126
-
126
+
127
127
  puts "Transaction completed with #{results.count} operations"
128
128
  true
129
129
  rescue Parse::Error => e
@@ -138,32 +138,32 @@ def complex_team_operation(workspace, new_members, new_owner)
138
138
  unless new_members.include?(new_owner)
139
139
  raise Parse::Error, "New owner must be in members list"
140
140
  end
141
-
141
+
142
142
  # Update workspace
143
143
  workspace.owner = new_owner
144
144
  workspace.member_count = new_members.count
145
145
  batch.add(workspace)
146
-
146
+
147
147
  # Create subscriptions for all new members
148
148
  new_members.each do |member|
149
149
  subscription = Subscription.new(
150
150
  workspace: workspace,
151
151
  user: member,
152
- grant: 'workspace',
153
- access_level: member == new_owner ? 'owner' : 'member'
152
+ grant: "workspace",
153
+ access_level: member == new_owner ? "owner" : "member",
154
154
  )
155
155
  batch.add(subscription)
156
156
  end
157
-
157
+
158
158
  # Create a project for the workspace
159
159
  project = Project.new(
160
160
  name: "#{workspace.name} Project",
161
161
  workspace: workspace,
162
- owner: new_owner
162
+ owner: new_owner,
163
163
  )
164
164
  batch.add(project)
165
165
  end
166
-
166
+
167
167
  puts "Complex operation completed successfully"
168
168
  rescue Parse::Error => e
169
169
  puts "Complex operation failed: #{e.message}"
@@ -191,16 +191,16 @@ end
191
191
  if __FILE__ == $0
192
192
  puts "Parse Transaction Examples"
193
193
  puts "========================="
194
-
194
+
195
195
  begin
196
196
  # Example usage (requires actual Parse server and data)
197
197
  # project = Project.first
198
198
  # new_owner = Parse::User.first(username: "new_owner")
199
- #
199
+ #
200
200
  # if project && new_owner
201
201
  # transfer_project_ownership_basic(project, new_owner)
202
202
  # end
203
-
203
+
204
204
  puts "\nTransaction support has been added to parse-stack!"
205
205
  puts "\nKey features:"
206
206
  puts "1. Atomic operations - all succeed or all fail"
@@ -211,9 +211,8 @@ if __FILE__ == $0
211
211
  puts " Parse::Object.transaction do |batch|"
212
212
  puts " # Add operations to batch"
213
213
  puts " end"
214
-
215
214
  rescue => e
216
215
  puts "Error: #{e.message}"
217
216
  puts e.backtrace
218
217
  end
219
- end
218
+ end
@@ -30,8 +30,8 @@ require "parse-stack-next"
30
30
  # ---------------------------------------------------------------------------
31
31
  Parse.setup(
32
32
  server_url: ENV.fetch("PARSE_SERVER_URL", "http://localhost:1337/parse"),
33
- app_id: ENV.fetch("PARSE_APP_ID"),
34
- api_key: ENV.fetch("PARSE_REST_KEY"),
33
+ app_id: ENV.fetch("PARSE_APP_ID"),
34
+ api_key: ENV.fetch("PARSE_REST_KEY"),
35
35
  master_key: ENV.fetch("PARSE_MASTER_KEY"),
36
36
  )
37
37
 
@@ -56,7 +56,7 @@ class Post < Parse::Object
56
56
  # NON-Ruby client they run here only if a beforeSave/afterSave webhook is
57
57
  # registered for Post. Registering beforeSave enables BOTH before_save and
58
58
  # before_create; afterSave enables both after_save and after_create.
59
- before_save :normalize_slug
59
+ before_save :normalize_slug
60
60
  before_create { self.published = false } # created drafts start unpublished
61
61
  after_create :enqueue_welcome # see note on afterSave below
62
62