parse-stack-next 4.5.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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
data/lib/parse/schema.rb
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
# Schema introspection and migration tools for Parse Server.
|
|
6
|
+
# Provides utilities to compare local Ruby models with server schema,
|
|
7
|
+
# generate migration scripts, and manage schema changes.
|
|
8
|
+
#
|
|
9
|
+
# @example Inspecting server schema
|
|
10
|
+
# schema = Parse::Schema.fetch("Song")
|
|
11
|
+
# puts schema.fields # => { "title" => "String", "duration" => "Number" }
|
|
12
|
+
#
|
|
13
|
+
# @example Comparing local model to server
|
|
14
|
+
# diff = Parse::Schema.diff(Song)
|
|
15
|
+
# puts diff.missing_on_server # Fields in model but not on server
|
|
16
|
+
# puts diff.missing_locally # Fields on server but not in model
|
|
17
|
+
#
|
|
18
|
+
# @example Generating migration
|
|
19
|
+
# migration = Parse::Schema.migration(Song)
|
|
20
|
+
# migration.apply! # Apply changes to server
|
|
21
|
+
#
|
|
22
|
+
module Schema
|
|
23
|
+
# Opt-in default Class Level Permissions applied to NEWLY-CREATED
|
|
24
|
+
# classes during {Schema::Migration#apply!}. Only used when the
|
|
25
|
+
# model class does not declare its own CLPs via
|
|
26
|
+
# `set_class_level_permissions`. Existing server classes are NEVER
|
|
27
|
+
# rewritten by this setting — the migrator only emits
|
|
28
|
+
# `classLevelPermissions` on the initial `create_schema` call.
|
|
29
|
+
#
|
|
30
|
+
# Default is `nil` (no CLPs sent — Parse Server's wide-open default
|
|
31
|
+
# applies). Setting this to a restrictive Hash (e.g. require master
|
|
32
|
+
# key, or require an authenticated user) gives integrators a single
|
|
33
|
+
# toggle to make new classes safe-by-default without touching every
|
|
34
|
+
# model.
|
|
35
|
+
#
|
|
36
|
+
# @example Lock new classes to authenticated reads + master-key writes
|
|
37
|
+
# Parse::Schema.default_class_level_permissions = {
|
|
38
|
+
# "find" => { "requiresAuthentication" => true },
|
|
39
|
+
# "get" => { "requiresAuthentication" => true },
|
|
40
|
+
# "count" => { "requiresAuthentication" => true },
|
|
41
|
+
# "create" => {},
|
|
42
|
+
# "update" => {},
|
|
43
|
+
# "delete" => {},
|
|
44
|
+
# "addField" => {},
|
|
45
|
+
# }
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash, nil]
|
|
48
|
+
@default_class_level_permissions = nil
|
|
49
|
+
class << self
|
|
50
|
+
attr_accessor :default_class_level_permissions
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parse field type mappings to Ruby types
|
|
54
|
+
TYPE_MAP = {
|
|
55
|
+
"String" => :string,
|
|
56
|
+
"Number" => :integer,
|
|
57
|
+
"Boolean" => :boolean,
|
|
58
|
+
"Date" => :date,
|
|
59
|
+
"File" => :file,
|
|
60
|
+
"GeoPoint" => :geopoint,
|
|
61
|
+
"Polygon" => :polygon,
|
|
62
|
+
"Array" => :array,
|
|
63
|
+
"Object" => :object,
|
|
64
|
+
"Pointer" => :pointer,
|
|
65
|
+
"Relation" => :relation,
|
|
66
|
+
"Bytes" => :bytes,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# Reverse mapping from Ruby types to Parse types
|
|
70
|
+
REVERSE_TYPE_MAP = {
|
|
71
|
+
string: "String",
|
|
72
|
+
integer: "Number",
|
|
73
|
+
float: "Number",
|
|
74
|
+
boolean: "Boolean",
|
|
75
|
+
date: "Date",
|
|
76
|
+
file: "File",
|
|
77
|
+
geopoint: "GeoPoint",
|
|
78
|
+
geo_point: "GeoPoint",
|
|
79
|
+
polygon: "Polygon",
|
|
80
|
+
array: "Array",
|
|
81
|
+
object: "Object",
|
|
82
|
+
pointer: "Pointer",
|
|
83
|
+
relation: "Relation",
|
|
84
|
+
bytes: "Bytes",
|
|
85
|
+
acl: "ACL",
|
|
86
|
+
}.freeze
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
# Fetch all schemas from the Parse Server.
|
|
90
|
+
# @param client [Parse::Client] optional client to use
|
|
91
|
+
# @return [Array<SchemaInfo>] array of schema information objects
|
|
92
|
+
def all(client: nil)
|
|
93
|
+
client ||= Parse.client
|
|
94
|
+
response = client.schemas
|
|
95
|
+
return [] unless response.success?
|
|
96
|
+
|
|
97
|
+
results = response.result.is_a?(Hash) ? response.result["results"] : response.result
|
|
98
|
+
(results || []).map { |data| SchemaInfo.new(data) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Fetch schema for a specific class.
|
|
102
|
+
# @param class_name [String, Class] the Parse class name or model class
|
|
103
|
+
# @param client [Parse::Client] optional client to use
|
|
104
|
+
# @return [SchemaInfo, nil] the schema info or nil if not found
|
|
105
|
+
def fetch(class_name, client: nil)
|
|
106
|
+
class_name = class_name.parse_class if class_name.respond_to?(:parse_class)
|
|
107
|
+
client ||= Parse.client
|
|
108
|
+
response = client.schema(class_name)
|
|
109
|
+
return nil unless response.success?
|
|
110
|
+
SchemaInfo.new(response.result)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Compare a local Parse::Object model with its server schema.
|
|
114
|
+
# @param model_class [Class] a Parse::Object subclass
|
|
115
|
+
# @param client [Parse::Client] optional client to use
|
|
116
|
+
# @return [SchemaDiff] the differences between local and server schema
|
|
117
|
+
def diff(model_class, client: nil)
|
|
118
|
+
raise ArgumentError, "Expected a Parse::Object subclass" unless model_class < Parse::Object
|
|
119
|
+
|
|
120
|
+
server_schema = fetch(model_class.parse_class, client: client)
|
|
121
|
+
SchemaDiff.new(model_class, server_schema)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Generate a migration for a model class.
|
|
125
|
+
# @param model_class [Class] a Parse::Object subclass
|
|
126
|
+
# @param client [Parse::Client] optional client to use
|
|
127
|
+
# @return [Migration] a migration object
|
|
128
|
+
def migration(model_class, client: nil)
|
|
129
|
+
diff_result = diff(model_class, client: client)
|
|
130
|
+
Migration.new(model_class, diff_result, client: client)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if a class exists on the server.
|
|
134
|
+
# @param class_name [String, Class] the Parse class name or model class
|
|
135
|
+
# @param client [Parse::Client] optional client to use
|
|
136
|
+
# @return [Boolean] true if the class exists
|
|
137
|
+
def exists?(class_name, client: nil)
|
|
138
|
+
!fetch(class_name, client: client).nil?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get all class names from the server.
|
|
142
|
+
# @param client [Parse::Client] optional client to use
|
|
143
|
+
# @return [Array<String>] array of class names
|
|
144
|
+
def class_names(client: nil)
|
|
145
|
+
all(client: client).map(&:class_name)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Represents schema information for a Parse class.
|
|
150
|
+
class SchemaInfo
|
|
151
|
+
attr_reader :class_name, :fields, :indexes, :class_level_permissions
|
|
152
|
+
|
|
153
|
+
def initialize(data)
|
|
154
|
+
@class_name = data["className"]
|
|
155
|
+
@fields = parse_fields(data["fields"] || {})
|
|
156
|
+
@indexes = data["indexes"] || {}
|
|
157
|
+
@class_level_permissions = data["classLevelPermissions"] || {}
|
|
158
|
+
@raw = data
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get field names.
|
|
162
|
+
# @return [Array<String>] field names
|
|
163
|
+
def field_names
|
|
164
|
+
@fields.keys
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get field type for a specific field.
|
|
168
|
+
# @param field_name [String, Symbol] the field name
|
|
169
|
+
# @return [Symbol, nil] the Ruby type symbol or nil
|
|
170
|
+
def field_type(field_name)
|
|
171
|
+
@fields[field_name.to_s]&.dig(:type)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Get pointer target class for a field.
|
|
175
|
+
# @param field_name [String, Symbol] the field name
|
|
176
|
+
# @return [String, nil] the target class name or nil
|
|
177
|
+
def pointer_target(field_name)
|
|
178
|
+
@fields[field_name.to_s]&.dig(:target_class)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Check if a field exists.
|
|
182
|
+
# @param field_name [String, Symbol] the field name
|
|
183
|
+
# @return [Boolean]
|
|
184
|
+
def has_field?(field_name)
|
|
185
|
+
@fields.key?(field_name.to_s)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check if this is a built-in Parse class.
|
|
189
|
+
# @return [Boolean]
|
|
190
|
+
def builtin?
|
|
191
|
+
@class_name.start_with?("_")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Get raw schema data.
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def to_h
|
|
197
|
+
@raw
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def parse_fields(fields_hash)
|
|
203
|
+
result = {}
|
|
204
|
+
fields_hash.each do |name, info|
|
|
205
|
+
type_str = info["type"]
|
|
206
|
+
ruby_type = TYPE_MAP[type_str] || type_str.to_s.downcase.to_sym
|
|
207
|
+
result[name] = {
|
|
208
|
+
type: ruby_type,
|
|
209
|
+
target_class: info["targetClass"],
|
|
210
|
+
required: info["required"] || false,
|
|
211
|
+
default_value: info["defaultValue"],
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
result
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Represents the difference between local model and server schema.
|
|
219
|
+
class SchemaDiff
|
|
220
|
+
attr_reader :model_class, :server_schema
|
|
221
|
+
|
|
222
|
+
def initialize(model_class, server_schema)
|
|
223
|
+
@model_class = model_class
|
|
224
|
+
@server_schema = server_schema
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if server schema exists.
|
|
228
|
+
# @return [Boolean]
|
|
229
|
+
def server_exists?
|
|
230
|
+
!@server_schema.nil?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Fields defined locally but missing on server.
|
|
234
|
+
# @return [Hash] field name => type pairs
|
|
235
|
+
def missing_on_server
|
|
236
|
+
return local_fields unless server_exists?
|
|
237
|
+
|
|
238
|
+
local = local_fields
|
|
239
|
+
server = server_field_names
|
|
240
|
+
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)
|
|
244
|
+
end
|
|
245
|
+
missing
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Fields on server but not defined locally.
|
|
249
|
+
# @return [Hash] field name => type pairs
|
|
250
|
+
def missing_locally
|
|
251
|
+
return {} unless server_exists?
|
|
252
|
+
|
|
253
|
+
server = @server_schema.fields
|
|
254
|
+
local = local_field_names
|
|
255
|
+
missing = {}
|
|
256
|
+
server.each do |name, info|
|
|
257
|
+
# Skip core fields
|
|
258
|
+
next if %w[objectId createdAt updatedAt ACL].include?(name)
|
|
259
|
+
missing[name] = info[:type] unless local.include?(name) || local.include?(name.underscore.to_sym)
|
|
260
|
+
end
|
|
261
|
+
missing
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Fields with type mismatches.
|
|
265
|
+
# @return [Hash] field name => { local: type, server: type }
|
|
266
|
+
def type_mismatches
|
|
267
|
+
return {} unless server_exists?
|
|
268
|
+
|
|
269
|
+
mismatches = {}
|
|
270
|
+
local_fields.each do |name, local_type|
|
|
271
|
+
next if core_field?(name)
|
|
272
|
+
name_str = name.to_s.camelize(:lower)
|
|
273
|
+
server_type = @server_schema.field_type(name_str)
|
|
274
|
+
next unless server_type
|
|
275
|
+
|
|
276
|
+
# Normalize types for comparison
|
|
277
|
+
normalized_local = normalize_type(local_type)
|
|
278
|
+
normalized_server = normalize_type(server_type)
|
|
279
|
+
|
|
280
|
+
if normalized_local != normalized_server
|
|
281
|
+
mismatches[name] = { local: local_type, server: server_type }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
mismatches
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Check if schemas are in sync.
|
|
288
|
+
# @return [Boolean]
|
|
289
|
+
def in_sync?
|
|
290
|
+
missing_on_server.empty? && missing_locally.empty? && type_mismatches.empty?
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Generate a human-readable summary.
|
|
294
|
+
# @return [String]
|
|
295
|
+
def summary
|
|
296
|
+
lines = ["Schema diff for #{@model_class.parse_class}:"]
|
|
297
|
+
|
|
298
|
+
if !server_exists?
|
|
299
|
+
lines << " - Class does not exist on server"
|
|
300
|
+
elsif in_sync?
|
|
301
|
+
lines << " - Schemas are in sync"
|
|
302
|
+
else
|
|
303
|
+
unless missing_on_server.empty?
|
|
304
|
+
lines << " Missing on server:"
|
|
305
|
+
missing_on_server.each { |n, t| lines << " + #{n}: #{t}" }
|
|
306
|
+
end
|
|
307
|
+
unless missing_locally.empty?
|
|
308
|
+
lines << " Missing locally:"
|
|
309
|
+
missing_locally.each { |n, t| lines << " - #{n}: #{t}" }
|
|
310
|
+
end
|
|
311
|
+
unless type_mismatches.empty?
|
|
312
|
+
lines << " Type mismatches:"
|
|
313
|
+
type_mismatches.each { |n, m| lines << " ~ #{n}: local=#{m[:local]}, server=#{m[:server]}" }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
lines.join("\n")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def local_fields
|
|
323
|
+
@model_class.fields.reject { |k, _| core_field?(k) }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def local_field_names
|
|
327
|
+
local_fields.keys.map(&:to_s)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def server_field_names
|
|
331
|
+
@server_schema&.field_names || []
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def core_field?(name)
|
|
335
|
+
%i[id object_id created_at updated_at acl objectId createdAt updatedAt ACL].include?(name.to_sym)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def normalize_type(type)
|
|
339
|
+
case type.to_sym
|
|
340
|
+
when :integer, :float, :number then :number
|
|
341
|
+
when :geo_point then :geopoint
|
|
342
|
+
else type.to_sym
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Represents a schema migration to be applied.
|
|
348
|
+
class Migration
|
|
349
|
+
attr_reader :model_class, :diff, :client
|
|
350
|
+
|
|
351
|
+
def initialize(model_class, diff, client: nil)
|
|
352
|
+
@model_class = model_class
|
|
353
|
+
@diff = diff
|
|
354
|
+
@client = client || Parse.client
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Check if migration is needed.
|
|
358
|
+
# @return [Boolean]
|
|
359
|
+
def needed?
|
|
360
|
+
!@diff.in_sync? || !@diff.server_exists?
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Get the operations that would be performed.
|
|
364
|
+
# @return [Array<Hash>] list of operations
|
|
365
|
+
def operations
|
|
366
|
+
ops = []
|
|
367
|
+
|
|
368
|
+
unless @diff.server_exists?
|
|
369
|
+
ops << { action: :create_class, class_name: @model_class.parse_class }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
@diff.missing_on_server.each do |name, type|
|
|
373
|
+
ops << {
|
|
374
|
+
action: :add_field,
|
|
375
|
+
field: name.to_s.camelize(:lower),
|
|
376
|
+
type: REVERSE_TYPE_MAP[type] || "String",
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
ops
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Preview the migration without applying.
|
|
384
|
+
# @return [String] human-readable preview
|
|
385
|
+
def preview
|
|
386
|
+
return "No migration needed" unless needed?
|
|
387
|
+
|
|
388
|
+
lines = ["Migration for #{@model_class.parse_class}:"]
|
|
389
|
+
operations.each do |op|
|
|
390
|
+
case op[:action]
|
|
391
|
+
when :create_class
|
|
392
|
+
lines << " CREATE CLASS #{op[:class_name]}"
|
|
393
|
+
when :add_field
|
|
394
|
+
lines << " ADD FIELD #{op[:field]} (#{op[:type]})"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
lines.join("\n")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Apply the migration to the server.
|
|
401
|
+
# @param dry_run [Boolean] if true, only preview without applying
|
|
402
|
+
# @return [Hash] results of the migration
|
|
403
|
+
def apply!(dry_run: false)
|
|
404
|
+
return { status: :skipped, message: "No migration needed" } unless needed?
|
|
405
|
+
|
|
406
|
+
if dry_run
|
|
407
|
+
return { status: :preview, operations: operations, preview: preview }
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
results = { status: :success, applied: [], errors: [] }
|
|
411
|
+
|
|
412
|
+
# Create class if needed
|
|
413
|
+
unless @diff.server_exists?
|
|
414
|
+
schema = build_schema
|
|
415
|
+
response = @client.create_schema(@model_class.parse_class, schema)
|
|
416
|
+
if response.success?
|
|
417
|
+
results[:applied] << { action: :create_class, class_name: @model_class.parse_class }
|
|
418
|
+
else
|
|
419
|
+
results[:errors] << { action: :create_class, error: response.error }
|
|
420
|
+
results[:status] = :partial
|
|
421
|
+
end
|
|
422
|
+
return results
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Add missing fields
|
|
426
|
+
@diff.missing_on_server.each do |name, type|
|
|
427
|
+
field_name = name.to_s.camelize(:lower)
|
|
428
|
+
field_schema = { "fields" => { field_name => field_definition(type) } }
|
|
429
|
+
|
|
430
|
+
response = @client.update_schema(@model_class.parse_class, field_schema)
|
|
431
|
+
if response.success?
|
|
432
|
+
results[:applied] << { action: :add_field, field: field_name, type: type }
|
|
433
|
+
else
|
|
434
|
+
results[:errors] << { action: :add_field, field: field_name, error: response.error }
|
|
435
|
+
results[:status] = :partial
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
results[:status] = :failed if results[:applied].empty? && results[:errors].any?
|
|
440
|
+
results
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private
|
|
444
|
+
|
|
445
|
+
def build_schema
|
|
446
|
+
fields = {}
|
|
447
|
+
@model_class.fields.each do |name, type|
|
|
448
|
+
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)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Add pointer targets
|
|
454
|
+
@model_class.references.each do |name, target_class|
|
|
455
|
+
field_name = name.to_s.camelize(:lower)
|
|
456
|
+
fields[field_name] = {
|
|
457
|
+
"type" => "Pointer",
|
|
458
|
+
"targetClass" => target_class.to_s,
|
|
459
|
+
}
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
schema = { "className" => @model_class.parse_class, "fields" => fields }
|
|
463
|
+
# Attach CLPs only for newly-created classes when the integrator
|
|
464
|
+
# has opted in via `Parse::Schema.default_class_level_permissions=`.
|
|
465
|
+
# Per-model CLPs declared via `set_class_level_permissions` win
|
|
466
|
+
# if present.
|
|
467
|
+
clps = model_class_level_permissions
|
|
468
|
+
schema["classLevelPermissions"] = clps if clps
|
|
469
|
+
schema
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Resolve CLPs for the model being migrated. Returns:
|
|
473
|
+
# - the model's explicitly-declared CLPs if it has any (looked up
|
|
474
|
+
# via the conventional accessor names some Parse-Stack models
|
|
475
|
+
# ship with), OR
|
|
476
|
+
# - {Parse::Schema.default_class_level_permissions} if set, OR
|
|
477
|
+
# - +nil+ to leave `classLevelPermissions` off the schema body so
|
|
478
|
+
# Parse Server uses its built-in defaults.
|
|
479
|
+
def model_class_level_permissions
|
|
480
|
+
%i[class_level_permissions classLevelPermissions clp].each do |reader|
|
|
481
|
+
next unless @model_class.respond_to?(reader)
|
|
482
|
+
val = @model_class.public_send(reader)
|
|
483
|
+
return val if val.is_a?(Hash) && !val.empty?
|
|
484
|
+
end
|
|
485
|
+
Parse::Schema.default_class_level_permissions
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def field_definition(type)
|
|
489
|
+
parse_type = REVERSE_TYPE_MAP[type.to_sym] || "String"
|
|
490
|
+
{ "type" => parse_type }
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "parse/stack"
|
|
5
|
+
require "parse/stack/tasks"
|
|
6
|
+
require "rails/generators"
|
|
7
|
+
require "rails/generators/named_base"
|
|
8
|
+
|
|
9
|
+
# Module namespace to show up in the generators list for Rails.
|
|
10
|
+
module ParseStack
|
|
11
|
+
# Adds support for rails when installing Parse::Stack to a Rails project.
|
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
|
13
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
14
|
+
|
|
15
|
+
desc "This generator creates an initializer file at config/initializers"
|
|
16
|
+
# @!visibility private
|
|
17
|
+
def generate_initializer
|
|
18
|
+
copy_file "parse.rb", "config/initializers/parse.rb"
|
|
19
|
+
copy_file "model_user.rb", File.join("app/models", "user.rb")
|
|
20
|
+
copy_file "model_role.rb", File.join("app/models", "role.rb")
|
|
21
|
+
copy_file "model_session.rb", File.join("app/models", "session.rb")
|
|
22
|
+
copy_file "model_installation.rb", File.join("app/models", "installation.rb")
|
|
23
|
+
copy_file "webhooks.rb", File.join("app/models", "webhooks.rb")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @!visibility private
|
|
28
|
+
class ModelGenerator < Rails::Generators::NamedBase
|
|
29
|
+
source_root File.expand_path(__dir__ + "/templates")
|
|
30
|
+
desc "Creates a Parse::Object model subclass."
|
|
31
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
32
|
+
check_class_collision
|
|
33
|
+
|
|
34
|
+
# @!visibility private
|
|
35
|
+
def create_model_file
|
|
36
|
+
@allowed_types = Parse::Properties::TYPES - [:acl, :id, :relation]
|
|
37
|
+
template "model.erb", File.join("app/models", class_path, "#{file_name}.rb")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
class <%= class_name %> < Parse::Object
|
|
3
|
+
# See: https://github.com/modernistik/parse-stack#defining-properties
|
|
4
|
+
|
|
5
|
+
# You can change the inferred Parse table/collection name below
|
|
6
|
+
# parse_class "<%= class_name.to_s.to_parse_class %>"
|
|
7
|
+
<% attributes.each do |attr|
|
|
8
|
+
parse_type = attr.type.to_s.downcase.to_sym
|
|
9
|
+
unless @allowed_types.include?(parse_type)
|
|
10
|
+
puts "\n[Warning] Skipping property `#{attr.name}` with type `#{parse_type}`. Type should be one of #{@allowed_types}."
|
|
11
|
+
next
|
|
12
|
+
end %>
|
|
13
|
+
property :<%= attr.name %>, :<%= parse_type -%>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
# See: https://github.com/modernistik/parse-stack#cloud-code-webhooks
|
|
17
|
+
# define a before save webhook for <%= class_name %>
|
|
18
|
+
webhook :before_save do
|
|
19
|
+
<%= class_name.to_s.underscore %> = parse_object
|
|
20
|
+
# perform any validations with <%= class_name.to_s.underscore %>
|
|
21
|
+
# use `error!(msg)` to fail the save
|
|
22
|
+
# ...
|
|
23
|
+
<%= class_name.to_s.underscore %>
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
## define an after save webhook for <%= class_name %>
|
|
27
|
+
#
|
|
28
|
+
# webhook :after_save do
|
|
29
|
+
# <%= class_name.to_s.underscore %> = parse_object
|
|
30
|
+
#
|
|
31
|
+
# end
|
|
32
|
+
|
|
33
|
+
## define a before delete webhook for <%= class_name %>
|
|
34
|
+
# webhook :before_delete do
|
|
35
|
+
# <%= class_name.to_s.underscore %> = parse_object
|
|
36
|
+
# # use `error!(msg)` to fail the delete
|
|
37
|
+
# true # allow the deletion
|
|
38
|
+
# end
|
|
39
|
+
|
|
40
|
+
## define an after delete webhook for <%= class_name %>
|
|
41
|
+
# webhook :after_delete do
|
|
42
|
+
# <%= class_name.to_s.underscore %> = parse_object
|
|
43
|
+
# end
|
|
44
|
+
|
|
45
|
+
## Example of a CloudCode Webhook function
|
|
46
|
+
## define a `helloWorld` Parse CloudCode function
|
|
47
|
+
# webhook :function, :helloWorld do
|
|
48
|
+
# "Hello!"
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class Parse::User < Parse::Object
|
|
2
|
+
# add additional properties
|
|
3
|
+
|
|
4
|
+
# define a before save webhook for Parse::User
|
|
5
|
+
# webhook :before_save do
|
|
6
|
+
# obj = parse_object # Parse::User
|
|
7
|
+
# # make changes to record....
|
|
8
|
+
# obj # will send the proper changelist back to Parse-Server
|
|
9
|
+
# end
|
|
10
|
+
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "parse/stack"
|
|
2
|
+
|
|
3
|
+
# Set your specific Parse keys in your ENV. For all connection options, see
|
|
4
|
+
# https://github.com/modernistik/parse-stack#connection-setup
|
|
5
|
+
Parse.setup app_id: ENV["PARSE_SERVER_APPLICATION_ID"],
|
|
6
|
+
api_key: ENV["PARSE_SERVER_REST_API_KEY"],
|
|
7
|
+
master_key: ENV["PARSE_SERVER_MASTER_KEY"], # optional
|
|
8
|
+
server_url: "https://localhost:1337/parse"
|
|
9
|
+
# optional
|
|
10
|
+
# logging: false,
|
|
11
|
+
# cache: Moneta.new(:File, dir: 'tmp/cache'),
|
|
12
|
+
# expires: 1 # cache ttl 1 second
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# See: https://github.com/modernistik/parse-stack#cloud-code-webhooks
|
|
2
|
+
Parse::Webhooks.route(:function, :helloWorld) do
|
|
3
|
+
# use the Parse::Payload instance methods in this block
|
|
4
|
+
name = params["name"].to_s #function params
|
|
5
|
+
|
|
6
|
+
# will return proper error response
|
|
7
|
+
# error!("Missing argument 'name'.") unless name.present?
|
|
8
|
+
|
|
9
|
+
name.present? ? "Hello #{name}!" : "Hello World!"
|
|
10
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module Stack
|
|
6
|
+
# Support for adding rake tasks to a Rails project.
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
rake_tasks do
|
|
9
|
+
require_relative "tasks"
|
|
10
|
+
Parse::Stack.load_tasks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
generators do
|
|
14
|
+
require_relative "generators/rails"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|