diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 093336c76..ca504f3f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,8 +48,11 @@ storm-framework/ ├── storm-spring/ # Spring integration (Java) ├── storm-kotlin-spring-boot-starter/ # Spring Boot starter (Kotlin) ├── storm-spring-boot-starter/ # Spring Boot starter (Java) +├── storm-spring-boot-test-autoconfigure/ # @DataStormTest test slice (both starters) ├── storm-ktor/ # Ktor integration (Kotlin) ├── storm-ktor-test/ # Ktor integration test support +├── storm-micrometer/ # Micrometer Observations binding +├── storm-gradle-plugin/ # Gradle plugin (id("st.orm")) │ ├── storm-kotlinx-serialization/ # kotlinx.serialization support ├── storm-jackson2/ # Jackson 2.x JSON support diff --git a/README.md b/README.md index 6820b44e6..5d89e2402 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,10 @@ Storm is modular; you add only what your stack uses. Versions come from the BOM | **Java + Spring Boot** | `storm-spring-boot-starter` | | **+ your database** | one dialect (runtime): `storm-postgresql`, `storm-mysql`, `storm-mariadb`, `storm-oracle`, `storm-mssqlserver`, `storm-sqlite`, `storm-h2` | | **+ JSON columns** | `storm-jackson2`, `storm-jackson3`, or `storm-kotlinx-serialization` | +| **+ metrics & tracing** | `storm-micrometer` (included in the Spring Boot starters) | +| **+ Spring Boot test slice** | `storm-spring-boot-test-autoconfigure` (test scope, `@DataStormTest`) | -The `storm-compiler-plugin-2.x` suffix matches your Kotlin major.minor version (e.g. `storm-compiler-plugin-2.0` for Kotlin 2.0.x). See [Installation](https://orm.st/docs/installation) for the full module overview. +The `storm-compiler-plugin-2.x` suffix matches your Kotlin major.minor version (e.g. `storm-compiler-plugin-2.0` for Kotlin 2.0.x). Gradle projects can skip the per-module setup entirely with the Storm Gradle plugin: `id("st.orm")` applies the BOM, core dependencies, metamodel processing, and compiler flags in one block. See [Installation](https://orm.st/docs/installation) for the full module overview. ### Dependency Management (BOM) diff --git a/docs/configuration.md b/docs/configuration.md index 4099903b7..389de6800 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -646,7 +646,7 @@ Storm's Kotlin API uses the Storm compiler plugin to automatically wrap string i See [String Templates](string-templates.md) for setup instructions for the compiler plugin. :::tip Monitoring -Storm exposes runtime metrics for template compilation, dirty checking, and entity cache behavior through JMX MBeans. See [Metrics](metrics.md) for details. +Storm exposes runtime metrics for template compilation, dirty checking, and entity cache behavior through JMX MBeans; see [Metrics](metrics.md). For query and transaction metrics and tracing through your observability stack, add the `storm-micrometer` module (included in the Spring Boot starters). ::: --- diff --git a/docs/installation.md b/docs/installation.md index 6e915d301..1f6727bd4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -279,6 +279,23 @@ Storm supports storing and reading JSON-typed columns. Pick the module that matc See [JSON Support](json.md) for usage details. +### Observability + +| Module | Provides | +|--------|----------| +| `storm-micrometer` | Micrometer Observations for queries and transactions (`storm.query`, `storm.transaction`), the OpenTelemetry database semantic conventions, and trace-context SQL comments | + +The Spring Boot starters include `storm-micrometer`; Ktor applications add it explicitly. See the observability sections of [Spring Integration](spring-integration.md#observability) and [Ktor Integration](ktor-integration.md#observability). + +### Testing + +| Module | Provides | +|--------|----------| +| `storm-test` | `@StormTest` JUnit 5 extension and `SqlCapture`, framework-free | +| `storm-spring-boot-test-autoconfigure` | The `@DataStormTest` Spring Boot test slice (test scope) | + +See [Testing](testing.md) and [Testing with @DataStormTest](spring-integration.md#testing-with-datastormtest). + ## Module Overview The following diagram shows how Storm's modules relate to each other. You only need the modules relevant to your language and integration choices. diff --git a/docs/metrics.md b/docs/metrics.md index 5b78857a2..7af75b774 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -5,6 +5,10 @@ import TabItem from '@theme/TabItem'; Storm exposes runtime metrics through JMX (Java Management Extensions) MBeans. These metrics give you visibility into template compilation performance, dirty checking behavior, and entity cache efficiency. All MBeans are registered automatically when Storm initializes and aggregate across all `ORMTemplate` instances in the JVM. +:::tip Micrometer Observations +The metrics on this page are JMX-based and always on. For metrics and tracing through your observability stack, the `storm-micrometer` module reports every query and transaction as Micrometer Observations; see the observability sections of [Spring Integration](spring-integration.md#observability) and [Ktor Integration](ktor-integration.md#observability). +::: + To view these metrics, connect to the JVM with any JMX client (JConsole, VisualVM, or your monitoring platform) and navigate to the `st.orm` domain. If your application uses Spring Boot Actuator, the MBeans are also accessible through Actuator's JMX endpoint. --- diff --git a/docs/repositories.md b/docs/repositories.md index 47b5ef67b..5d843c640 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -581,8 +581,8 @@ Storm repositories are plain interfaces, so Spring cannot discover them through @Configuration class AcmeRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - override val repositoryBasePackages: Array - get() = arrayOf("com.acme.repository") + override fun getRepositoryBasePackages(): Array = + arrayOf("com.acme.repository") } ``` diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 7fa15522d..f7dff4822 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -46,7 +46,7 @@ const CSS = ` .storm-home .btn.primary.go{background:linear-gradient(100deg,#a78bfa,#818cf8 50%,#7dd3fc);border-color:transparent} .storm-home .btn.primary.go:hover{filter:brightness(1.12)} - /* hero — left aligned */ + /* hero: left aligned */ .storm-home header{padding:46px 0 34px} .storm-home h1{font-size:clamp(42px,7vw,78px);line-height:.97;letter-spacing:-.04em;font-weight:800;margin:0} .storm-home .grad{background:linear-gradient(100deg,#a78bfa,#818cf8 50%,#7dd3fc);-webkit-background-clip:text;background-clip:text;color:transparent} @@ -359,15 +359,15 @@ export default function Home() { caption:"one-line queries to get all the data you need · no N+1", code:[ C("// A user's city is loaded in the same query.\n"), K("val "),P("user = userRepository."),F("getById"),P("("),N("1"),P(")\n"), - K("val "),P("cityName = user.city.name"),P(" "),C('// already loaded — no N+1, no lazy-init\n\n'), + K("val "),P("cityName = user.city.name"),P(" "),C('// already loaded: no N+1, no lazy-init\n\n'), C("// Filter across the graph, fully type-safe using the static metamodel.\n"), K("val "),P("users = userRepository."),F("findAll"),P("(User_.city.name "),K("eq "),S('"Sunnyvale"'),P(")") ] }, { name:'3 · repository', file:'UserRepository.kt', caption:"your own type-safe queries · CRUD inherited", - code:[ C("// Custom return types are just records — define them in-place.\n"), + code:[ C("// Custom return types are just records. Define them in-place.\n"), K("data class "),T("CityCount"),P("("),K("val "),P("city: "),T("City"),P(", "),K("val "),P("count: "),T("Long"),P(")\n\n"), - C("// Extend EntityRepository — all CRUD comes for free.\n"), + C("// Extend EntityRepository: all CRUD comes for free.\n"), K("interface "),T("UserRepository"),P(" : "),T("EntityRepository"),P("<"),T("User"),P(", "),T("Int"),P("> {\n"), P(" "),K("fun "),F("findByCity"),P("(city: "),T("City"),P(") = "),F("findAll"),P("(User_.city "),K("eq "),P("city)\n\n"), C(" // Query builder with SQL templates for the aggregate.\n"), @@ -386,7 +386,7 @@ export default function Home() { P(" orm "),K("insert "),T("User"),P("(email = "),S('"bob@acme.io"'),P(", name = "),S('"Bob"'),P(", city = city)\n"), P("}\n"), P("\n"), - C("// Full control when you need it: propagation, isolation, timeout — plus post-tx hooks.\n"), + C("// Full control when you need it: propagation, isolation, timeout, plus post-tx hooks.\n"), F("transaction"),P("(propagation = "),T("REQUIRES_NEW"),P(", isolation = "),T("REPEATABLE_READ"),P(", timeoutSeconds = "),N("5"),P(") {\n"), P(" "),K("val "),P("city = orm "),K("insert "),T("City"),P("(name = "),S('"San Jose"'),P(", population = "),N("1_013_240"),P(", country = "),S('"US"'),P(")\n"), P(" "),K("val "),P("user = orm "),K("insert "),T("User"),P("(email = "),S('"alice@acme.io"'),P(", name = "),S('"Alice"'),P(", city = city)\n"), @@ -395,7 +395,7 @@ export default function Home() { P("}") ] }, { name:'5 · sql', file:'UserService.kt', - caption:"full SQL when you want it — never locked in", + caption:"full SQL when you want it, never locked in", code:[ C("// Full control of SQL, with typed columns and tables; rows map to any data class.\n"), K("data class "),T("RankedCity"),P("("),K("val "),P("name: "),T("String"),P(", "),K("val "),P("rank: "),T("Long"),P(")\n\n"), K("val "),P("ranked = orm."),F("query"),P(" { "),S('"""'),P("\n"), @@ -430,9 +430,9 @@ export default function Home() { // Generated SQL per scene (index-aligned with SCENES). Shown via the "Show SQL" toggle. const SQL=[ - null, // entities — no query, so the Show SQL button is hidden for this scene + null, // entities: no query, so the Show SQL button is hidden for this scene - '-- getById(1) — joins the city graph, no N+1\n'+ + '-- getById(1): joins the city graph, no N+1\n'+ 'SELECT u.id, u.email, u.name, c.id, c.name, c.population, c.country\n'+ 'FROM "user" u\n'+ 'INNER JOIN city c ON u.city_id = c.id\n'+ @@ -482,7 +482,7 @@ export default function Home() { benefitsEl=document.getElementById('benefits'), codeareaEl=document.querySelector('.storm-home .codearea'); if(!codeEl) return; - // "Show SQL" toggle — reveals the generated SQL for the current scene. + // "Show SQL" toggle: reveals the generated SQL for the current scene. const editorEl=document.querySelector('.storm-home .editor'), sqlBtn=document.getElementById('sqlbtn'), sqlBtnText=document.getElementById('sqlbtntext'), sqlPanel=document.getElementById('sqlpanel'); let showSql=false, curIdx=0; @@ -546,7 +546,7 @@ export default function Home() { fnameEl.textContent=sc.file; statusEl.classList.remove('show'); - // Benefits scene — render the core-benefits grid instead of typed code. + // Benefits scene: render the core-benefits grid instead of typed code. if(sc.grid){ codeareaEl.classList.add('show-benefits'); benefitsEl.innerHTML=sc.grid.map(b=>'
'+b.t+''+b.d+'
').join(''); @@ -573,7 +573,7 @@ export default function Home() { // Rewind the horizontal scroller before typing. Desktop resets it for // free (the near-empty first render collapses scrollWidth, clamping // scrollLeft to 0), but iOS Safari keeps the stale offset when content - // shrinks — so one touch pan (even an accidental diagonal swipe while + // shrinks, so one touch pan (even an accidental diagonal swipe while // scrolling the page) would leave every later scene with the start of // each line cut off. codeEl.scrollLeft=0; @@ -715,9 +715,9 @@ export default function Home() { description: 'Storm is a type-safe, SQL-first Kotlin ORM. Define concise, immutable data-class entities and write one-line queries. Nested predicates and entity graphs compile to a single efficient query, eliminating accidental hidden N+1 queries. Drop to full SQL templates whenever you want; never locked in.', featureList: [ - 'Direct database control — every query explicit, no hidden N+1', - 'Stateless, immutable records — no proxies, no flush, no hidden state', - 'Type-safe and injection-safe — compile-time column and type checks, automatic bind parameters', + 'Direct database control: every query explicit, no hidden N+1', + 'Stateless, immutable records: no proxies, no flush, no hidden state', + 'Type-safe and injection-safe: compile-time column and type checks, automatic bind parameters', 'One-line queries with an optional full SQL template engine', 'Works with PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, SQLite and H2', 'Integrates with Spring Boot 3.x/4.x and Ktor', diff --git a/website/src/pages/quickstart.js b/website/src/pages/quickstart.js index c2e1ee11c..3e4ab3695 100644 --- a/website/src/pages/quickstart.js +++ b/website/src/pages/quickstart.js @@ -49,7 +49,7 @@ function buildBody(version) { P('}'); const entity = - C('// Entities.kt — plain data classes. This is the whole data layer.\n') + + C('// Entities.kt: plain data classes. This is the whole data layer.\n') + K('data class ') + T('Director') + P('(\n') + P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') + P(' ') + K('val ') + P('name: ') + T('String') + P(',\n') + diff --git a/website/src/pages/tutorials/build-a-rest-api.js b/website/src/pages/tutorials/build-a-rest-api.js index ddb99ce45..9ea77e70b 100644 --- a/website/src/pages/tutorials/build-a-rest-api.js +++ b/website/src/pages/tutorials/build-a-rest-api.js @@ -79,7 +79,7 @@ function buildBody(version) { P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') + P(' ') + K('val ') + P('name: ') + T('String') + P(',\n') + P(') : ') + T('Entity') + P('<') + T('Int') + P('>\n\n') + - C('// Bookmark.kt — @FK means the folder is loaded in the same query\n') + + C('// Bookmark.kt: @FK means the folder is loaded in the same query\n') + K('data class ') + T('Bookmark') + P('(\n') + P(' ') + A('@PK') + P(' ') + K('val ') + P('id: ') + T('Int') + P(' = ') + N('0') + P(',\n') + P(' ') + K('val ') + P('url: ') + T('String') + P(',\n') + @@ -99,7 +99,7 @@ function buildBody(version) { P('}'); const repo = - C('// Repositories.kt — CRUD is inherited; add only your own queries\n') + + C('// Repositories.kt: CRUD is inherited; add only your own queries\n') + K('interface ') + T('BookmarkRepository') + P(' : ') + T('EntityRepository') + P('<') + T('Bookmark') + P(', ') + T('Int') + P('> {\n') + P(' ') + K('fun ') + F('findByFolderName') + P('(name: ') + T('String') + P('): ') + T('List') + P('<') + T('Bookmark') + P('> =\n') + P(' ') + F('findAll') + P('(Bookmark_.folder.name ') + K('eq') + P(' name)\n') + @@ -166,7 +166,7 @@ function buildBody(version) { P('curl -s -X POST localhost:8080/bookmarks \\\n') + P(" -H 'content-type: application/json' \\\n") + P(' -d ') + S('\'{"url":"https://orm.st","title":"Storm","folderId":1}\'') + P('\n\n') + - C('# list them back — each bookmark includes its folder object\n') + + C('# list them back: each bookmark includes its folder object\n') + P('curl -s localhost:8080/bookmarks'); const test = diff --git a/website/src/pages/tutorials/exposed-ktor.js b/website/src/pages/tutorials/exposed-ktor.js index 2f75c3cad..2cc68401b 100644 --- a/website/src/pages/tutorials/exposed-ktor.js +++ b/website/src/pages/tutorials/exposed-ktor.js @@ -81,7 +81,7 @@ ${navHtml('tutorials')}

Repositories are stateless, so each read borrows a pooled connection just for the query; there is no transaction wrapper because none is needed. For quick endpoints, bare entity<User>() and projection<T>() extensions skip even the interface. Transactions appear where atomicity matters, and because they are suspend functions propagated through the coroutine context, they compose with Ktor naturally:

${editor({file: 'Routes.kt', tag: 'Kotlin · Storm + Ktor', code: CODE_STORM_WRITE})}

The onCommit callback keeps the event publication out of the transaction, so a rollback never announces an order that does not exist. See the transactions comparison for the full mapping.

-

For applications with a service layer, the optional storm-ktor-koin module bridges the same repositories into Koin: stormModule() exposes each one under its own interface type, so services declare repositories as constructor parameters and register with singleOf(::OrderService).

+

For applications with a service layer, the plugin exposes the template and every repository through Ktor's built-in dependency injection, each under its own interface type: services resolve them with by dependencies. Koin users keep the same wiring with a few lines of application code; the integration docs include the recipe.

04The translation table

diff --git a/website/static/skills/storm-repository-java.md b/website/static/skills/storm-repository-java.md index 8e47b2133..61b37aa78 100644 --- a/website/static/skills/storm-repository-java.md +++ b/website/static/skills/storm-repository-java.md @@ -351,7 +351,7 @@ var window = users.scroll(scrollable); ## Framework-Specific Repository Registration ### Spring Boot -With `storm-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-spring` (no starter) do you define a `RepositoryBeanFactoryPostProcessor` with `repositoryBasePackages`: +With `storm-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-spring` (no starter) do you switch scanning on with `@EnableStormRepositories(basePackages = ...)`, or define `RepositoryBeanFactoryPostProcessor(basePackages, ormTemplateBeanName, repositoryPrefix)` beans when multiple repository sets bind to different templates: ```java @Service public class UserService { @@ -368,10 +368,21 @@ UserRepository userRepository = orm.repository(UserRepository.class); ## Transactions -The Java API has **no Storm-managed transaction API** (the `transaction { }` blocks are Kotlin-only). Do not invent methods like `orm.transaction(...)` — they do not exist. +Since 1.13, the Java API has a full programmatic transaction API: `Transactions.transaction(...)` with the same semantics as Kotlin's `transaction { }` blocks — all seven propagation modes, isolation, timeout, read-only, rollback-only, and commit/rollback callbacks. The block is value-returning (void blocks `return null`), checked exceptions propagate to the caller unchanged, and the transaction binds to the first `ORMTemplate` that executes inside it: +```java +import static st.orm.template.Transactions.transaction; +import static st.orm.TransactionPropagation.REQUIRES_NEW; + +User created = transaction(tx -> userRepository.insertAndFetch(new User(null, email, "Alice", city))); + +transaction(REQUIRES_NEW, tx -> { + tx.onCommit(() -> log.info("audit committed")); + return auditRepository.insertAndFetch(entry); +}); +``` ### Spring Boot -Use `@Transactional` on service methods (standard Spring): +Both styles work and cooperate. `@Transactional` on service methods remains first-class, and Storm's `transaction(...)` blocks run through Spring's transaction managers when the template is Spring-composed (the starter does this automatically): a Storm block inside a `@Transactional` method joins it. ```java @Service public class UserService { @@ -383,11 +394,7 @@ public class UserService { ``` ### Standalone -Without Spring, manage transactions at the JDBC level: wrap the `DataSource` in a -transaction-aware proxy or use a library that provides one (e.g. Spring's -`DataSourceTransactionManager`/`TransactionTemplate` with -`TransactionAwareDataSourceProxy`). Storm participates in whatever transaction is -active on the connection it obtains from the `DataSource`. +Without Spring, `Transactions.transaction(...)` manages real JDBC transactions directly on the template's `DataSource` — no framework transaction manager is involved, and the blocking API is virtual-thread friendly. ## Verification diff --git a/website/static/skills/storm-repository-kotlin.md b/website/static/skills/storm-repository-kotlin.md index 85a1908b3..abe7ca1b6 100644 --- a/website/static/skills/storm-repository-kotlin.md +++ b/website/static/skills/storm-repository-kotlin.md @@ -452,7 +452,7 @@ val window = users.scroll(scrollable) Detect the project's framework from its build file and dependencies, then suggest the appropriate pattern: ### Spring Boot -With `storm-kotlin-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-kotlin-spring` (no starter) do you define a `RepositoryBeanFactoryPostProcessor` with `repositoryBasePackages`. +With `storm-kotlin-spring-boot-starter`, repository interfaces are auto-discovered and registered as beans — no configuration needed; just inject them. Only when using plain `storm-kotlin-spring` (no starter) do you switch scanning on with `@EnableStormRepositories(basePackages = [...])`, or define `RepositoryBeanFactoryPostProcessor(basePackages = ..., ormTemplateBeanName = ..., repositoryPrefix = ...)` beans when multiple repository sets bind to different templates. ```kotlin @Service class UserService(private val userRepository: UserRepository) { @@ -474,21 +474,22 @@ fun Application.module() { } ``` -When the app has a service layer, wire it with Koin via `st.orm:storm-ktor-koin`: `stormModule()` exposes the `ORMTemplate` and every auto-registered repository by type, so services take repositories as constructor parameters and register with `singleOf`: +When the app has a service layer, use Ktor's built-in dependency injection: the plugin registers every repository under its own interface type, so services declare repositories as constructor parameters and are provided in the dependency container: ```kotlin class UserService(private val userRepository: UserRepository) { ... } fun Application.module() { install(Storm) - install(Koin) { - modules(stormModule(), module { singleOf(::UserService) }) + dependencies { + provide { UserService(resolve()) } } routing { - val userService by inject() + val userService: UserService by dependencies get("/users/{email}") { call.respond(userService.find(call.parameters.getOrFail("email"))) } } } ``` +Koin users bridge the same repository registry with a few lines of application code; the Ktor integration docs include the recipe. ### Standalone Create repositories directly from the `ORMTemplate`: diff --git a/website/static/skills/storm-setup.md b/website/static/skills/storm-setup.md index 567aa27a9..37c43dbc6 100644 --- a/website/static/skills/storm-setup.md +++ b/website/static/skills/storm-setup.md @@ -59,12 +59,13 @@ Match the compiler plugin suffix to the project's Kotlin version: 2.0.x uses `st - Kotlin: `st.orm:storm-kotlin-spring-boot-starter` (replaces `storm-kotlin` + `storm-core`) - Java: `st.orm:storm-spring-boot-starter` (replaces `storm-java21` + `storm-core`) - These include auto-configuration: `ORMTemplate` is auto-registered as a Spring bean -- The starters also auto-discover repository interfaces and register them as beans — no configuration needed. Only plain `storm-spring`/`storm-kotlin-spring` (without the starter) requires defining a `RepositoryBeanFactoryPostProcessor` with `repositoryBasePackages` +- The starters also auto-discover repository interfaces and register them as beans — no configuration needed. Only plain `storm-spring`/`storm-kotlin-spring` (without the starter) requires switching scanning on: `@EnableStormRepositories(basePackages = ...)`, or a `RepositoryBeanFactoryPostProcessor(basePackages = ..., ormTemplateBeanName = ..., repositoryPrefix = ...)` bean per repository set in multi-template applications +- Optionally: `st.orm:storm-spring-boot-test-autoconfigure` (test scope) for the `@DataStormTest` test slice — the Storm counterpart of `@DataJpaTest` ### Ktor - Kotlin: `st.orm:storm-ktor` - Optionally: `st.orm:storm-ktor-test` (test scope, for `testStormApplication` DSL) -- Optionally: `st.orm:storm-ktor-koin` when the app uses Koin — `install(Koin) { modules(stormModule(), ...) }` exposes the `ORMTemplate` and every auto-registered repository by type, so services declare repositories as constructor parameters and wire with `singleOf(::MyService)` +- The plugin exposes the `ORMTemplate` and every auto-registered repository through Ktor's built-in dependency injection (`ktor-server-di`), each under its own interface type: `val users: UserRepository by dependencies`. Koin users bridge the same registry with a few lines of application code; the Ktor integration docs include the recipe - Add `com.zaxxer:HikariCP` when using the built-in HOCON-configured DataSource (`storm.datasource.jdbcUrl` etc. in application.conf) — not needed when passing your own DataSource to `install(Storm)` - Install with `install(Storm)`; repositories from the compile-time index auto-register, accessed via a bare `repository()` in routes; `orm`, `entity()`, and `projection()` extensions are available too - Run migrations in the plugin's `migration { }` hook so the default fail-mode schema validation sees the migrated schema