zero_ruby 0.1.0.alpha1
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/CHANGELOG.md +0 -0
- data/LICENSE.txt +21 -0
- data/README.md +245 -0
- data/lib/zero_ruby/argument.rb +75 -0
- data/lib/zero_ruby/configuration.rb +57 -0
- data/lib/zero_ruby/errors.rb +90 -0
- data/lib/zero_ruby/input_object.rb +121 -0
- data/lib/zero_ruby/lmid_store.rb +43 -0
- data/lib/zero_ruby/lmid_stores/active_record_store.rb +63 -0
- data/lib/zero_ruby/mutation.rb +141 -0
- data/lib/zero_ruby/push_processor.rb +126 -0
- data/lib/zero_ruby/schema.rb +124 -0
- data/lib/zero_ruby/type_names.rb +24 -0
- data/lib/zero_ruby/types/base_type.rb +54 -0
- data/lib/zero_ruby/types/big_int.rb +32 -0
- data/lib/zero_ruby/types/boolean.rb +30 -0
- data/lib/zero_ruby/types/float.rb +31 -0
- data/lib/zero_ruby/types/id.rb +33 -0
- data/lib/zero_ruby/types/integer.rb +31 -0
- data/lib/zero_ruby/types/iso8601_date.rb +43 -0
- data/lib/zero_ruby/types/iso8601_date_time.rb +43 -0
- data/lib/zero_ruby/types/string.rb +20 -0
- data/lib/zero_ruby/typescript_generator.rb +192 -0
- data/lib/zero_ruby/validator.rb +69 -0
- data/lib/zero_ruby/validators/allow_blank_validator.rb +31 -0
- data/lib/zero_ruby/validators/allow_null_validator.rb +26 -0
- data/lib/zero_ruby/validators/exclusion_validator.rb +29 -0
- data/lib/zero_ruby/validators/format_validator.rb +35 -0
- data/lib/zero_ruby/validators/inclusion_validator.rb +30 -0
- data/lib/zero_ruby/validators/length_validator.rb +42 -0
- data/lib/zero_ruby/validators/numericality_validator.rb +63 -0
- data/lib/zero_ruby/version.rb +5 -0
- data/lib/zero_ruby/zero_client.rb +25 -0
- data/lib/zero_ruby.rb +87 -0
- metadata +145 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "base_type"
|
|
5
|
+
|
|
6
|
+
module ZeroRuby
|
|
7
|
+
module Types
|
|
8
|
+
# ISO8601Date type for date values.
|
|
9
|
+
# Accepts ISO8601 formatted date strings, Date, Time, and DateTime objects.
|
|
10
|
+
# Coerces to Date.
|
|
11
|
+
class ISO8601Date < BaseType
|
|
12
|
+
class << self
|
|
13
|
+
def name
|
|
14
|
+
"ISO8601Date"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def coerce_input(value, _ctx = nil)
|
|
18
|
+
return nil if value.nil?
|
|
19
|
+
|
|
20
|
+
case value
|
|
21
|
+
when ::Date
|
|
22
|
+
value
|
|
23
|
+
when ::Time, ::DateTime
|
|
24
|
+
value.to_date
|
|
25
|
+
when ::String
|
|
26
|
+
coercion_error!(value, "empty string is not a valid #{name}") if value.empty?
|
|
27
|
+
parse_iso8601_date(value)
|
|
28
|
+
else
|
|
29
|
+
coercion_error!(value, "#{format_value_for_error(value)} (#{value.class}) cannot be coerced to #{name}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse_iso8601_date(value)
|
|
36
|
+
Date.iso8601(value)
|
|
37
|
+
rescue ArgumentError
|
|
38
|
+
coercion_error!(value, "#{format_value_for_error(value)} is not a valid ISO8601 date")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "base_type"
|
|
5
|
+
|
|
6
|
+
module ZeroRuby
|
|
7
|
+
module Types
|
|
8
|
+
# ISO8601DateTime type for datetime values.
|
|
9
|
+
# Accepts ISO8601 formatted strings, Time, and DateTime objects.
|
|
10
|
+
# Coerces to Time.
|
|
11
|
+
class ISO8601DateTime < BaseType
|
|
12
|
+
class << self
|
|
13
|
+
def name
|
|
14
|
+
"ISO8601DateTime"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def coerce_input(value, _ctx = nil)
|
|
18
|
+
return nil if value.nil?
|
|
19
|
+
|
|
20
|
+
case value
|
|
21
|
+
when ::Time
|
|
22
|
+
value
|
|
23
|
+
when ::DateTime
|
|
24
|
+
value.to_time
|
|
25
|
+
when ::String
|
|
26
|
+
coercion_error!(value, "empty string is not a valid #{name}") if value.empty?
|
|
27
|
+
parse_iso8601(value)
|
|
28
|
+
else
|
|
29
|
+
coercion_error!(value, "#{format_value_for_error(value)} (#{value.class}) cannot be coerced to #{name}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse_iso8601(value)
|
|
36
|
+
Time.iso8601(value)
|
|
37
|
+
rescue ArgumentError
|
|
38
|
+
coercion_error!(value, "#{format_value_for_error(value)} is not a valid ISO8601 datetime")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_type"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Types
|
|
7
|
+
class String < BaseType
|
|
8
|
+
class << self
|
|
9
|
+
def name
|
|
10
|
+
"String"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def coerce_input(value, _ctx = nil)
|
|
14
|
+
return nil if value.nil?
|
|
15
|
+
value.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZeroRuby
|
|
4
|
+
# Generates TypeScript type definitions from registered mutations.
|
|
5
|
+
# Similar to graphql-codegen, this introspects mutation argument definitions
|
|
6
|
+
# and generates corresponding TypeScript interfaces.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# TypeScriptGenerator.new(ZeroSchema).generate
|
|
10
|
+
# # => "// Auto-generated by zero-ruby - do not edit\n\nexport interface ..."
|
|
11
|
+
class TypeScriptGenerator
|
|
12
|
+
TYPE_MAP = {
|
|
13
|
+
"ZeroRuby::Types::String" => "string",
|
|
14
|
+
"ZeroRuby::Types::Integer" => "number",
|
|
15
|
+
"ZeroRuby::Types::Float" => "number",
|
|
16
|
+
"ZeroRuby::Types::Boolean" => "boolean",
|
|
17
|
+
"ZeroRuby::Types::ID" => "string",
|
|
18
|
+
"ZeroRuby::Types::BigInt" => "number",
|
|
19
|
+
"ZeroRuby::Types::ISO8601Date" => "string",
|
|
20
|
+
"ZeroRuby::Types::ISO8601DateTime" => "string"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(schema)
|
|
24
|
+
@schema = schema
|
|
25
|
+
@input_objects = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Generate TypeScript definitions
|
|
29
|
+
# @return [String] Complete TypeScript type definitions
|
|
30
|
+
def generate
|
|
31
|
+
collect_input_objects
|
|
32
|
+
|
|
33
|
+
parts = [
|
|
34
|
+
generate_header,
|
|
35
|
+
generate_scalars,
|
|
36
|
+
generate_input_objects,
|
|
37
|
+
generate_mutation_args,
|
|
38
|
+
generate_mutation_map
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
parts.compact.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def generate_header
|
|
47
|
+
<<~TS
|
|
48
|
+
// Auto-generated by zero-ruby - do not edit
|
|
49
|
+
// Generated at: #{Time.now.utc.iso8601}
|
|
50
|
+
TS
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_scalars
|
|
54
|
+
<<~TS
|
|
55
|
+
|
|
56
|
+
/** Scalar type mappings */
|
|
57
|
+
export type Scalars = {
|
|
58
|
+
String: string;
|
|
59
|
+
Integer: number;
|
|
60
|
+
Float: number;
|
|
61
|
+
Boolean: boolean;
|
|
62
|
+
};
|
|
63
|
+
TS
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def collect_input_objects
|
|
67
|
+
@schema.mutations.each do |_name, handler_class|
|
|
68
|
+
collect_input_objects_from_arguments(handler_class.arguments)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def collect_input_objects_from_arguments(arguments)
|
|
73
|
+
arguments.each do |_name, arg|
|
|
74
|
+
if input_object_type?(arg.type)
|
|
75
|
+
type_name = extract_type_name(arg.type)
|
|
76
|
+
unless @input_objects.key?(type_name)
|
|
77
|
+
@input_objects[type_name] = arg.type
|
|
78
|
+
# Recursively collect nested InputObjects
|
|
79
|
+
collect_input_objects_from_arguments(arg.type.arguments)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generate_input_objects
|
|
86
|
+
return nil if @input_objects.empty?
|
|
87
|
+
|
|
88
|
+
interfaces = @input_objects.map do |name, klass|
|
|
89
|
+
generate_interface(name, klass.arguments)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
"\n" + interfaces.join("\n\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def generate_mutation_args
|
|
96
|
+
return nil if @schema.mutations.empty?
|
|
97
|
+
|
|
98
|
+
interfaces = @schema.mutations.map do |name, handler_class|
|
|
99
|
+
interface_name = to_interface_name(name)
|
|
100
|
+
generate_interface(interface_name, handler_class.arguments)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
"\n" + interfaces.join("\n\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def generate_interface(name, arguments)
|
|
107
|
+
if arguments.empty?
|
|
108
|
+
return <<~TS.strip
|
|
109
|
+
/** #{name} */
|
|
110
|
+
export interface #{name} {}
|
|
111
|
+
TS
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
fields = arguments.map do |arg_name, arg|
|
|
115
|
+
optional = arg.optional? ? "?" : ""
|
|
116
|
+
ts_type = resolve_type(arg.type)
|
|
117
|
+
description = arg.description ? " /** #{arg.description} */\n" : ""
|
|
118
|
+
"#{description} #{to_camel_case(arg_name)}#{optional}: #{ts_type};"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
<<~TS.strip
|
|
122
|
+
/** #{name} */
|
|
123
|
+
export interface #{name} {
|
|
124
|
+
#{fields.join("\n")}
|
|
125
|
+
}
|
|
126
|
+
TS
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def generate_mutation_map
|
|
130
|
+
return nil if @schema.mutations.empty?
|
|
131
|
+
|
|
132
|
+
entries = @schema.mutations.map do |name, _handler_class|
|
|
133
|
+
interface_name = to_interface_name(name)
|
|
134
|
+
" \"#{name}\": #{interface_name};"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
<<~TS
|
|
138
|
+
|
|
139
|
+
/** All mutations mapped by name */
|
|
140
|
+
export interface MutationArgs {
|
|
141
|
+
#{entries.join("\n")}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type MutationName = keyof MutationArgs;
|
|
145
|
+
export type ArgsFor<T extends MutationName> = MutationArgs[T];
|
|
146
|
+
TS
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def resolve_type(type)
|
|
150
|
+
type_key = type.to_s
|
|
151
|
+
|
|
152
|
+
if TYPE_MAP.key?(type_key)
|
|
153
|
+
TYPE_MAP[type_key]
|
|
154
|
+
elsif input_object_type?(type)
|
|
155
|
+
extract_type_name(type)
|
|
156
|
+
else
|
|
157
|
+
# Unknown type, default to unknown
|
|
158
|
+
"unknown"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def input_object_type?(type)
|
|
163
|
+
defined?(ZeroRuby::InputObject) && type.is_a?(Class) && type < ZeroRuby::InputObject
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extract_type_name(type)
|
|
167
|
+
# Get just the class name without module prefixes
|
|
168
|
+
type.name.split("::").last
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Convert mutation name to interface name
|
|
172
|
+
# "todo.create" -> "TodoCreateArgs"
|
|
173
|
+
# "posts.bulk_update" -> "PostsBulkUpdateArgs"
|
|
174
|
+
def to_interface_name(mutation_name)
|
|
175
|
+
parts = mutation_name.to_s.split(".")
|
|
176
|
+
pascal = parts.map { |part| to_pascal_case(part) }.join
|
|
177
|
+
"#{pascal}Args"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Convert snake_case to PascalCase
|
|
181
|
+
def to_pascal_case(str)
|
|
182
|
+
str.to_s.split("_").map(&:capitalize).join
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Convert snake_case to camelCase
|
|
186
|
+
def to_camel_case(name)
|
|
187
|
+
parts = name.to_s.split("_")
|
|
188
|
+
return parts.first if parts.length == 1
|
|
189
|
+
parts.first + parts[1..].map(&:capitalize).join
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZeroRuby
|
|
4
|
+
# Base class for argument validators.
|
|
5
|
+
# Inspired by graphql-ruby's validation system.
|
|
6
|
+
class Validator
|
|
7
|
+
class << self
|
|
8
|
+
# Registry of validator classes by name
|
|
9
|
+
def validators
|
|
10
|
+
@validators ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a validator class
|
|
14
|
+
def register(name, klass)
|
|
15
|
+
validators[name.to_sym] = klass
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get a validator class by name
|
|
19
|
+
def get(name)
|
|
20
|
+
validators[name.to_sym]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Run all validations on a value
|
|
24
|
+
# @param validators_config [Hash] Configuration hash for validators
|
|
25
|
+
# @param mutation [ZeroRuby::Mutation] The mutation instance
|
|
26
|
+
# @param ctx [Hash] The context hash
|
|
27
|
+
# @param value [Object] The value to validate
|
|
28
|
+
# @return [Array<String>] Array of error messages
|
|
29
|
+
def validate!(validators_config, mutation, ctx, value)
|
|
30
|
+
return [] if validators_config.nil? || validators_config.empty?
|
|
31
|
+
|
|
32
|
+
errors = []
|
|
33
|
+
|
|
34
|
+
validators_config.each do |validator_name, config|
|
|
35
|
+
validator_class = get(validator_name)
|
|
36
|
+
next unless validator_class
|
|
37
|
+
|
|
38
|
+
validator = validator_class.new(config)
|
|
39
|
+
result = validator.validate(mutation, ctx, value)
|
|
40
|
+
errors.concat(Array(result)) if result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
errors
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :config
|
|
48
|
+
|
|
49
|
+
def initialize(config)
|
|
50
|
+
@config = config
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Validate a value
|
|
54
|
+
# @param mutation [ZeroRuby::Mutation] The mutation instance
|
|
55
|
+
# @param ctx [Hash] The context hash
|
|
56
|
+
# @param value [Object] The value to validate
|
|
57
|
+
# @return [String, Array<String>, nil] Error message(s) or nil if valid
|
|
58
|
+
def validate(mutation, ctx, value)
|
|
59
|
+
raise NotImplementedError, "Subclasses must implement #validate"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
protected
|
|
63
|
+
|
|
64
|
+
# Helper to format error messages
|
|
65
|
+
def error_message(message)
|
|
66
|
+
message
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that a value is not blank when allow_blank is false.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { allow_blank: false }
|
|
11
|
+
class AllowBlankValidator < Validator
|
|
12
|
+
def validate(mutation, ctx, value)
|
|
13
|
+
# If allow_blank is true (or truthy), blank values are allowed
|
|
14
|
+
return nil if config == true || config
|
|
15
|
+
|
|
16
|
+
# Check if value is blank (nil, empty string, or whitespace-only string)
|
|
17
|
+
is_blank = value.nil? ||
|
|
18
|
+
(value.respond_to?(:empty?) && value.empty?) ||
|
|
19
|
+
(value.is_a?(::String) && value.strip.empty?)
|
|
20
|
+
|
|
21
|
+
if is_blank
|
|
22
|
+
return "can't be blank"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Validator.register(:allow_blank, AllowBlankValidator)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that a value is not null when allow_null is false.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { allow_null: false }
|
|
11
|
+
class AllowNullValidator < Validator
|
|
12
|
+
def validate(mutation, ctx, value)
|
|
13
|
+
# If allow_null is true (or truthy), null values are allowed
|
|
14
|
+
return nil if config == true || config
|
|
15
|
+
|
|
16
|
+
if value.nil?
|
|
17
|
+
return "can't be null"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Validator.register(:allow_null, AllowNullValidator)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that a value is NOT included in a given set.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { exclusion: { in: ["admin", "root", "system"] } }
|
|
11
|
+
class ExclusionValidator < Validator
|
|
12
|
+
def validate(mutation, ctx, value)
|
|
13
|
+
return nil if value.nil?
|
|
14
|
+
|
|
15
|
+
excluded = config[:in] || config[:within]
|
|
16
|
+
return nil unless excluded
|
|
17
|
+
|
|
18
|
+
if excluded.respond_to?(:include?) ? excluded.include?(value) : excluded.cover?(value)
|
|
19
|
+
message = config[:message] || "is reserved"
|
|
20
|
+
return message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Validator.register(:exclusion, ExclusionValidator)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that a value matches a regular expression.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { format: { with: /\A[a-z0-9_]+\z/ } }
|
|
11
|
+
# validates: { format: { without: /[<>]/ } }
|
|
12
|
+
class FormatValidator < Validator
|
|
13
|
+
def validate(mutation, ctx, value)
|
|
14
|
+
return nil if value.nil?
|
|
15
|
+
|
|
16
|
+
str_value = value.to_s
|
|
17
|
+
errors = []
|
|
18
|
+
|
|
19
|
+
if config[:with] && !str_value.match?(config[:with])
|
|
20
|
+
message = config[:message] || "is invalid"
|
|
21
|
+
errors << message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if config[:without] && str_value.match?(config[:without])
|
|
25
|
+
message = config[:message] || "is invalid"
|
|
26
|
+
errors << message
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
errors.empty? ? nil : errors
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Validator.register(:format, FormatValidator)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates that a value is included in a given set.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { inclusion: { in: ["draft", "published", "archived"] } }
|
|
11
|
+
# validates: { inclusion: { in: 1..10 } }
|
|
12
|
+
class InclusionValidator < Validator
|
|
13
|
+
def validate(mutation, ctx, value)
|
|
14
|
+
return nil if value.nil?
|
|
15
|
+
|
|
16
|
+
allowed = config[:in] || config[:within]
|
|
17
|
+
return nil unless allowed
|
|
18
|
+
|
|
19
|
+
unless allowed.respond_to?(:include?) ? allowed.include?(value) : allowed.cover?(value)
|
|
20
|
+
message = config[:message] || "is not included in the list"
|
|
21
|
+
return message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Validator.register(:inclusion, InclusionValidator)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates the length of a string or array value.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { length: { minimum: 1, maximum: 200 } }
|
|
11
|
+
# validates: { length: { is: 10 } }
|
|
12
|
+
# validates: { length: { in: 5..10 } }
|
|
13
|
+
class LengthValidator < Validator
|
|
14
|
+
def validate(mutation, ctx, value)
|
|
15
|
+
return nil if value.nil?
|
|
16
|
+
|
|
17
|
+
length = value.respond_to?(:length) ? value.length : value.to_s.length
|
|
18
|
+
errors = []
|
|
19
|
+
|
|
20
|
+
if config[:minimum] && length < config[:minimum]
|
|
21
|
+
errors << "is too short (minimum is #{config[:minimum]})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if config[:maximum] && length > config[:maximum]
|
|
25
|
+
errors << "is too long (maximum is #{config[:maximum]})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if config[:is] && length != config[:is]
|
|
29
|
+
errors << "is the wrong length (should be #{config[:is]})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if config[:in] && !config[:in].cover?(length)
|
|
33
|
+
errors << "length is not in #{config[:in]}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
errors.empty? ? nil : errors
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Validator.register(:length, LengthValidator)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validator"
|
|
4
|
+
|
|
5
|
+
module ZeroRuby
|
|
6
|
+
module Validators
|
|
7
|
+
# Validates numeric constraints on a value.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# validates: { numericality: { greater_than: 0 } }
|
|
11
|
+
# validates: { numericality: { less_than_or_equal_to: 100 } }
|
|
12
|
+
# validates: { numericality: { equal_to: 42 } }
|
|
13
|
+
# validates: { numericality: { odd: true } }
|
|
14
|
+
# validates: { numericality: { even: true } }
|
|
15
|
+
class NumericalityValidator < Validator
|
|
16
|
+
def validate(mutation, ctx, value)
|
|
17
|
+
return nil if value.nil?
|
|
18
|
+
|
|
19
|
+
unless value.is_a?(Numeric)
|
|
20
|
+
return "is not a number"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
errors = []
|
|
24
|
+
|
|
25
|
+
if config[:greater_than] && value <= config[:greater_than]
|
|
26
|
+
errors << "must be greater than #{config[:greater_than]}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if config[:greater_than_or_equal_to] && value < config[:greater_than_or_equal_to]
|
|
30
|
+
errors << "must be greater than or equal to #{config[:greater_than_or_equal_to]}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if config[:less_than] && value >= config[:less_than]
|
|
34
|
+
errors << "must be less than #{config[:less_than]}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if config[:less_than_or_equal_to] && value > config[:less_than_or_equal_to]
|
|
38
|
+
errors << "must be less than or equal to #{config[:less_than_or_equal_to]}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if config[:equal_to] && value != config[:equal_to]
|
|
42
|
+
errors << "must be equal to #{config[:equal_to]}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if config[:other_than] && value == config[:other_than]
|
|
46
|
+
errors << "must be other than #{config[:other_than]}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if config[:odd] && value.to_i.even?
|
|
50
|
+
errors << "must be odd"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if config[:even] && value.to_i.odd?
|
|
54
|
+
errors << "must be even"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
errors.empty? ? nil : errors
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Validator.register(:numericality, NumericalityValidator)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZeroRuby
|
|
4
|
+
# ZeroClient model for LMID (Last Mutation ID) tracking.
|
|
5
|
+
# This model interfaces with Zero's zero_0.clients table, which is
|
|
6
|
+
# automatically created and managed by zero-cache.
|
|
7
|
+
#
|
|
8
|
+
# Table Schema (created by zero-cache):
|
|
9
|
+
# clientGroupID TEXT - The client group identifier
|
|
10
|
+
# clientID TEXT - The client identifier
|
|
11
|
+
# lastMutationID INTEGER - The last processed mutation ID for this client
|
|
12
|
+
# userID TEXT - The user identifier (optional)
|
|
13
|
+
#
|
|
14
|
+
# @note Do NOT run migrations for this table - zero-cache manages it.
|
|
15
|
+
# @see https://zero.rocicorp.dev/docs/mutators
|
|
16
|
+
class ZeroClient < ActiveRecord::Base
|
|
17
|
+
self.table_name = "zero_0.clients"
|
|
18
|
+
self.primary_key = nil # Composite key: clientGroupID + clientID
|
|
19
|
+
|
|
20
|
+
def readonly?
|
|
21
|
+
# Allow updates through the LMID store's direct SQL
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|