zero-rails-adapter 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +344 -0
  4. data/app/controllers/zero_rails_adapter/mutations_controller.rb +74 -0
  5. data/config/routes.rb +7 -0
  6. data/examples/nextjs/README.md +29 -0
  7. data/examples/nextjs/src/zero/mutators.ts +37 -0
  8. data/examples/nextjs/src/zero/schema.ts +20 -0
  9. data/lib/generators/zero_rails_adapter/install/install_generator.rb +15 -0
  10. data/lib/generators/zero_rails_adapter/install/templates/initializer.rb +52 -0
  11. data/lib/generators/zero_rails_adapter/mutator/mutator_generator.rb +31 -0
  12. data/lib/generators/zero_rails_adapter/mutator/templates/mutator.rb +14 -0
  13. data/lib/generators/zero_rails_adapter/typescript/typescript_generator.rb +18 -0
  14. data/lib/zero_rails_adapter/configuration.rb +57 -0
  15. data/lib/zero_rails_adapter/context.rb +19 -0
  16. data/lib/zero_rails_adapter/crud/dispatcher.rb +121 -0
  17. data/lib/zero_rails_adapter/engine.rb +7 -0
  18. data/lib/zero_rails_adapter/errors.rb +50 -0
  19. data/lib/zero_rails_adapter/identity.rb +9 -0
  20. data/lib/zero_rails_adapter/mutation.rb +50 -0
  21. data/lib/zero_rails_adapter/mutator.rb +70 -0
  22. data/lib/zero_rails_adapter/processor.rb +178 -0
  23. data/lib/zero_rails_adapter/registry.rb +51 -0
  24. data/lib/zero_rails_adapter/request.rb +93 -0
  25. data/lib/zero_rails_adapter/request_verifiers/api_key.rb +21 -0
  26. data/lib/zero_rails_adapter/storage/zero_schema.rb +95 -0
  27. data/lib/zero_rails_adapter/type_script/generator.rb +421 -0
  28. data/lib/zero_rails_adapter/version.rb +5 -0
  29. data/lib/zero_rails_adapter.rb +50 -0
  30. metadata +89 -0
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZeroRailsAdapter
4
+ class Request
5
+ REQUIRED_KEYS = %w[clientGroupID mutations pushVersion timestamp requestID].freeze
6
+
7
+ attr_reader :client_group_id, :mutations, :push_version, :timestamp,
8
+ :request_id, :schema, :app_id, :body
9
+
10
+ def self.parse(body, query:)
11
+ body = body.to_h.stringify_keys if body.respond_to?(:to_h)
12
+ query = query.to_h.stringify_keys if query.respond_to?(:to_h)
13
+ mutation_ids = extract_mutation_ids(body)
14
+
15
+ validate_body!(body, mutation_ids:)
16
+ validate_query!(query, mutation_ids:)
17
+
18
+ unless body["pushVersion"] == 1
19
+ raise UnsupportedPushVersionError.new(body["pushVersion"], mutation_ids:)
20
+ end
21
+
22
+ mutations = body["mutations"].map { |value| Mutation.parse(value) }
23
+ new(
24
+ body:,
25
+ client_group_id: body["clientGroupID"],
26
+ mutations:,
27
+ push_version: body["pushVersion"],
28
+ timestamp: body["timestamp"],
29
+ request_id: body["requestID"],
30
+ schema: query["schema"],
31
+ app_id: query["appID"]
32
+ )
33
+ rescue ParseError => error
34
+ if error.mutation_ids.empty?
35
+ raise ParseError.new(error.message, mutation_ids:, source: error.source)
36
+ end
37
+
38
+ raise
39
+ end
40
+
41
+ def self.validate_body!(body, mutation_ids:)
42
+ raise ParseError.new("Push body must be an object", mutation_ids:) unless body.is_a?(Hash)
43
+
44
+ missing = REQUIRED_KEYS.reject { |key| body.key?(key) }
45
+ raise ParseError.new("Push body is missing #{missing.join(', ')}", mutation_ids:) if missing.any?
46
+ raise ParseError.new("clientGroupID must be a string", mutation_ids:) unless body["clientGroupID"].is_a?(String)
47
+ raise ParseError.new("mutations must be an array", mutation_ids:) unless body["mutations"].is_a?(Array)
48
+ raise ParseError.new("pushVersion must be a number", mutation_ids:) unless body["pushVersion"].is_a?(Numeric)
49
+ raise ParseError.new("timestamp must be a number", mutation_ids:) unless body["timestamp"].is_a?(Numeric)
50
+ raise ParseError.new("requestID must be a string", mutation_ids:) unless body["requestID"].is_a?(String)
51
+ end
52
+ private_class_method :validate_body!
53
+
54
+ def self.validate_query!(query, mutation_ids:)
55
+ unless query.is_a?(Hash) && query["schema"].is_a?(String) && query["appID"].is_a?(String)
56
+ raise ParseError.new(
57
+ "Query parameters schema and appID are required",
58
+ mutation_ids:,
59
+ source: :query
60
+ )
61
+ end
62
+
63
+ unless query["schema"].match?(/\A[a-z0-9_]+\z/)
64
+ raise ParseError.new(
65
+ "Query parameter schema is invalid",
66
+ mutation_ids:,
67
+ source: :query
68
+ )
69
+ end
70
+ end
71
+ private_class_method :validate_query!
72
+
73
+ def self.extract_mutation_ids(body)
74
+ return [] unless body.is_a?(Hash) && body["mutations"].is_a?(Array)
75
+
76
+ body["mutations"].filter_map do |mutation|
77
+ next unless mutation.is_a?(Hash)
78
+ next unless mutation["clientID"].is_a?(String) && mutation["id"].is_a?(Numeric)
79
+
80
+ {"clientID" => mutation["clientID"], "id" => mutation["id"]}
81
+ end
82
+ end
83
+ private_class_method :extract_mutation_ids
84
+
85
+ def initialize(**attributes)
86
+ attributes.each { |name, value| instance_variable_set(:"@#{name}", value) }
87
+ end
88
+
89
+ def mutation_ids
90
+ mutations.map(&:identifier)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/security_utils"
4
+
5
+ module ZeroRailsAdapter
6
+ module RequestVerifiers
7
+ class ApiKey
8
+ def initialize(key:, header: "X-Api-Key")
9
+ @key = key.to_s
10
+ @header = header
11
+ end
12
+
13
+ def call(request)
14
+ candidate = request.headers[@header].to_s
15
+ return false if @key.empty? || candidate.bytesize != @key.bytesize
16
+
17
+ ActiveSupport::SecurityUtils.secure_compare(candidate, @key)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZeroRailsAdapter
4
+ module Storage
5
+ class ZeroSchema
6
+ class << self
7
+ def model(base_class:, table_name:, primary_key:)
8
+ key = [base_class, table_name, primary_key]
9
+ models_mutex.synchronize do
10
+ models[key] ||= Class.new(base_class) do
11
+ self.table_name = table_name
12
+ self.primary_key = primary_key
13
+ self.inheritance_column = :_zero_rails_adapter_type
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def models
21
+ @models ||= {}
22
+ end
23
+
24
+ def models_mutex
25
+ @models_mutex ||= Mutex.new
26
+ end
27
+ end
28
+
29
+ attr_reader :request, :transaction_class, :client_model,
30
+ :mutation_result_model
31
+
32
+ def initialize(request:, transaction_class:)
33
+ @request = request
34
+ @transaction_class = transaction_class
35
+ @client_model = self.class.model(
36
+ base_class: transaction_class,
37
+ table_name: "#{request.schema}.clients",
38
+ primary_key: %w[clientGroupID clientID]
39
+ )
40
+ @mutation_result_model = self.class.model(
41
+ base_class: transaction_class,
42
+ table_name: "#{request.schema}.mutations",
43
+ primary_key: %w[clientGroupID clientID mutationID]
44
+ )
45
+ end
46
+
47
+ def transaction(&block)
48
+ transaction_class.transaction(requires_new: true, &block)
49
+ end
50
+
51
+ def increment_lmid!(client_id)
52
+ attributes = client_identity(client_id)
53
+ record = client_model.create_or_find_by!(attributes) do |client|
54
+ client["lastMutationID"] = 1
55
+ end
56
+ created = record.previously_new_record?
57
+ record.lock!
58
+ record.update!("lastMutationID" => record["lastMutationID"].to_i + 1) unless created
59
+ record["lastMutationID"].to_i
60
+ rescue ActiveRecord::RecordNotUnique
61
+ retry
62
+ end
63
+
64
+ def write_result(client_id:, mutation_id:, result:)
65
+ mutation_result_model.create!(
66
+ "clientGroupID" => request.client_group_id,
67
+ "clientID" => client_id,
68
+ "mutationID" => mutation_id,
69
+ "result" => result
70
+ )
71
+ end
72
+
73
+ def cleanup(args)
74
+ scope = mutation_result_model.where("clientGroupID" => args["clientGroupID"])
75
+
76
+ if args["type"] == "bulk"
77
+ scope.where("clientID" => Array(args["clientIDs"])).delete_all
78
+ elsif args["clientID"].is_a?(String) && args["upToMutationID"].is_a?(Numeric)
79
+ scope.where("clientID" => args["clientID"])
80
+ .where('"mutationID" <= ?', args["upToMutationID"])
81
+ .delete_all
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def client_identity(client_id)
88
+ {
89
+ "clientGroupID" => request.client_group_id,
90
+ "clientID" => client_id
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZeroRailsAdapter
4
+ module TypeScript
5
+ class Generator
6
+ SCHEMA_TYPES = {
7
+ string: "string()",
8
+ text: "string()",
9
+ uuid: "string()",
10
+ integer: "number()",
11
+ bigint: "number()",
12
+ float: "number()",
13
+ decimal: "number()",
14
+ boolean: "boolean()",
15
+ date: "number()",
16
+ datetime: "number()",
17
+ timestamp: "number()",
18
+ time: "number()",
19
+ json: "json()",
20
+ jsonb: "json()"
21
+ }.freeze
22
+
23
+ ZOD_TYPES = {
24
+ string: "z.string()",
25
+ text: "z.string()",
26
+ uuid: "z.string().uuid()",
27
+ integer: "z.number()",
28
+ bigint: "z.number()",
29
+ float: "z.number()",
30
+ decimal: "z.number()",
31
+ boolean: "z.boolean()",
32
+ date: "z.number()",
33
+ datetime: "z.number()",
34
+ timestamp: "z.number()",
35
+ time: "z.number()",
36
+ json: "z.json()",
37
+ jsonb: "z.json()"
38
+ }.freeze
39
+
40
+ attr_reader :models
41
+
42
+ def initialize(models: nil)
43
+ @models = Array(models || ZeroRailsAdapter.configuration.model_provider.call)
44
+ .select { |model| active_record_model?(model) }
45
+ .uniq
46
+ .sort_by(&:table_name)
47
+ end
48
+
49
+ def schema
50
+ sections = [
51
+ "import {#{schema_imports.join(', ')}} from '@rocicorp/zero'",
52
+ table_definitions,
53
+ relationship_definitions,
54
+ schema_export
55
+ ]
56
+
57
+ sections.reject(&:empty?).join("\n\n") << "\n"
58
+ end
59
+
60
+ def mutators
61
+ sections = [
62
+ "import {defineMutator, defineMutators} from '@rocicorp/zero'\nimport {z} from 'zod'",
63
+ argument_schemas,
64
+ mutator_export
65
+ ]
66
+
67
+ sections.reject(&:empty?).join("\n\n") << "\n"
68
+ end
69
+
70
+ private
71
+
72
+ def active_record_model?(model)
73
+ model.is_a?(Class) && model < ActiveRecord::Base && !model.abstract_class?
74
+ end
75
+
76
+ def table_definitions
77
+ models.map do |model|
78
+ columns = model.columns.map do |column|
79
+ " #{property_name(column.name)}: #{zero_type(model, column)},"
80
+ end.join("\n")
81
+ keys = primary_keys(model).map { |key| quote(key) }.join(", ")
82
+
83
+ <<~TYPESCRIPT.chomp
84
+ const #{variable_name(model)} = table(#{quote(model.table_name)})
85
+ .columns({
86
+ #{columns}
87
+ })
88
+ .primaryKey(#{keys})
89
+ TYPESCRIPT
90
+ end.join("\n\n")
91
+ end
92
+
93
+ def relationship_definitions
94
+ relationships.group_by(&:first).map do |model, definitions|
95
+ kinds = definitions.map do |_source, reflection, _destination|
96
+ reflection.collection? ? "many" : "one"
97
+ end.uniq.sort.join(", ")
98
+ entries = definitions.map do |_source, reflection, destination|
99
+ source_fields, destination_fields =
100
+ relationship_fields(model, reflection, destination)
101
+ kind = reflection.collection? ? "many" : "one"
102
+
103
+ [
104
+ "#{property_name(reflection.name)}: #{kind}({",
105
+ " sourceField: [#{source_fields.map { |field| quote(field) }.join(', ')}],",
106
+ " destSchema: #{variable_name(destination)},",
107
+ " destField: [#{destination_fields.map { |field| quote(field) }.join(', ')}],",
108
+ "}),"
109
+ ].join("\n")
110
+ end.join("\n")
111
+
112
+ [
113
+ "const #{relationship_variable_name(model)} = relationships(#{variable_name(model)}, ({#{kinds}}) => ({",
114
+ indent(entries, 2),
115
+ "}))"
116
+ ].join("\n")
117
+ end.join("\n\n")
118
+ end
119
+
120
+ def schema_export
121
+ table_vars = models.map { |model| variable_name(model) }.join(", ")
122
+ relation_vars = relationships.map(&:first).uniq.map do |model|
123
+ relationship_variable_name(model)
124
+ end.join(", ")
125
+
126
+ lines = ["export const schema = createSchema({", " tables: [#{table_vars}],"]
127
+ lines << " relationships: [#{relation_vars}]," unless relation_vars.empty?
128
+ lines.concat([
129
+ "})",
130
+ "",
131
+ "export type Schema = typeof schema",
132
+ "",
133
+ "declare module '@rocicorp/zero' {",
134
+ " interface DefaultTypes {",
135
+ " schema: Schema",
136
+ " }",
137
+ "}"
138
+ ])
139
+ lines.join("\n")
140
+ end
141
+
142
+ def argument_schemas
143
+ models.flat_map do |model|
144
+ [
145
+ zod_schema(model, :create),
146
+ zod_schema(model, :update),
147
+ zod_schema(model, :destroy)
148
+ ]
149
+ end.join("\n\n")
150
+ end
151
+
152
+ def zod_schema(model, action)
153
+ columns = mutation_columns(model, action).map do |column|
154
+ " #{property_name(column.name)}: #{zod_type(model, column, action)},"
155
+ end.join("\n")
156
+
157
+ <<~TYPESCRIPT.chomp
158
+ const #{variable_name(model)}#{action.to_s.camelize}Args = z.object({
159
+ #{columns}
160
+ })
161
+ TYPESCRIPT
162
+ end
163
+
164
+ def mutator_export
165
+ tables = models.map do |model|
166
+ variable = variable_name(model)
167
+ table = property_name(model.table_name)
168
+ create_values = create_insert_values(model)
169
+ update_values = update_insert_values(model)
170
+
171
+ [
172
+ "#{table}: {",
173
+ " create: defineMutator(#{variable}CreateArgs, async ({tx, args}) => {",
174
+ " const now = Date.now()",
175
+ " await tx.mutate.#{table}.insert(#{create_values})",
176
+ " }),",
177
+ " update: defineMutator(#{variable}UpdateArgs, async ({tx, args}) => {",
178
+ " await tx.mutate.#{table}.update(#{update_values})",
179
+ " }),",
180
+ " destroy: defineMutator(#{variable}DestroyArgs, async ({tx, args}) => {",
181
+ " await tx.mutate.#{table}.delete(args)",
182
+ " }),",
183
+ "},"
184
+ ].join("\n")
185
+ end.join("\n")
186
+
187
+ [
188
+ "export const mutators = defineMutators({",
189
+ indent(tables, 2),
190
+ "})"
191
+ ].join("\n")
192
+ end
193
+
194
+ def create_insert_values(model)
195
+ additions = []
196
+ additions << "created_at: now" if model.column_names.include?("created_at")
197
+ additions << "updated_at: now" if model.column_names.include?("updated_at")
198
+ defaulted_columns(model).each do |column|
199
+ value = if generated_attribute_names(model, :create).include?(column.name)
200
+ "args.#{property_name(column.name)} ?? #{typescript_default(column)}"
201
+ else
202
+ typescript_default(column)
203
+ end
204
+ additions << "#{property_name(column.name)}: #{value}"
205
+ end
206
+ object_with_additions(additions)
207
+ end
208
+
209
+ def update_insert_values(model)
210
+ additions = []
211
+ additions << "updated_at: Date.now()" if model.column_names.include?("updated_at")
212
+ object_with_additions(additions)
213
+ end
214
+
215
+ def object_with_additions(additions)
216
+ return "args" if additions.empty?
217
+
218
+ "{...args, #{additions.join(', ')}}"
219
+ end
220
+
221
+ def mutation_columns(model, action)
222
+ primary = primary_keys(model)
223
+ case action
224
+ when :create
225
+ allowed = generated_attribute_names(model, action)
226
+ model.columns.select do |column|
227
+ !timestamp_column?(column) &&
228
+ (primary.include?(column.name) || allowed.include?(column.name))
229
+ end
230
+ when :update
231
+ allowed = generated_attribute_names(model, action)
232
+ model.columns.select do |column|
233
+ primary.include?(column.name) ||
234
+ (!timestamp_column?(column) && allowed.include?(column.name))
235
+ end
236
+ when :destroy
237
+ model.columns.select { |column| primary.include?(column.name) }
238
+ end
239
+ end
240
+
241
+ def generated_attribute_names(model, action)
242
+ Array(
243
+ ZeroRailsAdapter.configuration.generated_attributes.call(model, action)
244
+ ).map(&:to_s)
245
+ end
246
+
247
+ def timestamp_column?(column)
248
+ %w[created_at updated_at].include?(column.name)
249
+ end
250
+
251
+ def defaulted_columns(model)
252
+ primary = primary_keys(model)
253
+ model.columns.select do |column|
254
+ !primary.include?(column.name) &&
255
+ !timestamp_column?(column) &&
256
+ !column.null &&
257
+ !column.default.nil?
258
+ end
259
+ end
260
+
261
+ def zero_type(model, column)
262
+ if column.respond_to?(:array) && column.array
263
+ type = "json<readonly #{typescript_scalar(column)}[]>()"
264
+ return column.null ? "#{type}.optional()" : type
265
+ end
266
+
267
+ type = enum_values(model, column)&.then do |values|
268
+ "enumeration<#{values.map { |value| quote(value) }.join(' | ')}>()"
269
+ end
270
+ type ||= mapped_type(SCHEMA_TYPES, model, column)
271
+ type += ".optional()" if column.null
272
+ type
273
+ end
274
+
275
+ def zod_type(model, column, action)
276
+ values = enum_values(model, column)
277
+ type = if column.respond_to?(:array) && column.array
278
+ "z.array(#{mapped_type(ZOD_TYPES, model, column)})"
279
+ elsif values
280
+ "z.enum([#{values.map { |value| quote(value) }.join(', ')}])"
281
+ else
282
+ mapped_type(ZOD_TYPES, model, column)
283
+ end
284
+
285
+ if column.null
286
+ "#{type}.nullish()"
287
+ elsif action == :update && !primary_keys(model).include?(column.name)
288
+ "#{type}.optional()"
289
+ elsif action == :create && !column.default.nil?
290
+ "#{type}.optional()"
291
+ else
292
+ type
293
+ end
294
+ end
295
+
296
+ def mapped_type(mapping, model, column)
297
+ mapping.fetch(column.type.to_sym) do
298
+ raise UnsupportedColumnTypeError,
299
+ "Cannot generate Zero TypeScript for #{model.name}.#{column.name} (#{column.type})"
300
+ end
301
+ end
302
+
303
+ def enum_values(model, column)
304
+ definition = model.defined_enums[column.name]
305
+ definition&.keys
306
+ end
307
+
308
+ def schema_imports
309
+ imports = %w[createSchema table]
310
+ imports << "relationships" if relationships.any?
311
+ imports << "enumeration" if models.any? { |model| model.defined_enums.any? }
312
+ models.each do |model|
313
+ model.columns.each do |column|
314
+ imports << if enum_values(model, column)
315
+ "enumeration"
316
+ elsif column.respond_to?(:array) && column.array
317
+ "json"
318
+ else
319
+ mapped_type(SCHEMA_TYPES, model, column).delete_suffix("()")
320
+ end
321
+ end
322
+ end
323
+ imports.uniq.sort
324
+ end
325
+
326
+ def typescript_scalar(column)
327
+ case column.type.to_sym
328
+ when :string, :text, :uuid then "string"
329
+ when :integer, :bigint, :float, :decimal, :date, :datetime, :timestamp, :time
330
+ "number"
331
+ when :boolean then "boolean"
332
+ else "unknown"
333
+ end
334
+ end
335
+
336
+ def relationships
337
+ @relationships ||= models.flat_map do |model|
338
+ model.reflect_on_all_associations.filter_map do |reflection|
339
+ next if reflection.polymorphic? || reflection.options[:through]
340
+
341
+ destination = reflection.klass
342
+ [model, reflection, destination] if models.include?(destination)
343
+ rescue NameError
344
+ nil
345
+ end
346
+ end
347
+ end
348
+
349
+ def relationship_fields(model, reflection, destination)
350
+ if reflection.belongs_to?
351
+ [
352
+ Array(reflection.foreign_key).map(&:to_s),
353
+ association_primary_keys(reflection, destination)
354
+ ]
355
+ else
356
+ [
357
+ association_primary_keys(reflection, model),
358
+ Array(reflection.foreign_key).map(&:to_s)
359
+ ]
360
+ end
361
+ end
362
+
363
+ def association_primary_keys(reflection, model)
364
+ configured = reflection.options[:primary_key]
365
+ Array(configured || model.primary_key).map(&:to_s)
366
+ end
367
+
368
+ def primary_keys(model)
369
+ keys = Array(model.primary_key).compact.map(&:to_s)
370
+ return keys if keys.any?
371
+
372
+ raise UnsupportedColumnTypeError, "#{model.name} has no primary key"
373
+ end
374
+
375
+ def relationship_variable_name(model)
376
+ "#{variable_name(model)}Relationships"
377
+ end
378
+
379
+ def variable_name(model)
380
+ model.table_name.gsub(/[^a-zA-Z0-9_]/, "_").camelize(:lower)
381
+ end
382
+
383
+ def property_name(value)
384
+ string = value.to_s
385
+ string.match?(/\A[$A-Z_a-z][$\w]*\z/) ? string : quote(string)
386
+ end
387
+
388
+ def quote(value)
389
+ "'#{value.to_s.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'")}'"
390
+ end
391
+
392
+ def typescript_literal(value)
393
+ case value
394
+ when String then quote(value)
395
+ when Numeric then value.to_s
396
+ when true then "true"
397
+ when false then "false"
398
+ else
399
+ raise UnsupportedColumnTypeError, "Cannot serialize database default #{value.inspect}"
400
+ end
401
+ end
402
+
403
+ def typescript_default(column)
404
+ value = column.default
405
+ case column.type.to_sym
406
+ when :boolean
407
+ ActiveModel::Type::Boolean.new.cast(value) ? "true" : "false"
408
+ when :integer, :bigint, :float, :decimal
409
+ value.to_s
410
+ else
411
+ typescript_literal(value)
412
+ end
413
+ end
414
+
415
+ def indent(value, spaces)
416
+ prefix = " " * spaces
417
+ value.lines.map { |line| "#{prefix}#{line}" }.join.chomp
418
+ end
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZeroRailsAdapter
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_record"
5
+ require "active_model"
6
+ require "active_support"
7
+ require "active_support/core_ext/hash/keys"
8
+ require "active_support/core_ext/string/inflections"
9
+ require_relative "zero_rails_adapter/version"
10
+ require_relative "zero_rails_adapter/errors"
11
+ require_relative "zero_rails_adapter/identity"
12
+ require_relative "zero_rails_adapter/context"
13
+ require_relative "zero_rails_adapter/configuration"
14
+ require_relative "zero_rails_adapter/request_verifiers/api_key"
15
+ require_relative "zero_rails_adapter/registry"
16
+ require_relative "zero_rails_adapter/mutator"
17
+ require_relative "zero_rails_adapter/mutation"
18
+ require_relative "zero_rails_adapter/request"
19
+ require_relative "zero_rails_adapter/crud/dispatcher"
20
+ require_relative "zero_rails_adapter/type_script/generator"
21
+ require_relative "zero_rails_adapter/storage/zero_schema"
22
+ require_relative "zero_rails_adapter/processor"
23
+ require_relative "zero_rails_adapter/engine"
24
+
25
+ module ZeroRailsAdapter
26
+ class << self
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield configuration
33
+ end
34
+
35
+ def reset_configuration!
36
+ @configuration = Configuration.new
37
+ end
38
+
39
+ def registry
40
+ @registry ||= Registry.new
41
+ end
42
+
43
+ def define_mutator(name, superclass: Mutator, &definition)
44
+ Class.new(superclass).tap do |mutator_class|
45
+ mutator_class.mutation_name(name)
46
+ mutator_class.class_eval(&definition)
47
+ end
48
+ end
49
+ end
50
+ end