Scaffold Generator
Quickly generate complete MVC resources with models, controllers, views, tests, and migrations.
Native / mobile generators
| Command | Creates |
|---|---|
soli generate devices | Device registration |
soli generate client … | Native shells |
soli generate app_links | Deep-link proofs |
soli generate offline | Outbox sync |
Basic Usage
Generate a scaffold for a resource:
$ soli generate scaffold users $ soli generate scaffold products name:string price:decimal
What Gets Generated
Running soli generate scaffold users creates:
Field Types
Specify fields with name:type syntax:
| Type | HTML Input | Example |
|---|---|---|
| string | text | name:string |
| text | textarea | content:text |
| email (unique index) | email:email | |
| password | password (unique index) | password:password |
| integer | number | age:integer |
| float | number | price:float |
| boolean | checkbox | active:boolean |
| date | date picker | birthdate:date |
Generated Code
Running soli generate scaffold users name:string email:text generates:
Model
class Users < Model
static
# Fields
# name (string)
# email (text)
# Validations (auto-generated)
validates("name", { "presence": true })
validates("email", { "presence": true })
end
before_save("normalize_fields") # symbols also work: before_save(:normalize_fields)
end
Controller
class UsersController < Controller
static
this.layout = "application"
end
def index
users = Users.all()
render("users/index", {
"title": "Users"
})
end
def show
user = Users.find(params["id"])
render("users/show", {
"title": "View User"
})
end
def create
permitted = this._permit_params(params)
user = Users.create(permitted)
if user["valid"] == true
return redirect("/users")
end
render("users/new", {
"title": "New User"
})
end
def update
id = params["id"]
permitted = this._permit_params(params)
Users.update(id, permitted)
return redirect("/users")
end
def delete
id = params["id"]
Users.delete(id)
return redirect("/users")
end
# Mass assignment protection
def _permit_params(params)
{
"name": params["name"],
"email": params["email"]
}
end
end
Views
Index View
<div class="p-6">
<h1 class="text-2xl font-bold">Users</h1>
<table class="w-full">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% for user in users %>
<tr>
<td><%= user["id"] %></td>
<td><%= user["name"] %></td>
<td>
<a href="/users/<%= user["id"] %>">Show</a>
<a href="/users/<%= user["id"] %>/edit">Edit</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<a href="/users/new">New User</a>
</div>
Form Partial (shared by new/edit)
<form action="/users" method="POST">
<input type="text" name="name" value="<%= user["name"] %>">
<input type="text" name="email" value="<%= user["email"] %>">
<button type="submit">Submit</button>
</form>
New View
<h1 class="text-2xl font-bold mb-6">New User</h1>
<%= render("users/_form", { "user": user }) %>
<a href="/users" class="text-amber-400 hover:text-amber-300">← Back to Users</a>
Edit View
<h1 class="text-2xl font-bold mb-6">Edit User</h1>
<%= render("users/_form", { "user": user }) %>
<a href="/users" class="text-amber-400 hover:text-amber-300">← Back to Users</a>
Show View
<div class="p-6">
<h1 class="text-2xl font-bold mb-4">User Details</h1>
<dl class="space-y-4">
<div>
<dt class="text-sm text-gray-400">ID</dt>
<dd class="text-lg text-white"><%= user["id"] %></dd>
</div>
<div>
<dt class="text-sm text-gray-400">Name</dt>
<dd class="text-lg text-white"><%= user["name"] %></dd>
</div>
</dl>
<a href="/users/<%= user["id"] %>/edit" class="bg-yellow-600 text-white px-4 py-2 rounded">Edit</a>
<a href="/users" class="text-amber-400 ml-4">← Back</a>
</div>
Migration
def up(db)
db.create_collection("users")
end
def down(db)
db.drop_collection("users")
end
Tests
The generator writes one spec: the controller E2E file below. There is no model spec — add one yourself (tests/users_model_spec.sl) if you want model-level coverage. It would look like this:
describe("UsersModel") do
test("creates a valid record") do
result = Users.create({ "name": "Test User" })
assert(result["valid"])
end
end
Controller Test
describe("UsersController") do
before_each() do
as_guest()
end
describe("GET /users") do
test("returns list of users") do
response = get("/users")
assert_eq(res_status(response), 200)
end
test("renders with correct view assigns") do
response = get("/users")
assert(render_template())
assert_eq(view_path(), "users/index.html")
data = assigns()
assert_hash_has_key(data, "users")
end
end
describe("GET /users/:id") do
test("returns 404 for missing record") do
response = get("/users/99999")
assert_eq(res_status(response), 404)
end
end
describe("POST /users") do
test("creates new record with valid data") do
response = post("/users", {"name": "Test user"})
assert_eq(res_status(response), 302)
end
test("rejects invalid data") do
response = post("/users", {})
assert_eq(res_status(response), 422)
end
end
end
Auto-Validations
Fields with types string, text, email, password, and url automatically get presence: true validation:
class Users < Model
static
# Fields
# name (string)
# email (email)
# Validations (auto-generated)
validates("name", { "presence": true })
validates("email", { "presence": true })
end
before_save("normalize_fields") # symbols also work: before_save(:normalize_fields)
end
Mass Assignment Protection
Scaffolded controllers include built-in mass assignment protection to prevent security vulnerabilities. The _permit_params method whitelists only the allowed fields:
class UsersController < Controller
# POST /users
def create
# Only permit whitelisted fields
permitted = this._permit_params(params)
user = Users.create(permitted)
end
# Mass assignment protection
def _permit_params(params)
{
"name": params["name"],
"email": params["email"]
}
end
end
Security Best Practice
-
Never pass raw
req.paramsdirectly to model operations -
Always use
_permit_params()to filter input - Add sensitive fields (is_admin, role, permissions) to the blacklist if needed
Generated Routes
Scaffold automatically adds RESTful routes:
| HTTP | Path | Action |
|---|---|---|
| GET | /users | index |
| GET | /users/new | new |
| POST | /users | create |
| GET | /users/:id | show |
| GET | /users/:id/edit | edit |
| PUT | /users/:id | update |
| DELETE | /users/:id | delete |
Example
Generate a complete blog posts resource:
$ soli generate scaffold posts title:string content:text author:string published:boolean Success! Created scaffold for posts
Next Steps
- Collections are auto-created - no migration needed!
-
Start server:
soli serve . --dev -
Visit:
http://localhost:5011/posts - Tip: Use migrations to define indexes for better query performance in production.