parse-stack-next 5.0.1 → 5.1.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
  4. data/.github/dependabot.yml +13 -0
  5. data/.github/workflows/codeql.yml +1 -1
  6. data/.github/workflows/docs.yml +3 -3
  7. data/.github/workflows/release.yml +14 -3
  8. data/.github/workflows/ruby.yml +1 -1
  9. data/.gitignore +1 -0
  10. data/.yardopts +19 -0
  11. data/CHANGELOG.md +792 -0
  12. data/Gemfile +3 -0
  13. data/Gemfile.lock +8 -5
  14. data/README.md +15 -0
  15. data/Rakefile +5 -1
  16. data/docs/acl_clp_guide.md +553 -0
  17. data/docs/atlas_vector_search_guide.md +123 -22
  18. data/docs/client_sdk_guide.md +201 -5
  19. data/docs/usage_guide.md +21 -0
  20. data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
  21. data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
  22. data/lib/parse/agent/tools.rb +153 -1
  23. data/lib/parse/cache/redis.rb +53 -0
  24. data/lib/parse/client/caching.rb +18 -1
  25. data/lib/parse/client.rb +79 -12
  26. data/lib/parse/embeddings/cohere.rb +143 -6
  27. data/lib/parse/embeddings/provider.rb +20 -2
  28. data/lib/parse/embeddings/voyage.rb +102 -0
  29. data/lib/parse/embeddings.rb +332 -1
  30. data/lib/parse/live_query/client.rb +167 -4
  31. data/lib/parse/live_query/configuration.rb +12 -0
  32. data/lib/parse/live_query/subscription.rb +55 -2
  33. data/lib/parse/live_query.rb +123 -1
  34. data/lib/parse/lock.rb +342 -0
  35. data/lib/parse/lock_backend.rb +308 -0
  36. data/lib/parse/model/classes/audience.rb +5 -0
  37. data/lib/parse/model/classes/installation.rb +122 -0
  38. data/lib/parse/model/classes/job_schedule.rb +3 -1
  39. data/lib/parse/model/classes/job_status.rb +4 -1
  40. data/lib/parse/model/classes/push_status.rb +4 -1
  41. data/lib/parse/model/classes/session.rb +7 -0
  42. data/lib/parse/model/classes/user.rb +204 -0
  43. data/lib/parse/model/core/create_lock.rb +28 -146
  44. data/lib/parse/model/core/embed_managed.rb +162 -13
  45. data/lib/parse/model/core/parse_reference.rb +17 -1
  46. data/lib/parse/model/core/querying.rb +26 -2
  47. data/lib/parse/model/file.rb +523 -18
  48. data/lib/parse/query.rb +31 -1
  49. data/lib/parse/stack/version.rb +1 -1
  50. data/lib/parse/stack.rb +98 -1
  51. data/parse-stack-next.gemspec +2 -2
  52. metadata +17 -7
@@ -0,0 +1,387 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif;
4
+ font-size: 13px;
5
+ height: 101%;
6
+ overflow-x: hidden;
7
+ background: #fafafa;
8
+ }
9
+
10
+ h1 {
11
+ padding: 12px 10px;
12
+ padding-bottom: 0;
13
+ margin: 0;
14
+ font-size: 1.4em;
15
+ }
16
+ .clear {
17
+ clear: both;
18
+ }
19
+ .fixed_header {
20
+ position: fixed;
21
+ background: #fff;
22
+ width: 100%;
23
+ padding-bottom: 10px;
24
+ margin-top: 0;
25
+ top: 0;
26
+ z-index: 9999;
27
+ height: 70px;
28
+ }
29
+ #search {
30
+ position: absolute;
31
+ right: 5px;
32
+ top: 9px;
33
+ padding-left: 24px;
34
+ }
35
+ #noresults {
36
+ padding: 7px 12px;
37
+ background: #fff;
38
+ }
39
+ #content.insearch #search,
40
+ #content.insearch #noresults {
41
+ background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==)
42
+ no-repeat center left;
43
+ }
44
+ #full_list {
45
+ padding: 0;
46
+ list-style: none;
47
+ margin-left: 0;
48
+ margin-top: 80px;
49
+ font-size: 1.1em;
50
+ }
51
+ #full_list ul {
52
+ padding: 0;
53
+ }
54
+ #full_list li {
55
+ padding: 0;
56
+ margin: 0;
57
+ list-style: none;
58
+ }
59
+ #full_list li .item {
60
+ padding: 5px 5px 5px 12px;
61
+ }
62
+ #content.insearch #noresults {
63
+ margin-left: 7px;
64
+ }
65
+ #full_list li {
66
+ color: #666;
67
+ cursor: normal;
68
+ white-space: nowrap;
69
+ }
70
+ #full_list li.collapsed ul {
71
+ display: none;
72
+ }
73
+ #full_list li a.toggle {
74
+ cursor: default;
75
+ position: relative;
76
+ left: -5px;
77
+ top: 4px;
78
+ text-indent: -999px;
79
+ width: 10px;
80
+ height: 9px;
81
+ margin-left: -10px;
82
+ display: block;
83
+ float: left;
84
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC)
85
+ no-repeat bottom left;
86
+ }
87
+ #full_list li.collapsed a.toggle {
88
+ cursor: default;
89
+ background-position: top left;
90
+ }
91
+ #full_list li.deprecated {
92
+ text-decoration: line-through;
93
+ font-style: italic;
94
+ }
95
+ #full_list li.odd {
96
+ background: #f0f0f0;
97
+ }
98
+ #full_list li.even {
99
+ background: #fafafa;
100
+ }
101
+ #full_list .item:hover {
102
+ background: #ddd;
103
+ }
104
+ #full_list li small:before {
105
+ content: "(";
106
+ }
107
+ #full_list li small:after {
108
+ content: ")";
109
+ }
110
+ #full_list li small.search_info {
111
+ display: none;
112
+ }
113
+ a,
114
+ a:visited {
115
+ text-decoration: none;
116
+ color: #05a;
117
+ }
118
+ #full_list li.clicked > .item {
119
+ background: #05a;
120
+ color: #ccc;
121
+ }
122
+ #full_list li.clicked > .item a,
123
+ #full_list li.clicked > .item a:visited {
124
+ color: #eee;
125
+ }
126
+ #full_list li.clicked > .item a.toggle {
127
+ opacity: 0.5;
128
+ background-position: bottom right;
129
+ }
130
+ #full_list li.collapsed.clicked a.toggle {
131
+ background-position: top right;
132
+ }
133
+ #search input {
134
+ border: 1px solid #bbb;
135
+ border-radius: 3px;
136
+ }
137
+ #full_list_nav {
138
+ margin-left: 10px;
139
+ font-size: 0.9em;
140
+ display: block;
141
+ color: #aaa;
142
+ }
143
+ #full_list_nav a,
144
+ #nav a:visited {
145
+ color: #358;
146
+ }
147
+ #full_list_nav a:hover {
148
+ background: transparent;
149
+ color: #5af;
150
+ }
151
+ #full_list_nav span:after {
152
+ content: " | ";
153
+ }
154
+ #full_list_nav span:last-child:after {
155
+ content: "";
156
+ }
157
+
158
+ #content h1 {
159
+ margin-top: 0;
160
+ }
161
+ #full_list li small {
162
+ display: block;
163
+ font-size: 0.8em;
164
+ }
165
+ #full_list li small:before {
166
+ content: "";
167
+ }
168
+ #full_list li small:after {
169
+ content: "";
170
+ }
171
+ #full_list li small.search_info {
172
+ display: none;
173
+ }
174
+ #search {
175
+ width: 170px;
176
+ position: static;
177
+ margin: 3px;
178
+ margin-left: 10px;
179
+ font-size: 0.9em;
180
+ color: #666;
181
+ padding-left: 0;
182
+ padding-right: 24px;
183
+ }
184
+ #content.insearch #search {
185
+ background-position: center right;
186
+ }
187
+ #search input {
188
+ width: 110px;
189
+ }
190
+
191
+ #full_list.insearch ul {
192
+ display: block;
193
+ }
194
+ #full_list.insearch .item {
195
+ display: none;
196
+ }
197
+ #full_list.insearch .found {
198
+ display: block;
199
+ padding-left: 11px;
200
+ }
201
+ #full_list.insearch li a.toggle {
202
+ display: none;
203
+ }
204
+ #full_list.insearch li small.search_info {
205
+ display: block;
206
+ }
207
+
208
+ /* ============================================================
209
+ * parse-stack-next overlay (sidebar / class-list iframe)
210
+ * ============================================================ */
211
+
212
+ :root {
213
+ --ps-bg: #1E1F22;
214
+ --ps-bg-soft: #2A2C30;
215
+ --ps-bg-hover: #34373C;
216
+ --ps-fg: #E6E8EB;
217
+ --ps-fg-muted: #9CA3AF;
218
+ --ps-accent: #E63946;
219
+ --ps-link: #4FB8F4;
220
+ --ps-border: #3A3D42;
221
+ --ps-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter",
222
+ Roboto, "Helvetica Neue", Arial, sans-serif;
223
+ }
224
+
225
+ body {
226
+ background: var(--ps-bg) !important;
227
+ color: var(--ps-fg);
228
+ font-family: var(--ps-font);
229
+ font-size: 13px;
230
+ }
231
+
232
+ h1 {
233
+ color: var(--ps-fg);
234
+ font-weight: 650;
235
+ letter-spacing: -0.01em;
236
+ }
237
+
238
+ .fixed_header {
239
+ background: var(--ps-bg-soft) !important;
240
+ border-bottom: 1px solid var(--ps-border);
241
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
242
+ }
243
+
244
+ #search input,
245
+ #full_list_search input {
246
+ background: var(--ps-bg) !important;
247
+ color: var(--ps-fg) !important;
248
+ border: 1px solid var(--ps-border) !important;
249
+ border-radius: 4px;
250
+ padding: 4px 8px;
251
+ font-family: var(--ps-font);
252
+ }
253
+
254
+ #search input::placeholder { color: var(--ps-fg-muted); }
255
+
256
+ #full_list li,
257
+ #full_list li .item {
258
+ color: var(--ps-fg);
259
+ border: 0;
260
+ }
261
+
262
+ #full_list li.odd { background: transparent; }
263
+ #full_list li.even { background: rgba(255, 255, 255, .025); }
264
+
265
+ #full_list li a,
266
+ #full_list li .item a,
267
+ #full_list li a:visited,
268
+ #full_list li .item a:visited {
269
+ color: var(--ps-fg);
270
+ }
271
+
272
+ #full_list .item:hover,
273
+ #full_list li:hover,
274
+ #full_list li:hover .item {
275
+ background: var(--ps-bg-hover) !important;
276
+ color: var(--ps-fg);
277
+ }
278
+
279
+ #full_list li.clicked > .item,
280
+ #full_list li.clicked > .item a,
281
+ #full_list li.clicked > .item a:visited {
282
+ background: var(--ps-accent) !important;
283
+ color: #fff !important;
284
+ border-color: var(--ps-accent) !important;
285
+ }
286
+
287
+ #full_list li small { color: var(--ps-fg-muted); }
288
+ #full_list_nav { color: var(--ps-fg-muted); }
289
+
290
+ #full_list_nav a,
291
+ #full_list_nav a:visited { color: var(--ps-link); }
292
+ #full_list_nav a:hover { color: var(--ps-accent); }
293
+
294
+ #noresults {
295
+ background: var(--ps-bg-soft);
296
+ color: var(--ps-fg-muted);
297
+ border-radius: 4px;
298
+ }
299
+
300
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
301
+ ::-webkit-scrollbar-track { background: var(--ps-bg); }
302
+ ::-webkit-scrollbar-thumb { background: #4a4d52; border-radius: 4px; }
303
+ ::-webkit-scrollbar-thumb:hover { background: var(--ps-link); }
304
+
305
+ /* ============================================================
306
+ * Re-pin iframe-only layout values that common.css would otherwise
307
+ * stomp on.
308
+ *
309
+ * Both stylesheets are loaded inside the class-list iframe AND the
310
+ * main page. common.css generously styles tags like h1, #content,
311
+ * #header for the main page, where their layout role is "big page
312
+ * heading" / "main content column" / "top nav". Inside the iframe
313
+ * those same selectors mean "Class List" / "sidebar viewport" /
314
+ * "fixed top strip", and the sizes need to be drastically smaller.
315
+ * Re-pin them explicitly here so the iframe doesn't inherit the
316
+ * main-page proportions.
317
+ * ============================================================ */
318
+
319
+ #full_list_header,
320
+ body > #content > .fixed_header > h1,
321
+ .fixed_header h1 {
322
+ font-size: 1.4em !important;
323
+ font-weight: 650;
324
+ padding: 12px 10px 4px !important;
325
+ margin: 0 !important;
326
+ border: 0 !important;
327
+ display: block !important;
328
+ color: var(--ps-fg);
329
+ letter-spacing: -0.01em;
330
+ line-height: 1.2;
331
+ }
332
+
333
+ /* Reserve clearance for the fixed_header. With the larger header
334
+ * font + the flex search row, the stock 80px margin isn't enough. */
335
+ body > #content > #full_list {
336
+ margin-top: 120px !important;
337
+ }
338
+
339
+ /* The iframe's #content is just a passive scroll container, not the
340
+ * main-page article column. Wipe any main-page color/padding rules. */
341
+ body > #content {
342
+ background: var(--ps-bg) !important;
343
+ color: var(--ps-fg);
344
+ padding: 0;
345
+ margin: 0;
346
+ }
347
+
348
+ /* Top nav strip: "Classes | Methods | Files" links. */
349
+ #full_list_nav {
350
+ padding: 0 10px;
351
+ font-size: 0.85em;
352
+ color: var(--ps-fg-muted);
353
+ }
354
+
355
+ /* Search row inside the fixed header — keep it on one line. */
356
+ .fixed_header #search {
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 6px;
360
+ padding: 4px 10px 0;
361
+ width: auto;
362
+ }
363
+
364
+ .fixed_header #search label {
365
+ color: var(--ps-fg-muted);
366
+ font-size: 0.85em;
367
+ }
368
+
369
+ .fixed_header #search input {
370
+ flex: 1 1 auto;
371
+ min-width: 0;
372
+ }
373
+
374
+ /* Belt-and-braces: the fixed strip must be opaque and above the list. */
375
+ .fixed_header {
376
+ background: var(--ps-bg-soft) !important;
377
+ z-index: 9999;
378
+ height: auto;
379
+ min-height: 70px;
380
+ padding-bottom: 8px;
381
+ }
382
+
383
+ /* The collapse toggle PNG is a dark-on-transparent sprite; invert it
384
+ * so the arrow shows on dark backgrounds. */
385
+ #full_list li a.toggle {
386
+ filter: invert(0.85) hue-rotate(180deg) brightness(1.6);
387
+ }
@@ -469,6 +469,35 @@ module Parse
469
469
  },
470
470
  required: ["class_name", "pipeline"],
471
471
  },
472
+ output_schema: {
473
+ type: "object",
474
+ properties: {
475
+ class_name: { type: "string" },
476
+ pipeline_stages: { type: "integer", minimum: 0 },
477
+ result_count: { type: "integer", minimum: 0 },
478
+ # `route` is :mongo_direct or :parse_server but serializes
479
+ # to a Symbol-shaped String in JSON envelopes; declare it
480
+ # permissively as string.
481
+ route: { type: "string", description: "Routing tag: 'mongo_direct' or 'parse_server'." },
482
+ # Aggregation result rows are class-shape-dependent and may
483
+ # be the output of arbitrary $project / $group / $lookup
484
+ # stages. Object envelopes with open property sets are the
485
+ # honest representation.
486
+ results: {
487
+ type: "array",
488
+ items: { type: "object", additionalProperties: true },
489
+ },
490
+ pointer_classes: {
491
+ type: "object",
492
+ additionalProperties: { type: "string" },
493
+ description: "Optional. Field-name → Parse-class-name map when compact_pointers is on.",
494
+ },
495
+ auto_limited: { type: "boolean" },
496
+ auto_limit: { type: "integer", minimum: 1 },
497
+ hint: { type: "string" },
498
+ },
499
+ required: %w[class_name pipeline_stages result_count route results],
500
+ },
472
501
  },
473
502
 
474
503
  explain_query: {
@@ -753,6 +782,25 @@ module Parse
753
782
  },
754
783
  required: ["class_name"],
755
784
  },
785
+ output_schema: {
786
+ type: "object",
787
+ properties: {
788
+ class_name: { type: "string" },
789
+ # `format` is one of csv|markdown|table; same shape as the
790
+ # input enum.
791
+ format: { type: "string", enum: %w[csv markdown table] },
792
+ headers: { type: "array", items: { type: "string" } },
793
+ row_count: { type: "integer", minimum: 0 },
794
+ # The serialized output is the formatted CSV / Markdown /
795
+ # text-table string itself — clients render it as-is.
796
+ output: { type: "string" },
797
+ truncated: { type: "boolean" },
798
+ available_rows: { type: "integer", minimum: 0 },
799
+ row_cap: { type: "integer", minimum: 1 },
800
+ hint: { type: "string" },
801
+ },
802
+ required: %w[class_name format headers row_count output],
803
+ },
756
804
  },
757
805
 
758
806
  atlas_text_search: {
@@ -799,6 +847,50 @@ module Parse
799
847
  },
800
848
  required: %w[class_name query],
801
849
  },
850
+ output_schema: {
851
+ type: "object",
852
+ properties: {
853
+ class_name: { type: "string" },
854
+ count: { type: "integer", minimum: 0 },
855
+ # Each row is a Parse object projected through the class's
856
+ # agent_fields allowlist, with an Atlas-supplied `score`
857
+ # numeric and an optional `highlights` array when the
858
+ # caller passes highlight_field:. The row shape is class-
859
+ # dependent so additionalProperties is open.
860
+ results: {
861
+ type: "array",
862
+ items: {
863
+ type: "object",
864
+ properties: {
865
+ score: { type: "number" },
866
+ highlights: {
867
+ type: "array",
868
+ items: {
869
+ type: "object",
870
+ properties: {
871
+ path: { type: "string" },
872
+ texts: {
873
+ type: "array",
874
+ items: {
875
+ type: "object",
876
+ properties: {
877
+ value: { type: "string" },
878
+ type: { type: "string", description: "'hit' or 'text' per Atlas spec." },
879
+ },
880
+ required: %w[value],
881
+ },
882
+ },
883
+ },
884
+ required: %w[path],
885
+ },
886
+ },
887
+ },
888
+ additionalProperties: true,
889
+ },
890
+ },
891
+ },
892
+ required: %w[class_name count results],
893
+ },
802
894
  },
803
895
 
804
896
  atlas_autocomplete: {
@@ -838,6 +930,25 @@ module Parse
838
930
  },
839
931
  required: %w[class_name query field],
840
932
  },
933
+ output_schema: {
934
+ type: "object",
935
+ properties: {
936
+ class_name: { type: "string" },
937
+ field: { type: "string" },
938
+ # `suggestions` is the list of distinct field values that
939
+ # matched the autocomplete query (deduped, ordered by Atlas
940
+ # ranking). Strings only — autocomplete operates on text.
941
+ suggestions: { type: "array", items: { type: "string" } },
942
+ count: { type: "integer", minimum: 0 },
943
+ # Full matching Parse objects, projected through the class
944
+ # agent_fields allowlist.
945
+ results: {
946
+ type: "array",
947
+ items: { type: "object", additionalProperties: true },
948
+ },
949
+ },
950
+ required: %w[class_name field suggestions count results],
951
+ },
841
952
  },
842
953
 
843
954
  atlas_faceted_search: {
@@ -876,6 +987,40 @@ module Parse
876
987
  },
877
988
  required: %w[class_name facets],
878
989
  },
990
+ output_schema: {
991
+ type: "object",
992
+ properties: {
993
+ class_name: { type: "string" },
994
+ # $searchMeta lower-bound count. May be approximate for very
995
+ # large corpora — Atlas documents this; downstream clients
996
+ # should treat it as informative, not a precise total.
997
+ total_count: { type: "integer", minimum: 0 },
998
+ # Facets is a Map<facet_name, { buckets: [{_id, count}] }>.
999
+ # Bucket _id is heterogeneous (String for string facets,
1000
+ # Number/Date for numeric/date facets), so additionalProperties:true
1001
+ # on the bucket entry keeps the contract honest without
1002
+ # bloating the schema with per-type variants.
1003
+ facets: {
1004
+ type: "object",
1005
+ additionalProperties: {
1006
+ type: "object",
1007
+ properties: {
1008
+ buckets: {
1009
+ type: "array",
1010
+ items: { type: "object", additionalProperties: true },
1011
+ },
1012
+ },
1013
+ required: %w[buckets],
1014
+ },
1015
+ },
1016
+ count: { type: "integer", minimum: 0 },
1017
+ results: {
1018
+ type: "array",
1019
+ items: { type: "object", additionalProperties: true },
1020
+ },
1021
+ },
1022
+ required: %w[class_name total_count facets count results],
1023
+ },
879
1024
  },
880
1025
  }.freeze
881
1026
 
@@ -3474,7 +3619,14 @@ module Parse
3474
3619
  class_name: class_name,
3475
3620
  pipeline_stages: pipeline.size,
3476
3621
  result_count: results.size,
3477
- route: use_mongo_direct ? :mongo_direct : :parse_server,
3622
+ # Coerce to String here so the value lands in
3623
+ # `structuredContent` as a String (matching the
3624
+ # advertised output_schema `type: "string"`). Without the
3625
+ # `.to_s`, MCP clients validating structuredContent see a
3626
+ # Ruby Symbol pre-serialization and fail the type check;
3627
+ # downstream JSON serialization would convert it but the
3628
+ # client-side validator runs before that.
3629
+ route: (use_mongo_direct ? :mongo_direct : :parse_server).to_s,
3478
3630
  results: results,
3479
3631
  }
3480
3632
  result[:pointer_classes] = pointer_map if pointer_map.any?
@@ -118,6 +118,59 @@ module Parse
118
118
  @pool.increment(key, amount, options)
119
119
  end
120
120
 
121
+ # Lua compare-and-delete: delete `key` only if its current value
122
+ # equals `expected`. Atomic on the Redis server (the GET, the
123
+ # compare, and the DEL are one script invocation), which closes the
124
+ # check-then-delete race in a naive GET-then-DEL release where the
125
+ # lease can expire and be re-acquired by another holder between the
126
+ # two commands.
127
+ LOCK_RELEASE_SCRIPT = <<~LUA
128
+ if redis.call('get', KEYS[1]) == ARGV[1] then
129
+ return redis.call('del', KEYS[1])
130
+ else
131
+ return 0
132
+ end
133
+ LUA
134
+
135
+ # Atomically acquire a lock: SET key=owner only if absent, with a
136
+ # native expiry. Used by {Parse::LockBackend} for {Parse::Lock} and
137
+ # {Parse::CreateLock}. Deliberately bypasses Moneta's `create` —
138
+ # `Moneta.new(:Redis)` marshals BOTH keys and values, so a raw-Redis
139
+ # compare-and-delete on the marshaled blob would be fragile and
140
+ # coupled to Moneta's serializer config. Routing acquire AND release
141
+ # through plain-string raw Redis here keeps one consistent encoding
142
+ # across both ends of the lock and makes the keys human-inspectable
143
+ # in Redis (`parse-stack:lock:v1:<digest>`). Lock keys are
144
+ # short-lived (TTL ≤ 30s) so there is no migration concern when a
145
+ # deploy flips between the Moneta-encoded and raw-encoded paths.
146
+ #
147
+ # @param key [String] plain-string lock key.
148
+ # @param owner [String] unique-per-acquisition owner token.
149
+ # @param ttl [Integer] seconds until the key self-clears.
150
+ # @return [Boolean] true when the key was set (lock acquired).
151
+ def lock_acquire(key, owner, ttl)
152
+ @pool.pool.with do |store|
153
+ redis = backend_client(store)
154
+ # redis-rb returns "OK" on success, nil when NX fails.
155
+ !!redis.set(key, owner, nx: true, ex: ttl)
156
+ end
157
+ end
158
+
159
+ # Atomically release a lock via compare-and-delete. Only the holder
160
+ # whose `owner` token still matches the stored value deletes the
161
+ # key — a holder whose lease already expired and was re-acquired by
162
+ # someone else is a no-op, never a cross-holder delete.
163
+ #
164
+ # @param key [String] plain-string lock key.
165
+ # @param owner [String] the owner token from {#lock_acquire}.
166
+ # @return [Boolean] true when this owner's key was deleted.
167
+ def lock_release(key, owner)
168
+ @pool.pool.with do |store|
169
+ redis = backend_client(store)
170
+ redis.eval(LOCK_RELEASE_SCRIPT, keys: [key], argv: [owner]).to_i == 1
171
+ end
172
+ end
173
+
121
174
  # Clear cached entries belonging to this wrapper. Required for
122
175
  # `Parse::Client#clear_cache!` compatibility.
123
176
  #
@@ -143,6 +143,19 @@ module Parse
143
143
  @cache_key = "mk:#{@cache_key}" # prefix for master key requests
144
144
  end
145
145
 
146
+ # Optional ambient cache-tenant scope from `Parse.with_cache_tenant`.
147
+ # When present, composes between the configured namespace and the
148
+ # token/mk prefix as `T:<tenant>:` so a SCAN-delete over
149
+ # `<namespace>:T:<tenant>:*` evicts exactly one tenant, and
150
+ # `<namespace>:*` still evicts the whole namespace cleanly. The
151
+ # `T:` discriminator makes tenant prefixes unambiguously
152
+ # distinguishable from session-token hex prefixes (32-char hex)
153
+ # and from `mk:`, so legacy cache entries written before the
154
+ # tenant feature don't accidentally re-hydrate into a tenanted
155
+ # request and vice versa.
156
+ @cache_tenant = Parse.respond_to?(:current_cache_tenant) ? Parse.current_cache_tenant : nil
157
+ @cache_key = "T:#{@cache_tenant}:#{@cache_key}" if @cache_tenant
158
+
146
159
  # Namespace outermost so a SCAN over `<namespace>:*` evicts a whole
147
160
  # tenant/app cleanly without touching another app's entries.
148
161
  @cache_key = "#{@namespace}:#{@cache_key}" if @namespace
@@ -277,7 +290,11 @@ module Parse
277
290
  # @!visibility private
278
291
  def instrument_cache(event, **extra)
279
292
  return unless defined?(ActiveSupport::Notifications)
280
- payload = { event: event, namespace: @namespace }.merge!(extra)
293
+ payload = {
294
+ event: event,
295
+ namespace: @namespace,
296
+ cache_tenant: @cache_tenant,
297
+ }.merge!(extra)
281
298
  ActiveSupport::Notifications.instrument("parse.cache.#{event}", payload)
282
299
  end
283
300