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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Edgeway
|
|
7
|
+
module CLI
|
|
8
|
+
class CloudflareTest < Test
|
|
9
|
+
RUNTIME_DIR = "tmp/edgeway"
|
|
10
|
+
RUNNER = "cloudflare-test-runner.js"
|
|
11
|
+
MIN_NODE_MAJOR = 24
|
|
12
|
+
|
|
13
|
+
# The apps whose test environment uses the d1 adapter.
|
|
14
|
+
def self.handles?(database)
|
|
15
|
+
database.section?("test") && database.section("test")["adapter"] == "d1"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(root)
|
|
19
|
+
super
|
|
20
|
+
@config = Build::AppConfig.load(root, "test")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(args)
|
|
24
|
+
check_prerequisites
|
|
25
|
+
write_test_app
|
|
26
|
+
write_runtime
|
|
27
|
+
require_spinel_test_unit
|
|
28
|
+
argv = ["-I", @root, "-I", CLI.gem_dir + "/lib", *args]
|
|
29
|
+
exit WasmTestCLI.new(argv, root: @root, launcher: launcher).run
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def database_lines
|
|
35
|
+
[
|
|
36
|
+
'require "edgeway/db/memory"',
|
|
37
|
+
'require "edgeway/service/cloudflare/test_databases"',
|
|
38
|
+
'require "edgeway/service/cloudflare/jwt"',
|
|
39
|
+
'require "edgeway/service/fetch"',
|
|
40
|
+
"module Edgeway; TEST_DATABASES = #{@config.databases.keys.inspect}; end",
|
|
41
|
+
"module Edgeway; TEST_BINDINGS = #{bindings.inspect}; end"
|
|
42
|
+
]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bindings
|
|
46
|
+
{ "primary" => @config.binding_name }.merge(@config.databases)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def migration_directories
|
|
50
|
+
{ "primary" => "db/migrations" }.merge(@config.databases.to_h { |name, _binding| [name, "db/#{name}/migrations"] })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write_runtime
|
|
54
|
+
databases = bindings.map do |name, binding_name|
|
|
55
|
+
{ "binding" => binding_name, "migrations" => File.join(@root, migration_directories.fetch(name)) }
|
|
56
|
+
end
|
|
57
|
+
values = {
|
|
58
|
+
"env" => "test", "d1Binding" => @config.binding_name, "sessions" => @config.sessions,
|
|
59
|
+
"appRoot" => @root, "databases" => databases
|
|
60
|
+
}
|
|
61
|
+
directory = File.join(@root, RUNTIME_DIR)
|
|
62
|
+
Build::JsFiles.copy(directory, Build::JsFiles.config_js(values, "test"), tests: true)
|
|
63
|
+
File.write(File.join(directory, "package.json"), %({"type": "module"}\n))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def launcher
|
|
67
|
+
["node", *node_flags, File.join(@root, RUNTIME_DIR, RUNNER)]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def node_flags
|
|
71
|
+
has_jspi = system("node", "-e", "process.exit(typeof WebAssembly.Suspending === 'function' ? 0 : 1)")
|
|
72
|
+
has_jspi ? [] : ["--experimental-wasm-jspi", "--experimental-wasm-exnref"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def require_spinel_test_unit
|
|
76
|
+
require "spinel_test/unit/cli"
|
|
77
|
+
require_relative "cloudflare_test/wasm_test_cli"
|
|
78
|
+
rescue LoadError
|
|
79
|
+
raise Error, 'spinel_test-unit is not installed (add gem "spinel_test-unit", "~> 0.1", group: :test to the Gemfile and run bundle install)'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_prerequisites
|
|
83
|
+
check_node
|
|
84
|
+
raise Error, "no wrangler.toml in #{@root} (the tests run on the local D1 it describes)" unless File.exist?(File.join(@root, "wrangler.toml"))
|
|
85
|
+
|
|
86
|
+
check_wrangler
|
|
87
|
+
raise Error, "no WASI SDK found (install it with mise, or set WASI_SDK=/path/to/wasi-sdk)" unless Toolchain.wasi_sdk
|
|
88
|
+
raise Error, "no Spinel compiler found (install it with mise, or set SPINEL=/path/to/spinel)" unless Toolchain.spinel
|
|
89
|
+
|
|
90
|
+
check_migration_directories
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def check_node
|
|
94
|
+
raise Error, "node is not installed; `edgeway test` runs the tests on the local D1 of wrangler, which needs Node.js #{MIN_NODE_MAJOR} or later" unless Toolchain.find_on_path("node")
|
|
95
|
+
|
|
96
|
+
version, = Open3.capture2("node", "-p", "process.versions.node")
|
|
97
|
+
return if version.to_i >= MIN_NODE_MAJOR
|
|
98
|
+
|
|
99
|
+
raise Error, "Node.js #{version.strip} is too old: `edgeway test` needs #{MIN_NODE_MAJOR} or later (WebAssembly JSPI)"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def check_wrangler
|
|
103
|
+
return if File.directory?(File.join(@root, "node_modules/wrangler"))
|
|
104
|
+
|
|
105
|
+
raise Error, "wrangler is not installed in #{@root}/node_modules; run #{PackageManager.install} (or bin/setup)"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def check_migration_directories
|
|
109
|
+
@config.databases.each_key do |name|
|
|
110
|
+
directory = migration_directories.fetch(name)
|
|
111
|
+
next if File.directory?(File.join(@root, directory))
|
|
112
|
+
|
|
113
|
+
raise Error, "database #{name} (config/database.yml) has no #{directory} directory"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Test::PLATFORMS << CloudflareTest
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/edgeway/cli/generate.rb
CHANGED
|
@@ -9,32 +9,37 @@ module Edgeway
|
|
|
9
9
|
USAGE = <<~TEXT
|
|
10
10
|
Usage: edgeway generate model NAME [field:type ...]
|
|
11
11
|
edgeway generate controller NAME [action ...]
|
|
12
|
+
edgeway generate scaffold NAME [field:type ...]
|
|
12
13
|
|
|
13
14
|
Types: #{%w[string text integer float boolean datetime].join(", ")} (default string)
|
|
14
15
|
TEXT
|
|
15
16
|
|
|
16
|
-
# field type =>
|
|
17
|
+
# field type => its column's SQL type, its attribute type, and the values
|
|
18
|
+
# (as Ruby source) the scaffold's test creates and updates with.
|
|
17
19
|
# D1 is SQLite, which has no datetime/boolean type (only NULL, INTEGER,
|
|
18
20
|
# REAL, TEXT and BLOB): a datetime is TEXT (ISO 8601, as Model.now
|
|
19
21
|
# writes it) and a boolean is an INTEGER 0/1. It is SQLite's, not a
|
|
20
22
|
# Cloudflare, limit.
|
|
23
|
+
Type = Struct.new(:sql, :attr, :created, :updated)
|
|
21
24
|
TYPES = {
|
|
22
|
-
"string" =>
|
|
23
|
-
"text" =>
|
|
24
|
-
"integer" =>
|
|
25
|
-
"float" =>
|
|
26
|
-
"boolean" =>
|
|
27
|
-
"datetime" =>
|
|
25
|
+
"string" => Type.new("TEXT", ":string", '"MyString"', '"Updated"'),
|
|
26
|
+
"text" => Type.new("TEXT", ":string", '"MyText"', '"Updated"'),
|
|
27
|
+
"integer" => Type.new("INTEGER", ":integer", "1", "2"),
|
|
28
|
+
"float" => Type.new("REAL", ":float", "1.5", "2.5"),
|
|
29
|
+
"boolean" => Type.new("INTEGER NOT NULL DEFAULT 0", ":boolean", "true", "false"),
|
|
30
|
+
"datetime" => Type.new("TEXT", ":datetime", '"2026-01-01T00:00:00Z"', '"2026-02-01T00:00:00Z"')
|
|
28
31
|
}.freeze
|
|
29
32
|
|
|
33
|
+
Field = Struct.new(:name, :sql, :attr, :created, :updated)
|
|
34
|
+
|
|
30
35
|
RESOURCE_ACTIONS = %w[index show create update destroy].freeze
|
|
31
36
|
|
|
32
37
|
def self.run(args)
|
|
33
38
|
kind, name, *rest = args
|
|
34
|
-
raise Error, USAGE unless %w[model controller].include?(kind) && name && !name.start_with?("-")
|
|
39
|
+
raise Error, USAGE unless %w[model controller scaffold].include?(kind) && name && !name.start_with?("-")
|
|
35
40
|
|
|
36
41
|
generator = new(Dir.pwd)
|
|
37
|
-
|
|
42
|
+
generator.public_send(kind, name, rest)
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
def initialize(root)
|
|
@@ -46,21 +51,32 @@ module Edgeway
|
|
|
46
51
|
|
|
47
52
|
def model(name, fields)
|
|
48
53
|
base = underscore(name)
|
|
49
|
-
table = Inflector.pluralize(base)
|
|
50
54
|
columns = fields.map { |f| parse_field(f) }
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
write("test/models/#{base}_test.rb", model_test_source(camelize(base)))
|
|
54
|
-
puts "\nApply the migration locally: #{PackageManager.exec} wrangler d1 migrations apply DB_NAME --local"
|
|
55
|
+
write_all(model_files(base, columns))
|
|
56
|
+
puts_migration_hint
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def controller(name, actions)
|
|
58
60
|
base = underscore(name).delete_suffix("_controller")
|
|
59
61
|
actions.each { |a| raise Error, "invalid action name: #{a}" unless a.match?(/\A[a-z_][a-z0-9_]*\z/) }
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
write_all([
|
|
63
|
+
["app/controllers/#{base}_controller.rb", controller_source(camelize(base), actions)],
|
|
64
|
+
["test/controllers/#{base}_test.rb", controller_test_source(camelize(base))]
|
|
65
|
+
])
|
|
66
|
+
puts_route_hints(route_hints(base, actions))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def scaffold(name, fields)
|
|
70
|
+
base = underscore(Inflector.singularize(name))
|
|
71
|
+
plural = Inflector.pluralize(base)
|
|
72
|
+
columns = fields.map { |f| parse_field(f) }
|
|
73
|
+
# Every path is checked before the first is written: no half a scaffold.
|
|
74
|
+
write_all(model_files(base, columns) + [
|
|
75
|
+
["app/controllers/#{plural}_controller.rb", scaffold_controller_source(camelize(base), plural, columns)],
|
|
76
|
+
["test/controllers/#{plural}_test.rb", scaffold_test_source(camelize(base), plural, columns)]
|
|
77
|
+
])
|
|
78
|
+
add_route(plural)
|
|
79
|
+
puts_migration_hint
|
|
64
80
|
end
|
|
65
81
|
|
|
66
82
|
private
|
|
@@ -71,21 +87,60 @@ module Edgeway
|
|
|
71
87
|
name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
def camelize(name) =
|
|
90
|
+
def camelize(name) = Inflector.camelize(name)
|
|
75
91
|
|
|
76
|
-
# "title:string" =>
|
|
92
|
+
# "title:string" => Field.new("title", "TEXT", ":string", '"MyString"', '"Updated"')
|
|
77
93
|
def parse_field(spec)
|
|
78
94
|
name, type = spec.split(":", 2)
|
|
79
95
|
type ||= "string"
|
|
80
96
|
raise Error, "invalid field name: #{spec}" unless name.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
81
97
|
raise Error, "unknown field type #{type} (#{TYPES.keys.join(", ")})" unless TYPES.key?(type)
|
|
82
98
|
|
|
83
|
-
|
|
99
|
+
Field.new(name, *TYPES[type].to_a)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def model_files(base, columns)
|
|
103
|
+
table = Inflector.pluralize(base)
|
|
104
|
+
existing = Dir.glob("db/migrations/*_create_#{table}.sql", base: @root).first
|
|
105
|
+
raise Error, "#{existing} already exists" if existing
|
|
106
|
+
|
|
107
|
+
[
|
|
108
|
+
["app/models/#{base}.rb", model_source(camelize(base), columns)],
|
|
109
|
+
["db/migrations/#{next_migration_number}_create_#{table}.sql", migration_source(table, columns)],
|
|
110
|
+
["test/models/#{base}_test.rb", model_test_source(camelize(base))]
|
|
111
|
+
]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def puts_migration_hint
|
|
115
|
+
puts "\nApply the migration locally: #{PackageManager.exec} wrangler d1 migrations apply DB_NAME --local"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def add_route(plural)
|
|
119
|
+
path = File.join(@root, "config/routes.rb")
|
|
120
|
+
text = File.read(path)
|
|
121
|
+
match = text.match(/^([ \t]*)[\w.]*routes\.draw[ \t]+do[ \t]*\|[ \t]*(\w+)[ \t]*\|[ \t]*(\r?\n)/)
|
|
122
|
+
return puts_route_hints(["r.resources :#{plural}"], "no `routes.draw do |r|` block found") unless match
|
|
123
|
+
|
|
124
|
+
indent, var, eol = match.captures
|
|
125
|
+
line = "#{var}.resources :#{plural}"
|
|
126
|
+
return puts " identical #{line}" if text.match?(/^[ \t]*#{Regexp.escape(var)}\.resources[ \t]*\(?[ \t]*:#{plural}(?!\w)/)
|
|
127
|
+
|
|
128
|
+
close = text.index(/^#{Regexp.escape(indent)}end\b/, match.end(0))
|
|
129
|
+
return puts_route_hints([line], "no closing `end` found") unless close
|
|
130
|
+
|
|
131
|
+
File.write(path, text[0...close] + "#{indent} #{line}#{eol}" + text[close..])
|
|
132
|
+
puts " route #{line}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def puts_route_hints(hints, reason = nil)
|
|
136
|
+
puts "\nconfig/routes.rb not changed: #{reason}" if reason
|
|
137
|
+
puts "\nAdd to config/routes.rb:"
|
|
138
|
+
hints.each { |l| puts " #{l}" }
|
|
84
139
|
end
|
|
85
140
|
|
|
86
141
|
def model_source(klass, columns)
|
|
87
|
-
lines = columns.map do |
|
|
88
|
-
attr == ":boolean" ? " attribute :#{name}, :boolean, default: false" : " attribute :#{name}, #{attr}"
|
|
142
|
+
lines = columns.map do |c|
|
|
143
|
+
c.attr == ":boolean" ? " attribute :#{c.name}, :boolean, default: false" : " attribute :#{c.name}, #{c.attr}"
|
|
89
144
|
end
|
|
90
145
|
lines << " timestamps"
|
|
91
146
|
"class #{klass} < ApplicationModel\n#{lines.join("\n")}\nend\n"
|
|
@@ -93,7 +148,7 @@ module Edgeway
|
|
|
93
148
|
|
|
94
149
|
def migration_source(table, columns)
|
|
95
150
|
lines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"]
|
|
96
|
-
columns.each { |
|
|
151
|
+
columns.each { |c| lines << " #{c.name} #{c.sql}" }
|
|
97
152
|
lines << " created_at TEXT" << " updated_at TEXT"
|
|
98
153
|
"CREATE TABLE #{table} (\n#{lines.join(",\n")}\n);\n"
|
|
99
154
|
end
|
|
@@ -130,6 +185,96 @@ module Edgeway
|
|
|
130
185
|
RUBY
|
|
131
186
|
end
|
|
132
187
|
|
|
188
|
+
def scaffold_controller_source(klass, plural, columns)
|
|
189
|
+
var = underscore(klass)
|
|
190
|
+
lines = [
|
|
191
|
+
"class #{camelize(plural)}Controller < ApplicationController",
|
|
192
|
+
" def index = render json: #{klass}.order(:id).limit(50)",
|
|
193
|
+
"",
|
|
194
|
+
" def show = render json: #{klass}.find(params[:id])",
|
|
195
|
+
"",
|
|
196
|
+
" def create",
|
|
197
|
+
" #{var} = #{columns.empty? ? "#{klass}.create!" : "#{klass}.create!(#{var}_params)"}",
|
|
198
|
+
" render json: #{var}, status: :created",
|
|
199
|
+
" end",
|
|
200
|
+
"",
|
|
201
|
+
" def update",
|
|
202
|
+
" #{var} = #{klass}.find(params[:id])"
|
|
203
|
+
]
|
|
204
|
+
lines << " #{var}.update!(#{var}_params)" unless columns.empty?
|
|
205
|
+
lines += [
|
|
206
|
+
" render json: #{var}",
|
|
207
|
+
" end",
|
|
208
|
+
"",
|
|
209
|
+
" def destroy",
|
|
210
|
+
" #{klass}.find(params[:id]).destroy",
|
|
211
|
+
" head :no_content",
|
|
212
|
+
" end"
|
|
213
|
+
]
|
|
214
|
+
unless columns.empty?
|
|
215
|
+
permitted = columns.map { |c| ":#{c.name}" }.join(", ")
|
|
216
|
+
lines += ["", " private", "", " def #{var}_params = params.require(:#{var}).permit(#{permitted})"]
|
|
217
|
+
end
|
|
218
|
+
lines << "end"
|
|
219
|
+
lines.join("\n") + "\n"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def scaffold_test_source(klass, plural, columns)
|
|
223
|
+
var = underscore(klass)
|
|
224
|
+
create_args = columns.empty? ? "" : "(#{columns.map { |c| "#{c.name}: #{c.created}" }.join(", ")})"
|
|
225
|
+
hash = lambda do |value|
|
|
226
|
+
columns.empty? ? "{}" : "{ #{columns.map { |c| "\"#{c.name}\" => #{c.public_send(value)}" }.join(", ")} }"
|
|
227
|
+
end
|
|
228
|
+
checks = ->(value) { columns.map { |c| " assert_equal #{c.public_send(value)}, json[\"#{c.name}\"]" }.join("\n") }
|
|
229
|
+
<<~RUBY
|
|
230
|
+
require "edgeway/test"
|
|
231
|
+
require "test_helper"
|
|
232
|
+
|
|
233
|
+
class #{camelize(plural)}Test < Edgeway::IntegrationTest
|
|
234
|
+
def test_index
|
|
235
|
+
#{var} = #{klass}.create!#{create_args}
|
|
236
|
+
get "/#{plural}"
|
|
237
|
+
assert_response 200
|
|
238
|
+
assert_equal 1, json.size
|
|
239
|
+
assert_equal #{var}.id, json[0]["id"]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_show
|
|
243
|
+
#{var} = #{klass}.create!#{create_args}
|
|
244
|
+
get "/#{plural}/\#{#{var}.id}"
|
|
245
|
+
assert_response 200
|
|
246
|
+
assert_equal #{var}.id, json["id"]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_show_not_found
|
|
250
|
+
get "/#{plural}/0"
|
|
251
|
+
assert_response 404
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_create
|
|
255
|
+
post "/#{plural}", json: { "#{var}" => #{hash.call(:created)} }
|
|
256
|
+
assert_response 201
|
|
257
|
+
assert_equal 1, #{klass}.count
|
|
258
|
+
#{checks.call(:created)}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_update
|
|
262
|
+
#{var} = #{klass}.create!#{create_args}
|
|
263
|
+
patch "/#{plural}/\#{#{var}.id}", json: { "#{var}" => #{hash.call(:updated)} }
|
|
264
|
+
assert_response 200
|
|
265
|
+
#{checks.call(:updated)}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_destroy
|
|
269
|
+
#{var} = #{klass}.create!#{create_args}
|
|
270
|
+
delete "/#{plural}/\#{#{var}.id}"
|
|
271
|
+
assert_response 204
|
|
272
|
+
assert_equal 0, #{klass}.count
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
RUBY
|
|
276
|
+
end
|
|
277
|
+
|
|
133
278
|
def route_hints(base, actions)
|
|
134
279
|
return ["r.resources :#{base}"] if actions.empty?
|
|
135
280
|
|
|
@@ -147,10 +292,13 @@ module Edgeway
|
|
|
147
292
|
format("%04d", last + 1)
|
|
148
293
|
end
|
|
149
294
|
|
|
295
|
+
def write_all(files)
|
|
296
|
+
files.each { |rel, _text| raise Error, "#{rel} already exists" if File.exist?(File.join(@root, rel)) }
|
|
297
|
+
files.each { |rel, text| write(rel, text) }
|
|
298
|
+
end
|
|
299
|
+
|
|
150
300
|
def write(rel, text)
|
|
151
301
|
dest = File.join(@root, rel)
|
|
152
|
-
raise Error, "#{rel} already exists" if File.exist?(dest)
|
|
153
|
-
|
|
154
302
|
FileUtils.mkdir_p(File.dirname(dest))
|
|
155
303
|
File.write(dest, text)
|
|
156
304
|
puts " create #{rel}"
|
data/lib/edgeway/cli/new.rb
CHANGED
|
@@ -36,7 +36,7 @@ module Edgeway
|
|
|
36
36
|
FileUtils.chmod(0o755, dest) if File.executable?(src)
|
|
37
37
|
puts " create #{dest}"
|
|
38
38
|
end
|
|
39
|
-
puts "\nNext: cd #{@dir} && bin/setup &&
|
|
39
|
+
puts "\nNext: cd #{@dir} && bin/setup && bin/test && bin/build"
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
data/lib/edgeway/cli/test.rb
CHANGED
|
@@ -3,31 +3,50 @@
|
|
|
3
3
|
module Edgeway
|
|
4
4
|
module CLI
|
|
5
5
|
class Test
|
|
6
|
+
PLATFORMS = []
|
|
7
|
+
|
|
6
8
|
def self.run(args)
|
|
7
9
|
root = Dir.pwd
|
|
8
|
-
|
|
9
|
-
new(root).
|
|
10
|
-
exec("spinel_test-unit", "-I", root, "-I", CLI.gem_dir + "/lib", *args)
|
|
10
|
+
database = Build::DatabaseConfig.new(root)
|
|
11
|
+
(PLATFORMS.find { |platform| platform.handles?(database) } || Test).new(root).run(args)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def initialize(root)
|
|
14
15
|
@root = root
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
def run(args)
|
|
19
|
+
Precheck.sqlite!
|
|
20
|
+
write_test_app
|
|
21
|
+
exec("spinel_test-unit", "-I", @root, "-I", CLI.gem_dir + "/lib", *args)
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
def write_test_app
|
|
18
25
|
raise Error, "no config/routes.rb in #{@root}" unless File.exist?(File.join(@root, "config/routes.rb"))
|
|
19
26
|
|
|
20
27
|
FileUtils.mkdir_p(File.join(@root, "tmp"))
|
|
21
28
|
lines = ["# Generated by `edgeway test` from app/ and config/; do not edit.", 'require "edgeway"']
|
|
22
|
-
lines
|
|
23
|
-
lines << 'require "edgeway/db/sqlite"'
|
|
24
|
-
# Where the migrations are read from at test time (the binary may run
|
|
25
|
-
# from another directory); Edgeway::UnitTest#setup applies them.
|
|
26
|
-
lines << "module Edgeway; TEST_APP_ROOT = #{@root.dump}; end"
|
|
29
|
+
lines.concat(database_lines)
|
|
27
30
|
lines << ""
|
|
28
|
-
|
|
31
|
+
views = Build::ViewCompiler.new(@root).compile
|
|
32
|
+
File.write(File.join(@root, "tmp/views.rb"), views) if views
|
|
33
|
+
lines.concat(Build::AppFiles.new(@root).require_lines(views: views && "views"))
|
|
29
34
|
File.write(File.join(@root, "tmp/test_app.rb"), lines.join("\n") + "\n")
|
|
30
35
|
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def database_lines
|
|
40
|
+
[
|
|
41
|
+
'require "edgeway/db/memory"',
|
|
42
|
+
'require "edgeway/db/sqlite"',
|
|
43
|
+
'require "edgeway/test/sqlite_databases"',
|
|
44
|
+
# Where the migrations are read from at test time (the binary may run
|
|
45
|
+
# from another directory); Edgeway::TestDatabases applies them.
|
|
46
|
+
"module Edgeway; TEST_APP_ROOT = #{@root.dump}; end",
|
|
47
|
+
"module Edgeway; TEST_DATABASES = #{Build::DatabaseConfig.new(@root).names.inspect}; end"
|
|
48
|
+
]
|
|
49
|
+
end
|
|
31
50
|
end
|
|
32
51
|
end
|
|
33
52
|
end
|
|
@@ -19,6 +19,11 @@ module Edgeway
|
|
|
19
19
|
find_on_path("spinel") || mise_which("spinel")
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def self.wasi_sdk
|
|
23
|
+
dir = [ENV["WASI_SDK"], ENV["WASI_SDK_PATH"]].find { |value| !value.to_s.empty? } || "/opt/wasi-sdk"
|
|
24
|
+
File.directory?(dir) ? dir : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
def self.find_on_path(name)
|
|
23
28
|
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).map { |d| File.join(d, name) }
|
|
24
29
|
.find { |f| File.file?(f) && File.executable?(f) }
|
data/lib/edgeway/cli.rb
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "yaml"
|
|
7
7
|
require_relative "version"
|
|
8
|
-
require_relative "cli/app_config"
|
|
9
8
|
require_relative "cli/toolchain"
|
|
10
|
-
require_relative "cli/app_files"
|
|
11
9
|
require_relative "cli/build"
|
|
12
10
|
require_relative "cli/precheck"
|
|
13
11
|
require_relative "cli/test"
|
|
12
|
+
require_relative "cli/cloudflare_test"
|
|
14
13
|
require_relative "cli/routes"
|
|
15
14
|
require_relative "cli/package_manager"
|
|
16
15
|
require_relative "cli/generate"
|
|
@@ -22,9 +21,13 @@ module Edgeway
|
|
|
22
21
|
Usage: edgeway COMMAND [options]
|
|
23
22
|
|
|
24
23
|
new APP create a new app in APP/
|
|
25
|
-
build [--env ENV]
|
|
24
|
+
build [--env ENV] [--release]
|
|
25
|
+
compile the app to build/app.wasm (ENV: development);
|
|
26
|
+
--release builds for deploying (no --incremental)
|
|
26
27
|
test [ARGS] regenerate tmp/test_app.rb, run test/ with spinel_test-unit
|
|
27
|
-
|
|
28
|
+
(natively, or as wasm on the platform's local database, by database.yml's test:)
|
|
29
|
+
generate, g KIND NAME generate a model (NAME field:type ...), a controller (NAME action ...)
|
|
30
|
+
or a scaffold (NAME field:type ...)
|
|
28
31
|
routes print the routes
|
|
29
32
|
version print the version
|
|
30
33
|
TEXT
|
|
@@ -5,15 +5,35 @@ module Edgeway
|
|
|
5
5
|
include Rescuable
|
|
6
6
|
include Rendering
|
|
7
7
|
include Authentication
|
|
8
|
+
include Caching
|
|
9
|
+
include RateLimiting
|
|
8
10
|
|
|
9
11
|
NON_ACTIONS = ["render", "head", "headers", "params", "request", "performed?", "to_response",
|
|
10
12
|
"action?", "run_before_actions", "rescue_with_handler",
|
|
11
|
-
"authenticate!", "current_claims", "authenticated?"
|
|
13
|
+
"authenticate!", "current_claims", "authenticated?",
|
|
14
|
+
"action_name", "action_name=", "raw",
|
|
15
|
+
"expires_in", "no_store", "fresh_when", "stale?"]
|
|
12
16
|
|
|
13
17
|
def self.before_action(name, only: [], except: [])
|
|
18
|
+
add_before_action(Callback.new(name, only.map(&:to_s), except.map(&:to_s)))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.rate_limit(binding_name, by: nil, name: nil, retry_after: nil, only: [], except: [])
|
|
22
|
+
add_before_action(RateLimiting::Rule.build(binding_name, by, name, retry_after, only, except))
|
|
23
|
+
(@own_non_actions ||= []) << by.to_s unless by.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.own_non_actions = (@own_non_actions || [])
|
|
27
|
+
|
|
28
|
+
def self.non_action?(name)
|
|
29
|
+
lineage.any? { |c| c.own_non_actions.include?(name) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.add_before_action(callback)
|
|
14
33
|
@own_before_actions ||= []
|
|
15
|
-
@own_before_actions <<
|
|
34
|
+
@own_before_actions << callback
|
|
16
35
|
end
|
|
36
|
+
private_class_method :add_before_action
|
|
17
37
|
|
|
18
38
|
def self.own_before_actions = (@own_before_actions || [])
|
|
19
39
|
|
|
@@ -48,9 +68,11 @@ module Edgeway
|
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
attr_reader :request
|
|
71
|
+
attr_accessor :action_name
|
|
51
72
|
|
|
52
73
|
def initialize(request)
|
|
53
74
|
@request = request
|
|
75
|
+
@action_name = ""
|
|
54
76
|
@status = 200
|
|
55
77
|
@headers = {}
|
|
56
78
|
@response_body = ""
|
|
@@ -63,9 +85,16 @@ module Edgeway
|
|
|
63
85
|
def headers = @headers
|
|
64
86
|
|
|
65
87
|
def action?(name)
|
|
66
|
-
|
|
88
|
+
return false if name.start_with?("_view_") || name.start_with?("_render_")
|
|
89
|
+
respond_to?(name.to_sym) && !NON_ACTIONS.include?(name) && !self.class.non_action?(name)
|
|
67
90
|
end
|
|
68
91
|
|
|
92
|
+
def raw(text) = View::SafeHTML.new(text)
|
|
93
|
+
|
|
94
|
+
def _render_view(action) = nil
|
|
95
|
+
|
|
96
|
+
def _render_layout(content) = content
|
|
97
|
+
|
|
69
98
|
def params
|
|
70
99
|
@params ||= wrapped_params
|
|
71
100
|
end
|