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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +784 -125
- data/js/cloudflare-d1-sessions.js +68 -0
- data/js/cloudflare-kv.js +47 -0
- data/js/cloudflare-ratelimit.js +31 -0
- data/js/cloudflare-test-database.js +112 -0
- data/js/cloudflare-test-runner.js +76 -0
- data/js/cloudflare.js +144 -22
- data/js/cors.js +15 -16
- data/js/fetch.js +113 -0
- data/js/headers.js +16 -0
- data/js/jspi.js +32 -0
- data/js/jwt.js +6 -40
- data/js/runtime.js +42 -5
- data/lib/edgeway/application.rb +21 -9
- data/lib/edgeway/cache/limits.rb +8 -0
- data/lib/edgeway/cache/memory_store.rb +24 -0
- data/lib/edgeway/cache/null_store.rb +13 -0
- data/lib/edgeway/cache/store.rb +189 -0
- data/lib/edgeway/cache.rb +13 -0
- data/lib/edgeway/cli/build/app_config.rb +39 -0
- data/lib/edgeway/cli/build/app_files.rb +40 -0
- data/lib/edgeway/cli/build/cache_config.rb +108 -0
- data/lib/edgeway/cli/build/database_config.rb +96 -0
- data/lib/edgeway/cli/build/js_files.rb +30 -0
- data/lib/edgeway/cli/build/view_compiler.rb +191 -0
- data/lib/edgeway/cli/build.rb +50 -18
- data/lib/edgeway/cli/cloudflare_test/wasm_test_cli.rb +35 -0
- data/lib/edgeway/cli/cloudflare_test.rb +120 -0
- data/lib/edgeway/cli/generate.rb +174 -26
- data/lib/edgeway/cli/new.rb +1 -1
- data/lib/edgeway/cli/test.rb +28 -9
- data/lib/edgeway/cli/toolchain.rb +5 -0
- data/lib/edgeway/cli.rb +7 -4
- data/lib/edgeway/controller/base.rb +32 -3
- data/lib/edgeway/controller/caching.rb +140 -0
- data/lib/edgeway/controller/callbacks.rb +5 -1
- data/lib/edgeway/controller/dispatcher.rb +11 -1
- data/lib/edgeway/controller/rate_limiting.rb +145 -0
- data/lib/edgeway/controller/rendering.rb +24 -4
- data/lib/edgeway/db/adapter.rb +38 -0
- data/lib/edgeway/db/memory.rb +9 -2
- data/lib/edgeway/db/query_stats.rb +42 -0
- data/lib/edgeway/db/sqlite.rb +73 -9
- data/lib/edgeway/errors.rb +43 -0
- data/lib/edgeway/generators/app/templates/.github/workflows/ci.yml.tt +38 -0
- data/lib/edgeway/generators/app/templates/.ruby-version.tt +1 -0
- data/lib/edgeway/generators/app/templates/README.md.tt +8 -2
- data/lib/edgeway/generators/app/templates/app/views/layouts/application.html.erb +11 -0
- data/lib/edgeway/generators/app/templates/bin/build.tt +1 -1
- data/lib/edgeway/generators/app/templates/bin/deploy.tt +1 -1
- data/lib/edgeway/generators/app/templates/bin/setup.tt +2 -3
- data/lib/edgeway/generators/app/templates/bin/test.tt +7 -0
- data/lib/edgeway/generators/app/templates/config/cache.yml +22 -0
- data/lib/edgeway/generators/app/templates/config/database.yml +25 -2
- data/lib/edgeway/generators/app/templates/mise.toml.tt +1 -1
- data/lib/edgeway/generators/app/templates/public/.assetsignore +1 -0
- data/lib/edgeway/generators/app/templates/wrangler.toml.tt +39 -0
- data/lib/edgeway/http/auth/stub.rb +1 -3
- data/lib/edgeway/http/client.rb +80 -0
- data/lib/edgeway/http/request.rb +7 -22
- data/lib/edgeway/http/stub_transport.rb +38 -0
- data/lib/edgeway/inflector.rb +19 -0
- data/lib/edgeway/logger.rb +33 -5
- data/lib/edgeway/model/attributes.rb +93 -10
- data/lib/edgeway/model/base.rb +50 -5
- data/lib/edgeway/model/connection_registry.rb +25 -0
- data/lib/edgeway/model/macros.rb +5 -1
- data/lib/edgeway/model/persistence.rb +18 -11
- data/lib/edgeway/model/relation.rb +106 -56
- data/lib/edgeway/model/validations.rb +29 -23
- data/lib/edgeway/rate_limit/limiter.rb +9 -0
- data/lib/edgeway/rate_limit/null_limiter.rb +9 -0
- data/lib/edgeway/rate_limit/stub_limiter.rb +24 -0
- data/lib/edgeway/rate_limit.rb +2 -0
- data/lib/edgeway/routing/route.rb +1 -6
- data/lib/edgeway/server.rb +13 -0
- data/lib/edgeway/service/cloudflare/d1.rb +44 -30
- data/lib/edgeway/service/cloudflare/jwt.rb +3 -15
- data/lib/edgeway/service/cloudflare/kv.rb +45 -0
- data/lib/edgeway/service/cloudflare/kv_store.rb +58 -0
- data/lib/edgeway/service/cloudflare/rate_limit.rb +27 -0
- data/lib/edgeway/service/cloudflare/rate_limiter.rb +33 -0
- data/lib/edgeway/service/cloudflare/test_databases.rb +65 -0
- data/lib/edgeway/service/fetch.rb +26 -0
- data/lib/edgeway/service/import_result.rb +24 -0
- data/lib/edgeway/test/sqlite_databases.rb +18 -0
- data/lib/edgeway/test.rb +86 -26
- data/lib/edgeway/version.rb +1 -1
- data/lib/edgeway/view.rb +24 -0
- data/lib/edgeway.rb +9 -0
- metadata +50 -4
- data/lib/edgeway/cli/app_config.rb +0 -35
- 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}
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
+
value = @attributes[rule.attribute]
|
|
39
43
|
return false if value.nil?
|
|
40
|
-
found = self.class.where(rule.
|
|
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 :
|
|
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
|
-
@
|
|
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 "#{@
|
|
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 : "#{@
|
|
91
|
-
when "format" then @pattern.match?(v.to_s) ? nil : "#{@
|
|
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
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
118
|
-
return "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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,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
|
|
@@ -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")
|
data/lib/edgeway/server.rb
CHANGED
|
@@ -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);
|
|
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
|
-
#
|
|
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:
|
|
20
|
-
# runs the statement and answers the byte length of its result, and
|
|
21
|
-
# take(buf) copies the result into buf. JS wraps
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 { |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|