parse-stack-next 5.1.1 → 5.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.env.sample +12 -0
  3. data/.env.test +4 -4
  4. data/CHANGELOG.md +545 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +6 -1
  7. data/README.md +167 -38
  8. data/Rakefile +56 -10
  9. data/docs/atlas_vector_search_guide.md +110 -9
  10. data/docs/mcp_guide.md +433 -0
  11. data/docs/mongodb_direct_guide.md +66 -1
  12. data/docs/mongodb_index_optimization_guide.md +22 -1
  13. data/docs/usage_guide.md +15 -0
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +90 -19
  16. data/lib/parse/agent/describe.rb +1 -0
  17. data/lib/parse/agent/errors.rb +16 -0
  18. data/lib/parse/agent/mcp_client.rb +9 -0
  19. data/lib/parse/agent/mcp_dispatcher.rb +139 -7
  20. data/lib/parse/agent/mcp_rack_app.rb +621 -17
  21. data/lib/parse/agent/mcp_subscriptions.rb +607 -0
  22. data/lib/parse/agent/metadata_dsl.rb +58 -0
  23. data/lib/parse/agent/metadata_registry.rb +141 -1
  24. data/lib/parse/agent/prompt_hardening.rb +213 -0
  25. data/lib/parse/agent/result_formatter.rb +18 -3
  26. data/lib/parse/agent/tools.rb +167 -24
  27. data/lib/parse/agent.rb +692 -21
  28. data/lib/parse/client/request.rb +55 -4
  29. data/lib/parse/client/response.rb +4 -0
  30. data/lib/parse/client.rb +205 -7
  31. data/lib/parse/model/classes/installation.rb +27 -10
  32. data/lib/parse/model/classes/user.rb +8 -0
  33. data/lib/parse/model/core/actions.rb +58 -4
  34. data/lib/parse/model/core/embed_managed.rb +19 -14
  35. data/lib/parse/model/core/indexing.rb +108 -16
  36. data/lib/parse/model/core/querying.rb +29 -0
  37. data/lib/parse/model/model.rb +34 -3
  38. data/lib/parse/model/object.rb +1 -0
  39. data/lib/parse/query.rb +90 -24
  40. data/lib/parse/retrieval/agent_tool.rb +369 -0
  41. data/lib/parse/retrieval/chunk.rb +74 -0
  42. data/lib/parse/retrieval/chunker.rb +208 -0
  43. data/lib/parse/retrieval/retriever.rb +274 -0
  44. data/lib/parse/retrieval.rb +10 -0
  45. data/lib/parse/schema.rb +69 -20
  46. data/lib/parse/stack/version.rb +2 -2
  47. data/parse-stack-next.gemspec +1 -1
  48. data/scripts/docker/docker-compose.atlas.yml +14 -10
  49. data/scripts/docker/docker-compose.test.yml +24 -20
  50. data/scripts/docker/mongo-init.js +3 -3
  51. data/scripts/start-parse.sh +10 -0
  52. data/scripts/start_mcp_server.rb +1 -1
  53. data/scripts/test_server_connection.rb +1 -1
  54. data/scripts/vector_prototype/create_vector_index.js +1 -1
  55. data/scripts/vector_prototype/fetch_embeddings.py +2 -2
  56. data/scripts/vector_prototype/query_prototype.rb +1 -1
  57. data/scripts/vector_prototype/run.sh +4 -4
  58. metadata +10 -2
@@ -0,0 +1,274 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "chunker"
5
+ require_relative "chunk"
6
+
7
+ module Parse
8
+ # Retrieval-augmented-generation (RAG) helpers. `Parse::RAG` is a
9
+ # discoverability alias for this module.
10
+ #
11
+ # {.retrieve} is the agent-agnostic core: it embeds a natural-language
12
+ # query, runs Atlas `$vectorSearch` through the existing
13
+ # `Class.find_similar` (which enforces ACL/CLP mongo-direct), then
14
+ # splits each retrieved document's text field into scored
15
+ # {Parse::Retrieval::Chunk}s for presentation.
16
+ #
17
+ # The agent-facing `semantic_search` tool (see
18
+ # `lib/parse/retrieval/agent_tool.rb`) wraps {.retrieve} with the
19
+ # agent security envelope (tenant scope, `field_allowlist` projection,
20
+ # score quantization).
21
+ #
22
+ # == ACL model
23
+ #
24
+ # {.retrieve} does NOT implement a REST "two-stage" re-query. The
25
+ # vector path is mongo-direct only (Parse Server's REST `/aggregate`
26
+ # is master-key-only and bypasses ACL — see the project notes), and
27
+ # `acl_user:` / `acl_role:` scopes have no REST equivalent. ACL is
28
+ # enforced inside `find_similar` via a post-`$vectorSearch` `_rperm`
29
+ # `$match`. Scope kwargs (`session_token:` / `acl_user:` /
30
+ # `acl_role:` / `master:`) pass straight through `**scope_opts`.
31
+ module Retrieval
32
+ # Raised when a tenant-scope value conflicts with a caller-supplied
33
+ # `vector_filter` constraint on the same field — a scope-spoofing
34
+ # attempt. Mirrors the agent layer's tenant-scope refusal.
35
+ class TenantScopeConflict < ArgumentError; end
36
+
37
+ # Raised when the text field to chunk cannot be inferred from the
38
+ # class's `embed` declarations and was not passed explicitly.
39
+ class AmbiguousTextField < ArgumentError; end
40
+
41
+ module_function
42
+
43
+ # Recursively refuse any underscore-prefixed key, at any depth, in a
44
+ # caller-supplied filter. This is distinct from (and stricter than)
45
+ # the agent layer's flat `validate_keys!`: a Mongo-style filter is a
46
+ # nested structure, and an underscore key buried inside `$or` /
47
+ # `$elemMatch` / a hash value could clobber tenant scope or reach a
48
+ # reserved column (`_rperm`, `_p_*`, `_auth_data_*`). The walk is
49
+ # unconditional — it does not special-case operators.
50
+ #
51
+ # @param obj [Object] a filter Hash/Array (or anything; scalars pass).
52
+ # @param path [Array<String>] internal — accumulates the key path for
53
+ # the error message.
54
+ # @raise [ArgumentError] on any `_`-prefixed key.
55
+ def assert_no_underscore_keys!(obj, path = [])
56
+ case obj
57
+ when Hash
58
+ obj.each do |k, v|
59
+ ks = k.to_s
60
+ if ks.start_with?("_")
61
+ raise ArgumentError,
62
+ "filter key '#{(path + [ks]).join(".")}' is reserved (underscore-prefixed)."
63
+ end
64
+ assert_no_underscore_keys!(v, path + [ks])
65
+ end
66
+ when Array
67
+ obj.each_with_index { |v, i| assert_no_underscore_keys!(v, path + ["[#{i}]"]) }
68
+ end
69
+ obj
70
+ end
71
+
72
+ # Retrieve and chunk documents semantically similar to `query`.
73
+ #
74
+ # @param query [String] natural-language query.
75
+ # @param klass [Class, String] a Parse::Object subclass (or its
76
+ # class name) declaring a `:vector` property. `class:` is accepted
77
+ # as an alias.
78
+ # @param field [Symbol, nil] the `:vector` property to search.
79
+ # Auto-resolved by `find_similar` when the class has exactly one.
80
+ # @param text_field [Symbol, nil] the text property to chunk for
81
+ # presentation. Defaults to the sole text source of the class's
82
+ # `embed` declaration; raises {AmbiguousTextField} when it can't be
83
+ # inferred.
84
+ # @param k [Integer] number of documents to retrieve. Default 10.
85
+ # @param filter [Hash, nil] post-`$vectorSearch` `$match` filter.
86
+ # @param vector_filter [Hash, nil] Atlas-native pre-search filter.
87
+ # @param chunker [#chunk_with_meta, #chunk, nil] chunking strategy.
88
+ # Defaults to {Chunker::FixedSizeOverlap}.
89
+ # @param tenant_scope [Hash, nil] `{ field:, value: }` merged into
90
+ # `vector_filter` (closing the cross-tenant existence side
91
+ # channel) — not just a post-stage match.
92
+ # @param score_quantize [Boolean] round scores to 1 decimal (limits
93
+ # membership-inference probing in non-admin contexts).
94
+ # @param source_transform [#call, nil] optional callable applied to
95
+ # each raw source record before it is stored on a Chunk. The agent
96
+ # tool injects tenant-scope assertion + `field_allowlist`
97
+ # projection here; a `StandardError` raised by the callable
98
+ # propagates and aborts the whole call (fail-closed). Kept as an
99
+ # injection point so this model-layer method stays free of any
100
+ # agent-layer dependency.
101
+ # @param hybrid [Object, nil] reserved — raises {NotImplementedError}
102
+ # if truthy. Hybrid (vector + lexical) retrieval lands in a later
103
+ # release; the kwarg locks the API shape now.
104
+ # @param rerank [Object, nil] reserved — raises {NotImplementedError}
105
+ # if non-nil. Cross-encoder rerank lands in a later release.
106
+ # @param scope_opts [Hash] ACL/CLP scope kwargs forwarded verbatim to
107
+ # `find_similar`: `session_token:` / `acl_user:` / `acl_role:` /
108
+ # `master:`.
109
+ # @return [Array<Parse::Retrieval::Chunk>] descending by score; chunk
110
+ # order within a document is positional.
111
+ def retrieve(query:, klass: nil, field: nil, text_field: nil, k: 10,
112
+ filter: nil, vector_filter: nil, chunker: nil,
113
+ tenant_scope: nil, score_quantize: false,
114
+ source_transform: nil, hybrid: nil, rerank: nil,
115
+ **scope_opts)
116
+ raise NotImplementedError,
117
+ "Parse::Retrieval.retrieve: `hybrid:` is reserved for a future release." if hybrid
118
+ raise NotImplementedError,
119
+ "Parse::Retrieval.retrieve: `rerank:` is reserved for a future release." if rerank
120
+
121
+ # `class:` alias (reserved word — arrives via **scope_opts).
122
+ klass ||= scope_opts.delete(:class)
123
+ klass = resolve_class!(klass)
124
+
125
+ unless query.is_a?(String) && !query.strip.empty?
126
+ raise ArgumentError, "Parse::Retrieval.retrieve: `query:` must be a non-empty String."
127
+ end
128
+
129
+ resolved_text_field = (text_field || infer_text_field!(klass)).to_sym
130
+ merged_vector_filter = fold_tenant_scope(klass, vector_filter, tenant_scope)
131
+ chunker ||= default_chunker
132
+
133
+ raw_hits = klass.find_similar(
134
+ text: query,
135
+ k: k,
136
+ field: field,
137
+ filter: filter,
138
+ vector_filter: merged_vector_filter,
139
+ raw: true,
140
+ **scope_opts,
141
+ )
142
+ return [] if raw_hits.nil? || raw_hits.empty?
143
+
144
+ text_wire = wire_name(klass, resolved_text_field)
145
+
146
+ raw_hits.flat_map do |doc|
147
+ build_chunks_for(doc, klass, text_wire, score_quantize, source_transform, chunker)
148
+ end
149
+ end
150
+
151
+ # @!visibility private
152
+ def resolve_class!(klass)
153
+ resolved =
154
+ case klass
155
+ when nil
156
+ nil
157
+ when Class
158
+ klass
159
+ else
160
+ Parse::Model.find_class(klass.to_s)
161
+ end
162
+ unless resolved.is_a?(Class) && resolved.respond_to?(:find_similar)
163
+ raise ArgumentError,
164
+ "Parse::Retrieval.retrieve: `klass:`/`class:` must be a Parse::Object " \
165
+ "subclass with a :vector property (got #{klass.inspect})."
166
+ end
167
+ resolved
168
+ end
169
+
170
+ # @!visibility private
171
+ # Infer the text field to chunk from the class's `embed` directives:
172
+ # the sole text (non-image) source field. Raises when zero or more
173
+ # than one candidate exists — the caller must then pass `text_field:`.
174
+ def infer_text_field!(klass)
175
+ directives = klass.respond_to?(:embed_directives) ? klass.embed_directives.values : []
176
+ sources = directives.reject { |d| d.respond_to?(:image?) && d.image? }
177
+ .flat_map(&:sources).uniq
178
+ return sources.first if sources.length == 1
179
+ raise AmbiguousTextField,
180
+ "Parse::Retrieval.retrieve: cannot infer the text field to chunk for " \
181
+ "#{klass} (candidates: #{sources.inspect}); pass `text_field:` explicitly."
182
+ end
183
+
184
+ # @!visibility private
185
+ def default_chunker
186
+ Chunker::FixedSizeOverlap.new(size: 800, overlap: 100)
187
+ end
188
+
189
+ # @!visibility private
190
+ # Merge the tenant scope into the Atlas pre-search filter using the
191
+ # field's wire/storage column name. A pre-existing constraint on the
192
+ # same field with a different value is a spoof attempt and is refused.
193
+ def fold_tenant_scope(klass, vector_filter, tenant_scope)
194
+ return vector_filter if tenant_scope.nil?
195
+ field = tenant_scope[:field] || tenant_scope["field"]
196
+ value = tenant_scope.key?(:value) ? tenant_scope[:value] : tenant_scope["value"]
197
+ return vector_filter if field.nil?
198
+
199
+ wire = wire_name(klass, field)
200
+ base = vector_filter ? vector_filter.dup : {}
201
+ existing_key = base.keys.find { |k| k.to_s == wire }
202
+ if existing_key && base[existing_key] != value
203
+ raise TenantScopeConflict,
204
+ "Parse::Retrieval.retrieve: vector_filter pins #{wire.inspect} to " \
205
+ "#{base[existing_key].inspect} but the tenant scope requires #{value.inspect}."
206
+ end
207
+ base[wire] = value
208
+ base
209
+ end
210
+
211
+ # @!visibility private
212
+ # Ruby property symbol -> wire/storage column name. Prefers the
213
+ # class's explicit field_map alias; falls back to lowerCamelCase
214
+ # columnization. Matches the resolution MetadataRegistry uses.
215
+ def wire_name(klass, field)
216
+ sym = field.to_sym
217
+ fmap = klass.respond_to?(:field_map) ? klass.field_map : {}
218
+ mapped = fmap[sym]
219
+ (mapped || sym.to_s.columnize).to_s
220
+ end
221
+
222
+ # @!visibility private
223
+ def fetch_field(doc, wire, sym)
224
+ return doc[wire] if doc.key?(wire)
225
+ return doc[wire.to_sym] if doc.key?(wire.to_sym)
226
+ return doc[sym.to_s] if doc.key?(sym.to_s)
227
+ doc[sym]
228
+ end
229
+
230
+ # @!visibility private
231
+ def build_chunks_for(doc, klass, text_wire, score_quantize, source_transform, chunker)
232
+ object_id = (doc["_id"] || doc[:_id] || doc["objectId"] || doc[:objectId]).to_s
233
+ raw_score = doc["_vscore"] || doc[:_vscore]
234
+ score = quantize_score(raw_score, score_quantize)
235
+
236
+ text = fetch_field(doc, text_wire, text_wire)
237
+ meta = chunker.respond_to?(:chunk_with_meta) ? chunker.chunk_with_meta(text) : nil
238
+ chunks = meta ? meta[:chunks] : Array(chunker.chunk(text))
239
+ truncated = meta ? meta[:truncated] : false
240
+ # A document that matched on its vector but carries no presentation
241
+ # text yields no chunks (skipped, not an empty-content chunk).
242
+ return [] if chunks.empty?
243
+
244
+ source = source_transform ? source_transform.call(doc) : doc
245
+ count = chunks.length
246
+ chunks.each_with_index.map do |content, idx|
247
+ Chunk.new(
248
+ id: "#{object_id}##{idx}",
249
+ content: content,
250
+ score: score,
251
+ source: source,
252
+ metadata: {
253
+ chunk_index: idx,
254
+ chunk_count: count,
255
+ chunks_truncated: truncated,
256
+ object_id: object_id,
257
+ class: klass.parse_class,
258
+ },
259
+ )
260
+ end
261
+ end
262
+
263
+ # @!visibility private
264
+ def quantize_score(score, quantize)
265
+ return score if score.nil?
266
+ f = score.to_f
267
+ quantize ? ((f * 10).round / 10.0) : f
268
+ end
269
+ end
270
+
271
+ # Discoverability alias. "RAG" ages badly as a term; `Retrieval` is
272
+ # the canonical name.
273
+ RAG = Retrieval
274
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Parse::Retrieval — retrieval-augmented-generation (RAG) helpers.
5
+ #
6
+ # Entry point that loads the chunker, the {Parse::Retrieval::Chunk}
7
+ # value object, and the {Parse::Retrieval.retrieve} core. The
8
+ # `semantic_search` agent tool (which depends on the agent layer) is
9
+ # loaded separately from `lib/parse/agent.rb`.
10
+ require_relative "retrieval/retriever"
data/lib/parse/schema.rb CHANGED
@@ -231,16 +231,23 @@ module Parse
231
231
  end
232
232
 
233
233
  # Fields defined locally but missing on server.
234
- # @return [Hash] field name => type pairs
234
+ #
235
+ # Iterates the model's `field_map` (one entry per canonical property,
236
+ # canonical name => wire column name) rather than `fields` (which carries
237
+ # both the snake_case and camelCase keys for every property and therefore
238
+ # double-counts multi-word fields). The wire name resolved from
239
+ # `field_map` is the authoritative server column — including custom
240
+ # `field:` mappings — so this both dedupes and fixes custom-column
241
+ # detection. Result is keyed by the CANONICAL (snake) name with the type
242
+ # taken from `fields[name]`.
243
+ # @return [Hash] canonical field name => type pairs
235
244
  def missing_on_server
236
- return local_fields unless server_exists?
237
-
238
- local = local_fields
239
- server = server_field_names
245
+ server = server_exists? ? server_field_names : []
240
246
  missing = {}
241
- local.each do |name, type|
242
- name_str = name.to_s.camelize(:lower)
243
- missing[name] = type unless server.include?(name_str) || core_field?(name)
247
+ @model_class.field_map.each do |name, wire|
248
+ next if core_field?(name)
249
+ next if server.include?(wire.to_s)
250
+ missing[name] = @model_class.fields[name]
244
251
  end
245
252
  missing
246
253
  end
@@ -262,15 +269,25 @@ module Parse
262
269
  end
263
270
 
264
271
  # Fields with type mismatches.
265
- # @return [Hash] field name => { local: type, server: type }
272
+ #
273
+ # Iterates `field_map` (canonical name => wire column) rather than
274
+ # deriving the server key with `camelize(:lower)`, so a property with a
275
+ # custom `field:` wire column (e.g. `property :post_id, field:
276
+ # "postIdentifier"`) resolves to its real server column instead of a
277
+ # camelized guess. This both dedupes multi-word fields (which appear
278
+ # under two keys in `fields`) and matches the `missing_on_server`
279
+ # resolution path, so type drift on custom-mapped columns is no longer
280
+ # silently skipped.
281
+ # @return [Hash] canonical field name => { local: type, server: type }
266
282
  def type_mismatches
267
283
  return {} unless server_exists?
268
284
 
269
285
  mismatches = {}
270
- local_fields.each do |name, local_type|
286
+ @model_class.field_map.each do |name, wire|
271
287
  next if core_field?(name)
272
- name_str = name.to_s.camelize(:lower)
273
- server_type = @server_schema.field_type(name_str)
288
+ local_type = @model_class.fields[name]
289
+ next if local_type.nil?
290
+ server_type = @server_schema.field_type(wire.to_s)
274
291
  next unless server_type
275
292
 
276
293
  # Normalize types for comparison
@@ -285,11 +302,30 @@ module Parse
285
302
  end
286
303
 
287
304
  # Check if schemas are in sync.
305
+ #
306
+ # Strict / bidirectional: requires the local and server schemas to match
307
+ # in BOTH directions — no fields missing on the server, no fields missing
308
+ # locally, and no type mismatches. A server that is a strict superset of
309
+ # the local model is NOT "in sync" by this measure (use
310
+ # {#server_covers_local?} for the one-way local ⊆ server check).
288
311
  # @return [Boolean]
289
312
  def in_sync?
290
313
  missing_on_server.empty? && missing_locally.empty? && type_mismatches.empty?
291
314
  end
292
315
 
316
+ # Check whether the server schema covers every locally-defined field.
317
+ #
318
+ # One-way (local ⊆ server): true when nothing the model declares is
319
+ # missing on the server and there are no type mismatches. Unlike
320
+ # {#in_sync?}, this ignores server-only columns, so a server that is a
321
+ # strict superset of the local model still satisfies it. This is the
322
+ # predicate that determines whether a migration has any work to do —
323
+ # extra server columns are not something the migrator would add.
324
+ # @return [Boolean]
325
+ def server_covers_local?
326
+ missing_on_server.empty? && type_mismatches.empty?
327
+ end
328
+
293
329
  # Generate a human-readable summary.
294
330
  # @return [String]
295
331
  def summary
@@ -355,9 +391,17 @@ module Parse
355
391
  end
356
392
 
357
393
  # Check if migration is needed.
394
+ #
395
+ # A migration is needed when the class does not yet exist on the server,
396
+ # or when the server does not already cover every locally-defined field.
397
+ # Defined in terms of the one-way {SchemaDiff#server_covers_local?} rather
398
+ # than the strict bidirectional {SchemaDiff#in_sync?} so that a server
399
+ # which is a strict superset of the local model (extra server-only
400
+ # columns the migrator would never add) does not report a "needed"
401
+ # migration with zero operations.
358
402
  # @return [Boolean]
359
403
  def needed?
360
- !@diff.in_sync? || !@diff.server_exists?
404
+ !@diff.server_exists? || !@diff.server_covers_local?
361
405
  end
362
406
 
363
407
  # Get the operations that would be performed.
@@ -372,7 +416,7 @@ module Parse
372
416
  @diff.missing_on_server.each do |name, type|
373
417
  ops << {
374
418
  action: :add_field,
375
- field: name.to_s.camelize(:lower),
419
+ field: @model_class.field_map[name].to_s,
376
420
  type: REVERSE_TYPE_MAP[type] || "String",
377
421
  }
378
422
  end
@@ -424,7 +468,7 @@ module Parse
424
468
 
425
469
  # Add missing fields
426
470
  @diff.missing_on_server.each do |name, type|
427
- field_name = name.to_s.camelize(:lower)
471
+ field_name = @model_class.field_map[name].to_s
428
472
  field_schema = { "fields" => { field_name => field_definition(type) } }
429
473
 
430
474
  response = @client.update_schema(@model_class.parse_class, field_schema)
@@ -444,15 +488,20 @@ module Parse
444
488
 
445
489
  def build_schema
446
490
  fields = {}
447
- @model_class.fields.each do |name, type|
491
+ # Iterate `field_map` (canonical name => wire column) rather than
492
+ # `fields`, which carries both the snake_case and camelCase keys for
493
+ # every property and would emit a duplicate/phantom column for each
494
+ # multi-word or custom-`field:` property.
495
+ @model_class.field_map.each do |name, wire|
448
496
  next if %i[id object_id created_at updated_at acl objectId createdAt updatedAt ACL].include?(name)
449
- field_name = name.to_s.camelize(:lower)
450
- fields[field_name] = field_definition(type)
497
+ fields[wire.to_s] = field_definition(@model_class.fields[name])
451
498
  end
452
499
 
453
- # Add pointer targets
500
+ # Add pointer targets. `references` is keyed by the wire column name
501
+ # (the `parse_field`), so use it as-is — do not re-camelize, which
502
+ # would corrupt custom `field:` pointer columns.
454
503
  @model_class.references.each do |name, target_class|
455
- field_name = name.to_s.camelize(:lower)
504
+ field_name = name.to_s
456
505
  fields[field_name] = {
457
506
  "type" => "Pointer",
458
507
  "targetClass" => target_class.to_s,
@@ -2,10 +2,10 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Parse
5
- # @author Anthony Persaud, Henry Spindell, Adrian Curtin
5
+ # @author Adrian Curtin, Anthony Persaud, Henry Spindell
6
6
  # The Parse Server SDK for Ruby
7
7
  module Stack
8
8
  # The current version.
9
- VERSION = "5.1.1"
9
+ VERSION = "5.2.0"
10
10
  end
11
11
  end
@@ -6,7 +6,7 @@ require "parse/stack/version"
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "parse-stack-next"
8
8
  spec.version = Parse::Stack::VERSION
9
- spec.authors = ["Anthony Persaud", "Henry Spindell", "Adrian Curtin"]
9
+ spec.authors = ["Adrian Curtin", "Anthony Persaud", "Henry Spindell"]
10
10
  spec.email = ["adrian+parse-stack@neurosynq.net"]
11
11
 
12
12
  spec.summary = %q{Parse Server SDK for Ruby — ORM, queries, auth, and MongoDB-direct access}
@@ -8,7 +8,7 @@
8
8
  # Reset: docker-compose -f scripts/docker/docker-compose.atlas.yml down -v && docker-compose -f scripts/docker/docker-compose.atlas.yml up -d
9
9
  #
10
10
  # Run tests:
11
- # ATLAS_URI="mongodb://localhost:27020/parse_atlas_test?directConnection=true" ruby -Ilib:test test/lib/parse/atlas_search_integration_test.rb
11
+ # ATLAS_URI="mongodb://localhost:29020/parse_atlas_test?directConnection=true" ruby -Ilib:test test/lib/parse/atlas_search_integration_test.rb
12
12
  #
13
13
  # Stability notes (see commit history for context):
14
14
  # The mongodb-atlas-local image runs a supervisor ("runner") that intentionally
@@ -19,13 +19,17 @@
19
19
  # which checks both mongod AND mongot. We rely on that built-in healthcheck
20
20
  # rather than overriding it with a faster mongosh probe.
21
21
 
22
+ # Distinct Compose project name — kept separate from the main test stack
23
+ # (psnext-it) so `down` on one never tears down the other.
24
+ name: ${PSNEXT_PREFIX:-psnext-it}-atlas
25
+
22
26
  services:
23
27
  atlas-local:
24
28
  # Pinned patch tag rather than the floating :8.0 — mongodb-atlas-local has
25
29
  # known per-patch start-up flakiness (see testcontainers/testcontainers-java#10267
26
30
  # and MongoDB community forum 321179). Bump deliberately, not by drift.
27
31
  image: mongodb/mongodb-atlas-local:8.0.10
28
- container_name: parse-stack-atlas-local
32
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-atlas
29
33
  # `hostname` is called out by the official compose example — the embedded
30
34
  # single-node replica set advertises this name, and a stable value makes
31
35
  # restart-after-down work cleanly.
@@ -37,15 +41,15 @@ services:
37
41
  MONGOT_LOG_FILE: /dev/stderr
38
42
  # Loopback-only by default — Atlas Local has no auth configured.
39
43
  ports:
40
- - "${ATLAS_BIND:-127.0.0.1}:27020:27017"
44
+ - "${ATLAS_BIND:-127.0.0.1}:${ATLAS_HOST_PORT:-29020}:27017"
41
45
  volumes:
42
46
  # Persist mongod data, replset config, and — critically — the mongot
43
47
  # Lucene index directory. Without /data/mongot persistence, every restart
44
48
  # forces a full search re-index, which lengthens "ready" time and is a
45
49
  # real source of test flakiness. Matches the official compose example.
46
- - atlas-data:/data/db
47
- - atlas-configdb:/data/configdb
48
- - atlas-mongot:/data/mongot
50
+ - psnext-it-atlas-data:/data/db
51
+ - psnext-it-atlas-configdb:/data/configdb
52
+ - psnext-it-atlas-mongot:/data/mongot
49
53
  # Recover from genuine crashes automatically. Test runs `docker compose down`
50
54
  # explicitly, so this does not interfere with teardown.
51
55
  restart: unless-stopped
@@ -59,7 +63,7 @@ services:
59
63
 
60
64
  atlas-init:
61
65
  image: mongodb/mongodb-atlas-local:8.0.10
62
- container_name: parse-stack-atlas-init
66
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-atlas-init
63
67
  depends_on:
64
68
  atlas-local:
65
69
  # Uses the image's built-in healthcheck (see above), so this now waits
@@ -71,6 +75,6 @@ services:
71
75
  restart: "no"
72
76
 
73
77
  volumes:
74
- atlas-data:
75
- atlas-configdb:
76
- atlas-mongot:
78
+ psnext-it-atlas-data:
79
+ psnext-it-atlas-configdb:
80
+ psnext-it-atlas-mongot:
@@ -1,31 +1,35 @@
1
1
  version: '3.8'
2
2
 
3
+ # Distinct Compose project name so this stack never shares containers,
4
+ # networks, or volumes with another Parse test system on the same host.
5
+ name: ${PSNEXT_PREFIX:-psnext-it}
6
+
3
7
  services:
4
8
  mongo:
5
9
  image: mongo:8
6
- container_name: parse-stack-test-mongo
10
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-mongo
7
11
  # Bind to loopback so the test database isn't reachable from the LAN
8
12
  # when a developer runs `docker-compose up`. Override with
9
13
  # `MONGO_BIND=0.0.0.0` if you really want it exposed.
10
14
  ports:
11
- - "${MONGO_BIND:-127.0.0.1}:27019:27017"
15
+ - "${MONGO_BIND:-127.0.0.1}:${MONGO_HOST_PORT:-29017}:27017"
12
16
  environment:
13
17
  MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-admin}
14
18
  MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-password}
15
19
  MONGO_INITDB_DATABASE: admin
16
20
  volumes:
17
- - mongo-data:/data/db
21
+ - psnext-it-mongo-data:/data/db
18
22
  - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
19
23
 
20
24
  parse:
21
25
  build:
22
26
  context: ..
23
27
  dockerfile: docker/Dockerfile.parse
24
- container_name: parse-stack-test-server
28
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-server
25
29
  # Bind to loopback by default — Parse Server with the test master key
26
30
  # has no business being reachable on a developer's LAN.
27
31
  ports:
28
- - "${PARSE_BIND:-127.0.0.1}:2337:1337"
32
+ - "${PARSE_BIND:-127.0.0.1}:${PARSE_HOST_PORT:-29337}:1337"
29
33
  # `host.docker.internal` is needed so Parse Server can POST to the
30
34
  # in-process WEBrick that backs webhook integration tests. Docker
31
35
  # Desktop on Mac/Windows resolves this natively; on Linux we map it
@@ -45,10 +49,10 @@ services:
45
49
  # `start-parse.sh` will abort if any of these is unset, so any
46
50
  # name drift here surfaces immediately rather than booting Parse
47
51
  # Server with whatever placeholder default the SDK README documents.
48
- PARSE_SERVER_APPLICATION_ID: ${PARSE_APP_ID:-myAppId}
49
- PARSE_SERVER_MASTER_KEY: ${PARSE_MASTER_KEY:-myMasterKey}
50
- PARSE_SERVER_REST_API_KEY: ${PARSE_API_KEY:-test-rest-key}
51
- PARSE_SERVER_DATABASE_URI: mongodb://${MONGO_ROOT_USER:-admin}:${MONGO_ROOT_PASSWORD:-password}@mongo:27017/parse?authSource=admin
52
+ PARSE_SERVER_APPLICATION_ID: ${PARSE_APP_ID:-psnextItAppId}
53
+ PARSE_SERVER_MASTER_KEY: ${PARSE_MASTER_KEY:-psnextItMasterKey}
54
+ PARSE_SERVER_REST_API_KEY: ${PARSE_API_KEY:-psnext-it-rest-key}
55
+ PARSE_SERVER_DATABASE_URI: mongodb://${MONGO_ROOT_USER:-admin}:${MONGO_ROOT_PASSWORD:-password}@mongo:27017/parse_stack_next_it?authSource=admin
52
56
  # Accept client-supplied objectId on create. Required by the
53
57
  # `parse_reference precompute: true` DSL. The SDK only forwards
54
58
  # client objectIds under master-key authority; for non-master
@@ -78,36 +82,36 @@ services:
78
82
 
79
83
  redis:
80
84
  image: redis:7-alpine
81
- container_name: parse-stack-test-redis
85
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-redis
82
86
  # Loopback-only by default. Used by the cache integration test
83
87
  # (cache_redis_integration_test.rb) and the synchronize-create lock
84
88
  # tests. Override with `REDIS_BIND=0.0.0.0` if you need to point a
85
89
  # remote client at it during debugging.
86
90
  ports:
87
- - "${REDIS_BIND:-127.0.0.1}:6399:6379"
91
+ - "${REDIS_BIND:-127.0.0.1}:${REDIS_HOST_PORT:-29379}:6379"
88
92
  command: ["redis-server", "--save", "", "--appendonly", "no"]
89
93
 
90
94
  parse-dashboard:
91
95
  image: parseplatform/parse-dashboard:9
92
- container_name: parse-stack-test-dashboard
96
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-dashboard
93
97
  # Loopback-only by default. This compose is for `rake test:integration`
94
98
  # and local debugging; do not expose to the network.
95
99
  ports:
96
- - "${DASHBOARD_BIND:-127.0.0.1}:4040:4040"
100
+ - "${DASHBOARD_BIND:-127.0.0.1}:${DASHBOARD_HOST_PORT:-29040}:4040"
97
101
  environment:
98
102
  # `allowInsecureHTTP` and `useEncryptedPasswords: false` are kept
99
103
  # because this stack runs over plain HTTP on loopback. Do NOT copy
100
104
  # these settings to a deployed environment.
101
105
  PARSE_DASHBOARD_ALLOW_INSECURE_HTTP: "1"
102
- PARSE_SERVER_MASTER_KEY: ${PARSE_MASTER_KEY:-myMasterKey}
103
- PARSE_SERVER_APPLICATION_ID: ${PARSE_APP_ID:-myAppId}
104
- PARSE_SERVER_URL: http://localhost:2337/parse
106
+ PARSE_SERVER_MASTER_KEY: ${PARSE_MASTER_KEY:-psnextItMasterKey}
107
+ PARSE_SERVER_APPLICATION_ID: ${PARSE_APP_ID:-psnextItAppId}
108
+ PARSE_SERVER_URL: http://localhost:${PARSE_HOST_PORT:-29337}/parse
105
109
  PARSE_DASHBOARD_CONFIG: |
106
110
  {
107
111
  "apps": [{
108
- "serverURL": "http://localhost:2337/parse",
109
- "appId": "${PARSE_APP_ID:-myAppId}",
110
- "masterKey": "${PARSE_MASTER_KEY:-myMasterKey}",
112
+ "serverURL": "http://localhost:${PARSE_HOST_PORT:-29337}/parse",
113
+ "appId": "${PARSE_APP_ID:-psnextItAppId}",
114
+ "masterKey": "${PARSE_MASTER_KEY:-psnextItMasterKey}",
111
115
  "appName": "ParseStackTest"
112
116
  }],
113
117
  "users": [{
@@ -121,4 +125,4 @@ services:
121
125
  - parse
122
126
 
123
127
  volumes:
124
- mongo-data:
128
+ psnext-it-mongo-data:
@@ -1,6 +1,6 @@
1
1
  // MongoDB initialization script
2
2
  // This script runs when the MongoDB container is first created
3
- // It grants the admin user access to the parse database for direct queries
3
+ // It grants the admin user access to the parse_stack_next_it database for direct queries
4
4
 
5
5
  // The admin user needs readWriteAnyDatabase role to access all databases
6
6
  db = db.getSiblingDB('admin');
@@ -11,8 +11,8 @@ db.grantRolesToUser('admin', [
11
11
  { role: 'dbAdminAnyDatabase', db: 'admin' }
12
12
  ]);
13
13
 
14
- // Initialize the parse database
15
- db = db.getSiblingDB('parse');
14
+ // Initialize the parse_stack_next_it database
15
+ db = db.getSiblingDB('parse_stack_next_it');
16
16
 
17
17
  // Create a placeholder collection to ensure the database exists
18
18
  db.createCollection('_init');