edgeway 0.1.0 → 0.3.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +784 -125
  4. data/js/cloudflare-d1-sessions.js +68 -0
  5. data/js/cloudflare-kv.js +47 -0
  6. data/js/cloudflare-ratelimit.js +31 -0
  7. data/js/cloudflare-test-database.js +112 -0
  8. data/js/cloudflare-test-runner.js +76 -0
  9. data/js/cloudflare.js +144 -22
  10. data/js/cors.js +15 -16
  11. data/js/fetch.js +113 -0
  12. data/js/headers.js +16 -0
  13. data/js/jspi.js +32 -0
  14. data/js/jwt.js +6 -40
  15. data/js/runtime.js +42 -5
  16. data/lib/edgeway/application.rb +21 -9
  17. data/lib/edgeway/cache/limits.rb +8 -0
  18. data/lib/edgeway/cache/memory_store.rb +24 -0
  19. data/lib/edgeway/cache/null_store.rb +13 -0
  20. data/lib/edgeway/cache/store.rb +189 -0
  21. data/lib/edgeway/cache.rb +13 -0
  22. data/lib/edgeway/cli/build/app_config.rb +39 -0
  23. data/lib/edgeway/cli/build/app_files.rb +40 -0
  24. data/lib/edgeway/cli/build/cache_config.rb +108 -0
  25. data/lib/edgeway/cli/build/database_config.rb +96 -0
  26. data/lib/edgeway/cli/build/js_files.rb +30 -0
  27. data/lib/edgeway/cli/build/view_compiler.rb +191 -0
  28. data/lib/edgeway/cli/build.rb +50 -18
  29. data/lib/edgeway/cli/cloudflare_test/wasm_test_cli.rb +35 -0
  30. data/lib/edgeway/cli/cloudflare_test.rb +120 -0
  31. data/lib/edgeway/cli/generate.rb +174 -26
  32. data/lib/edgeway/cli/new.rb +1 -1
  33. data/lib/edgeway/cli/test.rb +28 -9
  34. data/lib/edgeway/cli/toolchain.rb +5 -0
  35. data/lib/edgeway/cli.rb +7 -4
  36. data/lib/edgeway/controller/base.rb +32 -3
  37. data/lib/edgeway/controller/caching.rb +140 -0
  38. data/lib/edgeway/controller/callbacks.rb +5 -1
  39. data/lib/edgeway/controller/dispatcher.rb +11 -1
  40. data/lib/edgeway/controller/rate_limiting.rb +145 -0
  41. data/lib/edgeway/controller/rendering.rb +24 -4
  42. data/lib/edgeway/db/adapter.rb +38 -0
  43. data/lib/edgeway/db/memory.rb +9 -2
  44. data/lib/edgeway/db/query_stats.rb +42 -0
  45. data/lib/edgeway/db/sqlite.rb +73 -9
  46. data/lib/edgeway/errors.rb +43 -0
  47. data/lib/edgeway/generators/app/templates/.github/workflows/ci.yml.tt +38 -0
  48. data/lib/edgeway/generators/app/templates/.ruby-version.tt +1 -0
  49. data/lib/edgeway/generators/app/templates/README.md.tt +8 -2
  50. data/lib/edgeway/generators/app/templates/app/views/layouts/application.html.erb +11 -0
  51. data/lib/edgeway/generators/app/templates/bin/build.tt +1 -1
  52. data/lib/edgeway/generators/app/templates/bin/deploy.tt +1 -1
  53. data/lib/edgeway/generators/app/templates/bin/setup.tt +2 -3
  54. data/lib/edgeway/generators/app/templates/bin/test.tt +7 -0
  55. data/lib/edgeway/generators/app/templates/config/cache.yml +22 -0
  56. data/lib/edgeway/generators/app/templates/config/database.yml +25 -2
  57. data/lib/edgeway/generators/app/templates/mise.toml.tt +1 -1
  58. data/lib/edgeway/generators/app/templates/public/.assetsignore +1 -0
  59. data/lib/edgeway/generators/app/templates/wrangler.toml.tt +39 -0
  60. data/lib/edgeway/http/auth/stub.rb +1 -3
  61. data/lib/edgeway/http/client.rb +80 -0
  62. data/lib/edgeway/http/request.rb +7 -22
  63. data/lib/edgeway/http/stub_transport.rb +38 -0
  64. data/lib/edgeway/inflector.rb +19 -0
  65. data/lib/edgeway/logger.rb +33 -5
  66. data/lib/edgeway/model/attributes.rb +93 -10
  67. data/lib/edgeway/model/base.rb +50 -5
  68. data/lib/edgeway/model/connection_registry.rb +25 -0
  69. data/lib/edgeway/model/macros.rb +5 -1
  70. data/lib/edgeway/model/persistence.rb +18 -11
  71. data/lib/edgeway/model/relation.rb +106 -56
  72. data/lib/edgeway/model/validations.rb +29 -23
  73. data/lib/edgeway/rate_limit/limiter.rb +9 -0
  74. data/lib/edgeway/rate_limit/null_limiter.rb +9 -0
  75. data/lib/edgeway/rate_limit/stub_limiter.rb +24 -0
  76. data/lib/edgeway/rate_limit.rb +2 -0
  77. data/lib/edgeway/routing/route.rb +1 -6
  78. data/lib/edgeway/server.rb +13 -0
  79. data/lib/edgeway/service/cloudflare/d1.rb +44 -30
  80. data/lib/edgeway/service/cloudflare/jwt.rb +3 -15
  81. data/lib/edgeway/service/cloudflare/kv.rb +45 -0
  82. data/lib/edgeway/service/cloudflare/kv_store.rb +58 -0
  83. data/lib/edgeway/service/cloudflare/rate_limit.rb +27 -0
  84. data/lib/edgeway/service/cloudflare/rate_limiter.rb +33 -0
  85. data/lib/edgeway/service/cloudflare/test_databases.rb +65 -0
  86. data/lib/edgeway/service/fetch.rb +26 -0
  87. data/lib/edgeway/service/import_result.rb +24 -0
  88. data/lib/edgeway/test/sqlite_databases.rb +18 -0
  89. data/lib/edgeway/test.rb +86 -26
  90. data/lib/edgeway/version.rb +1 -1
  91. data/lib/edgeway/view.rb +24 -0
  92. data/lib/edgeway.rb +9 -0
  93. metadata +50 -4
  94. data/lib/edgeway/cli/app_config.rb +0 -35
  95. data/lib/edgeway/cli/app_files.rb +0 -37
@@ -6,13 +6,13 @@ module Edgeway
6
6
  def valid?
7
7
  @errors = []
8
8
  @raw.each do |col, raw|
9
- @errors << "#{col} is out of range" if !raw.nil? && @attributes[col].nil?
9
+ @errors << "#{col} #{bad_value_text(col)}" if !raw.nil? && @attributes[col].nil?
10
10
  end
11
11
 
12
12
  self.class.rules.each do |rule|
13
- col = rule.column
13
+ col = rule.attribute
14
14
  value = rule.kind == "numericality" && @raw.key?(col) ? @raw[col] : @attributes[col]
15
- msg = rule.message(value)
15
+ msg = rule.message(value, self.class.type_of(col))
16
16
  @errors << msg unless msg.nil?
17
17
  end
18
18
 
@@ -26,7 +26,7 @@ module Edgeway
26
26
  if @errors.empty?
27
27
  self.class.rules.each do |rule|
28
28
  next unless rule.kind == "uniqueness"
29
- @errors << "#{rule.column} has already been taken" if taken?(rule)
29
+ @errors << "#{rule.attribute} has already been taken" if taken?(rule)
30
30
  end
31
31
  end
32
32
  @errors.empty?
@@ -34,10 +34,14 @@ module Edgeway
34
34
 
35
35
  private
36
36
 
37
+ def bad_value_text(column)
38
+ self.class.type_of(column) == :integer ? "is out of range" : "is invalid"
39
+ end
40
+
37
41
  def taken?(rule)
38
- value = @attributes[rule.column]
42
+ value = @attributes[rule.attribute]
39
43
  return false if value.nil?
40
- found = self.class.where(rule.column => value)
44
+ found = self.class.where(rule.attribute => value)
41
45
  rule.scope.each { |c| found = found.where(c => @attributes[self.class.column(c)]) }
42
46
  other = found.limit(1).to_a.first
43
47
  !other.nil? && (new_record? || other.id != id)
@@ -45,7 +49,7 @@ module Edgeway
45
49
  end
46
50
 
47
51
  class Rule
48
- attr_reader :column, :kind
52
+ attr_reader :attribute, :kind
49
53
 
50
54
  def self.scope_columns(options)
51
55
  return [] if options == true
@@ -70,25 +74,25 @@ module Edgeway
70
74
  end
71
75
 
72
76
  def initialize(attribute, kind, bounds, list, pattern)
73
- @column = attribute
77
+ @attribute = attribute
74
78
  @kind = kind
75
79
  @bounds = bounds
76
80
  @list = list
77
81
  @pattern = pattern
78
82
  end
79
83
 
80
- def message(v)
84
+ def message(v, type = nil)
81
85
  # Uniqueness needs a query: Validations#taken? does it.
82
86
  return nil if @kind == "uniqueness"
83
87
  if @kind == "presence"
84
- return "#{@column} can't be blank" if v.nil? || v.to_s.strip.empty?
88
+ return "#{@attribute} can't be blank" if v.nil? || blank_text?(v.to_s, type)
85
89
  return nil
86
90
  end
87
91
  return nil if v.nil?
88
92
  case @kind
89
- when "length" then length_message(v.to_s.length)
90
- when "inclusion" then @list.include?(v.to_s) ? nil : "#{@column} is not included in the list"
91
- when "format" then @pattern.match?(v.to_s) ? nil : "#{@column} is invalid"
93
+ when "length" then type == :binary ? length_message(v.to_s.bytesize, "bytes") : length_message(v.to_s.length, "characters")
94
+ when "inclusion" then @list.include?(v.to_s) ? nil : "#{@attribute} is not included in the list"
95
+ when "format" then @pattern.match?(v.to_s) ? nil : "#{@attribute} is invalid"
92
96
  when "numericality" then numericality_message(v)
93
97
  end
94
98
  end
@@ -97,15 +101,17 @@ module Edgeway
97
101
 
98
102
  private
99
103
 
100
- def length_message(n)
104
+ def blank_text?(text, type) = type == :binary ? text.empty? : text.strip.empty?
105
+
106
+ def length_message(n, unit)
101
107
  if @bounds.key?("minimum") && n < @bounds["minimum"]
102
- return "#{@column} is too short (minimum is #{@bounds["minimum"]} characters)"
108
+ return "#{@attribute} is too short (minimum is #{@bounds["minimum"]} #{unit})"
103
109
  end
104
110
  if @bounds.key?("maximum") && n > @bounds["maximum"]
105
- return "#{@column} is too long (maximum is #{@bounds["maximum"]} characters)"
111
+ return "#{@attribute} is too long (maximum is #{@bounds["maximum"]} #{unit})"
106
112
  end
107
113
  if @bounds.key?("is") && n != @bounds["is"]
108
- return "#{@column} is the wrong length (should be #{@bounds["is"]} characters)"
114
+ return "#{@attribute} is the wrong length (should be #{@bounds["is"]} #{unit})"
109
115
  end
110
116
  nil
111
117
  end
@@ -114,20 +120,20 @@ module Edgeway
114
120
  text = v.to_s
115
121
  integer = text.match?(/\A[+-]?\d+\z/)
116
122
  # Decimals and exponents too: Float#to_s writes 1.0e+20 and 1.0e-05.
117
- return "#{@column} is not a number" unless integer || text.match?(/\A[+-]?\d+(\.\d+)?([eE][+-]?\d+)?\z/)
118
- return "#{@column} must be an integer" if @bounds.key?("only_integer") && !integer
123
+ return "#{@attribute} is not a number" unless integer || text.match?(/\A[+-]?\d+(\.\d+)?([eE][+-]?\d+)?\z/)
124
+ return "#{@attribute} must be an integer" if @bounds.key?("only_integer") && !integer
119
125
  n = text.to_f
120
126
  if @bounds.key?("greater_than") && !(n > @bounds["greater_than"])
121
- return "#{@column} must be greater than #{@bounds["greater_than"]}"
127
+ return "#{@attribute} must be greater than #{@bounds["greater_than"]}"
122
128
  end
123
129
  if @bounds.key?("greater_than_or_equal_to") && !(n >= @bounds["greater_than_or_equal_to"])
124
- return "#{@column} must be greater than or equal to #{@bounds["greater_than_or_equal_to"]}"
130
+ return "#{@attribute} must be greater than or equal to #{@bounds["greater_than_or_equal_to"]}"
125
131
  end
126
132
  if @bounds.key?("less_than") && !(n < @bounds["less_than"])
127
- return "#{@column} must be less than #{@bounds["less_than"]}"
133
+ return "#{@attribute} must be less than #{@bounds["less_than"]}"
128
134
  end
129
135
  if @bounds.key?("less_than_or_equal_to") && !(n <= @bounds["less_than_or_equal_to"])
130
- return "#{@column} must be less than or equal to #{@bounds["less_than_or_equal_to"]}"
136
+ return "#{@attribute} must be less than or equal to #{@bounds["less_than_or_equal_to"]}"
131
137
  end
132
138
  nil
133
139
  end
@@ -0,0 +1,9 @@
1
+ module Edgeway
2
+ module RateLimit
3
+ class Limiter
4
+ def exceeded?(binding_name, key)
5
+ raise Edgeway::Error, "#{self.class.name} does not check limits"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "limiter"
2
+
3
+ module Edgeway
4
+ module RateLimit
5
+ class NullLimiter < Limiter
6
+ def exceeded?(binding_name, key) = false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "limiter"
2
+
3
+ module Edgeway
4
+ module RateLimit
5
+ class StubLimiter < Limiter
6
+ attr_reader :checks
7
+
8
+ def initialize
9
+ @exceeded_bindings = []
10
+ @checks = []
11
+ end
12
+
13
+ def stub(binding_name, exceeded)
14
+ @exceeded_bindings.delete(binding_name)
15
+ @exceeded_bindings << binding_name if exceeded
16
+ end
17
+
18
+ def exceeded?(binding_name, key)
19
+ @checks << [binding_name, key]
20
+ @exceeded_bindings.include?(binding_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "rate_limit/limiter"
2
+ require_relative "rate_limit/null_limiter"
@@ -13,12 +13,7 @@ module Edgeway
13
13
 
14
14
  def self.split(path) = path.split("/").reject(&:empty?)
15
15
 
16
- def controller_class_name
17
- parts = @controller_path.split("/").map do |part|
18
- part.split("_").map(&:capitalize).join
19
- end
20
- parts.join("::") + "Controller"
21
- end
16
+ def controller_class_name = Inflector.controller_class_name(@controller_path)
22
17
 
23
18
  def match(verb, path)
24
19
  return nil unless verb == @verb || (verb == "HEAD" && @verb == "GET")
@@ -7,6 +7,19 @@ module Edgeway
7
7
  def self.run
8
8
  response = Edgeway.application.call(HTTP::Request.from_env)
9
9
  print response.to_cgi
10
+ log_query_stats
11
+ end
12
+
13
+ def self.log_query_stats
14
+ Model.database_names.each { |name| log_database_stats(name) }
15
+ end
16
+
17
+ def self.log_database_stats(name)
18
+ stats = Model.query_stats(name)
19
+ return unless stats.queries > 0
20
+ fields = stats.log_fields
21
+ fields["database"] = name.to_s unless name == Model::PRIMARY
22
+ Edgeway.logger.info("query stats", fields)
10
23
  end
11
24
  end
12
25
  end
@@ -3,67 +3,69 @@
3
3
  # Spinel's, so CRuby can't read it (tests use edgeway/db/memory).
4
4
  require "json"
5
5
  require_relative "../../db/adapter"
6
+ require_relative "../import_result"
6
7
 
7
8
  module Edgeway
8
9
  module Cloudflare
9
10
  # The imports as Ruby methods: d1_query and d1_execute take the binding
10
- # name, the SQL and the binds (JSON text); d1_batch takes the binding name
11
+ # name, the SQL and the binds (JSON text); d1_exec takes the binding name
12
+ # and SQL text of one or more statements; d1_batch takes the binding name
11
13
  # and the statements ([[sql, binds], ...], JSON text), and answer the result as JSON
12
14
  # text:
13
15
  #
14
- # d1_query {"rows": [...]}
15
- # d1_execute {"changes": n, "last_insert_id": id}
16
- # d1_batch {"results": [{"changes": n, "last_insert_id": id}, ...]}
16
+ # d1_query {"rows": [...], "meta": META}
17
+ # d1_execute {"changes": n, "last_insert_id": id, "meta": META}
18
+ # d1_exec {"count": n, "meta": META}
19
+ # d1_batch {"results": [{"changes": n, "last_insert_id": id}, ...], "meta": META}
20
+ # META {"rows_read": n, "rows_written": n, "duration": ms,
21
+ # "served_by_region": "...", "served_by_primary": bool}
22
+ # (a batch's is the sum over its statements)
23
+ # BLOB a bind or a column value that is a BLOB is {"$blob": "<base64>"}
24
+ # (raw bytes can't cross: JSON text holds no arbitrary bytes)
25
+ # last_insert_id is a number; a rowid beyond 32 bits is a Bignum here,
26
+ # which last_insert_id_of turns into its decimal String
17
27
  # any {"error": "message"} when D1 failed
28
+ # any {"error": "message", "unavailable": true} when it failed
29
+ # in a transient way (DatabaseUnavailable)
18
30
  #
19
- # An import can't hand a string to wasm, so a call is two: query/execute
20
- # runs the statement and answers the byte length of its result, and
21
- # take(buf) copies the result into buf. JS wraps query and execute in
31
+ # An import can't hand a string to wasm, so a call is two: each import
32
+ # runs the statement(s) and answers the byte length of its result, and
33
+ # take(buf) copies the result into buf. JS wraps each import in
22
34
  # WebAssembly.Suspending: the D1 Promise is awaited with the wasm stack
23
35
  # suspended (JSPI), so they read like blocking calls here.
24
36
  module D1Imports
25
37
  ffi_source <<~C
26
- #include <stdlib.h>
27
-
28
38
  #define D1_IMPORT(name) __attribute__((import_module("edgeway_d1"), import_name(name)))
29
39
 
30
40
  D1_IMPORT("query") int js_d1_query(const char *binding, const char *sql, const char *binds);
31
41
  D1_IMPORT("execute") int js_d1_execute(const char *binding, const char *sql, const char *binds);
42
+ D1_IMPORT("exec") int js_d1_exec(const char *binding, const char *sql);
32
43
  D1_IMPORT("batch") int js_d1_batch(const char *binding, const char *statements);
33
44
  D1_IMPORT("take") void js_d1_take(char *buf);
34
45
 
35
- /* The last result, NUL-terminated; grown as needed and reused. */
36
- static char *result;
37
- static int result_cap;
38
-
39
- static const char *take_result(int length) {
40
- if (length + 1 > result_cap) {
41
- char *grown = realloc(result, length + 1);
42
- if (!grown) abort();
43
- result = grown;
44
- result_cap = length + 1;
45
- }
46
- js_d1_take(result);
47
- result[length] = 0;
48
- return result;
49
- }
46
+ static ImportResultBuffer d1_result;
50
47
 
51
48
  /* What ffi_func binds. Its own extern declaration would lose the
52
49
  import attributes, so the imports are wrapped. */
53
50
  const char *d1_query(const char *binding, const char *sql, const char *binds) {
54
- return take_result(js_d1_query(binding, sql, binds));
51
+ return take_import_result(&d1_result, js_d1_query(binding, sql, binds), js_d1_take);
55
52
  }
56
53
 
57
54
  const char *d1_execute(const char *binding, const char *sql, const char *binds) {
58
- return take_result(js_d1_execute(binding, sql, binds));
55
+ return take_import_result(&d1_result, js_d1_execute(binding, sql, binds), js_d1_take);
56
+ }
57
+
58
+ const char *d1_exec(const char *binding, const char *sql) {
59
+ return take_import_result(&d1_result, js_d1_exec(binding, sql), js_d1_take);
59
60
  }
60
61
 
61
62
  const char *d1_batch(const char *binding, const char *statements) {
62
- return take_result(js_d1_batch(binding, statements));
63
+ return take_import_result(&d1_result, js_d1_batch(binding, statements), js_d1_take);
63
64
  }
64
65
  C
65
66
  ffi_func :d1_query, [:str, :str, :str], :str
66
67
  ffi_func :d1_execute, [:str, :str, :str], :str
68
+ ffi_func :d1_exec, [:str, :str], :str
67
69
  ffi_func :d1_batch, [:str, :str], :str
68
70
  end
69
71
 
@@ -74,26 +76,38 @@ module Edgeway
74
76
  end
75
77
 
76
78
  def select_all(sql, binds)
79
+ check_binds(binds)
77
80
  result(D1Imports.d1_query(@binding_name, sql, JSON.generate(binds)))["rows"]
78
81
  end
79
82
 
80
83
  def execute(sql, binds)
84
+ check_binds(binds)
81
85
  res = result(D1Imports.d1_execute(@binding_name, sql, JSON.generate(binds)))
82
- { "changes" => res["changes"].to_i, "last_insert_id" => res["last_insert_id"].to_i }
86
+ write_result(res)
87
+ end
88
+
89
+ def exec(sql)
90
+ result(D1Imports.d1_exec(@binding_name, sql))
83
91
  end
84
92
 
85
93
  # One D1 batch(): the statements run as a single transaction, so any
86
94
  # failure rolls all of them back.
87
95
  def batch(statements)
96
+ statements.each { |s| check_binds(s[1]) }
88
97
  res = result(D1Imports.d1_batch(@binding_name, JSON.generate(statements)))
89
- res["results"].map { |r| { "changes" => r["changes"].to_i, "last_insert_id" => r["last_insert_id"].to_i } }
98
+ res["results"].map { |entry| write_result(entry) }
90
99
  end
91
100
 
92
101
  private
93
102
 
103
+ def write_result(entry)
104
+ { "changes" => entry["changes"].to_i, "last_insert_id" => last_insert_id_of(entry["last_insert_id"]) }
105
+ end
106
+
94
107
  def result(json)
95
108
  res = JSON.parse(json)
96
- fail_with("d1: #{res["error"]}") if res.key?("error")
109
+ raise_error_result(res, "d1: ") if res.key?("error")
110
+ record_meta(res["meta"]) if res.key?("meta")
97
111
  res
98
112
  end
99
113
  end
@@ -3,33 +3,21 @@
3
3
  # (ffi_source is Spinel's; tests use edgeway/http/auth/stub).
4
4
  require "json"
5
5
  require_relative "../../http/auth/jwt"
6
+ require_relative "../import_result"
6
7
 
7
8
  module Edgeway
8
9
  module Cloudflare
9
10
  module JWTImports
10
11
  ffi_source <<~C
11
- #include <stdlib.h>
12
-
13
12
  #define JWT_IMPORT(name) __attribute__((import_module("edgeway_jwt"), import_name(name)))
14
13
 
15
14
  JWT_IMPORT("verify") int js_jwt_verify(const char *token);
16
15
  JWT_IMPORT("take") void js_jwt_take(char *buf);
17
16
 
18
- /* The last result, NUL-terminated; grown as needed and reused. */
19
- static char *jwt_result;
20
- static int jwt_result_cap;
17
+ static ImportResultBuffer jwt_result;
21
18
 
22
19
  const char *jwt_verify(const char *token) {
23
- int length = js_jwt_verify(token);
24
- if (length + 1 > jwt_result_cap) {
25
- char *grown = realloc(jwt_result, length + 1);
26
- if (!grown) abort();
27
- jwt_result = grown;
28
- jwt_result_cap = length + 1;
29
- }
30
- js_jwt_take(jwt_result);
31
- jwt_result[length] = 0;
32
- return jwt_result;
20
+ return take_import_result(&jwt_result, js_jwt_verify(token), js_jwt_take);
33
21
  }
34
22
  C
35
23
  ffi_func :jwt_verify, [:str], :str
@@ -0,0 +1,45 @@
1
+ require "json"
2
+ require_relative "../import_result"
3
+ require_relative "kv_store"
4
+
5
+ module Edgeway
6
+ module Cloudflare
7
+ module KVImports
8
+ ffi_source <<~C
9
+ #define KV_IMPORT(name) __attribute__((import_module("edgeway_kv"), import_name(name)))
10
+
11
+ KV_IMPORT("get") int js_kv_get(const char *binding, const char *key);
12
+ KV_IMPORT("put") int js_kv_put(const char *binding, const char *key, const char *text, int expires_in);
13
+ KV_IMPORT("delete") int js_kv_delete(const char *binding, const char *key);
14
+ KV_IMPORT("take") void js_kv_take(char *buffer);
15
+
16
+ static ImportResultBuffer kv_result;
17
+
18
+ const char *kv_get(const char *binding, const char *key) {
19
+ return take_import_result(&kv_result, js_kv_get(binding, key), js_kv_take);
20
+ }
21
+
22
+ const char *kv_put(const char *binding, const char *key, const char *text, int expires_in) {
23
+ return take_import_result(&kv_result, js_kv_put(binding, key, text, expires_in), js_kv_take);
24
+ }
25
+
26
+ const char *kv_delete(const char *binding, const char *key) {
27
+ return take_import_result(&kv_result, js_kv_delete(binding, key), js_kv_take);
28
+ }
29
+ C
30
+ ffi_func :kv_get, [:str, :str], :str
31
+ ffi_func :kv_put, [:str, :str, :str, :int], :str
32
+ ffi_func :kv_delete, [:str, :str], :str
33
+ end
34
+
35
+ class KVImportTransport < KVTransport
36
+ def get(binding_name, key) = KVImports.kv_get(binding_name, key)
37
+
38
+ def put(binding_name, key, text, expires_in_seconds)
39
+ KVImports.kv_put(binding_name, key, text, expires_in_seconds)
40
+ end
41
+
42
+ def delete(binding_name, key) = KVImports.kv_delete(binding_name, key)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ require "json"
2
+ require_relative "../../cache/store"
3
+
4
+ module Edgeway
5
+ module Cloudflare
6
+ class KVTransport
7
+ def get(binding_name, key)
8
+ raise Edgeway::Error, "#{self.class.name} does not get"
9
+ end
10
+
11
+ def put(binding_name, key, text, expires_in_seconds)
12
+ raise Edgeway::Error, "#{self.class.name} does not put"
13
+ end
14
+
15
+ def delete(binding_name, key)
16
+ raise Edgeway::Error, "#{self.class.name} does not delete"
17
+ end
18
+ end
19
+
20
+ class KVStore < Cache::Store
21
+ def initialize(binding:, namespace: nil, expires_in: nil, transport:)
22
+ super(namespace: namespace, expires_in: expires_in)
23
+ @binding_name = binding
24
+ @transport = transport
25
+ end
26
+
27
+ def read_text(key)
28
+ check_key(key)
29
+ answer = parse_answer("get", @transport.get(@binding_name, key))
30
+ answer.nil? ? nil : answer["value"]
31
+ end
32
+
33
+ def write_text(key, text, expires_in_seconds)
34
+ check_key(key)
35
+ !parse_answer("put", @transport.put(@binding_name, key, text, expires_in_seconds)).nil?
36
+ end
37
+
38
+ def delete_text(key)
39
+ check_key(key)
40
+ !parse_answer("delete", @transport.delete(@binding_name, key)).nil?
41
+ end
42
+
43
+ private
44
+
45
+ def check_key(key)
46
+ raise ArgumentError, "cache key can't be . or .." if key == "." || key == ".."
47
+ end
48
+
49
+ def parse_answer(operation, json)
50
+ answer = JSON.parse(json)
51
+ return answer unless answer.key?("error")
52
+
53
+ Edgeway.logger.warn("cache operation failed", { "operation" => operation, "binding" => @binding_name, "error" => answer["error"] })
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ require "json"
2
+ require_relative "../import_result"
3
+ require_relative "rate_limiter"
4
+
5
+ module Edgeway
6
+ module Cloudflare
7
+ module RateLimitImports
8
+ ffi_source <<~C
9
+ #define RATE_LIMIT_IMPORT(name) __attribute__((import_module("edgeway_ratelimit"), import_name(name)))
10
+
11
+ RATE_LIMIT_IMPORT("check") int js_rate_limit_check(const char *binding, const char *key);
12
+ RATE_LIMIT_IMPORT("take") void js_rate_limit_take(char *buffer);
13
+
14
+ static ImportResultBuffer rate_limit_result;
15
+
16
+ const char *rate_limit_check(const char *binding, const char *key) {
17
+ return take_import_result(&rate_limit_result, js_rate_limit_check(binding, key), js_rate_limit_take);
18
+ }
19
+ C
20
+ ffi_func :rate_limit_check, [:str, :str], :str
21
+ end
22
+
23
+ class RateLimitImportTransport < RateLimitTransport
24
+ def check(binding_name, key) = RateLimitImports.rate_limit_check(binding_name, key)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ require "json"
2
+ require_relative "../../rate_limit"
3
+
4
+ module Edgeway
5
+ module Cloudflare
6
+ class RateLimitTransport
7
+ def check(binding_name, key)
8
+ raise Edgeway::Error, "#{self.class.name} does not check"
9
+ end
10
+ end
11
+
12
+ class RateLimiter < RateLimit::Limiter
13
+ def initialize(transport:)
14
+ @transport = transport
15
+ end
16
+
17
+ def exceeded?(binding_name, key)
18
+ answer = JSON.parse(@transport.check(binding_name, Digest::SHA256.hexdigest(key)))
19
+ raise ConfigurationError, "rate limit: #{answer["misconfigured"]}" if answer.key?("misconfigured")
20
+ return failed_open(binding_name, answer["error"]) if answer.key?("error")
21
+
22
+ answer["success"] == false
23
+ end
24
+
25
+ private
26
+
27
+ def failed_open(binding_name, message)
28
+ Edgeway.logger.warn("rate limit check failed", { "binding" => binding_name, "error" => message })
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "d1"
2
+
3
+ module Edgeway
4
+ module Cloudflare
5
+ module TestImports
6
+ ffi_source <<~C
7
+ __attribute__((import_module("edgeway_test"), import_name("reset"))) void js_test_reset(void);
8
+
9
+ int test_reset(void) {
10
+ js_test_reset();
11
+ return 0;
12
+ }
13
+ C
14
+ ffi_func :test_reset, [], :int
15
+ end
16
+
17
+ class TestD1Adapter < D1Adapter
18
+ attr_reader :log
19
+
20
+ def initialize(binding: "DB")
21
+ super(binding: binding)
22
+ @log = []
23
+ end
24
+
25
+ def select_all(sql, binds)
26
+ log_statement(sql, binds)
27
+ super(sql, binds)
28
+ end
29
+
30
+ def execute(sql, binds)
31
+ log_statement(sql, binds)
32
+ super(sql, binds)
33
+ end
34
+
35
+ def exec(sql)
36
+ log_statement(sql, [])
37
+ super(sql)
38
+ end
39
+
40
+ def batch(statements)
41
+ @log << "BATCH #{statements.size}"
42
+ super(statements)
43
+ end
44
+
45
+ def close
46
+ end
47
+
48
+ private
49
+
50
+ def log_statement(sql, binds)
51
+ @log << "#{sql} | #{JSON.generate(binds)}"
52
+ end
53
+ end
54
+ end
55
+
56
+ module TestDatabases
57
+ def self.reset
58
+ Cloudflare::TestImports.test_reset
59
+ end
60
+
61
+ def self.open(name)
62
+ Cloudflare::TestD1Adapter.new(binding: Edgeway::TEST_BINDINGS.fetch(name))
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ require_relative "../http/client"
2
+ require_relative "import_result"
3
+
4
+ module Edgeway
5
+ module FetchImports
6
+ ffi_source <<~C
7
+ #define FETCH_IMPORT(name) __attribute__((import_module("edgeway_fetch"), import_name(name)))
8
+
9
+ FETCH_IMPORT("request") int js_fetch_request(const char *payload);
10
+ FETCH_IMPORT("take") void js_fetch_take(char *buffer);
11
+
12
+ static ImportResultBuffer fetch_result;
13
+
14
+ const char *fetch_request(const char *payload) {
15
+ return take_import_result(&fetch_result, js_fetch_request(payload), js_fetch_take);
16
+ }
17
+ C
18
+ ffi_func :fetch_request, [:str], :str
19
+ end
20
+
21
+ class FetchTransport < HTTP::Transport
22
+ def request(payload)
23
+ FetchImports.fetch_request(payload)
24
+ end
25
+ end
26
+ end