parse-stack-next 5.7.1 → 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +25 -4
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +0 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +79 -5
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/webhooks.rb +0 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b773e88a55f8771a7d898978652e9be673ec496627fcbaa0b0eb68b950486812
|
|
4
|
+
data.tar.gz: 7813d865cb6cc09b6c44070186dbf67383304d9407a5de58fdbb723cce4dc081
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ffa5266044c369c673c8c5e4e57d4b37d882674eae6d8f7c79f3b1a8318a75dbd76d97036337d8a5416cfdcc022c1db3353e20de53857eaf8e37ac29913dfa7
|
|
7
|
+
data.tar.gz: e897bb63c7030751cbb2a9720e6fde91e23f6c118a6656e9063a84df4ec7305db1e3f8dd141e600f2e9765fb3660fdd26b0c36c2c321184c9743c6be976498c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.7.2
|
|
4
|
+
|
|
5
|
+
#### `between` accepts Ruby Range values
|
|
6
|
+
|
|
7
|
+
- **NEW**: The `between` constraint now accepts a Ruby `Range` in addition to
|
|
8
|
+
a 2-element array, so `Person.where(:age.between => 5..25)` and
|
|
9
|
+
`Record.where(:date.between => 5.days.ago...2.days.ago)` work directly. An
|
|
10
|
+
inclusive range (`..`) maps its upper bound to `$lte`, matching the existing
|
|
11
|
+
array form, while an exclusive range (`...`) maps it to `$lt` instead.
|
|
12
|
+
Beginless (`..25`) and endless (`5..`) ranges are also supported and
|
|
13
|
+
constrain only the side that is present, so `Person.where(:age.between =>
|
|
14
|
+
18..)` compiles to `{"$gte" => 18}` with no upper bound. The array form is
|
|
15
|
+
unchanged, and both forms produce identical output for the same bounds.
|
|
16
|
+
|
|
17
|
+
#### `Query#where_not_between` for the negated form of a range
|
|
18
|
+
|
|
19
|
+
- **NEW**: `Query#where_not_between(field, value)` adds the logical negation
|
|
20
|
+
of `between`: `Person.query.where_not_between(:age, 5..25)` compiles to
|
|
21
|
+
`age < 5 OR age > 25`, accepting the same Range and 2-element Array forms
|
|
22
|
+
as `between` (exclusive ranges flip the upper side to `$gte`, and a
|
|
23
|
+
beginless or endless Range negates to a single one-sided comparison with
|
|
24
|
+
no `$or` needed). It is not available as a `field.not_between => value`
|
|
25
|
+
symbol constraint: a range's negation is inherently an `$or` of two
|
|
26
|
+
comparisons, and only one `$or` group can be safely merged into a
|
|
27
|
+
compiled query, so a symbol constraint that unilaterally emitted one
|
|
28
|
+
could silently collide with an existing `$or` from `or_where`/`|`.
|
|
29
|
+
`where_not_between` instead composes the negation the way
|
|
30
|
+
`Parse::Query.and` already does, so it correctly nests inside a query's
|
|
31
|
+
other `.where` conditions instead of replacing them, and raises
|
|
32
|
+
`ArgumentError` if the query already has an `$or` group rather than
|
|
33
|
+
silently dropping part of it.
|
|
34
|
+
|
|
35
|
+
#### `embed_image` forwards a presigned URL when the source file has one
|
|
36
|
+
|
|
37
|
+
- **FIXED**: `embed_image` always sent the source file's bare `file.url` to
|
|
38
|
+
the embedding provider (or to the SDK's own `:bytes`-mode downloader). On a
|
|
39
|
+
private-bucket file adapter (S3/GCS configured with `presignedUrl: true`),
|
|
40
|
+
`file.url` is the canonical URL with its signature stripped, so the
|
|
41
|
+
provider's fetch (or the SDK's download) got a 403 instead of the image.
|
|
42
|
+
`Parse::File` already captures the signed variant in `file.presigned_url`
|
|
43
|
+
whenever Parse Server returns one, but `embed_image` never read it.
|
|
44
|
+
Recompute now forwards `file.presigned_url` when it is present and not yet
|
|
45
|
+
expired, and falls back to the bare URL otherwise, for both `source: :url`
|
|
46
|
+
and `source: :bytes`. The stored digest is still keyed on the bare
|
|
47
|
+
canonical URL, so a save that only rotates the file's signature does not
|
|
48
|
+
trigger a needless re-embed. The validity check ignores
|
|
49
|
+
`presigned_url_valid?`'s default 60-second safety buffer (meant for a
|
|
50
|
+
browser render, not an immediate server-side fetch), since on a
|
|
51
|
+
private-bucket adapter the fallback URL is not fetchable at all and would
|
|
52
|
+
otherwise 403 for the last minute of every signature's life.
|
|
53
|
+
`Parse::Embeddings::ImageFetch::FetchedImage#url` now stores the
|
|
54
|
+
query-stripped URL rather than the presigned one, since a live signature
|
|
55
|
+
has no reason to survive into that value object's `#inspect` output.
|
|
56
|
+
`source: :url` mode can now forward a presigned URL to the embedding
|
|
57
|
+
provider under the same `Parse::Embeddings.trust_provider_url_fetch`
|
|
58
|
+
consent already required to forward any URL; operators relying on private
|
|
59
|
+
buckets should confirm the provider's egress handling covers
|
|
60
|
+
credential-bearing URLs, not just public ones.
|
|
61
|
+
|
|
62
|
+
#### `Query#get` now resolves aliased `parse_class` names correctly
|
|
63
|
+
|
|
64
|
+
- **FIXED**: `Query#get` looked up the target class with a raw
|
|
65
|
+
`Object.const_get(@table)`, which only worked when the Parse class name
|
|
66
|
+
matched the Ruby constant name exactly. A model that renames its table via
|
|
67
|
+
`parse_class "SomeOtherName"` was never found by this lookup, so `get`
|
|
68
|
+
silently fell back to a generic `Parse::Object`/`Parse::Pointer` instead of
|
|
69
|
+
hydrating the declared model. `Query#get` now passes the table name through
|
|
70
|
+
to `Parse::Object.build` as a string, letting it run its own
|
|
71
|
+
`Parse::Model.find_class` resolution, which already understands
|
|
72
|
+
`parse_class` aliasing.
|
|
73
|
+
|
|
74
|
+
#### `_safe_warn` now writes through a configured logger
|
|
75
|
+
|
|
76
|
+
- **FIXED**: Internal warnings for authentication, timeout, and cloud-code
|
|
77
|
+
errors (`Parse::Client._safe_warn`) always wrote to STDERR, even when an
|
|
78
|
+
app had configured `Parse.logger = Rails.logger` (or any other logger) for
|
|
79
|
+
the rest of its Parse request/response logging. These warnings now route
|
|
80
|
+
through `Parse::Middleware::Logging.logger` when one is configured, so they
|
|
81
|
+
land in the same place as the app's other logs; STDERR remains the fallback
|
|
82
|
+
when no logger is configured, matching prior behavior. Every call site
|
|
83
|
+
raises the corresponding typed `Parse::Error` immediately after this
|
|
84
|
+
warning, so a configured logger that itself raises (a closed handle, a
|
|
85
|
+
full disk, a remote-aggregator client erroring on a socket) now falls back
|
|
86
|
+
to STDERR rather than propagating in place of the real error and masking
|
|
87
|
+
it.
|
|
88
|
+
|
|
3
89
|
### 5.7.1
|
|
4
90
|
|
|
5
91
|
#### Cache-invalidation webhooks no longer break every application hook for the same trigger
|
|
@@ -191,7 +191,7 @@ module Parse
|
|
|
191
191
|
|
|
192
192
|
result_hash = dispatch(method, params, agent, id, logger, subscription_manager)
|
|
193
193
|
{ status: result_hash[:status], body: result_hash[:body] }
|
|
194
|
-
rescue Parse::Agent::Unauthorized
|
|
194
|
+
rescue Parse::Agent::Unauthorized
|
|
195
195
|
{ status: 401, body: jsonrpc_error(body.is_a?(Hash) ? body["id"] : nil, -32001, "Unauthorized") }
|
|
196
196
|
rescue StandardError => e
|
|
197
197
|
# Do not leak the exception class name (gem fingerprinting). Server-
|
|
@@ -295,7 +295,7 @@ module Parse
|
|
|
295
295
|
else
|
|
296
296
|
{ status: 200, body: jsonrpc_envelope(id, result: result) }
|
|
297
297
|
end
|
|
298
|
-
rescue Parse::Agent::Unauthorized
|
|
298
|
+
rescue Parse::Agent::Unauthorized
|
|
299
299
|
{ status: 401, body: jsonrpc_error(id, -32001, "Unauthorized") }
|
|
300
300
|
rescue Parse::Agent::AccessDenied
|
|
301
301
|
# Class-authorization denial (agent_hidden / classes: allowlist), e.g.
|
data/lib/parse/agent.rb
CHANGED
|
@@ -2475,7 +2475,6 @@ module Parse
|
|
|
2475
2475
|
end
|
|
2476
2476
|
|
|
2477
2477
|
ActiveSupport::Notifications.instrument("parse.agent.tool_call", payload) do
|
|
2478
|
-
response = nil
|
|
2479
2478
|
# Install a fresh embedding accumulator for this tool span. The
|
|
2480
2479
|
# process-wide "parse.embeddings.embed" subscriber records each
|
|
2481
2480
|
# embed into it; the ensure below reads + restores it so the
|
data/lib/parse/client/request.rb
CHANGED
|
@@ -17,12 +17,24 @@ module Parse
|
|
|
17
17
|
# @!attribute [rw] body
|
|
18
18
|
# @return [Hash] the body of this request.
|
|
19
19
|
|
|
20
|
-
# TODO: Document opts and cache options.
|
|
21
|
-
|
|
22
20
|
# @!attribute [rw] opts
|
|
23
|
-
# @return [Hash]
|
|
21
|
+
# @return [Hash] per-request options consumed by {Parse::Client#request}
|
|
22
|
+
# when it builds the HTTP headers for this request. Recognized keys:
|
|
23
|
+
# * `:cache` — `false` sends `Cache-Control: no-cache`; `:write_only`
|
|
24
|
+
# skips the cache read but still writes the response; a `Numeric`
|
|
25
|
+
# overrides the cache expiration (seconds) for this request only.
|
|
26
|
+
# * `:use_master_key` — `false` forces the master key off for this
|
|
27
|
+
# request even if the client has one configured.
|
|
28
|
+
# * `:session_token` — a session token to authenticate this request as
|
|
29
|
+
# a specific user, bypassing the client's default auth context.
|
|
30
|
+
# * `:idempotent` — explicitly enables/disables idempotency-header
|
|
31
|
+
# generation for this request, overriding the class-level defaults.
|
|
32
|
+
# * `:request_id` — a caller-supplied idempotency key; see
|
|
33
|
+
# {.enable_idempotency!}.
|
|
24
34
|
# @!attribute [rw] cache
|
|
25
|
-
# @return [Boolean]
|
|
35
|
+
# @return [Boolean] unused by {Parse::Request} itself; retained as a
|
|
36
|
+
# plain accessor for callers that stash a cache handle or flag
|
|
37
|
+
# directly on the request object rather than through `opts[:cache]`.
|
|
26
38
|
attr_accessor :method, :path, :body, :headers, :opts, :cache
|
|
27
39
|
|
|
28
40
|
# @!visibility private
|
data/lib/parse/client.rb
CHANGED
|
@@ -505,23 +505,44 @@ module Parse
|
|
|
505
505
|
end
|
|
506
506
|
|
|
507
507
|
# @!visibility private
|
|
508
|
-
# Emit a redacted warning about a Parse::Response error
|
|
508
|
+
# Emit a redacted warning about a Parse::Response error.
|
|
509
509
|
#
|
|
510
510
|
# Routes the response error string through
|
|
511
511
|
# {Parse::Middleware::BodyBuilder.redact} to strip credentials (passwords,
|
|
512
512
|
# tokens, sessionTokens, access_tokens, authData) before logging, and
|
|
513
513
|
# truncates to {SAFE_WARN_MAX_ERROR_LENGTH} chars.
|
|
514
514
|
#
|
|
515
|
+
# Writes through {Parse::Middleware::Logging.logger} when the app has
|
|
516
|
+
# configured one (`Parse.logger = ...`), so these warnings land wherever
|
|
517
|
+
# the rest of the app's Parse request/response logging goes instead of
|
|
518
|
+
# bypassing it. Falls back to plain `warn` (STDERR) when no logger is
|
|
519
|
+
# configured, matching prior behavior. Every call site immediately
|
|
520
|
+
# raises the corresponding typed {Parse::Error} right after calling
|
|
521
|
+
# this method, so a misbehaving app-supplied logger (closed handle,
|
|
522
|
+
# full disk, a remote-aggregator client that raises on socket error)
|
|
523
|
+
# must not be allowed to propagate in its place and mask the real
|
|
524
|
+
# error — falls back to `warn` if the logger itself raises.
|
|
525
|
+
#
|
|
515
526
|
# @param tag [String] the bracketed prefix (e.g. "AuthenticationError").
|
|
516
527
|
# @param response [Parse::Response] the response carrying the error.
|
|
517
528
|
# @param name [String, nil] optional cloud-function or job name for context.
|
|
518
529
|
# @return [nil]
|
|
519
530
|
def _safe_warn(tag, response, name: nil)
|
|
520
531
|
err = Parse::Middleware::BodyBuilder.redact(response.error.to_s)[0, SAFE_WARN_MAX_ERROR_LENGTH]
|
|
521
|
-
if name
|
|
522
|
-
|
|
532
|
+
msg = if name
|
|
533
|
+
"[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})"
|
|
534
|
+
else
|
|
535
|
+
"[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})"
|
|
536
|
+
end
|
|
537
|
+
logger = Parse::Middleware::Logging.logger
|
|
538
|
+
if logger
|
|
539
|
+
begin
|
|
540
|
+
logger.warn(msg)
|
|
541
|
+
rescue StandardError
|
|
542
|
+
warn msg
|
|
543
|
+
end
|
|
523
544
|
else
|
|
524
|
-
warn
|
|
545
|
+
warn msg
|
|
525
546
|
end
|
|
526
547
|
nil
|
|
527
548
|
end
|
|
@@ -164,7 +164,15 @@ module Parse
|
|
|
164
164
|
|
|
165
165
|
mime = verify!(bytes, url: canonical)
|
|
166
166
|
bytes = strip_metadata(bytes, mime) if exif_strip
|
|
167
|
-
|
|
167
|
+
# Store the query-stripped URL, not `canonical` verbatim: when
|
|
168
|
+
# `url` is a presigned URL (a private-bucket file adapter),
|
|
169
|
+
# `canonical` carries a live signature, and FetchedImage#url is
|
|
170
|
+
# purely informational from here on (nothing re-fetches it).
|
|
171
|
+
# Keeping the signature out of the struct preserves the
|
|
172
|
+
# log-safety `#inspect` below was written for — third-party
|
|
173
|
+
# provider adapters and error reporters that capture locals
|
|
174
|
+
# would otherwise leak a valid bearer credential through it.
|
|
175
|
+
FetchedImage.new(bytes: bytes, mime_type: mime, url: Parse::File.strip_query(canonical))
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
# Verify raw bytes: sniff the magic, check the allowlist, and
|
data/lib/parse/embeddings.rb
CHANGED
|
@@ -382,6 +382,17 @@ module Parse
|
|
|
382
382
|
# `"true"`, or a non-matching String) raises
|
|
383
383
|
# {ConfirmationRequired}. Reset to `nil` to disable.
|
|
384
384
|
#
|
|
385
|
+
# For a `:file` source backed by a private-bucket adapter
|
|
386
|
+
# (S3/GCS with server-side presigning), the URL forwarded under
|
|
387
|
+
# this sentinel may be the file's presigned URL rather than its
|
|
388
|
+
# bare canonical one — a time-limited bearer credential for that
|
|
389
|
+
# object, not just a pointer to it (see {Parse::File#presigned_url}
|
|
390
|
+
# and `Parse::Core::EmbedManaged.embed_image`). Reviewing the
|
|
391
|
+
# provider's egress behavior before setting this sentinel should
|
|
392
|
+
# account for that: the provider (and anyone with access to its
|
|
393
|
+
# request logs) gains temporary read access to the object for
|
|
394
|
+
# however long the signature remains valid.
|
|
395
|
+
#
|
|
385
396
|
# @param value [String, nil] {TRUST_PROVIDER_URL_FETCH_SENTINEL} or nil.
|
|
386
397
|
# @raise [ConfirmationRequired] on any other value.
|
|
387
398
|
def trust_provider_url_fetch=(value)
|
|
@@ -395,10 +406,12 @@ module Parse
|
|
|
395
406
|
"String #{TRUST_PROVIDER_URL_FETCH_SENTINEL.inspect}. Plain `true` and " \
|
|
396
407
|
"other values are refused — forwarding image URLs to a third-party " \
|
|
397
408
|
"provider lets that provider issue an HTTP request from its own network " \
|
|
398
|
-
"with attacker-controllable host/path
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
409
|
+
"with attacker-controllable host/path, and for a private-bucket file may " \
|
|
410
|
+
"hand it a time-limited presigned URL rather than a bare pointer. Set the " \
|
|
411
|
+
"sentinel only after you have configured " \
|
|
412
|
+
"Parse::Embeddings.allowed_image_hosts AND reviewed the provider's " \
|
|
413
|
+
"documented egress behavior (DNS rebinding window, redirect policy, " \
|
|
414
|
+
"request-log retention)."
|
|
402
415
|
end
|
|
403
416
|
CONFIG_MUTEX.synchronize { @trust_provider_url_fetch = value }
|
|
404
417
|
end
|
|
@@ -687,7 +687,7 @@ module Parse
|
|
|
687
687
|
return if stored_digest == digest && target_present
|
|
688
688
|
|
|
689
689
|
provider = Parse::Embeddings.provider(directive.provider_name)
|
|
690
|
-
vectors = call_provider(provider, directive, input)
|
|
690
|
+
vectors = call_provider(provider, directive, input, record)
|
|
691
691
|
unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
|
|
692
692
|
raise Parse::Embeddings::InvalidResponseError,
|
|
693
693
|
"Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
|
|
@@ -774,16 +774,27 @@ module Parse
|
|
|
774
774
|
# provider a {Parse::Embeddings::ImageFetch::FetchedImage}; `:url`
|
|
775
775
|
# mode forwards the raw URL String (the provider validates and
|
|
776
776
|
# fetches it itself).
|
|
777
|
-
|
|
777
|
+
#
|
|
778
|
+
# `input` is the bare canonical URL used for the digest (see
|
|
779
|
+
# {.build_source_input}). It stays stable across saves, so an
|
|
780
|
+
# unsigned re-read of the same file location does not force a
|
|
781
|
+
# re-embed.
|
|
782
|
+
# The actual fetch/forward target prefers the file's presigned
|
|
783
|
+
# URL ({Parse::File#presigned_url}) when one is currently valid,
|
|
784
|
+
# since a private-bucket adapter's bare `file.url` is stripped of
|
|
785
|
+
# its signature and will not resolve for the provider or for the
|
|
786
|
+
# SDK's own `:bytes`-mode download.
|
|
787
|
+
def self.call_provider(provider, directive, input, record)
|
|
778
788
|
if directive.image?
|
|
789
|
+
fetch_url = presigned_fetch_url(record, directive, input)
|
|
779
790
|
source = if directive.bytes_mode?
|
|
780
791
|
Parse::Embeddings::ImageFetch.fetch!(
|
|
781
|
-
|
|
792
|
+
fetch_url,
|
|
782
793
|
allow_insecure: directive.allow_insecure ? true : false,
|
|
783
794
|
exif_strip: directive.exif_strip != false,
|
|
784
795
|
)
|
|
785
796
|
else
|
|
786
|
-
|
|
797
|
+
fetch_url
|
|
787
798
|
end
|
|
788
799
|
provider.embed_image([source],
|
|
789
800
|
input_type: directive.input_type,
|
|
@@ -793,6 +804,32 @@ module Parse
|
|
|
793
804
|
end
|
|
794
805
|
end
|
|
795
806
|
|
|
807
|
+
# @!visibility private
|
|
808
|
+
# Resolve the URL to actually fetch/forward for an image
|
|
809
|
+
# directive: the source file's currently-valid presigned URL if
|
|
810
|
+
# it has one, otherwise the bare canonical `fallback` (the same
|
|
811
|
+
# string used for the digest). Never used for text directives, so
|
|
812
|
+
# `directive.sources.first` is always a `:file` property here.
|
|
813
|
+
#
|
|
814
|
+
# Checks validity with a zero safety buffer rather than
|
|
815
|
+
# {Parse::File#presigned_url_valid?}'s default 60-second one. That
|
|
816
|
+
# default exists so a browser has time to render before a
|
|
817
|
+
# presigned URL goes stale; here it would instead spend the last
|
|
818
|
+
# 60 seconds of a perfectly usable presigned URL falling back to
|
|
819
|
+
# `fallback`, which on a private-bucket adapter is not fetchable
|
|
820
|
+
# at all. A fetch that starts immediately after this check has no
|
|
821
|
+
# meaningful use for that margin, and a 403 from a URL that
|
|
822
|
+
# expired mid-request is strictly better than a guaranteed 403
|
|
823
|
+
# from a URL known unfetchable in advance.
|
|
824
|
+
def self.presigned_fetch_url(record, directive, fallback)
|
|
825
|
+
file = record.public_send(directive.sources.first)
|
|
826
|
+
if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid?(buffer: 0)
|
|
827
|
+
file.presigned_url
|
|
828
|
+
else
|
|
829
|
+
fallback
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
796
833
|
# @!visibility private
|
|
797
834
|
# Concatenate source-field string values. `nil` and blank entries
|
|
798
835
|
# are skipped; remaining values are joined with a double newline.
|
|
@@ -253,7 +253,6 @@ module Parse
|
|
|
253
253
|
# same created_at date (down to the microsecond). This prevents getting the same
|
|
254
254
|
# record in the next query request.
|
|
255
255
|
exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
|
|
256
|
-
results = nil
|
|
257
256
|
cursor = next_cursor
|
|
258
257
|
end
|
|
259
258
|
end
|
|
@@ -356,7 +355,6 @@ module Parse
|
|
|
356
355
|
# Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user
|
|
357
356
|
# @return [Parse::Object] the most recently created object matching constraints.
|
|
358
357
|
def latest(constraints = {})
|
|
359
|
-
fetch_count = 1
|
|
360
358
|
if constraints.is_a?(Numeric)
|
|
361
359
|
fetch_count = constraints.to_i
|
|
362
360
|
constraints = {}
|
|
@@ -385,7 +383,6 @@ module Parse
|
|
|
385
383
|
# Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user
|
|
386
384
|
# @return [Parse::Object] the most recently updated object matching constraints.
|
|
387
385
|
def last_updated(constraints = {})
|
|
388
|
-
fetch_count = 1
|
|
389
386
|
if constraints.is_a?(Numeric)
|
|
390
387
|
fetch_count = constraints.to_i
|
|
391
388
|
constraints = {}
|
|
@@ -600,7 +597,6 @@ module Parse
|
|
|
600
597
|
parse_ids.compact!
|
|
601
598
|
# determines if the result back to the call site is an array or a single result
|
|
602
599
|
as_array = parse_ids.count > 1
|
|
603
|
-
results = []
|
|
604
600
|
|
|
605
601
|
# Default to write-only cache mode - find always gets fresh data
|
|
606
602
|
# but updates cache for future cached reads. Controlled by feature flag.
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -1867,7 +1867,6 @@ module Parse
|
|
|
1867
1867
|
# we should do a reverse lookup on who is registered for a different class type
|
|
1868
1868
|
# than their name with parse_class
|
|
1869
1869
|
klass = Parse::Model.find_class className
|
|
1870
|
-
o = nil
|
|
1871
1870
|
if klass.present?
|
|
1872
1871
|
# when creating objects from Parse JSON data, don't use dirty tracking since
|
|
1873
1872
|
# we are considering these objects as "pristine"
|
|
@@ -1561,7 +1561,6 @@ module Parse
|
|
|
1561
1561
|
|
|
1562
1562
|
# if it's a hash, then it should be {:key=>"objectId", :query=>[]}
|
|
1563
1563
|
remote_field_name = @operation.operand
|
|
1564
|
-
query = nil
|
|
1565
1564
|
if @value.is_a?(Hash)
|
|
1566
1565
|
res = @value.symbolize_keys
|
|
1567
1566
|
remote_field_name = res[:key] || remote_field_name
|
|
@@ -1613,7 +1612,6 @@ module Parse
|
|
|
1613
1612
|
|
|
1614
1613
|
# if it's a hash, then it should be {:key=>"objectId", :query=>[]}
|
|
1615
1614
|
remote_field_name = @operation.operand
|
|
1616
|
-
query = nil
|
|
1617
1615
|
if @value.is_a?(Hash)
|
|
1618
1616
|
res = @value.symbolize_keys
|
|
1619
1617
|
remote_field_name = res[:key] || remote_field_name
|
|
@@ -2263,7 +2261,6 @@ module Parse
|
|
|
2263
2261
|
# @return [Hash] the compiled constraint.
|
|
2264
2262
|
def build
|
|
2265
2263
|
remote_field_name = @operation.operand
|
|
2266
|
-
query = nil
|
|
2267
2264
|
|
|
2268
2265
|
if @value.is_a?(Hash)
|
|
2269
2266
|
res = @value.symbolize_keys
|
|
@@ -2314,7 +2311,6 @@ module Parse
|
|
|
2314
2311
|
# @return [Hash] the compiled constraint.
|
|
2315
2312
|
def build
|
|
2316
2313
|
remote_field_name = @operation.operand
|
|
2317
|
-
query = nil
|
|
2318
2314
|
|
|
2319
2315
|
if @value.is_a?(Hash)
|
|
2320
2316
|
res = @value.symbolize_keys
|
|
@@ -2516,6 +2512,19 @@ module Parse
|
|
|
2516
2512
|
# User.where(:name.between => ["Alice", "John"])
|
|
2517
2513
|
# # Generates: "name": { "$gte": "Alice", "$lte": "John" }
|
|
2518
2514
|
#
|
|
2515
|
+
# # A Ruby Range works the same way as a 2-element array. An inclusive
|
|
2516
|
+
# # range (`..`) maps its end to $lte, while an exclusive range (`...`)
|
|
2517
|
+
# # maps its end to $lt.
|
|
2518
|
+
# User.where(:age.between => 18..65)
|
|
2519
|
+
# # Generates: "age": { "$gte": 18, "$lte": 65 }
|
|
2520
|
+
#
|
|
2521
|
+
# Record.where(:date.between => 5.days.ago...2.days.ago)
|
|
2522
|
+
# # Generates: "date": { "$gte": <5 days ago>, "$lt": <2 days ago> }
|
|
2523
|
+
#
|
|
2524
|
+
# # Beginless/endless ranges only constrain the side that is present.
|
|
2525
|
+
# User.where(:age.between => 18..)
|
|
2526
|
+
# # Generates: "age": { "$gte": 18 }
|
|
2527
|
+
#
|
|
2519
2528
|
class BetweenConstraint < Constraint
|
|
2520
2529
|
# @!method between
|
|
2521
2530
|
# A registered method on a symbol to create the constraint.
|
|
@@ -2526,9 +2535,11 @@ module Parse
|
|
|
2526
2535
|
|
|
2527
2536
|
# @return [Hash] the compiled constraint.
|
|
2528
2537
|
def build
|
|
2538
|
+
return build_range(@value) if @value.is_a?(Range)
|
|
2539
|
+
|
|
2529
2540
|
value = formatted_value
|
|
2530
2541
|
unless value.is_a?(Array) && value.length == 2
|
|
2531
|
-
raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value]"
|
|
2542
|
+
raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range"
|
|
2532
2543
|
end
|
|
2533
2544
|
|
|
2534
2545
|
min_value, max_value = value
|
|
@@ -2542,6 +2553,51 @@ module Parse
|
|
|
2542
2553
|
Parse::Constraint::LessThanOrEqualConstraint.key => formatted_max,
|
|
2543
2554
|
} }
|
|
2544
2555
|
end
|
|
2556
|
+
|
|
2557
|
+
# @!visibility private
|
|
2558
|
+
# Extract raw (unformatted) `[min_value, max_value, exclude_max]`
|
|
2559
|
+
# bounds from a `between`-style value: a Range (`exclude_max`
|
|
2560
|
+
# reflects `exclude_end?`) or a 2-element Array (always inclusive
|
|
2561
|
+
# on both ends, matching {#build}'s array branch). Shared with
|
|
2562
|
+
# {Parse::Query#where_not_between}, which needs the same bounds
|
|
2563
|
+
# to build the negated (`$or` of the two flipped comparisons)
|
|
2564
|
+
# form — a shape {BetweenConstraint} itself cannot safely emit
|
|
2565
|
+
# from a single constraint's `#build` (see `where_not_between`'s
|
|
2566
|
+
# docs for why).
|
|
2567
|
+
# @param value [Range, Array] a `between`-style value.
|
|
2568
|
+
# @return [Array(Object, Object, Boolean)]
|
|
2569
|
+
# @raise [ArgumentError] if `value` isn't a Range or 2-element Array.
|
|
2570
|
+
def self.extract_bounds(value)
|
|
2571
|
+
if value.is_a?(Range)
|
|
2572
|
+
[value.begin, value.end, value.exclude_end?]
|
|
2573
|
+
elsif value.is_a?(Array) && value.length == 2
|
|
2574
|
+
[value[0], value[1], false]
|
|
2575
|
+
else
|
|
2576
|
+
raise ArgumentError, "#{name}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range"
|
|
2577
|
+
end
|
|
2578
|
+
end
|
|
2579
|
+
|
|
2580
|
+
private
|
|
2581
|
+
|
|
2582
|
+
# @return [Hash] the compiled constraint for a Ruby Range value.
|
|
2583
|
+
def build_range(range)
|
|
2584
|
+
bounds = {}
|
|
2585
|
+
|
|
2586
|
+
unless range.begin.nil?
|
|
2587
|
+
bounds[Parse::Constraint::GreaterThanOrEqualConstraint.key] = Parse::Constraint.formatted_value(range.begin)
|
|
2588
|
+
end
|
|
2589
|
+
|
|
2590
|
+
unless range.end.nil?
|
|
2591
|
+
upper_key = range.exclude_end? ? Parse::Constraint::LessThanConstraint.key : Parse::Constraint::LessThanOrEqualConstraint.key
|
|
2592
|
+
bounds[upper_key] = Parse::Constraint.formatted_value(range.end)
|
|
2593
|
+
end
|
|
2594
|
+
|
|
2595
|
+
if bounds.empty?
|
|
2596
|
+
raise ArgumentError, "#{self.class}: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)"
|
|
2597
|
+
end
|
|
2598
|
+
|
|
2599
|
+
{ @operation.operand => bounds }
|
|
2600
|
+
end
|
|
2545
2601
|
end
|
|
2546
2602
|
|
|
2547
2603
|
# @!visibility private
|
data/lib/parse/query.rb
CHANGED
|
@@ -1113,6 +1113,77 @@ module Parse
|
|
|
1113
1113
|
copy_query
|
|
1114
1114
|
end
|
|
1115
1115
|
|
|
1116
|
+
# Add a "field is NOT between" condition — the logical negation of
|
|
1117
|
+
# `.where(field.between => value)`: `field < min OR field > max` for a
|
|
1118
|
+
# fully-bounded Range/Array, or a single one-sided comparison when the
|
|
1119
|
+
# Range is beginless/endless (mirroring how {Parse::Constraint::BetweenConstraint}
|
|
1120
|
+
# itself only constrains the side that is present).
|
|
1121
|
+
#
|
|
1122
|
+
# Not available as a `field.not_between => value` symbol constraint,
|
|
1123
|
+
# unlike `.between`: a between-style range is inherently an OR of two
|
|
1124
|
+
# comparisons, and a single {Parse::Constraint}'s `#build` can only
|
|
1125
|
+
# safely contribute an AND'd clause to a query. A constraint that
|
|
1126
|
+
# unilaterally emitted a top-level `$or` would collide with (and
|
|
1127
|
+
# silently clobber, or be clobbered by) any other `$or` this query
|
|
1128
|
+
# already produces via {#or_where} / `|` / another `where_not_between`
|
|
1129
|
+
# call, since only one `$or` group merges correctly per query. This
|
|
1130
|
+
# method instead composes the negation the same way {Parse::Query.and}
|
|
1131
|
+
# does — concatenating compiled constraint arrays — which correctly
|
|
1132
|
+
# nests the OR inside the query's existing AND'd conditions instead of
|
|
1133
|
+
# replacing them the way {#or_where} would.
|
|
1134
|
+
#
|
|
1135
|
+
# @example
|
|
1136
|
+
# Person.query.where_not_between(:age, 5..25)
|
|
1137
|
+
# # age < 5 OR age > 25
|
|
1138
|
+
#
|
|
1139
|
+
# Record.query.where(:archived => false).where_not_between(:date, 5.days.ago...2.days.ago)
|
|
1140
|
+
# # archived == false AND (date < 5.days.ago OR date >= 2.days.ago)
|
|
1141
|
+
#
|
|
1142
|
+
# @param field [Symbol, String] the field to constrain.
|
|
1143
|
+
# @param value [Range, Array] a `between`-style value: a Range (including
|
|
1144
|
+
# beginless/endless/exclusive-end forms) or a 2-element `[min, max]` Array.
|
|
1145
|
+
# @return [self]
|
|
1146
|
+
# @raise [ArgumentError] if `value` isn't a Range or 2-element Array, or
|
|
1147
|
+
# is a fully-open (`nil..nil`) Range.
|
|
1148
|
+
def where_not_between(field, value)
|
|
1149
|
+
field = field.to_sym
|
|
1150
|
+
min_value, max_value, exclude_max = Parse::Constraint::BetweenConstraint.extract_bounds(value)
|
|
1151
|
+
|
|
1152
|
+
if min_value.nil? && max_value.nil?
|
|
1153
|
+
raise ArgumentError, "Query#where_not_between: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)."
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# A fully-bounded range needs its own `$or` group (`field < min OR
|
|
1157
|
+
# field > max`). Only ONE `$or` group survives the plain-Hash merge
|
|
1158
|
+
# every constraint's compiled output goes through (`constraint_reduce`
|
|
1159
|
+
# deep-merges compiled hashes; a second top-level `$or` key silently
|
|
1160
|
+
# overwrites the first rather than combining with it — the same
|
|
1161
|
+
# constraint that makes `field.not_between => value` unsafe as a
|
|
1162
|
+
# symbol constraint, see above). Fail loudly here instead of quietly
|
|
1163
|
+
# dropping half the query if this query already has one, from
|
|
1164
|
+
# `#or_where`, `|`, or an earlier `where_not_between` call.
|
|
1165
|
+
if min_value && max_value && @where.any? { |c| c.is_a?(Parse::Constraint::CompoundQueryConstraint) }
|
|
1166
|
+
raise ArgumentError,
|
|
1167
|
+
"Query#where_not_between: this query already has an `$or` group (from `or_where`, `|`, " \
|
|
1168
|
+
"or a prior `where_not_between` call). Only one `$or` group can be safely merged per " \
|
|
1169
|
+
"query. Compose the two queries with Parse::Query.and(...) instead."
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
negated = if min_value.nil?
|
|
1173
|
+
Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
|
|
1174
|
+
elsif max_value.nil?
|
|
1175
|
+
Parse::Query.new(@table).where(field.lt => min_value)
|
|
1176
|
+
else
|
|
1177
|
+
lower = Parse::Query.new(@table).where(field.lt => min_value)
|
|
1178
|
+
upper = Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
|
|
1179
|
+
Parse::Query.or(lower, upper)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
@where = @where + negated.where
|
|
1183
|
+
@results = nil
|
|
1184
|
+
self
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1116
1187
|
# Queries can be made using distinct, allowing you find unique values for a specified field.
|
|
1117
1188
|
# For this to be performant, please remember to index your database.
|
|
1118
1189
|
# @example
|
|
@@ -1409,7 +1480,6 @@ module Parse
|
|
|
1409
1480
|
return first_direct(limit_or_constraints)
|
|
1410
1481
|
end
|
|
1411
1482
|
|
|
1412
|
-
fetch_count = 1
|
|
1413
1483
|
if limit_or_constraints.is_a?(Hash)
|
|
1414
1484
|
conditions(limit_or_constraints)
|
|
1415
1485
|
# Check if limit was set in constraints, otherwise use 1
|
|
@@ -1490,15 +1560,19 @@ module Parse
|
|
|
1490
1560
|
# @return [Parse::Object] the object with the given ID.
|
|
1491
1561
|
# @raise [Parse::Error] if the object is not found.
|
|
1492
1562
|
def get(object_id)
|
|
1493
|
-
parse_class = Object.const_get(@table) if Object.const_defined?(@table)
|
|
1494
|
-
parse_class ||= Parse::Object
|
|
1495
|
-
|
|
1496
1563
|
response = client.fetch_object(@table, object_id)
|
|
1497
1564
|
if response.error?
|
|
1498
1565
|
raise Parse::Error.new(response.code, response.error)
|
|
1499
1566
|
end
|
|
1500
1567
|
|
|
1501
|
-
|
|
1568
|
+
# Pass the table name through as-is rather than pre-resolving it to
|
|
1569
|
+
# a Class: `Object.build` does its own `Parse::Model.find_class`
|
|
1570
|
+
# lookup against the String, which correctly honors `parse_class`
|
|
1571
|
+
# aliasing. Resolving to a Class first and handing that back to
|
|
1572
|
+
# `build` broke aliased lookups, since `find_class` would then
|
|
1573
|
+
# stringify the Ruby constant name (e.g. "Musician") instead of
|
|
1574
|
+
# matching the declared alias (e.g. "Artist").
|
|
1575
|
+
Parse::Object.build(response.result, @table)
|
|
1502
1576
|
end
|
|
1503
1577
|
|
|
1504
1578
|
# max_results is used to iterate through as many API requests as possible using
|
data/lib/parse/stack/version.rb
CHANGED
data/lib/parse/webhooks.rb
CHANGED