Search the site
DATA
Declare a data model once and get storage, a form, a table, an admin screen and public pages.
These are the models your data lives in. Dartvel has 3D models too, under Media, 3D and XR.
Every generated name is public, so your code never touches the private class.
ON THIS PAGE
Declare a data model
Data model options
Field annotations
Save, find and delete records
Use the generated form, table and page
Add an admin screen
Serve a public page per record
Catch stale writes
Keep and revert record history
Soft delete and restore
Search records
Protect sensitive fields
Status
// lib/models/article.dart
import 'package:dartvel_core/dartvel.dart';
@DVModel(
semantic: true,
history: DVHistory(keep: Duration(days: 365)),
softDelete: true,
subject: DVSubject.field('authorId'),
retain: DVRetention.indefinite,
)
class const _Article({
required final String slug,
@DVModel.pageTitle() required final String title,
@DVModel.mainContent() required final String body,
@DVModel.searchableField() required final String tags,
required final bool published,
required final String authorId,
@DVModel.sensitiveField() required final String editorNotes,
});Copy code to clipboard
Put it under lib/models, one model per file, as a private class with final fields.
The key is the slug field, else id, else the first String field.
Where records are stored is the framework's job. You read and write them through the model, on whichever database DATABASE_URL names.
Pick a name Dartvel does not use
Dartvel exports Get, Post, Put, Patch and Delete. A model with one of those names clashes with them in your code, so call it Article or BlogPost.
generatePublicPages: false
What it does: No public page per record. Pages are on by default, at /articles/:slug
publicPathsResolver: fn
What it does: Your function lists the paths to render statically
history: DVHistory(...)
What it does: Keeps each version, with history() and revert()
softDelete: true
What it does: destroy() hides the record and restore() brings it back
version: false
What it does: Turns off the stale-write check
searchable: true
What it does: Gives the model Article.search
tenantScoped: true
What it does: Records belong to the current tenant
subject:, retain:
What it does: Who the data is about and how long to keep it
offline: DVConflict.lastWriteWins
What it does: Reads and writes a copy on the device and syncs it when the server can be reached
indexes: [DVIndex([...], unique: true)]
What it does: Asks the database for more indexes than the key
access: DVModelAccess(...)
What it does: Who may view, create, update and delete through the data API, where no @DVPolicy class says
@DVModel.pageTitle()
Effect: The title on its public page
@DVModel.mainContent()
Effect: The body of its public page
@DVModel.featuredImage()
Effect: The image for the page and link previews on pages generated from models
@DVModel.pageOrder(n)
Effect: Where the field sits on the page
@DVModel.hideFromPage()
Effect: Leaves the field off the page
@DVModel.searchableField()
Effect: Indexes the field for search
@DVModel.sensitiveField()
Effect: Kept out of logs, tables, pages and public JSON
@DVModel.model3dField()
Effect: A 3D asset with a poster
@DVModel.retain(years:, because:)
Effect: Keeps one field longer, with the reason
@DVModel.validate(min:, max:, minLength:, maxLength:, pattern:)
Effect: Checked by save() before anything is written, and by Studio
@DVModel.uniqueField()
Effect: A unique index, and a write that repeats a value is refused
final Article article = Article(
slug: 'hello-world',
title: 'Hello, world',
body: 'The first article.',
tags: 'news',
published: false,
authorId: 'user-1',
editorNotes: 'Check the title',
);
await article.save();
final List<Article> all = await Article.all();
final Article? found = await Article.find('hello-world');
final Article published = await found!.copyWith(published: true).save();
await published.destroy();Copy code to clipboard
Model.all() and Model.find(key) read, model.save() creates or updates, and model.destroy() deletes.
copyWith keeps the version you read, so save() can spot a newer write.
Run dartvel db migrate after adding or changing a model. See Database.
Widget articleEditor(Article article) => article.Form();
Widget articleTable(List<Article> articles) => Article.Table(articles);
Widget articlePage(Article article) => Article.Page.sync(article);Copy code to clipboard
Article.Form() creates one and article.Form() edits that one. Saving is what the form does, and it checks the model's rules first. See Forms.
Article.Table(rows) sorts by column and skips sensitive fields.
Article.Page has .sync, .async, .signal and .fromId.
Functional
Class
@DVPage(title: 'Articles admin', policy: DVPolicies.viewAdmin)
Widget _articlesAdminPage(BuildContext context) => Article.Admin();Copy code to clipboard
Article.Admin() lists, creates, edits and deletes records.
It checks the Article.create, Article.update and Article.delete policies.
dartvel admin generate writes admin pages to lib/pages/_dartvel_admin.
Dartvel generates a public page for every record by default. To turn it off for a data model, set generatePublicPages: false on it.
The route comes from the model's name, in plural kebab-case, followed by the record's slug, or its id when the model has no slug: an Article is served at /articles/hello-world, and a BlogPost at /blog-posts/42.
dartvel build web renders the published records' pages statically and lists them in the sitemap.
Sensitive fields, and the field that names the record's subject, stay hidden. Only a viewer your viewSensitive policy allows sees them on the page.
They never appear in page titles, descriptions, structured data, the static build or the sitemap.
A record your view policy refuses gets a 404, the same as one that does not exist.
Users, sessions, tokens, audit logs and tenant data get no page unless you set generatePublicPages: true.
// lib/policies/article_policy.dart
import '../dartvel_client/dartvel_client.dart';
@DVPolicy(Article)
class ArticlePolicy {
// Who may open an article's page. Anyone else gets a 404.
bool view(DVSessionPrincipal? user, Article article) =>
article.published || user?.userId == article.authorId;
// Who also sees editorNotes and authorId on the page.
bool viewSensitive(DVSessionPrincipal? user, Article article) =>
user?.userId == article.authorId;
}Copy code to clipboard
Functional
Class
@DVPage(title: 'Article')
Widget _articlePage(BuildContext context) => Article.Page.fromId(
context.dvParams['slug'] ?? 'hello-world',
findById: (String slug) async => (await Article.find(slug))!,
);Copy code to clipboard
@DVModel(publicPathsResolver: productPaths)
class const _Product({
required final String slug,
required final String name,
required final bool published,
});
// Public, top level, and in the model's own file.
Future<List<String>> productPaths() async => <String>['starter-kit', 'pro-kit'];Copy code to clipboard
Partial
Spec section: Generated Model Pages
Planned work and implementation limits
A favicon taken from a record's featured image is not built.
Each save checks the version you read. A newer write in between throws DVConflictError.
try {
await article.save();
} on DVConflictError catch (conflict) {
// Someone saved a newer version after you read this one.
debugPrint('Stored version ${conflict.actualVersion}');
}
// Replace the stored row on purpose.
await Article.save(article, onConflict: DVConflict.lastWriteWins);Copy code to clipboard
DVConflict also has serverWins, fieldMerge and DVConflict.resolver(...).
// Needs @DVModel(history: DVHistory(...)).
final List<DVHistoryEntry> entries = await article.history();
for (final DVHistoryEntry entry in entries) {
debugPrint('version ${entry.version} at ${entry.at}');
}
await article.revert(to: entries.first);Copy code to clipboard
Partial
Spec section: Record History and Optimistic Concurrency
Planned work and implementation limits
Forms do not reload and merge on a conflict yet.
No Studio history view, scheduled prune or database-level atomic transactions.
// Needs @DVModel(softDelete: true).
await article.destroy(); // hidden from find() and all()
final Article? deleted = await Article.withDeleted.find(article.slug);
final List<Article> everything = await Article.withDeleted.all();
await Article.restore(article.slug); // visible againCopy code to clipboard
Article.useSearchProvider(DVInMemorySearchProvider<Article, ArticleFacets>(
records: await Article.all(),
document: (Article article) => '${article.title} ${article.body}',
));
final DVSearchResultPage<Article> page = await Article.search('dart', perPage: 10);Copy code to clipboard
Set a provider before you query. With none, a query throws.
DVPostgresSearchProvider searches with Postgres full text.
A sensitive field needs subject: on the model, or the build stops with DV-PRIVACY-001.
encrypted: true encrypts a String field with keys from DARTVEL_FIELD_KEYS.
onErase: DVErase.anonymize keeps the record and blanks the field on erasure.
Partial
Spec section: Sensitive Model Fields
Planned work and implementation limits
Writes that skip the model, such as imports and backfills, store encrypted fields as plain text.
No key rotation, and showInAdmin does not gate anything yet.
Built
Spec section: Models
FSL-1.1-MIT licensed. Built with Dartvel.
Dartvel is made by
To the bottom