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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. metadata +377 -0
@@ -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,4 @@
1
+ class Parse::Installation < Parse::Object
2
+ # See: https://github.com/modernistik/parse-stack#parseinstallation
3
+ # add additional properties here
4
+ end
@@ -0,0 +1,4 @@
1
+ class Parse::Role < Parse::Object
2
+ # See: https://github.com/modernistik/parse-stack#parserole
3
+ # add additional properties here
4
+ end
@@ -0,0 +1,4 @@
1
+ class Parse::Session < Parse::Object
2
+ # See: https://github.com/modernistik/parse-stack#parsesession
3
+ # add additional properties here
4
+ 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