erupt-sdk 0.1.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 +7 -0
- data/README.md +273 -0
- data/lib/erupt/defaults.json +1577 -0
- data/lib/erupt/http_server.rb +115 -0
- data/lib/erupt/model.rb +499 -0
- data/lib/erupt/node.rb +555 -0
- data/lib/erupt/repository.rb +113 -0
- data/lib/erupt/schema.json +2137 -0
- data/lib/erupt/schema.rb +68 -0
- data/lib/erupt/signature.rb +30 -0
- data/lib/erupt/sql.rb +226 -0
- data/lib/erupt/types.rb +300 -0
- data/lib/erupt/version.rb +5 -0
- data/lib/erupt-sdk.rb +4 -0
- data/lib/erupt.rb +12 -0
- data/sig/erupt/types.rbs +408 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f84d68ffc08198006a4a6e0533eb474c59a12afd898454366efff89a289ab267
|
|
4
|
+
data.tar.gz: bd8868651a4662eaa2226414fbd8348f84056b4cfbe8859bb987595d2d0be6e3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 53715a45d54b3e35fbfeca07deef44c1f2fe3ee769a8fd37bb842d9f918a5135697d2afec38a171e6498cb9a1d1ba58110a981dd435acb846aae728990e91374
|
|
7
|
+
data.tar.gz: f308104ca642f1d9e8e5aef727e9fca0bb9d4b9c00183af5adf84950dcdc29baafd4b5b6a78d2b614906f7606e3850dc7aaabde5c5ce497fb9ce0a017eabd302
|
data/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# erupt-sdk (Ruby)
|
|
2
|
+
|
|
3
|
+
Serve Ruby models as an erupt admin UI. Zero gem dependencies, Ruby 3.1+.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gem install erupt-sdk # or in a Gemfile: gem "erupt-sdk"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "erupt"
|
|
11
|
+
|
|
12
|
+
class Article
|
|
13
|
+
extend Erupt::Model
|
|
14
|
+
erupt "Article", order_by: "id desc"
|
|
15
|
+
field :id, pk: true
|
|
16
|
+
field :title, "Title", search: true, not_null: true, sortable: true
|
|
17
|
+
field :status, "Status", choices: { draft: "Draft", published: "Published" }, search: true
|
|
18
|
+
field :publish_date, "Publish Date", Date, search: true
|
|
19
|
+
field :body, "Body", type: Erupt::EditType::TEXTAREA, table: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
node = Erupt::Node.new("blog", ENV.fetch("ERUPT_ACCESS_TOKEN"), "http://erupt-server:8080")
|
|
23
|
+
node.register(Article, Erupt::MemoryRepository.new)
|
|
24
|
+
node.serve(port: 9090)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
1. On the erupt server create node `blog` (Node Config) and copy its access token.
|
|
28
|
+
2. Add a menu of type `table` with value `blog.Article`.
|
|
29
|
+
3. Run the node. The server must be able to reach it on `http://<node ip>:9090`; pass `address:` otherwise.
|
|
30
|
+
|
|
31
|
+
`field name, title, kind` infers the edit type from `kind`: `String` (or nothing) INPUT, `Integer` / `Float`
|
|
32
|
+
NUMBER, `:boolean` BOOLEAN, `Date` DATE, `Time` DATE_TIME, `choices:` CHOICE; `type:` overrides it.
|
|
33
|
+
`view: {...}` / `edit: {...}` on a field and extra keywords on `erupt` pass raw annotation attributes through.
|
|
34
|
+
Enum values have constants (`Erupt::EditType::TEXTAREA`, `Erupt::RowOperationMode::SINGLE`, ...).
|
|
35
|
+
|
|
36
|
+
Records are hashes keyed by field name (`row["title"]`); hashes handed to a repository may use symbols.
|
|
37
|
+
|
|
38
|
+
## Rack
|
|
39
|
+
|
|
40
|
+
A node is a Rack application, so it can live inside an existing app instead of on its own port. `start`
|
|
41
|
+
registers it (with the port the server reaches it on) and `stop` deregisters it:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# config/routes.rb (Rails): mount at the root, the server calls /erupt-api/... and /node-health
|
|
45
|
+
mount ERUPT_NODE, at: "/"
|
|
46
|
+
|
|
47
|
+
# config/initializers/erupt.rb
|
|
48
|
+
ERUPT_NODE = Erupt::Node.new("blog", ENV.fetch("ERUPT_ACCESS_TOKEN"), ENV.fetch("ERUPT_SERVER"), logger: Rails.logger)
|
|
49
|
+
ERUPT_NODE.register(Article, repo)
|
|
50
|
+
ERUPT_NODE.start(port: 3000)
|
|
51
|
+
at_exit { ERUPT_NODE.stop }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`serve` runs a small built-in threaded HTTP/1.1 server (keep-alive, chunked bodies) for the standalone case.
|
|
55
|
+
|
|
56
|
+
## References
|
|
57
|
+
|
|
58
|
+
Pass another model class (declared earlier, registered on the same node) as the kind. The repository stores
|
|
59
|
+
the referenced primary key; the UI shows `label:` (default `name`).
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class Category
|
|
63
|
+
extend Erupt::Model
|
|
64
|
+
erupt "Category"
|
|
65
|
+
field :id, pk: true
|
|
66
|
+
field :name, "Name", search: true
|
|
67
|
+
field :parent, "Parent Id", Integer
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Article
|
|
71
|
+
# ...
|
|
72
|
+
field :category, "Category", Category, search: true # table picker
|
|
73
|
+
field :topic, "Topic", Category, tree: :parent # tree picker over Category.parent
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Row operations
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class PublishForm # a form model needs no primary key
|
|
81
|
+
extend Erupt::Model
|
|
82
|
+
erupt "Publish Options"
|
|
83
|
+
field :publish_date, "Publish Date", Date, not_null: true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Article
|
|
87
|
+
# ...
|
|
88
|
+
operation :publish, "Publish", form: PublishForm, if_expr: "item.status !== 'published'" do |op|
|
|
89
|
+
op.rows.each { |row| op.repo.update(row["id"], status: "published", publish_date: op.param["publish_date"]) }
|
|
90
|
+
end # op.rows, op.param, op.repo, op.user
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`mode:` `SINGLE` / `MULTI` (default) / `MULTI_ONLY` / `BUTTON`. `confirm: false` skips the dialog, a string
|
|
95
|
+
replaces its text. `show: ->(user) { ... }` hides the operation and refuses to run it. A returned string is
|
|
96
|
+
executed as JavaScript in the browser, so return only code you wrote.
|
|
97
|
+
|
|
98
|
+
## DataProxy
|
|
99
|
+
|
|
100
|
+
Hooks around a model's reads and writes, as erupt's Java `DataProxy`. Override what you need; raise
|
|
101
|
+
`Erupt::Fail` to refuse with a message the user sees.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class ArticleProxy < Erupt::DataProxy
|
|
105
|
+
def add_behavior(record) # the new form's values
|
|
106
|
+
record["status"] = "draft"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate(record) # before every add and update
|
|
110
|
+
raise Erupt::Fail, "no spam" if record["title"].to_s.include?("spam")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def before_fetch(conditions) # SQL ANDed to the table's WHERE, values bound
|
|
114
|
+
["owner = ?", [Erupt.current_user["uid"]]]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def after_fetch(rows) # a table page, before it is sent
|
|
118
|
+
rows.each { |row| row["title"] = row["title"].strip }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class Article
|
|
123
|
+
extend Erupt::Model
|
|
124
|
+
erupt "Article", data_proxy: ArticleProxy # one proxy or an array, classes or instances
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Hook | Runs |
|
|
129
|
+
|------|------|
|
|
130
|
+
| `search_condition(condition)` | building the model: default values of the search bar |
|
|
131
|
+
| `before_fetch(conditions)` | before a table page is read: SQL ANDed to its WHERE as written, `"sql"` or `["sql with ?", [values]]` (with values, each `?` is one of them); SQL repositories only, `MemoryRepository` refuses it |
|
|
132
|
+
| `after_fetch(rows)` | after a table page is read |
|
|
133
|
+
| `add_behavior(record)` | for the new record form (`init-value`) |
|
|
134
|
+
| `edit_behavior(record)` | after a record is read by its key (edit form, detail view) |
|
|
135
|
+
| `validate(record)` | before an add or an update |
|
|
136
|
+
| `before_add` / `after_add(record)` | around `repository.add` |
|
|
137
|
+
| `before_update` / `after_update(record)` | around `repository.update`, with the submitted values |
|
|
138
|
+
| `before_delete` / `after_delete(record)` | around `repository.delete`, once per record as stored |
|
|
139
|
+
|
|
140
|
+
## Views
|
|
141
|
+
|
|
142
|
+
`vis:` declares the extra views of `@Erupt(vis = ...)`. A view's `filter` (one SQL string or an array, ANDed as
|
|
143
|
+
written) and `orderBy` (as `order_by`) are applied by the node when the table is read for that view, and kept
|
|
144
|
+
out of the build, as Java keeps them `@Transient`. A view without a `code` is addressed by its title. Like
|
|
145
|
+
`before_fetch`, a filter needs a SQL repository.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class Task
|
|
149
|
+
extend Erupt::Model
|
|
150
|
+
erupt "Task", vis: [
|
|
151
|
+
{ title: "Board", type: "BOARD", boardView: { groupField: "status" }, filter: "archived = 0" },
|
|
152
|
+
{ title: "Recent", type: "TABLE", orderBy: "created desc" }
|
|
153
|
+
]
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Dragging on a board, calendar or gantt view is saved by the erupt server through the node's `get` and `update`.
|
|
158
|
+
|
|
159
|
+
## Trees
|
|
160
|
+
|
|
161
|
+
`tree: { id: "id", label: "name", pid: "parent" }` shows the model as a tree, and
|
|
162
|
+
`linkTree: { field: "category" }` puts the tree of a reference field beside the table: the erupt server builds
|
|
163
|
+
both trees from the node's table, and the node keeps the rows under the picked tree node (`dependNode: true`
|
|
164
|
+
shows none until one is picked). `linkTree.field` must be a reference field.
|
|
165
|
+
|
|
166
|
+
## Field types and field code
|
|
167
|
+
|
|
168
|
+
Besides the inferred types, `type:` takes `TAGS`, `AUTO_COMPLETE`, `ATTACHMENT` (the erupt server stores the
|
|
169
|
+
upload, the node keeps its path), `HTML_EDITOR`, `MARKDOWN`, `CODE_EDITOR` (all text), `BUTTON` (form only),
|
|
170
|
+
and `CHECKBOX` / `TAB_TREE` with `ref:`: the picked records, stored as the JSON list of their keys. An `Array`
|
|
171
|
+
field with `choices:` is a `MULTI_CHOICE`.
|
|
172
|
+
|
|
173
|
+
A field can run its own code; the erupt server calls it through the node's `invoke` endpoint with the form's
|
|
174
|
+
values:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class Order
|
|
178
|
+
extend Erupt::Model
|
|
179
|
+
erupt "Order"
|
|
180
|
+
field :id, pk: true
|
|
181
|
+
field :status, "Status", fetch: ->(form) { { new: "New", paid: "Paid" } } # options, also TAGS: [...]
|
|
182
|
+
field :qty, "Qty", Integer, onchange: ->(form) { { formData: { total: form["qty"] * 9 }, editExpr: {} } }
|
|
183
|
+
field :city, "City", type: Erupt::EditType::AUTO_COMPLETE, complete: ->(form, typed) { lookup(typed) }
|
|
184
|
+
field :total, "Total", Float
|
|
185
|
+
field :recalc, "Recalc", type: Erupt::EditType::BUTTON, click: ->(form) { { eval: "alert('done')", formData: {} } }
|
|
186
|
+
field :tags, "Tags", Array, type: Erupt::EditType::CHECKBOX, ref: Tag
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`fetch` belongs to `CHOICE` / `MULTI_CHOICE` (`{ value => label }` or pairs) and `TAGS` (an array), `complete`
|
|
191
|
+
to `AUTO_COMPLETE`, `click` to `BUTTON`; `onchange` to any field. Raise `Erupt::Fail` to refuse with a message.
|
|
192
|
+
|
|
193
|
+
Also: `COLOR`, `ICON`, `SIGNATURE`, `MAP`, `HIDDEN` (text), `RATE`, `SLIDER` (numbers) and `TRANSFER` (like
|
|
194
|
+
`CHECKBOX`). A reference table depending on another field takes
|
|
195
|
+
`edit: { referenceTableType: { dependField: "province", dependColumn: "province" } }`: the erupt server keeps the
|
|
196
|
+
picked model's rows whose `dependColumn` holds the depend field's value.
|
|
197
|
+
|
|
198
|
+
## Drills
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
erupt "Author", drills: [{ title: "Books", link: { linkErupt: "Book", joinColumn: "author" } }]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
opens `Book` filtered to the rows whose `author` holds the author's `id` (`link.column`, `id` by default);
|
|
205
|
+
`link.linkCondition` adds SQL as written. Both models must be on the node.
|
|
206
|
+
|
|
207
|
+
## Server config and user info
|
|
208
|
+
|
|
209
|
+
`node.config` and `node.group_config` return the config text set for the node and its group on the erupt
|
|
210
|
+
server; `node.user_info` the acting user's full info (roles, org, post), while a request is served.
|
|
211
|
+
|
|
212
|
+
## Storage
|
|
213
|
+
|
|
214
|
+
Storage is any `Erupt::Repository` (`page`, `get`, `add`, `update`, `delete`). Shipped:
|
|
215
|
+
|
|
216
|
+
- `MemoryRepository` for tests and demos;
|
|
217
|
+
- `SqlRepository.new(executor, table, auto_create: false)` on PostgreSQL, MySQL or SQLite. The SDK depends on
|
|
218
|
+
no driver: an `Erupt::SqlExecutor` runs each statement with bound values on the driver you hand it. Create
|
|
219
|
+
the tables yourself, with columns named like the fields (`auto_create:` creates SQLite tables).
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
require "pg" # PostgreSQL (pg gem)
|
|
223
|
+
repo = Erupt::SqlRepository.new(Erupt::SqlExecutor.pg(PG.connect(ENV.fetch("DATABASE_URL"))), "article")
|
|
224
|
+
|
|
225
|
+
require "mysql2" # MySQL (mysql2 gem; statements are prepared)
|
|
226
|
+
repo = Erupt::SqlRepository.new(Erupt::SqlExecutor.mysql2(Mysql2::Client.new(**MYSQL)), "article")
|
|
227
|
+
|
|
228
|
+
require "sqlite3" # SQLite (sqlite3 gem)
|
|
229
|
+
repo = Erupt::SqlRepository.new(Erupt::SqlExecutor.sqlite3(SQLite3::Database.new("blog.db")), "article", auto_create: true)
|
|
230
|
+
|
|
231
|
+
require "connection_pool" # pooled: anything with `with` lends a connection per statement
|
|
232
|
+
repo = Erupt::SqlRepository.new(Erupt::SqlExecutor.pg(ConnectionPool.new(size: 5) { PG.connect(DSN) }), "article")
|
|
233
|
+
|
|
234
|
+
# any other library: a block that runs (sql, params) and returns rows as hashes keyed by column name
|
|
235
|
+
executor = Erupt::SqlExecutor.new(:postgres) do |sql, params|
|
|
236
|
+
ActiveRecord::Base.connection_pool.with_connection { |c| c.raw_connection.exec_params(sql, params).to_a }
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
A single connection is used by one thread at a time. Without a sort, every repository returns rows by primary
|
|
241
|
+
key ascending. `Erupt.current_user` returns the acting user inside a repository call.
|
|
242
|
+
|
|
243
|
+
Not yet: sub-tables (TAB_TABLE_*), TPL operations, i18n of messages. Trees, file
|
|
244
|
+
upload and Excel are done by the erupt server.
|
|
245
|
+
|
|
246
|
+
## Without a registry
|
|
247
|
+
|
|
248
|
+
From the repository, without RubyGems (Bundler):
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
gem "erupt-sdk", git: "https://github.com/erupt-io/erupt-sdk", tag: "ruby/v0.1.0", glob: "ruby/*.gemspec"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Or copy `ruby/lib/` (`erupt.rb` and `erupt/`, including its `.json` files) onto your load path and
|
|
255
|
+
`require "erupt"`. A copy does not update itself: when erupt changes the protocol or the annotation schema, replace the whole directory with the next release instead of merging by hand, and leave `Erupt::VERSION` untouched so the server shows which release the node runs.
|
|
256
|
+
|
|
257
|
+
## Generated code
|
|
258
|
+
|
|
259
|
+
`lib/erupt/types.rb` (a constant for every annotation enum value) and `sig/erupt/types.rbs` (RBS records for
|
|
260
|
+
every annotation attribute: `edit`, `view`, `erupt`, `row_operation`, ...) are generated from the erupt
|
|
261
|
+
annotation JSON Schema. Raw `view:` / `edit:` / `erupt` / `operation` attributes are checked against the same
|
|
262
|
+
schema when a model is declared, so a misspelt key or enum value fails at load instead of silently doing
|
|
263
|
+
nothing in the browser.
|
|
264
|
+
|
|
265
|
+
After an annotation change in erupt (regenerate there with `mvn -pl erupt-test -Pnode-schema test`):
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
ruby tools/gen_types.rb ../schema # sync schemas, regenerate types.rb / types.rbs
|
|
269
|
+
ruby tools/gen_types.rb ../schema --check # CI: fail when stale
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Tests: `ruby -Ilib -e 'Dir["./test/test_*.rb"].each { |f| require f }'` (SQLite-backed cases run where the
|
|
273
|
+
`sqlite3` gem is installed; `ERUPT_TEST_DATABASES=1` with the `pg` and `mysql2` gems runs `test_databases.rb`).
|