# Getting Started - Introduction **Remult** is a fullstack CRUD framework that uses your TypeScript model types to provide: - Secure REST API (highly configurable) - Type-safe frontend API client - Type-safe backend query builder #### Use the same model classes for both frontend and backend code With Remult it is simple to keep your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and increase development speed and maintainability by defining a single TypeScript model class (for each domain object) and sharing it between your frontend and backend code. As Remult is "aware" of the runtime context (frontend or backend), data validations and entity lifecycle hooks can be written in layer-agnostic TypeScript which will run, as needed, on either the frontend, the backend, or both. ## Choose Your Remult Learning Path Explore the flexibility of Remult through different learning paths tailored to match your style and project needs. ### `Option A`: Start with the Interactive Online Tutorial If you're new to Remult or prefer a guided, hands-on approach, we recommend starting with our [interactive online tutorial](https://learn.remult.dev). This tutorial will walk you through building a full-stack application step by step, providing immediate feedback and insights as you learn. ### `Option B`: Create a new Project [`npm init remult@latest`](./creating-a-project.md) ### `Option C`: Follow a Step-by-step Tutorial

### `Option D`: Quickstart Use this [Quickstart](./quickstart.md) guide to quickly setup and try out Remult or add Remult to an existing app. ### `Option E`: Browse Example Apps [Example Apps](./example-apps.md) ### `Option F`: Video Tutorials Check out these official [Remult video tutorials](https://youtube.com/playlist?list=PLlcnBwFkuOn166nXXxxfL9Hee-1GWlDSm&si=TDlwIFDLi4VMi-as). # Getting Started - Creating a project # Creating a Remult Project _The easiest way to start building a Remult app_ ```bash npm init remult@latest ``` Yes, that's it! ::: tip Let us know how you liked the process! [@remultjs](https://twitter.com/RemultJs) ::: ## Demo ![npm init remult](../public/npm_init_remult.gif) ## What you get ? ### 1. **Tailored Setup** Answer a few questions about your preferred tech stack and project requirements. `Project name`: The name of your project _(it will create a folder with this name)_ `Choose your Framework` `Choose your Web Server` _(if needed)_ `Choose your Database` `Authentication`: Do you want to add `auth.js` to your project directly ? including a complete implementation for `credentials` and `github` providers `Add CRUD demo`: A comprehensive example of how to use an entity. It will show you how to create, read, update and delete data. `Admin UI`: Will then be available at `/api/admin` ### 2. **Instant Configuration** Based on your answers, Remult will configure the project with the best-suited options. With all combinations of frameworks, servers, databases and authentication, we manage more than `180 different project flavors`! We are missing yours? Let us know ! ### 3. **Feature-Rich Demo** Once you run your project, you'll be greeted with a comprehensive dashboard that showcases all of Remult's powerful features. It will look like this: ![Remult Dashboard](/create-remult.png) Each tile is a fully functional example of a feature that you selected. ### 4. **Easy Eject** Simply remove the demo folder to eject the demo components. # Getting Started - Quickstart # Quickstart Jumpstart your development with this Quickstart guide. Learn to seamlessly integrate Remult in various stacks, from installation to defining entities for efficient data querying and manipulation. ### Experience Remult with an Interactive Tutorial For a guided, hands-on experience, [try our interactive online tutorial](https://learn.remult.dev/). It's the fastest way to get up and running with Remult and understand its powerful features. ## Installation The _remult_ package is all you need for both frontend and backend code. If you're using one `package.json` for both frontend and backend (or a meta-framework) - **install Remult once** in the project's root folder. If you're using multiple `package.json` files (monorepo) - **install Remult in both server and client folders**. ::: code-group ```sh [npm] npm install remult ``` ```sh [yarn] yarn add remult ``` ```sh [pnpm] pnpm add remult ``` ```sh [bun] bun add remult ``` ::: ## Server-side Initialization Remult is initialized on the server-side as a request handling middleware, with **a single line of code**. Here is the code for setting up the Remult middleware: ::: code-group ```ts [Express] import express from 'express' import { remultExpress } from 'remult/remult-express' const app = express() app.use(remultExpress({})) // [!code highlight] app.listen(3000) ``` ```ts [Fastify] import fastify from 'fastify' import { remultFastify } from 'remult/remult-fastify' (async () => { const server = fastify() await server.register(remultFastify({})) // [!code highlight] server.listen({ port: 3000 }) })() ``` ```ts [Next.js] // src/app/api/[...remult]/route.ts import { remultNextApp } from 'remult/remult-next' export const api = remultNextApp({}) // [!code highlight] export const { GET, POST, PUT, DELETE } = api ``` ```ts [Sveltekit] // src/routes/api/[...remult]/+server.ts import { remultSveltekit } from 'remult/remult-sveltekit' export const _api = remultSveltekit({}) // [!code highlight] export const { GET, POST, PUT, DELETE } = _api ``` ```ts [nuxt.js] // server/api/[...remult].ts import { remultNuxt } from 'remult/remult-nuxt' export const api = remultNuxt({}) export default defineEventHandler(api) // enable experimental decorators // Add to nuxt.config.ts nitro: { esbuild: { options: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }, }, vite: { esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }, ``` ```ts [Hapi] import { type Plugin, server } from '@hapi/hapi' import { remultHapi } from 'remult/remult-hapi' (async () => { const hapi = server({ port: 3000 }) await hapi.register(remultHapi({})) // [!code highlight] hapi.start() })() ``` ```ts [Hono] import { Hono } from 'hono' import { serve } from '@hono/node-server' import { remultHono } from 'remult/remult-hono' const app = new Hono() const api = remultHono({}) // [!code highlight] app.route('', api) // [!code highlight] serve(app) ``` ```ts [Nest] // src/main.ts import { remultExpress } from 'remult/remult-express' async function bootstrap() { const app = await NestFactory.create(AppModule) app.use(remultExpress({})) // [!code highlight] await app.listen(3000) } bootstrap() ``` ```ts{9-17} [Koa] import * as koa from 'koa' import * as bodyParser from 'koa-bodyparser' import { createRemultServer } from 'remult/server' const app = new koa() app.use(bodyParser()) const api = createRemultServer({}) app.use(async (ctx, next) => { const r = await api.handle(ctx.request) if (r) { ctx.response.body = r.data ctx.response.status = r.statusCode } else return await next() }) app.listen(3000, () => {}) ``` ::: ## Connecting a Database Use the `dataProvider` property of Remult's server middleware to set up a database connection for Remult. ::: tip Recommended - Use default local JSON files and connect a database later If the `dataProvider` property is not set, Remult stores data as JSON files under the `./db` folder. ::: Here are examples of connecting to some commonly used back-end databases: ::: tabs == Postgres Install node-postgres: ```sh npm i pg ``` Set the `dataProvider` property: ```ts{3,7,11-15} import express from "express" import { remultExpress } from "remult/remult-express" import { createPostgresDataProvider } from "remult/postgres" const app = express() const connectionString = "postgres://user:password@host:5432/database" app.use( remultExpress({ dataProvider: createPostgresDataProvider({ connectionString, // default: process.env["DATABASE_URL"] // configuration: {} // optional = a `pg.PoolConfig` object or "heroku" }) }) ) ``` Or use your existing postgres connection ```ts import { Pool } from 'pg' import { SqlDatabase } from 'remult' import { PostgresDataProvider } from 'remult/postgres' import { remultExpress } from 'remult/remult-express' const pg = new Pool({ connectionString: '....', }) const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase(new PostgresDataProvider(pg)), }), ) ``` == MySQL Install knex and mysql2: ```sh npm i knex mysql2 ``` Set the `dataProvider` property: ```ts{3,9-18} import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ // Knex client configuration for MySQL client: "mysql2", connection: { user: "your_database_user", password: "your_database_password", host: "127.0.0.1", database: "test" } }) }) ) ``` Or use your existing knex provider ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: '...', connection: '...', }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // [!code highlight] }), ) ``` == MongoDB Install mongodb: ```sh npm i mongodb ``` Set the `dataProvider` property: ```ts{3-4,10-14} import express from "express" import { remultExpress } from "remult/remult-express" import { MongoClient } from "mongodb" import { MongoDataProvider } from "remult/remult-mongo" const app = express() app.use( remultExpress({ dataProvider: async () => { const client = new MongoClient("mongodb://localhost:27017/local") await client.connect() return new MongoDataProvider(client.db("test"), client) } }) ) ``` == SQLite There are several sqlite providers supported ### Better-sqlite3 Install better-sqlite3: ```sh npm i better-sqlite3 ``` Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import Database from 'better-sqlite3' // [!code highlight] import { BetterSqlite3DataProvider } from 'remult/remult-better-sqlite3' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new BetterSqlite3DataProvider(new Database('./mydb.sqlite')), // [!code highlight] ), // [!code highlight] }), ) ``` ### sqlite3 This version of sqlite3 works even on stackblitz Install sqlite3: ```sh npm i sqlite3 ``` Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import sqlite3 from 'sqlite3' // [!code highlight] import { Sqlite3DataProvider } from 'remult/remult-sqlite3' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new Sqlite3DataProvider(new sqlite3.Database('./mydb.sqlite')), // [!code highlight] ), // [!code highlight] }), ) ``` ### bun:sqlite Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import { Database } from 'bun:sqlite' // [!code highlight] import { BunSqliteDataProvider } from 'remult/remult-bun-sqlite' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new BunSqliteDataProvider(new Database('./mydb.sqlite')), // [!code highlight] ), // [!code highlight] }), ) ``` ### sql.js Install sqlite3: ```sh npm i sql.js ``` Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import initSqlJs from 'sql.js' // [!code highlight] import { SqlJsDataProvider } from 'remult/remult-sql-js' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new SqlJsDataProvider(initSqlJs().then((x) => new x.Database())), // [!code highlight] ), // [!code highlight] }), ) ``` ### Turso Install turso: ```sh npm install @libsql/client ``` Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import { createClient } from '@libsql/client' // [!code highlight] import { TursoDataProvider } from 'remult/remult-turso' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new TursoDataProvider( // [!code highlight] createClient({ // [!code highlight] url: process.env.TURSO_DATABASE_URL, // [!code highlight] authToken: process.env.TURSO_AUTH_TOKEN, // [!code highlight] }), // [!code highlight] ), // [!code highlight] ), // [!code highlight] }), ) ``` == Microsoft SQL Server Install knex and tedious: ```sh npm i knex tedious ``` Set the `dataProvider` property: ```ts{5,11-25} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ // Knex client configuration for MSSQL client: "mssql", connection: { server: "127.0.0.1", database: "test", user: "your_database_user", password: "your_database_password", options: { enableArithAbort: true, encrypt: false, instanceName: `sqlexpress` } } }) }) ) ``` Or use your existing knex provider ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: '...', connection: '...', }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // [!code highlight] }), ) ``` == DuckDB Install DuckDB: ```sh npm i duckdb ``` Set the `dataProvider` property: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import { Database } from 'duckdb' // [!code highlight] import { DuckDBDataProvider } from 'remult/remult-duckdb' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new DuckDBDataProvider(new Database(':memory:')), // [!code highlight] ), // [!code highlight] }), ) ``` == Oracle Install knex and oracledb: ```sh npm i knex oracledb ``` Set the `dataProvider` property: ```ts{5,11-19} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ // Knex client configuration for Oracle client: "oracledb", connection: { user: "your_database_user", password: "your_database_password", connectString: "SERVER" } }) }) ) ``` Or use your existing knex provider ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: '...', connection: '...', }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // [!code highlight] }), ) ``` == JSON Files Set the `dataProvider` property: ```ts{5-6,12-14} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { JsonDataProvider } from "remult" import { JsonEntityFileStorage } from "remult/server" const app = express() app.use( remultExpress({ dataProvider: async () => new JsonDataProvider(new JsonEntityFileStorage("./db")) }) ) ``` ::: ## Integrate Auth **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with a [`getUser`](./ref_remultserveroptions.md#getuser) function that extracts a user object (which implements the minimal Remult `UserInfo` interface) from a request. Here are examples of integrating some commonly used auth providers: ::: code-group ```ts [express-session] import express from 'express' import session from 'express-session' import { remultExpress } from 'remult/remult-express' const app = express() app.use( session({ /* ... */ }), ) app.post('/api/signIn', (req, res) => { req.session!['user'] = { id: 1, name: 'admin', roles: ['admin'] } }) app.use( remultExpress({ getUser: (req) => req.session!['user'], // [!code highlight] }), ) ``` ```ts{8-13} [next-auth] // src/app/api/[...remult]/route.ts import { remultNextApp } from 'remult/remult-next' import { getServerSession } from 'next-auth' import { authOptions } from '../auth/[...nextauth]/route' export const api = remultNextApp({ getUser: async () => { const user = (await getServerSession(authOptions))?.user return user?.email && user?.name ? { id: user?.email, name: user?.name } : undefined }, }) export const { POST, PUT, DELETE, GET, withRemult } = api ``` ::: ## Defining and Serving an Entity Remult entity classes are shared between frontend and backend code. ```ts // shared/product.ts import { Entity, Fields } from 'remult' @Entity('products', { allowApiCrud: true, allowApiDelete: 'admin', }) export class Product { @Fields.uuid() id!: string @Fields.string() name = '' @Fields.number() unitPrice = 0 } ``` Alternatively, [generate entities](./entities-codegen-from-db-schema.md) from an existing Postgres database. ### Serve Entity CRUD API All Remult server middleware options contain an [`entities`](./ref_remultserveroptions.md#entities) array. Use it to register your Entity. ```ts // backend/index.ts app.use( remultExpress({ entities: [Product], // [!code highlight] }), ) ``` ## Using your Entity on the Client To start querying and mutating data from the client-side using Remult, use the [`remult.repo`](./ref_remult.md#repo) function to create a [`Repository`](./ref_repository.md) object for your entity class. This approach simplifies data operations, allowing you to interact with your backend with the assurance of type safety. ```ts // frontend/code.ts import { remult } from 'remult' import { Product } from '../shared/product' const productsRepo = remult.repo(Product) async function playWithRemult() { // add a new product to the backend database await productsRepo.insert({ name: 'Tofu', unitPrice: 5 }) // fetch products from backend database const products = await productsRepo.find({ where: { unitPrice: { '>=': 5 } }, orderBy: { name: 'asc' }, limit: 10, }) console.log(products) // update product data const tofu = products.filter((p) => p.name === 'Tofu') await productsRepo.save({ ...tofu, unitPrice: tofu.unitPrice + 5 }) // delete product await productsRepo.delete(tofu) } playWithRemult() ``` ## Client-side Customization ::: tip Recommended Defaults By default, remult uses the browser's [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), and makes data API calls using the base URL `/api` (same-origin). ::: ### Changing the default API base URL To use a different origin or base URL for API calls, set the remult object's `apiClient.url` property. ```ts remult.apiClient.url = 'http://localhost:3002/api' ``` ### Using an alternative HTTP client Set the `remult` object's `apiClient.httpClient` property to customize the HTTP client used by Remult: ::: code-group ```ts [Axios instead of Fetch] import axios from 'axios' import { remult } from 'remult' remult.apiClient.httpClient = axios ``` ```ts [Angular HttpClient instead of Fetch] //... import { HttpClientModule, HttpClient } from '@angular/common/http' import { remult } from 'remult' @NgModule({ //... imports: [ //... HttpClientModule, ], }) export class AppModule { constructor(http: HttpClient) { remult.apiClient.httpClient = http } } ``` ::: # Getting Started - Example Apps # Example Apps We have already a _ton_ of examples! Pick and choose the one that fits your needs 😊 ## Todo MVC ## CRM Demo A fully featured CRM! Make sure to check out the link: Dev / Admin on top right! ## Shadcn React Table Using remult with server side sorting, filtering, paging & CRUD ## TanStack React Table Example of using remult with react table - most basic design, with server side sorting, paging & filtering ## 🚀 Ready to play An environment to reproduce issues using stackblitz, with optional sqlite database ## Group by Example And example of the usage of groupBy ## Todo for most frameworks - [React & Express](https://github.com/remult/remult/tree/main/examples/react-todo) - [React & bun & Hono](https://github.com/remult/remult/tree/main/examples/bun-react-hono-monorepo-todo) - [Next.js (App Router)](https://github.com/remult/remult/tree/main/examples/nextjs-app-router-todo) - [Next.js (Pages)](https://github.com/remult/remult/tree/main/examples/nextjs-todo) - [Angular & Express](https://github.com/remult/remult/tree/main/examples/angular-todo) - [Angular & Fastify](https://github.com/remult/remult/tree/main/examples/angular-todo-fastify) - [Vue](https://github.com/remult/remult/tree/main/examples/vue-todo) - [Nuxt3](https://github.com/remult/remult/tree/main/examples/nuxt-todo) - [SvelteKit](https://github.com/remult/remult/tree/main/examples/sveltekit-todo) - [SolidStart](https://github.com/remult/remult/tree/main/examples/solid-start-todo) ## Other example - [Using BackendMethod queued option](https://stackblitz.com/edit/github-vwfkxu?file=src%2FApp.tsx) - [Using SubscriptionChannel to update the frontend](https://stackblitz.com/edit/github-3nmwrp?file=src%2FApp.tsx) - [Next.js Auth with remult user table](https://github.com/noam-honig/nextjs-auth-remult-user-table) - [Unit tests for api](https://stackblitz.com/edit/api-test-example?file=test.spec.ts,model.ts) - [Extending field options with open api specific properties](https://github.com/noam-honig/adding-open-api-options/) # Entities - Fields # Field Types ## Common field types There are also several built in Field decorators for common use case: ### @Fields.string A field of type string ```ts @Fields.string() title = ''; ``` ### @Fields.number Just like TypeScript, by default any number is a decimal (or float). ```ts @Fields.number() price = 1.5 ``` ### @Fields.integer For cases where you don't want to have decimal values, you can use the `@Fields.integer` decorator ```ts @Fields.integer() quantity = 0; ``` ### @Fields.boolean ```ts @Fields.boolean() completed = false ``` ### @Fields.date ```ts @Fields.date() statusDate = new Date() ``` ### @Fields.dateOnly Just like TypeScript, by default any `Date` field includes the time as well. For cases where you only want a date, and don't want to meddle with time and time zone issues, use the `@Fields.dateOnly` ```ts @Fields.dateOnly() birthDate?:Date; ``` ### @Fields.createdAt Automatically set on the backend on insert, and can't be set through the API ```ts @Fields.createdAt() createdAt = new Date() ``` ### @Fields.updatedAt Automatically set on the backend on update, and can't be set through the API ```ts @Fields.updatedAt() updatedAt = new Date() ``` ## JSON Field You can store JSON data and arrays in fields. ```ts @Fields.json() tags: string[] = [] ``` ## Auto Generated Id Field Types ### @Fields.uuid This id value is determined on the backend on insert, and can't be updated through the API. ```ts @Fields.uuid() id:string ``` ### @Fields.cuid This id value is determined on the backend on insert, and can't be updated through the API. Uses the [@paralleldrive/cuid2](https://www.npmjs.com/package/@paralleldrive/cuid2) package ```ts @Fields.cuid() id:string ``` ### @Fields.autoIncrement This id value is determined by the underlying database on insert, and can't be updated through the API. ```ts @Fields.autoIncrement() id:number ``` ### MongoDB ObjectId Field To indicate that a field is of type object id, change it's `fieldTypeInDb` to `dbid`. ```ts @Fields.string({ dbName: '_id', valueConverter: { fieldTypeInDb: 'dbid', }, }) id: string = '' ``` ## Enum Field Enum fields allow you to define a field that can only hold values from a specific enumeration. The `@Fields.enum` decorator is used to specify that a field is an enum type. When using the `@Fields.enum` decorator, an automatic validation is added that checks if the value is valid in the specified enum. ```ts @Fields.enum(() => Priority) priority = Priority.Low; ``` In this example, the `priority` field is defined as an enum type using the `@Fields.enum` decorator. The `Priority` enum is passed as an argument to the decorator, ensuring that only valid `Priority` enum values can be assigned to the `priority` field. The `Validators.enum` validation is used and ensures that any value assigned to this field must be a member of the `Priority` enum, providing type safety and preventing invalid values. ## Literal Fields (Union of string values) Literal fields let you restrict a field to a specific set of string values using the `@Fields.literal` decorator. This is useful for fields with a finite set of possible values. ```ts @Fields.literal(() => ['open', 'closed', 'frozen', 'in progress'] as const) status: 'open' | 'closed' | 'frozen' | 'in progress' = 'open'; ``` In this example, we use the `as const` assertion to ensure that the array `['open', 'closed', 'frozen', 'in progress']` is treated as a readonly array, which allows TypeScript to infer the literal types 'open', 'closed', 'frozen', and 'in progress' for the elements of the array. This is important for the type safety of the `status` field. The `status` field is typed as `'open' | 'closed' | 'frozen' | 'in progress'`, which means it can only hold one of these string literals. The `@Fields.literal` decorator is used to specify that the `status` field can hold values from this set of strings, and it uses the `Validators.in` validator to ensure that the value of `status` matches one of the allowed values. For better reusability and maintainability, and to follow the DRY (Don't Repeat Yourself) principle, it is recommended to refactor the literal type and the array of allowed values into separate declarations: ```ts const statuses = ['open', 'closed', 'frozen', 'in progress'] as const; type StatusType = typeof statuses[number]; @Fields.literal(() => statuses) status: StatusType = 'open'; ``` In this refactored example, `statuses` is a readonly array of the allowed values, and `StatusType` is a type derived from the elements of `statuses`. The `@Fields.literal` decorator is then used with the `statuses` array, and the `status` field is typed as `StatusType`. This approach makes it easier to manage and update the allowed values for the `status` field, reducing duplication and making the code more robust and easier to maintain. ## ValueListFieldType ### Overview The `ValueListFieldType` is useful in cases where simple enums and unions are not enough, such as when you want to have more properties for each value. For example, consider representing countries where you want to have a country code, description, currency, and international phone prefix. ### Defining a ValueListFieldType Using enums or union types for this purpose can be challenging. Instead, you can use the `ValueListFieldType`: ```ts @ValueListFieldType() export class Country { static us = new Country('us', 'United States', 'USD', '1') static canada = new Country('ca', 'Canada', 'CAD', '1') static france = new Country('fr', 'France', 'EUR', '33') constructor( public id: string, public caption: string, public currency: string, public phonePrefix: string, ) {} } ``` ### Using in an Entity In your entity, you can define the field as follows: ```ts @Field(() => Country) country: Country = Country.us; ``` ### Accessing Properties The property called `id` will be stored in the database and used through the API, while in the code itself, you can use each property: ```ts call('+' + person.country.phonePrefix + person.phone) ``` Note: Only the `id` property is saved in the database and used in the API. Other properties, such as `caption`, `currency`, and `phonePrefix`, are only accessible in the code and are not persisted in the database. ### Getting Optional Values To get the optional values for `Country`, you can use the `getValueList` function, which is useful for populating combo boxes: ```ts console.table(getValueList(Country)) ``` ### Special Properties: id and caption The `id` and `caption` properties are special in that the `id` will be used to save and load from the database, and the `caption` will be used as the display value. ### Automatic Generation of id and caption If `id` and/or `caption` are not provided, they are automatically generated based on the static member name. For example: ```ts @ValueListFieldType() export class TaskStatus { static open = new TaskStatus() // { id: 'open', caption: 'Open' } static closed = new TaskStatus() // { id: 'closed', caption: 'Closed' } id!: string caption!: string constructor() {} } ``` In this case, the `open` member will have an `id` of `'open'` and a `caption` of `'Open'`, and similarly for the `closed` member. ### Handling Partial Lists of Values In cases where you only want to generate members for a subset of values, you can use the `getValues` option of `@ValueListFieldType` to specify which values should be included: ```ts @ValueListFieldType({ getValues: () => [ Country.us, Country.canada, Country.france, { id: 'uk', caption: 'United Kingdom', currency: 'GBP', phonePrefix: '44' } ] }) ``` This approach is useful when you want to limit the options available for a field to a specific subset of values, without needing to define all possible values as static members. ::: warning Warning: TypeScript may throw an error similar to `Uncaught TypeError: Currency_1 is not a constructor`. This happens in TypeScript versions <5.1.6 and target es2022. It's a TypeScript bug. To fix it, upgrade to version >=5.1.6 or change the target from es2022. Alternatively, you can call the `ValueListFieldType` decorator as a function after the type: ```ts export class TaskStatus { static open = new TaskStatus() static closed = new TaskStatus() id!: string caption!: string constructor() {} } ValueListFieldType()(TaskStatus) ``` ::: ### Summary The `ValueListFieldType` enables the creation of more complex value lists that provide greater flexibility and functionality for your application's needs beyond what enums and unions can offer. By allowing for additional properties and partial lists of values, it offers a versatile solution for representing and managing data with multiple attributes. ## Control Field Type in Database In some cases, you may want to explicitly specify the type of a field in the database. This can be useful when you need to ensure a specific data type or precision for your field. To control the field type in the database, you can use the `fieldTypeInDb` option within the `valueConverter` property of a field decorator. For example, if you want to ensure that a numeric field is stored as a decimal with specific precision in the database, you can specify the `fieldTypeInDb` as follows: ```ts @Fields.number({ valueConverter: { fieldTypeInDb: 'decimal(16,8)' } }) price=0; ``` In this example, the `price` field will be stored as a `decimal` with 16 digits in total and 8 digits after the decimal point in the database. This allows you to control the storage format and precision of numeric fields in your database schema. ## Creating Custom Field Types Sometimes, you may need to create custom field types to handle specific requirements or use cases in your application. By creating custom field types, you can encapsulate the logic for generating, validating, and converting field values. ### Example: Creating a Custom ID Field Type with NanoID NanoID is a tiny, secure, URL-friendly, unique string ID generator. You can create a custom field type using NanoID to generate unique IDs for your entities. Here's an example of how to create a custom NanoID field type: ```typescript import { nanoid } from 'nanoid' import { Fields, type FieldOptions } from 'remult' export function NanoIdField( ...options: FieldOptions[] ) { return Fields.string( { allowApiUpdate: false, // Disallow updating the ID through the API defaultValue: () => nanoid(), // Generate a new NanoID as the default value saving: (_, record) => { if (!record.value) { record.value = nanoid() // Generate a new NanoID if the value is not set } }, }, ...options, ) } ``` In this example, the `NanoIdField` function creates a custom field type based on the `Fields.string` type. It uses the `nanoid` function to generate a unique ID as the default value and ensures that the ID is generated before saving the record if it hasn't been set yet. This custom field type can be used in your entities to automatically generate and assign unique IDs using NanoID. ## Customize DB Value Conversions Sometimes you want to control how data is saved to the db, or the dto object. You can do that using the `valueConverter` option. For example, the following code will save the `tags` as a comma separated string in the db. ```ts @Fields.object({ valueConverter: { toDb: x => (x ? x.join(",") : undefined), fromDb: x => (x ? x.split(",") : undefined) } }) tags: string[] = [] ``` You can also refactor it to create your own FieldType ```ts import { Field, FieldOptions, Remult } from 'remult' export function CommaSeparatedStringArrayField( ...options: ( | FieldOptions | ((options: FieldOptions, remult: Remult) => void) )[] ) { return Fields.object( { valueConverter: { toDb: (x) => (x ? x.join(',') : undefined), fromDb: (x) => (x ? x.split(',') : undefined), }, }, ...options, ) } ``` And then use it: ```ts{9} @CommaSeparatedStringArrayField() tags: string[] = [] ``` There are several ready made valueConverters included in the `remult` package, which can be found in `remult/valueConverters` ## Class Fields Sometimes you may want a field type to be a class, you can do that, you just need to provide an implementation for its transition from and to JSON. For example: ```ts export class Phone { constructor(public phone: string) {} call() { window.open('tel:' + this.phone) } } @Entity('contacts') export class Contact { //... @Field(() => Phone, { valueConverter: { fromJson: (x) => (x ? new Phone(x) : undefined!), toJson: (x) => (x ? x.phone : undefined!), }, }) phone?: Phone } ``` Alternatively you can decorate the `Phone` class with the `FieldType` decorator, so that whenever you use it, its `valueConverter` will be used. ```ts @FieldType({ valueConverter: { fromJson: (x) => (x ? new Phone(x) : undefined!), toJson: (x) => (x ? x.phone : undefined!), }, }) export class Phone { constructor(public phone: string) {} call() { window.open('tel:' + this.phone) } } @Entity('contacts') export class Contact { //... @Field(() => Phone) phone?: Phone } ``` # Entities - Relations 🚀 --- outline: [2, 3] --- # Relations Between Entities ::: tip **Interactive Learning Available! 🚀** Looking to get hands-on with this topic? Try out our new [**interactive tutorial**](https://learn.remult.dev/in-depth/1-relations/1-many-to-one) on Relations, where you can explore and practice directly in the browser. This guided experience offers step-by-step lessons to help you master relations in Remult with practical examples and exercises. [Click here to dive into the interactive tutorial on Relations!](https://learn.remult.dev/in-depth/1-relations/1-many-to-one) ::: ### Understanding Entity Relations in Remult In Remult, entity relations play a useful role in modeling and navigating the complex relationships that exist within your data. To illustrate this concept, we will use two primary entities: `Customer` and `Order`. These entities will serve as the foundation for discussing various types of relations and how to define and work with them . To experiment with these entities online, you can access the following CodeSandbox link, which is preconfigured with these two entities and a postgres database: [CodeSandbox - Remult Entity Relations Example](https://codesandbox.io/p/devbox/remult-postgres-demo-f934f8) Feel free to explore and experiment with the provided entities and their relations in the CodeSandbox environment. #### Customer Entity ```typescript // customer.ts import { Entity, Fields } from 'remult' @Entity('customers') export class Customer { @Fields.cuid() id = '' @Fields.string() name = '' @Fields.string() city = '' } ``` The `Customer` entity represents individuals or organizations with attributes such as an ID, name, and city. Each customer can be uniquely identified by their `id`. #### Order Entity ```typescript // order.ts import { Entity, Fields } from 'remult' @Entity('orders') export class Order { @Fields.cuid() id = '' @Fields.string() customer = '' @Fields.number() amount = 0 } ``` The `Order` entity represents transactions or purchases made by customers. Each order is associated with a `customer`, representing the customer who placed the order, and has an `amount` attribute indicating the total purchase amount. Throughout the following discussion, we will explore how to define and use relations between these entities, enabling you to create sophisticated data models and efficiently query and manipulate data using Remult. Whether you are dealing with one-to-one, one-to-many, or many-to-many relationships, understanding entity relations is essential for building robust and feature-rich applications with Remult. ## Simple Many-to-One In Remult, many-to-one relations allow you to establish connections between entities, where multiple records of one entity are associated with a single record in another entity. Let's delve into a common use case of a many-to-one relation, specifically the relationship between the `Order` and `Customer` entities. ### Defining the Relation To establish a many-to-one relation from the `Order` entity to the `Customer` entity, you can use the `@Relations.toOne()` decorator in your entity definition: ```typescript // order.ts import { Entity, Fields, Relations } from 'remult' import { Customer } from '../customer.js' @Entity('orders') export class Order { @Fields.cuid() id = '' @Fields.string() // [!code --] customer = '' // [!code --] @Relations.toOne(() => Customer) // [!code ++] customer?: Customer // [!code ++] @Fields.number() amount = 0 } ``` In this example, each `Order` is associated with a single `Customer`. The `customer` property in the `Order` entity represents this relationship. ### Fetching Relational Data When querying data that involves a many-to-one relation, you can use the `include` option to specify which related entity you want to include in the result set. In this case, we want to include the associated `Customer` when querying `Order` records. Here's how you can include the relation in a query using Remult: ```typescript{3-5} const orderRepo = remult.repo(Order) const orders = await orderRepo.find({ include: { customer: true, }, }) ``` #### Resulting Data Structure The result of the query will contain the related `Customer` information within each `Order` record, creating a nested structure. Here's an example result of running `JSON.stringify` on the `orders` array: ```json [ { "id": "adjkzsio3efees8ew0wnsqma", "customer": { "id": "m4ozs74onwwroav3o1xs1qi8", "name": "Larkin - Fadel", "city": "London" }, "amount": 90 }, { "id": "gefhsed1clknmogcgiigo9jo", "customer": { "id": "m4ozs74onwwroav3o1xs1qi8", "name": "Larkin - Fadel", "city": "London" }, "amount": 3 } ] ``` As shown in the result, each `Order` object contains a nested `customer` object, which holds the details of the associated customer, including their `id`, `name`, and `city`. This structured data allows you to work seamlessly with the many-to-one relationship between `Order` and `Customer` entities . ### Querying a Single Item To retrieve a single `Order` item along with its associated `Customer`, you can use the `findFirst` method provided by your repository (`orderRepo` in this case). Here's an example of how to perform this query: ```typescript const singleOrder = await orderRepo.findFirst( { id: 'adjkzsio3efees8ew0wnsqma', }, { include: { customer: true, }, }, ) ``` ### Relation Loading In Remult, by default, a relation is not loaded unless explicitly specified in the `include` statement of a query. This behavior ensures that you only load the related data you require for a specific task, optimizing performance and minimizing unnecessary data retrieval. Here's an example: ```typescript const orderRepo = remult.repo(Order) // Query without including the 'customer' relation const ordersWithoutCustomer = await orderRepo.find({}) ``` In the above query, the `customer` relation will not be loaded and have the value of `undefined` because it is not specified in the `include` statement. #### Overriding Default Behavior with `defaultIncluded` Sometimes, you may have scenarios where you want a relation to be included by default in most queries, but you also want the flexibility to exclude it in specific cases. Remult allows you to control this behavior by using the `defaultIncluded` setting in the relation definition. ```typescript @Relations.toOne(() => Customer, { defaultIncluded: true, // [!code ++] }) customer = ""; ``` In this example, we set `defaultIncluded` to `true` for the `customer` relation in the `Order` entity. This means that, by default, the `customer` relation will be loaded in most queries unless explicitly excluded. #### Example: Excluding `customer` Relation in a Specific Query ```typescript const orders = await orderRepo.find({ include: { customer: false, // [!code ++] }, }) ``` In this query, we override the default behavior by explicitly setting `customer: false` in the `include` statement. This instructs Remult not to load the `customer` relation for this specific query, even though it is set to be included by default. By combining the default behavior with the ability to override it in specific queries, Remult provides you with fine-grained control over relation loading, ensuring that you can optimize data retrieval based on your application's requirements and performance considerations. ## Advanced Many-to-One In certain scenarios, you may require more granular control over the behavior of relations and want to access specific related data without loading the entire related entity. Remult provides advanced configuration options to meet these requirements. Let's explore how to achieve this level of control through advanced relation configurations. ### Custom Relation Field In Remult, you can define custom relation fields that allow you to access the `id` without loading the entire related entity. To define a custom relation field, follow these steps: #### Step 1: Define a Custom Field in the Entity In your entity definition, define a custom field that will hold the identifier or key of the related entity. This field serves as a reference to the related entity without loading the entity itself. ```typescript{5-8} @Entity("orders") export class Order { @Fields.cuid() id = ""; @Fields.string() // [!code ++] customerId = ""; // Custom field to hold the related entity's identifier // [!code ++] @Relations.toOne(() => Customer, "customerId") // [!code ++] @Relations.toOne(() => Customer) // [!code --] customer?: Customer; @Fields.number() amount = 0; } ``` In this example, we define a custom field called `customerId`, which stores the identifier of the related `Customer` entity. #### Step 2: Define the Relation Using `toOne` Use the `@Relations.toOne` decorator to define the relation, specifying the types for the `fromEntity` and `toEntity` in the generic parameters. Additionally, provide the name of the custom field (in this case, `"customerId"`) as the third argument. ```typescript @Entity('orders') export class Order { @Fields.cuid() id = '' @Fields.string() customerId = '' // Custom field to hold the related entity's identifier @Relations.toOne(() => Customer, 'customerId') // [!code ++] customer = '' @Fields.number() amount = 0 } ``` This configuration establishes a relation between `Order` and `Customer` using the `customerId` field as the reference. #### Migrating from a Simple `toOne` Relation to a Custom Field Relation with Existing Data When transitioning from a simple `toOne` relation to a custom field relation in Remult and you already have existing data, it's important to ensure a smooth migration. In this scenario, you need to make sure that the newly introduced custom field (`customerId` in this case) can access the existing data in your database. This is accomplished using the `dbName` option. Here's how to perform this migration: ##### 1. Understand the Existing Data Structure Before making any changes, it's crucial to understand the structure of your existing data. In the case of a simple `toOne` relation, there may be rows in your database where a field (e.g., `customer`) holds the identifier of the related entity. ##### 2. Define the Custom Field with `dbName` When defining the custom field in your entity, use the `dbName` option to specify the name of the database column where the related entity's identifier is stored. This ensures that the custom field (`customerId` in this example) correctly accesses the existing data in your database. ```typescript @Entity('orders') export class Order { @Fields.cuid() id = '' @Fields.string({ dbName: 'customer' }) // Use dbName to match existing data // [!code ++] customerId = '' @Relations.toOne(() => Customer, 'customerId') customer?: Customer @Fields.number() amount = 0 } ``` In this example, we use the `dbName` option to specify that the `customerId` field corresponds to the `customer` column in the database. This mapping ensures that the custom field can access the existing data that uses the `customer` column for the related entity's identifier. #### Using the `field` Option for Custom Relation Configuration When you require additional customization for a relation field in Remult, you can utilize the field option to specify additional options for the related field. ```typescript @Relations.toOne(() => Customer, { field: "customerId", // [!code ++] caption: "The Customer", }) ``` In this example, we use the `field` option to define a custom relation between the `Order` and `Customer` entities. Here are some key points to understand about using the `field` option: 1. **Custom Relation Field**: The `field` option allows you to specify a custom field name (e.g., `"customerId"`) that represents the relationship between entities. This field can be used to access related data without loading the entire related entity. 2. **Additional Configuration**: In addition to specifying the `field`, you can include other options as well. In this example, we set the `caption` option to provide a descriptive caption for the relation field. Using the `field` option provides you with granular control over how the relation field is configured and accessed . You can customize various aspects of the relation to meet your specific requirements, enhance documentation, and improve the overall usability of your codebase. ### Relation Based on Multiple Fields In some scenarios, establishing a relation between entities requires considering multiple fields to ensure the correct association. Remult provides the flexibility to define relations based on multiple fields using the `fields` option. Here's how to create a relation based on multiple fields in Remult: #### Defining Entities Let's consider a scenario where both `Order` and `Customer` entities belong to specific branches, and we need also the `branchId` fields to ensure the correct association. First, define your entities with the relevant fields: ```typescript{0} @Entity('customers') export class Customer { @Fields.cuid() id = '' @Fields.number() // [!code ++] branchId = 0 // [!code ++] @Fields.string() name = '' @Fields.string() city = '' } @Entity('orders') export class Order { @Fields.cuid() id = '' @Fields.number() // [!code ++] branchId = 0 // [!code ++] @Fields.string({ dbName: 'customer' }) customerId = '' @Relations.toOne(() => Customer, { fields: {//[!code ++] branchId: 'branchId', // Field from Customer entity : Field from Order// [!code ++] id: 'customerId', // [!code ++] }, // [!code ++] }) customer?: Customer @Fields.number() amount = 0 } ``` In this example, we have two entities: `Customer` and `Order`. Both entities have a `branchId` field that represents the branch they belong to. To create a relation based on these fields, we specify the `fields` option in the relation configuration. #### Using the `fields` Option In the `@Relations.toOne` decorator, use the `fields` option to specify the mapping between fields in the related entity (`Customer`) and your entity (`Order`). Each entry in the `fields` object corresponds to a field in the related entity and maps it to a field in your entity. ```typescript @Relations.toOne(() => Customer, { fields: {// [!code ++] branchId: 'branchId', // Field from Customer entity : Field from Order// [!code ++] id: 'customerId',// [!code ++] },// [!code ++] }) customer?: Customer; ``` In this configuration: - `branchId` from the `Customer` entity is mapped to `branchId` in the `Order` entity. - `id` from the `Order` entity is mapped to `customerId` in the `Customer` entity. This ensures that the relation between `Order` and `Customer` is based on both the `branchId` and `customerId` fields, providing a comprehensive association between the entities. By utilizing the `fields` option, you can create relations that consider multiple fields, ensuring accurate and meaningful associations between your entities in Remult. ## One-to-Many In Remult, you can easily define a `toMany` relation to retrieve multiple related records. Let's consider a scenario where you want to retrieve a list of orders for each customer. We'll start with the basic `toOne` relation example and then add a `toMany` relation to achieve this: #### Basic `toOne` Relation Example First, let's define the `Customer` and `Order` entities with a basic `toOne` relation: ```typescript{9-10} @Entity("customers") export class Customer { @Fields.cuid() id = ""; @Fields.string() name = ""; @Fields.string() city = ""; } @Entity("orders") export class Order { @Fields.cuid() id = ""; @Relations.toOne(() => Customer) customer?: Customer; @Fields.number() amount = 0; } ``` In this initial setup: - The `Order` entity has a property `customer`, which is decorated with `@Relations.toOne(() => Customer)`. This establishes a relation between an order and its associated customer. ### Adding a `toMany` Relation Now, let's enhance this setup to include a `toMany` relation that allows you to retrieve a customer's orders: ```typescript @Entity('customers') export class Customer { @Fields.cuid() id = '' @Fields.string() name = '' @Fields.string() city = '' @Relations.toMany(() => Order) // [!code ++] orders?: Order[] // [!code ++] } ``` In this updated configuration: - The `Customer` entity has a property `orders`, which is decorated with `@Relations.toMany(() => Order)`. This indicates that a customer can have multiple orders. With this setup, you can use the `orders` property of a `Customer` entity to retrieve all the orders associated with that customer. This provides a convenient way to access and work with a customer's orders. By defining a `toMany` relation, you can easily retrieve and manage multiple related records, such as a customer's orders. ### Fetching Relational Data To retrieve customers along with their associated order in Remult, you can use the `include` option in your query. Let's see how to fetch customers with their orders using the `include` option: ```typescript const customerRepo = remult.repo(Customer) const customers = await customerRepo.find({ include: { orders: true, }, }) ``` In this code snippet: - We first obtain a repository for the `Customer` entity using `remult.repo(Customer)`. - Next, we use the `find` method to query the `Customer` entity. Within the query options, we specify the `include` option to indicate that we want to include related records. - Inside the `include` option, we specify `orders: true`, indicating that we want to fetch the associated orders for each customer. As a result, the `customers` variable will contain an array of customer records, with each customer's associated orders included. This allows you to easily access and work with both customer and order data. #### Resulting Data Structure When you fetch customers along with their associated orders using the `include` option in Remult, the result will be an array that includes both customer and order data. Here's an example result of running `JSON.stringify` on the `customers` array: ```json [ { "id": "ik68p3oxqg1ygdffpryqwkpw", "name": "Fay, Ebert and Sporer", "city": "London", "orders": [ { "id": "m7m3xqyx4kwjaqcd0cu33q8g", "amount": 15 }, { "id": "rbkcrz6nc45zn4xfxmjise21", "amount": 10 } ] } ] ``` In this example: - Each customer is represented as an object with properties such as `id`, `name`, and `city`. - The `orders` property within each customer object contains an array of associated order records. - Each order record within the `orders` array includes properties like `id` and `amount`. This structured result allows you to easily navigate and manipulate the data . You can access customer information as well as the details of their associated orders, making it convenient to work with related records in your application's logic and UI. ### Specifying Reference Fields In Remult, you can specify a field or fields for `toMany` relations to have more control over how related records are retrieved. This can be useful when you want to customize the behavior of the relation. Here's how you can specify a field or fields for `toMany` relations: #### Specifying a Single Field To specify a single field for a `toMany` relation, you can use the `field` option. This option allows you to define the field in your entity that establishes the relation. For example: ```typescript @Relations.toMany(() => Order, { field: "customer", }) ``` In this case, the `field` option is set to `"customer"`, indicating that the `customer` field in the `Order` entity establishes the relation between customers and their orders. #### Specifying Multiple Fields In some cases, you may need to specify multiple fields to establish a `toMany` relation. To do this, you can use the `fields` option, which allows you to define a mapping of fields between entities. Here's an example: ```typescript @Relations.toMany(() => Order, { fields: { branchId: "branchId", customerId: "id", }, }) ``` In this example, the `fields` option is used to specify that the `branchId` field in the `Order` entity corresponds to the `branchId` field in the `Customer` entity, and the `customerId` field in the `Order` entity corresponds to the `id` field in the `Customer` entity. By specifying fields in this manner, you have fine-grained control over how the relation is established and how related records are retrieved. This allows you to tailor the behavior of `toMany` relations to your specific use case and data model. ### Customizing a `toMany` Relation In Remult, you can exercise precise control over a `toMany` relation by utilizing the `findOptions` option. This option allows you to define specific criteria and behaviors for retrieving related records. Here's how you can use `findOptions` to fine-tune a `toMany` relation: ```typescript @Relations.toMany(() => Order, { fields: { branchId: "branchId", customerId: "id", }, findOptions: { limit: 5, orderBy: { amount: "desc", }, where: { amount: { $gt: 10 }, }, }, }) ``` In this example, we've specified the following `findOptions`: - `limit: 5`: Limits the number of related records to 5. Only the first 5 related records will be included. - `orderBy: { amount: "desc" }`: Orders the related records by the `amount` field in descending order. This means that records with higher `amount` values will appear first in the result. - `where: { amount: { $gt: 10 } }`: Applies a filter to include only related records where the `amount` is greater than 10. This filters out records with an `amount` of 10 or lower. By using `findOptions` in this manner, you gain precise control over how related records are retrieved and included in your query results. This flexibility allows you to tailor the behavior of the `toMany` relation to suit your specific application requirements and use cases. #### Fine-Tuning a `toMany` Relation with `include` In Remult, you can exercise even more control over a `toMany` relation by using the `include` option within your queries. This option allows you to further customize the behavior of the relation for a specific query. Here's how you can use `include` to fine-tune a `toMany` relation: ```typescript const orders = await customerRepo.find({ include: { orders: { limit: 10, where: { completed: true, }, }, }, }) ``` In this code snippet: - We use the `include` option within our query to specify that we want to include the related `orders` for each customer. - Inside the `include` block, we can provide additional options to control the behavior of this specific inclusion. For example: - `limit: 10` limits the number of related orders to 10 per customer. This will override the `limit` set in the original relation. - `where: { completed: true }` filters the included orders to only include those that have been marked as completed. The `where` option specified within `include` will be combined with the `where` conditions defined in the `findOptions` of the relation using an "and" relationship. This means that both sets of conditions must be satisfied for related records to be included. Using `include` in this way allows you to fine-tune the behavior of your `toMany` relation to meet the specific requirements of each query, making Remult a powerful tool for building flexible and customized data retrieval logic in your application. ## Repository `relations` In Remult, managing relationships between entities is a crucial aspect of working with your data. When dealing with a `toMany` relationship, Remult provides you with powerful tools through the repository's `relations` property to handle related rows efficiently, whether you want to retrieve them or insert new related records. ### Inserting Related Records Consider a scenario where you have a `Customer` entity with a `toMany` relationship to `Order` entities. You can create a new customer and insert related orders in a straightforward manner: ```typescript const customer = await customerRepo.insert({ name: 'Abshire Inc' }) await customerRepo.relations(customer).orders.insert([ { amount: 5, }, { amount: 7, }, ]) ``` In this example, you first create a new `Customer` entity with the name "Abshire Inc." Then, using the `relations` method, you access the related `orders`. By calling the `insert` method on the `orders` relation, you can add new order records. Remult automatically sets the `customer` field for these orders based on the specific customer associated with the `relations` call. ### Loading Unfetched Relations Another powerful use of the `repository` methods is to load related records that were not initially retrieved. Let's say you have found a specific customer and want to access their related orders: ```typescript const customerRepo = remult.repo(Customer) const customer = await customerRepo.findFirst({ name: 'Abshire Inc' }) const orders = await customerRepo.relations(customer).orders.find() ``` Here, you first search for a customer with the name "Abshire Inc." After locating the customer, you can use the `relations` method again to access their related orders. By calling the `find` method on the `orders` relation, you retrieve all related order records associated with the customer. #### Contextual Repository: Tailored Operations for Related Data The `relations` method serves as a specialized repository, tightly associated with the particular customer you supply to it. This dedicated repository offers a tailored context for performing operations related to the specific customer's connection to orders. It enables you to seamlessly find related records, insert new ones, calculate counts, and perform other relevant actions within the precise scope of that customer's relationship with orders. This versatile capability streamlines the management of intricate relationships in your application, ensuring your data interactions remain organized and efficient. Remult's repository methods empower you to seamlessly manage and interact with related data, making it easier to work with complex data structures and relationships in your applications. Whether you need to insert related records or load unfetched relations, these tools provide the flexibility and control you need to handle your data efficiently. Certainly, here's an extension of the "Loading Unfetched Relations" section that covers the topic of fetching unloaded `toOne` relations using the `findOne` function: --- ### Fetching Unloaded `toOne` Relations with `findOne` In addition to loading unfetched `toMany` relations, Remult offers a convenient way to retrieve `toOne` relations that were not initially loaded. This capability is especially useful when dealing with many-to-one relationships. Consider the following example, where we have a many-to-one relation between orders and customers. We want to fetch the customer related to a specific order, even if we didn't load it initially: ```ts const orderRepo = remult.repo(Order) const order = await orderRepo.findFirst({ id: 'm7m3xqyx4kwjaqcd0cu33q8g' }) const customer = await orderRepo.relations(order).customer.findOne() ``` In this code snippet: 1. We first obtain the order using the `findFirst` function, providing the order's unique identifier. 2. Next, we use the `relations` method to access the repository's relations and then chain the `customer` relation using dot notation. 3. Finally, we call `findOne()` on the `customer` relation to efficiently retrieve the related customer information. This approach allows you to access and load related data on-demand, providing flexibility and control over your data retrieval process. Whether you're working with loaded or unloaded relations, Remult's intuitive functions give you the power to seamlessly access the data you need. --- You can seamlessly incorporate this extension into the "Loading Unfetched Relations" section of your documentation to provide a comprehensive overview of working with both `toMany` and `toOne` relations. --- ### Accessing Relations with `activeRecord` If you're following the `activeRecord` pattern and your entity inherits from `EntityBase` or `IdEntity`, you can access relations directly from the entity instance. This approach offers a convenient and straightforward way to work with relations. #### Inserting Related Records You can insert related records directly from the entity instance. For example, consider a scenario where you have a `Customer` entity and a `toMany` relation with `Order` entities. Here's how you can insert related orders for a specific customer: ```ts const customer = await customerRepo.insert({ name: 'Abshire Inc' }) await customer._.relations.orders.insert([ { amount: 5, }, { amount: 7, }, ]) ``` In this code: - We create a new `Customer` instance using `customerRepo.insert()` and set its properties. - Using `customer._.relations.orders`, we access the `orders` relation of the customer. - We insert two orders related to the customer by calling `.insert()` on the `orders` relation. #### Retrieving Related Records Fetching related records is just as straightforward. Let's say you want to find a customer by name and then retrieve their related orders: ```ts const customer = await customerRepo.findFirst({ name: 'Abshire Inc' }) const orders = await customer._.relations.orders.find() ``` In this code: - We search for a customer with the specified name using `customerRepo.findFirst()`. - Once we have the customer instance, we access their `orders` relation with `customer._.relations.orders`. - We use `.find()` to retrieve all related orders associated with the customer. Using the `activeRecord` pattern and direct access to relations simplifies the management of related data, making it more intuitive and efficient. ## Many-to-Many In Remult, you can effectively handle many-to-many relationships between entities by using an intermediate table. This approach is especially useful when you need to associate multiple instances of one entity with multiple instances of another entity. In this section, we'll walk through the process of defining and working with many-to-many relationships using this intermediate table concept. #### Entity Definitions: To illustrate this concept, let's consider two entities: `Customer` and `Tag`. In this scenario, multiple customers can be associated with multiple tags. ```ts @Entity('customers') export class Customer { @Fields.cuid() id = '' @Fields.string() name = '' @Fields.string() city = '' } @Entity('tags') export class Tag { @Fields.cuid() id = '' @Fields.string() name = '' } ``` ### Intermediate Table To establish this relationship, we'll create an intermediate table called `tagsToCustomers`. In this table, both `customerId` and `tagId` fields are combined as the primary key. ```ts @Entity('tagsToCustomers', { id: { customerId: true, tagId: true, }, }) export class TagsToCustomers { @Fields.string() customerId = '' @Fields.string() tagId = '' @Relations.toOne(() => Tag, 'tagId') tag?: Tag } ``` - To uniquely identify associations between customers and tags in a many-to-many relationship, we use the combined `customerId` and `tagId` fields as the primary key, specified using the 'id' option in the `@Entity` decorator. - In this scenario, we've defined a `toOne` relation to the `Tag` entity within the `TagsToCustomers` entity to efficiently retrieve tags associated with a specific customer. This approach simplifies the management of many-to-many relationships while ensuring unique identification of each association. Now, let's enhance our customer entity with a toMany relationship, enabling us to fetch all of its associated tags effortlessly. ```ts @Entity('customers') export class Customer { @Fields.cuid() id = '' @Fields.string() name = '' @Fields.string() city = '' @Relations.toMany(() => TagsToCustomers, 'customerId') // [!code ++] tags?: TagsToCustomers[] // [!code ++] } ``` ### Working with Many-to-Many Relationships Let's explore how to interact with many-to-many relationships using an intermediate table in Remult. #### 1. Adding Tags to a Customer: To associate a tag with a customer, consider the follow code: ```ts const tags = await remult .repo(Tag) .insert([ { name: 'vip' }, { name: 'hot-lead' }, { name: 'influencer' }, { name: 'manager' }, ]) // Create the tags const customerRepo = remult.repo(Customer) const customer = await customerRepo.findFirst({ name: 'Abshire Inc' }) await customerRepo .relations(customer) .tags.insert([{ tag: tags[0] }, { tag: tags[2] }]) ``` Here's an explanation of what's happening in this code: 1. We first insert some tags into the "tags" entity. 2. We then create a repository instance for the "customer" entity using `remult.repo(Customer)`. 3. We retrieve a specific customer by searching for one with the name "Abshire Inc" using `customerRepo.findFirst({ name: "Abshire Inc" })`. The `customer` variable now holds the customer entity. 4. To associate tags with the customer, we use the `relations` method provided by the repository. This method allows us to work with the customer's related entities, in this case, the "tags" relation to the TagsToCustomers entity. 5. Finally, we call the `insert` method on the "tags" relationship and provide an array of tag objects to insert. In this example, we associate the customer with the "vip" tag and the "influencer" tag by specifying the tags' indices in the `tags` array. **2. Retrieving Tags for a Customer:** To fetch the tags associated with a specific customer: Certainly, here's a shorter explanation: ```ts const customer = await customerRepo.findFirst( { name: 'Abshire Inc' }, { include: { tags: { include: { tag: true, }, }, }, }, ) ``` In this code, we're querying the "customer" entity to find a customer named "Abshire Inc." We're also including the related "tags" for that customer, along with the details of each tag. This allows us to fetch both customer and tag data in a single query, making it more efficient when working with related entities. ### Resulting Data Structure Here's an example result of running `JSON.stringify` on the `customer` object: ```json { "id": "fki6t24zkykpljvh4jurzs97", "name": "Abshire Inc", "city": "New York", "tags": [ { "customerId": "fki6t24zkykpljvh4jurzs97", "tagId": "aewm0odq9758nopgph3x7brt", "tag": { "id": "cf8xv3myluc7pmsgez3p9hn9", "name": "vip" } }, { "customerId": "fki6t24zkykpljvh4jurzs97", "tagId": "aewm0odq9758nopgph3x7brt", "tag": { "id": "aewm0odq9758nopgph3x7brt", "name": "influencer" } } ] } ``` Utilizing an intermediate table for managing many-to-many relationships in Remult allows for a flexible and efficient approach to handle complex data associations. Whether you are connecting customers with tags or other entities, this method provides a powerful way to maintain data integrity and perform queries effectively within your application. --- In this guide, we've explored the essential concepts of managing entity relations within the Remult library. From one-to-one to many-to-many relationships, we've covered the declaration, customization, and querying of these relations. By understanding the nuances of entity relations, users can harness the full potential of Remult to build robust TypeScript applications with ease. # Relations 🚀 - Filtering and Relations # Filtering and Relations ::: tip **Interactive Learning Available! 🚀** Looking to get hands-on with this topic? Try out our new [**interactive tutorial**](https://learn.remult.dev/in-depth/4-filtering/1-custom-filters) on Filtering relations, where you can explore and practice directly in the browser. This guided experience offers step-by-step lessons to help you master filtering in Remult with practical examples and exercises. [Click here to dive into the interactive tutorial on Filtering and Relations!](https://learn.remult.dev/in-depth/4-filtering/1-custom-filters) ::: In this article, we'll discuss several relevant techniques for one-to-many relations. Consider the following scenario where we have a customer entity and an Orders entity. We'll use the following entities and data for this article. ```ts import { Entity, Field, Fields, remult, Relations } from 'remult' @Entity('customers') export class Customer { @Fields.autoIncrement() id = 0 @Fields.string() name = '' @Fields.string() city = '' @Relations.toMany(() => Order) orders?: Order[] } @Entity('orders') export class Order { @Fields.autoIncrement() id = 0 @Relations.toOne(() => Customer) customer!: Customer @Fields.number() amount = 0 } ``` ::: tip Use Case in this article Let's say that we want to filter all the orders of customers who are in London. Let's have a look at the different options to achieve this. ::: ## Option 1 - Use In Statement Add the `where` inline to the `find` method. ```ts console.table( await repo(Order).find({ where: { customer: await repo(Customer).find({ where: { city: 'London', }, }), }, }), ) ``` ## Option 2 - Use Custom Filter We can refactor this to a custom filter that will be easier to use and will run on the backend ```ts import { Filter } from 'remult' @Entity('orders', { allowApiCrud: true }) export class Order { //... static filterCity = Filter.createCustom( async ({ city }) => ({ customer: await repo(Customer).find({ where: { city } }), }), ) } ``` And then we can use it: ```ts console.table( await repo(Order).find({ where: Order.filterCity({ city: 'London', }), }), ) ``` ## Option 3 - Custom Filter (SQL) We can improve on the custom filter by using the database's in statement capabilities: ```ts import { SqlDatabase } from 'remult' @Entity('orders', { allowApiCrud: true }) export class Order { //... static filterCity = Filter.createCustom( async ({ city }) => SqlDatabase.rawFilter( ({ param }) => `customer in (select id from customers where city = ${param(city)})`, ), ) } ``` We can also reuse the entity definitions by using `dbNamesOf` and `filterToRaw` ```ts import { dbNamesOf } from 'remult' @Entity('orders', { allowApiCrud: true }) export class Order { //... static filterCity = Filter.createCustom( async ({ city }) => { const orders = await dbNamesOf(Order) const customers = await dbNamesOf(Customer) return SqlDatabase.rawFilter( async ({ filterToRaw }) => `${orders.customer} in (select ${customers.id} from ${customers} where ${await filterToRaw(Customer, { city })})`, ) }, ) } ``` ## Option 4 - sqlExpression field ```ts @Entity('orders', { allowApiCrud: true }) export class Order { //... @Fields.string({ sqlExpression: async () => { const customer = await dbNamesOf(Customer) const order = await dbNamesOf(Order) return `( select ${customer.city} from ${customer} where ${customer.id} = ${order.customer} )` }, }) city = '' } ``` - This adds a calculated `city` field to the `Order` entity that we can use to order by or filter ```ts console.table( await repo(Order).find({ where: { city: 'London', }, }), ) ``` ::: details Side Note In this option, `city` is always calculated, and the `sqlExpression` is always executed. Not a big deal, but it's woth mentioning. (Check out Option 5 for a solution) ::: ## Option 5 - Dedicated entity ```ts export class OrderWithCity extends Order { @Fields.string({ sqlExpression: async () => { const customer = await dbNamesOf(Customer) const order = await dbNamesOf(Order) return `( select ${customer.city} from ${customer} where ${customer.id} = ${order.customer} )` }, }) city = '' } ``` Like this, in your code, you can use `OrderWithCity` or `Order` depending on your needs. ::: tip As `OrderWithCity` extends `Order`, everything in `Order` is also available in `OrderWithCity` 🎉. ::: # Entities - Lifecycle Hooks # Entity Lifecycle Hooks In Remult, you can take advantage of Entity Lifecycle Hooks to add custom logic and actions at specific stages of an entity's lifecycle. There are five lifecycle events available: `validation`, `saving`, `saved`, `deleting`, and `deleted`. These hooks allow you to perform actions or validations when specific events occur in the entity's lifecycle. ## Validation - **Runs On**: Backend and Frontend. - **Purpose**: To perform validations on the entity's data before saving. - **Example**: ```ts @Entity("tasks", { validation: async (task, e) => { if (task.title.length < 5) { throw new Error("Task title must be at least 5 characters long."); } }, }) ``` You can run custom validation like in this example, and you can also use [builtin validation](./validation.md). ## Saving - **Runs On**: Backend (or Frontend if using a local frontend database). - **Purpose**: To execute custom logic before an entity is saved. - **Example**: ```ts @Entity("tasks", { saving: async (task, e) => { if (e.isNew) { task.createdAt = new Date(); // Set the creation date for new tasks. } task.lastUpdated = new Date(); // Update the last updated date. }, }) ``` ## Saved - **Runs On**: Backend (or Frontend if using a local frontend database). - **Purpose**: To perform actions after an entity has been successfully saved. - **Example**: Useful for triggering additional processes or updates after saving. ## Deleting - **Runs On**: Backend (or Frontend if using a local frontend database). - **Purpose**: To execute custom logic before an entity is deleted. - **Example**: You can use this to ensure related data is properly cleaned up or archived. ## Deleted - **Runs On**: Backend (or Frontend if using a local frontend database). - **Purpose**: To perform actions after an entity has been successfully deleted. - **Example**: Similar to the `saved` event, this is useful for any post-deletion processes. ## Field Saving Hook Additionally, you can define a field-specific `saving` hook that allows you to perform custom logic on a specific field before the entity `saving` hook. It has the following signature: ```ts @Fields.Date({ saving: (task, fieldRef, e) => { if (e.isNew) task.createdAt = new Date() }, }) createdAt = new Date() ``` or using the fieldRef ```ts @Fields.Date({ saving: (_, fieldRef, e) => { if (e.isNew) fieldRef.value = new Date() }, }) createdAt = new Date() ``` You can use the field `saving` hook to perform specialized actions on individual fields during the entity's saving process. ## Lifecycle Event Args Each lifecycle event receives an instance of the relevant entity and an event args of type `LifecycleEvent`. The `LifecycleEvent` object provides various fields and methods to interact with the entity and its context. Here are the fields available in the `LifecycleEvent`: - `isNew`: A boolean indicating whether the entity is new (being created). - `fields`: A reference to the entity's fields, allowing you to access and modify field values. - `id`: The ID of the entity. - `originalId`: The original ID of the entity, which may differ during certain operations. - `repository`: The repository associated with the entity. - `metadata`: The metadata of the entity, providing information about its structure. - `preventDefault()`: A method to prevent the default behavior associated with the event. - `relations`: Access to repository relations for the entity, allowing you to work with related data. ## Example Usage Here's an example of how to use Entity Lifecycle Hooks to add custom logic to the `saving` event: ```ts @Entity("tasks", { saving: async (task, e) => { if (e.isNew) { task.createdAt = new Date(); // Set the creation date for new tasks. } task.lastUpdated = new Date(); // Update the last updated date. }, }) ``` In this example, we've defined a `saving` event for the `Task` entity. When a task is being saved, the event handler is called. If the task is new (not yet saved), we set its `createdAt` field to the current date. In either case, we update the `lastUpdated` field with the current date. Entity Lifecycle Hooks provide a powerful way to customize the behavior of your entities and ensure that specific actions or validations are performed at the right time in the entity's lifecycle. You can use these hooks to streamline your application's data management and enforce business rules. # Entities - Migrations # Migrations Managing database schemas is crucial in web development. Traditional migration approaches introduce complexity and risks. Remult, designed for data-driven web apps with TypeScript, offers a simpler method. ## You Don't Necessarily Need Migrations Migration files are standard but can complicate database schema management. They're prone to errors, potentially leading to conflicts or downtime. Remult proposes a streamlined alternative: automatic schema synchronization. This approach simplifies schema management by ensuring your database schema aligns with your application code without the manual overhead of traditional migrations. ### Embracing Schema Synchronization with Remult Remult offers an alternative: automatic schema synchronization. **By default, Remult checks for and synchronizes your database schema with the entity types** provided in the `RemultServerOptions.entities` property when the server loads. This feature automatically adds any missing tables or columns, significantly simplifying schema management. ::: tip No Data Loss with Remult's Safe Schema Updates **Remult's schema synchronization** ensures **safe and automatic updates** to your database schema. By only adding new tables or columns without altering existing ones, Remult prevents data loss. This design offers a secure way to evolve your application's database schema. ::: #### Disabling Automatic Schema Synchronization For manual control, Remult allows disabling automatic schema synchronization: ```typescript const api = remultExpress({ entities: [], // Your entities here ensureSchema: false, // Disables automatic schema synchronization, Default: true }) ``` #### Manually Triggering Schema Synchronization In certain scenarios, you might want to manually trigger the `ensureSchema` function to ensure that your database schema is up-to-date with your entity definitions. Here's how you can do it: ```ts remult.dataProvider.ensureSchema!(entities.map((x) => remult.repo(x).metadata)) ``` ## Quick Start: Introducing Migrations to Your Application Introducing migrations to your Remult application involves a few straightforward steps. The goal is to ensure that your migrations and API share the same data provider and entity definitions. Here's how you can do it: ### 1. Refactor Your Configuration Start by refactoring the `dataProvider` and `entities` definitions from the `api.ts` file to a new file named `src/server/config.ts`. This allows you to use the same configurations for both your API and migrations. In your `src/server/config.ts` file, define your entities and data provider as follows: ```ts import { createPostgresDataProvider } from 'remult/postgres' import { Task } from '../shared/task' export const entities = [Task /* ...other entities */] export const dataProvider = createPostgresDataProvider({ connectionString: 'your connection string', }) ``` :::tip Using environment variables In most cases, the connection string for your database will not be hard-coded but stored in an environment variable for security and flexibility. A common practice is to use a `.env` file to store environment variables in development and load them using the `dotenv` npm package. Here's how you can set it up: 1. Install the `dotenv` package: ```sh npm install dotenv ``` 2. Create a `.env` file in the root of your project and add your database connection string: ``` DATABASE_URL=your_connection_string ``` 3. At the beginning of your `src/server/config.ts` file, load the environment variables: ```ts import { config } from 'dotenv' config() ``` 4. Access the connection string using `process.env`: ```ts export const dataProvider = createPostgresDataProvider({ connectionString: process.env['DATABASE_URL'], }) ``` By following these steps, you ensure that your application securely and flexibly manages the database connection string. ::: ### 2. Adjust the API Configuration Next, adjust your `api.ts` file to use the configurations from the `config.ts` file, and disable the `ensureSchema` migrations: ```ts import { remultExpress } from 'remult/remult-express' import { dataProvider, entities } from './config' export const api = remultExpress({ entities, dataProvider, ensureSchema: false, }) ``` ### 3. Generate the migration ::: tip Prettier The migration generator uses `prettier` to format the generated code for better readability and consistency. If you don't already have `prettier` installed in your project, we recommend installing it as a development dependency using the following command: ```sh npm i -D prettier ``` ::: To enable automatic generation of migration scripts, follow these steps: 1. **Create the Migrations Folder:** In your `src/server` directory, create a new folder named `migrations`. This folder will hold all your migration scripts. 2. **Create the Migration Generator File:** Inside the `migrations` folder, create a file named `generate-migrations.ts`. This file will contain the script that generates migration scripts based on changes in your entities. Here's the revised section: 3. **Populate the Generator File:** Add the following code to `generate-migrations.ts`: ```ts import { generateMigrations } from 'remult/migrations' import { dataProvider, entities } from '../config' generateMigrations({ dataProvider, // The data provider for your database entities, // Entity classes to include in the migration endConnection: true, // Close the database connection after generating migrations (useful for standalone scripts) }) ``` This script generates migration scripts based on changes in your entities. If you're calling this method on a server where the database connection should remain open, omit the `endConnection` parameter or set it to `false`. 4. **Generate Migrations:** To generate the migration scripts, run the `generate-migrations.ts` script using the following command: ```sh npx tsx src/server/migrations/generate-migrations.ts ``` This command will create two important files: 1. **`migrations-snapshot.json`**: This file stores the last known state of your entities. It helps the migration generator understand what changes have been made since the last migration was generated. 2. **`migrations.ts`**: This file contains the actual migration scripts that need to be run to update your database schema. The structure of this file is as follows: ```ts import type { Migrations } from 'remult/migrations' export const migrations: Migrations = { 0: async ({ sql }) => { await sql(`--sql CREATE SCHEMA IF NOT EXISTS public; CREATE TABLE "tasks" ( "id" VARCHAR DEFAULT '' NOT NULL PRIMARY KEY, "title" VARCHAR DEFAULT '' NOT NULL )`) }, } ``` Each migration script is associated with a unique identifier (in this case, `0`) and contains the SQL commands necessary to update the database schema. By running this script whenever you make changes to your entities, you can automatically generate the necessary migration scripts to keep your database schema in sync with your application's data model. It's important to note that each migration can include any code that the developer wishes to include, not just SQL statements. The `sql` parameter is provided to facilitate running SQL commands, but you can also include other logic or code as needed. Additionally, developers are encouraged to add their own custom migrations to address specific requirements or changes that may not be covered by automatically generated migrations. This flexibility allows for a more tailored approach to managing database schema changes. ### 4. Run the Migrations To apply the migrations to your database, you'll need to create a script that executes them. #### Setting Up the Migration Script 1. **Create the Migration Script:** In the `src/server/migrations` folder, add a file named `migrate.ts`. 2. **Populate the Script:** Add the following code to `migrate.ts`: ```ts import { migrate } from 'remult/migrations' import { dataProvider } from '../config' import { migrations } from './migrations' migrate({ dataProvider, migrations, endConnection: true, // Close the database connection after applying migrations }) ``` This script sets up the migration process. The `migrate` function checks the last migration executed on the database and runs all subsequent migrations based on their index in the `migrations` file. The entire call to `migrate` is executed in a transaction, ensuring that either all required migration steps are executed or none at all, maintaining the integrity of your database schema. ::: warning Warning: Database Transaction Support for Structural Changes It's important to note that some databases, like MySQL, do not support rolling back structural changes as part of a transaction. This means that if you make changes to the database schema (such as adding or dropping tables or columns) and something goes wrong, those changes might not be automatically rolled back. Developers need to be aware of this limitation and plan their migrations accordingly to avoid leaving the database in an inconsistent state. Always consult your database's documentation to understand the specifics of transaction support and plan your migrations accordingly. ::: 3. **Execute the Script:** Run the migration script using the following command: ```sh npx tsx src/server/migrations/migrate.ts ``` ## Integrating Migrations into Your Deployment Process You have a couple of options for when and how to run your migrations: - **As Part of the Build Step:** You can include the migration script as part of your build or deployment process. This way, if the migration fails, the deployment will also fail, preventing potential issues with an inconsistent database state. - **During Application Initialization:** Alternatively, you can run the migrations when your application loads by using the `initApi` option in your `api.ts` file: ```ts // src/server/api.ts import { remultExpress } from 'remult/remult-express' import { dataProvider, entities } from './config' import { migrate } from 'remult/migrations/migrate' import { migrations } from './migrations/migrations' import { remult } from 'remult' export const api = remultExpress({ entities, dataProvider, initApi: async () => { await migrate({ dataProvider: remult.dataProvider, migrations, endConnection: false, //it's the default :) }) }, }) ``` This approach ensures that the migrations are applied each time the API initializes. Note that the `migrate` and `generateMigrations` functions typically close the connection used by the `dataProvider` when they complete. In this code, we disable this behavior using the `endConnection: false` option, instructing the `migrate` function to keep the `dataProvider` connection open when it completes. Choose the approach that best fits your application's deployment and initialization process. ### Migration Philosophy: Embracing Backward Compatibility We believe in designing migrations with a backward compatibility mindset. This approach ensures that older versions of the code can operate smoothly with newer versions of the database. To achieve this, we recommend: - Never dropping columns or tables. - Instead of altering a column, adding a new column and copying the data to it as part of the migration process. This philosophy minimizes disruptions and ensures a smoother transition during database schema updates. # Entities - Generate from Existing DB # Generate Entities from Existing Database ## Remult kit Want to use Remult for full-stack CRUD with your existing database? Check out this video to see how to connect http://remult.dev to your existing database and start building type-safe #fullstack apps with any #typescript frontend, backend, and any DB! Watch now 👉 https://youtu.be/5QCzJEO-qQ0. # Entities - Offline Support # Offline Support In modern web applications, providing a seamless user experience often involves enabling offline functionality. This ensures that users can continue to interact with the application even without an active internet connection. Remult supports several offline databases that can be used to store data in the browser for offline scenarios, enhancing the application's resilience and usability. ## Using Local Database for Specific Calls To utilize a local database for a specific call, you can pass the `dataProvider` as a second parameter to the `repo` function. This allows you to specify which database should be used for that particular operation. ```typescript import { localDb } from './some-file.ts' console.table(await repo(Task, localDb).find()) ``` In this example, `localDb` is used as the data provider for the `Task` repository, enabling data fetching from the local database. ## JSON in LocalStorage / SessionStorage For simple data storage needs, you can use JSON data providers that leverage the browser's `localStorage` or `sessionStorage`. ```typescript import { JsonDataProvider, Remult } from 'remult' export const remultLocalStorage = new Remult(new JsonDataProvider(localStorage)) ``` This approach is straightforward and suitable for small datasets that need to persist across sessions or page reloads. ## JSON Storage in IndexedDB For more complex offline storage needs, such as larger datasets and structured queries, `IndexedDB` provides a robust solution. Using Remult’s `JsonEntityIndexedDbStorage`, you can store entities in `IndexedDB`, which is supported across all major browsers. This allows for efficient offline data management while offering support for larger volumes of data compared to `localStorage` or `sessionStorage`. ```typescript import { JsonDataProvider } from 'remult' import { JsonEntityIndexedDbStorage } from 'remult' // Initialize the JsonEntityIndexedDbStorage const db = new JsonDataProvider(new JsonEntityIndexedDbStorage()) // Use the local IndexedDB to store and fetch tasks console.table(await repo(Task, db).find()) ``` In this example, `JsonEntityIndexedDbStorage` is used to persist the data to `IndexedDB`. This method is ideal for applications with large data sets or those requiring more complex interactions with the stored data in offline mode. ## JSON Storage in OPFS (Origin Private File System) Origin Private File System (OPFS) is a modern browser feature supported by Chrome and Safari, allowing for more structured and efficient data storage in the frontend. ```typescript import { JsonDataProvider } from 'remult' import { JsonEntityOpfsStorage } from 'remult' const localDb = new JsonDataProvider(new JsonEntityOpfsStorage()) ``` Using OPFS with Remult's `JsonDataProvider` provides a robust solution for storing entities in the frontend, especially for applications requiring more complex data handling than what `localStorage` or `sessionStorage` can offer. Certainly! Here's the adjusted section on `sql.js` with an enriched code sample: ## `sql.js`: A SQLite Implementation for the Frontend For applications requiring advanced database functionality, [`sql.js`](https://sql.js.org/) provides a SQLite implementation that runs entirely in the frontend. This allows you to use SQL queries and transactions, offering a powerful and flexible data management solution for offline scenarios. Before using `sql.js` in your project, you need to install the package and its TypeScript definitions. Run the following commands in your terminal: ```bash npm install sql.js npm install @types/sql.js --save-dev ``` After installing the necessary packages, you can use the following code sample in your project: ```typescript import { SqlDatabase } from 'remult' import { SqlJsDataProvider } from 'remult/remult-sql-js' import initSqlJs from 'sql.js' let sqlDb: Database // Initialize the SqlJsDataProvider with a new database instance const sqlJsDataProvider = new SqlJsDataProvider( initSqlJs({ locateFile: (file) => `https://sql.js.org/dist/${file}`, // for complete offline support, change this to a url that is available offline }).then((x) => { // Load the database from localStorage if it exists const dbData = localStorage.getItem('sqljs-db') if (dbData) { const buffer = new Uint8Array(JSON.parse(dbData)) return (sqlDb = new x.Database(buffer)) } return (sqlDb = new x.Database()) }), ) // Set up an afterMutation hook to save the database to localStorage after any mutation sqlJsDataProvider.afterMutation = async () => { const db = sqlDb const buffer = db.export() localStorage.setItem('sqljs-db', JSON.stringify([...buffer])) } const localDb = new SqlDatabase(sqlJsDataProvider) ``` This code sets up a SQLite database using `sql.js` in your Remult project, with support for saving to and loading from `localStorage`. ## Summary Remult's support for various offline databases empowers developers to create web applications that provide a seamless user experience, even in offline scenarios. Whether using simple JSON storage in `localStorage` or more advanced solutions like OPFS or `sql.js`, Remult offers the flexibility to choose the right data storage solution for your application's needs. By leveraging these offline capabilities, you can ensure that your application remains functional and responsive, regardless of the user's connectivity status. # Entities - Active Record & EntityBase # Mutability and the Active Record Pattern The Active Record pattern is a concept in software architecture, particularly useful when working with mutable objects whose state may change over time. This design pattern facilitates direct interaction with the database through the object representing a row of the data table. In this article, we'll delve into the fundamentals of the Active Record pattern, contrasting it with immutable patterns, and exploring its implementation and advantages in software development. ### Immutable vs. Mutable Patterns In modern software development, handling data objects can generally be approached in two ways: immutable and mutable patterns. **Immutable objects** do not change once they are created. Any modification on an immutable object results in a new object. For example, in the React framework, immutability is often preferred: ```typescript // Immutable update const updatePerson = { ...person, name: 'newName' } ``` However, libraries like MobX offer the flexibility to work with mutable objects while still providing the reactivity that React components need. **Mutable objects**, on the other hand, allow for changes directly on the object itself: ```typescript // Mutable update person.name = 'newName' ``` Mutable patterns are especially prevalent in scenarios where the state of objects changes frequently, making them a staple in many programming environments outside of React. ### The Role of Active Record Pattern The Active Record pattern embodies the concept of mutability by binding business logic to object data models. Typically, each model instance corresponds to a row in the database, with the class methods providing the functionality to create, read, update, and delete records. ### Warning: Mutable Objects in React Using mutable objects with the Active Record pattern in React (without libraries like MobX) requires careful handling. React’s rendering cycle is built around the premise of immutability; it typically relies on immutable state management to trigger re-renders. When mutable objects change state outside the scope of React's `useState` or `useReducer`, React does not automatically know to re-render the affected components. This can lead to issues where the UI does not reflect the current application state. These challenges can be mitigated by integrating state management tools that are designed to work well with mutable objects, such as MobX. MobX provides mechanisms to track changes in data and automatically re-render components when mutations occur. This aligns more naturally with the Active Record pattern within the context of React, ensuring that the UI stays in sync with the underlying data. #### Using EntityBase and IdEntity In practice, leveraging the Active Record pattern often involves inheriting from classes such as `EntityBase` or `IdEntity` (a variant of `EntityBase` with a UUID as the identifier). These base classes enrich models with methods that simplify manipulations of their attributes and their persistence in the database. ```typescript @Entity('people') export class Person extends IdEntity { @Fields.string() name = '' } ``` **Explanation:** The `Person` class represents individuals in the 'people' table and inherits from `IdEntity`. This inheritance means that there is no need to explicitly define an `id` field for this class, as `IdEntity` automatically includes a UUID field (`id`). Consequently, `Person` benefits from all the functionalities of `EntityBase`, which include tracking changes and handling CRUD operations, while also automatically gaining a UUID as the identifier. ### Mutable vs EntityBase (active-record) **Traditional approach without Active Record:** ```typescript // Updating a person's name the traditional way await repo(Person).update(person, { name: 'newName' }) ``` **Using Active Record with EntityBase:** ```typescript // Active Record style person.name = 'newName' await person.save() ``` This pattern also simplifies other operations: ```typescript // Deleting a record await person.delete() // Checking if the record is new if (person.isNew()) { // Perform a specific action } ``` #### Helper Members in EntityBase EntityBase provides additional utility members like `_` and `$` to facilitate more complex interactions: - **`_` (EntityRef Object):** Allows performing operations on a specific instance of an entity. ```typescript await person._.reload() ``` - **`$` (FieldsRef):** Provides access to detailed information about each field in the current instance, such as their original and current values: ```typescript // Logging changes in a field console.log( `Name changed from "${person.$.name.originalValue}" to "${person.name}"`, ) ``` ### Alternative Implementations Even without direct inheritance from `EntityBase`, similar functionalities can be achieved using helper functions such as `getEntityRef`, which encapsulates an entity instance for manipulation and persistence: ```typescript const ref = getEntityRef(person) await ref.save() ``` ### Conclusion The Active Record pattern offers a straightforward and intuitive approach to interacting with database records through object-oriented models. It is particularly beneficial in environments where business logic needs to be tightly coupled with data manipulation, providing a clear and efficient way to handle data state changes. However, integrating the Active Record pattern with mutable objects in React can be challenging. # Active Record & EntityBase - Entity Backend Methods # Entity Instance Backend Methods When leveraging the Active Record pattern, backend methods for entity instances offer a powerful way to integrate client-side behavior with server-side logic. These methods, when invoked, transport the entire entity's state from the client to the server and vice versa, even if the data has not yet been saved. This feature is particularly useful for executing entity-specific operations that require a round-trip to the server to maintain consistency and integrity. ## Overview of Entity Backend Methods Entity backend methods enable all the fields of an entity, including unsaved values, to be sent to and from the server during the method's execution. This approach is essential for operations that rely on the most current state of an entity, whether or not the changes have been persisted to the database. ### Defining a Backend Method To define a backend method, use the `@BackendMethod` decorator to annotate methods within an entity class. This decorator ensures that the method is executed on the server, taking advantage of server-side resources and permissions. Here is an example demonstrating how to define and use a backend method in an entity class: ```typescript @Entity('tasks', { allowApiCrud: true, }) export class Task extends IdEntity { @Fields.string() title = '' @Fields.boolean() completed = false @BackendMethod({ allowed: true }) async toggleCompleted() { this.completed = !this.completed console.log({ title: this.title, titleOriginalValue: this.$.title.originalValue, }) await this.save() } } ``` ### Calling the Backend Method from the Frontend Once the backend method is defined, it can be called from the client-side code. This process typically involves fetching an entity instance and then invoking the backend method as shown below: ```typescript const task = await remult.repo(Task).findFirst() await task.toggleCompleted() ``` ### Security Considerations ::: danger It's important to note that backend methods bypass certain API restrictions that might be set on the entity, such as `allowApiUpdate=false`. This means that even if an entity is configured not to allow updates through standard API operations, it can still be modified through backend methods if they are permitted by their `allowed` setting. Consequently, developers must explicitly handle security and validation within these methods to prevent unauthorized actions. The principle here is that if a user has permission to execute the `BackendMethod`, then all operations within that method are considered authorized. It is up to the developer to implement any necessary restrictions within the method itself. ::: # Active Record & EntityBase - Mutable Controllers # Introduction to Mutable Controllers and Backend Methods In web development architectures, mutable controllers offer a convenient way to manage state and facilitate interactions between the client (frontend) and the server (backend). These controllers are useful in scenarios where state needs to be maintained and manipulated across server calls, providing a streamlined approach to handling data. ## Overview of Controller Backend Methods A Controller is a class designed to encapsulate business logic and data processing. When a backend method in a controller is called, it ensures that all field values are preserved and appropriately transferred between the frontend and backend, maintaining state throughout the process. ### Defining a Mutable Controller The mutable controller is typically defined in a shared module, allowing both the frontend and backend to interact with it efficiently. Below is an example of how to define such a controller and a backend method within it. ### Explanation with Data Flow and Example Usage This example demonstrates the use of a mutable controller, `UserSignInController`, to handle the sign-in process for users in a web application. Let's break down the key components of this example: 1. **Controller Definition**: The `UserSignInController` is a class annotated with `@Controller('UserSignInController')`, indicating that it serves as a controller for handling user sign-in operations. 2. **Data Flow**: When the `signInUser` backend method is called from the frontend, all the values of the controller fields (`email`, `password`, `rememberMe`) will be sent to the backend for processing. Once the method completes its execution, the updated values (if any) will be sent back to the frontend. ### Example Usage Here's how you can use the `UserSignInController` on the frontend to initiate the sign-in process: ```typescript const signInController = new UserSignInController() signInController.email = 'user@example.com' signInController.password = 'password123' signInController.rememberMe = true // Optional: Set to true if the user wants to remain logged in try { const user = await signInController.signInUser() console.log(`User signed in: ${user.email}`) } catch (error) { console.error('Sign-in failed:', error.message) } ``` In this example, we create an instance of `UserSignInController` and set its `email`, `password`, and `rememberMe` fields with the appropriate values. We then call the `signInUser` method to initiate the sign-in process. If successful, we log a message indicating that the user has signed in. If an error occurs during the sign-in process, we catch the error and log a corresponding error message. This usage demonstrates how to interact with the mutable controller to handle user sign-in operations seamlessly within a web application. ### Summary Mutable controllers and backend methods provide a powerful mechanism for managing state and handling user interactions in web applications. By encapsulating business logic and data processing within controllers, developers can ensure consistent behavior and efficient data flow between the frontend and backend. With the ability to preserve and transfer field values during server calls, mutable controllers facilitate a smooth and responsive user experience, enhancing the overall functionality and performance of web applications. # Stacks # Stacks ## Frameworks
## Servers
## Databases
# Stacks - Framework # Select a framework
# Framework - React # React ## Create a React Project with Vite To set up a new React project using Vite, run the following commands: ```sh npm create vite@latest remult-react-project -- --template react-ts cd remult-react-project ``` ## Install Remult Install the latest version of Remult: ```bash npm install remult@latest ``` ## Enable TypeScript Decorators in Vite To enable the use of decorators in your React app, modify the `vite.config.ts` file by adding the following to the `defineConfig` section: ```ts{6-12} // vite.config.ts // ... export default defineConfig({ plugins: [react()], esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` This configuration ensures that TypeScript decorators are enabled for the project. ## Proxy API Requests from Vite DevServer to the API Server In development, your React app will be served from `http://localhost:5173`, while the API server will run on `http://localhost:3002`. To allow the React app to communicate with the API server during development, use Vite's [proxy](https://vitejs.dev/config/#server-proxy) feature. Add the following proxy configuration to the `vite.config.ts` file: ```ts{6} // vite.config.ts //... export default defineConfig({ plugins: [react()], server: { proxy: { "/api": "http://localhost:3002" } }, esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` This setup proxies all requests starting with `/api` from `http://localhost:5173` to your API server running at `http://localhost:3002`. ## Configure a Server Now that the app is set up, [Select an API Server](../server/) # Framework - Angular # Angular ## Create an Angular Project To set up a new Angular project, use the Angular CLI: ```sh ng new remult-angular cd remult-angular ``` ## Install Remult Install the latest version of Remult in your Angular project: ```bash npm install remult@latest ``` ## Proxy API Requests from Angular DevServer to the API Server In development, your Angular app will be served from `http://localhost:4200`, while the API server will run on `http://localhost:3002`. To allow the Angular app to communicate with the API server during development, you can use Angular's [proxy](https://angular.io/guide/build#proxying-to-a-backend-server) feature. 1. Create a file named `proxy.conf.json` in the root folder of your project with the following content: ```json // proxy.conf.json { "/api": { "target": "http://localhost:3002", "secure": false } } ``` This configuration redirects all API calls from the Angular dev server to the API server running at `http://localhost:3002`. ## Adjust the `package.json` Modify the `package.json` to use the newly created proxy configuration when serving the Angular app: ```json // package.json "dev": "ng serve --proxy-config proxy.conf.json --open", ``` Running the `dev` script will start the Angular dev server with the proxy configuration enabled. ## Configure a Server Now that the app is set up, [Select an API Server](../server/) # Framework - Vue # Vue ## Create a Vue Project with Vite To set up a new Vue project using Vite, run the following commands: ```sh npm init -y vue@latest cd remult-vue-project ``` ## Install Remult Install the latest version of Remult: ```bash npm install remult@latest ``` ## Enable TypeScript Decorators in Vite To enable the use of decorators in your React app, modify the `vite.config.ts` file by adding the following to the `defineConfig` section: ```ts{6-12} // vite.config.ts // ... export default defineConfig({ plugins: [vue()], esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` This configuration ensures that TypeScript decorators are enabled for the project. ## Proxy API Requests from Vite DevServer to the API Server In development, your React app will be served from `http://localhost:5173`, while the API server will run on `http://localhost:3002`. To allow the React app to communicate with the API server during development, use Vite's [proxy](https://vitejs.dev/config/#server-proxy) feature. Add the following proxy configuration to the `vite.config.ts` file: ```ts{6} // vite.config.ts //... export default defineConfig({ plugins: [vue()], server: { proxy: { "/api": "http://localhost:3002" } }, esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` This setup proxies all requests starting with `/api` from `http://localhost:5173` to your API server running at `http://localhost:3002`. ## Configure a Server Now that the app is set up, [Select an API Server](../server/) # Framework - Next.js # Next.js ## Create a Next.js Project To create a new Next.js project, run the following command: ```sh npx -y create-next-app@latest remult-nextjs ``` When prompted, use these answers: ```sh ✔ Would you like to use TypeScript? ... Yes ✔ Would you like to use ESLint? ... No ✔ Would you like to use Tailwind CSS? ... No ✔ Would you like to use `src/` directory? ... Yes ✔ Would you like to use App Router? (recommended) ... Yes ✔ Would you like to customize the default import alias? ... No ``` Afterward, navigate into the newly created project folder: ```sh cd remult-nextjs ``` ## Install Remult Install the latest version of Remult: ```bash npm install remult ``` ## Bootstrap Remult in the Backend Remult is bootstrapped in a Next.js app by creating a [catch-all dynamic API route](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments). This route will pass API requests to an object created using the `remultNextApp` function. 1. **Create an API file** In the `src/` directory, create a file called `api.ts` with the following code to set up Remult: ```ts // src/api.ts import { remultNextApp } from 'remult/remult-next' export const api = remultNextApp({}) ``` 2. **Create the API Route** In the `src/app/api` directory, create a `[...remult]` subdirectory. Inside that directory, create a `route.ts` file with the following code: ```ts // src/app/api/[...remult]/route.ts import { api } from '../../../api' export const { POST, PUT, DELETE, GET } = api ``` This file serves as a catch-all route for the Next.js API, handling all API requests by routing them through Remult. ## Enable TypeScript Decorators To enable the use of decorators in your Next.js app, modify the `tsconfig.json` file. Add the following entry under the `compilerOptions` section: ```json{7} // tsconfig.json { ... "compilerOptions": { ... "experimentalDecorators": true // add this line ... } } ``` ## Run the App To start the development server, open a terminal and run the following command: ```sh npm run dev ``` Your Next.js app is now running with Remult integrated and listening for API requests. # Framework - Sveltekit # SvelteKit ## Create a SvelteKit Project To create a new SvelteKit project, run the following command: ```sh npx sv@latest create remult-sveltekit-todo ``` During the setup, answer the prompts as follows: 1. **Which Svelte app template?**: ... `minimal` Project 2. **Add type checking with TypeScript?** ... Yes, using `TypeScript` syntax 3. **Select additional options**: ... We didn't select anything for this tutorial. Feel free to adapt it to your needs. 4. **Which package manager?**: ... We took `npm`, if you perfer others, feel free. Once the setup is complete, navigate into the project directory: ```sh cd remult-sveltekit-todo ``` ## Install Required Packages and Remult Install Remult and any necessary dependencies by running: ```sh npm install remult --save-dev ``` ## Bootstrap Remult To set up Remult in your SvelteKit project: 1. Create your remult `api` ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' export const api = remultSveltekit({}) ``` ::: 2. Create a remult `api route` ::: code-group ```ts [src/routes/api/[...remult]/+server.ts] import { api } from '../../../server/api' export const { GET, POST, PUT, DELETE } = api ``` ::: ## Final Tweaks Remult uses TypeScript decorators to enhance classes into entities. To enable decorators in your SvelteKit project, modify the `tsconfig.json` file by adding the following to the `compilerOptions` section: ```json [tsconfig.json] { "compilerOptions": { "experimentalDecorators": true // [!code ++] } } ``` ## Run the App To start the development server, run the following command: ```sh npm run dev ``` Your SvelteKit app will be available at [http://localhost:5173](http://localhost:5173). Your SvelteKit project with Remult is now up and running. # Extra ## Extra - Remult in other SvelteKit routes To enable remult across all sveltekit route ::: code-group ```ts [src/hooks.server.ts] import { sequence } from '@sveltejs/kit/hooks' import { api as handleRemult } from './server/api' export const handle = sequence( // Manage your sequence of handlers here handleRemult, ) ``` ::: ## Extra - Universal load & SSR To Use remult in ssr `PageLoad` - this will leverage the `event`'s fetch to load data on the server without reloading it on the frontend, and abiding to all api rules even when it runs on the server ::: code-group ```ts [src/routes/+page.ts] import { remult } from 'remult' import type { PageLoad } from './$types' export const load = (async (event) => { // Instruct remult to use the special svelte fetch // Like this univeral load will work in SSR & CSR remult.useFetch(event.fetch) return repo(Task).find() }) satisfies PageLoad ``` ::: ::: tip You can add this in `+layout.ts` as well and all routes **under** will have the correct fetch out of the box. ::: ## Extra - Server load If you return a remult entity from the `load` function of a `+page.server.ts`, SvelteKit will complain and show this error: ```bash Error: Data returned from `load` while rendering / is not serializable: Cannot stringify arbitrary non-POJOs (data.tasks[0]) ``` To fix this, you can use `repo(Entity).toJson()` in the server load function and `repo(Entity).fromJson()` in the .svelte file to serialize and deserialize well the entity. ::: code-group ```ts [src/routes/+page.server.ts] import { repo } from 'remult' import type { PageServerLoad } from './$types' import { Task } from '../demo/todo/Task' export const load = (async () => { const tasks = repo(Task).toJson(await repo(Task).find()) return { tasks, } }) satisfies PageServerLoad ``` ```svelte [src/routes/+page.svelte] ``` ::: --- #### Since `@sveltejs/kit@2.11.0`, there is a new feature: [Universal-hooks-transport](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) With this new feature, you can get rid of `repo(Entity).toJson()` and `repo(Entity).fromJson()` thanks to this file: `hooks.ts`. ::: code-group ```ts [src/hooks.ts] import { repo, type ClassType } from 'remult' import { Task } from './demo/todo/Task' import type { Transport } from '@sveltejs/kit' import { api } from './server/api' // You can have: // A/ a local entity array to work only these ones (like here) // or // B/ import a global entity array that will be // shared between backend and frontend (not in ./server/api.ts) const entities = [Task] export const transport: Transport = { remultTransport: { encode: (value: any) => { for (let index = 0; index < entities.length; index++) { const element = entities[index] as ClassType if (value instanceof element) { return { ...repo(element).toJson(value), entity_key: repo(element).metadata.key, } } } }, decode: (value: any) => { for (let index = 0; index < entities.length; index++) { const element = entities[index] as ClassType if (value.entity_key === repo(element).metadata.key) { return repo(element).fromJson(value) } } }, }, } ``` ```ts [src/routes/+page.server.ts] import { repo } from 'remult' import type { PageServerLoad } from './$types' import { Task } from '../demo/todo/Task' export const load = (async () => { // const tasks = repo(Task).toJson(await repo(Task).find()) // [!code --] const tasks = await repo(Task).find() return { tasks, } }) satisfies PageServerLoad ``` ```svelte [src/routes/+page.svelte] ``` ::: ## Extra - Svelte 5 & Reactivity Remult is fully compatible with Svelte 5, Rune, and Reactivity. To take full advantage of it, add this snippet: ::: code-group ```html [src/routes/+layout.svelte] ``` ::: Then you can use `$state`, `$derived` like any other places ::: code-group ```html [src/routes/+page.svelte] ``` ::: # Framework - Nuxt ### Create a Nuxt Project To create a new Nuxt project, run the following command: ```sh npx nuxi init remult-nuxt-todo cd remult-nuxt-todo ``` ### Install Remult Install Remult in your Nuxt project by running the following command: ```sh npm install remult ``` ### Enable TypeScript Decorators To enable the use of TypeScript decorators in your Nuxt project, modify the `nuxt.config.ts` file as follows: ```ts [nuxt.config.ts] // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: '2024-04-03', devtools: { enabled: true }, nitro: { esbuild: { options: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }, }, vite: { esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }, }) ``` ### Bootstrap Remult 1. **Create the API File** In the `server/api/` directory, create a dynamic API route that integrates Remult with Nuxt. The following code sets up the API and defines the entities to be used: ```ts [server/api/[...remult].ts] import { remultNuxt } from 'remult/remult-nuxt' import { Task } from '../../demo/todo/Task.js' export const api = remultNuxt({ admin: true, entities: [Task], }) export default defineEventHandler(api) ``` This setup uses the Remult `Task` entity and registers the API routes dynamically for the entities within the app. ### Run the App To start the development server, run: ```sh npm run dev ``` The Nuxt app will now be running on the default address [http://localhost:3000](http://localhost:3000). ### Setup Completed Your Nuxt app with Remult is now set up and ready to go. You can now move on to defining your entities and building your task list app. # Framework - SolidStart # SolidStart ### Step 1: Create a New SolidStart Project Run the following command to initialize a new SolidStart project: ```sh npm init solid@latest remult-solid-start ``` Answer the prompts as follows: ```sh o Is this a Solid-Start project? Yes o Which template would you like to use? basic o Use TypeScript? Yes ``` Once completed, navigate to the project directory: ```sh cd remult-solid-start ``` ### Step 2: Install Remult To install the Remult package, run: ```sh npm i remult ``` ### Step 3: Bootstrap Remult in the Backend Remult is integrated into `SolidStart` using a [catch-all dynamic API route](https://start.solidjs.com/core-concepts/routing#catch-all-routes), which passes API requests to a handler created using the `remultSolidStart` function. 1. **Create the Remult API Configuration File** In the `src` directory, create a file named `api.ts` with the following code: ```ts // src/api.ts import { remultSolidStart } from 'remult/remult-solid-start' export const api = remultSolidStart({}) ``` 2. **Set Up the Catch-All API Route** In the `src/routes/api/` directory, create a file named `[...remult].ts` with the following code: ```ts // src/routes/api/[...remult].ts import { api } from '../../api.js' export const { POST, PUT, DELETE, GET } = api ``` ### Step 4: Enable TypeScript Decorators 1. **Install Babel Plugins for Decorators**: ```sh npm i -D @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties ``` 2. **Configure Babel Plugins in SolidStart**: Add the following configuration to the `app.config.ts` file to enable TypeScript decorators: ```ts{6-14} // app.config.ts import { defineConfig } from "@solidjs/start/config" export default defineConfig({ //@ts-ignore solid: { babel: { plugins: [ ["@babel/plugin-proposal-decorators", { version: "legacy" }], ["@babel/plugin-transform-class-properties"], ], }, }, }) ``` ### Setup Complete Your SolidStart project is now set up with Remult and ready to run. You can now proceed to the next steps of building your application. # Stacks - Server # Select a server
# Server - Express # Express ### Install Required Packages To set up your Express server with Remult, run the following commands to install the necessary packages: ```sh npm install express remult npm install --save-dev @types/express tsx ``` ### Bootstrap Remult in the Backend Remult is integrated into your backend as an `Express middleware`. 1. **Create the API File** Create a new `api.ts` file in the `src/server/` folder with the following code to set up the Remult middleware: ```ts // src/server/api.ts import { remultExpress } from 'remult/remult-express' export const api = remultExpress() ``` 2. **Register the Middleware** Update the `index.ts` file in your `src/server/` folder to include the Remult middleware. Add the following lines: ```ts{4,7} // src/server/index.ts import express from "express" import { api } from "./api.js" const app = express() app.use(api) app.listen(3002, () => console.log("Server started")) ``` ::: warning ESM Configuration In this tutorial, we are using ECMAScript modules (`esm`) for the Node.js server. This means that when importing files, you must include the `.js` suffix (as shown in the `import { api } from "./api.js"` statement). Additionally, make sure to set `"type": "module"` in your `package.json` file. ::: #### Create the Server's TypeScript Configuration In the root folder, create a TypeScript configuration file named `tsconfig.server.json` to manage the server's settings: ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` This configuration enables TypeScript decorators, ensures compatibility with ECMAScript modules, and specifies the file paths for the server and shared code. #### Create an `npm` Script to Start the API Server To simplify the development process, add a new script in your `package.json` file to start the Express server in development mode: ```json // package.json "dev-node": "tsx watch --env-file=.env --tsconfig tsconfig.server.json src/server" ``` - `tsx`: A TypeScript Node.js execution environment that watches for file changes and automatically restarts the server on each save. - `--env-file=.env`: Ensures environment variables are loaded from the `.env` file. - `--tsconfig tsconfig.server.json`: Specifies the TypeScript configuration file for the server. #### Start the Node Server Finally, open a new terminal and run the following command to start the development server: ```sh npm run dev-node ``` The server will now run on port 3002. `tsx` will watch for any file changes, automatically restarting the server whenever updates are made. # Server - Fastify # Fastify ### Install Required Packages To set up your Fastify server with Remult, run the following commands to install the necessary packages: ```sh npm install fastify remult npm install --save-dev tsx ``` ### Bootstrap Remult in the Backend Remult is integrated into your backend as Fastify middleware. 1. **Create the API File** Create a new `api.ts` file in the `src/server/` folder with the following code to set up the Remult middleware for Fastify: ```ts // src/server/api.ts import { remultFastify } from 'remult/remult-fastify' export const api = remultFastify() ``` 2. **Register the Middleware** Update the `index.ts` file in your `src/server/` folder to include the Remult middleware. Add the following lines: ```ts{5,9} // src/server/index.ts import fastify from "fastify" import { api } from "./api.js" const app = Fastify(); app.register(api); app.listen({ port: 3002 }, () => console.log("Server started")) ``` ::: warning ESM Configuration Similar to the Express setup, when using ECMAScript modules (`esm`) in Fastify, you must include the `.js` suffix when importing files (as shown in the `import { api } from "./api.js"` statement). Also, ensure that `"type": "module"` is set in your `package.json`. ::: #### Create the Server's TypeScript Configuration In the root folder, create a TypeScript configuration file named `tsconfig.server.json` for the server project: ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` This configuration enables TypeScript decorators, ensures compatibility with ECMAScript modules, and specifies the file paths for the server and shared code. #### Create an `npm` Script to Start the API Server To simplify the development process, add a new script in your `package.json` to start the Fastify server in development mode: ```json // package.json "dev-node": "tsx watch --env-file=.env --tsconfig tsconfig.server.json src/server" ``` - `tsx`: A TypeScript Node.js execution environment that watches for file changes and automatically restarts the server on each save. - `--env-file=.env`: Ensures environment variables are loaded from the `.env` file. - `--tsconfig tsconfig.server.json`: Specifies the TypeScript configuration file for the server. #### Start the Fastify Server Open a new terminal and run the following command to start the development server: ```sh npm run dev-node ``` The server will now run on port 3002. `tsx` will watch for any file changes, automatically restarting the Fastify server whenever updates are made. # Server - Hono # Hono ### Install Required Packages To set up your Hono server with Remult, install the necessary packages: ```sh npm install hono remult npm install --save-dev tsx ``` ### Bootstrap Remult in the Backend Remult is integrated into your backend using the `remultHono` adapter for Hono. 1. **Create the API File** Create a new `api.ts` file in the `src/server/` folder with the following code to set up the Remult middleware for Hono: ```ts // src/server/api.ts import { remultHono } from 'remult/remult-hono' export const api = remultHono() ``` 2. **Register the Middleware** Update the `index.ts` file in your `src/server/` folder to include the Remult middleware. Add the following code: ```ts{5,7-8} // src/server/index.ts import { Hono } from 'hono' import { serve } from '@hono/node-server' import { api } from './api.js' const app = new Hono() app.route('', api) serve(app,{ port:3002 }) ``` ::: warning ESM Configuration When using ECMAScript modules (`esm`) in Hono, ensure you include the `.js` suffix when importing files, as shown in the `import { api } from './api.js'` statement. Also, make sure that `"type": "module"` is set in your `package.json`. ::: #### Create the Server's TypeScript Configuration In the root folder, create a TypeScript configuration file named `tsconfig.server.json` for the Hono server: ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` This configuration enables TypeScript decorators, ensures compatibility with ECMAScript modules, and specifies the file paths for the server and shared code. #### Create an `npm` Script to Start the API Server Add a new script in your `package.json` to start the Hono server in development mode: ```json // package.json "dev-node": "tsx watch --env-file=.env --tsconfig tsconfig.server.json src/server" ``` - `tsx`: A TypeScript execution environment that watches for file changes and automatically restarts the server on each save. - `--env-file=.env`: Ensures environment variables are loaded from the `.env` file. - `--tsconfig tsconfig.server.json`: Specifies the TypeScript configuration file for the server. #### Start the Hono Server Open a new terminal and run the following command to start the development server: ```sh npm run dev-node ``` The server will now run on port 3002. `tsx` will watch for file changes, automatically restarting the Hono server whenever updates are made. # Server - Hapi # Hapi ### Install Required Packages To set up your Hapi server with Remult, install the necessary packages: ```sh npm install @hapi/hapi remult npm install --save-dev tsx ``` ### Bootstrap Remult in the Backend Remult is integrated into your backend as a Hapi plugin. 1. **Create the API File** Create a new `api.ts` file in the `src/server/` folder with the following code to set up the Remult middleware for Hapi: ```ts // src/server/api.ts import { remultHapi } from 'remult/remult-hapi' export const api = remultHapi() ``` 2. **Register the Middleware** Update the `index.ts` file in your `src/server/` folder to include the Remult middleware. Add the following code: ```ts{5-7,10} // src/server/index.ts import { server } from '@hapi/hapi' import { api } from './api.js' const hapi = server({ port: 3002 }) await hapi.register(api) hapi.start().then(() => console.log("Server started")) ``` ::: warning ESM Configuration When using ECMAScript modules (`esm`) in Hapi, ensure you include the `.js` suffix when importing files, as shown in the `import { api } from './api.js'` statement. Also, make sure that `"type": "module"` is set in your `package.json`. ::: #### Create the Server's TypeScript Configuration In the root folder, create a TypeScript configuration file named `tsconfig.server.json` for the Hapi server: ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` This configuration enables TypeScript decorators, ensures compatibility with ECMAScript modules, and specifies the file paths for the server and shared code. #### Create an `npm` Script to Start the API Server Add a new script in your `package.json` to start the Hapi server in development mode: ```json // package.json "dev-node": "tsx watch --env-file=.env --tsconfig tsconfig.server.json src/server" ``` - `tsx`: A TypeScript execution environment that watches for file changes and automatically restarts the server on each save. - `--env-file=.env`: Ensures environment variables are loaded from the `.env` file. - `--tsconfig tsconfig.server.json`: Specifies the TypeScript configuration file for the server. #### Start the Hapi Server Open a new terminal and run the following command to start the development server: ```sh npm run dev-node ``` The server will now run on port 3002. `tsx` will watch for file changes, automatically restarting the Hapi server whenever updates are made. # Server - Koa # Koa ### Install Required Packages To set up your Koa server with Remult, run the following commands to install the necessary packages: ```sh npm install koa koa-bodyparser remult npm install --save-dev @types/koa @types/koa-bodyparser tsx ``` ### Bootstrap Remult in the Backend Remult is integrated into your backend as middleware for Koa. 1. **Create the API File** Create a new `api.ts` file in the `src/server/` folder with the following code to set up the Remult middleware: ```ts title="src/server/api.ts" // src/server/api.ts import { createRemultServer } from 'remult/server' export const api = createRemultServer() ``` 2. **Register the Middleware** Update the `index.ts` file in your `src/server/` folder to include the Remult middleware. Add the following lines: ```ts title="src/server/index.ts" add={3,5,9} // src/server/index.ts import * as koa from 'koa' import * as bodyParser from 'koa-bodyparser' import { api } from './api.js' const app = new koa() app.use(bodyParser()) // Enables JSON body parsing for API requests app.use(async (ctx, next) => { const r = await api.handle(ctx.request) // Handle API requests with Remult if (r) { ctx.response.body = r.data ctx.response.status = r.statusCode } else { await next() // If not handled by Remult, pass on to the next middleware } }) app.listen(3002, () => { console.log('Server started on port 3002') }) ``` ::: warning ESM Configuration In this tutorial, we are using ECMAScript modules (`esm`) for the Node.js server. When importing files, you must include the `.js` suffix (as shown in the `import { api } from "./api.js"` statement). Additionally, make sure to set `"type": "module"` in your `package.json` file. ::: #### Create the Server's TypeScript Configuration In the root folder, create a TypeScript configuration file named `tsconfig.server.json` to manage the server's settings: ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` This configuration enables TypeScript decorators, ensures compatibility with ECMAScript modules, and specifies the file paths for the server and shared code. #### Create an `npm` Script to Start the API Server To simplify the development process, add a new script in your `package.json` file to start the Koa server in development mode: ```json // package.json "dev-node": "tsx watch --env-file=.env --tsconfig tsconfig.server.json src/server" ``` - `tsx`: A TypeScript Node.js execution environment that watches for file changes and automatically restarts the server on each save. - `--env-file=.env`: Ensures environment variables are loaded from the `.env` file. - `--tsconfig tsconfig.server.json`: Specifies the TypeScript configuration file for the server. #### Start the Koa Server Finally, open a new terminal and run the following command to start the development server: ```sh npm run dev-node ``` The server will now run on port 3002. `tsx` will watch for any file changes, automatically restarting the server whenever updates are made. # Server - nest # Nest.js ### Bootstrap Remult in the Nest.js back-end 1. Create a `main.ts` file in the `src/` folder with the following code: ```ts title="src/main.ts" // src/main.ts import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { remultExpress } from 'remult/remult-express' async function bootstrap() { const app = await NestFactory.create(AppModule) app.use(remultExpress()) // Integrate Remult as middleware await app.listen(3002) // Start server on port 3002 } bootstrap() ``` 2. Add a simple `AppModule` in `src/app.module.ts`: ```ts title="src/app.module.ts" // src/app.module.ts import { Module } from '@nestjs/common' @Module({}) export class AppModule {} ``` ### Run the Nest.js server Run the server with: ```sh npm run start ``` Your Nest.js app with Remult is now up and running on port `3002`. # Stacks - Database # Choose a Database By default, if no database provider is specified, Remult will use a simple JSON file-based database. This will store your data in JSON files located in the `db` folder at the root of your project.
# Database - Json files ## JSON Files You can store data in JSON files using Remult. Here's how to configure your server: ### Step 1: Configure the `dataProvider` In your `index.ts` (or server file), configure the `dataProvider` to use JSON files as the storage mechanism: ```ts{5-6,12-14} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { JsonDataProvider } from "remult" import { JsonEntityFileStorage } from "remult/server" const app = express() app.use( remultExpress({ dataProvider: async () => new JsonDataProvider(new JsonEntityFileStorage("./db")) // Data will be stored in the 'db' folder }) ) ``` ### Explanation: - **`JsonDataProvider`**: This is the data provider that will store your data in JSON format. - **`JsonEntityFileStorage`**: Specifies the directory where the JSON files will be stored (in this case, `./db`). - **`"./db"`**: The path where JSON files for entities will be created. Ensure the folder exists or it will be created automatically. This configuration allows you to store and manage your application data in JSON files, ideal for small projects or quick setups. # Database - PostgreSQL # PostgreSQL To set up PostgreSQL as the database provider for your Remult application, you'll need to configure the `dataProvider` property in the `api.ts` file. ### Step 1: Install the `node-postgres` package Run the following command to install the necessary PostgreSQL client for Node.js: ```sh npm i pg ``` ### Step 2: Set the `dataProvider` Property In the `api.ts` file, configure the `dataProvider` property to connect to your PostgreSQL database: ```ts{3,7,11-15} import express from "express" import { remultExpress } from "remult/remult-express" import { createPostgresDataProvider } from "remult/postgres" const app = express() const connectionString = "postgres://user:password@host:5432/database" app.use( remultExpress({ dataProvider: createPostgresDataProvider({ connectionString, // default: process.env["DATABASE_URL"] // configuration: {} // optional: a `pg.PoolConfig` object or "heroku" }) }) ) ``` ### Alternative: Use an Existing PostgreSQL Connection If you already have a PostgreSQL connection set up, you can pass it directly to Remult: ```ts import { Pool } from 'pg' import { SqlDatabase } from 'remult' import { PostgresDataProvider } from 'remult/postgres' import { remultExpress } from 'remult/remult-express' const pg = new Pool({ connectionString: 'your-connection-string-here', }) const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase(new PostgresDataProvider(pg)), }), ) ``` In this example, the `pg.Pool` is used to create the PostgreSQL connection, and `SqlDatabase` is used to interface with the `PostgresDataProvider`. # Database - MySQL # MySQL ### Step 1: Install `knex` and `mysql2` Run the following command to install the required packages: ```sh npm i knex mysql2 ``` ### Step 2: Set the `dataProvider` Property In your `api.ts` file, configure the `dataProvider` to connect to your MySQL database using `Knex`: ```ts{3,9-18} import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ client: "mysql2", // Specify the MySQL client connection: { user: "your_database_user", password: "your_database_password", host: "127.0.0.1", database: "test", }, }), }) ) ``` ### Alternative: Use an Existing Knex Provider If you're already using a `knex` instance in your project, you can pass it directly to Remult: ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: 'mysql2', connection: { user: 'your_database_user', password: 'your_database_password', host: '127.0.0.1', database: 'test', }, }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // Use the existing knex instance }), ) ``` # Database - MongoDB ## MongoDB To use MongoDB as the database provider for your Remult application, follow the steps below. ### Step 1: Install MongoDB Driver Run the following command to install the `mongodb` package: ```sh npm i mongodb ``` ### Step 2: Set the `dataProvider` Property In your `api.ts` or server file, configure the `dataProvider` to connect to your MongoDB database: ```ts{3-4,10-14} import express from "express" import { remultExpress } from "remult/remult-express" import { MongoClient } from "mongodb" import { MongoDataProvider } from "remult/remult-mongo" const app = express() app.use( remultExpress({ dataProvider: async () => { const client = new MongoClient("mongodb://localhost:27017/local") await client.connect() return new MongoDataProvider(client.db("test"), client) } }) ) ``` This setup connects to a MongoDB instance running on `localhost` and uses the `test` database. The `MongoDataProvider` manages the connection, allowing Remult to interact with MongoDB seamlessly. # Database - SQLite3 Here’s the polished version of the **sqlite3** setup: ### SQLite3 Setup This version of **SQLite3** works well even on platforms like StackBlitz. ### Step 1: Install SQLite3 Run the following command to install the `sqlite3` package: ```sh npm i sqlite3 ``` ### Step 2: Configure the `dataProvider` In your `api.ts` or server file, configure the `dataProvider` to connect to the SQLite database using **sqlite3**: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' import sqlite3 from 'sqlite3' import { Sqlite3DataProvider } from 'remult/remult-sqlite3' const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( new Sqlite3DataProvider(new sqlite3.Database('./mydb.sqlite')), ), }), ) ``` This configuration connects to an SQLite database stored in the `mydb.sqlite` file. The `Sqlite3DataProvider` is wrapped inside the `SqlDatabase` class, enabling Remult to work with SQLite databases smoothly across different environments, including StackBlitz. # Database - Better SQLite3 Here’s the polished version of the **Better-sqlite3** setup: ### Better-sqlite3 To use **Better-sqlite3** as the database provider for your Remult application, follow these steps: ### Step 1: Install Better-sqlite3 Run the following command to install the `better-sqlite3` package: ```sh npm i better-sqlite3 ``` ### Step 2: Configure the `dataProvider` In your `api.ts` or server file, configure the `dataProvider` to connect to the SQLite database using **Better-sqlite3**: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' import Database from 'better-sqlite3' import { BetterSqlite3DataProvider } from 'remult/remult-better-sqlite3' const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( new BetterSqlite3DataProvider(new Database('./mydb.sqlite')), ), }), ) ``` This setup connects to an SQLite database stored in the `mydb.sqlite` file. The `BetterSqlite3DataProvider` is wrapped inside the `SqlDatabase` class to allow Remult to interact with SQLite efficiently. # Database - MSSQL # Microsoft SQL Server ### Step 1: Install Required Packages Install `knex` and `tedious` to enable Microsoft SQL Server integration. ```sh npm i knex tedious ``` ### Step 2: Configure the `dataProvider` In your `index.ts` (or server file), configure the `dataProvider` to use Microsoft SQL Server with the following `knex` client configuration: ```ts{5,11-25} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ // Knex client configuration for MSSQL client: "mssql", connection: { server: "127.0.0.1", // SQL Server address database: "test", // Your database name user: "your_database_user", // SQL Server user password: "your_database_password", // Password for the SQL Server user options: { enableArithAbort: true, // Required option for newer versions of MSSQL encrypt: false, // Set to true if using Azure instanceName: "sqlexpress", // Optional: Define the SQL Server instance name }, }, }), }) ) ``` ### Step 3: Use an Existing `knex` Provider (Optional) If you have an existing `knex` instance, you can easily integrate it with Remult like this: ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: 'mssql', // Specify MSSQL as the client connection: { // Add your MSSQL connection details here server: '127.0.0.1', user: 'your_database_user', password: 'your_database_password', database: 'test', }, }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // Use your existing knex instance }), ) ``` ### Explanation: - **`tedious`**: The underlying driver used by `knex` to connect to SQL Server. - **`client: "mssql"`**: Specifies that we are using Microsoft SQL Server. - **`createKnexDataProvider`**: Allows you to use `knex` to connect to SQL Server as the data provider for Remult. - **`options`**: The additional configuration for SQL Server, including `enableArithAbort` and `encrypt`. This setup lets you easily connect Remult to Microsoft SQL Server using `knex` for query building and `tedious` as the driver. # Database - Bun SQLite ### Bun:SQLite ### Step 1: Configure the `dataProvider` In your `api.ts` or server file, configure the `dataProvider` to use `bun:sqlite` as follows: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' import { Database } from 'bun:sqlite' import { BunSqliteDataProvider } from 'remult/remult-bun-sqlite' const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( new BunSqliteDataProvider(new Database('./mydb.sqlite')), ), }), ) ``` ### Explanation: - **bun:sqlite**: This uses Bun's native SQLite database, `bun:sqlite`, to manage SQLite databases efficiently in a Bun-based environment. - **BunSqliteDataProvider**: The `BunSqliteDataProvider` integrates the Bun SQLite database as a data provider for Remult. - **SqlDatabase**: Wraps the `BunSqliteDataProvider` to make it compatible with Remult's SQL-based data provider system. This setup allows you to use Bun's SQLite implementation as the database provider for your Remult application, leveraging Bun’s performance benefits with SQLite. # Database - sqljs ### sql.js ### Step 1: Install sql.js Run the following command to install the `sql.js` package: ```sh npm i sql.js ``` ### Step 2: Configure the `dataProvider` In your `api.ts` or server file, configure the `dataProvider` to use `sql.js`: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' import initSqlJs from 'sql.js' import { SqlJsDataProvider } from 'remult/remult-sql-js' const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( new SqlJsDataProvider(initSqlJs().then((SQL) => new SQL.Database())), ), }), ) ``` ### Explanation: - **sql.js**: This setup initializes an in-memory SQLite database using `sql.js`, a library that runs SQLite in the browser or in Node.js. - **SqlJsDataProvider**: The `SqlJsDataProvider` is used to integrate the `sql.js` database as a Remult data provider. - **Async Initialization**: The `initSqlJs()` function initializes the SQL.js engine and sets up the database instance. This configuration allows you to use an in-memory SQLite database in your Remult application, powered by `sql.js`. # Database - Turso Here’s the polished version of the **Turso** setup: ### Turso Setup ### Step 1: Install Turso Client Run the following command to install the `@libsql/client` package: ```sh npm install @libsql/client ``` ### Step 2: Configure the `dataProvider` In your `api.ts` or server file, configure the `dataProvider` to connect to Turso using the Turso client: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' import { createClient } from '@libsql/client' import { TursoDataProvider } from 'remult/remult-turso' const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( new TursoDataProvider( createClient({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN, }), ), ), }), ) ``` ### Explanation: - **Turso Client**: This configuration uses the `@libsql/client` package to connect to the Turso database. - **Environment Variables**: Ensure you have `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` defined in your environment to securely pass the database connection URL and authentication token. - **SqlDatabase**: The `TursoDataProvider` is wrapped with the `SqlDatabase` class, allowing seamless integration of Turso as a Remult data provider. This setup allows you to use Turso as the backend database for your application. # Database - DuckDb ## DuckDB To use DuckDB as the database provider in your Remult-based application, follow these steps: ### Step 1: Install DuckDB Run the following command to install `duckdb`: ```sh npm i duckdb ``` ### Step 2: Configure the `dataProvider` In your `index.ts` (or server file), configure the `dataProvider` to use DuckDB: ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' import { SqlDatabase } from 'remult' // [!code highlight] import { Database } from 'duckdb' // [!code highlight] import { DuckDBDataProvider } from 'remult/remult-duckdb' // [!code highlight] const app = express() app.use( remultExpress({ dataProvider: new SqlDatabase( // [!code highlight] new DuckDBDataProvider(new Database(':memory:')), // [!code highlight] ), // [!code highlight] }), ) app.listen(3000, () => console.log('Server is running on port 3000')) ``` ### Explanation: - **DuckDB setup**: The database is initialized with `new Database(':memory:')` to create an in-memory database. Replace `':memory:'` with a file path if you want to persist the database to disk. - **SqlDatabase**: `SqlDatabase` is used to connect Remult with DuckDB through the `DuckDBDataProvider`. This setup allows you to use DuckDB as your database provider in a Remult project. # Database - Oracle ## Oracle Database To use an Oracle database as the data provider for your Remult-based application, follow these steps: ### Step 1: Install Required Packages Install `knex` and `oracledb`: ```sh npm i knex oracledb ``` ### Step 2: Configure the `dataProvider` In your `index.ts` (or server file), configure the `dataProvider` to use Oracle through `knex`: ```ts{5,11-19} // index.ts import express from "express" import { remultExpress } from "remult/remult-express" import { createKnexDataProvider } from "remult/remult-knex" const app = express() app.use( remultExpress({ dataProvider: createKnexDataProvider({ // Knex client configuration for Oracle client: "oracledb", connection: { user: "your_database_user", password: "your_database_password", connectString: "SERVER" // Specify your Oracle server connection string } }) }) ) app.listen(3000, () => console.log("Server is running on port 3000")) ``` ### Step 3: Using an Existing `knex` Provider If you're already using a `knex` instance, you can easily plug it into Remult: ```ts import express from 'express' import { KnexDataProvider } from 'remult/remult-knex' import { remultExpress } from 'remult/remult-express' import knex from 'knex' const knexDb = knex({ client: 'oracledb', connection: { user: 'your_database_user', password: 'your_database_password', connectString: 'SERVER', }, }) const app = express() app.use( remultExpress({ dataProvider: new KnexDataProvider(knexDb), // Reuse your existing knex provider }), ) app.listen(3000, () => console.log('Server is running on port 3000')) ``` ### Explanation: - **Knex configuration**: `client: "oracledb"` indicates you're using Oracle, and `connection` contains the necessary credentials and connection string. - **Existing knex provider**: If you already have a `knex` instance, it can be reused directly with Remult. This setup integrates Oracle into your Remult-based application. # Server-side Code - Backend Methods # Backend Methods Backend methods run on the backend and are used to improve performance, execute server-only code (e.g., sending emails), or perform operations not accessible through the API. ## Static Backend Methods Static backend methods represent the most straightforward type, transmitting their parameters to the backend and delivering their outcome to the frontend. 1. **Define the Backend Method:** ```typescript import { BackendMethod, remult } from 'remult' import { Task } from './Task' export class TasksController { /** * Sets the completion status of all tasks. * @param {boolean} completed - The completion status to set for all tasks. */ @BackendMethod({ allowed: true }) static async setAll(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` Each controller can house one or more backend methods, each serving distinct purposes tailored to your application's needs. In the provided example, the `TasksController` class contains a single backend method named `setAll`, responsible for setting the completion status of all tasks. The method name, such as `setAll`, serves as the URL for the corresponding REST endpoint on the backend server. It's worth noting that you can configure a prefix for these endpoints using the `apiPrefix` option, providing flexibility in structuring your backend API routes. The allowed: true parameter signifies that the backend method can be invoked by anyone. Alternatively, you can customize the authorization settings for finer control over who can access the method. For instance, setting allow: Allow.authenticated restricts access to authenticated users only, ensuring that only logged-in users can utilize the method. Similarly, specifying allow: 'admin' limits access to users with administrative privileges, granting access exclusively to administrators. These options offer granular control over authorization, allowing you to tailor access permissions based on your application's specific requirements and security considerations. 2. **Register the Controller:** ```typescript // Register TasksController in the controllers array of the remultExpress options export const api = remultExpress({ entities: [Task], controllers: [TasksController], }) ``` 3. **Call from the Frontend:** ```typescript await TasksController.setAll(true) ``` This example demonstrates how to define and use a static backend method, `setAll`, within the `TasksController` class. When called from the frontend, this method sets the completion status of all tasks to the specified value (`true` in this case). The method leverages Remult's `BackendMethod` decorator to handle the communication between the frontend and backend seamlessly. # Server-side Code - Server-only Dependencies # Backend only code One of the main advantages of remult is that you write code once, and it runs both on the server and in the browser. However, if you are using a library that only works on the server, the fact that the same code is bundled to the frontend can cause problems. For example, when you build an Angular project, you'll get `Module not found` errors. This article will walk through such a scenario and how it can be solved. For this example, our customer would like us to document each call to the `updatePriceOnBackend` method in a log file. Our first instinct would be to add in the `products.controller.ts` file an import to `fs` (Node JS file system component) and write the following code: ```ts{1,10} import * as fs from 'fs'; ..... @BackendMethod({allowed:true}) static async updatePriceOnBackend(priceToUpdate:number,remult?:Remult){ let products = await remult.repo(Products).find(); for (const p of products) { p.price.value += priceToUpdate; await p.save(); } fs.appendFileSync('./logs/log.txt', new Date() + " " + remult.user.name + " update price\n"); } ``` ::: danger Error As soon as we do that, we'll get the following errors on the `ng-serve` terminal ```sh ERROR in ./src/app/products/products.controller.ts Module not found: Error: Can't resolve 'fs' in 'C:\try\test19\my-project\src\app\products' i 「wdm」: Failed to compile. ``` ::: We get this error because the `fs` module on which we rely here is only relevant in the remult of a `Node JS` server and not in the context of the browser. There are two ways to handle this: ## Solution 1 - exclude from bundler ::: tabs == vite ### Exclude in `vite.config` Instruct vite to exclude the `server-only` packages from the bundle ```ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], build: { // [!code ++] rollupOptions: { // [!code ++] external: ['fs', 'nodemailer', 'node-fetch'], // [!code ++] }, // [!code ++] }, // [!code ++] optimizeDeps: { // [!code ++] exclude: ['fs', 'nodemailer', 'node-fetch'], // [!code ++] }, // [!code ++] }) ``` == Webpack and Angular version <=16 Instruct `webpack` not to include the `fs` package in the `frontend` bundle by adding the following JSON to the main section of the project's `package.json` file. _package.json_ ```json "browser": { "jsonwebtoken": false } ``` - note that you'll need to restart the react/angular dev server. == Angular 17 1. You'll need to either remove `types` entry in the `tsconfig.app.json` or add the types you need to that types array. 2. In `angular.json` you'll need to add an entry called `externalDependencies` to the `architect/build/options` key for your project ```json{21-23} // angular.json { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "remult-angular-todo": { "projectType": "application", "schematics": {}, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/remult-angular-todo", "index": "src/index.html", "browser": "src/main.ts", "externalDependencies": [ "fs" ], "polyfills": [ "zone.js" ], //... ``` ::: ## Solution 2 - abstract the call Abstract the call and separate it to backend only files and `inject` it only when we are running on the server. **Step 1**, abstract the call - We'll remove the import to `fs,` and instead of calling specific `fs` methods, we'll define and call a method `writeToLog` that describes what we are trying to do: ```ts import * as fs from 'fs'; // [!code --] // We'll define an abstract `writeTiLog` function and use it in our code static writeToLog:(textToWrite:string)=>void; // [!code ++] ..... @BackendMethod({allowed:true}) static async updatePriceOnBackend(priceToUpdate:number,remult?:Remult){ let products = await remult.repo(Products).find(); for (const p of products) { p.price.value += priceToUpdate; await p.save(); } fs.appendFileSync('./logs/log.txt', new Date() + " " + remult.user.name + " update price\n"); // [!code --] ProductsController.writeToLog(new Date() + " " + remult.user.name + " update price\n"); // [!code ++] } ``` The method `writeToLog` that we've defined serves as a place holder which we'll assign to in the remult of the server. It receives one parameter of type `string` and returns `void`. **Step 2**, implement the method: In the `/src/app/server` folder, we'll add a file called `log-writer.ts` with the following code: ```ts{3} import * as fs from 'fs'; import { ProductsController } from '../products/products.controller'; ProductsController.writeToLog = what => fs.appendFileSync('./logs/log.txt', what); ``` Here we set the implementation of the `writeToLog` method with the actual call to the `fs` module. This file is intended to only run on the server, so it'll not present us with any problem. **Step 3**, load the `log-writer.ts` file: In the `/src/app/server/server-init.ts` file, load the `log-writer.ts` file using an `import` statement ```ts{2} import '../app.module'; import './log-writer'; //load the log-writer.ts file import { Pool } from 'pg'; import { config } from 'dotenv'; import { PostgresDataProvider, PostgresSchemaBuilder } from '@remult/server-postgres'; import * as passwordHash from 'password-hash'; ``` That's it - it'll work now. ::: tip If you're still getting an error - check that you have a `logs` folder on your project :) ::: ## Additional Resources Check out this video where I implemented a similar solution when running into the same problem using `bcrypt`: # Guides - Access Control # Access Control ::: tip **Interactive Learning Available! 🚀** Looking to get hands-on with this topic? Try out our new [**interactive tutorial**](https://learn.remult.dev/in-depth/7-access-control/1-field-level-control) on Access Control, where you can explore and practice directly in the browser. This guided experience offers step-by-step lessons to help you master Access Control in Remult with practical examples and exercises. [Click here to dive into the interactive tutorial on Access Control!](https://learn.remult.dev/in-depth/7-access-control/1-field-level-control) ::: Access control is essential for ensuring that users can only access resources they are authorized to in web applications. This article explores the various layers of access control, focusing on a framework that provides a granular approach to securing your application. ## Entity-Level Authorization Entity-level authorization governs CRUD (Create, Read, Update, Delete) operations at the entity level. Each entity can define permissions for these operations using the following options: - `allowApiRead`: Controls read access. - `allowApiInsert`: Controls insert access. - `allowApiUpdate`: Controls update access. - `allowApiDelete`: Controls delete access. Each option can be set to a boolean, a string role, an array of string roles, or an arrow function: ```typescript // Allows all CRUD operations @Entity("tasks", { allowApiCrud: true }) // Only users with the 'admin' role can update @Entity("tasks", { allowApiUpdate: 'admin' }) // Only users with 'admin' or 'manager' roles can delete @Entity("tasks", { allowApiDelete: ['admin', 'manager'] }) // Only the user 'Jane' can read @Entity("tasks", { allowApiRead: () => remult.user?.name == 'Jane' }) // Only authenticated users can perform CRUD operations @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ## Row-Level Authorization Row-level authorization allows control over which rows a user can access or modify. ### Authorization on Specific Rows The `allowApiUpdate`, `allowApiDelete`, and `allowApiInsert` options can also accept a function that receives the specific item as the first parameter, allowing row-level authorization: ```ts // Users can only update tasks they own @Entity("tasks", { allowApiUpdate: task => task.owner == remult.user?.id }) ``` ### Filtering Accessible Rows To limit the rows a user has access to, use the `apiPrefilter` option: ```ts @Entity("tasks", { apiPrefilter: () => { // Admins can access all rows if (remult.isAllowed("admin")) return {} // Non-admins can only access rows where they are the owner return { owner: remult.user!.id } } }) ``` The `apiPrefilter` adds a filter to all CRUD API requests, ensuring that only authorized data is accessible through the API. ### Preprocessing Filters for API Requests For more complex scenarios, you can use `apiPreprocessFilter` to dynamically modify the filter based on the specific request and additional filter information: ```ts @Entity("tasks", { apiPreprocessFilter: async (filter, {getPreciseValues}) => { // Ensure that users can only query tasks for specific customers const preciseValues = await getPreciseValues(); if (!preciseValues.customerId) { throw new ForbiddenError("You must specify a valid customerId filter"); } return filter; } }) ``` In this example, `apiPreprocessFilter` uses the `getPreciseValues` method to ensure that users must specify a valid `customerId` filter when querying tasks, allowing for more granular control over the data that is accessible through the API. **Note:** The `preciseValues` object includes the actual values that are used in the filter. For example, in the code sample above, if the `customerId` filter specifies the values `'1'`, `'2'`, and `'3'`, then `preciseValues.customerId` will be an array containing these values. This allows you to check and enforce specific filter criteria in your preprocessing logic. This added note explains the significance of the `preciseValues` property and how it includes the actual values used in the filter, providing an example for clarity. ### Warning: API Filters Do Not Affect Backend Queries It's important to note that `apiPrefilter` and `apiPreprocessFilter` only apply to API requests. They do not affect backend queries, such as those executed through backend methods or non-Remult routes. For instance, in a sign-in scenario, a backend method might need to check all user records to verify a user's existence without exposing all user data through the API. Once authenticated, the user should only have access to their own record for updates. ### Backend Filters for Consistent Access Control To apply similar filtering logic to backend queries, you can use `backendPrefilter` and `backendPreprocessFilter`: ```ts @Entity("tasks", { backendPrefilter: () => { // Admins can access all rows if (remult.isAllowed("admin")) return {} // Non-admins can only access rows where they are the owner return { owner: remult.user!.id } }, backendPreprocessFilter: async (filter, {getPreciseValues}) => { // Apply additional filtering logic for backend queries const preciseValues = await getPreciseValues(filter); if (!preciseValues.owner) { throw new ForbiddenError("You must specify a valid owner filter"); } return filter; } }) ``` In this example, `backendPrefilter` and `backendPreprocessFilter` ensure that non-admin users can only access their own tasks in backend queries, providing consistent access control across both API and backend operations. ## Field-Level Authorization Field-level authorization allows control over individual fields within an entity: - `includeInApi`: Determines if the field is included in the API response. - `allowApiUpdate`: Controls if a field can be updated. If false, any change to the field is ignored. Examples: ```ts // This field will not be included in the API response @Fields.string({ includeInApi: false }) password = "" // Only users with the 'admin' role can update this field @Fields.boolean({ allowApiUpdate: "admin" }) admin = false // Titles can only be updated by the task owner @Fields.string({ allowApiUpdate: task => task.owner === remult.user!.id }) title='' // This field can only be updated when creating a new entity @Fields.string({ allowApiUpdate: (c) => getEntityRef(c).isNew() }) Description = "" ``` ### Field Masking To mask a field, combine a non-API field with a `serverExpression` that returns the masked value: ```ts // This field is not included in the API response @Fields.string({ includeInApi: false }) password = "" // The field value is masked in the API response @Fields.string({ serverExpression: () => "***", // Update the real password field when the masked field is changed saving: async (user, fieldRef, e) => { if (fieldRef.valueChanged()) { user.password = await User.hash(user.updatePassword) } }, }) updatePassword = "" ``` ## BackendMethod Authorization Backend methods use the `allowed` option to determine authorization: ```ts // Only authenticated users can execute this method @BackendMethod({ allowed: Allow.authenticated }) static async doSomething() { // something } ``` The `allowed` option can receive a boolean, a string role, an array of role strings, or a function. ## Reusing Access Control Definitions in the Frontend Access control definitions set in entities can be reused as a single source of truth in the frontend. This allows for consistent and centralized management of access control logic across your application. For example, in a React component, you can conditionally render UI elements based on the access control rules defined in the entity: ::: code-group ```tsx [React] function UserComponent({ user }: { user: User }) { //... return ( {user.name} {/* Only show the admin field if the user is allowed to see it */} {userRepo.fields.admin.includeInApi(user) && {user.admin}} {/* Only show the delete button if the user is allowed to delete the admin */} {userRepo.metadata.apiDeleteAllowed(user) && ( )} ) } ``` ```html [Angular] {{user.name}} {{user.admin}} ``` ```vue [Vue] {{user.name}} {{user.admin}} ``` ```svelte [Svelte] {{user.name}} {#if userRepo.fields.admin.includeInApi(user)} {{user.admin}} {/if} {#if userRepo.metadata.apiDeleteAllowed(user)} {/if} ``` ::: ## Additional Resources Check out this informative [YouTube video](https://www.youtube.com/watch?v=9lWQwAUcKEM). It discusses the concepts covered in this article and provides practical examples to help you understand how to implement robust access control in your applications. --- This article provides a comprehensive overview of the layers of access control in web applications, offering a granular approach to securing your application at the entity, row, field, and method levels. # Guides - Admin UI # Admin UI Enjoy a fully featured Admin UI for your entities, you can do CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ## Enabling the Admin UI Add the Admin UI to your application by setting the `admin` option to `true` in the remult configuration. ```ts export const api = remultSveltekit({ entities: [], admin: true, // Enable the Admin UI }) ``` ## Accessing and Using the Admin UI Navigate to `/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ## Features - **Entity List**: On the left side of the screen you have the entity list, you can use the search field to search for entities. - **Entity Details**: Clicking on an entity in the menu will open the entity details screen (in the middle), here you can view filter & paginate your data _(top right)_. You can also see all relations of entity by clicking on the arrow on the left of each row. The last column is dedicated for actions where you can edit or delete an entity. On top left you can also add a new entity by clicking on the `+`. - **Entity Diagram**: Clicking on the Diagram entry will open the entity diagram screen, here you can see the entity relationships. ![Remult Admin Diagram](/remult-admin-diagram.png) - **Settings**: On top left, you have a menu _(remult logo)_ where you can find various settings for your admin ui. - You want to confirm a delete all the time? - You want to display Captions or Keys? - Multiple options for automatic diagram layout (you want also do your own layout) - You don't use cookies? No problem, you can set your bearer token (it will only be in session) ## Demo in video Watch this quick demo to see the Remult Admin UI in action: This video showcases the key features and functionality of the Remult Admin UI, giving you a practical overview of how it can streamline your entity management process. # Escape Hatches - Custom/SQL Filters # Leveraging Custom Filters for Enhanced Data Filtering In modern web applications, efficiently filtering data is essential for providing a seamless user experience. Whether it's an e-commerce platform filtering products, a task management system sorting tasks, or any other application that requires data manipulation, the ability to apply complex filters is crucial. Custom filters offer a powerful solution, enabling developers to create reusable, declarative, and versatile filters that are executed on the backend and easily utilized from the frontend. This article delves into the concept of custom filters, illustrating their advantages and practical applications. ## The Advantages of Custom Filters Custom filters provide several benefits that make them an attractive choice for handling data filtering in web applications: 1. **Declarative and Readable:** Custom filters allow you to express filtering logic in a clear, declarative manner. This improves code readability and maintainability, making it easier to understand and modify filtering criteria. 2. **Reusability:** By encapsulating filtering logic in custom filters, you can reuse the same filters across different parts of your application, reducing code duplication and ensuring consistency in filtering behavior. 3. **Backend Execution:** Custom filters are evaluated on the backend, leveraging the full capabilities of the underlying database or data provider. This enables more efficient data processing and allows you to perform complex operations that would be difficult or impossible to handle on the frontend. 4. **Composability:** Custom filters can be combined with other filters, both custom and standard, allowing you to build complex filtering logic in a modular and maintainable way. 5. **Flexibility with Data Providers:** Custom filters can be used with various data providers, including SQL databases, in-memory JSON arrays, and others. This flexibility allows you to apply custom filters in different contexts and with different data storage solutions. 6. **Enhanced Security:** When using custom filters with parameterized queries or data provider-specific filtering methods, you can mitigate the risk of injection attacks and ensure that user input is properly sanitized. ## Practical Example: Filtering Orders in an E-Commerce Application Consider an e-commerce application where you need to filter orders based on their status and creation year. Without custom filters, the filtering logic might be repetitive and scattered throughout the codebase. By using custom filters, you can encapsulate this logic in a reusable component, simplifying the code and making it more maintainable. In the following sections, we'll explore how to implement custom filters in this scenario, demonstrating their advantages and how they can be used to create more efficient and readable code. ## The Problem with Repetitive Filtering Consider a scenario where you have an `Order` entity, and you frequently need to filter orders that are considered "active" based on their status and creation year. Without custom filters, your code might look something like this: ```ts await repo(Order).find({ where: { status: ['created', 'confirmed', 'pending', 'blocked', 'delayed'], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, }, }) ``` This code is not only repetitive but also clutters your application, making it harder to maintain. Moreover, it generates lengthy REST API calls, such as: ``` /api/orders?status.in=%5B%22created%22%2C%22confirmed%22%2C%22pending%22%2C%22blocked%22%2C%22delayed%22%5D&createdAt.gte=2023-12-31T22%3A00%3A00.000Z&createdAt.lt=2024-12-31T22%3A00%3A00.000Z ``` ## Introducing Custom Filters Custom filters allow you to refactor your filtering logic into a reusable and declarative component. Here's how you can define a custom filter for active orders: ```ts class Order { //... static activeOrdersFor = Filter.createCustom( async ({ year }) => { return { status: ['created', 'confirmed', 'pending', 'blocked', 'delayed'], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, } }, ) } ``` - **First Generic Parameter (`Order`):** This parameter specifies the entity class that the filter is associated with. In this case, it's the `Order` class. This is important because it ensures that the filter criteria you define are compatible with the fields and types of the `Order` entity. - **Second Generic Parameter (`{ year: number }`):** This parameter defines the type of the argument that the filter will receive when executed. In this example, the filter expects an object with a single property `year` of type `number`. This allows you to pass dynamic values to the filter when you use it in a query, making the filter more flexible and reusable. - **Callback Function (`async ({ year }) => { ... }`):** This function is where you define the actual filtering criteria. It receives an argument matching the type specified in the second generic parameter. Inside the function, you return an object representing the filter conditions. In this case, the conditions are based on the `status` and `createdAt` fields of the `Order` entity. Now, you can use this custom filter in your queries: ```ts await repo(Order).find({ where: Order.activeOrders({ year }), }) ``` This generates a much simpler REST API call: ``` /api/orders?%24custom%24activeOrders=%7B%22year%22%3A2024%7D ``` ## Composability of Custom Filters One of the key advantages of custom filters is their ability to be composed with other filters. This means you can combine custom filters with regular filters or even other custom filters to build complex filtering logic. Let's take a closer look at the example you provided: ```ts await repo(Order).find({ where: { customerId: '123', $and: [Order.activeOrders({ year })], }, }) ``` In this query, we're filtering orders based on two criteria: 1. The `customerId` should be "123". 2. The order should satisfy the conditions defined in the `activeOrders` custom filter for the specified year. By using the `$and` operator, we're able to combine the custom filter with a regular filter. This demonstrates the composability of custom filters, allowing you to build more complex and nuanced filtering logic while maintaining readability and reusability. ### More on Composability The power of composability doesn't stop there. You can also combine multiple custom filters to create even more specific filters. For example, suppose you have another custom filter called `highValueOrders` that filters orders based on their total value: ```ts class Order { //... static highValueOrders = Filter.createCustom(() => { return { totalValue: { $gt: 1000 }, } }) } ``` You can then combine this with the `activeOrders` filter to find high-value active orders for a specific year: ```ts await repo(Order).find({ where: { $and: [Order.activeOrders({ year }), Order.highValueOrders()], }, }) ``` This ability to compose filters allows you to create modular and reusable filtering logic, which can significantly improve the maintainability and clarity of your code. ### Evaluating Custom Filters on the Backend One of the significant advantages of custom filters is that they are evaluated on the backend. This allows you to perform complex data-related operations that would be inefficient or impossible to do solely on the frontend. For instance, you can leverage database queries or other server-side logic to build your filtering criteria. Let's examine the example you provided: ```ts static activeOrders = Filter.createCustom< Order, { year: number; customerCity: string } >(async ({ year, customerCity }) => { const customers = await repo(Customer).find({ where: { city: customerCity }, }) return { customerId: { $in: customers.map((c) => c.id) }, status: ["created", "confirmed", "pending", "blocked", "delayed"], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, } }) ``` In this example, the custom filter `activeOrders` now takes an additional parameter `customerCity`. The filter performs a database query to fetch all customers from the specified city. It then uses the IDs of these customers to filter orders that belong to them. This is combined with the existing criteria of filtering orders based on their status and creation year. ::: tip Key Points - **Backend Evaluation:** The filter is evaluated on the backend, where it has access to the database and can perform efficient queries. This offloads complex data processing from the frontend to the backend, where it can be handled more effectively. - **Complex Filtering:** By leveraging backend capabilities, you can create filters that involve complex operations, such as fetching related data from other tables or entities (in this case, fetching customers based on their city). - **Asynchronous Operations:** Notice the use of `async` in the filter definition. This allows you to perform asynchronous operations, such as database queries, within your custom filter. ::: ## Leveraging Database Capabilities with Raw SQL in Custom Filters Since custom filters are **evaluated on the backend**, you have the opportunity to harness the raw capabilities of the underlying database. This can be particularly useful when you need to perform complex operations that are more efficiently handled by the database itself. For instance, you can use raw SQL queries to improve the performance or functionality of your custom filters. Let's modify the `activeOrders` custom filter to use a raw SQL query for filtering orders based on the customer's city: ```ts static activeOrders = Filter.createCustom< Order, { year: number; customerCity: string } >(async ({ year, customerCity }) => { return { status: ["created", "confirmed", "pending", "blocked", "delayed"], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, $and: [ SqlDatabase.rawFilter(({param}) => // [!code highlight] `"customerId" in (select id from customers where city = ${param(customerCity)})` // [!code highlight] ), // [!code highlight] ], } }) ``` In this example, we've added a `$and` condition that uses `SqlDatabase.rawFilter` to include a raw SQL fragment in our filter. This SQL fragment selects the IDs of customers from the specified city and uses them to filter the orders. This generates the following sql: ```sql select "id", "status", "customerId", "createdAt" from "orders" where "status" in ($2, $3, $4, $5, $6) and "createdAt" >= $7 and "createdAt" < $8 and ("customerId" in (select id from customers where city = $9)) Order By "id" ``` #### Important Notes - **Parameterized Queries:** It's crucial to use parameterized queries (e.g., `builder.param(customerCity)`) when incorporating user-supplied values into your SQL queries. This helps prevent SQL injection attacks by ensuring that user input is properly escaped. - **Performance Considerations:** Leveraging raw SQL can lead to significant performance improvements, especially for complex queries. However, it's important to ensure that your SQL queries are well-optimized to avoid potential performance issues. #### Usage Example Using the custom filter remains straightforward: ```ts await repo(Order).find({ where: Order.activeOrders({ year: 2024, customerCity: 'New York' }), }) ``` ### Using `dbNamesOf` with Table Names and Aliases The `dbNamesOf` utility function can be customized to include the table name in the SQL queries. This is particularly useful for ensuring consistency between your entity definitions and your raw SQL queries. Here's an updated example of the `activeOrders` custom filter using `dbNamesOf` with table names and aliases: ```ts static activeOrders = Filter.createCustom< Order, { year: number; customerCity: string } >(async ({ year, customerCity }) => { const order = await dbNamesOf(Order, { // [!code highlight] tableName: true, // [!code highlight] }) const customer = await dbNamesOf(Customer, { // [!code highlight] tableName: "c", // [!code highlight] }) // [!code highlight] return { status: ["created", "confirmed", "pending", "blocked", "delayed"], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, $and: [ SqlDatabase.rawFilter(({param}) => // [!code highlight] `${order.customerId} in (select ${customer.id} from ${customer} as c // [!code highlight] where ${customer.city} = ${param(customerCity)})` // [!code highlight] ), ], } }) ``` In this example: - The `Order` table is referenced with its full name. - The `Customer` table is aliased as `"c"`, and this alias is used in the SQL query. ### Explanation of `tableName` and Aliases - **`tableName: true`:** By setting `tableName: true`, you indicate that you want to include the table name when referring to fields, resulting in SQL expressions like `"customer"."id"`. - **Aliases:** You can use aliases for table names, which is particularly useful in complex join scenarios. For example, setting `tableName: "c"` would use the alias `"c"` for the table name in the SQL query. ### Resulting SQL Query Using the `activeOrders` custom filter with the enhancements mentioned above would generate the following SQL query: ```sql select "id", "status", "customerId", "createdAt" from "orders" where "status" in ($2, $3, $4, $5, $6) and "createdAt" >= $7 and "createdAt" < $8 and ("orders"."customerId" in (select "c"."id" from "customers" as c where c."city" = $9)) Order By "id" ``` In this SQL query, the `Customer` table is aliased as `"c"`, and this alias is used throughout the query to ensure consistency with the entity definitions and to handle complex join scenarios effectively. ### SQL-Based Custom Filters: Unleashing the Power of Composability The greatest advantage of using SQL-based custom filters lies in their composability and the ability to handle complex situations. By breaking down filtering logic into smaller, atomic custom filters, developers can compose these filters to create more sophisticated and nuanced filtering criteria. This modular approach not only enhances the readability and maintainability of the code but also allows for greater flexibility in constructing complex queries. For instance, consider a scenario where you need to filter orders based on multiple criteria, such as status, creation year, customer location, and order value. By creating separate custom filters for each of these criteria, you can easily combine them to form a comprehensive filtering solution. This composability ensures that your filtering logic can adapt to various requirements without becoming convoluted or difficult to manage. Furthermore, the ability to handle complex situations is a significant advantage of SQL-based custom filters. By leveraging the raw power of SQL, you can perform advanced operations such as subqueries, joins, and aggregate functions directly within your filters. This opens up a wide range of possibilities for data analysis and manipulation, enabling you to tackle complex filtering scenarios with ease. SQL is a language that is widely recognized and understood by AI technologies such as ChatGPT, Copilot and others. This makes it possible to generate highly optimized queries with ease. These AI technologies can assist in writing SQL queries, ensuring they are efficient and effective. This is particularly beneficial when dealing with complex data structures and large datasets, where writing optimal queries can be challenging. With the assistance of AI, developers can focus more on the logic of their applications, while the AI handles the intricacies of SQL query optimization. In summary, the composability of SQL-based custom filters, coupled with their ability to handle complex situations, makes them an invaluable tool for developers seeking to create flexible, efficient, and powerful data filtering solutions in their web applications. ### Using Raw Filters with Different Data Providers Custom filters with raw filters are not limited to SQL databases. You can also use raw filters with other data providers, such as Knex or an in-memory JSON data provider. This flexibility allows you to leverage the power of raw filters in various contexts, depending on your application's needs. #### Knex Example Knex is a popular SQL query builder for Node.js. You can use Knex with custom filters to define complex filtering logic directly using the Knex query builder syntax. ```typescript static idBetween = Filter.createCustom( ({ from, to }) => { return KnexDataProvider.rawFilter(({ knexQueryBuilder }) => { knexQueryBuilder.andWhereBetween('id', [from, to]); }); } ); ``` In this example, the `idBetween` custom filter uses Knex to filter `Task` entities whose `id` falls between the specified `from` and `to` values. #### JSON Example For applications that use an in-memory JSON data provider, you can define custom filters that operate directly on the JSON data. ```typescript static titleLengthFilter = Filter.createCustom( ({ minLength }) => { return ArrayEntityDataProvider.rawFilter((item) => { return item.title?.length > minLength; }); } ); ``` In this example, the `titleLengthFilter` custom filter filters `Task` entities based on the length of their `title` property, ensuring that it exceeds the specified `minLength`. ## Conclusion Custom filters represent a powerful tool in the arsenal of web developers, offering a flexible and efficient way to handle data filtering in web applications. By encapsulating filtering logic into reusable components, custom filters not only enhance code readability and maintainability but also enable the execution of complex filtering operations on the backend. This leads to improved performance and security, as well as the ability to compose intricate filtering criteria with ease. The versatility of custom filters extends to their compatibility with various data providers, from SQL databases to in-memory JSON arrays, allowing developers to leverage the most suitable data handling mechanisms for their specific use cases. Moreover, the declarative nature of custom filters ensures that the filtering logic remains clear and concise, facilitating easier debugging and future modifications. In conclusion, adopting custom filters in your web development projects can significantly streamline the process of data filtering, resulting in cleaner, more efficient, and more secure code. By embracing this approach, developers can focus on delivering a seamless user experience, confident in the knowledge that their data filtering logic is both robust and adaptable. # Escape Hatches - Direct Database Access # Accessing the Underlying Database in Remult While Remult provides a powerful abstraction for working with databases, there might be scenarios where you need to access the underlying database directly. This could be for performing complex queries, optimizations, or other database-specific operations that are not covered by Remult's API. :::warning Directly executing custom SQL can be dangerous and prone to SQL injection attacks. Always use parameterized queries and the `param` method provided by Remult to safely include user input in your queries. ::: ## Accessing SQL Databases For SQL-based databases, Remult provides the SqlDatabase class to interact directly with the database and allows you to run raw SQL queries directly. This is useful for executing complex queries that involve operations like GROUP BY, bulk updates, and other advanced SQL features. ### Basic SQL Query ```typescript const sql = SqlDatabase.getDb() const result = await sql.execute('SELECT COUNT(*) AS count FROM tasks') console.log(result.rows[0].count) ``` This approach is straightforward but can lead to inconsistencies if the database schema changes. #### the `dbNamesOf` function: The `dbNamesOf` function dynamically retrieves the database table and column names based on your entity definitions, ensuring that your queries stay in sync with your data model. This enhances consistency, maintainability, and searchability in your code. ```typescript const tasks = await dbNamesOf(Task) const sql = SqlDatabase.getDb() const result = await sql.execute(`SELECT COUNT(*) AS count FROM ${tasks}`) console.log(result.rows[0].count) ``` ##### Create index example ```typescript const tasks = await dbNamesOf(Task) const sql = SqlDatabase.getDb() await sql.execute(`CREATE INDEX idx_task_title ON ${tasks}(${tasks.title});`) ``` ### Using Bound Parameters The `param` method safely incorporates user input into the query, reducing the risk of SQL injection by using parameterized queries. ```typescript const priceToUpdate = 5 const products = await dbNamesOf(Product) const sql = SqlDatabase.getDb() let command = sql.createCommand() await command.execute( `UPDATE ${products} SET ${products.price} = ${ products.price } + ${command.param(priceToUpdate)}`, ) ``` When executed, this code will run the following SQL: ```sql UPDATE products SET price = price + $1 Arguments: { '$1': 5 } ``` ### Leveraging EntityFilter for SQL Databases The `filterToRaw` function converts Remult's `EntityFilter` objects into SQL where clauses, enabling you to incorporate complex filtering logic defined in your models into custom SQL queries. This allows for reusability and integration with backend filters. #### Benefits of filterToRaw - **Reusability**: Allows you to reuse complex filters defined in your Remult models in custom SQL queries. - **Integration**: Respects any **backendPrefilter** and **backendPreprocessFilter** applied to your entities, ensuring consistent access control and data manipulation rules. ```typescript const order = await dbNamesOf(Order) const sql = SqlDatabase.getDb() const command = sql.createCommand() const filterSql = await SqlDatabase.filterToRaw( Order, { status: ['created', 'confirmed', 'pending', 'blocked', 'delayed'], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, }, command, ) const result = await command.execute( `SELECT COUNT(*) FROM ${order} WHERE ${filterSql}`, ) console.log(result.rows[0].count) ``` Resulting SQL: ```sql SELECT COUNT(*) FROM "orders" WHERE "status" IN ($1, $2, $3, $4, $5) AND "createdAt" >= $6 AND "createdAt" < $7 ``` Using `customFilter`: ```typescript const order = await dbNamesOf(Order) const sql = SqlDatabase.getDb() const command = sql.createCommand() const filterSql = await SqlDatabase.filterToRaw( Order, Order.activeOrders({ year, customerCity: 'London' }), command, ) const result = await command.execute( `SELECT COUNT(*) FROM ${order} WHERE ${filterSql}`, ) console.log(result.rows[0].count) ``` Resulting SQL: ```sql SELECT COUNT(*) FROM "orders" WHERE "status" IN ($1, $2, $3, $4, $5) AND "createdAt" >= $6 AND "createdAt" < $7 AND ("orders"."customerId" IN ( SELECT "customers"."id" FROM "customers" WHERE "customers"."city" = $8 )) ``` ## Accessing Other Databases ## Knex ```typescript const tasks = await dbNamesOf(Task) const knex = KnexDataProvider.getDb() const result = await knex(tasks.$entityName).count() console.log(result[0].count) ``` ### Leveraging EntityFilter for Knex ```ts const tasks = await dbNamesOf(Task) const knex = KnexDataProvider.getDb() const r = await knex(tasks.$entityName) .count() .where(await KnexDataProvider.filterToRaw(Task, { id: [1, 3] })) console.log(r[0].count) ``` ## MongoDB ```ts const tasks = await dbNamesOf(Task) const mongo = MongoDataProvider.getDb() const r = await(await mongo.collection(tasks.$entityName)).countDocuments() console.log(r) ``` ### Leveraging EntityFilter for MongoDb ```ts const tasks = await dbNamesOf(Task) const mongo = MongoDataProvider.getDb() const r = await(await mongo.collection(tasks.$entityName)).countDocuments( await MongoDataProvider.filterToRaw(Task, { id: [1, 2] }), ) console.log(r) ``` ## Native postgres ```ts const tasks = await dbNamesOf(Task) const sql = PostgresDataProvider.getDb() const r = await sql.query(`select count(*) as c from ${tasks}`) console.log(r.rows[0].c) ``` ## Conclusion Accessing the underlying database directly in Remult provides the flexibility to handle complex use cases that might not be covered by the ORM layer. However, it's important to use this capability judiciously and securely, especially when dealing with user input, to avoid potential security vulnerabilities like SQL injection. By leveraging utilities like `dbNamesOf` and `filterToRaw # Escape Hatches - Using Remult in Non-Remult Routes --- keywords: [ Error: remult object was requested outside of a valid context, try running it within initApi or a remult request cycle, ] --- # Using Remult in Non-Remult Routes When using the CRUD api or [BackendMethods](./backendMethods.md), `remult` is automatically available. Still, there are many use cases where you may want to user remult in your own routes or other code without using `BackendMethods` but would still want to take advantage of `Remult` as an ORM and use it to check for user validity, etc... If you tried to use the `remult` object, you may have got the error: ## Error: remult object was requested outside of a valid context, try running it within initApi or a remult request cycle Here's how you can use remult in this context, according to the server you're using: ::: tabs == Express ### withRemult middleware You can use remult as an express middleware for a specific route, using `api.withRemult` ```ts{1} app.post('/api/customSetAll', api.withRemult, async (req, res) => { // .... }) ``` Or as an express middleware for multiple routes ```ts app.use(api.withRemult) // [!code highlight] app.post('/api/customSetAll', async (req, res) => { // .... }) ``` ### withRemultAsync promise wrapper Use the `api.withRemultAsync` method in promises ```ts import express from 'express' import { remultExpress } from 'remult/remult-express' const app = express(); ... const api = remultExpress({ entities:[Task] }) app.post('/api/customSetAll', async (req, res) => { // use remult in a specific piece of code // [!code highlight] await api.withRemultAsync(req, async ()=> { // [!code highlight] if (!remult.authenticated()) { res.sendStatus(403); return; } if (!remult.isAllowed("admin")) { res.sendStatus(403); return; } const taskRepo = remult.repo(Task); for (const task of await taskRepo.find()) { task.completed = req.body.completed; await taskRepo.save(task); } res.send(); }) }); ``` You can also use it without sending the request object, for non request related code ```ts{2} setInterval(async () => { api.withRemultAsync(undefined, async () => { // .... }) }, 10000) ``` == Fastify ```ts import fastify from 'fastify' import { remultFastify } from 'remult/remult-fastify' (async () => { const server = fastify() await server.register(remultFastify({})) // [!code highlight] server.get('/api/test', async (req, res) => { return { result: await api.withRemult(req, () => remult.repo(Task).count()), // [!code highlight] } }) server.listen({ port: 3000 }) })() ``` == Hono ```ts import { Hono } from 'hono' import { remultHono } from 'remult/remult-hono' const app = new Hono() const api = remultHono({}) app.get('/test1', api.withRemult, async (c) => // [!code highlight] c.text('hello ' + (await repo(Task).count())), ) app.route('', api) export default app ``` == Next.js app router ```ts // src/app/api/test/route.ts import { NextResponse } from 'next/server' import { repo } from 'remult' import { Task } from '../../../shared/task' import { api } from '../../../api' export const dynamic = 'force-dynamic' export async function GET(req: Request) { return api.withRemult(async () => { return NextResponse.json({ result: repo(Task).count(), user: remult.user, }) }) } ``` == Sveltekit You can use the `withRemult` method in specific routes ```ts // src/routes/api/test/+server.ts import { json, type RequestHandler } from '@sveltejs/kit' import { remult } from 'remult' import { Task } from '../../../shared/Task' import { api } from '../../../server/api' export const GET: RequestHandler = async (event) => { return api.withRemult(event, async () => json({ result: await remult.repo(Task).count() }), ) } ``` You can also define the withRemult as a hook, to make remult available throughout the application ```ts // src/hooks.server.ts import type { Handle } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { api as handleRemult } from './server/api' export const handle = sequence( // Handle remult server side handleRemult, ) ``` == SolidStart You can use the `withRemult` method in specific routes ```ts // src/routes/api/test.ts import { remult } from 'remult' import { Task } from '../../../shared/Task' import { api } from '../../../server/api' export function GET() { return api.withRemult(event, async () => ({ result: await remult.repo(Task).count() }), ) } ``` You can also use the same method for any "use server" function ```ts export function getCount(){ return api.withRemult(event, async () => ({ result: await remult.repo(Task).count() }), ) } ``` You can also define the withRemult as a hook, to make remult available throughout the application ```ts // src/hooks.server.ts import type { Handle } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { api as handleRemult } from './server/api' export const handle = sequence( // Handle remult server side handleRemult, ) ``` == Hapi ```ts import { type Plugin, server } from '@hapi/hapi' import { remultHapi } from 'remult/remult-hapi' (async () => { const hapi = server({ port: 3000 }) const api = remultHapi({}) await hapi.register(api) // [!code highlight] server.route({ method: 'GET', path: '/api/test2', handler: async (request, h) => { return api.withRemult(request, async () => { return { result: await remult.repo(Task).count(), } }) }, }) hapi.start() })() ``` ::: # Escape Hatches - Avoiding Decorators # Working without decorators If you prefer to work without decorators, or use `remult` in a javascript project (without typescript) you can use the following: ## Entity ::: code-group ```ts [Typescript] import { Entity, Fields, describeEntity } from 'remult' export class Task { id!: string title = '' completed = false } describeEntity( Task, 'tasks', { allowApiCrud: true, }, { id: Fields.uuid(), title: Fields.string(), completed: Fields.boolean(), }, ) ``` ```js [Javascript] import { Entity, Fields, describeEntity } from 'remult' export class Task { id title = '' completed = false } describeEntity( Task, 'tasks', { allowApiCrud: true, }, { id: Fields.uuid(), title: Fields.string(), completed: Fields.boolean(), }, ) ``` ::: This is the same entity that is detailed in the [Entities section of the tutorial](https://remult.dev/tutorials/react/entities.html) ## Static BackendMethod ```ts{12-14} import { BackendMethod, describeBackendMethods, remult } from "remult"; import { Task } from "./Task"; export class TasksController { static async setAll(completed: boolean) { const taskRepo = remult.repo(Task); for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }); } } } describeBackendMethods(TasksController, { setAll: { allowed: "admin" } }) ``` This is the same backend method that is detailed in the [Backend methods of the tutorial](https://remult.dev/tutorials/react/backend-methods.html#refactor-from-front-end-to-back-end) # Escape Hatches - Extensibility --- tags: - options - bespoke options - customizing options - type augmentation - module augmentation - UserInfo - RemultContext - context --- # Extensibility [Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) in TypeScript allows you to extend existing types with custom properties or methods. This enhances the functionality of third-party libraries like `remult` without altering their source code, enabling seamless integration of custom features while maintaining type safety. In Remult, you can use TypeScript's module augmentation to enhance your application with custom features. Here are some examples: 1. **Add more fields to the User object:** Extend the `UserInfo` interface to include additional fields like `email` and `phone`. 2. **Add custom options/metadata to fields and entities:** Extend the `FieldOptions` or `EntityOptions` interfaces to include custom properties such as `placeholderText` or `helpText`. 3. **Add fields/methods to the `remult.context` object:** Extend the `RemultContext` interface to include additional properties or methods that can be accessed throughout your code. ## Setting Up the types.d.ts File for Custom Type Extensions To set up the `types.d.ts` file for custom type extensions in Remult: 1. **Create a TypeScript Declaration File:** Add a file named `types.d.ts` in the `src` folder of your project. This file will be used to declare custom type extensions, such as additional user info fields. ```ts // src/types.d.ts export {} declare module 'remult' { interface UserInfo { phone: string // [!code highlight] email: string // [!code highlight] } } ``` The `export {}` is required to indicate that this file is a module, as per the [Vue.js documentation on augmenting global properties](https://vuejs.org/guide/typescript/options-api.html#augmenting-global-properties). 2. **Include the Declaration File in tsconfig:** Make sure that the `types.d.ts` file is included in the `include` section of your `tsconfig.json` file. If you have a separate `tsconfig` for the server, ensure that it's also added there. ```json // tsconfig.server.json { "compilerOptions": { //... }, "include": ["src/server/**/*", "src/shared/**/*", "src/types.d.ts"] // [!code highlight] } ``` 3. **Utilize the Custom Fields in Your Code:** Once you've defined custom fields in the `types.d.ts` file and ensured they're included in your `tsconfig.json`, you can start using them throughout your application. For instance, if you've added `phone` and `email` to the `UserInfo` interface, you can access these properties in your code as follows: ```ts // Accessing custom user info fields console.log(remult.user.phone) console.log(remult.user.email) ``` This enables you to seamlessly integrate the new fields into your application's logic and user interface. ## Enhancing Field and Entity Definitions with Custom Options One of the key motivations for adding custom options to `FieldOptions` or `EntityOptions` is to maintain consistency and centralize the definition of entities and fields in your application. By keeping these definitions close to the entity or field, you ensure a single source of truth for your application's data model. This approach enhances maintainability and readability, as all relevant information and metadata about an entity or field are located in one place. Additionally, it allows for easier integration with UI components, as custom options like `placeholderText` can be directly accessed and used in your frontend code. For adding custom options to `FieldOptions` or `EntityOptions`, such as `placeholderText`: 1. **Extend FieldOptions:** In your `types.d.ts` file, extend the `FieldOptions` interface to include your custom options. For example: ```ts declare module 'remult' { interface FieldOptions { placeholderText?: string // [!code highlight] } } export {} ``` 2. **Set Custom Option:** Specify the `placeholderText` in your entity field options: ```ts import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true }) export class Task { @Fields.uuid() id!: string @Fields.string({ placeholderText: 'Please enter a task title', // [!code highlight] }) title = '' @Fields.boolean() completed = false } ``` 3. **Use in UI:** Access the custom option in your UI components: ```html{2} ``` By following these steps, you can extend `FieldOptions` with custom options that can be utilized throughout your project. ### Extending Remult's `context` Property for Request-Specific Information Augmenting Remult's `context` property is particularly useful because it allows you to store and access request-specific information throughout your code. This can be especially handy for including data from the request and utilizing it in entities or backend methods. For example, you can add a custom property `origin` to the `RemultContext` interface: ```ts declare module 'remult' { export interface RemultContext { origin?: string // [!code highlight] } } ``` Then, set the `origin` property in the `initRequest` option in the `api.ts` file: ```ts export const api = remultExpress({ initRequest: async (_, req) => { remult.context.origin = req.headers.origin // [!code highlight] }, entities: [Task], //... }) ``` You can now use the `origin` property anywhere in your code, for example: ```ts @BackendMethod({ allowed: Roles.admin }) static async doSomethingImportant() { console.log(remult.context.origin); // [!code highlight] } ``` or in an entity's saving event: ```ts @Entity("tasks", { saving: task => { task.lastUpdateDate = new Date(); task.lastUpdateUser = remult.user?.name; task.lastUpdateOrigin = remult.context.origin; // [!code highlight] }, //... }); ``` By leveraging module augmentation, you can tailor Remult to your specific needs, adding custom options and extending interfaces to suit your application's requirements. # Integrations - Open API # Adding Swagger and openApi In short, swagger provides a quick UI that describes the api which is exposed by the application. To add swagger to a `remult` application follow these steps: 1. Install the `swagger-ui-express` package: ```sh npm i swagger-ui-express npm i --save-dev @types/swagger-ui-express ``` 2. In the `/src/server/index.ts` file add the following code: ```ts{2,6-9} import express from 'express'; import swaggerUi from 'swagger-ui-express'; import { remultExpress } from 'remult/remult-express'; const app = express(); let api = remultExpress(); app.use(api); const openApiDocument = api.openApiDoc({ title: "remult-react-todo" }); app.get("/api/openApi.json", (req, res) => {res.json(openApiDocument)}); app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument)); app.listen(3002, () => console.log("Server started")); ``` ## Adding Swagger UI to a NextJs App To add swagger UI to a `NextJs` application follow these steps: 1. Install the following packages: ```sh # With npm npm i swagger-ui-react npm i -D @types/swagger-ui-react # With yarn yarn add swagger-ui-react yarn add -D @types/swagger-ui-react ``` 2. Get the openApi document from RemultNextAppServer: ```ts // src/api.ts import { Task } from '@/shared/Task' import { TasksController } from '@/shared/TasksController' import { remultNextApp } from 'remult/remult-next' export const api = remultNextApp({ admin: true, entities: [Task], controllers: [TasksController], }) // Export this here 👇 export const openApiDoc = api.openApiDoc({ title: 'Todo App', }) export const { POST, PUT, DELETE, GET } = api ``` 3. Create a new page to render Swagger UI: ```tsx // src/app/api-doc/page.tsx import { openApiDoc } from '@/api' // 👈 Import the openApiDoc you exported earlier import ReactSwagger from './react-swagger' export default async function IndexPage() { return (
) } ``` ```tsx // src/app/api-doc/react-swagger.tsx 'use client' import SwaggerUI from 'swagger-ui-react' import 'swagger-ui-react/swagger-ui.css' type Props = { spec: Record } function ReactSwagger({ spec }: Props) { return } export default ReactSwagger ``` 4. Navigate to `http://localhost:3000/api-doc` to see the Swagger UI. ![Remult Admin](../public/example_remult-next-swagger-ui-page.png) # Adding open api specific field options Checkout the following example project that demos how to add `openApi` specific options to field options [stackblitz](https://stackblitz.com/github/noam-honig/adding-open-api-options?file=server/build-open-api.ts,shared/task.ts) [github](https://www.github.com/noam-honig/adding-open-api-options) # Integrations - GraphQL # Adding Graphql To add graphql to a `remult` application follow these steps: 1. Install the `graphql-yoga` packages: ```sh npm i graphql-yoga ``` ## Express: In the `/src/server/index.ts` file add the following code: ```ts{3-4,12-22} import express from 'express'; import { remultExpress } from 'remult/remult-express'; import { createSchema, createYoga } from 'graphql-yoga' import { remultGraphql } from 'remult/graphql'; const app = express() const entities = [Task] let api = remultExpress({ entities }); app.use(api); const { typeDefs, resolvers } = remultGraphql({ entities }); const yoga = createYoga({ graphqlEndpoint: '/api/graphql', schema: (createSchema({ typeDefs, resolvers })) }) app.use(yoga.graphqlEndpoint, api.withRemult, yoga) app.listen(3002, () => console.log("Server started")); ``` ## Next App Router ```ts // Next.js Custom Route Handler: https://nextjs.org/docs/app/building-your-application/routing/router-handlers import { createYoga, createSchema } from 'graphql-yoga' import { remultGraphql } from 'remult/graphql' import { api } from '../../../api' import { Task } from '../../../shared/task' const { typeDefs, resolvers } = remultGraphql({ entities: [Task], }) const yoga = createYoga({ // While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint graphqlEndpoint: '/api/graphql', schema: createSchema({ typeDefs, resolvers, }), // Yoga needs to know how to create a valid Next response fetchAPI: { Response }, }) const handleRequest = (request: any, ctx: any) => api.withRemult(() => yoga.handleRequest(request, ctx)) export { handleRequest as GET, handleRequest as POST } ``` ## Svelte `src/routes/api/graphql/+server.ts` ```ts import type { RequestEvent } from '@sveltejs/kit' import { createSchema, createYoga } from 'graphql-yoga' import { remultGraphql } from 'remult/graphql' import { Task } from '../../../shared/Task' const { typeDefs, resolvers } = remultGraphql({ entities: [Task], }) const yogaApp = createYoga({ schema: createSchema({ typeDefs, resolvers, }), // While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint graphqlEndpoint: '/api/graphql', fetchAPI: { Response }, }) export { yogaApp as GET, yogaApp as OPTIONS, yogaApp as POST } ``` # API Reference - Entity # Entity Decorates classes that should be used as entities. Receives a key and an array of EntityOptions. #### example: ```ts import { Entity, Fields } from "remult"; @Entity("tasks", { allowApiCrud: true }) export class Task { @Fields.uuid() id!: string; @Fields.string() title = ''; @Fields.boolean() completed = false; } ``` #### note: EntityOptions can be set in two ways: #### example: ```ts // as an object @Entity("tasks",{ allowApiCrud:true }) ``` #### example: ```ts // as an arrow function that receives `remult` as a parameter @Entity("tasks", (options,remult) => options.allowApiCrud = true) ``` ## caption A human readable name for the entity ## allowApiRead Determines if this Entity is available for get requests using Rest Api #### description: Determines if one has any access to the data of an entity. #### see: - [allowed](http://remult.dev/docs/allowed.html) - to restrict data based on a criteria, use [apiPrefilter](https://remult.dev/docs/ref_entity.html#apiprefilter) ## allowApiUpdate Determines if this entity can be updated through the api. #### see: - [allowed](http://remult.dev/docs/allowed.html) - [Access Control](https://remult.dev/docs/access-control) ## allowApiDelete Determines if entries for this entity can be deleted through the api. #### see: - [allowed](http://remult.dev/docs/allowed.html) - [Access Control](https://remult.dev/docs/access-control) ## allowApiInsert Determines if new entries for this entity can be posted through the api. #### see: - [allowed](http://remult.dev/docs/allowed.html) - [Access Control](https://remult.dev/docs/access-control) ## allowApiCrud sets the `allowApiUpdate`, `allowApiDelete` and `allowApiInsert` properties in a single set ## apiPrefilter An optional filter that determines which rows can be queried using the API. This filter is applied to all CRUD operations to ensure that only authorized data is accessible. Use `apiPrefilter` to restrict data based on user profile or other conditions. #### example: ```ts // Only include non-archived items in API responses apiPrefilter: { archive: false } ``` #### example: ```ts // Allow admins to access all rows, but restrict non-admins to non-archived items apiPrefilter: () => remult.isAllowed("admin") ? {} : { archive: false } ``` #### see: [EntityFilter](https://remult.dev/docs/access-control.html#filtering-accessible-rows) ## apiPreprocessFilter An optional function that allows for preprocessing or modifying the EntityFilter for a specific entity type before it is used in API CRUD operations. This function can be used to enforce additional access control rules or adjust the filter based on the current context or specific request. #### example: ```typescript @Entity("tasks", { apiPreprocessFilter: async (filter, { getPreciseValues }) => { // Ensure that users can only query tasks for specific customers const preciseValues = await getPreciseValues(); if (!preciseValues.customerId) { throw new ForbiddenError("You must specify a valid customerId filter"); } return filter; } }) ``` ## backendPreprocessFilter Similar to apiPreprocessFilter, but for backend operations. ## backendPrefilter A filter that will be used for all queries from this entity both from the API and from within the backend. #### example: ```ts backendPrefilter: { archive:false } ``` #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) ## defaultOrderBy An order by to be used, in case no order by was specified #### example: ```ts defaultOrderBy: { name: "asc" } ``` #### example: ```ts defaultOrderBy: { price: "desc", name: "asc" } ``` ## saving An event that will be fired before the Entity will be saved to the database. If the `error` property of the entity's ref or any of its fields will be set, the save will be aborted and an exception will be thrown. this is the place to run logic that we want to run in any case before an entity is saved. #### example: ```ts @Entity("tasks", { saving: async (task, e) => { if (e.isNew) { task.createdAt = new Date(); // Set the creation date for new tasks. } task.lastUpdated = new Date(); // Update the last updated date. }, }) ``` #### link: LifeCycleEvent object #### see: [Entity Lifecycle Hooks](http://remult.dev/docs/lifecycle-hooks) ## saved A hook that runs after an entity has been successfully saved. #### link: LifeCycleEvent object #### see: [Entity Lifecycle Hooks](http://remult.dev/docs/lifecycle-hooks) ## deleting A hook that runs before an entity is deleted. #### link: LifeCycleEvent object #### see: [Entity Lifecycle Hooks](http://remult.dev/docs/lifecycle-hooks) ## deleted A hook that runs after an entity has been successfully deleted. #### link: LifeCycleEvent object #### see: [Entity Lifecycle Hooks](http://remult.dev/docs/lifecycle-hooks) ## validation A hook that runs to perform validation checks on an entity before saving. This hook is also executed on the frontend. #### link: LifeCycleEvent object #### see: [Entity Lifecycle Hooks](http://remult.dev/docs/lifecycle-hooks) ## dbName The name of the table in the database that holds the data for this entity. If no name is set, the `key` will be used instead. #### example: ```ts dbName:'myProducts' You can also add your schema name to the table name ``` #### example: ```ts dbName:'public."myProducts"' ``` ## sqlExpression For entities that are based on SQL expressions instead of a physical table or view #### example: ```ts @Entity('people', { sqlExpression:`select id,name from employees union all select id,name from contractors`, }) export class Person { @Fields.string() id='' @Fields.string() name='' } ``` ## id An arrow function that identifies the `id` column to use for this entity #### example: ```ts //Single column id @Entity("products", { id: 'productCode' }) ``` #### example: ```ts //Multiple columns id @Entity("orderDetails", { id:['orderId:', 'productCode'] }) ``` ## entityRefInit Arguments: * **ref** * **row** ## apiRequireId * **apiRequireId** # API Reference - Field # Field Decorates fields that should be used as fields. for more info see: [Field Types](https://remult.dev/docs/field-types.html) FieldOptions can be set in two ways: #### example: ```ts // as an object @Fields.string({ includeInApi:false }) title=''; ``` #### example: ```ts // as an arrow function that receives `remult` as a parameter @Fields.string((options,remult) => options.includeInApi = true) title=''; ``` ## valueType The value type for this field ## caption A human readable name for the field. Can be used to achieve a consistent caption for a field throughout the app #### example: ```ts ``` ## allowNull If it can store null in the database ## required If a value is required ## includeInApi Specifies whether this field should be included in the API. This can be configured based on access control levels. #### example: ```ts // Do not include in the API @Fields.string({ includeInApi: false }) password = ''; // Include in the API for 'admin' only @Fields.number({ includeInApi: 'admin' }) salary = 0; ``` #### see: - [allowed](https://remult.dev/docs/allowed.html) - [Access Control](https://remult.dev/docs/access-control) ## allowApiUpdate Determines whether this field can be updated via the API. This setting can also be controlled based on user roles or other access control checks. #### example: ```ts // Prevent API from updating this field @Fields.string({ allowApiUpdate: false }) createdBy = remult.user?.id; ``` #### see: - [allowed](https://remult.dev/docs/allowed.html) - [Access Control](https://remult.dev/docs/access-control) ## validate An arrow function that'll be used to perform validations on it #### example: ```ts @Fields.string({ validate: Validators.required }) * ``` #### example: ```ts @Fields.string({ validate: task=>task.title.length>3 || "Too Short" }) ``` #### example: ```ts @Fields.string({ validate: task=>{ if (task.title.length<3) throw "Too Short"; } }) ``` #### example: ```ts @Fields.string({ validate: (_, fieldValidationEvent)=>{ if (fieldValidationEvent.value.length < 3) fieldValidationEvent.error = "Too Short"; } }) ``` ## saving Will be fired before this field is saved to the server/database ## serverExpression An expression that will determine this fields value on the backend and be provided to the front end ## dbName The name of the column in the database that holds the data for this field. If no name is set, the key will be used instead. #### example: ```ts @Fields.string({ dbName: 'userName'}) userName='' ``` ## sqlExpression Used or fields that are based on an sql expressions, instead of a physical table column #### example: ```ts @Fields.integer({ sqlExpression:e=> 'length(title)' }) titleLength = 0; @Fields.string() title=''; ``` ## dbReadOnly For fields that shouldn't be part of an update or insert statement ## valueConverter The value converter to be used when loading and saving this field ## displayValue an arrow function that translates the value to a display value ## defaultValue an arrow function that determines the default value of the field, when the entity is created using the `repo.create` method ## inputType The html input type for this field ## lazy * **lazy** ## target The entity type to which this field belongs ## key The key to be used for this field # API Reference - ValueConverter # ValueConverter Interface for converting values between different formats, such as in-memory objects, database storage, JSON data transfer objects (DTOs), and HTML input elements. ## fromJson Converts a value from a JSON DTO to the valueType. This method is typically used when receiving data from a REST API call or deserializing a JSON payload. #### returns: The converted value. #### example: ```ts fromJson: val => new Date(val) ``` Arguments: * **val** - The value to convert. ## toJson Converts a value of valueType to a JSON DTO. This method is typically used when sending data to a REST API or serializing an object to a JSON payload. #### returns: The converted value. #### example: ```ts toJson: val => val?.toISOString() ``` Arguments: * **val** - The value to convert. ## fromDb Converts a value from the database format to the valueType. #### returns: The converted value. #### example: ```ts fromDb: val => new Date(val) ``` Arguments: * **val** - The value to convert. ## toDb Converts a value of valueType to the database format. #### returns: The converted value. #### example: ```ts toDb: val => val?.toISOString() ``` Arguments: * **val** - The value to convert. ## toInput Converts a value of valueType to a string suitable for an HTML input element. #### returns: The converted value as a string. #### example: ```ts toInput: (val, inputType) => val?.toISOString().substring(0, 10) ``` Arguments: * **val** - The value to convert. * **inputType** - The type of the input element (optional). ## fromInput Converts a string from an HTML input element to the valueType. #### returns: The converted value. #### example: ```ts fromInput: (val, inputType) => new Date(val) ``` Arguments: * **val** - The value to convert. * **inputType** - The type of the input element (optional). ## displayValue Returns a displayable string representation of a value of valueType. #### returns: The displayable string. #### example: ```ts displayValue: val => val?.toLocaleDateString() ``` Arguments: * **val** - The value to convert. ## fieldTypeInDb Specifies the storage type used in the database for this field. This can be used to explicitly define the data type and precision of the field in the database. #### example: ```ts // Define a field with a specific decimal precision in the database @Fields.number({ valueConverter: { fieldTypeInDb: 'decimal(18,8)' } }) price=0; ``` ## inputType Specifies the type of HTML input element suitable for values of valueType. #### example: ```ts inputType = 'date'; ``` # API Reference - Validation # Validation Validation is a key part of any application, and you will see that it's builtin Remult ! Let's dive into it... First of all, some props brings automatic validation, for example `required` and `minLength` for strings : ```ts @Fields.string({ minLength: 5 }) title = '' ``` You can establish your own validation rules by using the `validate` prop and do any custom code you want : ```ts @Fields.string({ validate: (task)=> task.title.length > 5 || "too short" }) title = '' ``` You want to focus only on the value? ```ts @Fields.string({ validate: valueValidator(value => value.length > 5) }) title = '' ``` The `validate` prop can also use buildin validators like this : ```ts import { Validators } from 'remult' @Fields.string({ validate: Validators.minLength(5) }) title = '' ``` It supports array of validators as well : ```ts import { Validators } from 'remult' @Fields.string({ validate: [ Validators.minLength(5), Validators.maxLength(10), (task)=> task.title.startsWith('No') || "Need to start with No" ] }) title = '' ``` Some validators like `unique` is running on the backend side, and nothing changes, you just have to use it : ```ts import { Validators } from 'remult' @Fields.string({ validate: [ Validators.minLength(5), Validators.unique() ] }) title = '' ``` Also in custom validator you can check if you are in the backend or not : ```ts import { Validators, isBackend } from 'remult' @Fields.string({ validate: [ Validators.unique(), (task) => { if (isBackend()) { // check something else... // throw "a custom message" } } ] }) title = '' ``` If you want to customize the error message, you can do it globally : ```ts Validators.unique.defaultMessage = 'Existe déjà!' ``` # API Reference - Validators # Validators Class containing various field validators. ## constructor * **new Validators** ## defaultMessage * **defaultMessage** ## email Validator to check if a value is a valid email address. ## enum Validator to check if a value exists in a given enum. ## in Validator to check if a value is one of the specified values. ## max Validator to check if a value is less than or equal to a maximum value. ## maxLength Validator to check if a string's length is less than or equal to a maximum length. ## min Validator to check if a value is greater than or equal to a minimum value. ## minLength Validator to check if a string's length is greater than or equal to a minimum length. ## notNull Validator to check if a value is not null. ## range Validator to check if a value is within a specified range. ## regex Validator to check if a value matches a given regular expression. ## relationExists Validator to check if a related value exists in the database. ## required Validator to check if a value is required (not null or empty). ## unique Validator to ensure a value is unique in the database. ## uniqueOnBackend * **uniqueOnBackend** ## url Validator to check if a value is a valid URL. # API Reference - Relations # Relations * **Relations** ## constructor * **new Relations** ## toMany Define a toMany relation between entities, indicating a one-to-many relationship. This method allows you to establish a relationship where one entity can have multiple related entities. #### returns: A decorator function to apply the toMany relation to an entity field. Example usage: ``` @Relations.toMany(() => Order) orders?: Order[]; // or with a custom field name: @Relations.toMany(() => Order, "customerId") orders?: Order[]; ``` Arguments: * **toEntityType** * **fieldInToEntity** - (Optional) The field in the target entity that represents the relation. Use this if you want to specify a custom field name for the relation. ## toOne Define a to-one relation between entities, indicating a one-to-one relationship. If no field or fields are provided, it will automatically create a field in the database to represent the relation. #### returns: A decorator function to apply the to-one relation to an entity field. Example usage: ``` @Relations.toOne(() => Customer) customer?: Customer; ``` ``` Fields.string() customerId?: string; @Relations.toOne(() => Customer, "customerId") customer?: Customer; ``` ``` Fields.string() customerId?: string; @Relations.toOne(() => Customer, { field: "customerId", defaultIncluded: true }) customer?: Customer; ``` ``` Fields.string() customerId?: string; @Relations.toOne(() => Customer, { fields: { customerId: "id", }, }) customer?: Customer; ``` Arguments: * **toEntityType** * **options** - (Optional): An object containing options for configuring the to-one relation. * **caption** - A human readable name for the field. Can be used to achieve a consistent caption for a field throughout the app #### example: ```ts ``` * **fields** - An object specifying custom field names for the relation. Each key represents a field in the related entity, and its value is the corresponding field in the source entity. For example, `{ customerId: 'id' }` maps the 'customerId' field in the related entity to the 'id' field in the source entity. This is useful when you want to define custom field mappings for the relation. * **field** - The name of the field for this relation. * **findOptions** - Find options to apply to the relation when fetching related entities. You can specify a predefined set of find options or provide a function that takes the source entity and returns find options dynamically. These options allow you to customize how related entities are retrieved. * **defaultIncluded** - Determines whether the relation should be included by default when querying the source entity. When set to true, related entities will be automatically included when querying the source entity. If false or not specified, related entities will need to be explicitly included using the `include` option. # API Reference - RelationOptions # RelationOptions Options for configuring a relation between entities. ## caption A human readable name for the field. Can be used to achieve a consistent caption for a field throughout the app #### example: ```ts ``` ## fields An object specifying custom field names for the relation. Each key represents a field in the related entity, and its value is the corresponding field in the source entity. For example, `{ customerId: 'id' }` maps the 'customerId' field in the related entity to the 'id' field in the source entity. This is useful when you want to define custom field mappings for the relation. ## field The name of the field for this relation. ## findOptions Find options to apply to the relation when fetching related entities. You can specify a predefined set of find options or provide a function that takes the source entity and returns find options dynamically. These options allow you to customize how related entities are retrieved. ## defaultIncluded Determines whether the relation should be included by default when querying the source entity. When set to true, related entities will be automatically included when querying the source entity. If false or not specified, related entities will need to be explicitly included using the `include` option. # API Reference - Remult # Remult * **Remult** ## repo Return's a `Repository` of the specific entity type #### example: ```ts const taskRepo = remult.repo(Task); ``` #### see: [Repository](https://remult.dev/docs/ref_repository.html) Arguments: * **entity** - the entity to use * **dataProvider** - an optional alternative data provider to use. Useful for writing to offline storage or an alternative data provider ## user Returns the current user's info ## initUser Fetches user information from the backend and updates the `remult.user` object. Typically used during application initialization and user authentication. #### returns: A promise that resolves to the user's information or `undefined` if unavailable. ## authenticated Checks if a user was authenticated ## isAllowed checks if the user has any of the roles specified in the parameters #### example: ```ts remult.isAllowed("admin") ``` #### see: [Allowed](https://remult.dev/docs/allowed.html) Arguments: * **roles** ## isAllowedForInstance checks if the user matches the allowedForInstance callback #### see: [Allowed](https://remult.dev/docs/allowed.html) Arguments: * **instance** * **allowed** ## useFetch * **useFetch** Arguments: * **fetch** ## dataProvider The current data provider ## constructor Creates a new instance of the `remult` object. Can receive either an HttpProvider or a DataProvider as a parameter - which will be used to fetch data from. If no provider is specified, `fetch` will be used as an http provider Arguments: * **http** ## call Used to call a `backendMethod` using a specific `remult` object #### example: ```ts await remult.call(TasksController.setAll, undefined, true); ``` Arguments: * **backendMethod** - the backend method to call * **classInstance** - the class instance of the backend method, for static backend methods use undefined * **args** - the arguments to send to the backend method ## onFind A helper callback that can be used to debug and trace all find operations. Useful in debugging scenarios Arguments: * **metadata** * **options** * **limit** - Determines the number of rows returned by the request, on the browser the default is 100 rows #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **page** - Determines the page number that will be used to extract the data #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **where** - filters the data #### example: ```ts await taskRepo.find({where: { completed:false }}) ``` #### see: For more usage examples see [EntityFilter](https://remult.dev/docs/entityFilter.html) * **orderBy** - Determines the order of items returned . #### example: ```ts await this.remult.repo(Products).find({ orderBy: { name: "asc" }}) ``` #### example: ```ts await this.remult.repo(Products).find({ orderBy: { price: "desc", name: "asc" }}) ``` ## clearAllCache * **clearAllCache** ## entityRefInit A helper callback that is called whenever an entity is created. ## context context information that can be used to store custom information that will be disposed as part of the `remult` object ## apiClient The api client that will be used by `remult` to perform calls to the `api` ## liveQueryStorage * **liveQueryStorage** ## subscriptionServer * **subscriptionServer** ## liveQueryPublisher * **liveQueryPublisher** ## liveQuerySubscriber * **liveQuerySubscriber** # API Reference - ApiClient # ApiClient Interface for configuring the API client used by Remult to perform HTTP calls to the backend. ## httpClient The HTTP client to use when making API calls. It can be set to a function with the `fetch` signature or an object that has `post`, `put`, `delete`, and `get` methods. This can also be used to inject logic before each HTTP call, such as adding authorization headers. #### example: ```ts // Using Axios remult.apiClient.httpClient = axios; ``` #### example: ```ts // Using Angular HttpClient remult.apiClient.httpClient = httpClient; ``` #### see: If you want to add headers using angular httpClient, see: https://medium.com/angular-shots/shot-3-how-to-add-http-headers-to-every-request-in-angular-fab3d10edc26 #### example: ```ts // Using fetch (default) remult.apiClient.httpClient = fetch; ``` #### example: ```ts // Adding bearer token authorization remult.apiClient.httpClient = ( input: RequestInfo | URL, init?: RequestInit ) => { return fetch(input, { ...init, headers: authToken ? { ...init?.headers, authorization: 'Bearer ' + authToken, } : init?.headers, cache: 'no-store', }) } ``` ## url The base URL for making API calls. By default, it is set to '/api'. It can be modified to be relative or to use a different domain for the server. #### example: ```ts // Relative URL remult.apiClient.url = './api'; ``` #### example: ```ts // Different domain remult.apiClient.url = 'https://example.com/api'; ``` ## subscriptionClient The subscription client used for real-time data updates. By default, it is set to use Server-Sent Events (SSE). It can be set to any subscription provider as illustrated in the Remult tutorial for deploying to a serverless environment. #### see: https://remult.dev/tutorials/react-next/deployment.html#deploying-to-a-serverless-environment ## wrapMessageHandling A function that wraps message handling for subscriptions. This is useful for executing some code before or after any message arrives from the subscription. For example, in Angular, to refresh a specific part of the UI, you can call the `NgZone` run method at this time. #### example: ```ts // Angular example import { Component, NgZone } from '@angular/core'; import { remult } from "remult"; export class AppComponent { constructor(zone: NgZone) { remult.apiClient.wrapMessageHandling = handler => zone.run(() => handler()); } } ``` # API Reference - Repository # Repository used to perform CRUD operations on an `entityType` ## find returns a result array based on the provided options Arguments: * **options** * **limit** - Determines the number of rows returned by the request, on the browser the default is 100 rows #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **page** - Determines the page number that will be used to extract the data #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **where** - filters the data #### example: ```ts await taskRepo.find({where: { completed:false }}) ``` #### see: For more usage examples see [EntityFilter](https://remult.dev/docs/entityFilter.html) * **orderBy** - Determines the order of items returned . #### example: ```ts await this.remult.repo(Products).find({ orderBy: { name: "asc" }}) ``` #### example: ```ts await this.remult.repo(Products).find({ orderBy: { price: "desc", name: "asc" }}) ``` ## liveQuery returns a result array based on the provided options Arguments: * **options** * **limit** - Determines the number of rows returned by the request, on the browser the default is 100 rows #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **page** - Determines the page number that will be used to extract the data #### example: ```ts await this.remult.repo(Products).find({ limit:10, page:2 }) ``` * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **where** - filters the data #### example: ```ts await taskRepo.find({where: { completed:false }}) ``` #### see: For more usage examples see [EntityFilter](https://remult.dev/docs/entityFilter.html) * **orderBy** - Determines the order of items returned . #### example: ```ts await this.remult.repo(Products).find({ orderBy: { name: "asc" }}) ``` #### example: ```ts await this.remult.repo(Products).find({ orderBy: { price: "desc", name: "asc" }}) ``` ## findFirst returns the first item that matchers the `where` condition #### example: ```ts await taskRepo.findFirst({ completed:false }) ``` #### example: ```ts await taskRepo.findFirst({ completed:false },{ createIfNotFound: true }) ``` Arguments: * **where** - filters the data #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) * **options** * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **where** - filters the data #### example: ```ts await taskRepo.find({where: { completed:false }}) ``` #### see: For more usage examples see [EntityFilter](https://remult.dev/docs/entityFilter.html) * **orderBy** - Determines the order of items returned . #### example: ```ts await this.remult.repo(Products).find({ orderBy: { name: "asc" }}) ``` #### example: ```ts await this.remult.repo(Products).find({ orderBy: { price: "desc", name: "asc" }}) ``` * **useCache** - determines if to cache the result, and return the results from cache. * **createIfNotFound** - If set to true and an item is not found, it's created and returned ## findOne returns the first item that matchers the `where` condition #### example: ```ts await taskRepo.findOne({ where:{ completed:false }}) ``` #### example: ```ts await taskRepo.findFirst({ where:{ completed:false }, createIfNotFound: true }) ``` Arguments: * **options** * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **where** - filters the data #### example: ```ts await taskRepo.find({where: { completed:false }}) ``` #### see: For more usage examples see [EntityFilter](https://remult.dev/docs/entityFilter.html) * **orderBy** - Determines the order of items returned . #### example: ```ts await this.remult.repo(Products).find({ orderBy: { name: "asc" }}) ``` #### example: ```ts await this.remult.repo(Products).find({ orderBy: { price: "desc", name: "asc" }}) ``` * **useCache** - determines if to cache the result, and return the results from cache. * **createIfNotFound** - If set to true and an item is not found, it's created and returned ## findId returns the items that matches the id. If id is undefined | null, returns null Arguments: * **id** * **options** * **load** * **include** - An option used in the `find` and `findFirst` methods to specify which related entities should be included when querying the source entity. It allows you to eagerly load related data to avoid N+1 query problems. #### param: An object specifying the related entities to include, their options, and filtering criteria. Example usage: ``` const orders = await customerRepo.find({ include: { // Include the 'tags' relation for each customer. tags: true, }, }); ``` In this example, the `tags` relation for each customer will be loaded and included in the query result. #### see: - Relations.toMany - Relations.toOne - RelationOptions * **useCache** - determines if to cache the result, and return the results from cache. * **createIfNotFound** - If set to true and an item is not found, it's created and returned ## groupBy Performs an aggregation on the repository's entity type based on the specified options. #### returns: The result of the aggregation. #### example: ```ts // Grouping by country and city, summing the salary field, and ordering by country and sum of salary: const results = await repo.groupBy({ group: ['country', 'city'], sum: ['salary'], where: { salary: { $ne: 1000 }, }, orderBy: { country: 'asc', salary: { sum: 'desc', }, }, }); // Accessing the results: console.log(results[0].country); // 'uk' console.log(results[0].city); // 'London' console.log(results[0].$count); // count for London, UK console.log(results[0].salary.sum); // Sum of salaries for London, UK ``` Arguments: * **options** - The options for the aggregation. * **group** - Fields to group by. The result will include one entry per unique combination of these fields. * **sum** - Fields to sum. The result will include the sum of these fields for each group. * **avg** - Fields to average. The result will include the average of these fields for each group. * **min** - Fields to find the minimum value. The result will include the minimum value of these fields for each group. * **max** - Fields to find the maximum value. The result will include the maximum value of these fields for each group. * **distinctCount** - Fields to count distinct values. The result will include the distinct count of these fields for each group. * **where** - Filters to apply to the query before aggregation. #### see: EntityFilter * **orderBy** - Fields and aggregates to order the results by. The result can be ordered by groupBy fields, sum fields, average fields, min fields, max fields, and distinctCount fields. ## aggregate Performs an aggregation on the repository's entity type based on the specified options. #### returns: The result of the aggregation. #### example: ```ts // Aggregating (summing the salary field across all items): const totalSalary = await repo.aggregate({ sum: ['salary'], }); console.log(totalSalary.salary.sum); // Outputs the total sum of salaries ``` Arguments: * **options** - The options for the aggregation. ## query Fetches data from the repository in a way that is optimized for handling large sets of entity objects. Unlike the `find` method, which returns an array, the `query` method returns an iterable `QueryResult` object. This allows for more efficient data handling, particularly in scenarios that involve paging through large amounts of data. The method supports pagination and aggregation in a single request. When aggregation options are provided, the result will include both the items from the current page and the results of the requested aggregation. The `query` method is designed for asynchronous iteration using the `for await` statement. #### example: ```ts // Basic usage with asynchronous iteration: for await (const task of taskRepo.query()) { // Perform some operation on each task } ``` #### example: ```ts // Querying with pagination: const query = taskRepo.query({ where: { completed: false }, pageSize: 100, }); let paginator = await query.paginator(); console.log('Number of items on the current page:', paginator.items.length); console.log('Total pages:', Math.ceil(paginator.aggregate.$count / 100)); if (paginator.hasNextPage) { paginator = await paginator.nextPage(); console.log('Items on the next page:', paginator.items.length); } ``` #### example: ```ts // Querying with aggregation: const query = await repo.query({ where: { completed: false }, pageSize: 50, aggregates: { sum: ['salary'], average: ['age'], } }); let paginator = await query.paginator(); // Accessing paginated items console.table(paginator.items); // Accessing aggregation results console.log('Total salary:', paginator.aggregates.salary.sum); // Sum of all salaries console.log('Average age:', paginator.aggregates.age.average); // Average age ``` Arguments: * **options** ## count Returns a count of the items matching the criteria. #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) #### example: ```ts await taskRepo.count({ completed:false }) ``` Arguments: * **where** - filters the data #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) ## validate Validates an item #### example: ```ts const error = repo.validate(task); if (error){ alert(error.message); alert(error.modelState.title);//shows the specific error for the title field } // Can also be used to validate specific fields const error = repo.validate(task,"title") ``` Arguments: * **item** * **fields** ## save saves an item or item[] to the data source. It assumes that if an `id` value exists, it's an existing row - otherwise it's a new row #### example: ```ts await taskRepo.save({...task, completed:true }) ``` Arguments: * **item** ## insert Insert an item or item[] to the data source #### example: ```ts await taskRepo.insert({title:"task a"}) ``` #### example: ```ts await taskRepo.insert([{title:"task a"}, {title:"task b", completed:true }]) ``` Arguments: * **item** ## update Updates an item, based on its `id` #### example: ```ts taskRepo.update(task.id,{...task,completed:true}) ``` Arguments: * **id** * **item** ## updateMany Updates all items that match the `where` condition. Arguments: * **options** * **where** - filters the data #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) * **set** ## upsert Inserts a new entity or updates an existing entity based on the specified criteria. If an entity matching the `where` condition is found, it will be updated with the provided `set` values. If no matching entity is found, a new entity will be created with the given data. The `upsert` method ensures that a row exists based on the `where` condition: if no entity is found, a new one is created. It can handle both single and multiple upserts. #### returns: A promise that resolves with the inserted or updated entity, or an array of entities if multiple options were provided. #### example: ```ts // Upserting a single entity: updates 'task a' if it exists, otherwise creates it. taskRepo.upsert({ where: { title: 'task a' }, set: { completed: true } }); ``` #### example: ```ts // Upserting a single entity without additional `set` values: ensures that a row with the title 'task a' exists. taskRepo.upsert({ where: { title: 'task a' } }); ``` #### example: ```ts // Upserting multiple entities: ensures both 'task a' and 'task b' exist, updating their `completed` status if found. taskRepo.upsert([ { where: { title: 'task a' }, set: { completed: true } }, { where: { title: 'task b' }, set: { completed: true } } ]); ``` Arguments: * **options** - The options that define the `where` condition and the `set` values. Can be a single object or an array of objects. ## delete Deletes an Item Arguments: * **id** ## deleteMany Deletes all items that match the `where` condition. Arguments: * **options** * **where** - filters the data #### see: [EntityFilter](http://remult.dev/docs/entityFilter.html) ## create Creates an instance of an item. It'll not be saved to the data source unless `save` or `insert` will be called. It's useful to start or reset a form taking your entity default values into account. Arguments: * **item** ## toJson * **toJson** Arguments: * **item** ## fromJson Translates a json object to an item instance Arguments: * **x** * **isNew** ## getEntityRef returns an `entityRef` for an item returned by `create`, `find` etc... Arguments: * **item** ## fields Provides information about the fields of the Repository's entity #### example: ```ts console.log(repo.fields.title.caption) // displays the caption of a specific field console.log(repo.fields.title.options)// writes the options that were defined for this field ``` ## metadata The metadata for the `entity` #### See: [EntityMetadata](https://remult.dev/docs/ref_entitymetadata.html) ## addEventListener * **addEventListener** Arguments: * **listener** ## relations * **relations** Arguments: * **item** # API Reference - RemultServerOptions # RemultServerOptions * **RemultServerOptions** ## entities Entities to use for the api ## controllers Controller to use for the api ## getUser Will be called to get the current user based on the current request ## initRequest Will be called for each request and can be used for configuration ## initApi Will be called once the server is loaded and the data provider is ready ## dataProvider Data Provider to use for the api. #### see: [Connecting to a Database](https://remult.dev/docs/databases.html). ## ensureSchema Will create tables and columns in supporting databases. default: true #### description: when set to true, it'll create entities that do not exist, and add columns that are missing. ## rootPath The path to use for the api, default:/api #### description: If you want to use a different api path adjust this field ## defaultGetLimit The default limit to use for find requests that did not specify a limit ## logApiEndPoints When set to true (default) it'll console log each api endpoint that is created ## subscriptionServer A subscription server to use for live query and message channels ## liveQueryStorage A storage to use to store live queries, relevant mostly for serverless scenarios or larger scales ## contextSerializer Used to store the context relevant info for re running a live query ## admin When set to true, will display an admin ui in the `/api/admin` url. Can also be set to an arrow function for fine grained control #### example: ```ts admin: true ``` #### example: ```ts admin: ()=> remult.isAllowed('admin') ``` #### see: [allowed](http://remult.dev/docs/allowed.html) ## queueStorage Storage to use for backend methods that use queue ## error This method is called whenever there is an error in the API lifecycle. #### example: ```ts export const api = remultExpress({ error: async (e) => { if (e.httpStatusCode == 400) { e.sendError(500, { message: "An error occurred" }) } } }) ``` # API Reference - EntityFilter --- tags: - Where - Filter - Entity Where - Entity Filter --- # EntityFilter Used to filter the desired result set ### Basic example ```ts where: { status: 1 } ``` ( this will include only items where the status is equal to 1. ### In Statement ```ts where:{ status:[1,3,5] } //or where:{ status:{ $in:[1,3,5]: } } ``` ### Not Equal ```ts where:{ status:{ "!=":1 }} //or where:{ status:{ $ne:1 }} ``` ### Not in ```ts where:{status:{ "!=":[1,2,3] }} //or where:{status:{ $ne:[1,2,3] }} //or where:{status:{ $nin:[1,2,3] }} ``` ### Comparison operators ```ts where:{ status:{ ">":1 }} where:{ status:{ ">=":1 }} where:{ status:{ "<":1 }} where:{ status:{ "<=":1 }} //or where:{ status:{ $gt:1 }} where:{ status:{ $gte:1 }} where:{ status:{ $lt:1 }} where:{ status:{ $lte:1 }} ``` ### Contains ```ts where: { name: { $contains: 'joe' } } ``` ### Not Contains ```ts where: { name: { $notContains: 'joe' } } ``` ### Starts With ```ts where: { name: { $startsWith: 'joe' } } ``` ### Ends With ```ts where: { name: { $endsWith: 'joe' } } ``` ### Id Equal ```ts where: { person: { $id: 123456 } } ``` ### Multiple conditions has an `and` relationship ```ts where: { status:1, archive:false } ``` ### $and ```ts where: { $and: [{ status: 1 }, { archive: false }] } ``` ### $or ```ts where: { $or: [{ status: 1 }, { archive: false }] } ``` ### $not ```ts where: { $not: { status: 1 } } ``` # API Reference - EntityMetadata # EntityMetadata Metadata for an `Entity`, this metadata can be used in the user interface to provide a richer UI experience ## entityType The class type of the entity ## key The Entity's key also used as it's url ## fields Metadata for the Entity's fields ## caption A human readable caption for the entity. Can be used to achieve a consistent caption for a field throughout the app #### example: ```ts

Create a new item in {taskRepo.metadata.caption}

``` #### see: EntityOptions.caption ## dbName The name of the table in the database that holds the data for this entity. If no name is set in the entity options, the `key` will be used instead. #### see: EntityOptions.dbName ## options The options send to the `Entity`'s decorator #### see: EntityOptions ## apiUpdateAllowed true if the current user is allowed to update an entity instance #### see: * @example Arguments: * **item** ## apiReadAllowed true if the current user is allowed to read from entity #### see: EntityOptions.allowApiRead #### example: ```ts const taskRepo = remult.repo(Task); if (taskRepo.metadata.apiReadAllowed){ await taskRepo.find() } ``` ## apiDeleteAllowed true if the current user is allowed to delete an entity instance * #### see: EntityOptions.allowApiDelete #### example: ```ts const taskRepo = remult.repo(Task); if (taskRepo.metadata.apiDeleteAllowed(task)){ // display delete button } ``` Arguments: * **item** ## apiInsertAllowed true if the current user is allowed to create an entity instance #### see: EntityOptions.allowApiInsert #### example: ```ts const taskRepo = remult.repo(Task); if (taskRepo.metadata.apiInsertAllowed(task)){ // display insert button } ``` Arguments: * **item** ## getDbName * **getDbName** ## idMetadata Metadata for the Entity's id #### see: EntityOptions.id for configuration # API Reference - FieldMetadata # FieldMetadata Metadata for a `Field`, this metadata can be used in the user interface to provide a richer UI experience ## valueType The field's value type (number,string etc...) ## key The field's member name in an object. #### example: ```ts const taskRepo = remult.repo(Task); console.log(taskRepo.metadata.fields.title.key); // result: title ``` ## caption A human readable caption for the field. Can be used to achieve a consistent caption for a field throughout the app #### example: ```ts ``` #### see: FieldOptions#caption for configuration details ## dbName The name of the column in the database that holds the data for this field. If no name is set, the key will be used instead. #### example: ```ts @Fields.string({ dbName: 'userName'}) userName='' ``` #### see: FieldOptions#dbName for configuration details ## options The options sent to this field's decorator ## inputType The `inputType` relevant for this field, determined by the options sent to it's decorator and the valueConverter in these options ## allowNull if null is allowed for this field #### see: FieldOptions#allowNull for configuration details ## target The class that contains this field #### example: ```ts const taskRepo = remult.repo(Task); Task == taskRepo.metadata.fields.title.target //will return true ``` ## getDbName * **getDbName** ## isServerExpression Indicates if this field is based on a server express ## dbReadOnly indicates that this field should only be included in select statement, and excluded from update or insert. useful for db generated ids etc... #### see: FieldOptions#dbReadOnly for configuration details ## valueConverter the Value converter for this field ## displayValue Get the display value for a specific item #### see: FieldOptions#displayValue for configuration details #### example: ```ts repo.fields.createDate.displayValue(task) //will display the date as defined in the `displayValue` option defined for it. ``` Arguments: * **item** ## apiUpdateAllowed Determines if the current user is allowed to update a specific entity instance. #### example: ```ts const taskRepo = remult.repo(Task); // Check if the current user is allowed to update a specific task if (taskRepo.metadata.apiUpdateAllowed(task)){ // Allow user to edit the entity } ``` #### see: FieldOptions#allowApiUpdate for configuration details #### returns: True if the update is allowed. Arguments: * **item** - Partial entity instance to check permissions against. ## includedInApi Determines if a specific entity field should be included in the API based on the current user's permissions. This method checks visibility permissions for a field within a partial entity instance. #### example: ```ts const employeeRepo = remult.repo(Employee); // Determine if the 'salary' field of an employee should be visible in the API for the current user if (employeeRepo.fields.salary.includedInApi({ id: 123, name: 'John Doe' })) { // The salary field is included in the API } ``` #### see: FieldOptions#includeInApi for configuration details #### returns: True if the field is included in the API. Arguments: * **item** - The partial entity instance used to evaluate field visibility. ## toInput Adapts the value for usage with html input #### example: ```ts @Fields.dateOnly() birthDate = new Date(1976,5,16) //... input.value = repo.fields.birthDate.toInput(person) // will return '1976-06-16' ``` #### see: ValueConverter#toInput for configuration details Arguments: * **value** * **inputType** ## fromInput Adapts the value for usage with html input #### example: ```ts @Fields.dateOnly() birthDate = new Date(1976,5,16) //... person.birthDate = repo.fields.birthDate.fromInput(personFormState) // will return Date ``` #### see: ValueConverter#fromInput for configuration details Arguments: * **inputValue** * **inputType** # API Reference - Allowed # Allowed Throughout the api you'll see methods that use the `Allowed` data type, for example `allowApiRead` etc... The `Allowed` data type can be set to one of the following value: - true/false ```ts { allowApiRead: true } ``` - a Role - Checks if the current user has this role. ```ts { allowApiRead: 'admin' } ``` or with a constant ```ts { allowApiRead: Roles.admin } ``` - An Array of Roles - checks if the current user has at least one of the roles in the array ```ts { allowApiRead: [Roles.admin, Roles.productManager] } ``` - A function that get's a `remult` object as a parameter and returns true or false ```ts { allowApiRead: Allow.authenticated } } ``` or: ```ts { allowApiRead: () => remult.user.name === 'superman' } } ``` # AllowedForInstance In some cases, the allowed can be evaluated with regards to a specific instance, for example `allowApiUpdate` can consider specific row values. The Allowed for Instance method accepts two parameters: 1. The relevant `remult` object 2. The relevant entity instance For Example: ```ts @Entity("tasks", { allowApiUpdate: (task) => remult.isAllowed("admin") && !task!.completed }) ``` # API Reference - BackendMethod # BackendMethod Decorator indicating that the decorated method runs on the backend. It allows the method to be invoked from the frontend while ensuring that the execution happens on the server side. By default, the method runs within a database transaction, meaning it will either complete entirely or fail without making any partial changes. This behavior can be controlled using the `transactional` option in the `BackendMethodOptions`. For more details, see: [Backend Methods](https://remult.dev/docs/backendMethods.html). #### example: ```typescript @BackendMethod({ allowed: true }) async someBackendMethod() { // method logic here } ``` ## allowed Determines when this `BackendMethod` can execute, see: [Allowed](https://remult.dev/docs/allowed.html) ## apiPrefix Used to determine the route for the BackendMethod. #### example: ```ts {allowed:true, apiPrefix:'someFolder/'} ``` ## transactional Controls whether this `BackendMethod` runs within a database transaction. If set to `true`, the method will either complete entirely or fail without making any partial changes. If set to `false`, the method will not be transactional and may result in partial changes if it fails. #### default: ```ts true ``` #### example: ```ts {allowed: true, transactional: false} ``` ## queue EXPERIMENTAL: Determines if this method should be queued for later execution ## blockUser EXPERIMENTAL: Determines if the user should be blocked while this `BackendMethod` is running ## paramTypes * **paramTypes** # API Reference - QueryResult # QueryResult The result of a call to the `query` method in the `Repository` object. ## [asyncIterator] returns an iterator that iterates the rows in the result using a paging mechanism #### example: ```ts for await (const task of taskRepo.query()) { await taskRepo.save({ ...task, completed }); } ``` ## count returns the number of rows that match the query criteria ## getPage gets the items in a specific page Arguments: * **pageNumber** ## forEach Performs an operation on all the items matching the query criteria Arguments: * **what** ## paginator Returns a `Paginator` object that is used for efficient paging # API Reference - Paginator # Paginator An interface used to paginating using the `query` method in the `Repository` object #### example: ```ts ``` #### example: ```ts const query = taskRepo.query({ where: { completed: false }, pageSize: 100, }) const count = await query.count() console.log('Paged: ' + count / 100) let paginator = await query.paginator() console.log(paginator.items.length) if (paginator.hasNextPage) { paginator = await paginator.nextPage() console.log(paginator.items.length) } ``` ## items the items in the current page ## hasNextPage True if next page exists ## count the count of the total items in the `query`'s result ## nextPage Gets the next page in the `query`'s result set # API Reference - LiveQuery # LiveQuery The `LiveQuery` interface represents a live query that allows subscribing to changes in the query results. ## subscribe Subscribes to changes in the live query results. #### returns: A function that can be used to unsubscribe from the live query. #### example: ```ts // Subscribing to changes in a live query const unsubscribe = taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: 'asc' } //where: { completed: true }, }) .subscribe(info => setTasks(info.applyChanges)); // Later, to unsubscribe unsubscribe(); ``` Arguments: * **next** - A function that will be called with information about changes in the query results. # API Reference - LiveQueryChangeInfo # LiveQueryChangeInfo The `LiveQueryChangeInfo` interface represents information about changes in the results of a live query. ## items The updated array of result items. ## changes The changes received in the specific message. The change types can be "all" (replace all), "add", "replace", or "remove". ## applyChanges Applies the changes received in the message to an existing array. This method is particularly useful with React to update the component's state based on the live query changes. #### returns: The updated array of result items after applying the changes. #### example: ```ts // Using applyChanges in a React component with useEffect hook useEffect(() => { return taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: 'asc' } //where: { completed: true }, }) .subscribe(info => setTasks(info.applyChanges)); }, []); ``` Arguments: * **prevState** - The previous state of the array of result items. # API Reference - Filter # Filter The `Filter` class is a helper class that focuses on filter-related concerns. It provides methods for creating and applying filters in queries. ## getPreciseValues Retrieves precise values for each property in a filter for an entity. #### returns: A promise that resolves to a FilterPreciseValues object containing the precise values for each property. #### example: ```ts const preciseValues = await Filter.getPreciseValues(meta, { status: { $ne: 'active' }, $or: [ { customerId: ["1", "2"] }, { customerId: "3" } ] }); console.log(preciseValues); // Output: // { // "customerId": ["1", "2", "3"], // Precise values inferred from the filter // "status": undefined, // Cannot infer precise values for 'status' // } ``` Arguments: * **metadata** - The metadata of the entity being filtered. * **filter** - The filter to analyze. ## getPreciseValues Retrieves precise values for each property in a filter for an entity. #### returns: A promise that resolves to a FilterPreciseValues object containing the precise values for each property. #### example: ```ts const preciseValues = await where.getPreciseValues(); console.log(preciseValues); // Output: // { // "customerId": ["1", "2", "3"], // Precise values inferred from the filter // "status": undefined, // Cannot infer precise values for 'status' // } ``` ## createCustom Creates a custom filter. Custom filters are evaluated on the backend, ensuring security and efficiency. When the filter is used in the frontend, only its name is sent to the backend via the API, where the filter gets translated and applied in a safe manner. #### returns: A function that returns an `EntityFilter` of type `entityType`. #### example: ```ts class Order { //... static activeOrdersFor = Filter.createCustom( async ({ year }) => { return { status: ['created', 'confirmed', 'pending', 'blocked', 'delayed'], createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1), }, } }, ) } // Usage await repo(Order).find({ where: Order.activeOrders({ year }), }) ``` #### see: [Sql filter and Custom filter](/docs/custom-filter.html) [Filtering and Relations](/docs/filtering-and-relations.html) Arguments: * **translator** - A function that returns an `EntityFilter`. * **key** - An optional unique identifier for the custom filter. ## entityFilterToJson Translates an `EntityFilter` to a plain JSON object that can be stored or transported. #### returns: A plain JSON object representing the `EntityFilter`. #### example: ```ts // Assuming `Task` is an entity class const jsonFilter = Filter.entityFilterToJson(Task, { completed: true }); // `jsonFilter` can now be stored or transported as JSON ``` Arguments: * **entityDefs** - The metadata of the entity associated with the filter. * **where** - The `EntityFilter` to be translated. ## entityFilterFromJson Translates a plain JSON object back into an `EntityFilter`. #### returns: The reconstructed `EntityFilter`. #### example: ```ts // Assuming `Task` is an entity class and `jsonFilter` is a JSON object representing an EntityFilter const taskFilter = Filter.entityFilterFromJson(Task, jsonFilter); // Using the reconstructed `EntityFilter` in a query const tasks = await remult.repo(Task).find({ where: taskFilter }); for (const task of tasks) { // Do something for each task based on the filter } ``` Arguments: * **entityDefs** - The metadata of the entity associated with the filter. * **packed** - The plain JSON object representing the `EntityFilter`. ## fromEntityFilter Converts an `EntityFilter` to a `Filter` that can be used by the `DataProvider`. This method is mainly used internally. #### returns: A `Filter` instance that can be used by the `DataProvider`. #### example: ```ts // Assuming `Task` is an entity class and `taskFilter` is an EntityFilter const filter = Filter.fromEntityFilter(Task, taskFilter); // `filter` can now be used with the DataProvider ``` Arguments: * **entity** - The metadata of the entity associated with the filter. * **whereItem** - The `EntityFilter` to be converted. ## constructor * **new Filter** Arguments: * **apply** ## resolve Resolves an entity filter. This method takes a filter which can be either an instance of `EntityFilter` or a function that returns an instance of `EntityFilter` or a promise that resolves to an instance of `EntityFilter`. It then resolves the filter if it is a function and returns the resulting `EntityFilter`. #### returns: The resolved entity filter. Arguments: * **filter** - The filter to resolve. ## toJson * **toJson** # API Reference - Sort # Sort The `Sort` class is used to describe sorting criteria for queries. It is mainly used internally, but it provides a few useful functions for working with sorting. ## toEntityOrderBy Translates the current `Sort` instance into an `EntityOrderBy` object. #### returns: An `EntityOrderBy` object representing the sort criteria. ## constructor Constructs a `Sort` instance with the provided sort segments. Arguments: * **segments** - The sort segments to be included in the sort criteria. ## Segments The segments of the sort criteria. ## reverse Reverses the sort order of the current sort criteria. #### returns: A new `Sort` instance with the reversed sort order. ## compare Compares two objects based on the current sort criteria. #### returns: A negative value if `a` should come before `b`, a positive value if `a` should come after `b`, or zero if they are equal. Arguments: * **a** - The first object to compare. * **b** - The second object to compare. * **getFieldKey** - An optional function to get the field key for comparison. ## translateOrderByToSort Translates an `EntityOrderBy` to a `Sort` instance. #### returns: A `Sort` instance representing the translated order by. Arguments: * **entityDefs** - The metadata of the entity associated with the order by. * **orderBy** - The `EntityOrderBy` to be translated. ## createUniqueSort Creates a unique `Sort` instance based on the provided `Sort` and the entity metadata. This ensures that the sort criteria result in a unique ordering of entities. #### returns: A `Sort` instance representing the unique sort criteria. Arguments: * **entityMetadata** - The metadata of the entity associated with the sort. * **orderBy** - The `Sort` instance to be made unique. ## createUniqueEntityOrderBy Creates a unique `EntityOrderBy` based on the provided `EntityOrderBy` and the entity metadata. This ensures that the order by criteria result in a unique ordering of entities. #### returns: An `EntityOrderBy` representing the unique order by criteria. Arguments: * **entityMetadata** - The metadata of the entity associated with the order by. * **orderBy** - The `EntityOrderBy` to be made unique. # API Reference - SqlDatabase # SqlDatabase A DataProvider for Sql Databases #### example: ```ts const db = new SqlDatabase(new PostgresDataProvider(pgPool)) ``` #### see: [Connecting a Database](https://remult.dev/docs/quickstart#connecting-a-database) ## getDb Gets the SQL database from the data provider. #### returns: The SQL database. #### see: [Direct Database Access](https://remult.dev/docs/running-sql-on-the-server) Arguments: * **dataProvider** - The data provider. ## createCommand Creates a new SQL command. #### returns: The SQL command. #### see: [Direct Database Access](https://remult.dev/docs/running-sql-on-the-server) ## execute Executes a SQL command. #### returns: The SQL result. #### see: [Direct Database Access](https://remult.dev/docs/running-sql-on-the-server) Arguments: * **sql** - The SQL command. ## wrapIdentifier Wraps an identifier with the database's identifier syntax. ## ensureSchema * **ensureSchema** Arguments: * **entities** ## getEntityDataProvider Gets the entity data provider. #### returns: The entity data provider. Arguments: * **entity** - The entity metadata. ## transaction Runs a transaction. Used internally by remult when transactions are required #### returns: The promise of the transaction. Arguments: * **action** - The action to run in the transaction. ## rawFilter Creates a raw filter for entity filtering. #### returns: - The entity filter with a custom SQL filter. #### example: ```ts SqlDatabase.rawFilter(({param}) => `"customerId" in (select id from customers where city = ${param(customerCity)})` ) ``` #### see: [Leveraging Database Capabilities with Raw SQL in Custom Filters](https://remult.dev/docs/custom-filter.html#leveraging-database-capabilities-with-raw-sql-in-custom-filters) Arguments: * **build** - The custom SQL filter builder function. ## filterToRaw Converts a filter to a raw SQL string. #### see: [Leveraging Database Capabilities with Raw SQL in Custom Filters](https://remult.dev/docs/running-sql-on-the-server#leveraging-entityfilter-for-sql-databases) Arguments: * **repo** * **condition** * **sqlCommand** * **dbNames** * **wrapIdentifier** ## LogToConsole `false` _(default)_ - No logging `true` - to log all queries to the console `oneLiner` - to log all queries to the console as one line a `function` - to log all queries to the console as a custom format #### example: ```ts SqlDatabase.LogToConsole = (duration, query, args) => { console.log("be crazy ;)") } ``` ## durationThreshold Threshold in milliseconds for logging queries to the console. ## constructor Creates a new SQL database. #### example: ```ts const db = new SqlDatabase(new PostgresDataProvider(pgPool)) ``` Arguments: * **sql** - The SQL implementation. ## end # API Reference - SubscriptionChannel # SubscriptionChannel The `SubscriptionChannel` class is used to send messages from the backend to the frontend, using the same mechanism used by live queries. #### example: ```ts // Defined in code that is shared between the frontend and the backend const statusChange = new SubscriptionChannel<{ oldStatus: number, newStatus: number }>("statusChange"); // Backend: Publishing a message statusChange.publish({ oldStatus: 1, newStatus: 2 }); // Frontend: Subscribing to messages statusChange.subscribe((message) => { console.log(`Status changed from ${message.oldStatus} to ${message.newStatus}`); }); // Note: If you want to publish from the frontend, use a BackendMethod for that. ``` ## constructor Constructs a new `SubscriptionChannel` instance. Arguments: * **channelKey** - The key that identifies the channel. ## channelKey The key that identifies the channel. ## publish Publishes a message to the channel. This method should only be used on the backend. Arguments: * **message** - The message to be published. * **remult** - An optional instance of Remult to use for publishing the message. ## subscribe Subscribes to messages from the channel. This method should only be used on the frontend. #### returns: A promise that resolves to a function that can be used to unsubscribe from the channel. Arguments: * **next** - A function that will be called with each message received. * **remult** - An optional instance of Remult to use for the subscription. # API Reference - generateMigrations # generateMigrations Generates migration scripts based on changes in entities. #### see: [Migrations](https://remult.dev/docs/migrations.html) Arguments: * **options** - Configuration options for generating migrations. * **entities** - An array of entity classes whose changes will be included in the migration. * **dataProvider** - The data provider instance or a function returning a promise of the data provider. * **migrationsFolder** - (Optional) The path to the folder where migration scripts will be stored. Default is 'src/migrations'. * **snapshotFile** - (Optional) The path to the file where the snapshot of the last known state will be stored. Default is 'migrations-snapshot.json' in the `migrationsFolder`. * **migrationsTSFile** - (Optional) The path to the TypeScript file where the generated migrations will be written. Default is 'migrations.ts' in the `migrationsFolder`. * **endConnection** - (Optional) Determines whether to close the database connection after generating migrations. Default is false. # API Reference - migrate # migrate Applies migration scripts to update the database schema. #### see: [Migrations](https://remult.dev/docs/migrations.html) Arguments: * **options** - Configuration options for applying migrations. * **migrations** - An object containing the migration scripts, each keyed by a unique identifier. * **dataProvider** - The data provider instance or a function returning a promise of the data provider. * **migrationsTable** - (Optional) The name of the table that tracks applied migrations. Default is '__remult_migrations_version'. * **endConnection** - (Optional) Determines whether to close the database connection after applying migrations. Default is false. * **beforeMigration** - (Optional) A callback function that is called before each migration is applied. Receives an object with the migration index. * **afterMigration** - (Optional) A callback function that is called after each migration is applied. Receives an object with the migration index and the duration of the migration. # API Reference - REST API Spec # Entity Rest Api Breakdown All entities automatically expose a rest API based on the parameters defined in its decorator. The API supports the following actions (we'll use the `products` entity as an example, and a specific product with an id=7): | Http Method | Description | example | requires | | ----------- | ----------------------------------------------------------------------------------- | --------------- | -------------- | | GET | returns an array of rows | /api/products | allowApiRead | | GET | returns a single row based on its id | /api/products/7 | allowApiRead | | POST | creates a new row based on the object sent in the body, and returns the new row | /api/products | allowApiInsert | | PUT | updates an existing row based on the object sent in the body and returns the result | /api/products/7 | allowApiUpdate | | DELETE | deletes an existing row | /api/products/7 | allowApiDelete | ## Sort Add \_sort and \_order (ascending order by default) ``` https://mySite.com/api/products?_sort=price&_order=desc ``` ## Filter You can filter the rows using different operators ``` https://mySite.com/api/products?price.gte=5&price.lte=10 ``` ### Filter Operators | operator | description | example | | ------------ | --------------------- | -------------------------------------------------- | | `none` | Equal To | price=10 | | .ne | Not Equal | price.ne=10 | | .in | is in json array | price.in=%5B10%2C20%5D _(url encoded - `[10,20]`)_ | | .contains | Contains a string | name.contains=ee | | .notContains | Not contains a string | name.notContains=ee | | .startsWith | Starts with a string | name.startsWith=ee | | .endsWith | Ends with a string | name.endsWith=ee | | .gt | Greater than | price.gt=10 | | .gte | Greater than or equal | price.gte=10 | | .lt | Lesser than | price.lt=10 | | .lte | Lesser than or equal | price.lte=10 | | .null | is or is not null | price.null=true | - you can add several filter conditions using the `&` operator. ### Count ``` https://mySite.com/api/products?price.gte=10&__action=count ``` returns: ```JSON { "count": 4 } ``` ## Paginate The default page size is 100 rows. ``` https://mySite.com/api/products?_limit=25 ``` ``` https://mySite.com/api/products?_limit=5&_page=3 ``` :::tip You can use it all in conjunction: ``` https://mySite.com/api/products?price.gte=5&price.lte=10&_sort=price&_order=desc&_limit=5&_page=3 ``` ::: # Active Record & Mutable - EntityBase # EntityBase * **EntityBase** ## constructor * **new EntityBase** ## $ * **$** ## _ * **_** ## assign * **assign** Arguments: * **values** ## delete * **delete** ## isNew * **isNew** ## save * **save** # Active Record & Mutable - IdEntity # IdEntity * **IdEntity** ## constructor * **new IdEntity** ## id * **id** ## $ * **$** ## _ * **_** ## assign * **assign** Arguments: * **values** ## delete * **delete** ## isNew * **isNew** ## save * **save** # Active Record & Mutable - EntityRef # EntityRef * **EntityRef** ## hasErrors * **hasErrors** ## undoChanges * **undoChanges** ## save * **save** ## reload * **reload** ## delete * **delete** ## isNew * **isNew** ## wasChanged * **wasChanged** ## wasDeleted * **wasDeleted** ## getId * **getId** ## getOriginalId * **getOriginalId** ## toApiJson * **toApiJson** ## validate * **validate** ## clone * **clone** ## subscribe * **subscribe** Arguments: * **listener** ## error * **error** ## repository * **repository** ## metadata * **metadata** ## apiUpdateAllowed * **apiUpdateAllowed** ## apiDeleteAllowed * **apiDeleteAllowed** ## apiInsertAllowed * **apiInsertAllowed** ## isLoading * **isLoading** ## fields * **fields** ## relations * **relations** # Active Record & Mutable - FieldRef # FieldRef * **FieldRef** ## subscribe * **subscribe** Arguments: * **listener** ## valueChanged * **valueChanged** ## load Loads the related value - returns null if the related value is not found ## valueIsNull * **valueIsNull** ## originalValueIsNull * **originalValueIsNull** ## validate * **validate** ## error * **error** ## displayValue * **displayValue** ## value * **value** ## originalValue * **originalValue** ## inputValue * **inputValue** ## entityRef * **entityRef** ## container * **container** ## metadata * **metadata** # Active Record & Mutable - getEntityRef # getEntityRef Retrieves the EntityRef object associated with the specified entity instance. The EntityRef provides methods for performing operations on the entity instance. #### returns: The EntityRef object associated with the specified entity instance. #### throws: If throwException is true and the EntityRef object cannot be retrieved. #### see: [Active Record & EntityBase](https://remult.dev/docs/active-record) Arguments: * **entity** - The entity instance. * **throwException** - Indicates whether to throw an exception if the EntityRef object cannot be retrieved. # Active Record & Mutable - getFields # getFields * **getFields** Arguments: * **container** * **remult** # Remult Blog - Introducing Remult --- title: "Introducing Remult: The Backend to Frontend Framework You Always Wanted" sidebar: false editLink: false --- > March 14, 2023 | Noam Honig # Introducing Remult: The Backend to Frontend Framework You Always Wanted Application developers, as their name implies, like to develop applications––they ultimately care very little about frontend vs. backend, and really just want to deliver value to users. Being an application developer myself, and very much like other application developers, one of the things that constantly drives my decision making when selecting tools and frameworks is the fact that I’m also quite lazy. My main objective is to be able to ship applications with as little effort as possible, and my pet peeve is silly repetitive, mechanical tasks that make me die a little inside every time I need to perform them. For example, I don’t like to have to remember to align things that don’t automatically align themselves, as one common example of a repetitive task that is completely superfluous. I guess my trademark is that when I encounter challenges, I will always look for ways to automate a solution. (Like the app I once built in the 90s to send a romantic text message to my girlfriend once a day––to overcome my own romantic shortcomings). I've been building tools and frameworks to increase developer productivity most of my professional career. This all started back in 2006, when I identified a need for modernizing applications created using a low-code generator, to C#.NET. I decided to not only create a fully automated migration solution but more importantly, create a C# class library that would enable an equivalent speed of development that was previously made possible by using the low-code tool. This was a very high bar, as developers that are used to working in low-code are also often the type that don’t really like the details or the bits and bytes of the internals. A developer who is happy to use low-code tooling is essentially only looking to write code that gets a specific task done. Being able to replicate this in a coding framework needed to provide as seamless and simple of an experience that they had until now––but with code. Hundreds of organizations have used Firefly’s migration solution, from fortune 500s and governments to small software houses. The C# library provided is still in use today, enabling “low-code level”, highly productive application development, combined with the flexibility of code. The ability to efficiently ship full applications with a simple library, to me, felt like something that had to be portable and replicated to the modern web. The wheels began to turn. ## What Brought Us Here Like many other web developers, when Node.js came out, I was excited about the promise of backend to frontend that came with it, that finally we don’t have to learn two languages to build a full stack application. Just the sheer mental switch between languages––for me it was C# on the backend and then Javascript on the frontend, and this always came with its toll of friction and context switch that could even be considered debilitating to my dev workflow. Even with the possibility Node.js presented of enabling Javascript on both ends, still not many developers chose to do so. All of the best practices and learning resources continued to recommend using two separate approaches for backend and frontend, and a complete separation of tools, libraries and frameworks with very little sharing of code and patterns for frontend and backend. This modus operandi of having two separate languages each with its own syntax and logic, creates the need for a lot of repetitive boilerplate, such as code to pull the data from the database (for every single application entity), code to expose entity CRUD operations as an API, with four, five or even six different routes per entity, methods for these routes, and these all would again get duplicated and reused hundreds of times, and each time further complicated. You get the idea. And this is just on the backend. Now on the frontend you have reverse code for this, you have code that takes these JSON responses and rebuilds objects out of these, for you to be able use them on the frontend. Just trying to get data from the database to the users, but in the meantime, you need code to read the database, serialize the JSON, send it over a route, only to have to deserialize and query it on the frontend, just to get it in front of the user. This is all mechanical code that practically does nothing. Where, eventually, all of this is repeatable. Wait, there’s more. Every API route needs to be able to fetch data, provide some sort of server-side paging, sorting and filtering, delete, insert, and update, all of these very generic actions are repeated over and over by all developers building full stack applications all the time in millions of lines of code. Now let’s talk about concerns that cross over from the frontend to the backend that get duplicated. Up until a couple of years ago the frontend and backend were at best sharing types, but there’s so much more to types than just strings or integers. Commons questions like: how do you serialize these from JSON and then to JSON? How do you validate them? Today, validations on the frontend and backend are operations that are completely separate. Which begs the questions WHY? Why should I have to remember (as a lazy developer mind you) to have to perform two separate validations on the frontend and the backend? ![Duplicate boilerplate code](./introducing-remult-part-1/boilerplate.png) There’s also a cultural aspect with this dichotomy between frontend and backend code, where there needs to be such impeccable communication and alignment between the developers, that is almost an impossible feat. At the end of the day, all of those places are places of friction where things can go wrong, with two completely different developers maintaining the code. ## Enter Remult Remember when I said that when I encounter a challenge, my first course of action is to try and automate it away? I couldn’t fathom how in 2018 it still is not viable to be able to get the same code to run on the frontend and the backend. I started toying with this idea to see if I could truly make this possible, improve my own productivity (and hopefully for other developers too)––from validations, to typing, through authentication and authorization, all of the typical code that’s constantly being duplicated. ### The Remult Backstory Remult started as a side project, without a name, and with a completely different (albeit noble) purpose. My wife volunteered at a food bank, and as a result, I too was volunteered to participate in distributing food parcels to the needy. One day, as I was driving to distribute the parcels, holding a paper with a list of addresses I found myself getting lost in places you don’t want to get lost, and I knew I had to build them an app to help volunteers navigate efficiently. I knew I could solve a lot of friction in the process of delivering food parcels to the needy through code––which is what I do best, and I wanted to focus on building the actual application and its capabilities, and not the pieces that hold it together. So I built an application for inventory and distribution of our local food bank in Even Yehuda, an application they could use to generate distribution lists for volunteer couriers, and for the couriers to be able to navigate to and report back on delivery. I wrote the app, and at the same time the framework as well, the very framework I wanted to have when building web applications. One that would focus on the data flow from the backend database all the way through to the frontend framework (the framework of your choice––whether Angular, React, or Vue––it shouldn’t matter). ![Food Bank App](./introducing-remult-part-1/food-bank-app.jpg) Instead of having to go through the entire process described above of serializing objects for every HTTP call on the frontend, and then reversing the entire process back into JSON from the backend to the frontend––this framework now made it possible to query on the frontend, using objects, and then automated the entire process from the frontend to the backend and back again. I finally had the framework I dreamed of that eliminates the need to write all of this boilerplate, repetitive, duct tape code over and over again. With its growth and use, a colleague and I worked on the framework, invested in its ability to scale and its stability, improved its API that underwent several iterations, and added many features. The application built upon this frameworkIt was quickly adopted by other food banks around Israel who often encountered similar challenges with parcel delivery. Our application after its first year managed to help distribute 17,000 parcels from food banks around Israel. We were quite proud of this achievement––we started feeling like our framework could possibly withstand the test of scale, but we had no idea what was to come next.. ## What COVID Taught us About Scale and Security Then COVID hit––and lock downs cut people in need off from the entire world. Suddenly, the need to distribute food to the elderly and disabled skyrocketed. The demand grew from 17,000 parcels annually to 17,000 parcels a day. The app was then contributed for free to municipalities, NGOs and even the IDF’s Home Front to enable better inventory, allocation and distribution of parcels around Israel. Once the application was adopted by the IDF, it also underwent a battery of security testing––cyber and penetration testing, which leveled up its security significantly. The backend to frontend framework, and the application built upon it that was supposed to be just an experiment, withstood the scale of half a million parcel distributions in 2020 alone, and since then has maintained a similar number and is only growing. During COVID it was adopted by multiple countries around the globe––from Australia, to the EU, USA, and South Africa––to respond to similar needs during the pandemic. This is the backbone upon which Remult was built and battle tested, with all of this running on a $16-a-month Heroku server. Once the pandemic was behind us, my co-creator and I realized we had learned a lot. We understood the framework was robust and could scale, was aligned with security best practices, and delivered the promise of democratizing the ability to build full stack applications without all of the known friction. We wanted to share this with the world. So we open sourced the framework, to enable other lazy developers like us to be able to invest their energy in writing excellent applications that deliver value to users, and not on repeatable, mechanical code that apparently actually **can be automated** and shared by the backend and frontend. Check out [Remult](https://remult.dev), and if you like it, give it a star. Let us know what you’d like to see next, and also feel free to contribute to the project. In our next post, we’ll do a roundup of tools in the ecosystem looking to solve similar challenges though different approaches, and unpack where Remult fits in, and what it’s optimized for. # react - Tutorial - Setup # Build a Full-Stack React Application ### Create a simple todo app with Remult using a React frontend In this tutorial, we are going to create a simple app to manage a task list. We'll use `React` for the UI, `Node.js` + `Express.js` for the API server, and Remult as our full-stack CRUD framework. For deployment to production, we'll use [railway.app](https://railway.app/) and a `PostgreSQL` database. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ::: tip Prefer Angular? Check out the [Angular tutorial](../angular/). ::: ### Prefer an Interactive Online Learning Experience? If you'd rather follow along with an interactive, online tutorial, [try our interactive tutorial here](https://learn.remult.dev). It provides a hands-on, guided approach to building the same full-stack todo app with React and Remult. --- ### Prerequisites This tutorial assumes you are familiar with `TypeScript` and `React`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. # Setup for the Tutorial This tutorial requires setting up a React project, an API server project, and a few lines of code to add Remult. You can either **use a starter project** to speed things up, or go through the **step-by-step setup**. ## Option 1: Clone the Starter Project 1. Clone the _react-vite-express-starter_ repository from GitHub and install its dependencies. ```sh git clone https://github.com/remult/react-vite-express-starter.git remult-react-todo cd remult-react-todo npm install ``` 2. Open your IDE. 3. Open a terminal and run the `dev` npm script. ```sh npm run dev ``` 4. Open another terminal and run the `dev-node` npm script ```sh npm run dev-node ``` The default "Vite + React" app main screen should be available at the default Vite dev server address [http://127.0.0.1:5173](http://127.0.0.1:5173). At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. ## Option 2: Step-by-step Setup ### Create a React project using Vite Create the new React project. ```sh npm create -y vite@latest remult-react-todo -- --template react-ts cd remult-react-todo ``` ::: warning Run into issues scaffolding the Vite project? See [Vite documentation](https://vitejs.dev/guide/#scaffolding-your-first-vite-project) for help. ::: In this tutorial, we'll be using the root folder created by `Vite` as the root folder for our server project as well. ### Install required packages We need `Express` to serve our app's API, and, of course, `Remult`. For development, we'll use [tsx](https://www.npmjs.com/package/tsx) to run the API server. ```sh npm i express remult npm i --save-dev @types/express tsx ``` ### Create the API server project The starter API server TypeScript project contains a single module that initializes `Express`, and begins listening for API requests. 1. Open your IDE. 2. Create a `server` folder under the `src/` folder created by Vite. 3. Create an `index.ts` file in the `src/server/` folder with the following code: ```ts [index.ts] // src/server/index.ts import express from 'express' const app = express() app.listen(3002, () => console.log('Server started')) ``` ### Bootstrap Remult in the back-end Remult is loaded in the back-end as an `Express middleware`. 1. Create an `api.ts` file in the `src/server/` folder with the following code: ```ts // src/server/api.ts import { remultExpress } from 'remult/remult-express' export const api = remultExpress() ``` 2. Add the highlighted code lines to register the middleware in the main server module `index.ts`. ```ts{4,7} // src/server/index.ts import express from "express" import { api } from "./api.js" const app = express() app.use(api) app.listen(3002, () => console.log("Server started")) ``` ::: warning ESM In this tutorial we will be using `esm` for the node.js server - that means that where ever we import a file we have to include the `.js` suffix for it as we did above in the `import { api } from "./api.js` statement ::: ### Final tweaks Our full stack starter project is almost ready. Let's complete these final configurations. #### Enable TypeScript decorators in Vite Add the following entry to the `defineConfig` section of the `vite.config.ts` file to enable the use of decorators in the React app. ```ts{6-12} // vite.config.ts // ... export default defineConfig({ plugins: [react()], esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` #### Create the server tsconfig file In the root folder, create a TypeScript configuration file `tsconfig.server.json` for the server project. ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` #### Proxy API requests from Vue DevServer (vite) to the API server The react app created in this tutorial is intended to be served from the same domain as its API. However, for development, the API server will be listening on `http://localhost:3002`, while the react app is served from the default `http://localhost:5173`. We'll use the [proxy](https://vitejs.dev/config/#server-proxy) feature of Vite to divert all calls for `http://localhost:5173/api` to our dev API server. Configure the proxy by adding the following entry to the `vite.config.ts` file: ```ts{6} // vite.config.ts //... export default defineConfig({ plugins: [react()], server: { proxy: { "/api": "http://localhost:3002" } }, esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` ### Run the app 1. Open a terminal and start the vite dev server. ```sh npm run dev ``` 2. Add an `npm` script named `dev-node` to start the dev API server in the `package.json`. ```json // package.json "dev-node": "tsx watch --tsconfig tsconfig.server.json src/server" ``` 3. Open another terminal and start the `node` server ```sh npm run dev-node ``` The server is now running and listening on port 3002. `tsx` is watching for file changes and will restart the server when code changes are saved. The default "Vite + React" app main screen should be available at the default Vite dev server address [http://127.0.0.1:5173](http://127.0.0.1:5173). ### Remove React default styles The react default styles won't fit our todo app. If you'd like a nice-looking app, replace the contents of `src/index.css` with [this CSS file](https://raw.githubusercontent.com/remult/react-vite-express-starter/master/src/index.css). Otherwise, you can simply **delete the contents of `src/index.css`**. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # react - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between frontend and backend. 2. Create a file `Task.ts` in the `src/shared/` folder, with the following code: ```ts // src/shared/Task.ts import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.cuid() id = '' @Fields.string() title = '' @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` 3. In the server's `api` module, register the `Task` entity with Remult by adding `entities: [Task]` to an `options` object you pass to the `remultExpress()` middleware: ```ts{4,7} // src/server/api.ts import { remultExpress } from "remult/remult-express" import { Task } from "../shared/Task.js" export const api = remultExpress({ entities: [Task] }) ``` ::: warning ESM In this tutorial we will be using `esm` for the node.js server - that means that where ever we import a file we have to include the `.js` suffix for it as we did above in the `import { Task } from "../shared/Task.js"` statement ::: The [@Entity](../../docs/ref_entity.md) decorator tells Remult this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as a default database collection/table name), and an `options` argument used to define entity-related properties and operations, discussed in the next sections of this tutorial. To initially allow all CRUD operations for tasks, we set the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add a tasks. 1. Open a browser with the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - _Clean car_. ```sh curl http://localhost:3002/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks) and see that the array now contains one item. 4. Use `curl` to `POST` a few more tasks: ```sh curl http://localhost:3002/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` - Note that the `POST` endpoint can accept a single `Task` or an array of `Task`s. 5. Refresh the browser again, to see that the tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your React application by setting the `admin` option to `true` in the `remultExpress()` ::: code-group ```ts [src/server/api.ts] import { remultExpress } from 'remult/remult-express' import { Task } from '../shared/Task.js' export const api = remultExpress({ entities: [Task], admin: true, // Enable the Admin UI }) ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:5173/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks in a React component. Replace the contents of `src/App.tsx` with the following code: ```tsx // src/App.tsx import { useEffect, useState } from 'react' import { remult } from 'remult' import { Task } from './shared/Task' const taskRepo = remult.repo(Task) export default function App() { const [tasks, setTasks] = useState([]) useEffect(() => { taskRepo.find().then(setTasks) }, []) return (

Todos

{tasks.map((task) => { return (
{task.title}
) })}
) } ``` Here's a quick overview of the different parts of the code snippet: - `taskRepo` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `tasks` is a Task array React state to hold the list of tasks. - React's useEffect hook is used to call the Remult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server, once when the React component is loaded. After the browser refreshes, the list of tasks appears. # react - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. In the `useEffect` hook, pass an `options` argument to the `find` method call and set its `limit` property to 20. ```ts{6} // src/App.tsx useEffect(() => { taskRepo .find({ limit: 20 }) .then(setTasks) }, []) ``` There aren't enough tasks in the database for this change to have an immediate effect, but it will have one later on when we'll add more tasks. ::: tip To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. In the `useEffect` hook, set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ```ts{7} // src/App.tsx useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } }) .then(setTasks) }, []) ``` ## Server side Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust the `useEffect` hook to fetch only `completed` tasks. ```ts{8} // src/App.tsx useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, where: { completed: true } }) .then(setTasks) }, []) ``` ::: warning Note Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Settings the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```ts{8} // src/App.tsx useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true }, }) .then(setTasks) }, []) ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # react - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. Add the highlighted `newTaskTitle` state and `addTask` function the App Component ```ts{5-16} // src/App.tsx export default function App() { const [tasks, setTasks] = useState([]) const [newTaskTitle, setNewTaskTitle] = useState("") const addTask = async (e: FormEvent) => { e.preventDefault() try { const newTask = await taskRepo.insert({ title: newTaskTitle }) setTasks([...tasks, newTask]) setNewTaskTitle("") } catch (error: unknown) { alert((error as { message: string }).message) } } //... ``` - the call to `taskRepo.insert` will make a post request to the server, insert the new task to the `db`, and return the new `Task` object with all it's info (including the id generated by the database) ::: warning Import FormEvent This code requires adding an import of `FormEvent` from `react`. ::: Next let's adjust the `tsx` to display a form to add new tasks ```tsx{7-14} // src/App.tsx return (

Todos

setNewTaskTitle(e.target.value)} />
{tasks.map(task => { return (
{task.title}
) })}
) ``` Try adding a few tasks to see how it works ## Mark Tasks as completed Modify the contents of the `tasks.map` iteration within the `App` component to include the following `setCompleted` function and call it in the input's `onChange` event. ```tsx{5-6,8-9,16} // src/App.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) return (
setCompleted(e.target.checked)} /> {task.title}
) }) } ``` - The `setTask` function is used to replace the state of the changed task in the `tasks` array - The `taskRepo.save` method update the `task` to the server and returns the updated value ## Rename Tasks and Save them To make the tasks in the list updatable, we'll bind the `tasks` React state to `input` elements and add a _Save_ button to save the changes to the backend database. Modify the contents of the `tasks.map` iteration within the `App` component to include the following `setTitle` and `saveTask` functions and add an `input` and a save `button`. ```tsx{11,13-19,28-29} // src/App.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { setTask(await taskRepo.save(task)) } catch (error: unknown) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} />
) }) } ``` - The `setTitle` function, called from the `input`'s `onChange` event, saves the value from the `input` to the `tasks` state. - The `saveTask` function, called from the `button`'s' `onClick`event, saves the `task` object to the backend. Make some changes and refresh the browser to verify the backend database is updated. ::: tip Browser's Network tab As you play with these `CRUD` capabilities, monitor the network tab and see that they are all translated to `rest` api calls. ::: ## Delete Tasks Let's add a _Delete_ button next to the _Save_ button of each task in the list. Add the highlighted `deleteTask` function and _Delete_ `button` Within the `tasks.map` iteration in the `return` section of the `App` component. ```tsx{21-28,39} // src/App.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { setTask(await taskRepo.save(task)) } catch (error: unknown) { alert((error as { message: string }).message) } } const deleteTask = async () => { try { await taskRepo.delete(task) setTasks(tasks.filter(t => t !== task)) } catch (error: unknown) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} />
) }) } ``` # react - Tutorial - Validation # Validation Validating user entered data is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, [CRUD operations](crud.md) catch these exceptions, and alert the user. We leave it to you to decide how to handle validation errors in your application. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ```ts{3-5} // src/shared/Task.ts @Fields.string({ validate: Validators.required }) title = "" ``` ::: warning Import Validators This code requires adding an import of `Validators` from `remult`. ::: ::: warning Manual browser refresh required For this change to take effect, you **must manually refresh the browser**. ::: After the browser is refreshed, try creating a new `task` or saving an existing one with an empty title - the _"Should not be empty"_ error message is displayed. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:3002/api/tasks` API route, providing an invalid title. ```sh curl -i http://localhost:3002/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` An http error is returned and the validation error text is included in the response body, ## Custom Validation The `validate` property of the first argument of `Remult` field decorators can be set to an arrow function which will be called to validate input on both front-end and back-end. Try something like this and see what happens: ```ts // src/shared/Task.ts @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } }) title = "" ``` ::: warning Remove Validators Import This code no longer requires the `Validators` import from 'remult' and it should be removed ::: # react - Tutorial - Live Queries # Live Queries Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. Let's add realtime multiplayer capabilities to this app. ## Realtime updated todo list Let's switch from fetching Tasks once when the React component is loaded, and manually maintaining state for CRUD operations, to using a realtime updated live query subscription **for both initial data fetching and subsequent state changes**. 1. Modify the contents of the `useEffect` hook in the `App` component: ```ts{4-5,10} // src/App.tsx useEffect(() => { return taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true }, }) .subscribe(info => setTasks(info.applyChanges)) }, []) ``` Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` function which we use as a return value for the `useEffect` hook, so that it'll be called when the component unmounts. 2. As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we should remove the manual adding of new Tasks to the component's state: ```ts{7} // src/App.tsx const addTask = async (e: FormEvent) => { e.preventDefault() try { // const newTask = await taskRepo.insert({ title: newTaskTitle }) <- Delete this line await taskRepo.insert({ title: newTaskTitle }) // <- replace with this line // setTasks([...tasks, newTask]) <-- this line is no longer needed setNewTaskTitle("") } catch (error: unknown) { alert((error as { message: string }).message) } } ``` 3. Optionally remove other redundant state changing code: ```tsx{11-12,18-19,28} // src/App.tsx //... { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => // setTask(await taskRepo.save({ ...task, completed })) <- Delete this line await taskRepo.save({ ...task, completed }) // <- replace with this line const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { // setTask(await taskRepo.save(task)) <- Delete this line await taskRepo.save(task) // <- replace with this line } catch (error: unknown) { alert((error as { message: string }).message) } } const deleteTask = async () => { try { await taskRepo.delete(task) // setTasks(tasks.filter(t => t !== task)) <- Delete this line } catch (error: unknown) { alert((error as { message: string }).message) } } //... }) } ``` Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # react - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/completed Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async function to the `App` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```ts // src/App.tsx const setAllCompleted = async (completed: boolean) => { for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } ``` The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the return section of the `App` component, just before the closing `` tag. Both of the buttons' `onClick` events will call the `setAllCompleted` method with the appropriate value of the `completed` argument. ```tsx // src/App.tsx
``` Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor the `for` loop from the `setAllCompleted` function of the `App` function component into a new, `static`, `setAllCompleted` method in the `TasksController` class, which will run on the server. ```ts // src/shared/TasksController.ts import { BackendMethod, remult } from 'remult' import { Task } from './Task.js' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (the `allowed` property will be discussed later on in this tutorial). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register `TasksController` by adding it to the `controllers` array of the `options` object passed to `remultExpress()`, in the server's `api` module: ```ts{4,8} // src/server/api.ts //... import { TasksController } from "../shared/TasksController.js" export const api = remultExpress({ //... controllers: [TasksController] }) ``` 3. Replace the `for` iteration in the `setAllCompleted` function of the `App` component with a call to the `setAllCompleted` method in the `TasksController`. ```tsx{4} // src/App.tsx const setAllCompleted = async (completed: boolean) => { await TasksController.setAllCompleted(completed) } ``` ::: warning Import TasksController Remember to add an import of `TasksController` in `App.tsx`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser refreshed, the _"Set all..."_ buttons function exactly the same, but much faster. # react - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use `Express`'s [cookie-session](https://expressjs.com/en/resources/middleware/cookie-session.html) middleware to store an authenticated user's session within a cookie. The `user` property of the session will be set by the API server upon a successful simplistic sign-in (based on username without password). ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts{4} // src/shared/Task.ts @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ::: warning Import Allow This code requires adding an import of `Allow` from `remult`. ::: After the browser refreshes, **the list of tasks disappeared** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:3002/api/tasks ``` ::: ::: danger Authorized server-side code can still modify tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts // src/shared/TasksController.ts @BackendMethod({ allowed: Allow.authenticated }) ``` **This code requires adding an import of `Allow` from `remult`.** ::: ## User Authentication Let's add a sign-in area to the todo app, with an `input` for typing in a `username` and a sign-in `button`. The app will have two valid `username` values - _"Jane"_ and _"Steve"_. After a successful sign-in, the sign-in area will be replaced by a "Hi [username]" message. ### Backend setup 1. Open a terminal and run the following command to install the required packages: ```sh npm i cookie-session npm i --save-dev @types/cookie-session ``` 2. Modify the main server module `index.ts` to use the `cookie-session` Express middleware. ```ts{5,8-12} // src/server/index.ts //... import session from "cookie-session" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) //... ``` The `cookie-session` middleware stores session data, digitally signed using the value of the `secret` property, in an `httpOnly` cookie, sent by the browser to all subsequent API requests. 3. add a `shared/AuthController.ts` file and include the following code: ```ts add={4-6,8-12} // src/shared/AuthController.ts import { BackendMethod, remult } from 'remult' import type express from 'express' // eslint-disable-next-line @typescript-eslint/no-unused-vars import type from 'cookie-session' // required to access the session member of the request object declare module 'remult' { export interface RemultContext { request?: express.Request } } export class AuthController { // } ``` ### Code Explanation - We import the necessary modules from `remult` and types for `express` and `cookie-session`. - We extend the `RemultContext` interface to include an optional `request` property of type `express.Request`. - Remult will automatically set the `request` with the current request. Since Remult works with any server framework, we need to type it to the correct server, which in this case is Express. This typing gives us access to the request object and its session, managed by `cookie-session`. - This `request` can be accessed using `remult.context.request`. Next, we'll add a static list of users and a sign-in method. (In a real application, you would use a database, but for this tutorial, a static list will suffice.) ```ts add={1,4-17} const validUsers = [{ name: 'Jane' }, { name: 'Alex' }] export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name) if (user) { remult.user = { id: user.name, name: user.name, } remult.context.request!.session!['user'] = remult.user return remult.user } else { throw Error("Invalid user, try 'Alex' or 'Jane'") } } } ``` ### Code Explanation - We define a static list of valid users. - The `signIn` method is decorated with `@BackendMethod({ allowed: true })`, making it accessible from the frontend. - The method checks if the provided `name` exists in the `validUsers` list. If it does, it sets `remult.user` to an object that conforms to the `UserInfo` type from Remult and stores this user in the request session. - If the user is not found, it throws an error. Next, we'll add the sign-out method: ```ts add={7-11} export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { //... } @BackendMethod({ allowed: true }) static async signOut() { remult.context.request!.session!['user'] = undefined return undefined } } ``` - The `signOut` method clears the user session, making the user unauthenticated. 4. Update `remultExpress` configuration. ```ts{3,5,6} // src/server/api.ts import { AuthController } from '../shared/AuthController.js' export const api = remultExpress({ //... controllers: [TaskController, AuthController] getUser: (req) => req.session!['user'], }) ``` ### Code Explanation - Register the `AuthController` so that the frontend can call its `signIn` and `signOut` methods - `getUser` function: The getUser function is responsible for extracting the user information from the session. If a user is found in the session, Remult will treat the request as authenticated, and this user will be used for authorization purposes. ### Frontend setup 1. Create a file `src/Auth.tsx` and place the following `Auth` component code in it: ```ts // src/Auth.tsx import { FormEvent, useEffect, useState } from "react"; import { remult } from "remult"; import App from "./App"; import { AuthController } from "./shared/AuthController"; export default function Auth() { const [username, setUsername] = useState(""); const [signedIn, setSignedIn] = useState(false); async function signIn(e: FormEvent) { e.preventDefault(); try { remult.user = await AuthController.signIn(username); setSignedIn(true); } catch (error: unknown) { alert((error as { message: string }).message); } } async function signOut() { await AuthController.signOut(); remult.user = undefined; setSignedIn(false); } useEffect(() => { remult.initUser().then(() => { setSignedIn(remult.authenticated()); }); }, []); if (!signedIn) return ( <>

Todos

setUsername(e.target.value)} placeholder="Username, try Steve or Jane" />
); return ( <>
Hello {remult.user!.name}
); } ``` 2. In the `main.tsx` file, Replace the `App` component with the `Auth` component. ```ts{5,10} // src/main.tsx import React from "react" import ReactDOM from "react-dom/client" import Auth from "./Auth" import "./index.css" ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ) ``` The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. Let's define an `admin` role for our todo app, and enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts{7-8,18} // src/shared/Task.ts import { Allow, Entity, Fields, Validators } from "remult" @Entity("tasks", { allowApiCrud: Allow.authenticated, allowApiInsert: "admin", allowApiDelete: "admin" }) export class Task { @Fields.uuid() id!: string @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } allowApiUpdate: "admin" }) title = "" @Fields.boolean() completed = false } ``` 2. Let's give the user _"Jane"_ the `admin` role by modifying the `roles` array of her `validUsers` entry. ```ts{3,13} // src/shared/AuthController.ts const validUsers = [{ name: "Jane", admin: true }, { name: "Steve" }]; export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name); if (user) { remult.user = { id: user.name, name: user.name, roles: user.admin ? ["admin"] : [], }; remult.context.request!.session!["user"] = remult.user; return remult.user; } else { throw Error("Invalid user, try 'Steve' or 'Jane'"); } } ``` **Sign in to the app as _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert ```tsx{4,13} // src/App.tsx
{taskRepo.metadata.apiInsertAllowed() && (
setNewTaskTitle(e.target.value)} />
)} ...
``` And let's do the same for the `delete` button: ```tsx{12,14} // src/App.tsx return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} /> {taskRepo.metadata.apiDeleteAllowed(task) && ( )}
) ``` This way we can keep the frontend consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values. # react - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. **In production, we'd like to use a `Postgres` database table instead.** ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add the highlighted code to the `api` server module. ```ts{5,9-11} // src/server/api.ts //... import { createPostgresDataProvider } from "remult/postgres" export const api = remultExpress({ //... dataProvider: createPostgresDataProvider({ connectionString: "your connection string" }) }) ``` # react - Tutorial - Deployment # Deployment Let's deploy the todo app to [railway.app](https://railway.app/). ## Prepare for Production In this tutorial, we'll deploy both the React app and the API server as [one server-side app](https://create-react-app.dev/docs/deployment/#other-solutions), and redirect all non-API requests to return the React app. We will deploy an ESM node server project In addition, to follow a few basic production best practices, we'll use [compression](https://www.npmjs.com/package/compression) middleware to improve performance and [helmet](https://www.npmjs.com/package/helmet) middleware for security 1. Add the highlighted code lines to `src/server/index.ts`, and modify the `app.listen` function's `port` argument to prefer a port number provided by the production host's `PORT` environment variable. ```ts{16-21} // src/server/index.ts import express from "express" import { api } from "./api.js" import session from "cookie-session" import { auth } from "./auth.js" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) app.use(auth) app.use(api) const frontendFiles = process.cwd() + "/dist"; app.use(express.static(frontendFiles)); app.get("/*", (_, res) => { res.sendFile(frontendFiles + "/index.html"); }); app.listen(process.env["PORT"] || 3002, () => console.log("Server started")); ``` 3. Modify the highlighted code in the api server module to prefer a `connectionString` provided by the production host's `DATABASE_URL` environment variable. ```ts{4,7-9} // src/server/api.ts //... const DATABASE_URL = process.env["DATABASE_URL"]; export const api = remultExpress({ dataProvider: DATABASE_URL ? createPostgresDataProvider({ connectionString: DATABASE_URL }) : undefined, //... }) ``` ::: warning Note In order to connect to a local PostgresDB, add `DATABASE_URL` to an .env file, or simply replace `process.env["DATABASE_URL"]` with your `connectionString`. If no `DATABASE_URL` has found, it'll fallback to our local JSON files. ::: 4. Modify the project's `build` npm script to additionally transpile the API server's TypeScript code to JavaScript (using `tsc`). ```json // package.json "build": "tsc && vite build && tsc -p tsconfig.server.json", ``` 5. Modify the project's `start` npm script to start the production Node.js server. ```json // package.json "start": "node dist/server/" ``` The todo app is now ready for deployment to production. ## Test Locally To test the application locally run ```sh npm run build npm run start ``` ::: warning Build Errors If you get an error `error TS5096: Option 'allowImportingTsExtensions' can only be used when either 'noEmit' or 'emitDeclarationOnly' is set.` do not set the `emitDeclarationOnly` flag! You are getting the error because somewhere in your code you've imported from `.ts` instead of `.js` - fix it and build again ::: Now navigate to http://localhost:3002 and test the application locally ## Deploy to Railway In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Set a project name. 3. Once that's done run the following command to open the project on railway.dev: ```sh railway open ``` 4. Once that's done run the following command to upload the project to railway: ```sh railway up ``` 5. Add Postgres Database: 1. In the project on `railway.dev`, click `+ Create` 2. Select `Database` 3. Select `Add PostgresSQL` 6. Configure the environment variables 1. Click on the project card (not the Postgres one) 2. Switch to the `variables` tab 3. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 4. Add another variable called `SESSION_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 5. Switch to the `settings` tab 6. Under `Environment` click on `Generate Domain` 7. Click on the `Deploy` button on the top left. 7. Once the deployment is complete - 8. Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, play with it and enjoy. To see a larger more complex code base, visit our [CRM example project](https://www.github.com/remult/crm-demo) Love Remult?  Give our repo a star.⭐ # angular - Tutorial - Setup # Build a Full-Stack Angular Application ### Create a simple todo app with Remult using an Angular frontend In this tutorial, we are going to create a simple app to manage a task list. We'll use `Angular` for the UI, `Node.js` + `Express.js` for the API server, and Remult as our full-stack CRUD framework. For deployment to production, we'll use [railway.app](https://railway.app/) and a `PostgreSQL` database. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ::: tip Prefer React? Check out the [React tutorial](../react/). ::: ### Prerequisites This tutorial assumes you are familiar with `TypeScript` and `Angular`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. If Angular CLI is not already installed - then install it. ```sh npm i -g @angular/cli ``` # Setup for the Tutorial This tutorial requires setting up an Angular project, an API server project, and a few lines of code to add Remult. You can either **use a starter project** to speed things up, or go through the **step-by-step setup**. ## Option 1: Clone the Starter Project 1. Clone the _angular-express-starter_ repository from GitHub and install its dependencies. ```sh git clone https://github.com/remult/angular-express-starter.git remult-angular-todo cd remult-angular-todo npm install ``` 2. Open your IDE. 3. Open a terminal and run the `dev` npm script. ```sh npm run dev ``` The default Angular app main screen should be displayed. At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. ## Option 2: Step-by-step Setup ### Create an Angular project Create the new Angular project. ```sh ng new remult-angular-todo ``` ::: warning Note The `ng new` command prompts you for information about features to include in the initial app project. Accept the defaults by pressing the Enter or Return key. ::: In this tutorial, we'll be using the root folder created by `Angular` as the root folder for our server project as well. ```sh cd remult-angular-todo ``` ### Install required packages We need `Express` to serve our app's API, and, of course, `Remult`. For development, we'll use [tsx](https://www.npmjs.com/package/tsx) to run the API server. ```sh npm i express remult npm i --save-dev @types/express tsx ``` ### Create the API server project The starter API server TypeScript project contains a single module that initializes `Express`, and begins listening for API requests. 1. Open your IDE. 2. Add the following entry to the `compilerOptions` section of the `tsconfig.json` file to enable the use of Synthetic Default Imports. ```json{7-8} // tsconfig.json { ... "compilerOptions": { ... "allowSyntheticDefaultImports": true, ... } ... } ``` 2. Create a `server` folder under the `src/` folder created by Angular cli. 3. Create an `index.ts` file in the `src/server/` folder with the following code: ```ts // src/server/index.ts import express from 'express' const app = express() app.listen(3002, () => console.log('Server started')) ``` ### Bootstrap Remult in the back-end Remult is loaded in the back-end as an `Express middleware`. 1. Create an `api.ts` file in the `src/server/` folder with the following code: ```ts // src/server/api.ts import { remultExpress } from 'remult/remult-express' export const api = remultExpress() ``` 2. Add the highlighted code lines to register the middleware in the main server module `index.ts`. ```ts{4,7} // src/server/index.ts import express from "express" import { api } from "./api" const app = express() app.use(api) app.listen(3002, () => console.log("Server started")) ``` ### Final tweaks Our full stack starter project is almost ready. Let's complete these final configurations. #### Proxy API requests from Angular DevServer to the API server The Angular app created in this tutorial is intended to be served from the same domain as its API. However, for development, the API server will be listening on `http://localhost:3002`, while the Angular dev server is served from the default `http://localhost:4200`. We'll use the [proxy](https://angular.io/guide/build#proxying-to-a-backend-server) feature of Angular dev server to divert all calls for `http://localhost:4200/api` to our dev API server. Create a file `proxy.conf.json` in the root folder, with the following contents: ```json // proxy.conf.json { "/api": { "target": "http://localhost:3002", "secure": false } } ``` ### Run the app 1. Add a script called `dev` that will run the angular `dev` server with the proxy configuration we've set and a script called `dev-node` to run the api. ```json // package.json "scripts": { ... "dev": "ng serve --proxy-config proxy.conf.json --open", "dev-node": "tsx watch src/server", ... } ``` 1. Open a terminal and start the angular dev server. ```sh npm run dev ``` 3. Open another terminal and start the `node` server ```sh npm run dev-node ``` The server is now running and listening on port 3002. `tsx` is watching for file changes and will restart the server when code changes are saved. The default Angular app main screen should be displayed on the regular port - 4200. Open it in the browser at [http://localhost:4200/](http://localhost:4200/). ### Remove Angular default styles The angular default styles won't fit our todo app. If you'd like a nice-looking app, replace the contents of `src/styles.css` with [this CSS file](https://raw.githubusercontent.com/remult/angular-express-starter/master/src/styles.css). Otherwise, you can simply **delete the contents of `src/styles.css`**. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # angular - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between frontend and backend. 2. Create a file `Task.ts` in the `src/shared/` folder, with the following code: ```ts // src/shared/Task.ts import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.cuid() id = '' @Fields.string() title = '' @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` 3. In the server's `api` module, register the `Task` entity with Remult by adding `entities: [Task]` to an `options` object you pass to the `remultExpress()` middleware: ```ts{4,7} // src/server/api.ts import { remultExpress } from "remult/remult-express" import { Task } from "../shared/Task" export const api = remultExpress({ entities: [Task] }) ``` The [@Entity](../../docs/ref_entity.md) decorator tells Remult this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as a default database collection/table name), and an `options` argument used to define entity-related properties and operations, discussed in the next sections of this tutorial. To initially allow all CRUD operations for tasks, we set the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add a tasks. 1. Open a browser with the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - _Clean car_. ```sh curl http://localhost:3002/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks) and see that the array now contains one item. 4. Use `curl` to `POST` a few more tasks: ```sh curl http://localhost:3002/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` - Note that the `POST` endpoint can accept a single `Task` or an array of `Task`s. 5. Refresh the browser again, to see that the tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your Angular application by setting the `admin` option to `true` in the `remultExpress()` ::: code-group ```ts [src/server/api.ts] import { remultExpress } from 'remult/remult-express' import { Task } from '../shared/Task.js' export const api = remultExpress({ entities: [Task], admin: true, // Enable the Admin UI }) ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:3002/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks in an Angular component. 1. Create a `Todo` component using Angular's cli ```sh ng g c todo ``` 2. Import the new component in the `app.component.ts` ```ts{8,18} //src/app/app.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { TodoComponent } from './todo/todo.component'; @Component({ selector: 'app-root', standalone: true, imports: [ CommonModule, RouterOutlet, HttpClientModule, FormsModule, TodoComponent, ], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent { title = 'remult-angular-todo'; } ``` 3. Replace the `app.components.html` to use the `todo` component. ```html ``` 4. Add the highlighted code lines to the `TodoComponent` class file: ```ts{5-7,12,,17-21} // src/app/todo/todo.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { remult } from 'remult'; import { Task } from '../../shared/Task'; @Component({ selector: 'app-todo', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './todo.component.html', styleUrl: './todo.component.css', }) export class TodoComponent { taskRepo = remult.repo(Task); tasks: Task[] = []; ngOnInit() { this.taskRepo.find().then((items) => (this.tasks = items)); } } ``` Here's a quick overview of the different parts of the code snippet: - We've imported the `FormsModule` for angular's forms support - `taskRepo` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `tasks` is a Task array. - The `ngOnInit` method calls theRemult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server, once when the component is loaded. 5. Replace the contents of `todo.component.html` with the following HTML: ```html

todos

{{task.title}}
``` After the browser refreshes, the list of tasks appears. # angular - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. In the `ngOnInit` method, pass an `options` argument to the `find` method call and set its `limit` property to 20. ```ts{5} // src/app/todo/todo.component.ts ngOnInit() { this.taskRepo.find({ limit: 20 }).then((items) => (this.tasks = items)); } ``` There aren't enough tasks in the database for this change to have an immediate effect, but it will have one later on when we'll add more tasks. ::: tip To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. In the `ngOnInit` method, set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ```ts{6} // src/app/todo/todo.component.ts ngOnInit() { this.taskRepo.find({ limit: 20, orderBy: { createdAt:"asc" } }).then((items) => (this.tasks = items)); } ``` ## Server side Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust the `ngOnInit` method to fetch only `completed` tasks. ```ts{7} // src/app/todo/todo.component.ts ngOnInit() { this.taskRepo.find({ limit: 20, orderBy: { createdAt:"asc" }, where: { completed: true } }).then((items) => (this.tasks = items)); } ``` ::: warning Note Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Settings the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```ts{5} ngOnInit() { this.taskRepo.find({ limit: 20, orderBy: { createdAt:"asc" }, //where: { completed: true } }).then((items) => (this.tasks = items)); } ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # angular - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. 1. Add a `newTaskTitle` field and an `addTask` method to the `ToDoComponent` ```ts{4-13} // src/app/todo/todo.component.ts export class TodoComponent implements OnInit { newTaskTitle = "" async addTask() { try { const newTask = await this.taskRepo.insert({ title: this.newTaskTitle }) this.tasks.push(newTask) this.newTaskTitle = "" } catch (error: any) { alert(error.message) } } } ``` - the call to `taskRepo.insert` will make a post request to the server, insert the new task to the `db`, and return the new `Task` object with all it's info (including the id generated by the database) 2. Add an _Add Task_ form in the html template: ```html{5-12}

todos

{{ task.title }}
``` Try adding a few tasks to see how it works ## Rename Tasks and Mark as Completed To make the tasks in the list updatable, we'll bind the `input` elements to the `Task` properties and add a _Save_ button to save the changes to the backend database. 1. Add a `saveTask` method to save the state of a task to the backend database ```ts // src/app/todo/todo.component.ts async saveTask(task: Task) { try { await this.taskRepo.save(task) } catch (error: any) { alert(error.message) } } ``` 2) Modify the contents of the `tasks` div to include the following `input` elements and a _Save_ button to call the `saveTask` method. ```html{4-10}
``` Make some changes and refresh the browser to verify the backend database is updated. ## Delete Tasks Let's add a _Delete_ button next to the _Save_ button of each task in the list. 1. Add the following `deleteTask` method to the `TodoComponent` class: ```ts // src/app/todo/todo.component.ts async deleteTask(task: Task) { await this.taskRepo.delete(task); this.tasks = this.tasks.filter(t => t !== task); } ``` 2. Add a _Delete_ button in the html: ```html{11}
``` # angular - Tutorial - Validation # Validation Validating user entered data is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, [CRUD operations](crud.md) catch these exceptions, and alert the user. We leave it to you to decide how to handle validation errors in your application. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ```ts{3-5} // src/shared/Task.ts @Fields.string({ validate: Validators.required }) title = "" ``` ::: warning Import Validators This code requires adding an import of `Validators` from `remult`. ::: ::: warning Manual browser refresh required For this change to take effect, you **must manually refresh the browser**. ::: After the browser is refreshed, try creating a new `task` or saving an existing one with an empty title - the _"Should not be empty"_ error message is displayed. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:3002/api/tasks` API route, providing an invalid title. ```sh curl -i http://localhost:3002/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` An http error is returned and the validation error text is included in the response body, ## Custom Validation The `validate` property of the first argument of `Remult` field decorators can be set to an arrow function which will be called to validate input on both front-end and back-end. Try something like this and see what happens: ```ts // src/shared/Task.ts @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } }) title = "" ``` # angular - Tutorial - Live Queries # Live Queries Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. Let's add realtime multiplayer capabilities to this app. ## One Time Setup We'll need angular to run it's change detection when we receive messages from the backend - to do that we'll add the following code to `AppComponent` ```ts{3-5,7-9} // src/app/app.component.ts import { Component, NgZone } from '@angular/core'; import { remult } from "remult" //... export class AppComponent { constructor(zone: NgZone) { remult.apiClient.wrapMessageHandling = handler => zone.run(() => handler()) } } ``` ## Realtime updated todo list Let's switch from fetching Tasks once when the Angular component is loaded, and manually maintaining state for CRUD operations, to using a realtime updated live query subscription **for both initial data fetching and subsequent state changes**. 1. Modify the contents of the `ngOnInit` method in the `Todo` component: Modify the `TodoComponent` with the following changes ```ts{3,5,9,11-12,17,19-21} // src/app/todo/todo.component.ts import { Component, OnDestroy, OnInit } from '@angular/core'; ... export class TodoComponent implements OnInit, OnDestroy { //... taskRepo = remult.repo(Task) tasks: Task[] = [] unsubscribe = () => {} ngOnInit() { this.unsubscribe = this.taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true }, }) .subscribe(info => (this.tasks = info.applyChanges(this.tasks))) } ngOnDestroy() { this.unsubscribe() } } ``` Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` method, which we store in the `unsubscribe` member and call in the `ngOnDestroy` hook, so that it'll be called when the component unmounts. 2. As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we should remove the manual adding of new Tasks to the component's state: ```ts{6} // src/app/todo/todo.component.ts async addTask() { try { const newTask = await this.taskRepo.insert({ title: this.newTaskTitle }) //this.tasks.push(newTask) <-- this line is no longer needed this.newTaskTitle = "" } catch (error: any) { alert(error.message) } } ``` 3. Optionally remove other redundant state changing code: ```ts{5} // src/app/todo/todo.component.ts async deleteTask(task: Task) { await this.taskRepo.delete(task); // this.tasks = this.tasks.filter(t => t !== task); <-- this line is no longer needed } ``` Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # angular - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/completed Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async method to the `TodoComponent` class, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```ts // src/app/todo/todo.component.ts async setAllCompleted(completed: boolean) { for (const task of await this.taskRepo.find()) { await this.taskRepo.save({ ...task, completed }); } } ``` The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the `TodoComponent` just before the closing `` tag. Both of the buttons' `click` events will call the `setAllCompleted` method with the appropriate value of the `completed` argument. ```html
``` Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor the `for` loop from the `setAllCompleted` method of the `TodoComponent`into a new, `static`, `setAllCompleted` method in the `TasksController` class, which will run on the server. ```ts // src/shared/TasksController.ts import { BackendMethod, remult } from 'remult' import { Task } from './Task' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (the `allowed` property will be discussed later on in this tutorial). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register `TasksController` by adding it to the `controllers` array of the `options` object passed to `remultExpress()`, in the server's `api` module: ```ts{4,8} // src/server/api.ts //... import { TasksController } from "../shared/TasksController" export const api = remultExpress({ //... controllers: [TasksController] }) ``` 3. Replace the `for` iteration in the `setAllCompleted` function of the `App` component with a call to the `setAllCompleted` method in the `TasksController`. ```ts{4} // src/app/todo/todo.component.ts async setAllCompleted(completed: boolean) { await TasksController.setAllCompleted(completed); } ``` ::: warning Import TasksController Remember to add an import of `TasksController` in `todo.component.ts`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser refreshed, the _"Set all..."_ buttons function exactly the same, but much faster. # angular - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use `Express`'s [cookie-session](https://expressjs.com/en/resources/middleware/cookie-session.html) middleware to store an authenticated user's session within a cookie. The `user` property of the session will be set by the API server upon a successful simplistic sign-in (based on username without password). ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts{4} // src/shared/Task.ts @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ::: warning Import Allow This code requires adding an import of `Allow` from `remult`. ::: After the browser refreshes, **the list of tasks disappeared** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:3002/api/tasks ``` ::: ::: danger Authorized server-side code can still modify tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts // src/shared/TasksController.ts @BackendMethod({ allowed: Allow.authenticated }) ``` **This code requires adding an import of `Allow` from `remult`.** ::: ## User Authentication Let's add a sign-in area to the todo app, with an `input` for typing in a `username` and a sign-in `button`. The app will have two valid `username` values - _"Jane"_ and _"Steve"_. After a successful sign-in, the sign-in area will be replaced by a "Hi [username]" message. ### Backend setup 1. Open a terminal and run the following command to install the required packages: ```sh npm i cookie-session npm i --save-dev @types/cookie-session ``` 2. Modify the main server module `index.ts` to use the `cookie-session` Express middleware. ```ts{5,8-12} // src/server/index.ts //... import session from "cookie-session" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) //... ``` The `cookie-session` middleware stores session data, digitally signed using the value of the `secret` property, in an `httpOnly` cookie, sent by the browser to all subsequent API requests. 3. add a `shared/AuthController.ts` file and include the following code: ```ts add={4-6,8-12} // src/shared/AuthController.ts import { BackendMethod, remult } from 'remult' import type express from 'express' // eslint-disable-next-line @typescript-eslint/no-unused-vars import type from 'cookie-session' // required to access the session member of the request object declare module 'remult' { export interface RemultContext { request?: express.Request } } export class AuthController { // } ``` ### Code Explanation - We import the necessary modules from `remult` and types for `express` and `cookie-session`. - We extend the `RemultContext` interface to include an optional `request` property of type `express.Request`. - Remult will automatically set the `request` with the current request. Since Remult works with any server framework, we need to type it to the correct server, which in this case is Express. This typing gives us access to the request object and its session, managed by `cookie-session`. - This `request` can be accessed using `remult.context.request`. Next, we'll add a static list of users and a sign-in method. (In a real application, you would use a database, but for this tutorial, a static list will suffice.) ```ts add={1,4-17} const validUsers = [{ name: 'Jane' }, { name: 'Alex' }] export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name) if (user) { remult.user = { id: user.name, name: user.name, } remult.context.request!.session!['user'] = remult.user return remult.user } else { throw Error("Invalid user, try 'Alex' or 'Jane'") } } } ``` ### Code Explanation - We define a static list of valid users. - The `signIn` method is decorated with `@BackendMethod({ allowed: true })`, making it accessible from the frontend. - The method checks if the provided `name` exists in the `validUsers` list. If it does, it sets `remult.user` to an object that conforms to the `UserInfo` type from Remult and stores this user in the request session. - If the user is not found, it throws an error. Next, we'll add the sign-out method: ```ts add={7-11} export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { //... } @BackendMethod({ allowed: true }) static async signOut() { remult.context.request!.session!['user'] = undefined return undefined } } ``` - The `signOut` method clears the user session, making the user unauthenticated. 4. Update `remultExpress` configuration. ```ts{3,5,6} // src/server/api.ts import { AuthController } from '../shared/AuthController.js' export const api = remultExpress({ //... controllers: [TaskController, AuthController] getUser: (req) => req.session!['user'], }) ``` ### Code Explanation - Register the `AuthController` so that the frontend can call its `signIn` and `signOut` methods - `getUser` function: The getUser function is responsible for extracting the user information from the session. If a user is found in the session, Remult will treat the request as authenticated, and this user will be used for authorization purposes. ### Frontend setup 1. Create an `Auth` component using Angular's cli ```sh ng g c auth ``` 2. Add the highlighted code lines to the `AuthComponent` class file: ```ts // src/app/auth/auth.component.ts import { Component, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { UserInfo, remult } from 'remult' import { HttpClient, HttpClientModule } from '@angular/common/http' import { FormsModule } from '@angular/forms' import { TodoComponent } from '../todo/todo.component' @Component({ selector: 'app-auth', standalone: true, imports: [CommonModule, FormsModule, TodoComponent, HttpClientModule], templateUrl: './auth.component.html', styleUrl: './auth.component.css', }) export class AuthComponent implements OnInit { signInUsername = '' remult = remult async signIn() { try { remult.user = await AuthController.signIn(this.signInUsername) } catch (error: unknown) { alert((error as { message: string }).message) } } async signOut() { await AuthController.signOut() remult.user = undefined } ngOnInit() { remult.initUser() } } ``` 3. Replace the contents of auth.component.html with the following html: ```html

todos

Hello {{ remult.user?.name }}
``` 4. Replace the `TodoComponent` with the `AuthComponent` in the `AppComponent` ```ts{6,12} //src/app/app.component.ts import { Component, NgZone } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { AuthComponent } from './auth/auth.component'; import { remult } from 'remult'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterOutlet, AuthComponent], templateUrl: './app.component.html', styleUrl: './app.component.css', }) ``` 5. Change the `app.component.html` to use the `AuthComponent` instead of the `TodoComponent` ```html ``` The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. Let's define an `admin` role for our todo app, and enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts{7-8,18} // src/shared/Task.ts import { Allow, Entity, Fields, Validators } from "remult" @Entity("tasks", { allowApiCrud: Allow.authenticated, allowApiInsert: "admin", allowApiDelete: "admin" }) export class Task { @Fields.uuid() id!: string @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } allowApiUpdate: "admin" }) title = "" @Fields.boolean() completed = false } ``` 2. Let's give the user _"Jane"_ the `admin` role by modifying the `roles` array of her `validUsers` entry. ```ts{3,13} // src/shared/AuthController.ts const validUsers = [{ name: "Jane", admin: true }, { name: "Steve" }]; export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name); if (user) { remult.user = { id: user.name, name: user.name, roles: user.admin ? ["admin"] : [], }; remult.context.request!.session!["user"] = remult.user; return remult.user; } else { throw Error("Invalid user, try 'Steve' or 'Jane'"); } } ``` **Sign in to the app as _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. Modify the contents of todo.component.html to only display the form and delete buttons if these operations are allowed based on the entity's metadata: ```html{5,22}

todos

``` This way we can keep the frontend consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values, # angular - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. **In production, we'd like to use a `Postgres` database table instead.** ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add the highlighted code to the `api` server module. ```ts{5,9-11} // src/server/api.ts //... import { createPostgresDataProvider } from "remult/postgres" export const api = remultExpress({ //... dataProvider: createPostgresDataProvider({ connectionString: "your connection string" }) }) ``` # angular - Tutorial - Deployment # Deployment Let's deploy the todo app to [railway.app](https://railway.app/). ## Prepare for Production In this tutorial, we'll deploy both the Angular app and the API server as [one server-side app](https://create-react-app.dev/docs/deployment/#other-solutions), and redirect all non-API requests to return the Angular app. In addition, to follow a few basic production best practices, we'll use [compression](https://www.npmjs.com/package/compression) middleware to improve performance and [helmet](https://www.npmjs.com/package/helmet) middleware for security 1. Add the highlighted code lines to `src/server/index.ts`, and modify the `app.listen` function's `port` argument to prefer a port number provided by the production host's `PORT` environment variable. ```ts{16-21} // src/server/index.ts import express from "express" import { api } from "./api.js" import session from "cookie-session" import { auth } from "./auth.js" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) app.use(auth) app.use(api) const frontendFiles = process.cwd() + "/dist/remult-angular-todo/browser"; app.use(express.static(frontendFiles)); app.get("/*", (_, res) => { res.sendFile(frontendFiles + "/index.html"); }); app.listen(process.env["PORT"] || 3002, () => console.log("Server started")); ``` ::: warning Angular versions <17 If you're using angular version 16 or less, the result path is: `'/dist/remult-angular-todo/browser` ::: 3. Modify the highlighted code in the api server module to prefer a `connectionString` provided by the production host's `DATABASE_URL` environment variable. ```ts{4,7-9} // src/server/api.ts //... const DATABASE_URL = process.env["DATABASE_URL"]; export const api = remultExpress({ dataProvider: DATABASE_URL ? createPostgresDataProvider({ connectionString: DATABASE_URL }) : undefined, //... }) ``` ::: warning Note In order to connect to a local PostgresDB, add `DATABASE_URL` to an .env file, or simply replace `process.env["DATABASE_URL"]` with your `connectionString`. If no `DATABASE_URL` has found, it'll fallback to our local JSON files. ::: 4. In the root folder, create a TypeScript configuration file `tsconfig.server.json` for the build of the server project using TypeScript. ```json // tsconfig.server.json { "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", "esModuleInterop": true, "noEmit": false, "outDir": "dist", "skipLibCheck": true, "rootDir": "src" }, "include": ["src/server/index.ts"] } ``` 5. Modify the project's `build` npm script to additionally transpile the API server's TypeScript code to JavaScript (using `tsc`). ```json // package.json "build": "ng build && tsc -p tsconfig.server.json" ``` 6. Modify the project's `start` npm script to start the production Node.js server. ```json // package.json "start": "node dist/server/" ``` The todo app is now ready for deployment to production. ## Test Locally To test the application locally run ```sh npm run build npm run start ``` Now navigate to http://localhost:3002 and test the application locally ## Deploy to Railway In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Set a project name. 3. Once that's done run the following command to open the project on railway.dev: ```sh railway open ``` 4. Once that's done run the following command to upload the project to railway: ```sh railway up ``` 5. Add Postgres Database: 1. In the project on `railway.dev`, click `+ Create` 2. Select `Database` 3. Select `Add PostgresSQL` 6. Configure the environment variables 1. Click on the project card (not the Postgres one) 2. Switch to the `variables` tab 3. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 4. Add another variable called `SESSION_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 5. Switch to the `settings` tab 6. Under `Environment` click on `Generate Domain` 7. Click on the `Deploy` button on the top left. 7. Once the deployment is complete - 8. Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, play with it and enjoy. Love Remult?  Give our repo a star.⭐ # angular - Tutorial - Appendix: Observable Live Query # Appendix - Observable Live Query To use `liveQuery` as an observable add the following utility function to your code ```ts // src/app/from-live-query.ts import { LiveQuery } from 'remult' import { Observable } from 'rxjs' export function fromLiveQuery(q: LiveQuery) { return new Observable((sub) => q.subscribe(({ items }) => sub.next(items)), ) } ``` 1. Adjust the `TodoComponent` ```ts{4,6-11} // src/app/todo/todo.component.ts ... export class TodoComponent { taskRepo = remult.repo(Task); tasks$ = fromLiveQuery( this.taskRepo.liveQuery({ limit: 20, orderBy: { createdAt: 'asc' }, }) ); ``` Note that we've removed `ngOnInit` and `ngOnDestroy` as they are no longer needed 2. Adjust the `todo.component.html` ```html{3}
``` # vue - Tutorial - Setup # Build a Full-Stack Vue Application ### Create a simple todo app with Remult using a Vue frontend In this tutorial, we are going to create a simple app to manage a task list. We'll use `Vue` for the UI, `Node.js` + `Express.js` for the API server, and Remult as our full-stack CRUD framework. For deployment to production, we'll use [railway.app](https://railway.app/) and a `PostgreSQL` database. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ::: tip Prefer React? Check out the [React tutorial](../react/). ::: ### Prerequisites This tutorial assumes you are familiar with `TypeScript` and `Vue`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. # Setup for the Tutorial This tutorial requires setting up a Vue project, an API server project, and a few lines of code to add Remult. You can either **use a starter project** to speed things up, or go through the **step-by-step setup**. ## Option 1: Clone the Starter Project 1. Clone the _vue-express-starter_ repository from GitHub and install its dependencies. ```sh git clone https://github.com/remult/vue-express-starter.git remult-vue-todo cd remult-vue-todo npm install ``` 2. Open your IDE. 3. Open a terminal and run the `dev` npm script. ```sh npm run dev ``` 4. Open another terminal and run the `dev-node` npm script ```sh npm run dev-node ``` The default "Vue" app main screen should be available at the default Vite dev server address [http://127.0.0.1:5173](http://127.0.0.1:5173). At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. ## Option 2: Step-by-step Setup ### Create a Vue project Create the new Vue project. ```sh npm init -y vue@latest ``` The command command prompts you for information about features to include in the initial app project. Here are the answers used in this tutorial: 1. Project name: ... **remult-vue-todo** 2. Add Typescript? ... **Yes** 3. For the rest of the answers, simply select the default. ::: warning Run into issues scaffolding the Vite project? See [Vite documentation](https://vitejs.dev/guide/#scaffolding-your-first-vite-project) for help. ::: Once completed, run: ```sh cd remult-vue-todo ``` In this tutorial, we'll be using the root folder created by `Vue` as the root folder for our server project as well. ### Install required packages We need `Express` to serve our app's API, and, of course, `Remult`. For development, we'll use [tsx](https://www.npmjs.com/package/tsx) to run the API server. ```sh npm i express remult npm i --save-dev @types/express tsx ``` ### Create the API server project The starter API server TypeScript project contains a single module that initializes `Express`, and begins listening for API requests. 1. Open your IDE. 2. Create a `server` folder under the `src/` folder created by Vite. 3. Create an `index.ts` file in the `src/server/` folder with the following code: ```ts // src/server/index.ts import express from 'express' const app = express() app.listen(3002, () => console.log('Server started')) ``` ### Bootstrap Remult in the back-end Remult is loaded in the back-end as an `Express middleware`. 1. Create an `api.ts` file in the `src/server/` folder with the following code: ```ts // src/server/api.ts import { remultExpress } from 'remult/remult-express' export const api = remultExpress() ``` 2. Add the highlighted code lines to register the middleware in the main server module `index.ts`. ```ts{4,7} // src/server/index.ts import express from "express" import { api } from "./api.js" const app = express() app.use(api) app.listen(3002, () => console.log("Server started")) ``` ::: warning ESM In this tutorial we will be using `esm` for the node.js server - that means that where ever we import a file we have to include the `.js` suffix for it as we did above in the `import { api } from "./api.js` statement ::: ### Final tweaks Our full stack starter project is almost ready. Let's complete these final configurations. #### Enable TypeScript decorators in Vite Add the following entry to the `defineConfig` section of the `vite.config.ts` file to enable the use of decorators in the Vue app. ```ts{6-12} // vite.config.ts // ... export default defineConfig({ plugins: [vue()], esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` #### Create the server tsconfig file In the root folder, create a TypeScript configuration file `tsconfig.server.json` for the server project. ```json { "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", "module": "nodenext" }, "include": ["src/server/**/*", "src/shared/**/*"] } ``` #### Proxy API requests from Vue DevServer (vite) to the API server The Vue app created in this tutorial is intended to be served from the same domain as its API. However, for development, the API server will be listening on `http://localhost:3002`, while the Vue app is served from the default `http://localhost:5173`. We'll use the [proxy](https://vitejs.dev/config/#server-proxy) feature of Vite to divert all calls for `http://localhost:5173/api` to our dev API server. Configure the proxy by adding the following entry to the `vite.config.ts` file: ```ts{6} // vite.config.ts //... export default defineConfig({ plugins: [vue()], server: { proxy: { "/api": "http://localhost:3002" } }, esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, }, }, }, }); ``` ### Run the app 1. Open a terminal and start the vite dev server. ```sh npm run dev ``` 2. Add an `npm` script named `dev-node` to start the dev API server in the `package.json`. ```json // package.json "dev-node": "tsx watch --tsconfig tsconfig.server.json src/server" ``` 3. Open another terminal and start the `node` server ```sh npm run dev-node ``` The server is now running and listening on port 3002. `tsx` is watching for file changes and will restart the server when code changes are saved. The default "Vue" app main screen should be available at the default Vite dev server address [http://127.0.0.1:5173](http://127.0.0.1:5173). ### Remove Vue default styles The vue default styles won't fit our todo app. If you'd like a nice-looking app, replace the contents of `src/assets/main.css` with [this CSS file](https://raw.githubusercontent.com/remult/vue-express-starter/master/src/assets/main.css). Otherwise, you can simply **delete the contents of `src/assets/main.css`**. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # vue - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between frontend and backend. 2. Create a file `Task.ts` in the `src/shared/` folder, with the following code: ```ts // src/shared/Task.ts import { Entity, Fields } from "remult" @Entity("tasks", { allowApiCrud: true }) export class Task { @Fields.cuid() id = "" @Fields.string() title = "" @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` 3. In the server's `api` module, register the `Task` entity with Remult by adding `entities: [Task]` to an `options` object you pass to the `remultExpress()` middleware: ```ts{4,7} // src/server/api.ts import { remultExpress } from "remult/remult-express" import { Task } from "../shared/Task.js" export const api = remultExpress({ entities: [Task] }) ``` ::: warning ESM In this tutorial we will be using `esm` for the node.js server - that means that where ever we import a file we have to include the `.js` suffix for it as we did above in the `import { Task } from "../shared/Task.js"` statement ::: The [@Entity](../../docs/ref_entity.md) decorator tells Remult this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as a default database collection/table name), and an `options` argument used to define entity-related properties and operations, discussed in the next sections of this tutorial. To initially allow all CRUD operations for tasks, we set the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add a tasks. 1. Open a browser with the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - *Clean car*. ```sh curl http://localhost:3002/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:3002/api/tasks](http://localhost:3002/api/tasks) and see that the array now contains one item. 4. Use `curl` to `POST` a few more tasks: ```sh curl http://localhost:3002/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` - Note that the `POST` endpoint can accept a single `Task` or an array of `Task`s. 5. Refresh the browser again, to see that the tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your React application by setting the `admin` option to `true` in the `remultExpress()` ::: code-group ```ts [src/server/api.ts] import { remultExpress } from 'remult/remult-express' import { Task } from '../shared/Task.js' export const api = remultExpress({ entities: [Task], admin: true, // Enable the Admin UI }) ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:5173/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks in a Vue component. Replace the contents of `src/App.vue` with the following code: ```vue // src/App.vue ``` Here's a quick overview of the different parts of the code snippet: - `taskRepo` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `tasks` is a Task array Vue `ref` that holds the list of tasks. - Vue's `onMounted` hook is used to call the Remult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server, once when the Vue component is loaded. After the browser refreshes, the list of tasks appears. # vue - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. In the `onMounted` hook, pass an `options` argument to the `find` method call and set its `limit` property to 20. ```ts{6} // src/App.vue onMounted(() => taskRepo .find({ limit: 20 }) .then(items => (tasks.value = items)) ) ``` There aren't enough tasks in the database for this change to have an immediate effect, but it will have one later on when we'll add more tasks. ::: tip To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. In the `onMounted` hook, set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ```ts{7} // src/App.vue onMounted(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } }) .then(items => (tasks.value = items)) ) ``` ## Server side Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust the `onMounted` hook to fetch only `completed` tasks. ```ts{8} // src/App.vue onMounted(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, where: { completed: true } }) .then(items => (tasks.value = items)) ) ``` ::: warning Note Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Settings the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```ts{6} onMounted(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true } }) .then(items => (tasks.value = items)) ) ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # vue - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. Add the highlighted `newTaskTitle` ref and `addTask` function, and the relevant `
` to the App Component ```vue{5-14,20-23} // src/App.vue ``` - the call to `taskRepo.insert` will make a post request to the server, insert the new task to the `db`, and return the new `Task` object with all it's info (including the id generated by the database) Try adding a few tasks to see how it works ## Rename Tasks and Mark as Completed To make the tasks in the list updatable, we'll bind the `input` elements to the `Task` properties and add a _Save_ button to save the changes to the backend database. ```vue{5-11,16-18} // src/App.vue ``` - The `taskRepo.save` method update the `task` to the server and returns the updated value - - The `saveTask` function, called from the `button`'s `click` event, and the `checkbox`'s change event saves the `task` object to the backend. Make some changes and refresh the browser to verify the backend database is updated. ::: tip Browser's Network tab As you play with these `CRUD` capabilities, monitor the network tab and see that they are all translated to `rest` api calls. ::: ## Delete Tasks Let's add a _Delete_ button next to the _Save_ button of each task in the list. Add the highlighted `deleteTask` function and _Delete_ `button` ```vue{5-12,20} // src/App.vue ``` # vue - Tutorial - Validation # Validation Validating user entered data is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, [CRUD operations](crud.md) catch these exceptions, and alert the user. We leave it to you to decide how to handle validation errors in your application. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ```ts{3-5} // src/shared/Task.ts @Fields.string({ validate: Validators.required }) title = "" ``` ::: warning Import Validators This code requires adding an import of `Validators` from `remult`. ::: ::: warning Manual browser refresh required For this change to take effect, you **must manually refresh the browser**. ::: After the browser is refreshed, try creating a new `task` or saving an existing one with an empty title - the _"Should not be empty"_ error message is displayed. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:3002/api/tasks` API route, providing an invalid title. ```sh curl -i http://localhost:3002/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` An http error is returned and the validation error text is included in the response body, ## Custom Validation The `validate` property of the first argument of `Remult` field decorators can be set to an arrow function which will be called to validate input on both front-end and back-end. Try something like this and see what happens: ```ts // src/shared/Task.ts @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } }) title = "" ``` # vue - Tutorial - Live Queries # Live Queries Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. Let's add realtime multiplayer capabilities to this app. ## Realtime updated todo list Let's switch from fetching Tasks once when the Vue component is loaded, and manually maintaining state for CRUD operations, to using a realtime updated live query subscription **for both initial data fetching and subsequent state changes**. Adjust the `onMounted` hook in the `App.vue` file ```ts{4,6,11} // src/App.vue onMounted(() => onUnmounted( taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true }, }) .subscribe(info => (tasks.value = info.applyChanges(tasks.value))) ) ) ``` Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` function, we return it to the `onUnmounted` hook so that it'll be called when the component unmounts. 2. As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we should remove the manual adding of new Tasks to the component's state: ```ts{6} // src/App.vue async function addTask() { try { const newTask = await taskRepo.insert({ title: newTaskTitle.value }) //tasks.value.push(newTask) <-- this line is no longer needed newTaskTitle.value = "" } catch (error: unknown) { alert((error as { message: string }).message) } } ``` 3. Optionally remove other redundant state changing code: ```ts{6} // src/App.vue async function deleteTask(task: Task) { try { await taskRepo.delete(task) //tasks.value = tasks.value.filter((t) => task !== t); <-- this line is no longer needed } catch (error: unknown) { alert((error as { message: string }).message) } } ``` Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # vue - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/completed Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async function to the `App` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```ts // src/App.vue async function setAllCompleted(completed: boolean) { for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } ``` The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the end of the `` section of the `App` component. Both of the buttons' `@click` events will call the `setAllCompleted` function with the appropriate value of the `completed` argument. ```vue // src/App.vue
``` Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor the `for` loop from the `setAllCompleted` function of the `App` function component into a new, `static`, `setAllCompleted` method in the `TasksController` class, which will run on the server. ```ts // src/shared/TasksController.ts import { BackendMethod, remult } from "remult" import { Task } from "./Task.js" export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (the `allowed` property will be discussed later on in this tutorial). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register `TasksController` by adding it to the `controllers` array of the `options` object passed to `remultExpress()`, in the server's `api` module: ```ts{4,8} // src/server/api.ts //... import { TasksController } from "../shared/TasksController.js" export const api = remultExpress({ //... controllers: [TasksController] }) ``` 3. Replace the `for` iteration in the `setAllCompleted` function of the `App` component with a call to the `setAllCompleted` method in the `TasksController`. ```ts{4} // src/App.vue async function setAllCompleted(completed: boolean) { await TasksController.setAllCompleted(completed) } ``` ::: warning Import TasksController Remember to add an import of `TasksController` in `App.vue`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser refreshed, the _"Set all..."_ buttons function exactly the same, but much faster. # vue - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use `Express`'s [cookie-session](https://expressjs.com/en/resources/middleware/cookie-session.html) middleware to store an authenticated user's session within a cookie. The `user` property of the session will be set by the API server upon a successful simplistic sign-in (based on username without password). ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts{4} // src/shared/Task.ts @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ::: warning Import Allow This code requires adding an import of `Allow` from `remult`. ::: After the browser refreshes, **the list of tasks disappeared** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:3002/api/tasks ``` ::: ::: danger Authorized server-side code can still modify tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts // src/shared/TasksController.ts @BackendMethod({ allowed: Allow.authenticated }) ``` **This code requires adding an import of `Allow` from `remult`.** ::: ## User Authentication Let's add a sign-in area to the todo app, with an `input` for typing in a `username` and a sign-in `button`. The app will have two valid `username` values - _"Jane"_ and _"Steve"_. After a successful sign-in, the sign-in area will be replaced by a "Hi [username]" message. ### Backend setup 1. Open a terminal and run the following command to install the required packages: ```sh npm i cookie-session npm i --save-dev @types/cookie-session ``` 2. Modify the main server module `index.ts` to use the `cookie-session` Express middleware. ```ts{5,8-12} // src/server/index.ts //... import session from "cookie-session" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) //... ``` The `cookie-session` middleware stores session data, digitally signed using the value of the `secret` property, in an `httpOnly` cookie, sent by the browser to all subsequent API requests. 3. add a `shared/AuthController.ts` file and include the following code: ```ts add={4-6,8-12} // src/shared/AuthController.ts import { BackendMethod, remult } from 'remult' import type express from 'express' // eslint-disable-next-line @typescript-eslint/no-unused-vars import type from 'cookie-session' // required to access the session member of the request object declare module 'remult' { export interface RemultContext { request?: express.Request } } export class AuthController { // } ``` ### Code Explanation - We import the necessary modules from `remult` and types for `express` and `cookie-session`. - We extend the `RemultContext` interface to include an optional `request` property of type `express.Request`. - Remult will automatically set the `request` with the current request. Since Remult works with any server framework, we need to type it to the correct server, which in this case is Express. This typing gives us access to the request object and its session, managed by `cookie-session`. - This `request` can be accessed using `remult.context.request`. Next, we'll add a static list of users and a sign-in method. (In a real application, you would use a database, but for this tutorial, a static list will suffice.) ```ts add={1,4-17} const validUsers = [{ name: 'Jane' }, { name: 'Alex' }] export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name) if (user) { remult.user = { id: user.name, name: user.name, } remult.context.request!.session!['user'] = remult.user return remult.user } else { throw Error("Invalid user, try 'Alex' or 'Jane'") } } } ``` ### Code Explanation - We define a static list of valid users. - The `signIn` method is decorated with `@BackendMethod({ allowed: true })`, making it accessible from the frontend. - The method checks if the provided `name` exists in the `validUsers` list. If it does, it sets `remult.user` to an object that conforms to the `UserInfo` type from Remult and stores this user in the request session. - If the user is not found, it throws an error. Next, we'll add the sign-out method: ```ts add={7-11} export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { //... } @BackendMethod({ allowed: true }) static async signOut() { remult.context.request!.session!['user'] = undefined return undefined } } ``` - The `signOut` method clears the user session, making the user unauthenticated. 4. Update `remultExpress` configuration. ```ts{3,5,6} // src/server/api.ts import { AuthController } from '../shared/AuthController.js' export const api = remultExpress({ //... controllers: [TaskController, AuthController] getUser: (req) => req.session!['user'], }) ``` ### Code Explanation - Register the `AuthController` so that the frontend can call its `signIn` and `signOut` methods - `getUser` function: The getUser function is responsible for extracting the user information from the session. If a user is found in the session, Remult will treat the request as authenticated, and this user will be used for authorization purposes. ### Frontend setup 1. Create a file `src/Auth.vue` and place the following `Auth` component code in it: ```vue // src/Auth.vue ``` 2. In the `main.vue` file, change the `rootComponent` to `Auth`. ```ts{4,8} // src/main.ts import { createApp } from "vue" import Auth from "./Auth.vue" import "./assets/main.css" createApp(Auth).mount("#app") ``` The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. Let's define an `admin` role for our todo app, and enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts{7-8,18} // src/shared/Task.ts import { Allow, Entity, Fields, Validators } from "remult" @Entity("tasks", { allowApiCrud: Allow.authenticated, allowApiInsert: "admin", allowApiDelete: "admin" }) export class Task { @Fields.uuid() id!: string @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } allowApiUpdate: "admin" }) title = "" @Fields.boolean() completed = false } ``` 2. Let's give the user _"Jane"_ the `admin` role by modifying the `roles` array of her `validUsers` entry. ```ts{3,13} // src/shared/AuthController.ts const validUsers = [{ name: "Jane", admin: true }, { name: "Steve" }]; export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name); if (user) { remult.user = { id: user.name, name: user.name, roles: user.admin ? ["admin"] : [], }; remult.context.request!.session!["user"] = remult.user; return remult.user; } else { throw Error("Invalid user, try 'Steve' or 'Jane'"); } } ``` **Sign in to the app as _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert ```vue{8,23} // src/App.vue ``` This way we can keep the frontend consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values. # vue - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. **In production, we'd like to use a `Postgres` database table instead.** ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add the highlighted code to the `api` server module. ```ts{5,9-11} // src/server/api.ts //... import { createPostgresDataProvider } from "remult/postgres" export const api = remultExpress({ //... dataProvider: createPostgresDataProvider({ connectionString: "your connection string" }) }) ``` # vue - Tutorial - Deployment # Deployment Let's deploy the todo app to [railway.app](https://railway.app/). ## Prepare for Production In this tutorial, we'll deploy both the Vue app and the API server as [one server-side app](https://create-react-app.dev/docs/deployment/#other-solutions), and redirect all non-API requests to return the Vue app. We will deploy an ESM node server project In addition, to follow a few basic production best practices, we'll use [compression](https://www.npmjs.com/package/compression) middleware to improve performance and [helmet](https://www.npmjs.com/package/helmet) middleware for security 1. Add the highlighted code lines to `src/server/index.ts`, and modify the `app.listen` function's `port` argument to prefer a port number provided by the production host's `PORT` environment variable. ```ts{16-21} // src/server/index.ts import express from "express" import { api } from "./api.js" import session from "cookie-session" import { auth } from "./auth.js" const app = express() app.use( session({ secret: process.env["SESSION_SECRET"] || "my secret" }) ) app.use(auth) app.use(api) const frontendFiles = process.cwd() + "/dist"; app.use(express.static(frontendFiles)); app.get("/*", (_, res) => { res.sendFile(frontendFiles + "/index.html"); }); app.listen(process.env["PORT"] || 3002, () => console.log("Server started")); ``` 3. Modify the highlighted code in the api server module to prefer a `connectionString` provided by the production host's `DATABASE_URL` environment variable. ```ts{4,7-9} // src/server/api.ts //... const DATABASE_URL = process.env["DATABASE_URL"]; export const api = remultExpress({ dataProvider: DATABASE_URL ? createPostgresDataProvider({ connectionString: DATABASE_URL }) : undefined, //... }) ``` ::: warning Note In order to connect to a local PostgresDB, add `DATABASE_URL` to an .env file, or simply replace `process.env["DATABASE_URL"]` with your `connectionString`. If no `DATABASE_URL` has found, it'll fallback to our local JSON files. ::: 4. Modify the project's `build` npm script to additionally transpile the API server's TypeScript code to JavaScript (using `tsc`). ```json // package.json "build": "run-p type-check \"build-only {@}\" -- && tsc -p tsconfig.server.json" ``` 5. Add `start` npm script to start the production Node.js server. ```json // package.json "start": "node dist/server/" ``` The todo app is now ready for deployment to production. ## Test Locally To test the application locally run ```sh npm run build npm run start ``` ::: warning Build Errors If you get an error `error TS5096: Option 'allowImportingTsExtensions' can only be used when either 'noEmit' or 'emitDeclarationOnly' is set.` do not set the `emitDeclarationOnly` flag! You are getting the error because somewhere in your code you've imported from `.ts` instead of `.js` - fix it and build again ::: Now navigate to http://localhost:3002 and test the application locally ## Deploy to Railway In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Set a project name. 3. Once that's done run the following command to open the project on railway.dev: ```sh railway open ``` 4. Once that's done run the following command to upload the project to railway: ```sh railway up ``` 5. Add Postgres Database: 1. In the project on `railway.dev`, click `+ Create` 2. Select `Database` 3. Select `Add PostgresSQL` 6. Configure the environment variables 1. Click on the project card (not the Postgres one) 2. Switch to the `variables` tab 3. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 4. Add another variable called `SESSION_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 5. Switch to the `settings` tab 6. Under `Environment` click on `Generate Domain` 7. Click on the `Deploy` button on the top left. 7. Once the deployment is complete - 8. Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, play with it and enjoy. To see a larger more complex code base, visit our [CRM example project](https://www.github.com/remult/crm-demo) Love Remult?  Give our repo a star.⭐ # SvelteKit - Tutorial - Setup # Build a Full-Stack SvelteKit Application ### Create a simple todo app with Remult using a SvelteKit In this tutorial, we are going to create a simple app to manage a task list. We'll use `SvelteKit` for the UI & the backend and Remult as our full-stack CRUD framework. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ::: tip You want to have a look at the end result ? You can `degit` the final result and read the `README.md` file in the project to check it out. ```sh npx degit remult/remult/examples/sveltekit-todo remult-sveltekit-todo cd remult-sveltekit-todo ``` ::: ### Prerequisites This tutorial assumes you are familiar with `SvelteKit`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. # Setup for the Tutorial This tutorial requires setting up a Sveltekit project, and a few lines of code to add Remult. ## Step-by-step Setup ### Create a Sveltekit project Create the new Sveltekit project. ```sh npx sv@latest create remult-sveltekit-todo ``` The command prompts you for information about features to include in the initial app project. Here are the answers used in this tutorial: 1. **Which Svelte app template?**: ... `minimal` Project 2. **Add type checking with TypeScript?** ... Yes, using `TypeScript` syntax 3. **Select additional options**: ... We didn't select anything for this tutorial. Feel free to adapt it to your needs. 4. **Which package manager?**: ... We took `npm`, if you perfer others, feel free. Once completed, change to the app directory: ```sh cd remult-sveltekit-todo ``` ### Install required packages and Remult ```sh npm i remult --save-dev ``` ### Bootstrap Remult 1. Open your IDE. 2. Create your remult `api` ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' export const api = remultSveltekit({}) ``` ::: 3. Create a remult `api route` ::: code-group ```ts [src/routes/api/[...remult]/+server.ts] import { api } from '../../../server/api' export const { GET, POST, PUT, DELETE } = api ``` ::: ### Final tweaks Our full stack starter project is almost ready. Remult makes use of decorators to enhance regular Typescript classes into entities. Add the following entry to the `compilerOptions` section of the `tsconfig.json` file to enable the use of decorators. ::: code-group ```json [tsconfig.json] { "compilerOptions": { "experimentalDecorators": true // [!code ++] } } ``` ::: ### Run the app Open a terminal and start the vite dev server. ```sh npm run dev ``` The default "Sveltekit" app main screen should be available at the default Vite dev server address http://localhost:5173. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # SvelteKit - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between the frontend and the backend. 2. Create a file `Task.ts` in the `src/shared/` folder, with the following code: ::: code-group ```ts [src/shared/Task.ts] import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.cuid() id!: string @Fields.string() title: string = '' @Fields.boolean() completed: boolean = false @Fields.createdAt() createdAt?: Date } ``` ::: The [@Entity](../../docs/ref_entity.md) decorator tells Remult that this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as the default database collection/table name), and an optional `options` object of type `EntityOptions`. This is used to define entity-related properties and operations, discussed in the next sections of this tutorial. Initially, we are going to allow all CRUD operations on tasks, by setting the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: 3. Register the `Task` entity with Remult by adding `entities: [Task]` to the `options` object that is passed to the remult hook: ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' import { Task } from '../shared/Task' // [!code ++] export const api = remultSveltekit({ entities: [Task], // [!code ++] }) ``` ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add tasks. By default Remult exposes the `/api/` endpoint. Resources (entities) can then be accessed by appending the entity's `key` -- _tasks_ in this case. 1. Open a browser with the url: [http://localhost:5173/api/tasks](http://localhost:5173/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - _Clean car_. If you prefer, you can use a graphical tool such as Postman, Insomnia or Thunder Client. ```sh curl http://localhost:5173/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:5173/api/tasks](http://localhost:5173/api/tasks) and notice that the array now contains one item. 4. The `POST` endpoint can accept a single `Task` or an array of `Task`s. Add a few more tasks: ```sh curl http://localhost:5173/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` 5. Refresh the browser again, to see that the new tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start off by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your Sveltekit application by setting the `admin` option to `true` in the `remultSveltekit()` ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' import { Task } from '../shared/Task' export const api = remultSveltekit({ entities: [Task], admin: true, // Enable the Admin UI }) ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:5173/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks. Let's do it simply in the root of the app by adding this code in `+page.svelte`: ::: code-group ```svelte [src/routes/+page.svelte]

todos

{#each tasks as task}
{task.title}
{/each}
``` ::: Here's a quick overview of the different parts of the code snippet: - ` remult.repo(Task)` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `$effect` is used to call the Remult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server, once when the component is loaded. After the browser refreshes, the list of tasks appears. ### Styling the Output Remult is un-opinionated in as far as front-end styling is concerned. To demonstrate, let's style our app using vanilla CSS. Simply create these 2 files: ::: code-group ```svelte [src/routes/+layout.svelte] Remult+Sveltekit Todo App {@render children?.()} ``` ```css [src/app.css] @charset "utf-8"; body { font-family: Arial; background-color: whitesmoke; justify-content: center; margin: 0; } h1 { color: #ef4444; font-style: italic; font-size: 3.75rem; font-weight: inherit; text-align: center; } main { max-width: 500px; min-width: 300px; margin: auto; background-color: white; box-sizing: border-box; border: 1px solid lightgray; border-radius: 0.5rem; box-shadow: 0 2px 4px #0003, 0 25px 50px #0000001a; } main > div, main > form { padding: 0.5rem 1rem; border-bottom: 1px solid lightgray; display: flex; align-items: center; gap: 0.25rem; justify-content: space-between; } main > div:has(input[type='checkbox']) { justify-content: inherit; } input { font-family: inherit; font-size: 100%; width: 100%; border: 0; padding: 0.5rem; } input:checked + input, input:checked + span { text-decoration: line-through; } input:placeholder-shown { font-style: italic; } input[type='checkbox'] { width: 36px; height: 36px; height: 1.5rem; } button { cursor: pointer; padding: 0.5rem 0.5rem; background-color: white; font-family: inherit; font-size: 85%; line-height: inherit; border: 2px solid #0000001a; border-radius: 0.5rem; } ``` ::: ...and voila!, our app should look much better!! Feel free to improve or substitute the styling as you deem fit. ::: tip The styles imported into `src/routes/+layout.svelte` will apply to all pages in the app - unless explicitly overriden. ::: # SvelteKit - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. To do so, simply pass a `limit` option to the `find` method call: ::: code-group ```svelte [src/routes/+page.svelte] $effect(() => { repo(Task) .find( { limit: 20 } // [!code ++] ) .then((t) => (tasks = t)); }); ``` ::: Depending on the number of tasks that you have added, you may not have enough tasks in the database for this change to have an immediate visible effect, but it will have one later on when we add more tasks. ::: tip Using `limit` only returns the first page of data. To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. Set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ::: code-group ```svelte [src/routes/+page.svelte] $effect(() => { repo(Task) .find({ limit: 20, orderBy: { createdAt: "asc" } // [!code ++] }) .then((t) => (tasks = t)); }); ``` ::: ## Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust your function to fetch only `completed` tasks. ::: code-group ```svelte [src/routes/+page.svelte] $effect(() => { repo(Task) .find({ limit: 20, orderBy: { createdAt: "asc" }, where: { completed: true } // [!code ++] }) .then((t) => (tasks = t)); }); ``` ::: ::: warning NOTE: Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Settings the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```svelte [src/routes/+page.svelte] {6} $effect(() => { repo(Task) .find({ limit: 20, orderBy: { createdAt: "asc" } // where: { completed: true } }) .then((t) => (tasks = t)); }); ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # SvelteKit - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. We create a form which executes the `addTask` function that invokes `taskRepo.insert()`. Update your `+page.svelte` as follows: ::: code-group ```svelte [src/routes/+page.svelte]

todos

// [!code ++] // [!code ++] // [!code ++] // [!code ++] {#each tasks as task}
{task.title}
{/each}
``` ::: The call to `insert` will make a post request to the server, insert the new task to the db, and return the new Task object with all it's info (including the id generated by the database) Try adding a few tasks to see how it works. ## Mark Tasks as Completed 1. Add a `setCompleted` function in the script section as follows: ```ts const setCompleted = async (task: Task, completed: boolean) => { await repo(Task).save({ ...task, completed }) } ``` 2. Modify the checkbox to invoke the method: ```svelte
setCompleted(task, e.currentTarget.checked)} /> {task.title}
``` ## Rename Tasks To make the tasks in the list updatable, we'll use an `input` element and bind it to the task's `title` property. We'll also add a _Save_ button to commit the changes to the backend database. 1. Add a `saveTask` function in the script section as follows: ```ts const saveTask = async (e: Event, task: Task) => { e.preventDefault() await repo(Task).save({ ...task }) } ``` 2. Update the html part ```svelte {#each tasks as task}
setCompleted(task, e.currentTarget.checked)} />
{/each} ``` The `saveTask` function saves the task that is passed in. Since the task's title is bound to the `input`, changes are made directly to the task. Make some changes and refresh the browser to verify that the backend database is updated. ::: tip Browser's Network tab As you play with these `CRUD` capabilities, monitor the network tab and see that they are all translated to `rest` api calls. ::: ## Delete Tasks Let's add a _Delete_ button next to the **Save** button of each task in the list. 1. Add the `deleteTask` function ```ts const deleteTask = async (e: Event, task: Task) => { e.preventDefault() await repo(Task).delete(task) tasks = tasks.filter((c) => c.id !== task.id) } ``` 2. Add the **Delete** button ```svelte {#each tasks as task}
setCompleted(task, e.currentTarget.checked)} /> // [!code ++]
{/each} ``` # SvelteKit - Tutorial - Validation # Validation Validating user input is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, we will catch these exceptions, and alert the user. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ::: code-group ```ts [src/shared/Task.ts] import { Validators } from 'remult'; @Fields.string({ validate: Validators.required }) title: string = ''; ``` ::: 2. In `+page.svelte`, sorround the `addTask` in a `try-catch` block to capture the error: ::: code-group ```svelte [src/routes/+page.svelte] let newTaskTitle = $state(""); const addTask = async (event: Event) => { event.preventDefault(); try {// [!code ++] const newTask = await repo(Task).insert({ title: newTaskTitle }); tasks = [...tasks, newTask]; newTaskTitle = ""; } catch (error) {// [!code ++] alert((error as { message: string }).message);// [!code ++] }// [!code ++] }; ``` ::: After the browser is refreshed, try creating a new task or saving an existing one with an empty title - the "**Title: Should not be empty**" error message is displayed. Sorround all the other functions in `try-catch` in a similar manner and notify the user accordingly. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:5173/api/tasks` endpoint, providing an invalid title. ```sh curl -i http://localhost:5173/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` A HTTP **400 Bad Request** error is returned and the validation error text is included in the body: ```ts { modelState: { title: 'Should not be empty' }, message: 'Title: Should not be empty' } ``` ::: tip You should probably update all your code to handle these errors gracefully with `try-catch` blocks. ::: ## Custom Validation Remult accords you the ability to easly create your own validation rules. The `validate` property allows an arrow function which accepts an instance of the entity to be validated. This function will be called to validate input on both front-end and back-end. Try something like this and see what happens: ::: code-group ```ts [src/shared/Task.ts] @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "The title must be at least 3 characters long" } }) title = "" ``` ::: # SvelteKit - Tutorial - Live Queries # Live Queries Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh their browser. Let's add realtime multiplayer capabilities to this app. ## Realtime updated todo list Let's update our component like follows _(make sure you add and remove some lines as indicated)_ ::: code-group ```svelte [/src/routes/+page.svelte]

todos

{#each tasks as task}
setCompleted(task, e.currentTarget.checked)} />
{/each}
``` ::: Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` function, by just returning in the `$effect` function we automatically unsubscribe when the component unmounts. _(You can also do it manually in a `onDestroy` hook)_ - As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we removed the manual adding/removing of new Tasks to the component's state. Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # SvelteKit - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/complete Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async function to `+page.svelte` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ::: code-group ```svelte [src/routes/+page.svelte] ``` ::: The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the end of the `` section of the markup. Both of the buttons' `on:click` events will call the `setAllCompleted` function with the appropriate value of the `completed` argument. ::: code-group ```svelte [src/routes/+page.svelte]
``` ::: Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved pushes a `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. You can verify this on the Network tab of your browser's Developer Tools. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor into a new, `static`, `setAllCompleted` method in the `TasksController` class, which will run on the server. ::: code-group ```ts [src/shared/TasksController.ts] import { BackendMethod, remult } from 'remult' import { Task } from './Task' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.update(task.id, { completed }) } } } ``` ::: The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (`/api/setAllCompleted`) and allow CRUD operations on this end-point (`{allowed: true}` - more details to follow). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register the new `TasksController` class by adding it to the `controllers` array of the `options` object passed to `remultSveltekit()`: ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' import { Task } from '../shared/Task' import { TasksController } from '../shared/TasksController' // [!code ++] export const api = remultSveltekit({ admin: true, entities: [Task], // [!code ++] controllers: [TasksController], // [!code ++] }) ``` ::: 3.Replace the for iteration in the `setAllCompleted` function of with a call to the `setAllCompleted` method in the `TasksController`. ::: code-group ```ts [src/routes/+page.svelte] const setAllCompleted = async (completed: boolean) => { // for (const task of await repo(Task).find()) { // [!code --] // await repo(Task).save({ ...task, completed });// [!code --] // }// [!code --] await TasksController.setAllCompleted(completed) // [!code ++] } ``` ::: ::: warning Import TasksController Remember to add an import of `TasksController` in `+page.svelte`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser is refreshed, the _"Set all..."_ buttons function exactly the same but now makes only a single request to the back, and is faster. # SvelteKit - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism. The only requirement is that you provide Remult with an object which implements the Remult `UserInfo` interface: ```ts export interface UserInfo { id: string name?: string roles?: string[] } ``` In this tutorial, we'll use [Auth.js](https://authjs.dev/) for authentication. ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ::: code-group ```ts [src/app/shared/Task.ts] import { Allow } from 'remult'// [!code ++] @Entity("tasks", { allowApiCrud: Allow.authenticated // [!code ++] }) ``` After the browser refreshes, **the list of tasks disappears** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:5173/api/tasks ``` ::: ### Server-side Authorization Open your database (`db/tasks.json`), and click on **Mark All Completed** and **Mark All Incomplete** buttons in turn. You will notice that the `completed` field is toggling. Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ::: code-group ```ts [src/shared/TasksController.ts] import { Allow } from 'remult' @BackendMethod({ allowed: Allow.authenticated }) ``` ::: Try toggling the `completed` field and you will notice that we now require to be authenticated - even on the backend. ## User Authentication Let's set-up `Auth.js` to authenticate users to our app. ### Backend setup 1. Install `auth-core` and `auth-sveltekit`: ```sh npm i @auth/core @auth/sveltekit -D ``` 2. `Auth.js` requires a "secret" - a random string used to hash tokens, sign cookies and generate cryptographic keys. Create a file called `.env.local` at the root of the project, and set the secret `AUTH_SECRET` to a random string. ::: code-group ```bash [.env.local] AUTH_SECRET=something-secret ``` :::tip You can use an [online UUID generator](https://www.uuidgenerator.net/) to generate a completely random string ::: 3. In `+hooks.server.ts`, let's create two handles - `handleAuth` to handle authentication from `Auth.js` with a list of allowed users. - `handleRemult` to provide the remult context. Using Sveltekit's `sequence`, we ensure the chain of handles. The results would look like this: ::: code-group ```ts [src/hooks.server.ts] import type { Handle } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { SvelteKitAuth } from '@auth/sveltekit' import Credentials from '@auth/sveltekit/providers/credentials' import { api as handleRemult } from './server/api' import type { UserInfo } from 'remult' /** * Users that are allowed to log in. */ const validUsers: UserInfo[] = [ { id: '1', name: 'Jane', roles: ['admin'] }, { id: '2', name: 'Steve' }, ] /** * Handle authentication with authjs as an example * Based on article at https://authjs.dev/reference/sveltekit */ export const { handle: handleAuth } = SvelteKitAuth({ trustHost: true, providers: [ Credentials({ credentials: { name: { placeholder: 'Try Steve or Jane', }, }, authorize: (info) => validUsers.find((user) => user.name === info?.name) || null, }), ], callbacks: { session: ({ session, token }) => ({ ...session, user: validUsers.find((user) => user.id === token?.sub), }), }, }) export const handle = sequence( // 1. Handle authentication handleAuth, // 2. Handle remult server side handleRemult, ) ``` ::: This (very) simplistic approach use Auth.js [Credentials Provider](https://next-auth.js.org/providers/credentials) to authorize users by looking up the user's name in a predefined list of valid users. We've configured the `session` `callback` to include the user info as part of the session data, so that Remult on the frontend will have the authorization info. 4. Finally, add `getUser` to `remultSveltekit` to tell remult who is connected. Inside this function, you have access to `event`, where the session was set by Auth.js before. ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' import { Task } from '../shared/Task' import { TasksController } from '../shared/TasksController' import type { UserInfo } from 'remult' // [!code ++] export const api = remultSveltekit({ admin: true, entities: [Task], controllers: [TasksController], getUser: async (event) => { const auth = await event?.locals?.auth() // [!code ++] return auth?.user as UserInfo // [!code ++] }, }) ``` ::: ### Frontend setup 1. Create a new `+layout.server.ts` to update `remult.user` ::: code-group ```ts [src/routes/+layout.server.ts] import { remult } from 'remult' import type { LayoutServerLoad } from './$types' import { redirect } from '@sveltejs/kit' // will protect every route in the app export const load = (async () => { if (!remult.authenticated()) { throw redirect(303, '/auth/signin') } return { user: remult.user, } }) satisfies LayoutServerLoad ``` ::: 2. In our front-end (`+layout.svelte`), update the user globally. ::: code-group ```svelte [src/routes/+layout.ts] Remult+Sveltekit Todo App {@render children?.()} ``` ::: The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. You will notice that our `UserInfo` contains a `roles` array. Information contained in this array can be used to enforce role-based authorization. For our todo app we need to enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to enforce the three authorization rules above. ::: code-group ```ts [src/shared/Task.ts] import { Allow, Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: Allow.authenticated, allowApiInsert: 'admin', allowApiDelete: 'admin', }) export class Task { @Fields.cuid() id!: string @Fields.string({ validate: (task) => { if (task.title.length < 3) throw 'The title must be at least 3 characters long' }, allowApiUpdate: 'admin', }) title: string = '' @Fields.boolean() completed: boolean = false @Fields.createdAt() completedAt: Date = new Date() } ``` ::: In our list of users - `usersDB`; we have defined two users - Jane and Steve; with Jane being assigned an `admin` role. **Sign in to the app alternating between _"Jane"_ and _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert ::: code-group ```svelte [src/routes/+page.svelte]
{#if repo(Task).metadata.apiInsertAllowed()}// [!code ++]
{/if}// [!code ++]
``` ::: And let's do the same for the `delete` button: ::: code-group ```svelte [src/routes/+page.svelte]
setCompleted(task, e.currentTarget.checked)} /> {#if repo(Task).metadata.apiDeleteAllowed(task)} // [!code ++] {/if}// [!code ++]
``` ::: This way we can keep the UI consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values. # SvelteKit - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. In production, you will often want to use a proper database. Remult supports a (long) list of relational and non-relational databases. In this tutorial, let's use `Postgres`. ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article to find out more. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add an environment variables called DATABASE_URL and set it with your connection string: ::: code-group ```sh [.env.local] DATABASE_URL=postgresql://username:password@host:port/dbname[?paramspec] ``` ::: 3. Add a `dataProvider` to Remult's handler. ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' import { Task } from './shared/Task' import { TasksController } from './shared/TasksController' import { createPostgresDataProvider } from 'remult/postgres' // [!code ++] import { DATABASE_URL } from '$env/static/private' // [!code ++] export const api = remultSveltekit({ entities: [Task], controllers: [TasksController], dataProvider: DATABASE_URL // [!code ++] ? createPostgresDataProvider({ connectionString: DATABASE_URL }) // [!code ++] : undefined, // [!code ++] getUser: async (event) => { const auth = await event?.locals?.auth() return auth?.user as UserInfo }, }) ``` ::: Once the application restarts, it'll try to use postgres as the data source for your application. If `DATABASE_URL` is found, it'll automatically create the `tasks` table for you. If `DATABASE_URL` is not has found, it'll just fallback to our local JSON files. ::: tip You can also disable this automatic migration behavior. It's not part of this tutorial so if you want to learn more, follow this [link](/docs/migrations). ::: # SvelteKit - Tutorial - Deployment # Deployment Let's deploy the todo app to [railway.app](https://railway.app/). ## Prepare for Production In order to deploy to a Node.js environment, you need to change Sveltekit's adaptor to `@sveltejs/adapter-node`. 1. Install `adapter-node`: ```sh npm i @sveltejs/adapter-node --save-dev ``` 2. In `svelte.config.js`, change the adapter: ::: code-group ```js [svelte.config.js] import adapter from '@sveltejs/adapter-auto' // [!code --] import adapter from '@sveltejs/adapter-node' // [!code ++] ``` ::: In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Select `Empty Project` 3. Set a project name. 4. Once it's done add a database by running the following command: ```sh railway add ``` 5. Select `postgressql` as the database. 6. Once that's done run the following command to upload the project to railway: ```sh railway up ``` 7. got to the `railway` project's site and click on the project 8. Switch to the `settings` tab 9. Under `Environment` click on `Generate Domain` 10. Copy the `generated url`, you'll need it for [NEXTAUTH_URL](https://next-auth.js.org/configuration/options#nextauth_url) on step 14 11. Switch to the `variables` tab 12. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 13. Add another variable called `AUTH_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 14. Add another variable called `NEXTAUTH_URL` and set it to the `generated url` which was created on step 10. 15. Wait for railway to finish deploying your changes and Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, on a node js server
Love Remult?  Give our repo a star.⭐ # SvelteKit - Tutorial - Go further / Extra # SvelteKit ## Create a SvelteKit Project To create a new SvelteKit project, run the following command: ```sh npx sv@latest create remult-sveltekit-todo ``` During the setup, answer the prompts as follows: 1. **Which Svelte app template?**: ... `minimal` Project 2. **Add type checking with TypeScript?** ... Yes, using `TypeScript` syntax 3. **Select additional options**: ... We didn't select anything for this tutorial. Feel free to adapt it to your needs. 4. **Which package manager?**: ... We took `npm`, if you perfer others, feel free. Once the setup is complete, navigate into the project directory: ```sh cd remult-sveltekit-todo ``` ## Install Required Packages and Remult Install Remult and any necessary dependencies by running: ```sh npm install remult --save-dev ``` ## Bootstrap Remult To set up Remult in your SvelteKit project: 1. Create your remult `api` ::: code-group ```ts [src/server/api.ts] import { remultSveltekit } from 'remult/remult-sveltekit' export const api = remultSveltekit({}) ``` ::: 2. Create a remult `api route` ::: code-group ```ts [src/routes/api/[...remult]/+server.ts] import { api } from '../../../server/api' export const { GET, POST, PUT, DELETE } = api ``` ::: ## Final Tweaks Remult uses TypeScript decorators to enhance classes into entities. To enable decorators in your SvelteKit project, modify the `tsconfig.json` file by adding the following to the `compilerOptions` section: ```json [tsconfig.json] { "compilerOptions": { "experimentalDecorators": true // [!code ++] } } ``` ## Run the App To start the development server, run the following command: ```sh npm run dev ``` Your SvelteKit app will be available at [http://localhost:5173](http://localhost:5173). Your SvelteKit project with Remult is now up and running. # Extra ## Extra - Remult in other SvelteKit routes To enable remult across all sveltekit route ::: code-group ```ts [src/hooks.server.ts] import { sequence } from '@sveltejs/kit/hooks' import { api as handleRemult } from './server/api' export const handle = sequence( // Manage your sequence of handlers here handleRemult, ) ``` ::: ## Extra - Universal load & SSR To Use remult in ssr `PageLoad` - this will leverage the `event`'s fetch to load data on the server without reloading it on the frontend, and abiding to all api rules even when it runs on the server ::: code-group ```ts [src/routes/+page.ts] import { remult } from 'remult' import type { PageLoad } from './$types' export const load = (async (event) => { // Instruct remult to use the special svelte fetch // Like this univeral load will work in SSR & CSR remult.useFetch(event.fetch) return repo(Task).find() }) satisfies PageLoad ``` ::: ::: tip You can add this in `+layout.ts` as well and all routes **under** will have the correct fetch out of the box. ::: ## Extra - Server load If you return a remult entity from the `load` function of a `+page.server.ts`, SvelteKit will complain and show this error: ```bash Error: Data returned from `load` while rendering / is not serializable: Cannot stringify arbitrary non-POJOs (data.tasks[0]) ``` To fix this, you can use `repo(Entity).toJson()` in the server load function and `repo(Entity).fromJson()` in the .svelte file to serialize and deserialize well the entity. ::: code-group ```ts [src/routes/+page.server.ts] import { repo } from 'remult' import type { PageServerLoad } from './$types' import { Task } from '../demo/todo/Task' export const load = (async () => { const tasks = repo(Task).toJson(await repo(Task).find()) return { tasks, } }) satisfies PageServerLoad ``` ```svelte [src/routes/+page.svelte] ``` ::: --- #### Since `@sveltejs/kit@2.11.0`, there is a new feature: [Universal-hooks-transport](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) With this new feature, you can get rid of `repo(Entity).toJson()` and `repo(Entity).fromJson()` thanks to this file: `hooks.ts`. ::: code-group ```ts [src/hooks.ts] import { repo, type ClassType } from 'remult' import { Task } from './demo/todo/Task' import type { Transport } from '@sveltejs/kit' import { api } from './server/api' // You can have: // A/ a local entity array to work only these ones (like here) // or // B/ import a global entity array that will be // shared between backend and frontend (not in ./server/api.ts) const entities = [Task] export const transport: Transport = { remultTransport: { encode: (value: any) => { for (let index = 0; index < entities.length; index++) { const element = entities[index] as ClassType if (value instanceof element) { return { ...repo(element).toJson(value), entity_key: repo(element).metadata.key, } } } }, decode: (value: any) => { for (let index = 0; index < entities.length; index++) { const element = entities[index] as ClassType if (value.entity_key === repo(element).metadata.key) { return repo(element).fromJson(value) } } }, }, } ``` ```ts [src/routes/+page.server.ts] import { repo } from 'remult' import type { PageServerLoad } from './$types' import { Task } from '../demo/todo/Task' export const load = (async () => { // const tasks = repo(Task).toJson(await repo(Task).find()) // [!code --] const tasks = await repo(Task).find() return { tasks, } }) satisfies PageServerLoad ``` ```svelte [src/routes/+page.svelte] ``` ::: ## Extra - Svelte 5 & Reactivity Remult is fully compatible with Svelte 5, Rune, and Reactivity. To take full advantage of it, add this snippet: ::: code-group ```html [src/routes/+layout.svelte] ``` ::: Then you can use `$state`, `$derived` like any other places ::: code-group ```html [src/routes/+page.svelte] ``` ::: # Next.js - Tutorial - Setup # Build a Full-Stack Next.js Application ### Create a simple todo app with Remult using Next.js In this tutorial, we are going to create a simple app to manage a task list. We'll use `Next.js`, and Remult as our full-stack CRUD framework. For deployment to production, we'll use [Vercel](https://vercel.com/) and a `PostgreSQL` database. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ### Prerequisites This tutorial assumes you are familiar with `TypeScript`, `React` and `Next.js`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. # Setup for the Tutorial This tutorial requires setting up a Next.js project and a few lines of code to add Remult. You can either **use a starter project** to speed things up, or go through the **step-by-step setup**. ## Option 1: Clone the Starter Project 1. Clone the _remult-nextjs-todo_ repository from GitHub and install its dependencies. ```sh git clone https://github.com/remult/nextjs-app-starter.git remult-nextjs-todo cd remult-nextjs-todo npm install ``` 2. Open your IDE. 3. Open a terminal and run the `dev` npm script. ```sh npm run dev ``` The default Next.js app main screen should be displayed (except for the styles which were modified for the tutorial). At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. ## Option 2: Step-by-step Setup ### Create a Next.js project 1. Create the new Next.js project. ```sh npx -y create-next-app@latest remult-nextjs-todo ``` Answer the questions as follows: ```sh ✔ Would you like to use TypeScript? ... Yes ✔ Would you like to use ESLint? ... No ✔ Would you like to use Tailwind CSS? ... No ✔ Would you like to use `src/` directory? ... Yes ✔ Would you like to use App Router? (recommended) ... Yes ✔ Would you like to customize the default import alias? ... No ``` 2. Go to the created folder. ```sh cd remult-nextjs-todo ``` ### Install Remult ```sh npm i remult ``` ### Bootstrap Remult in the back-end Remult is bootstrapped in a `Next.js` using a [catch all dynamic API route](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments), that passes the handling of requests to an object created using the `remultNextApp` function. 1. Open your IDE. 2. In the `src` directory, create a file called `api.ts` with the following code: ```ts // src/api.ts import { remultNextApp } from 'remult/remult-next' export const api = remultNextApp({}) ``` 3. Create an `api` directory within the app folder, and inside it, create a `[...remult]` subdirectory. Inside the `app/api/[...remult]` directory, craft a `route.ts` file with the following code. This file functions as a catch all route for the Next.js API route, effectively managing all incoming API requests. ```ts // src/app/api/[...remult]/route.ts import { api } from '../../../api' export const { POST, PUT, DELETE, GET } = api ``` ### Enable TypeScript decorators Add the following entry to the `compilerOptions` section of the `tsconfig.json` file to enable the use of decorators in the React app. ```json{7} // tsconfig.json { ... "compilerOptions": { ... "experimentalDecorators": true // add this ... } ... } ``` ### Run the app Open a terminal and start the app. ```sh npm run dev ``` The default `Next.js` main screen should be displayed. ### Remove Next.js default styles The Next.js default styles won't fit our todo app. If you'd like a nice-looking app, replace the contents of `app/globals.css` with [this CSS file](https://raw.githubusercontent.com/remult/nextjs-app-starter/main/src/app/globals.css). Otherwise, you can simply **delete the contents of `app/globals.css`**. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # Next.js - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between frontend and backend. 2. Create a file `Task.ts` in the `shared/` folder, with the following code: ```ts // src/shared/Task.ts import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.cuid() id = '' @Fields.string() title = '' @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` 3. In the `src/api.ts` api route, register the `Task` entity with Remult by adding `entities: [Task]` to an `options` object you pass to the `remultNextApp()` function: ```ts{4,7} // src/api.ts import { remultNextApp } from "remult/remult-next" import { Task } from "./shared/Task" const api = remultNextApp({ entities: [Task] }) export const { POST, PUT, DELETE, GET } = api ``` The [@Entity](../../docs/ref_entity.md) decorator tells Remult this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as a default database collection/table name), and an `options` argument used to define entity-related properties and operations, discussed in the next sections of this tutorial. To initially allow all CRUD operations for tasks, we set the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add a tasks. 1. Open a browser with the url: [http://localhost:3000/api/tasks](http://localhost:3000/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - _Clean car_. ```sh curl http://localhost:3000/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:3000/api/tasks](http://localhost:3000/api/tasks) and see that the array now contains one item. 4. Use `curl` to `POST` a few more tasks: ```sh curl http://localhost:3000/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` - Note that the `POST` endpoint can accept a single `Task` or an array of `Task`s. 5. Refresh the browser again, to see that the tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your Next.js application by setting the `admin` option to `true` in the `remultNextApp()` ::: code-group ```ts [src/api.ts] import { remultNextApp } from 'remult/remult-next' import { Task } from './shared/Task' const api = remultNextApp({ entities: [Task], admin: true, }) export const { POST, PUT, DELETE, GET } = api ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:3000/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks in a React component. In the `src` folder create a `components` folder and in it create a `todo.tsx` file and place the following code in it: ```tsx // src/components/todo.tsx 'use client' import { useEffect, useState } from 'react' import { remult } from 'remult' import { Task } from '../shared/Task' const taskRepo = remult.repo(Task) export default function Todo() { const [tasks, setTasks] = useState([]) useEffect(() => { taskRepo.find().then(setTasks) }, []) return (

Todos

{tasks.map((task) => { return (
{task.title}
) })}
) } ``` Here's a quick overview of the different parts of the code snippet: - `taskRepo` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `tasks` is a Task array React state to hold the list of tasks. - React's useEffect hook is used to call the Remult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server once when the React component is loaded. ### Display the todo Component Replace the contents of `src/app/page.tsx` with the following code: ```tsx // src/app/page.tsx import Todo from '../components/todo' export default function Home() { return } ``` After the browser refreshes, the list of tasks appears. # Next.js - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. In the `useEffect` hook defined in the `Todo` component, pass an `options` argument to the `find` method call and set its `limit` property to 20. ```ts{9-13} // src/components/todo.tsx //... export default function Todo() { //... useEffect(() => { taskRepo .find({ limit: 20 }) .then(setTasks) }, []) //... } ``` There aren't enough tasks in the database for this change to have an immediate effect, but it will have one later on when we'll add more tasks. ::: tip To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. In the `useEffect` hook, set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ```ts{7} // src/components/todo.tsx useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } }) .then(setTasks) }, []) ``` ## Server Side Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust the `useEffect` hook to fetch only `completed` tasks. ```ts{8} // src/components/todo.tsx useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, where: { completed: true } }) .then(setTasks) }, []) ``` ::: warning Note Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Setting the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```ts{6} useEffect(() => { taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, //where: { completed: true }, }) .then(setTasks); }, []); ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # Next.js - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. Add the highlighted `newTaskTitle` state and `addTask` function the Home Component ```ts{5-16} // src/components/todo.tsx export default function Todo() { const [tasks, setTasks] = useState([]) const [newTaskTitle, setNewTaskTitle] = useState("") const addTask = async (e: FormEvent) => { e.preventDefault() try { const newTask = await taskRepo.insert({ title: newTaskTitle }) setTasks([...tasks, newTask]) setNewTaskTitle("") } catch (error: unknown) { alert((error as { message: string }).message) } } //... ``` - the call to `taskRepo.insert` will make a post request to the server, insert the new task to the `db`, and return the new `Task` object with all it's info (including the id generated by the database) ::: warning Import FormEvent This code requires adding an import of `FormEvent` from `react`. ::: Next let's adjust the `tsx` to display a form to add new tasks ```tsx{7-14} // src/components/todo.tsx return (

Todos

setNewTaskTitle(e.target.value)} />
{tasks.map(task => { return (
{task.title}
) })}
) ``` Try adding a few tasks to see how it works ## Mark Tasks as completed Modify the contents of the `tasks.map` iteration within the `Todo` component to include the following `setCompleted` function and call it in the input's `onChange` event. ```tsx{5-6,8-9,16} // src/components/todo.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) return (
setCompleted(e.target.checked)} /> {task.title}
) }) } ``` - The `setTask` function is used to replace the state of the changed task in the `tasks` array - The `taskRepo.save` method update the `task` to the server and returns the updated value ## Rename Tasks and Save them To make the tasks in the list updatable, we'll bind the `tasks` React state to `input` elements and add a _Save_ button to save the changes to the backend database. Modify the contents of the `tasks.map` iteration within the `Todo` component to include the following `setTitle` and `saveTask` functions and add an `input` and a save `button`. ```tsx{11,13-19,28-29} // src/components/todo.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { setTask(await taskRepo.save(task)) } catch (error: unknown) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} />
) }) } ``` - The `setTitle` function, called from the `input`'s `onChange` event, saves the value from the `input` to the `tasks` state. - The `saveTask` function, called from the `button`'s' `onClick`event , saves the `task` object to the backend. Make some changes and refresh the browser to verify the backend database is updated. ::: tip Browser's Network tab As you play with these `CRUD` capabilities, monitor the network tab and see that they are all translated to `rest` api calls. ::: ## Delete Tasks Let's add a _Delete_ button next to the _Save_ button of each task in the list. Add the highlighted `deleteTask` function and _Delete_ `button` Within the `tasks.map` iteration in the `return` section of the `Todo` component. ```tsx{21-28,39} // src/components/todo.tsx { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => setTask(await taskRepo.save({ ...task, completed })) const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { setTask(await taskRepo.save(task)) } catch (error: unknown) { alert((error as { message: string }).message) } } const deleteTask = async () => { try { await taskRepo.delete(task) setTasks(tasks.filter(t => t !== task)) } catch (error: unknown) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} />
) }) } ``` # Next.js - Tutorial - Validation # Validation Validating user entered data is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, [CRUD operations](crud.md) catch these exceptions, and alert the user. We leave it to you to decide how to handle validation errors in your application. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ```ts{3-5} // src/shared/Task.ts @Fields.string({ validate: Validators.required }) title = "" ``` ::: warning Import Validators This code requires adding an import of `Validators` from `remult`. ::: ::: warning Manual browser refresh required For this change to take effect, you **must manually refresh the browser**. ::: After the browser is refreshed, try creating a new `task` or saving an existing one with an empty title - the _"Should not be empty"_ error message is displayed. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:3000/api/tasks` API route, providing an invalid title. ```sh curl -i http://localhost:3000/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` An http error is returned and the validation error text is included in the response body, ## Custom Validation The `validate` property of the first argument of `Remult` field decorators can be set to an arrow function which will be called to validate input on both front-end and back-end. Try something like this and see what happens: ```ts // src/shared/Task.ts @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } }) title = "" ``` # Next.js - Tutorial - Live Queries # Live Queries :rocket: ::: tip New Feature Live queries are a new feature introduced in version 0.18. ::: Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. Let's add realtime multiplayer capabilities to this app. ## Realtime updated todo list Let's switch from fetching Tasks once when the React component is loaded, and manually maintaining state for CRUD operations, to using a realtime updated live query subscription **for both initial data fetching and subsequent state changes**. 1. Modify the contents of the `useEffect` hook in the `src/components/todo.tsx` file ```ts{4-5,10} // src/components/todo.tsx useEffect(() => { return taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: "asc" } //where: { completed: true }, }) .subscribe(info => setTasks(info.applyChanges)) }, []) ``` Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` function which we use as a return value for the `useEffect` hook, so that it'll be called when the component unmounts. 2. As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we should remove the manual adding of new Tasks to the component's state: ```ts{7} // src/components/todo.tsx const addTask = async (e: FormEvent) => { e.preventDefault() try { await taskRepo.insert({ title: newTaskTitle }) // ^ this no longer needs to be a variable as we are not using it to set the state. // setTasks([...tasks, newTask]) <-- this line is no longer needed setNewTaskTitle("") } catch (error: unknown) { alert((error as { message: string }).message) } } ``` 3. Optionally remove other redundant state changing code: ```tsx{11-12,18-19,28} // src/components/todo.tsx //... { tasks.map(task => { const setTask = (value: Task) => setTasks(tasks => tasks.map(t => (t === task ? value : t))) const setCompleted = async (completed: boolean) => // setTask(await taskRepo.save({ ...task, completed })) <- Delete this line await taskRepo.save({ ...task, completed }) // <- replace with this line const setTitle = (title: string) => setTask({ ...task, title }) const saveTask = async () => { try { // setTask(await taskRepo.save(task)) <- Delete this line await taskRepo.save(task) // <- replace with this line } catch (error: unknown) { alert((error as { message: string }).message) } } const deleteTask = async () => { try { await taskRepo.delete(task) // setTasks(tasks.filter(t => t !== task)) <- Delete this line } catch (error: unknown) { alert((error as { message: string }).message) } } //... }) } ``` Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. Check the browser's network tab, you'll see a call to the `/api/stream` route which receives messages on every update. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # Next.js - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/completed Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async function to the `Todo` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```ts // src/components/todo.tsx const setAllCompleted = async (completed: boolean) => { for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } ``` The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the return section of the `Todo` component, just before the closing `` tag. Both of the buttons' `onClick` events will call the `setAllCompleted` method with the appropriate value of the `completed` argument. ```tsx // src/components/todo.tsx
``` Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor the `for` loop from the `setAllCompleted` function of the `Todo` function component into a new, `static`, `setAllCompleted` method in the `TasksController` class, which will run on the server. ```ts // src/shared/TasksController.ts import { BackendMethod, remult } from 'remult' import { Task } from './Task' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (the `allowed` property will be discussed later on in this tutorial). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register `TasksController` by adding it to the `controllers` array of the `options` object passed to `createRemultServer()`, in the server's `api` module: ```ts{4,8} // src/api.ts //... import { TasksController } from "./shared/TaskController" export const api = remultNextApp({ //... controllers: [TasksController] }) ``` 3. Replace the `for` iteration in the `setAllCompleted` function of the `Todo` component with a call to the `setAllCompleted` method in the `TasksController`. ```tsx{4} // src/components/todo.tsx const setAllCompleted = async (completed: boolean) => { await TasksController.setAllCompleted(completed) } ``` ::: warning Import TasksController Remember to add an import of `TasksController` in `src/components/todo.tsx`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser refreshed, the _"Set all..."_ buttons function exactly the same, but much faster. # Next.js - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use [NextAuth.js](https://next-auth.js.org/) for authentication. ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts{4} // src/app/shared/Task.ts @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ::: warning Import Allow This code requires adding an import of `Allow` from `remult`. ::: After the browser refreshes, **the list of tasks disappeared** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:3000/api/tasks ``` ::: ::: danger Authorized server-side code can still modify tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts // src/shared/TasksController.ts @BackendMethod({ allowed: Allow.authenticated }) ``` **This code requires adding an import of `Allow` from `remult`.** ::: ## User Authentication Let's set-up `NextAuth.js` to authenticate users to our app. ### Backend setup 1. Install `next-auth`: ```sh npm i next-auth ``` 2. `NextAuth` requires a "secret" used to encrypt the NextAuth.js JWT, see [Options | NextAuth.js](https://next-auth.js.org/configuration/options#nextauth_secret) for more info. Create a file called `.env.local` and set the `NEXTAUTH_SECRET` to a random string. ``` // .env.local NEXTAUTH_SECRET=something-secret ``` :::tip you can use an [online UUID generator](https://www.uuidgenerator.net/) to generate a completely random string ::: 3. Inside the `src` folder, create an `auth.ts` file with the following code. ```ts // src/auth.ts import NextAuth, { getServerSession } from 'next-auth/next' import Credentials from 'next-auth/providers/credentials' import { UserInfo } from 'remult' const validUsers: UserInfo[] = [ { id: '1', name: 'Jane' }, { id: '2', name: 'Steve' }, ] function findUser(name?: string | null) { return validUsers.find((user) => user.name === name) } export const auth = NextAuth({ providers: [ Credentials({ credentials: { name: { placeholder: 'Try Steve or Jane', }, }, authorize: (credentials) => findUser(credentials?.name) || null, }), ], callbacks: { session: ({ session }) => ({ ...session, user: findUser(session.user?.name), }), }, }) export async function getUserOnServer() { const session = await getServerSession() return findUser(session?.user?.name) } ``` This (very) simplistic NextAuth.js [Credentials](https://next-auth.js.org/providers/credentials) authorizes users by looking up the user's name in a predefined list of valid users. We've configured the `session` `callback` to include the user info as part of the session info, so that remult on the frontend will have the authorization info. 4. Create an `auth` folder within the 'api' folder, and inside it, create a `[...nextauth]` subdirectory. Inside the `src/app/api/auth/[...nextauth]` directory, craft a `route.ts` file with the following code. ```ts // src/app/api/auth/[...nextauth]/route.ts import { auth } from '../../../../auth' export { auth as GET, auth as POST } ``` ### Frontend setup 1. Create a `src/components/auth.tsx` file, and place the following code to it: ```tsx // src/components/auth.tsx import { signIn, signOut, useSession } from 'next-auth/react' import { useEffect } from 'react' import { UserInfo, remult } from 'remult' import Todo from './todo' export default function Auth() { const session = useSession() remult.user = session.data?.user as UserInfo useEffect(() => { if (session.status === 'unauthenticated') signIn() }, [session]) if (session.status !== 'authenticated') return <> return (
Hello {remult.user?.name}{' '}
) } ``` 2. Update the `src/app/page.tsx` with the highlighted changes: ```tsx{3-5,9-11} // src/app/page.tsx "use client" import { SessionProvider } from "next-auth/react" import Auth from "../components/auth" export default function Home() { return ( ) } ``` ### Connect Remult-Next On the Backend Once an authentication flow is established, integrating it with Remult in the backend is as simple as providing Remult with a `getUser` function that uses the `getUserOnServer` function we've created in `src/auth.ts` ```ts{3,7} // src/api.ts import { getUserOnServer } from "./auth" const api = remultNextApp({ //... getUser: getUserOnServer, }) //... ``` The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. Let's define an `admin` role for our todo app, and enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts{7-8,18} // src/shared/Task.ts import { Allow, Entity, Fields } from "remult" @Entity("tasks", { allowApiCrud: Allow.authenticated, allowApiInsert: "admin", allowApiDelete: "admin" }) export class Task { @Fields.uuid() id!: string @Fields.string({ validate: (task) => { if (task.title.length < 3) throw "Too Short" } allowApiUpdate: "admin" }) title = "" @Fields.boolean() completed = false } ``` 2. Let's give the user _"Jane"_ the `admin` role by modifying the `roles` array of her `validUsers` entry. ```ts{4} // src/auth.ts const validUsers = [ { id: "1", name: "Jane", roles: ["admin"] }, { id: "2", name: "Steve" } ] ``` **Sign in to the app as _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert ```tsx{4,13} // src/components/todo.tsx
{taskRepo.metadata.apiInsertAllowed() && (
setNewTaskTitle(e.target.value)} />
)} ...
``` And let's do the same for the `delete` button: ```tsx{12,14} // src/components/todo.tsx return (
setCompleted(e.target.checked)} /> setTitle(e.target.value)} /> {taskRepo.metadata.apiDeleteAllowed(task) && ( )}
) ``` This way we can keep the frontend consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values. # Next.js - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. **In production, we'd like to use a `Postgres` database table instead.** ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add an environment variables called `DATABASE_URL` and set it with your connection string ``` // .env.local ... DATABASE_URL=your connection string ``` 3. Add the highlighted code to the `api` server module. ```ts{5,7,11-13} // src/api.ts //... import { createPostgresDataProvider } from "remult/postgres" const DATABASE_URL = process.env["DATABASE_URL"] const api = remultNextApp({ //... dataProvider: DATABASE_URL ? createPostgresDataProvider({ connectionString: DATABASE_URL }) : undefined, }) ``` Once the application restarts, it'll try to use postgres as the data source for your application. If `DATABASE_URL` env variable has found, it'll automatically create the `tasks` table for you - as you'll see in the `terminal` window. If no `DATABASE_URL` has found, it'll just fallback to our local JSON files. ::: tip Database configurations You can set more options using the `configuration` property, for example `ssl` and others. ```ts createPostgresDataProvider({ configuration: { ssl: true, }, }) ``` ::: # Next.js - Tutorial - Deployment # Deployment You can deploy the application to a standard node.js server, or a server-less server. We'll review both options. ## Deploy to a node.js server Let's deploy the todo app to [railway.app](https://railway.app/). In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Select `Empty Project` 3. Set a project name. 4. Once it's done add a database by running the following command: ```sh railway add ``` 5. Select `postgressql` as the database. 6. Once that's done run the following command to upload the project to railway: ```sh railway up ``` 7. got to the `railway` project's site and click on the project 8. Switch to the `settings` tab 9. Under `Environment` click on `Generate Domain` 10. Copy the `generated url`, you'll need it for [NEXTAUTH_URL](https://next-auth.js.org/configuration/options#nextauth_url) on step 14 11. Switch to the `variables` tab 12. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 13. Add another variable called `SESSION_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 14. Add another variable called `NEXTAUTH_URL` and set it to the `generated url` which was created on step 10. 15. Wait for railway to finish deploying your changes and Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, on a node js server Next we'll explore deployment to a server-less environment. ## Deploying to a serverless environment ### LiveQuery Serverless Support Any `serverless` platform can't be used to maintain an active subscription channel for our live query, we'll need to use a 3rd party provider for that. If you're not using `liveQuery` you can skip to the next step. In this demo, we'll use [ably.com](https://ably.com/) Follow these steps only if you want to use `liveQuery` in the app 1. ```sh npm i ably ``` 2. Goto [ably.com](https://ably.com/) create a user and click on the "Create new app" button 3. Select a name and click `create app` 4. Click on the `API Keys` button on top. 5. Copy the first api key (with the many capabilities), create an entry in the `.env.local` file, name it `ABLY_API_KEY` and paste the api key there. 6. Configure `ably` as the `subscriptionServer` ```ts{4-5,8-10} // src/api.ts //... import ably from "ably" import { AblySubscriptionServer } from "remult/ably" const api = remultNextApp({ subscriptionServer: new AblySubscriptionServer( new ably.Rest(process.env["ABLY_API_KEY"]!) ) //... }) ``` 7. Next, we'll need to create a route that `ably`'s client on the front-end will use to get a `token` for a user that wants to subscribe to a channel - in the `src/app/api` folder, createa folder called `getAblyToken` and in it create a file called `route.ts` ```ts // src/app/api/getAblyToken/route.ts import ably from 'ably' import { NextResponse } from 'next/server' export async function POST() { const token = await new ably.Rest( process.env['ABLY_API_KEY']!, ).auth.createTokenRequest({ capability: { '*': ['subscribe'] } }) return NextResponse.json(token) } ``` 8) Configure the `front-end` to use ably as it's `subscriptionClient` by adding a new `useEffect` hook and configure `ably` to use the `api/getAblyToken` route we've created as it's `authUrl` - we'll that in the `Auth` component ```tsx{3-4,12-15} // src/components/auth.tsx import ably from "ably" import { AblySubscriptionClient } from "remult/ably" export default function Auth() { const session = useSession() remult.user = session.data?.user as UserInfo useEffect(() => { if (session.status === "unauthenticated") signIn() else if (session.status === "authenticated") remult.apiClient.subscriptionClient = new AblySubscriptionClient( new ably.Realtime({ authUrl: "/api/getAblyToken", authMethod: "POST" }) ) }, [session]) ``` 9) Configure `remultNextApp` to store live-queries in the `dataProvider` ```ts{4,6,8-9} // src/api.ts //... import { DataProviderLiveQueryStorage } from "remult/server" const dataProvider = createPostgresDataProvider() const api = remultNextApp({ dataProvider, liveQueryStorage: new DataProviderLiveQueryStorage(dataProvider) //... }) ``` Let's deploy the todo app to [vercel](https://vercel.com/). ### Postgres We'll use vercel's postgres as out database, and that requires the following changes to the `createPostgresDataProvider` options. ```ts{4-7} // src/api.ts const dataProvider = createPostgresDataProvider({ connectionString: process.env["POSTGRES_URL"] || process.env["DATABASE_URL"], configuration: { ssl: Boolean(process.env["POSTGRES_URL"]), }, }) ``` - Vercel sends the connection string using the `POSTGRES_URL` environment variable, other providers use the `DATABASE_URL` - this code supports them both. - Ssl is required with vercel - by my local pg doesn't, so we condition ssl based on the environment variable. ### Create a github repo Vercel deploys automatically whenever you push to github, so the first step of deployment is to create a github repo and push all your changes to it. ### Create a vercel project 1. Create a vercel account if you don't already have one. 2. Goto [https://vercel.com/new](https://vercel.com/new) 3. Select your `github` repo and click `import` 4. Configure the project's name and in the `> Environment Variables` section, `NEXTAUTH_SECRET` and `ABLY_API_KEY` environment variables 5. Click `Deploy` 6. Now we need to define the postgres database. 7. Wait for vercel to complete it's deployment 8. Click on `Continue to Dashboard` 9. Select the `Storage` tab 10. Create new Database and select Postgres 11. Accept the terms 12. Select region and click Create & continue 13. Click Connect 14. Click on Settings, Environment Variables and see that the `POSTGRES_URL` and other environment variables were added. 15. At the time of this article, vercel did not yet automatically redeploy once you configure a database, so in order to redeploy, click on the `Deployments` tab 16. 3 dots at the end of the deployment line and select `Redeploy` and click `Redeploy` 17. Once completed click on 'Visit'. That's it - our application is deployed to production on vercel, play with it and enjoy. [Code at this stage](https://github.com/noam-honig/remult-nextjs-app-router-todo) To see a larger more complex code base, visit our [CRM example project](https://www.github.com/remult/crm-demo) Love Remult?  Give our repo a star.⭐ # SolidStart - Tutorial - Setup # Build a Full-Stack SolidStart Application ### Create a simple todo app with Remult using SolidStart In this tutorial, we are going to create a simple app to manage a task list. We'll use `SolidStart`, and Remult as our full-stack CRUD framework. For deployment to production, we'll use [railway.app](https://railway.app/) and a `PostgreSQL` database. By the end of the tutorial, you should have a basic understanding of Remult and how to use it to accelerate and simplify full stack app development. ::: tip You want to have a look at the end result ? You can `degit` the final result and read the `README.md` file in the project to check it out. ```sh npx degit remult/remult/examples/solid-start-todo remult-solid-start-todo cd remult-solid-start-todo ``` ::: ### Prerequisites This tutorial assumes you are familiar with `TypeScript`, `Solid` and `SolidStart`. Before you begin, make sure you have [Node.js](https://nodejs.org) and [git](https://git-scm.com/) installed. # Setup for the Tutorial This tutorial requires setting up a SolidStart project and a few lines of code to add Remult. You can either **use a starter project** to speed things up, or go through the **step-by-step setup**. ## Option 1: Clone the Starter Project 1. Clone the _remult-solid-start-todo_ repository from GitHub and install its dependencies. ```sh git clone https://github.com/remult/solid-start-app-starter.git remult-solid-start-todo cd remult-solid-start-todo npm install ``` 2. Open your IDE. 3. Open a terminal and run the `dev` npm script. ```sh npm run dev ``` The default SolidStart app main screen should be displayed (except for the styles which were modified for the tutorial). At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. ## Option 2: Step-by-step Setup ### Create a SolidStart project 1. Create the new SolidStart project. ```sh npm init solid@latest remult-solid-start-todo ``` Answer the questions as follows: ```sh o Is this a Solid-Start project? | Yes | o Which template would you like to use? | basic | o Use Typescript? | Yes ``` 2. Go to the created folder. ```sh cd remult-solid-start-todo ``` ### Install Remult ```sh npm i remult ``` ### Bootstrap Remult in the back-end Remult is bootstrapped in a `SolidStart` using a [catch all dynamic API route](https://start.solidjs.com/core-concepts/routing#catch-all-routes), that passes the handling of requests to an object created using the `remultSolidStart` function. 1. Open your IDE. 2. Create an `api.ts` file in the `src` folder with the following code - which will have the remult's api configuration. ```ts // src/api.ts import { remultSolidStart } from 'remult/remult-solid-start' export const api = remultSolidStart({}) ``` 3. In the `routes` directory within the `src` folder, create an `api` directory. Inside the `src/routes/api/` directory, craft a `[...remult].ts` file with the following code. This file functions as a catch all route for the SolidStart API route, effectively managing all incoming API requests. ```ts // src/routes/api/[...remult].ts import { api } from '../../api.js' export const { POST, PUT, DELETE, GET } = api ``` ### Enable TypeScript decorators To enable TypeScript decorators: 1. Install the required plugin ```sh npm i -D @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties ``` 2. Add the following entry to the `app.config.ts` file to enable the use of decorators in the solid start app. ```ts{6-14} // app.config.ts import { defineConfig } from "@solidjs/start/config" export default defineConfig({ //@ts-ignore solid: { babel: { plugins: [ ["@babel/plugin-proposal-decorators", { version: "legacy" }], ["@babel/plugin-transform-class-properties"], ], }, }, }) ``` ### Run the app Open a terminal and start the app. ```sh npm run dev ``` The default `SolidStart` main screen should be displayed. ### Cleanup SolidStart basic example 1. Replace the content of `src/app.tsx` with the following code: ```tsx //src/app.tsx import { MetaProvider, Title } from '@solidjs/meta' import { Router } from '@solidjs/router' import { FileRoutes } from '@solidjs/start/router' import { Suspense } from 'solid-js' import './app.css' export default function App() { return ( ( SolidStart - Basic {props.children} )} > ) } ``` ### Remove SolidStart default styles The SolidStart default styles won't fit our todo app. If you'd like a nice-looking app, replace the contents of `src/app.css` with [this CSS file](https://raw.githubusercontent.com/remult/solid-start-app-starter/main/src/app.css). Otherwise, you can simply **delete the contents of `src/app.css`**. ### Setup completed At this point, our starter project is up and running. We are now ready to move to the [next step of the tutorial](./entities.md) and start creating the task list app. # SolidStart - Tutorial - Entities # Entities Let's start coding the app by defining the `Task` entity class. The `Task` entity class will be used: - As a model class for client-side code - As a model class for server-side code - By `remult` to generate API endpoints, API queries, and database commands The `Task` entity class we're creating will have an auto-generated `id` field, a `title` field, a `completed` field and an auto-generated `createdAt` field. The entity's API route ("tasks") will include endpoints for all `CRUD` operations. ## Define the Model 1. Create a `shared` folder under the `src` folder. This folder will contain code shared between frontend and backend. 2. Create a file `Task.ts` in the `shared/` folder, with the following code: ```ts // src/shared/Task.ts import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.cuid() id = '' @Fields.string() title = '' @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` 3. In the `src/api.ts` file, register the `Task` entity with Remult by adding `entities: [Task]` to an `options` object you pass to the `remultSolidStart()` function: ```ts{4,7} // src/api.ts import { remultSolidStart } from "remult/remult-solid-start" import { Task } from "./shared/Task" export const api = remultSolidStart({ entities: [Task] }) ``` The [@Entity](../../docs/ref_entity.md) decorator tells Remult this class is an entity class. The decorator accepts a `key` argument (used to name the API route and as a default database collection/table name), and an `options` argument used to define entity-related properties and operations, discussed in the next sections of this tutorial. To initially allow all CRUD operations for tasks, we set the option [allowApiCrud](../../docs/ref_entity.md#allowapicrud) to `true`. The [@Fields.cuid](../../docs/field-types.md#fields-cuid) decorator tells Remult to automatically generate a short random id using the [cuid](https://github.com/paralleldrive/cuid) library. This value can't be changed after the entity is created. The [@Fields.string](../../docs/field-types.md#fields-string) decorator tells Remult the `title` property is an entity data field of type `String`. This decorator is also used to define field-related properties and operations, discussed in the next sections of this tutorial and the same goes for `@Fields.boolean` and the `completed` property. The [@Fields.createdAt](../../docs/field-types.md#fields-createdat) decorator tells Remult to automatically generate a `createdAt` field with the current date and time. ::: tip For a complete list of supported field types, see the [Field Types](../../docs/field-types.md) section in the Remult documentation. ::: ## Test the API Now that the `Task` entity is defined, we can start using the REST API to query and add a tasks. 1. Open a browser with the url: [http://localhost:3000/api/tasks](http://localhost:3000/api/tasks), and you'll see that you get an empty array. 2. Use `curl` to `POST` a new task - _Clean car_. ```sh curl http://localhost:3000/api/tasks -d "{\"title\": \"Clean car\"}" -H "Content-Type: application/json" ``` 3. Refresh the browser for the url: [http://localhost:3000/api/tasks](http://localhost:3000/api/tasks) and see that the array now contains one item. 4. Use `curl` to `POST` a few more tasks: ```sh curl http://localhost:3000/api/tasks -d "[{\"title\": \"Read a book\"},{\"title\": \"Take a nap\", \"completed\":true },{\"title\": \"Pay bills\"},{\"title\": \"Do laundry\"}]" -H "Content-Type: application/json" ``` - Note that the `POST` endpoint can accept a single `Task` or an array of `Task`s. 5. Refresh the browser again, to see that the tasks were stored in the db. ::: warning Wait, where is the backend database? While remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file**. Notice that a `db` folder has been created under the root folder, with a `tasks.json` file containing the created tasks. ::: ## Admin UI ### Enabling the Admin UI Add the Admin UI to your Solid Start application by setting the `admin` option to `true` in the `remultSolidStart()` ::: code-group ```ts [src/api.ts] import { remultSolidStart } from 'remult/remult-next' import { Task } from './shared/Task' export const api = remultSolidStart({ entities: [Task], admin: true, // Enable the Admin UI }) ``` ::: ### Accessing and Using the Admin UI Navigate to `http://localhost:3000/api/admin` to access the Admin UI. Here, you can perform CRUD operations on your entities, view their relationships via the Diagram entry, and ensure secure management with the same validations and authorizations as your application. ![Remult Admin](/remult-admin.png) ### Features - **CRUD Operations**: Directly create, update, and delete tasks through the Admin UI. - **Entity Diagram**: Visualize relationships between entities for better data structure understanding. - **Security**: Operations are secure, adhering to application-defined rules. ## Display the Task List Let's start developing the web app by displaying the list of existing tasks in a solid component. In the `src/components` folder create a `Todo.tsx` file and place the following code in it: Edit the `src/components/Todo.tsx` file ```tsx // src/components/Todo.tsx import { repo } from 'remult' import { createStore } from 'solid-js/store' import { For, onMount } from 'solid-js' import { Task } from '../shared/Task.js' const taskRepo = repo(Task) export default function Todo() { const [tasks, setTasks] = createStore([]) onMount(() => taskRepo.find().then(setTasks)) return (
{(task) => { return (
{task.title}
) }}
) } ``` Here's a quick overview of the different parts of the code snippet: - `taskRepo` is a Remult [Repository](../../docs/ref_repository.md) object used to fetch and create Task entity objects. - `tasks` is a Task array solid store to hold the list of tasks. - solid's onMount hook is used to call the Remult [repository](../../docs/ref_repository.md)'s [find](../../docs/ref_repository.md#find) method to fetch tasks from the server once when the solid component is loaded. ### Display the todo Component Replace the contents of `src/routes/index.tsx` with the following code: ```tsx // src/routes/index.tsx import Todo from '../components/Todo.jsx' export default function Home() { return (

Todos

) } ``` After the browser refreshes, the list of tasks appears. # SolidStart - Tutorial - Paging, Sorting and Filtering # Paging, Sorting and Filtering The RESTful API created by Remult supports **server-side paging, sorting, and filtering**. Let's use that to limit, sort and filter the list of tasks. ## Limit Number of Fetched Tasks Since our database may eventually contain a lot of tasks, it make sense to use a **paging strategy** to limit the number of tasks retrieved in a single fetch from the back-end database. Let's limit the number of fetched tasks to `20`. In the `onMount` hook defined in the `Todo` component, pass an `options` argument to the `find` method call and set its `limit` property to 20. ```ts{11} // src/components/Todo.tsx //... export default function Todo() { //... onMount(() => taskRepo .find({ limit: 20, }) .then(setTasks) ) //... } ``` There aren't enough tasks in the database for this change to have an immediate effect, but it will have one later on when we'll add more tasks. ::: tip To query subsequent pages, use the [Repository.find()](../../docs/ref_repository.md#find) method's `page` option. ::: ## Sorting By Creation Date We would like old tasks to appear first in the list, and new tasks to appear last. Let's sort the tasks by their `createdAt` field. In the `onMount` hook, set the `orderBy` property of the `find` method call's `option` argument to an object that contains the fields you want to sort by. Use "asc" and "desc" to determine the sort order. ```ts{7} // src/components/Todo.tsx onMount(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" } }) .then(setTasks) ) ``` ## Server Side Filtering Remult supports sending filter rules to the server to query only the tasks that we need. Adjust the `onMount` hook to fetch only `completed` tasks. ```ts{8} // src/components/Todo.tsx onMount(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, where: { completed: true } }) .then(setTasks) ) ``` ::: warning Note Because the `completed` field is of type `boolean`, the argument is **compile-time checked to be of the `boolean` type**. Setting the `completed` filter to `undefined` causes it to be ignored by Remult. ::: Play with different filtering values, and eventually comment it out, since we do need all the tasks ```ts{6} onMount(() => taskRepo .find({ limit: 20, orderBy: { createdAt: "asc" }, //where: { completed: true }, }) .then(setTasks); ); ``` ::: tip Learn more Explore the reference for a [comprehensive list of filtering options](../../docs/entityFilter.md). ::: # SolidStart - Tutorial - CRUD Operations # CRUD Operations ## Adding new tasks Now that we can see the list of tasks, it's time to add a few more. Add the highlighted `newTaskTitle` state and `addTask` function the Home Component ```ts{5-16} // src/components/Todo.tsx export default function Todo() { const [tasks, setTasks] = createStore([]) const [newTaskTitle, setNewTaskTitle] = createSignal("") async function addTask(e: Event) { e.preventDefault() try { const newTask = await taskRepo.insert({ title: newTaskTitle() }) setTasks([...tasks, newTask]) setNewTaskTitle("") } catch (error) { alert((error as { message: string }).message) } } //... ``` ::: warning Import createSignal This code requires adding an import of `createSignal` from `solid-js`. ::: - the call to `taskRepo.insert` will make a post request to the server, insert the new task to the `db`, and return the new `Task` object with all it's info (including the id generated by the database) Next let's adjust the `tsx` to display a form to add new tasks ```tsx{5-12} // src/components/Todo.tsx return (
setNewTaskTitle(e.currentTarget.value)} />
{(task) => { return (
{task.title}
) }}
) ``` Try adding a few tasks to see how it works ## Mark Tasks as completed Modify the contents of the `For` iteration within the `Todo`component to include the following `setCompleted` function and call it in the input's`onInput` event. ```tsx{4-8,14} // src/components/Todo.tsx {(task, i) => { async function setCompleted(completed: boolean) { const updatedTask = await taskRepo.update(task, { completed }) setTasks(i(), updatedTask) } return (
setCompleted(e.target.checked)} /> {task.title}
) }}
``` - The `taskRepo.update` method updates the `task` to the server and returns the updated value ## Rename Tasks and Save them To make the tasks in the list updatable, we'll bind the `tasks` solid state to `input` elements and add a _Save_ button to save the changes to the backend database. Modify the contents of the `For` iteration within the `Todo`component to include the following`setTitle`and`saveTask`functions and add an`input`and a save`button`. ```tsx{9-15,24-28} // src/components/Todo.tsx {(task, i) => { async function setCompleted(completed: boolean) { const updatedTask = await taskRepo.update(task, { completed }) setTasks(i(), updatedTask) } async function saveTask() { try { await taskRepo.save(task) } catch (error) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTasks(i(), "title", e.target.value)} />
) }}
``` - The `saveTask` function, called from the `button`'s' `onClick`event , saves the `task` object to the backend. Make some changes and refresh the browser to verify the backend database is updated. ::: tip Browser's Network tab As you play with these `CRUD` capabilities, monitor the network tab and see that they are all translated to `rest` api calls. ::: ## Delete Tasks Let's add a _Delete_ button next to the _Save_ button of each task in the list. Add the highlighted `deleteTask` function and _Delete_ `button` Within the `For` iteration in the `return` section of the `Todo` component. ```tsx{16-23,36} // src/components/Todo.tsx {(task, i) => { async function setCompleted(completed: boolean) { const updatedTask = await taskRepo.update(task, { completed }) setTasks(i(), updatedTask) } async function saveTask() { try { await taskRepo.save(task) } catch (error) { alert((error as { message: string }).message) } } async function deleteTask() { try { await taskRepo.delete(task) setTasks(tasks.filter((t) => t !== task)) } catch (error) { alert((error as { message: string }).message) } } return (
setCompleted(e.target.checked)} /> setTasks(i(), "title", e.target.value)} />
) }}
``` # SolidStart - Tutorial - Validation # Validation Validating user entered data is usually required both on the client-side and on the server-side, often causing a violation of the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) design principle. **With Remult, validation code can be placed within the entity class, and Remult will run the validation logic on both the frontend and the relevant API requests.** ::: warning Handling validation errors When a validation error occurs, Remult will throw an exception. In this tutorial, [CRUD operations](crud.md) catch these exceptions, and alert the user. We leave it to you to decide how to handle validation errors in your application. ::: ## Validate the Title Field Task titles are required. Let's add a validity check for this rule. 1. In the `Task` entity class, modify the `Fields.string` decorator for the `title` field to include an object literal argument and set the object's `validate` property to `Validators.required`. ```ts{3-5} // src/shared/Task.ts @Fields.string({ validate: Validators.required }) title = "" ``` ::: warning Import Validators This code requires adding an import of `Validators` from `remult`. ::: After the browser is refreshed, try creating a new `task` or saving an existing one with an empty title - the _"Should not be empty"_ error message is displayed. ### Implicit server-side validation The validation code we've added is called by Remult on the server-side to validate any API calls attempting to modify the `title` field. Try making the following `POST` http request to the `http://localhost:3000/api/tasks` API route, providing an invalid title. ```sh curl -i http://localhost:3000/api/tasks -d "{\"title\": \"\"}" -H "Content-Type: application/json" ``` An http error is returned and the validation error text is included in the response body, ## Custom Validation The `validate` property of the first argument of `Remult` field decorators can be set to an arrow function which will be called to validate input on both front-end and back-end. Try something like this and see what happens: ```ts // src/shared/Task.ts @Fields.string({ validate: (task) => task.title.length > 2 || "Too Short", }) title = "" ``` # SolidStart - Tutorial - Live Queries # Live Queries :rocket: Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. Let's add realtime multiplayer capabilities to this app. ## Realtime updated todo list Let's switch from fetching Tasks once when the solid component is loaded, and manually maintaining state for CRUD operations, to using a realtime updated live query subscription **for both initial data fetching and subsequent state changes**. 1. Modify the contents of the `onMount` hook in the `src/components/Todo.tsx` file ```ts{4,6,11} // src/components/Todo.tsx onMount(() => onCleanup( taskRepo .liveQuery({ limit: 20, orderBy: { createdAt: "asc" }, //where: { completed: true }, }) .subscribe((info) => setTasks(info.applyChanges)) ) ) ``` Let's review the change: - Instead of calling the `repository`'s `find` method we now call the `liveQuery` method to define the query, and then call its `subscribe` method to establish a subscription which will update the Tasks state in realtime. - The `subscribe` method accepts a callback with an `info` object that has 3 members: - `items` - an up to date list of items representing the current result - it's useful for readonly use cases. - `applyChanges` - a method that receives an array and applies the changes to it - we send that method to the `setTasks` state function, to apply the changes to the existing `tasks` state. - `changes` - a detailed list of changes that were received - The `subscribe` method returns an `unsubscribe` function which we send to the `onCleanup` function, so that it'll be called when the component unmounts. ::: warning Import onCleanup This code requires adding an import of `onCleanup` from `solid-js`. ::: 2. As all relevant CRUD operations (made by all users) will **immediately update the component's state**, we should remove the manual adding of new Tasks to the component's state: ```ts{7} // src/components/Todo.tsx async function addTask(e: Event) { e.preventDefault() try { await taskRepo.insert({ title: newTaskTitle() }) // ^ this no longer needs to be a variable as we are not using it to set the state. // setTasks([...tasks, newTask]) <-- this line is no longer needed setNewTaskTitle("") } catch (error) { alert((error as { message: string }).message) } } ``` 3. Optionally remove other redundant state changing code: ```tsx{7-9,21} // src/components/Todo.tsx //... {(task, i) => { async function setCompleted(completed: boolean) { //const updatedTask = await taskRepo.update(task, { completed }) <- Delete this line //setTasks(i(), updatedTask) <- Delete this line await taskRepo.update(task, { completed }) // <- replace with this line } async function saveTask() { try { await taskRepo.save(task) } catch (error) { alert((error as { message: string }).message) } } async function deleteTask() { try { await taskRepo.delete(task) // setTasks(tasks.filter((t) => t !== task)) <- Delete this line } catch (error) { alert((error as { message: string }).message) } } ``` Open the todo app in two (or more) browser windows/tabs, make some changes in one window and notice how the others are updated in realtime. ::: tip Under the hood The default implementation of live-queries uses HTTP Server-Sent Events (SSE) to push realtime updates to clients, and stores live-query information in-memory. Check the browser's network tab, you'll see a call to the `/api/stream` route which receives messages on every update. For serverless environments _(or multi servers)_, live-query updates can be pushed using integration with third-party realtime providers, such as [Ably](https://ably.com/) (or others), and live-query information can be stored to any database supported by Remult. ::: # SolidStart - Tutorial - Backend methods # Backend methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring**. ## Set All Tasks as Un/completed Let's add two buttons to the todo app: "Set all as completed" and "Set all as uncompleted". 1. Add a `setAllCompleted` async function to the `Todo` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```ts // src/components/Todo.tsx async function setAllCompleted(completed: boolean) { for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } ``` The `for` loop iterates the array of `Task` objects returned from the backend, and saves each task back to the backend with a modified value in the `completed` field. 2. Add the two buttons to the return section of the `Todo` component, just before the closing `` tag. Both of the buttons' `onClick` events will call the `setAllCompleted` method with the appropriate value of the `completed` argument. ```tsx // src/components/Todo.tsx
``` Make sure the buttons are working as expected before moving on to the next step. ## Refactor from Front-end to Back-end With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. A simple way to prevent this is to expose an API endpoint for `setAllCompleted` requests, and run the same logic on the server instead of the client. 1. Create a new `TasksController` class, in the `shared` folder, and refactor the `for` loop from the `setAllCompleted` function of the `Todo`function component into a new,`static`, `setAllCompleted`method in the`TasksController` class, which will run on the server. ```ts // src/shared/TasksController.ts import { BackendMethod, repo } from 'remult' import { Task } from './Task' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = repo(Task) for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }) } } } ``` The `@BackendMethod` decorator tells Remult to expose the method as an API endpoint (the `allowed` property will be discussed later on in this tutorial). **Unlike the front-end `Remult` object, the server implementation interacts directly with the database.** 2. Register `TasksController` by adding it to the `controllers` array of the `options` object passed to `createRemultServer()`, in the server's `api` module: ```ts{4,8} // src/api.ts //... import { TasksController } from "./shared/TasksController.js" export const api = remultSolidStart({ //... controllers: [TasksController] }) ``` 3. Replace the `for` iteration in the `setAllCompleted` function of the `Todo`component with a call to the`setAllCompleted`method in the`TasksController`. ```tsx{4} // src/components/Todo.tsx async function setAllCompleted(completed: boolean) { await TasksController.setAllCompleted(completed) } ``` ::: warning Import TasksController Remember to add an import of `TasksController` in `src/components/Todo.tsx`. ::: ::: tip Note With Remult backend methods, argument types are compile-time checked. :thumbsup: ::: After the browser refreshed, the _"Set all..."_ buttons function exactly the same, but much faster. # SolidStart - Tutorial - Authentication and Authorization # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism, and only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use method outlined in the [Authentication](https://start.solidjs.com/advanced/session/) article of `SolidStart` ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator, by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts{4} // src/app/shared/Task.ts @Entity("tasks", { allowApiCrud: Allow.authenticated }) ``` ::: warning Import Allow This code requires adding an import of `Allow` from `remult`. ::: After the browser refreshes, **the list of tasks disappeared** and the user can no longer create new tasks. ::: details Inspect the HTTP error returned by the API using cURL ```sh curl -i http://localhost:3000/api/tasks ``` ::: ::: danger Authorized server-side code can still modify tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts // src/shared/TasksController.ts @BackendMethod({ allowed: Allow.authenticated }) ``` **This code requires adding an import of `Allow` from `remult`.** ::: ## User Authentication Let's set-up `SolidStart` authentication to authenticate users to our app. ### Backend setup 1. Create an `auth.ts` file in the `src` folder with the following code. ```ts // src/auth.ts import { action, redirect } from '@solidjs/router' import { useSession } from 'vinxi/http' import { type UserInfo } from 'remult' const validUsers: UserInfo[] = [ { id: '1', name: 'Jane' }, { id: '2', name: 'Steve' }, ] export async function getSession() { 'use server' return await useSession<{ user?: UserInfo }>({ password: process.env['SESSION_SECRET'] || 'Something secret used for development only', }) } export const loginAction = action(async (formData: FormData) => { 'use server' const username = String(formData.get('username')) try { const session = await getSession() const user = validUsers.find((x) => x.name === username) if (!user) throw Error("Invalid user, try 'Steve' or 'Jane'") await session.update({ user }) } catch (err) { return err as Error } throw redirect('/') }, 'login') export async function logout() { 'use server' const session = await getSession() await session.update({ user: null! }) } export async function getUser() { 'use server' const session = await getSession() return session?.data?.user } ``` - The (very) simplistic `loginAction` endpoint accepts a `FormData` with a `username` property, looks it up in a predefined dictionary of valid users and, if found, sets the user's information to the `user` property of the request's `session`. - The `logout` function clears the `user` value from the current session. - The `getUser` function extracts the value of the current user from the session and returns it. 2. Create a `src/routes/login.tsx` file, and place the following code to it: ```tsx // src/routes/login.tsx import { useSubmission } from '@solidjs/router' import { loginAction } from '../auth.js' import { Show } from 'solid-js' export default function Home() { const sub = useSubmission(loginAction) return ( <>

Login

{sub.result?.message}
) } ``` 3. Replace the content of the `src/routes/index.tsx` file with the following code: ```tsx // src/routes/index.tsx import { getUser, logout } from '../auth.js' import { useNavigate } from '@solidjs/router' import { Show, createSignal, onMount } from 'solid-js' import { remult } from 'remult' import Todo from '../components/Todo.jsx' export default function Home() { const [authenticated, setAuthenticated] = createSignal(false) const navigate = useNavigate() onMount(async () => { remult.user = await getUser() if (remult.authenticated()) setAuthenticated(true) else navigate('/login') }) return (

Todos

Hello {remult.user?.name}
) } ``` - We use the `onMount` hook the update the `remult.user` in the `frontend`, based on the user from the current session. That user info can then be used in the front-end for user roles based content ::: warning Solid Hydration error or page not found As we were working on this tutorial with the rc version of solid start we got this error - we found that **hard refreshing the site (Ctrl F5) solves it**. ::: ### Connect remult-solid-start On the Backend Once an authentication flow is established, integrating it with Remult in the backend is as simple as providing Remult with a `getUser` function from the `src/auth.ts` ```ts{3,7} // src/api.ts import { getUser } from "./auth.js" export const api = remultSolidStart({ //... getUser, }) //... ``` The todo app now supports signing in and out, with **all access restricted to signed in users only**. ## Role-based Authorization Usually, not all application users have the same privileges. Let's define an `admin` role for our todo app, and enforce the following authorization rules: - All signed in users can see the list of tasks. - All signed in users can set specific tasks as `completed`. - Only users belonging to the `admin` role can create, delete or edit the titles of tasks. 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts{7-8,18} // src/shared/Task.ts import { Allow, Entity, Fields } from "remult" @Entity("tasks", { allowApiCrud: Allow.authenticated, allowApiInsert: "admin", allowApiDelete: "admin" }) export class Task { @Fields.uuid() id!: string @Fields.string({ validate: (task) => task.title.length > 2 || "Too Short", allowApiUpdate: "admin" }) title = "" @Fields.boolean() completed = false } ``` 2. Let's give the user _"Jane"_ the `admin` role by modifying the `roles` array of her `validUsers` entry. ```ts{4} // src/auth.ts const validUsers = [ { id: "1", name: "Jane", roles: ["admin"] }, { id: "2", name: "Steve" } ] ``` **Sign in to the app as _"Steve"_ to test that the actions restricted to `admin` users are not allowed. :lock:** ## Role-based Authorization on the Frontend From a user experience perspective it only makes sense that users that can't add or delete, would not see these buttons. Let's reuse the same definitions on the Frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert ```tsx{4,13} // src/components/Todo.tsx
setNewTaskTitle(e.currentTarget.value)} />
...
``` ::: warning Import Show This code requires adding an import of `Show` from `solid-js`. ::: And let's do the same for the `delete` button: ```tsx{15,17} // src/components/Todo.tsx return (
setCompleted(e.target.checked)} /> setTasks(i(), "title", e.target.value)} />
``` This way we can keep the frontend consistent with the `api`'s Authorization rules - Note We send the `task` to the `apiDeleteAllowed` method, because the `apiDeleteAllowed` option, can be sophisticated and can also be based on the specific item's values. # SolidStart - Tutorial - Database # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. **In production, we'd like to use a `Postgres` database table instead.** ::: tip Learn more See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. ::: ::: warning Don't have Postgres installed? Don't have to. Don't worry if you don't have Postgres installed locally. In the next step of the tutorial, we'll configure the app to use Postgres in production, and keep using JSON files in our dev environment. **Simply install `postgres-node` per step 1 below and move on to the [Deployment section of the tutorial](deployment.md).** ::: 1. Install `postgres-node` ("pg"). ```sh npm i pg ``` 2. Add the highlighted code to the `api` server module. ```ts{5,9-11} // src/api.ts //... import { createPostgresDataProvider } from "remult/postgres" export const api = remultSolidStart({ //... dataProvider: createPostgresDataProvider({ connectionString: "your connection string" }) }) ``` # SolidStart - Tutorial - Deployment # Deployment Let's deploy the todo app to [railway.app](https://railway.app/). ## Prepare for Production Modify the highlighted code in the `src/api.ts` to prefer a `connectionString` provided by the production host's `DATABASE_URL` environment variable. ```ts{4,6-8} // src/api.ts //... const DATABASE_URL = process.env["DATABASE_URL"]; export const api = remultExpress({ dataProvider: DATABASE_URL ? createPostgresDataProvider({ connectionString: DATABASE_URL }) : undefined, //... }) ``` ::: warning Note In order to connect to a local PostgresDB, add `DATABASE_URL` to an .env file, or simply replace `process.env["DATABASE_URL"]` with your `connectionString`. If no `DATABASE_URL` has found, it'll fallback to our local JSON files. ::: ## Test Locally To test the application locally run ```sh npm run build npm run start ``` Now navigate to http://localhost:3000 and test the application locally ## Deploy to Railway In order to deploy the todo app to [railway](https://railway.app/) you'll need a `railway` account. You'll also need [Railway CLI](https://docs.railway.app/develop/cli#npm) installed, and you'll need to login to railway from the cli, using `railway login`. Click enter multiple times to answer all its questions with the default answer 1. Create a Railway `project`. From the terminal in your project folder run: ```sh railway init ``` 2. Select `Empty Project` 3. Set a project name. 4. Open the project on `railway` using: ```sh railway open ``` 5. Click the `Add Service` and add: - a Postgres Database - an Empty service 6. Once that's done run the following command to upload the project to railway: ```sh railway up ``` And select the empty service name that was just created to upload the source code to it. 7. Once completed, go back to the railway UI in the browser and select the created service (not the database) 8. Switch to the `variables` tab 9. Click on `+ New Variable`, and in the `VARIABLE_NAME` click `Add Reference` and select `DATABASE_URL` 10. Add another variable called `SESSION_SECRET` and set it to a random string, you can use an [online UUID generator](https://www.uuidgenerator.net/) 11. Switch to the `settings` tab 12. Under `Environment` click on `Generate Domain` 13. Click the `deploy` button to deploy the changes, and wait for the deployment to complete 14. Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live) ::: warning Note If you run into trouble deploying the app to Railway, try using Railway's [documentation](https://docs.railway.app/deploy/deployments). ::: That's it - our application is deployed to production, play with it and enjoy. To see a larger more complex code base, visit our [CRM example project](https://www.github.com/remult/crm-demo) Love Remult?  Give our repo a star.⭐ # Interactive Tutorial - --- type: lesson title: Welcome to the Remult Tutorial template: after-backend-methods focus: /frontend/Todo.tsx --- # Welcome to the Remult Tutorial Hey there, and welcome to the Remult Tutorial! 👋 Remult is a full-stack JavaScript library designed to simplify the development of data-driven applications. It provides: - A powerful Backend ORM - Zero-boilerplate CRUD REST & Realtime APIs - A type-safe API client for the Frontend - TypeScript entities as a single source of truth (SSO) for: - Authorization - Validation - Entity-related business logic By embracing the principles of SSO, Remult streamlines the process of building CRUD applications, making your development experience smoother and more efficient. ## The Tutorial In this tutorial, we'll guide you through the creation of a full-stack todo app, similar to the one you see in the `preview` window on the right. We'll cover: - Setting up a REST API - Integrating with the Frontend - Implementing paging, sorting, and filtering - Performing insert, update, and delete operations - Enabling realtime updates - Adding validation - Writing backend methods - Implementing authentication & authorization - Configuring a database Ready to dive in? Click on the next lesson to start building your app with Remult. Happy coding! 🎉 # Interactive Tutorial - --- type: lesson title: Entity focus: /shared/Task.ts template: before-entity --- # The Entity In Remult, the core element is an `entity`. An entity represents a business object, such as an order or customer. In our tutorial, we'll use a `Task` entity for our todo application. Here's the code for the entity we'll use: ```ts title="shared/Task.ts" add={3-5,7,10,13,16,19} import { Entity, Fields } from 'remult' @Entity('tasks', { allowApiCrud: true, }) export class Task { @Fields.uuid() id = '' @Fields.string() title = '' @Fields.boolean() completed = false @Fields.createdAt() createdAt?: Date } ``` ### Code Explanation - `@Entity('tasks', { allowApiCrud: true })` defines the `Task` entity and configures it to allow all CRUD operations - later we'll restrict that using authorization. - `@Fields.uuid()` generates a unique ID for each task. - `@Fields.string()` and `@Fields.boolean()` define the `title` and `completed` fields, respectively. - `@Fields.createdAt()` automatically sets the creation date. This entity will be used to define the database, API, frontend query language, validation, authorization, and any other definition that revolves around the `task`. We've placed the entity's source code in the `shared` folder to indicate that it's shared between the frontend and the backend. # Interactive Tutorial - --- type: lesson title: Rest Api focus: /backend/index.ts template: before-frontend --- ## The Rest Api For this tutorial, we'll use Express (Remult works with many JavaScript web frameworks including Express, Fastify, Next.js, Sveltekit, nuxt.js, Hapi, Hono, Nest, and Koa). Open `backend/index.ts` and add the following lines to include the `Task` in the REST API: ```ts title="backend/index.ts" add={2,3,6-9} import express from 'express' import { remultExpress } from 'remult/remult-express' import { Task } from '../shared/Task.js' export const app = express() export const api = remultExpress({ entities: [Task], }) app.use(api) ``` ### Code Explanation - We import the necessary `remultExpress` module for integrating Remult with Express. - We import the `Task` entity from the `shared` folder. - We use the `remultExpress` function to set up the Remult REST API and register the `Task` entity in its `entities` array. - Finally, we tell Express to use the API with `app.use(api)`. ### See that it works Click on the `Test the Api` button in the preview window, you should see an empty JSON array in the result. > You can also open the `network` tab in the developer tools and see the requests that are being sent to the nodejs server :::tip If you right click on the `preview` window, and select `inspect`, you'll be able to run the api call directly from the developer tools console (at least on chrome) ```js await fetch('/api/tasks').then((result) => result.json()) ``` ::: # Interactive Tutorial - --- type: lesson title: Insert Data on the Backend focus: /backend/index.ts template: before-frontend --- ## Insert Data on the Backend Next, we'll add some tasks on the backend so we can use them later. ```ts title="backend/index.ts" add={4,9-21} import express from 'express' import { remultExpress } from 'remult/remult-express' import { Task } from '../shared/Task.js' import { repo } from 'remult' export const app = express() export const api = remultExpress({ entities: [Task], initApi: async () => { const taskRepo = repo(Task) if ((await taskRepo.count()) === 0) { await taskRepo.insert([ { title: 'Clean car' }, { title: 'Read a book' }, { title: 'Buy groceries', completed: true }, { title: 'Do laundry' }, { title: 'Cook dinner', completed: true }, { title: 'Walk the dog' }, ]) } }, }) app.use(api) ``` ### Code Explanation - We added the `initApi` option to the `remultExpress` configuration. - `initApi` is an asynchronous function that runs once when the server is loaded and the API is ready. It allows us to perform initial setup tasks for the API. - We use the `repo` function to get the repository for the `Task` entity. The line `const taskRepo = repo(Task)` gets a Repository of type `Task` that we'll use to perform all CRUD operations relevant to `Task`. - The `if ((await taskRepo.count()) === 0)` check ensures that if there are no tasks in the database, we insert a few default tasks to get started. - The `taskRepo.insert([...])` operation inserts an array of tasks into the database if it's initially empty, providing some sample data to work with. ### See That It Works Click on the `Test the API` button in the preview window. You should see a JSON array with the tasks we defined in the result. > **Note:** While Remult supports [many relational and non-relational databases](https://remult.dev/docs/databases.html), in this tutorial we start by storing entity data in a backend **JSON file** stored in the `db` folder for the project. Later in this tutorial, we'll switch to using SQLite. # Interactive Tutorial - --- type: lesson title: Display the Task List focus: /frontend/Todo.tsx autoReload: true --- ## Display the Task List Next, we'll use the tasks from the backend and display them in the frontend. We've prepared a `Todo` React component that displays an array of `Task`. Note that we're using the same "shared" `Task` type that we previously used in the backend. Let's add the following code to display the tasks: ```ts title="frontend/Todo.tsx" add={3,5,9-11} import { useEffect, useState } from 'react' import { Task } from '../shared/Task.js' import { repo } from 'remult' const taskRepo = repo(Task) export function Todo() { const [tasks, setTasks] = useState([]) useEffect(() => { taskRepo.find({}).then(setTasks) }, []) return (

Todos

{tasks.map((task) => { return (
{task.title}
) })}
) } ``` ### Code Explanation - We ask Remult for a `Repository` of type `Task` and store it in `taskRepo`. The repository is used to perform all CRUD operations for our tasks. - Previously, we used a Repository in the `initApi` on the backend to create rows directly in our database. Now, we use the same Repository abstraction on the frontend. When it runs in the frontend, the same methods will perform REST API calls to the backend to get and manipulate the data. - We use the `find` method of the repository in the `useEffect` hook to get the tasks from the backend and set them in the state. This code makes a REST API call to the backend at the `/api/tasks` URL to get the tasks and display them. You can see the tasks in the preview window below. # Interactive Tutorial - --- type: chapter title: Introduction --- # Interactive Tutorial - --- type: lesson title: Paging focus: /frontend/Todo.tsx --- # Paging The RESTful API created by Remult supports server-side paging, sorting, and filtering. Let's use these features to limit, sort, and filter the list of tasks. ```ts title="frontend/Todo.tsx" add={4,5} useEffect(() => { taskRepo .find({ limit: 2, page: 2, }) .then(setTasks) }, []) ``` ### Code Explanation - We update the `useEffect` hook to use the `find` method with paging options. - The `limit` option specifies the number of tasks to retrieve. - The `page` option specifies which page of results to retrieve. This code results in the following REST API request: `/api/tasks?_limit=2&_page=2` Try playing with different values for `limit` and `page` to see the results in the preview window. # Interactive Tutorial - --- type: lesson title: Sorting focus: /frontend/Todo.tsx --- # Sorting Remult's RESTful API also supports server-side sorting. Let's sort the list of tasks by their title. ```tsx title="frontend/Todo.tsx" add={4-6} useEffect(() => { taskRepo .find({ orderBy: { title: 'asc', }, }) .then(setTasks) }, []) ``` ### Code Explanation - We update the `useEffect` hook to use the `find` method with sorting options. - The `orderBy` option specifies the field to sort by and the sort direction (`asc` for ascending, `desc` for descending). - The sorting is performed on the server, which means the server sorts the data before sending it back to the client. This code results in the following REST API request: `/api/tasks?_sort=createdAt&_order=asc` Try changing the sort direction and sorting by different fields to see the results in the preview window. # Interactive Tutorial - --- type: lesson title: Filtering focus: /frontend/Todo.tsx --- # Filtering Remult's RESTful API also supports server-side filtering. Let's filter the list of tasks to show only the completed ones. ```tsx title="frontend/Todo.tsx" add={4-6} useEffect(() => { taskRepo .find({ where: { completed: true, }, }) .then(setTasks) }, []) ``` ### Code Explanation - We update the `useEffect` hook to use the `find` method with filtering options. - The `where` option specifies the condition to filter by. In this case, we're filtering tasks to show only those that are completed (`completed: true`). - The filtering is performed on the server, which means the server filters the data before sending it back to the client. This code results in the following REST API request: `/api/tasks?completed=true` You can experiment with other types of conditions like `$contains`, `$startsWith`, `$and`, `$not`, and `$or`. All these are detailed in the [Remult documentation](https://remult.dev/docs/entityFilter). Try changing the filter condition to see the results in the preview window. # Interactive Tutorial - --- type: chapter title: Querying Data --- # Interactive Tutorial - --- type: lesson title: Insert focus: /frontend/Todo.tsx --- # Inserting Data First, let's add the React code for adding a new task. ```tsx title="frontend/Todo.tsx" add={3-6} export function Todo() { const [tasks, setTasks] = useState([]); const [newTaskTitle, setNewTaskTitle] = useState(''); async function addTask(e: FormEvent) { e.preventDefault(); } useEffect(() => { taskRepo.find().then(setTasks); }, []); ``` ### Code Explanation - We added a state variable `newTaskTitle` to store the title of the new task being added. - We defined the `addTask` function, which will handle the form submission. For now, it just prevents the default form submission behavior with `e.preventDefault()`. Next, let's add the JSX for the form to input new tasks. ```tsx title="frontend/Todo.tsx" add={5-12} return (

Todos

setNewTaskTitle(e.target.value)} />
{tasks.map((task) => { return (
{task.title}
) })}
) ``` ### Code Explanation - We added a form element with an `onSubmit` handler that calls the `addTask` function when the form is submitted. - Inside the form, we added an input element bound to the `newTaskTitle` state variable. The `onChange` handler updates the `newTaskTitle` state as the user types. - We added a button to submit the form. Now let's call Remult to insert the new task. ```tsx title="frontend/Todo.tsx" add={3-9} async function addTask(e: FormEvent) { e.preventDefault() try { const newTask = await taskRepo.insert({ title: newTaskTitle }) setTasks([...tasks, newTask]) setNewTaskTitle('') } catch (error: any) { alert((error as { message: string }).message) } } ``` ### Code Explanation - We used the `taskRepo.insert` method to insert a new task with the title stored in `newTaskTitle`. This makes a REST API `POST` call to the backend to insert the new task into the database. - If the task is successfully inserted, we update the `tasks` state with the new task and clear the `newTaskTitle` input field. - If there's an error, we display an alert with the error message. This code results in the following REST API request to insert the new task: `POST /api/tasks` Try adding new tasks using the form in the preview window below. # Interactive Tutorial - --- type: lesson title: Update focus: /frontend/Todo.tsx --- # Updating Data Let's add the functionality to update a task's completion status. We'll start by defining a function to handle the update. ```tsx title="frontend/Todo.tsx" add={1-4} async function setCompleted(task: Task, completed: boolean) { const updatedTask = await taskRepo.update(task, { completed }) setTasks(tasks.map((t) => (t.id === updatedTask.id ? updatedTask : t))) } useEffect(() => { taskRepo.find().then(setTasks) }, []) ``` ### Code Explanation - Before the `useEffect` hook we added the `setCompleted` function, which takes a `task` and a `completed` status as arguments. - The `taskRepo.update` method updates the `completed` status of the given task. This makes a REST API call to the backend to update the task in the database. - After updating the task, we update the `tasks` state with the updated task, replacing the old task in the list. Next, let's modify the JSX to call the `setCompleted` function when the checkbox is toggled. ```tsx title="frontend/Todo.tsx" add={7} { tasks.map((task) => { return (
setCompleted(task, e.target.checked)} /> {task.title}
) }) } ``` ### Code Explanation - We added an `onChange` handler to the checkbox input that calls the `setCompleted` function when the checkbox is toggled. - The `onChange` handler passes the `task` and the new `checked` status of the checkbox to the `setCompleted` function. This code results in the following REST API request to update the task: `PUT /api/tasks/{taskId}` Try toggling the completion status of tasks using the checkboxes in the preview window below. # Interactive Tutorial - --- type: lesson title: Delete focus: /frontend/Todo.tsx --- # Deleting Data Let's add the functionality to delete a task. We'll start by defining a function to handle the deletion. ```tsx title="frontend/Todo.tsx" add={1-8} async function deleteTask(task: Task) { try { await taskRepo.delete(task) setTasks(tasks.filter((t) => t.id !== task.id)) } catch (error: any) { alert((error as { message: string }).message) } } useEffect(() => { taskRepo.find().then(setTasks) }, []) ``` ### Code Explanation - Before the `useEffect` hook we added the `deleteTask` function, which takes a `task` as an argument. - The `taskRepo.delete` method deletes the given task. This makes a REST API call to the backend to delete the task from the database. - After deleting the task, we update the `tasks` state by filtering out the deleted task from the list. - If there's an error, we display an alert with the error message. - We kept the existing `useEffect` hook to fetch the tasks when the component mounts. Next, let's modify the JSX to call the `deleteTask` function when the delete button is clicked. ```tsx title="frontend/Todo.tsx" add={10-12} { tasks.map((task) => { return (
setCompleted(task, e.target.checked)} /> {task.title}
) }) } ``` ### Code Explanation - We added a button with an `onClick` handler that calls the `deleteTask` function when the button is clicked. - The `onClick` handler passes the `task` to the `deleteTask` function. This code results in the following REST API request to delete the task: `DELETE /api/tasks/{taskId}` Try deleting tasks using the delete buttons in the preview window below. # Interactive Tutorial - --- type: chapter title: Manipulating Data --- # Interactive Tutorial - --- type: lesson title: Required focus: /shared/Task.ts --- # Validation Introduction In most web applications, validation code is spread and duplicated across multiple places. You need frontend validation, and you need API validation, all in various forms, controllers, routes, and services. It's very hard to get validations right across the entire application, and often you'll run into validation errors on the API that do not display gracefully on the frontend. Even worse, validation that is done only on the frontend can be bypassed by calling the API directly. Remult handles that concern with the SSO (Single Source of Truth) approach, where you define your validation once—in your entity—and that validation code runs both on the frontend and the API, ensuring consistent validation across the stack. ## Required Validation Let's start with a simple `required` validation. Adjust the `title` field to be `required`: ```ts title="shared/Task.ts" add={5-7} export class Task { @Fields.uuid() id = '' @Fields.string({ required: true, }) title = '' //.... } ``` ### Code Explanation - We added the `required: true` option to the `title` field using the `@Fields.string` decorator. - This ensures that the `title` field is required and cannot be empty. ### Validation Behavior - This validation will be first checked on the frontend, without making an API call (frontend validation). - The same validation will also be executed on the API, ensuring consistency across the stack. Try adding a task with no title to see the validation in action. Also - checkout the browser's network tab, and see that there is no network call to the backend - the validation is performed in the frontend. > **Note:** In this tutorial, the errors appear in the browser's alert dialog as specified in the code in our `Todo.tsx` component: > > ```tsx title="frontend/Todo.tsx" add={8} > async function addTask(e: FormEvent) { > e.preventDefault() > try { > const newTask = await taskRepo.insert({ title: newTaskTitle }) > setTasks([...tasks, newTask]) > setNewTaskTitle('') > } catch (error: any) { > alert((error as { message: string }).message) > } > } > ``` --- When an error occurs, it returns an error of type `ErrorInfo` that specifies the error for each field, allowing you to create a great UI where the error for a field is displayed next to its input. Here's an example of the error response: ```json { "message": "Title: Should not be empty", "modelState": { "title": "Should not be empty" } } ``` This ensures that the validation error is clear and can be displayed appropriately in the UI. ### Validation on the Api The same validation code that runs in the frontend, is also used to validate the api - if anyone tries to send an invalid request to the api they'll fail. :::tip You can try and bypass the frontend validation, by making a post call directly from the browser's console. Right click on the `preview` window, and select `inspect`, you'll be able to run the api call directly from the developer tools console (at least on chrome) ```js await fetch('/api/tasks', { method: 'POST', body: '' }).then((r) => r.json()) ``` See the result error object that's returned ::: # Interactive Tutorial - --- type: lesson title: Built-In Validations focus: /shared/Task.ts --- # Built-In Validations Remult comes with a set of built-in validations that you can easily choose from. These validations are defined in the `Validators` class. ## Minimum Length Validation For example, let's use the `minLength` validation: ```ts title="shared/Task.ts" add={5-7} export class Task { @Fields.uuid() id = '' @Fields.string({ validate: Validators.minLength(2), }) title = '' //.... } ``` ### Code Explanation - We added the `validate` option to the `title` field using the `Validators.minLength(2)` validation. - This ensures that the `title` field must have at least 2 characters. ## Chaining Multiple Validators You can also chain multiple validators: ```ts title="shared/Task.ts" add={5-7} export class Task { @Fields.uuid() id = '' @Fields.string({ validate: [Validators.minLength(2), Validators.maxLength(5)], }) title = '' //.... } ``` ### Code Explanation - We chained the `Validators.minLength(2)` and `Validators.maxLength(5)` validations. - This ensures that the `title` field must have at least 2 characters and at most 5 characters. ## Customizing Validation Messages You can also customize the validation message: ```ts title="shared/Task.ts" add={5-10} export class Task { @Fields.uuid() id = '' @Fields.string({ validate: [ Validators.minLength(2), Validators.maxLength(5, (length) => `maximum ${length} characters`), ], }) title = '' //.... } ``` ### Code Explanation - We customized the validation message for the `Validators.maxLength(5)` validation. - The custom message function `(length) => 'maximum ${length} characters'` will be used if the validation fails. ### Try It Out Try adding tasks with titles that do not meet these validation requirements to see the validation in action. The errors returned will include the validation messages specified. # Interactive Tutorial - --- type: lesson title: Custom Validations focus: /shared/Task.ts --- # Custom Validations You can also define custom validation logic for your fields. Let's add a custom validation to the `title` field to ensure it is longer than 2 characters. ```ts title="shared/Task.ts" add={5-7} export class Task { @Fields.uuid() id = '' @Fields.string({ validate: (task) => task.title.length > 2 || 'too short', }) title = '' //.... } ``` ### Code Explanation - We added a custom validation function to the `title` field using the `validate` option. - The custom validation function checks if the `title` field's length is greater than 2. If the condition is not met, it returns the error message `'too short'`. - We specified the `Task` type in the generic definition of `Fields.string`, which provides type safety and ensures the custom validation function receives the correct type. - This arrow function will run both on the frontend as frontend validation and on the backend as API validation, ensuring consistent validation across the stack. ### Try It Out Try adding tasks with titles shorter than 3 characters to see the custom validation in action. The error message `'too short'` will be returned if the validation fails. # Interactive Tutorial - --- type: chapter title: Validation --- # Interactive Tutorial - --- type: lesson title: Live Query focus: /frontend/Todo.tsx --- # Introduction Our todo list app can have multiple users using it at the same time. However, changes made by one user are not seen by others unless they manually refresh the browser. ## Try it Out In the preview window below, we can see `user-a` and `user-b`. Try making changes to data as `user-a` and see how `user-b` needs to click `reload page` to see these changes. # Interactive Tutorial - --- type: lesson title: Realtime Updates focus: /frontend/Todo.tsx --- # Realtime Updates To enable real-time updates, we'll modify the `useEffect` hook to use `liveQuery`. ```tsx title="frontend/Todo.tsx" add={2-6} useEffect(() => { return taskRepo.liveQuery().subscribe((info) => setTasks(info.applyChanges)) }, []) ``` ### Code Explanation - We use the `liveQuery` method from `taskRepo` to subscribe to real-time updates for tasks. - The `subscribe` method listens for changes and updates the state with `info.applyChanges`. - We return the result of `subscribe` from the `useEffect` hook, so that once the component unmounts, it will automatically unsubscribe from the updates. ### Try It Out Try making changes as `user-a` in the preview and see the effect on `user-b`. You'll notice that changes made by one user are immediately reflected for the other user without the need to reload the page. ### Implementation Details - The real-time updates implementation is adapter-based. The default implementation used for development and up to several hundreds of users uses Server-Sent Events (SSE). - There are multiple adapters available to use other technologies, including third-party providers such as Ably. ### Simplifying State Management Now that we can rely on `liveQuery`, we no longer need to manually update the `tasks` state, as `liveQuery` will handle that for us. ```tsx title="frontend/Todo.tsx" add={7,16,22} async function addTask(e: FormEvent) { e.preventDefault() try { const newTask = await taskRepo.insert({ title: newTaskTitle, }) // setTasks([...tasks, newTask]); <-- this line is no longer needed setNewTaskTitle('') } catch (error: any) { alert((error as { message: string }).message) } } async function setCompleted(task: Task, completed: boolean) { const updatedTask = await taskRepo.update(task, { completed }) // setTasks(tasks.map((t) => t.id === updatedTask.id ? updatedTask : t)); <-- these lines are no longer needed } async function deleteTask(task: Task) { try { await taskRepo.delete(task) // setTasks(tasks.filter((t) => t.id !== task.id)); <-- these lines are no longer needed } catch (error: any) { alert((error as { message: string }).message) } } ``` ### Code Explanation - In the `addTask`, `setCompleted`, and `deleteTask` functions, we removed the lines that manually update the `tasks` state. - With `liveQuery`, the state updates automatically whenever there are changes to the tasks, simplifying our state management. # Interactive Tutorial - --- type: chapter title: Live Query template: live-query --- # Interactive Tutorial - --- type: lesson title: Updating Multiple Tasks focus: /frontend/Todo.tsx --- # Backend Methods When performing operations on multiple entity objects, performance considerations may necessitate running them on the server. **With Remult, moving client-side logic to run on the server is a simple refactoring.** ## Set All Tasks as Completed or Uncompleted Let's add two buttons to the todo app: "Set All Completed" and "Set All Uncompleted". ### Step 1: Add the `setAllCompleted` Function Add a `setAllCompleted` async function to the `Todo` function component, which accepts a `completed` boolean argument and sets the value of the `completed` field of all the tasks accordingly. ```tsx title="frontend/Todo.tsx" add={1-6} async function setAllCompleted(completed: boolean) { for (const task of await taskRepo.find()) { await taskRepo.update(task, {completed }); } } useEffect(...) ``` ### Code Explanation - The `setAllCompleted` function iterates through the array of `Task` objects returned from the backend and saves each task back to the backend with a modified value in the `completed` field. ### Step 2: Add Buttons to the `Todo` Component Add the two buttons to the return section of the `Todo` component, just before the closing `` tag. Both of the buttons' `onClick` events will call the `setAllCompleted` method with the appropriate value of the `completed` argument. ```tsx title="frontend/Todo.tsx" add={1-4}
``` ### Code Explanation - We added two buttons with `onClick` handlers that call the `setAllCompleted` function with `true` or `false`, respectively, to set all tasks as completed or uncompleted. ### Try It Out Make sure the buttons are working as expected before moving on to the next step. Click the "Set All Completed" button to mark all tasks as completed and the "Set All Uncompleted" button to mark all tasks as uncompleted. ### Performance Considerations With the current state of the `setAllCompleted` function, each modified task being saved causes an API `PUT` request handled separately by the server. As the number of tasks in the todo list grows, this may become a performance issue. In the next lesson, we'll refactor this code to address these performance challenges by moving the logic to the server. --- Would you like to proceed with another section or make further adjustments? # Interactive Tutorial - --- type: lesson title: Refactor to Backend focus: /shared/TasksController.ts --- # Refactor from Frontend to Backend To improve performance, let's refactor the frontend code and move it to the backend. ## Step 1: Create the Backend Method We'll add a `shared/TasksController.ts` file with the following code: ```ts title="shared/TasksController.ts" add={5-11} import { BackendMethod, remult } from 'remult' import { Task } from './Task.js' export class TasksController { @BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo = remult.repo(Task) for (const task of await taskRepo.find()) { await taskRepo.update(task, { completed }) } } } ``` ### Code Explanation - We created a `TasksController` class to contain backend methods. - The `setAllCompleted` method is decorated with `@BackendMethod({ allowed: true })`, making it accessible from the frontend. - Inside `setAllCompleted`, we get the repository for `Task` using `remult.repo(Task)`. - We iterate through the tasks and update each one with the new `completed` status. - Previously, in the frontend, the `taskRepo` repository performed HTTP calls to the backend. Now that we're on the backend, the `taskRepo` repository makes direct API calls to the database. - An advantage of this approach is that using the `taskRepo` repository allows us to use the same coding style for both the frontend and backend, making it easier for us as developers to switch back and forth. ## Step 2: Register the Controller Head over to the `backend/index.ts` file and register the controller: ```ts title="backend/index.ts" add={3} export const api = remultExpress({ entities: [Task], controllers: [TasksController], initApi: async () => { //... }, }) ``` ### Code Explanation - By adding `controllers: [TasksController]`, we make the `TasksController` available for API calls from the frontend. ## Step 3: Adjust the Frontend Adjust the `frontend/Todo.tsx` component to call the backend method: ```tsx title="frontend/Todo.tsx" add={4} async function setAllCompleted(completed: boolean) { await TasksController.setAllCompleted(completed) } ``` ### Code Explanation - We removed the `for` loop and the direct update calls from the frontend. - We now call `TasksController.setAllCompleted(completed)` to perform the updates on the backend. - After the backend method completes, we refresh the task list by calling `taskRepo.find().then(setTasks)`. - An advantage of this approach is that the call to `setAllCompleted` is strongly typed, protecting us from spelling or typing mistakes using TypeScript. ### Try It Out Click the "Set All Completed" and "Set All Uncompleted" buttons to see the improved performance with the backend method handling the updates. # Interactive Tutorial - --- type: chapter title: Backend Methods --- # Interactive Tutorial - --- type: lesson title: Authentication and Authorization template: after-backend-methods focus: /shared/Task.ts --- # Authentication and Authorization Our todo app is nearly functionally complete, but it still doesn't fulfill a very basic requirement - that users should log in before they can view, create, or modify tasks. Remult provides a flexible mechanism that enables placing **code-based authorization rules** at various levels of the application's API. To maintain high code cohesion, **entity and field-level authorization code should be placed in entity classes**. **Remult is completely unopinionated when it comes to user authentication.** You are free to use any kind of authentication mechanism and are only required to provide Remult with an object which implements the Remult `UserInfo` interface. In this tutorial, we'll use `Express`'s [cookie-session](https://expressjs.com/en/resources/middleware/cookie-session.html) middleware to store an authenticated user's session within a cookie. The `user` property of the session will be set by the API server upon a successful simplistic sign-in (based on username without password). ## Tasks CRUD Requires Sign-in This rule is implemented within the `Task` `@Entity` decorator by modifying the value of the `allowApiCrud` property. This property can be set to a function that accepts a `Remult` argument and returns a `boolean` value. Let's use the `Allow.authenticated` function from Remult. ```ts title="shared/Task.ts" add={2} @Entity("tasks", { allowApiCrud: remult.authenticated }) ``` ### Code Explanation - We updated the `allowApiCrud` property in the `Task` entity to use `remult.authenticated`, which ensures that CRUD operations on tasks require an authenticated user. ## Try It Out Try it out and see that once you make this change, no data will appear below since you are not signed in, and therefore not authenticated. ### Authorized Server-side Code Can Still Modify Tasks Although client CRUD requests to `tasks` API endpoints now require a signed-in user, the API endpoint created for our `setAllCompleted` server function remains available to unauthenticated requests. Since the `allowApiCrud` rule we implemented does not affect the server-side code's ability to use the `Task` entity class for performing database CRUD operations, **the `setAllCompleted` function still works as before**. To fix this, let's implement the same rule using the `@BackendMethod` decorator of the `setAllCompleted` method of `TasksController`. ```ts title="shared/TasksController.ts" add={2} export class TasksController { @BackendMethod({ allowed: remult.authenticated }) static async setAllCompleted(completed: boolean) { ``` ### Code Explanation - We updated the `allowed` property in the `@BackendMethod` decorator to use `remult.authenticated`, ensuring that the `setAllCompleted` function requires an authenticated user to execute. # Interactive Tutorial - --- type: lesson title: Authentication template: auth focus: /shared/AuthController.ts --- ## Authentication In this lesson, we'll implement a basic sign-in mechanism using cookie session. Let's add a `shared/AuthController.ts` file and include the following code: ```ts title="shared/AuthController.ts" add={2-3,5-9} import { BackendMethod, remult } from 'remult' import type express from 'express' import type from 'cookie-session' declare module 'remult' { export interface RemultContext { request?: express.Request } } export class AuthController { // } ``` ### Code Explanation - We import the necessary modules from `remult` and types for `express` and `cookie-session`. - We extend the `RemultContext` interface to include an optional `request` property of type `express.Request`. - Remult will automatically set the `request` with the current request. Since Remult works with any server framework, we need to type it to the correct server, which in this case is Express. This typing gives us access to the request object and its session, managed by `cookie-session`. - This `request` can be accessed using `remult.context.request`. Next, we'll add a static list of users and a sign-in method. (In a real application, you would use a database, but for this tutorial, a static list will suffice.) ```ts title="shared/AuthController.ts" add={1,4-17} const validUsers = [{ name: 'Jane' }, { name: 'Alex' }] export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name) if (user) { remult.user = { id: user.name, name: user.name, } remult.context.request!.session!['user'] = remult.user return remult.user } else { throw Error("Invalid user, try 'Alex' or 'Jane'") } } } ``` ### Code Explanation - We define a static list of valid users. - The `signIn` method is decorated with `@BackendMethod({ allowed: true })`, making it accessible from the frontend. - The method checks if the provided `name` exists in the `validUsers` list. If it does, it sets `remult.user` to an object that conforms to the `UserInfo` type from Remult and stores this user in the request session. - If the user is not found, it throws an error. Next, we'll add the sign-out method: ```ts title="shared/AuthController.ts" add={7-11} export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { //... } @BackendMethod({ allowed: true }) static async signOut() { remult.context.request!.session!['user'] = undefined return undefined } } ``` ### Code Explanation - The `signOut` method clears the user session, making the user unauthenticated. Next, we'll adjust the `backend/index.ts` file: ```ts title="backend/index.ts" add={2-3,9-14,19-20} import express from 'express' import session from 'cookie-session' import { AuthController } from '../shared/AuthController' //... export const app = express() app.enable('trust proxy') // required for stackblitz and other reverse proxy scenarios app.use( session({ signed: false, // only for dev on stackblitz, use secret in production // secret: process.env['SESSION_SECRET'] || 'my secret', }), ) export const api = remultExpress({ entities: [Task], controllers: [TasksController, AuthController], getUser: (request) => request.session?.['user'], //... }) ``` ### Code Explanation - The `signOut` method clears the user session, making the user unauthenticated. - We import `session` from `cookie-session` and `AuthController`. - We enable `trust proxy` for reverse proxy scenarios like StackBlitz. - **We've set `signed: false` in the session configuration** due to an issue in StackBlitz that causes problems with signed cookies. This is for development purposes only and **in production**, you should **remove `signed: false`** and **encrypt the cookie using a secret** by setting the `secret` option (e.g., `secret: process.env['SESSION_SECRET'] || 'my secret'`). - We register `AuthController` in the `controllers` array. - We add `getUser: (request) => request.session?.['user']` to extract the user from the session. ### Frontend Authentication In `frontend/Auth.tsx`, we'll call the `AuthController` to sign in, sign out, etc. ```tsx title="frontend/Auth.tsx" add={3-7,11,15} async function signIn(f: FormEvent) { f.preventDefault() try { setCurrentUser((remult.user = await AuthController.signIn(name))) } catch (error) { alert((error as ErrorInfo).message) } } async function signOut() { setCurrentUser(await AuthController.signOut()) } useEffect(() => { remult.initUser().then(setCurrentUser) }, []) ``` ### Code Explanation - The `signIn` function calls `AuthController.signIn` and sets the current user if successful. - The `signOut` function calls `AuthController.signOut` to clear the current user. - The `useEffect` hook uses the `initUser` method to fetch the current user when the component mounts. ### Try It Out Try signing in as `Alex` or `Jane` and verify that you can perform CRUD operations on tasks. Sign out and ensure that you can no longer access the tasks. # Interactive Tutorial - --- type: lesson title: Role-based Authorization template: auth-3 focus: /shared/Task.ts --- ## Role-based Authorization In most applications, different users have different levels of access. Let's define an `admin` role for our todo app and enforce the following authorization rules: - All signed-in users can see the list of tasks. - All signed-in users can mark specific tasks as `completed`. - Only users with the `admin` role can create or delete tasks. ### Step 1: Modify the Task Entity Class 1. Modify the highlighted lines in the `Task` entity class to reflect the top three authorization rules. ```ts title="shared/Task.ts" add={3-4} @Entity('tasks', { allowApiCrud: remult.authenticated, allowApiInsert: 'admin', allowApiDelete: 'admin', }) export class Task { //... } ``` ### Code Explanation - `allowApiCrud: remult.authenticated`: Ensures that only authenticated users can perform basic CRUD operations. - `allowApiInsert: 'admin'`: Restricts the creation of tasks to users with the `admin` role. - `allowApiDelete: 'admin'`: Restricts the deletion of tasks to users with the `admin` role. ### Step 2: Assign Roles in the AuthController 2. Let's make _"Jane"_ an admin and use it to determine her roles in the `signIn` method. ```ts title="shared/AuthController.ts" add={4,17} const validUsers = [ { name: 'Jane', admin: true, }, { name: 'Alex' }, ] export class AuthController { @BackendMethod({ allowed: true }) static async signIn(name: string) { const user = validUsers.find((user) => user.name === name) if (user) { remult.user = { id: user.name, name: user.name, roles: user.admin ? ['admin'] : [], } remult.context.request!.session!['user'] = remult.user return remult.user } else { throw Error("Invalid user, try 'Alex' or 'Jane'") } } //... } ``` ### Code Explanation - We added an `admin` property to the `Jane` user object in the `validUsers` array. - In the `signIn` method, we assign the `admin` role to `remult.user.roles` if the user is an admin. If the user is not an admin, `roles` is set to an empty array. - The user's role is stored in the session, allowing Remult to enforce authorization rules based on the user's role in subsequent requests. ### Try It Out Sign in to the app as _"Alex"_ to test that actions restricted to `admin` users, such as creating or deleting tasks, are not allowed. Then, sign in as _"Jane"_ to confirm that these actions are permitted for admin users. # Interactive Tutorial - --- type: lesson title: Role-based Authorization on the Frontend template: auth-3 focus: /frontend/Todo.tsx --- ## Role-based Authorization on the Frontend From a user experience perspective, it only makes sense that users who can't add or delete tasks shouldn't see the buttons for those actions. Let's reuse the same authorization definitions on the frontend. We'll use the entity's metadata to only show the form if the user is allowed to insert tasks. ### Step 1: Hide the Add Task Form ```tsx title="frontend/Todo.tsx" add={5,14} return (

Todos

{taskRepo.metadata.apiInsertAllowed() && (
setNewTaskTitle(e.target.value)} />
)} {error && (
Error: {error.message}
)} ``` ### Code Explanation - We use `taskRepo.metadata.apiInsertAllowed()` to check if the current user is allowed to insert tasks. If the user has the required permissions, the form to add a new task is displayed; otherwise, it's hidden. ### Step 2: Hide the Delete Button Let's apply the same logic to the `delete` button: ```tsx title="frontend/Todo.tsx" add={11,18} { tasks.map((task) => { return (
setCompleted(task, e.target.checked)} /> {task.title} {taskRepo.metadata.apiDeleteAllowed(task) && ( )}
) }) } ``` ### Code Explanation - We use `taskRepo.metadata.apiDeleteAllowed(task)` to check if the current user is allowed to delete the specific task. The delete button is only displayed if the user has the necessary permissions. - We pass the `task` object to the `apiDeleteAllowed` method because this authorization check can be more sophisticated and might depend on the specific values of the task. ### Keeping the Frontend Consistent By using these methods, we ensure that the frontend stays consistent with the API's authorization rules. Users only see the actions they are allowed to perform, creating a seamless and secure user experience. ### Try It Out Test the app by signing in as different users (e.g., as an admin and a regular user) and verify that the add and delete buttons appear or disappear based on the user's role. # Interactive Tutorial - --- type: chapter title: Authentication & Authorization --- # Interactive Tutorial - --- type: lesson title: Database focus: /backend/index.ts template: after-backend-methods --- # Database Up until now the todo app has been using a plain JSON file to store the list of tasks. In this lesson we'll demontstrate how easy it is to switch to one of the many databases that are supported by remult, in this case Sqlite. > ##### Learn more > > See the [Quickstart](https://remult.dev/docs/quickstart.html#connecting-a-database) article for the (long) list of relational and non-relational databases Remult supports. In the `backend/index.ts` file, set the `dataProvider` to use `sqlite` ```ts title="backend/index.ts" add={4-6} export const api = remultExpress({ entities: [Task], controllers: [TasksController], dataProvider: new SqlDatabase( new Sqlite3DataProvider(new sqlite3.Database('.database.sqlite')), ), //... }) ``` And that's it, now you use `sqlite` as the database. > ##### Don't believe it? > > You can see the actual sql's executed by remult, by adding the following line > > ```ts > SqlDatabase.LogToConsole = 'oneLiner' > ``` > > And click on `Toggle Terminal` button to see the sql's that execute with the operations you perform > Just don't forget to turn it off once you're done, to improve performance # Interactive Tutorial - --- type: chapter title: Database --- # Interactive Tutorial - --- type: part title: Basics --- # Interactive Tutorial - --- type: lesson title: Many to One template: relations focus: /shared/Order.ts --- # Relations In this chapter, we’ll explore how to work with entity relations in Remult using customers and orders as our example. ![Relations](./relations.png) ## Many-to-One Relation To create a many-to-one relation in Remult, we use the `@Relations.toOne` decorator. This decorator establishes a connection between the `Order` entity and the `Customer` entity, where multiple orders can be associated with a single customer. For instance, in our project, the `Order` entity includes a reference to the `Customer` entity. The highlighted lines below in `Order.ts` show how this relation is defined: ```file:/shared/Order.ts ins={8-9} collapse={1-3} title="shared/Order.ts" ``` This setup creates a many-to-one relationship where each `Order` is connected to a `Customer`. ### Seed Data In the `SeedData` file, you can see how this relationship is leveraged. While inserting data into the `Order` table, the `customer` field is populated with a customer object that was previously inserted: ```file:/shared/SeedData.ts title="shared/SeedData.ts" collapse={1-4} add=/customer:\s*\w+/ ``` This snippet shows how orders reference existing customer objects, creating a meaningful connection between the two entities. ### Fetching Relational Data When querying `Order` data, we can use the `include` option to retrieve the associated customer data. By default, relations are not automatically included in queries unless explicitly requested. Here’s how to include the `Customer` info when fetching orders: ```ts title="frontend/Page.tsx" add={2-4} const orders = await repo(Order).find({ include: { customer: true, }, }) ``` This will return the `Order` data along with the related `Customer` data. If you set the `include` value to `false`, the `customer` field will be `undefined` in the result. You can experiment by toggling the `include` value between `true` and `false` to observe how the results change. ### Always Including a Relation If you want the related `Customer` data to be automatically included in every query, you can set the `defaultIncluded` option when defining the relation. This ensures the relation is always loaded unless explicitly excluded: ```ts add=/defaultIncluded: true,/ @Relations.toOne(() => Customer, { defaultIncluded: true, }) customer?: Customer ``` This setting saves you from having to manually include the relation in each query, ensuring the related data is always available when you need it. By using relations effectively, you can create more sophisticated and connected data models, enhancing the power and flexibility of your applications built with Remult. Here’s a polished version of the paragraph: ### Relations in Remult Admin Relations are seamlessly integrated into the [Remult Admin UI](https://remult.dev/docs/admin-ui). To explore how relations are displayed, simply click the "Remult Admin UI" link at the bottom. # Interactive Tutorial - --- type: lesson title: One to Many template: relations focus: /shared/Customer.ts --- # One to Many In this lesson, we'll explore how to set up and work with a one-to-many relation in Remult, where one `Customer` can have many `Order` records. ## Defining the Relation We begin by setting the relation in the `shared/Customer.ts` file. This one-to-many relation will allow a customer to be linked to multiple orders. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-3} ins={12-13} ``` This creates a one-to-many relation where each `Customer` can have multiple `Order` records. The `orders` field is now an array of `Order` objects. ## Fetching Related Data Just like with many-to-one relations, you can use the `include` option in the `find` method to fetch related `Order` data for each `Customer`. This ensures that the associated orders are included in your query results. ```ts title="frontend/Page.tsx" add={2-4} const customers = await repo(Customer).find({ include: { orders: true, }, }) ``` This query fetches customers and includes their related orders. You can experiment by toggling the `include` value between `true` and `false` to observe how the results change. ## Inserting Child Entities (Orders) into a Parent (Customer) You can also insert related `Order` items directly into a `Customer` repository. For example, in the `shared/SeedData.ts` file, you can insert customer records and their corresponding orders as shown below: ```file:/shared/SeedData.ts title="shared/SeedData.ts" add={12-26} ``` Here’s what’s happening: - First, we create three customer records using `cRepo.insert()`. - Then, we use `cRepo.relations(c1).orders.insert()` to insert orders related to `Customer 1`. - Similarly, we insert related orders for `Customer 2` and `Customer 3`. By using the `relations` method provided by the repository, you can easily manage the insertion of related child entities (in this case, orders) directly into their parent (customer). ## Repository Methods for Relations Most repository methods, such as `find`, `insert`, `update`, `updateMany`, `delete`, and `deleteMany`, can be used in this way through the `relations` method. This allows you to perform various operations on related entities within the context of their parent entity. For example, you can retrieve all orders related to a customer: ```ts const ordersForCustomer = await cRepo.relations(customer).orders.find() ``` This flexibility makes it easy to manage related data within Remult, simplifying many common data manipulation tasks. --- In this lesson, we've learned how to define a one-to-many relation between `Customer` and `Order`, and how to query and insert related data using Remult. These techniques give you the power to effectively model and work with complex data relationships in your applications. Here’s a polished version of the text: --- ### Relations in Remult Admin In the Remult Admin UI, `one-to-many` relations are displayed directly within the table view. For example, you can see all the orders associated with a customer right from the `Customer` table view. ![Customers and their orders](./to-many-in-the-admin.png) To explore how this works, click the "Remult Admin UI" link at the bottom left of the interface. # Interactive Tutorial - --- type: lesson title: Id Based Relations template: relations focus: /shared/Order.ts --- # ID-Based Relations ID-based relations provide more control over how related entities are managed. By explicitly including the foreign key (such as `customerId`) in the entity, you gain more flexibility and can optimize performance by reducing the need to load the related entity in some cases. ## Defining an ID-Based Relation In the `Order` entity, we add a `customerId` field to store the ID of the related `Customer`. We then reference this field in the `@Relations.toOne` decorator to establish the relationship between `Order` and `Customer`. It's important to use the correct type arguments, ``, to ensure proper type checking for this relation. ```file:/shared/Order.ts ins={8-10} collapse={1-3} title="shared/Order.ts" ``` In this setup, the `customerId` field holds the reference to the customer, and the `@Relations.toOne` decorator connects the `Order` entity to the `Customer` entity. ## Defining the Inverse Relation On the `Customer` entity, we define the inverse of the relation using `@Relations.toMany`. This decorator links a `Customer` to multiple `Order` records, allowing us to retrieve all orders related to a specific customer. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-3} ins={12-13} ``` Now, the `Customer` entity has an `orders` array, representing all the orders associated with that customer. ## Try it out Check out the output and see that the `customerId` is included even if you do not explicitly include the relation. This gives you the flexibility to work directly with the ID without always needing to load the related entity. ## Working with Existing Data If you already have existing data in your database where the foreign key column is named `customer`, but you want to use `customerId` in your code, you can use the `dbName` property to map the `customerId` field to the `customer` column in the database. ```ts @Fields.integer({ dbName: 'customer' }) customerId = 0 ``` This ensures that your code uses `customerId` while mapping it correctly to the `customer` column in the database, allowing for seamless integration with existing data. --- By using ID-based relations, you have greater control over your data models and can optimize performance by limiting unnecessary entity loading. This approach also provides a clean and efficient way to manage relations in your Remult applications. --- Let me know if this works! # Interactive Tutorial - --- type: lesson title: Many to Many template: relations-with-products focus: /shared/ProductInOrder.ts --- # Many-to-Many Relations In some applications, an entity might need to have a relationship with multiple entities from another table, and vice versa. In our case, an `Order` can contain many `Products`, and the same `Product` can appear in many `Orders`. This is a classic **many-to-many** relationship. To implement this relationship, we use an intermediate entity called `ProductInOrder`, which serves as a bridge between `Order` and `Product`. This intermediate entity stores the association between an order and a product, along with additional details that pertain to the relationship, such as quantity or price, if needed. ![Many to Many relation](./diagram.png) ### Defining the Many-to-Many Relation Let's define the `ProductInOrder` entity, which establishes the many-to-many relation between `Order` and `Product`. ```file:/shared/ProductInOrder.ts title="shared/ProductInOrder.ts" ``` ### Key Points 1. **Composite Primary Key**: We define the composite primary key using both `orderId` and `productId` (`id: ['orderId', 'productId']`). This ensures that the combination of these two fields uniquely identifies each record in `ProductInOrder`. Using a composite key improves performance when querying or joining tables because it provides a direct and efficient way to locate specific rows. 2. **Relation to `Product`**: The `@Relations.toOne()` decorator establishes a relationship between `ProductInOrder` and the `Product` entity. This allows us to easily fetch the related `Product` information (such as the name or price) when querying `ProductInOrder`. ### Defining the `Order` Entity In the `Order` entity, we use a `toMany` relation to link each order to its products through the `ProductInOrder` entity. This allows us to keep track of all the products in a particular order. ```file:/shared/Order.ts title="shared/Order.ts" collapse={1-4,8-13} add={15-16} ``` ### Querying the Data When you want to fetch the data, including the related products, you can use the `include` option to fetch not only the `ProductInOrder` records but also the corresponding `Product` details. ```ts title="frontend/Page.tsx" repo(Customer).find({ include: { orders: { include: { products: { include: { product: true, // Fetch product details such as name, price, etc. }, }, }, }, }, }) ``` ### Why Not Use a Built-in Many-to-Many Feature? A natural question arises: why not create a built-in many-to-many relation directly between `Order` and `Product` without an intermediate table? The reason is that most **many-to-many relationships** in real-world applications are not that simple. Typically, you will need to store additional information about the relationship itself, such as **quantity**, **pricing**, **discounts**, or **status**. By using an intermediate entity like `ProductInOrder`, you have the flexibility to store these additional attributes alongside the relationship. This approach is much more versatile and better suited to real-world use cases than a basic many-to-many relation, which can be too limited for most scenarios. ### Summary In this lesson, we've learned how to model many-to-many relationships using an intermediate table. By creating the `ProductInOrder` entity, we've enabled a flexible many-to-many relationship between `Order` and `Product`. This approach allows us to include additional fields, such as quantity, in the relationship while maintaining optimal performance through the use of composite keys. # Interactive Tutorial - --- type: chapter title: Relations --- # Interactive Tutorial - --- type: lesson title: Custom Filter template: relations focus: /frontend/Page.tsx --- # Custom Filter In this lesson, we'll explore how to simplify and reuse filtering logic in your application using **Custom Filters**. Consider the following scenario where we need to filter `Order` records by status and a specific year: ```file:/frontend/Page.tsx title="frontend/Page.tsx" collapse={1-5,21-100} {11-17} ``` If you need to apply this same filter in multiple places throughout your application, this can lead to repetitive code. Rewriting this filter over and over not only increases the likelihood of errors but also makes the code harder to maintain and less readable. This is where **Custom Filters** come in handy. Custom Filters allow you to encapsulate and reuse filtering logic, simplifying your code and improving maintainability. ## Benefits of Custom Filters Custom Filters offer several advantages: 1. **Executes on the Server**: The filter is processed on the server, meaning it has the full power of the backend. This allows for more complex operations, such as database queries, and offloads the filtering from the client, improving performance. 2. **Reusability**: You can reuse the same filtering logic in different parts of your application without rewriting it each time. 3. **Maintainability**: If you need to update your filtering criteria, you can do it in one place, ensuring consistency throughout your app. 4. **Readability**: By encapsulating the filter logic, your code becomes cleaner and easier to understand. 5. **Flexibility**: Custom Filters allow you to pass dynamic parameters, making them adaptable to various scenarios. ## Defining a Custom Filter in the Order Entity In the `Order` entity, we'll define a custom filter to encapsulate the filtering logic for active orders within a given year. ```solution:/shared/Order.ts title="shared/Order.ts" collapse={1-9,11-28} add={30-40} ``` ### Breakdown of the Code 1. **Custom Filter Definition**: - `Filter.createCustom()` defines a custom filter for the `Order` entity. - The filter accepts an argument object with a `year` property and returns an object representing the filter criteria. - In this case, the filter checks for `Order` records with specific statuses and filters orders within the specified year. 2. **Static Method**: - The `activeOrdersFor` is a static method, meaning it belongs to the class itself and can be used without creating an instance of the `Order` class. - This method dynamically generates the filter based on the `year` parameter passed in by the user. 3. **Status and Order Date Filters**: - The filter checks if the order's status is one of the following: `'created', 'confirmed', 'pending', 'blocked', 'delayed'`. - It also filters the orders by their `orderDate`, ensuring only orders from the specified year are included in the results. ## Using the Custom Filter Once the custom filter is defined, you can use it in your code as follows: ```solution:/frontend/Page.tsx title="shared/Page.tsx" collapse={1-5,15-100} add={11} ``` ### Explanation of Usage - **Simplified Code**: By using `Order.activeOrdersFor({ year })`, you're applying the filtering logic in a clean and reusable manner. You no longer need to duplicate the filtering conditions wherever this logic is required. - **Dynamic Parameters**: The `{ year }` argument allows the filter to be used with different years, making it adaptable to different contexts. - **Backend Evaluation**: The filtering is handled on the backend, meaning that you avoid sending large datasets to the client and applying the filters there, which optimizes performance. ## Composability: Combining Filters One of the powerful features of custom filters is their **composability**. You can combine a custom filter with other filters to create more complex query logic. This is useful when you want to add additional filtering conditions on top of your custom filter. ### Example: Combining with an `amount` Filter Let’s say you want to find active orders for a given year, but you also want to filter the orders based on their total `amount`. You can easily combine filters using the `$and` operator: ```tsx repo(Order).find({ where: { $and: [ Order.activeOrdersFor({ year }), { amount: { $gt: 100 }, }, ], }, }), ``` ### Explanation 1. **Custom Filter (`activeOrdersFor`)**: Filters the orders by their status and order date for the specified year. 2. **Additional Filter (Amount)**: The second filter adds an extra condition that only includes orders where the total amount is greater than 100. 3. **Combining with `$and`**: The `$and` operator combines both filters, ensuring that only orders that satisfy both the `activeOrdersFor` filter and the `amount` filter are included in the results. ### Recap With composable custom filters, you can build modular, reusable filters that combine seamlessly with other conditions, making your code more flexible and maintainable. Whether you're filtering by status, date, or custom logic like order amount, custom filters allow you to easily manage complex queries with less effort. # Interactive Tutorial - --- type: lesson title: Filter Based on Relation template: relations focus: /shared/Order.ts --- # Filter Based on Relation When working with relational data, you might encounter scenarios where you need to filter records based on data from related entities. A common example is retrieving all orders for customers located in specific cities, such as London or New York. In this lesson, we'll explore how to achieve this by utilizing **custom filters** that apply conditions to related entities. This approach makes it easier to handle complex filtering requirements while keeping your code clean and reusable. ## Scenario: Filtering Orders by Customer's City Imagine we want to display all orders from customers who live in either London or New York. To accomplish this, we need to filter the `Order` entity based on a related field (`city`) from the `Customer` entity. We'll define a **custom filter** in the `Order` entity that allows us to query orders based on the city of the related customer. ## Step 1: Define the Custom Filter In the `Order` entity, we will create a custom filter called `fromCity` that will filter orders based on the city of the related customer. This filter will retrieve the customers from the specified city and then use their `id` values to filter the corresponding orders. ```file:/shared/Order.ts title="shared/Order.ts" collapse={1-8,10-21} {23-35} ``` ### Explanation of the Code 1. **Customer and Order Entities**: The `Order` entity is related to the `Customer` entity via the `customerId` field. The `@Relations.toOne` decorator establishes this relation. 2. **Custom Filter (`fromCity`)**: - This custom filter queries the `Customer` repository to find customers whose `city` contains the specified string (e.g., "New York" or "London"). - Once the customers are retrieved, their `id` values are used to filter orders by `customerId`. This approach allows us to query orders based on data from the related `Customer` entity. 3. **Backend Execution**: The custom filter logic is executed on the server, meaning the customer retrieval and the subsequent filtering happen on the backend, ensuring efficient data handling. --- ## Step 2: Using the Filter on the Frontend To apply the `fromCity` custom filter in our frontend component, we'll use it in a `find` method to retrieve the relevant orders. Additionally, we will combine this filter with an extra condition to only include orders with an `amount` greater than 5. Here’s the implementation in the frontend: ```file:/frontend/Page.tsx title="/frontend/Page.tsx" collapse={1-6,23-37} add={12} ``` ### Explanation of the Frontend Code 1. **Combining Filters**: - We use the `fromCity` custom filter to get all orders from customers living in New York. - The `$and` operator combines this filter with an additional condition, ensuring that only orders with an `amount` greater than 5 are included. 2. **Including Related Data**: - The `include` option is used to include customer data (e.g., city, name) in the result, allowing us to display the customer's city alongside the order information. 3. **Displaying the Data**: - The fetched data is displayed in the component, with each order showing its ID, order date, customer city, and amount. --- ## Benefits of Using Custom Filters with Relations ### 1. Flexibility in Filtering Custom filters allow you to define dynamic filtering logic that can be reused across your application. In this example, the `fromCity` filter can be applied in any scenario where you need to retrieve orders based on the customer's city, making the filtering logic more flexible and reusable. ### 2. Backend Efficiency By executing the filter on the server, custom filters can take full advantage of backend resources, such as querying a database. This offloads the data processing from the frontend, resulting in faster performance and reduced data transfer. ### 3. Composability Custom filters can be combined with other conditions (e.g., filtering by order amount) to create complex and nuanced queries. This composability ensures that you can handle a wide variety of filtering needs without duplicating logic. ### 4. Cleaner Code By encapsulating the filtering logic in the `Order` entity, we avoid cluttering the frontend code with complex query conditions. This makes the frontend code cleaner and easier to maintain. --- ### Summary Filtering based on relations is a common requirement in web applications, and custom filters provide an elegant way to handle it. By encapsulating the filtering logic in reusable components, we can efficiently query data based on related entities while keeping the code clean, readable, and maintainable. The flexibility, efficiency, and composability of custom filters make them an essential tool for managing complex filtering scenarios in your applications. # Interactive Tutorial - --- type: lesson title: Filter Based on Relation Using SQL template: relations focus: /shared/Order.ts --- # Filter Based on Relation Using SQL In this lesson, we'll explore how to perform advanced filtering using SQL directly within custom filters. While Remult allows us to define filters using high-level syntax, there are cases where SQL queries can provide even more control, flexibility, and performance for filtering based on related entities. Imagine a scenario where you want to filter orders based on the city of the customer, but this time, we'll leverage raw SQL to enhance performance, handle more complex conditions, and directly access the underlying database features. ## Why Use SQL in Custom Filters? - **Performance**: SQL-based filters allow you to use the full power of the database's query optimizer, ensuring that complex joins and subqueries are handled efficiently. - **Advanced Capabilities**: SQL provides access to advanced features like joins, aggregate functions, and subqueries, which can be harder to express in high-level filtering syntax. - **Flexibility**: SQL filters allow for precise control over how your queries are executed, including optimizations like using indexes or specific execution plans. - **Backend Execution**: Since these filters run on the server, they take advantage of server-side resources and avoid transferring unnecessary data to the frontend. ## Scenario: Filtering Orders by Customer's City Using SQL Let's revisit our previous example where we filtered orders based on the customer's city. This time, we'll implement the filter using raw SQL for maximum control and efficiency. In the `Order` entity, we'll define a custom filter using `SqlDatabase.rawFilter` to filter orders by the `city` field from the related `Customer` entity. ```file:/shared/Order.ts title="shared/Order.ts" collapse={1-15,17-27} {29-47} ``` ### Explanation of the Code 1. **Using `dbNamesOf`**: - The `dbNamesOf` utility is used to dynamically generate the correct column names for the `Order` and `Customer` entities, including the table name prefixes. This ensures that the generated SQL query matches the database schema and avoids potential naming conflicts. - For the `Customer` entity, we specify an alias (`'c'`) for the table to make the SQL query more readable. 2. **SQL-Based Filter**: - The `SqlDatabase.rawFilter` function allows us to define a custom SQL query for filtering. We use a subquery to select the `id` values of customers whose `city` matches the provided value. These `id` values are then used to filter orders based on the `customerId` field. - The `param` function ensures that the city parameter is properly escaped, protecting against SQL injection and improving security. 3. **Efficiency**: By using SQL directly, we ensure that the filtering is performed in the database, leveraging its optimized querying capabilities. This is particularly useful for large datasets or complex conditions. --- ## Step 2: Using the SQL Filter on the Frontend Now, let's use the `fromCity` SQL-based custom filter in the frontend component to fetch orders where the customer's city matches "London" or "New York". ```file:/frontend/Page.tsx title="/frontend/Page.tsx" collapse={1-6,23-37} add={12} ``` ### Explanation of the Frontend Code 1. **Using the SQL Filter**: - We use the `fromCity` custom filter to retrieve orders from customers whose city contains "New York". This filter is applied as part of the `find` query, **just like using any other filter**. The fact that this filter is SQL-based is abstracted away in the frontend code, making it seamless for developers to use without needing to worry about the underlying SQL logic. - We also apply an additional condition to filter orders where the `amount` is greater than 5, further showcasing how custom filters can be combined with standard filters for flexible data retrieval. 2. **Combining Filters**: - The `$and` operator is used to combine the SQL-based filter with other conditions, such as filtering by order `amount`. This demonstrates the **composability of filters**, where you can easily build more complex queries by combining different filtering logic together. 3. **Displaying the Data**: - The customer details (including the city) are included in the result and displayed alongside the order information in the frontend. The SQL filter seamlessly integrates into the data retrieval process, with the **SQL being evaluated on the backend** to maximize security and efficiency. By keeping the SQL processing on the server, the risk of exposing sensitive logic or data manipulation vulnerabilities on the client side is greatly reduced. This approach allows you to write highly performant and secure filters while maintaining a clean and familiar syntax on the frontend. The backend handles the complexity and ensures that only the necessary data is passed to the frontend, without exposing raw SQL queries or internal database structures. --- ## SQL Query Logging To help debug and optimize your queries, you can enable SQL query logging in Remult. This will print the actual SQL queries being executed to the terminal, allowing you to inspect the generated SQL and ensure it's behaving as expected. To enable SQL logging, simply add the following line to your code: ```ts SqlDatabase.LogToConsole = true ``` With this enabled, the SQL queries executed by Remult will be logged to the console, giving you insight into how your filters are being translated into SQL. --- ### Translating Standard EntityFilter to SqlFilter Using `filterToRaw` One of the powerful features of Remult is the ability to translate standard `EntityFilter` objects into SQL queries using `filterToRaw`. This allows you to define filters in a more declarative, high-level way while still taking advantage of SQL's performance and flexibility on the backend. In this section, we'll demonstrate how using `filterToRaw` within a custom filter can enhance flexibility and reduce complexity. #### Example: `fromCity` Filter with `filterToRaw` In this example, we enhance the `fromCity` custom filter by using `filterToRaw` to dynamically translate a standard `EntityFilter` into a SQL query. This approach combines the declarative nature of `EntityFilter` with the efficiency of SQL-based filtering. ```solution:/shared/Order.ts title="shared/Order.ts" collapse={1-15,17-28} {42,44-46} ``` ### Breakdown of the Code 1. **Declarative Filter with `EntityFilter`**: - Instead of manually crafting a SQL filter for the customer's `city` field, we define a standard `EntityFilter` using `{ city: { $contains: city } }`. This makes the filter more flexible, readable, and consistent with how you would typically filter data using Remult. 2. **Dynamic SQL Translation with `filterToRaw`**: - The `filterToRaw` function takes the `EntityFilter` and translates it into a SQL condition. This SQL condition is then inserted directly into the larger SQL query that filters orders based on their associated customers' cities. - In this case, we are dynamically generating the `WHERE` clause for the `Customer` table to match records where the `city` contains the specified string. 3. **Efficient SQL Query Generation**: - The final SQL query is generated based on both the `EntityFilter` for the `Customer` and the overall filter for the `Order`. This ensures that the query is executed efficiently on the backend, leveraging the power of SQL to perform the filtering operation. - By relying on `filterToRaw`, the SQL translation is handled automatically, ensuring that the query is optimized and preventing potential errors when manually crafting SQL conditions. ### Advantages of Using `filterToRaw` 1. **Simplified Code**: - Using `filterToRaw` allows you to avoid manually writing raw SQL conditions for each filter. Instead, you can rely on the higher-level, declarative `EntityFilter` syntax, which is easier to read, maintain, and reuse. 2. **Consistency Across Filters**: - `filterToRaw` ensures that your filters are consistent with how filtering is typically done in Remult. Whether you are using the standard `find` method or a custom SQL-based filter, the filter logic remains the same, reducing duplication and potential errors. 3. **Leverage SQL Efficiency**: - While the filter is written in a declarative form, it is translated into highly efficient SQL that runs on the backend. This ensures that complex filtering logic can still take advantage of SQL's performance and indexing capabilities. 4. **Flexibility**: - With `filterToRaw`, you can easily apply other standard Remult filters in conjunction with SQL-based filters. This allows you to build complex filtering logic without losing the benefits of either approach. ### Example of Generated SQL Query When using the `fromCity` filter with `filterToRaw`, the following SQL query might be generated: ```sql SELECT "id", "status", "customerId", "orderDate", "amount" FROM "orders" WHERE "customerId" IN ( SELECT "c"."id" FROM "customers" AS c WHERE c."city" LIKE '%New York%' ) ORDER BY "orderDate" ASC ``` In this example: - The `filterToRaw` function dynamically translates the `{ city: { $contains: city } }` filter into the SQL condition `c."city" LIKE '%New York%'`. - This SQL is then combined with the main query that retrieves the orders, ensuring that the filter is efficiently executed on the backend. ### Conclusion By using `filterToRaw`, you can combine the best of both worlds: the simplicity and readability of declarative filters with the power and performance of SQL-based filtering. This approach not only simplifies your code but also ensures that your filters are executed efficiently on the server, making it ideal for complex data retrieval scenarios. ## Benefits of SQL-Based Filters ### 1. Performance SQL-based filters allow you to take full advantage of the database's query optimizer, which can significantly improve the performance of complex filtering operations. By offloading the filtering to the database, you can reduce the amount of data that needs to be transferred to the frontend and improve response times. ### 2. Full Control Over SQL Queries When using SQL-based filters, you have full control over the generated SQL queries. This allows you to fine-tune the queries for specific use cases, optimize performance, and handle complex conditions that may be difficult to express using high-level filtering syntax. ### 3. Handling Complex Relations With SQL, you can handle complex relational queries that involve multiple entities, joins, subqueries, and more. This is especially useful when working with large datasets or when you need to perform operations that go beyond simple filtering. ### 4. Backend Execution By executing the SQL queries on the backend, you leverage the full power of the server's database, ensuring efficient data processing. This also minimizes the load on the frontend and reduces data transfer, leading to better performance and scalability. --- ## Summary In this lesson, we've explored how to use raw SQL within custom filters to perform advanced filtering based on relations. By leveraging SQL, you can take full advantage of the database's querying capabilities, handle complex relational logic, and improve the performance of your application. By combining SQL-based filters with the power of Remult, you can create highly efficient and flexible filtering logic that runs on the backend, making it a powerful tool for building scalable and performant web applications. # Interactive Tutorial - --- type: lesson title: Sql Relations Filter template: relations focus: /shared/Order.ts --- ### SQL Relations Filter :::warn **Experimental Feature:** This API is subject to change in future versions of Remult. ::: Filtering based on relations can be a powerful tool when querying data that involves multiple interconnected entities. The new `sqlRelationsFilter` function is designed to simplify and streamline filtering data based on relational information while utilizing the power of SQL for performance. ### What is `sqlRelationsFilter`? `sqlRelationsFilter` is a utility designed for simplifying the process of filtering entities based on relational data by using SQL's capabilities to execute the filtering on the backend. It leverages Remult’s entity relations and generates SQL queries that optimize how you query data. Let's consider an example where we want to filter orders based on their related customer’s city: ```file:/shared/Order.ts title="shared/Order.ts" collapse={1-9,11-22} {26-30} ``` ### Breakdown of the Code 1. **Filter Definition**: - The `fromCity` filter is defined as a custom filter using `Filter.createCustom`. It takes a single argument, `city`, which will be used to filter orders based on the related customer’s city. 2. **Using `sqlRelationsFilter`**: - `sqlRelationsFilter(Order)` is called to set up a filter for the `Order` entity. This function simplifies the task of querying orders based on their relationships (in this case, the `customer` relation). 3. **`customer.some()`**: - The `.some()` method is applied to the `customer` relation. It allows you to define a condition that checks whether any related `Customer` entity satisfies the condition. In this case, we are looking for customers whose city contains the specified string (`$contains: city`). 4. **SQL Efficiency**: - Behind the scenes, `sqlRelationsFilter` translates this logic into an optimized SQL query that performs the filtering on the backend. This ensures that even complex relation-based filters are executed efficiently at the database level. ### Why Use `sqlRelationsFilter`? - **Simplified Syntax**: `sqlRelationsFilter` reduces the complexity of writing relation-based queries by abstracting away the SQL translation. You define the filter conditions declaratively, and the utility handles the SQL generation. - **SQL Power**: While the filter is defined in a high-level, declarative way, it leverages the full power of SQL for execution. This ensures that your relation-based filters are as performant as possible. - **Optimized for Relations**: Filtering based on relations (e.g., orders based on customer data) can be tricky when working with large datasets. `sqlRelationsFilter` optimizes this process by generating SQL that efficiently queries relational data, preventing performance bottlenecks in your application. ### Example of How to Use It in Your Application Let’s say you have a frontend application where you want to display orders based on the customer’s city. You can use the `fromCity` filter directly in your component or page like this: ```file:/frontend/Page.tsx title="/frontend/Page.tsx" collapse={1-6,23-37} add={12} ``` ### SQL Efficiency and Security - **Efficient Backend Execution**: Using `sqlRelationsFilter`, the filter is executed directly on the backend. This ensures that the heavy lifting of filtering large datasets is done by the database, not the frontend, improving performance and reducing load times. - **Security**: Since the filter is executed on the backend, it mitigates risks such as SQL injection. Additionally, all parameters are properly sanitized, ensuring a secure and efficient query execution. ### Example of Generated SQL Using `sqlRelationsFilter`, a query like the one above may generate the following SQL: ```sql SELECT "orders"."id", "orders"."orderDate", "orders"."amount", "orders"."customerId" FROM "orders" WHERE "orders"."customerId" IN ( SELECT "customers"."id" FROM "customers" WHERE "customers"."city" LIKE '%New York%' ); ``` This SQL query: - Selects orders where the related customer is from a city that contains "New York". - Uses an efficient `IN` clause to find matching customer IDs and returns the associated orders. ### Summary of Benefits - **Simple Syntax**: `sqlRelationsFilter` provides a clean, declarative way to filter based on relations. - **Performance**: The filter is translated to efficient SQL that is executed on the backend, leveraging the power of the underlying database. - **Security**: By handling filters on the server, it ensures that queries are properly sanitized and secure. - **Optimized for Relations**: Specifically designed for cases where you need to filter entities based on related entities, such as filtering orders based on customer information. --- With `sqlRelationsFilter`, handling relation-based filtering in Remult becomes simpler, more efficient, and more powerful. Whether you’re working with large datasets or complex relational models, this utility helps you build queries that are both performant and easy to maintain. # Interactive Tutorial - --- type: chapter title: Advanced Filtering --- # Interactive Tutorial - --- type: lesson title: Introduction template: relations focus: /shared/Customer.ts --- ### SQL Expressions for Entity Fields In Remult, `sqlExpression` fields provide a convenient way to bring SQL’s computational power directly into your entity fields. This allows you to define fields based on SQL expressions, which perform calculations on the backend and can be easily used for sorting and filtering, making your application more efficient and reducing the need for additional queries. --- ## Example: Total Order Amount for Each Customer In this example, we’ll add a `totalAmount` field to the `Customer` entity. This field calculates the total order amount for each customer using a SQL sum function, which aggregates the `amount` field from the `Order` table. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-4,6-13} add={15-26} ``` ### Explanation of the Code - **`sqlExpression`**: The `sqlExpression` option allows you to define a SQL-based calculation as an entity field. Here, it’s used to sum up the `amount` values in the `Order` table where the `customerId` matches the current customer’s ID. - **`dbNamesOf` Utility**: This function ensures that the table and column names align with the database schema, providing consistency and accuracy when constructing SQL queries. - **Dynamic Calculation**: The `totalAmount` field dynamically calculates the sum of order amounts for each customer, offering real-time insights into customer spending. ### Using `totalAmount` in Queries With `sqlExpression`, you can treat `totalAmount` like a standard field, enabling advanced filtering and sorting directly within your queries. #### Sorting by `totalAmount` To retrieve customers ordered by the total amount they’ve spent, in descending order: ```ts const customersSortedByAmount = await repo(Customer).find({ orderBy: { totalAmount: 'desc', }, }) ``` #### Filtering by `totalAmount` You can also filter customers based on their total spending. For example, to find customers who have spent more than $50: ```ts const highSpendingCustomers = await repo(Customer).find({ where: { totalAmount: { $gt: 50 }, }, }) ``` In this query: - The `where` condition filters customers based on their `totalAmount`, letting you retrieve only those who meet the specified spending criteria. - As the calculation is performed on the backend, it remains efficient even with large datasets. --- ## Benefits of `sqlExpression` 1. **Backend Efficiency**: By offloading calculations to the database, `sqlExpression` fields enable faster query performance, especially for large datasets. 2. **Single Query**: Aggregation and calculations happen in the same query, reducing code complexity and minimizing client-server communication. 3. **Real-time Values**: Fields like `totalAmount` reflect the latest data, as they’re calculated each time the field is accessed. 4. **Sorting and Filtering**: You can seamlessly sort or filter based on `sqlExpression` fields, making it easier to create complex queries without additional backend logic. --- With `sqlExpression` fields, you can incorporate powerful SQL computations into your entities, simplifying data aggregation and improving application performance. This feature is ideal for cases like summing order totals, calculating averages, or performing other backend-based calculations, all while keeping your code clean and efficient. # Interactive Tutorial - --- type: lesson title: Getting a field from a relation template: relations focus: /shared/Order.ts --- ### SQL Expressions for Fields Based on Relations With Remult’s `sqlExpression` feature, you can create fields that pull data from related entities. This approach is especially useful when you want to sort, filter, or display information from a related entity directly within the current entity’s context. --- ## Example: Adding Customer City to the Order Entity Suppose you want to display and sort orders based on the city of each order’s customer. Instead of loading each customer’s data separately, you can add a `customerCity` field to the `Order` entity, which will retrieve the customer’s city information directly from the database. ```file:/shared/Order.ts title="Shared/Order.ts" collapse={1-4,6-13} add={14-23} ``` ### Explanation of the Code - **SQL Expression as a Related Field**: The `sqlExpression` for `customerCity` pulls the `city` field from the `Customer` entity, using a subquery to fetch the value based on the `customerId` in the `Order` entity. - **`dbNamesOf` Utility**: Ensures that table and column names match the schema, reducing errors and improving consistency. - **Dynamic Data**: The `customerCity` field provides real-time data from the related `Customer` entity, allowing you to view the customer’s city alongside order information. ### Using `customerCity` for Sorting and Filtering With `customerCity` as a field in the `Order` entity, you can now sort and filter orders by their customer’s city without needing to load or query the `Customer` entity directly. #### Sorting by `customerCity` To sort orders by the customer’s city in ascending order: ```ts const ordersSortedByCity = await repo(Order).find({ orderBy: { customerCity: 'asc', }, }) ``` #### Filtering by `customerCity` To retrieve orders where the customer’s city is "London": ```ts const ordersFromLondon = await repo(Order).find({ where: { customerCity: 'London', }, }) ``` In this query: - Sorting and filtering directly by `customerCity` keeps your code cleaner and reduces the need for extra joins or nested queries. - By leveraging `sqlExpression`, you optimize performance as the field data is retrieved from the database in a single query. --- ## Benefits of Using `sqlExpression` for Related Fields 1. **Efficient Data Retrieval**: Fetch data from related entities without additional queries or client-server communication. 2. **Improved Performance**: Since the database performs the subquery, it remains efficient even with large datasets. 3. **Simplified Code**: Sorting and filtering by related fields becomes as simple as using any other field. 4. **Real-time Information**: The related field’s data remains current, reflecting any changes to the related entity. --- In this lesson, you’ve seen how `sqlExpression` can transform your data handling by enabling seamless access to fields from related entities. This feature is ideal for situations where you need to use related data for sorting, filtering, or displaying, all while keeping your code efficient and streamlined. # Interactive Tutorial - --- type: lesson title: Sql Relations template: relations focus: /shared/Order.ts --- ### Leveraging `sqlRelations` for Advanced SQL-Based Relationships :::warn **Experimental Feature:** This API is subject to change in future versions of Remult. ::: The `sqlRelations` API (currently experimental) enhances the way you handle relationships in Remult by enabling SQL-like expressions directly on related entities. This allows for the seamless use of fields from related entities, simplifying complex queries and calculations. Let’s explore its use in a few examples that demonstrate the power of `sqlRelations`. --- ## Example 1: Adding Customer City to the Order Entity In many cases, you may want to display information from a related entity, such as a customer’s city, directly within the `Order` entity. With `sqlRelations`, you can do this using a straightforward syntax. ```file:/shared/Order.ts title="shared/Order.ts" collapse={1-5,7-14} add={16-19} ``` ### Explanation - **Direct Relation Field**: `sqlRelations(Order).customer.city` pulls the `city` field from the `Customer` entity, eliminating the need for a join. - **Dynamic Data**: The `customerCity` field within `Order` automatically updates whenever the related `Customer` data changes. --- ## Example 2: Counting Related Records in Customer To show the number of orders a customer has, you can define a `orderCount` field in the `Customer` entity using `sqlRelations`. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-5,7-14,21-100} add={16-19} ``` ### Explanation - **Counting Relations**: `sqlRelations(Customer).orders.$count()` counts the number of related `Order` records for each customer. - **Efficient Aggregation**: This aggregation is done directly in SQL, making it highly efficient for large datasets. --- ## Example 3: Counting Orders with Specific Conditions Suppose you want to count only the customer’s orders where the amount is over a certain threshold (e.g., greater than 50). You can add a `bigOrderCount` field to the `Customer` entity. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-5,7-20,28-100} add={21-27} ``` ### Explanation - **Conditional Counting**: This field uses `$count` with a filter condition to count only orders with an amount greater than 50. - **Dynamic Filtering**: You can specify any criteria here, providing flexibility for conditional counts within the related entity. --- ## Example 4: Dynamic SQL Aggregation For advanced calculations, such as summing up the total order amount for each customer, you can use `$subQuery` to create custom SQL aggregations. ```file:/shared/Customer.ts title="shared/Customer.ts" collapse={1-5,7-28} add={29-33} ``` ### Explanation - **Custom Aggregations**: The `$subQuery` method allows you to define a custom SQL expression for advanced calculations. Here, it calculates the total order amount for each customer. - **Dynamic Syntax**: `sqlRelations` provides flexibility for creating any kind of SQL-based aggregation, allowing you to customize the expression to fit your needs. --- ## Summary of `sqlRelations` Benefits 1. **Efficiency**: By generating SQL expressions directly within entity fields, `sqlRelations` minimizes database calls and performs calculations in SQL, improving performance. 2. **Simplified Code**: Directly including related entity fields and aggregations within your entity definitions streamlines code and enhances readability. 3. **Dynamic Aggregations**: With `$subQuery`, you can create complex aggregations based on related data, enabling powerful data insights with minimal effort. This approach provides a powerful way to manage complex relationships and aggregations in Remult, bringing the flexibility of SQL into the realm of structured, type-safe TypeScript fields. # Interactive Tutorial - --- type: chapter title: Sql Expression Entity Field --- # Interactive Tutorial - --- type: lesson title: Intro & Field-Level Authorization template: access-control focus: /shared/Task.ts --- # Field-Level Authorization in Remult This lesson builds upon the foundational [Authentication & Authorization](../../../1-basics/7-auth/1-introduction/) article, so please review that if you haven't yet. Now, let’s dive into adding fine-grained access control by managing authorization on a field-by-field basis within your entities. ### Adding `ownerId` Field with Controlled Updates To start, we want each task to record the `ownerId`, which tracks who created or owns the task. Here’s how to add the `ownerId` field to the `Task` entity: ```file:/shared/Task.ts title="shared/Task.ts" collapse={1-5,7-17} add={21-24} @Fields.string({ allowApiUpdate: false, // Prevents updates via API }) ownerId = remult.user?.id || '' ``` Setting `allowApiUpdate: false` ensures that once set, the `ownerId` cannot be modified through the API. This is useful for data fields you want to update only on the backend, while still allowing API updates for other fields. :::warn ### API Rules Apply Only to API Access The `allowApiUpdate`, `includeInApi`, and other access control options only apply to requests made through the API. They do not restrict access to data within backend methods or any code that directly interacts with the database on the backend - **including SSR (Server Side Rendering) scenarios**. If you execute code directly on the backend, such as through a backend method or non-API route, you will still be able to view and modify all fields, regardless of the access restrictions defined for the API. **Be mindful** of this distinction, as it is crucial for securing your application. Backend operations should be handled with caution, especially when dealing with sensitive or restricted data. ::: ### Controlling Field Access by Role With Remult, the `allowApiUpdate` option lets you control which users or roles can update a specific field. For example: ```ts @Fields.boolean({ allowApiUpdate: 'admin', // Only users with the 'admin' role can update }) completed = false ``` This configuration restricts updates to users with the `admin` role only. To see this in action, try signing in as "Alex" (non-admin) and "Jane" (admin), and notice how the ability to change the `completed` status differs. #### Tip: Try it out in the Admin UI The same authorization rules apply to fields in the Remult Admin UI. You can experiment further by navigating to the `Remult Admin UI` link, where you’ll see that updates follow the same API restrictions set in the code. ### Conditional Authorization with Arrow Functions Authorization can also be controlled using more dynamic conditions through arrow functions. For example, suppose we want either an `admin` or the task `owner` to be able to update the `completed` field. We could define the field like this: ```ts @Fields.boolean({ allowApiUpdate: task => remult.isAllowed("admin") || task.ownerId === remult.user?.id, }) completed = false ``` - `allowApiUpdate` is set to an arrow function that takes the `task` entity as a parameter. - **Condition 1**: `remult.isAllowed("admin")` checks if the current user has the "admin" role. If they do, they can update the field. - **Condition 2**: `task.ownerId === remult.user?.id` checks if the current user is the task owner by comparing the `ownerId` field in the task to the current user’s ID. If either condition is true (i.e., the user is an "admin" or the task owner), the `completed` field can be updated. If both are false, the update is blocked. This conditional approach allows flexible, role-based, and ownership-based control over specific fields in an entity. ### Allowing Updates Only on New Rows Another useful pattern is to restrict updates to a field when a task is first created but disallow changes afterward: ```ts allowApiUpdate: (task) => getEntityRef(task).isNew() ``` This ensures the field is set initially but prevents further modifications. ## Hiding Fields with `includeInApi` The `includeInApi` option allows you to prevent specific fields from appearing in the API response altogether. This is particularly useful for sensitive data like passwords or other restricted fields. ```ts @Fields.string({ includeInApi: false, // Omits this field from API responses }) password = '' ``` ### Versatile Options for `includeInApi` Just like `allowApiUpdate`, the `includeInApi` option offers versatility. You can set it to `true`, `false`, specific roles, or an arrow function for dynamic control over who can access the field in the API response. This makes `includeInApi` highly adaptable to various access requirements. Examples: 1. **Role-Based Access**: ```ts @Fields.string({ includeInApi: 'admin', // Only users with 'admin' role see this field }) privateInfo = '' ``` 2. **Conditional Access with an Arrow Function**: ```ts @Fields.string({ includeInApi: task => remult.isAllowed("admin") || task.ownerId === remult.user?.id, }) privateNotes = "" ``` In this example: - **Admin Access**: If the user has the "admin" role, they see the `privateNotes` field. - **Owner Access**: The task owner can also see `privateNotes`. This flexibility with `includeInApi` allows you to apply fine-grained control over which users can access specific data, enhancing security and providing precise control over your API’s data exposure. ### Summary By using `allowApiUpdate` and `includeInApi`, you have fine-grained control over which fields users can modify or view based on roles, data ownership, and custom conditions. # Interactive Tutorial - --- type: lesson title: Row-Level Authorization template: access-control focus: /shared/Task.ts --- # Row-Level Authorization Row-level authorization enables control over specific rows of an entity based on user roles, ownership, or custom logic. This feature is essential for applications that need fine-grained permissions. Consider the following example: ```file:/shared/Task.ts title="shared/Task.ts" collapse={13-30} add={4-8} ``` ### Understanding Each Authorization Option 1. **`allowApiRead: true`** - `allowApiRead` controls whether rows are accessible for reading through the API, and it defaults to `true`, unlike other options that default to `false`. - Although you cannot use an arrow function with `allowApiRead` to restrict specific rows, this can be achieved using row-level filters, which we’ll cover in the next lesson, **"Filtering Rows Based on User Permissions"**. 2. **`allowApiInsert: remult.authenticated`** - Restricts the ability to create new tasks to authenticated users. Any user who is not logged in will be denied insert access. 3. **`allowApiDelete: 'admin'`** - Limits deletion of tasks to users with the `admin` role, preventing other users from deleting tasks through the API. 4. **`allowApiUpdate` with Conditional Logic** - The `allowApiUpdate` option here uses an arrow function to set conditional update access based on role and ownership: ```typescript allowApiUpdate: (task) => remult.isAllowed('admin') || task.ownerId === remult.user?.id, ``` - This configuration allows: - Admin users to update any task, and - Non-admin users to update only their own tasks, identified by matching `task.ownerId` to the current user’s ID. - Such logic provides flexibility for controlling access at a granular level, aligning permissions with both general access and specific ownership. ### Versatility Across Options Each of these options—`allowApiRead`, `allowApiInsert`, `allowApiDelete`, and `allowApiUpdate`—can accept different inputs to define permissions: - **Boolean values** (`true` or `false`) for universal access or denial. - **Role strings** or arrays of roles (e.g., `'admin'` or `['admin', 'manager']`) for access control based on user roles. - **Arrow functions** for `allowApiInsert`, `allowApiDelete`, and `allowApiUpdate`, providing custom logic based on roles, user IDs, or specific row attributes. In the upcoming lesson, **"Filtering Rows Based on User Permissions"**, we’ll explore how to apply row-level access control dynamically, allowing each user to view only the rows they are permitted to access using specific filters. # Interactive Tutorial - --- type: lesson title: Filtering Rows Based on User Permissions template: access-control focus: /shared/Task.ts --- # Filtering Rows Based on User Permissions Filtering rows based on user permissions ensures that users only access the rows they are authorized to see, update, or delete. This lesson covers using `apiPrefilter` to define permission-based filters applied to API requests, providing security and data isolation for users. ```file:/shared/Task.ts title="shared/Task.ts" collapse={12-30} add={4-8} ``` ### Code Explanation 1. **Admin Access** - If the user has an `admin` role, `apiPrefilter` returns an empty filter (`{}`), allowing access to all rows without restriction. 2. **Authenticated User Access** - For authenticated users who are not admins, `apiPrefilter` restricts access to rows where the `ownerId` matches the current user's ID, allowing users to view only their own tasks. 3. **Unauthorized Access** - If the user is not authenticated or doesn’t meet any conditions, `apiPrefilter` raises a `ForbiddenError`, blocking access entirely. ### Authorization Beyond Read Access `apiPrefilter` not only restricts read access but also applies to `update` and `delete` operations. Users can only update or delete rows they have permission to access, adding a layer of security to ensure data isolation across user roles. ### Important Considerations - **API Scope** - `apiPrefilter` applies only to API rules. This means it governs only API-based access, so backend methods or direct backend queries bypass this filter. To apply similar restrictions to backend access, use `backendPrefilter`. - **Consistent Rules with `backendPrefilter`** - In cases where you need uniform access control across both API and backend, adding a `backendPrefilter` alongside `apiPrefilter` helps ensure that both API and backend methods adhere to the same filtering logic. This is essential in scenarios where sensitive data could be exposed or modified directly within backend logic. ### Try It Out! To see `apiPrefilter` in action, try signing in as different users: - **Sign in as Alex** (non-admin): Alex can only see, update, and delete tasks he owns. - **Sign in as Jane** (admin): Jane, as an admin, can access, update, and delete all tasks. By switching between these accounts, you’ll observe how `apiPrefilter` limits access based on user roles, ensuring that each user only interacts with rows they're authorized to see and modify. --- By combining `apiPrefilter` with row-specific rules in `allowApiUpdate`, `allowApiDelete`, and `allowApiInsert`, you gain a powerful framework for dynamic, row-level security based on user permissions. This approach protects data integrity and enhances security across various layers of application logic. # Interactive Tutorial - --- type: lesson title: Filtering Related Rows Based on User Permissions template: access-control-2 focus: /shared/TimeEntry.ts --- # Filtering Related Rows Based on User Permissions In this lesson, we’ll explore how to apply user-based access controls to related rows in your data model. Specifically, we’ll filter the `TimeEntry` API to ensure that only entries for tasks the user is allowed to view are accessible. This involves reusing the `apiPrefilter` logic from the `Task` entity and applying it to related entities like `TimeEntry`. ## Step 1: Refactor `apiPrefilter` into a Custom Filter First, we’ll refactor the `apiPrefilter` logic in `Task` into a reusable custom filter. This allows us to apply the same filtering logic in multiple places, ensuring consistency. ### Task Entity In `Task`, we define a static custom filter, `allowedTasks`, which checks the user's role: ```file:/shared/Task.ts title="shared/Task.ts" {4} add={23-28} collapse={8-21} ``` ### Explanation of the Code - **`allowedTasks` Custom Filter**: This filter uses `remult.isAllowed` to check if the user has the `admin` role. Admins can access all tasks, while other authenticated users can only access their own tasks. - **`apiPrefilter` in Task**: We then use `allowedTasks` within `apiPrefilter`, ensuring that only allowed tasks are accessible through the API. ## Step 2: Apply the Custom Filter in `TimeEntry` Now that we have the `allowedTasks` filter, we can use it in the `TimeEntry` entity to restrict access based on the user’s permissions for related tasks. ### TimeEntry Entity In `TimeEntry`, we apply the `allowedTasks` filter to only show time entries associated with tasks the user is permitted to view: ```file:/shared/TimeEntry.ts title="shared/TimeEntry.ts" add={6-10} collapse={13-26} ``` ### Explanation of the Code - **`apiPrefilter` in TimeEntry**: This prefilter checks `TimeEntry` rows by filtering based on the tasks the user can access. First, we fetch the IDs of tasks allowed for the user by calling `Task.allowedTasks()`. We then use these IDs to filter the `TimeEntry` API, ensuring that only time entries related to accessible tasks are visible. ### Try It Out! - **Sign in as Alex** (non-admin): Alex can only see time entries for tasks he owns. - **Sign in as Jane** (admin): Jane can access all time entries, regardless of the task owner. This setup demonstrates how to efficiently apply consistent access control across related entities using a reusable custom filter. --- ### Improving Performance with SQL-Based Filtering Below, we modify `apiPrefilter` in `TimeEntry` to use `SqlDatabase.rawFilter`. This lets us directly create a SQL-based filter that leverages the related `Task` entity filter without fetching task data in advance: ```solution:/shared/TimeEntry.ts title="shared/TimeEntry.ts" add={6-18} collapse={21-36} ``` ### Explanation of the Code 1. **SQL-Based Filter for Task Access**: Instead of fetching allowed tasks and filtering in memory, we use `SqlDatabase.rawFilter` to create a dynamic SQL subquery that applies `Task.allowedTasks()` directly in the database. 2. **Roles of `dbNamesOf`, `rawFilter`, and `filterToRaw`**: - **`dbNamesOf`**: This function retrieves database-specific names for entity fields and tables, which helps build queries compatible with the database schema. In this example, we use `dbNamesOf` to get the table names and field references for `Task` and `TimeEntry`, ensuring SQL compatibility. - **`rawFilter`**: The `SqlDatabase.rawFilter` function enables direct SQL manipulation for custom filters. This bypasses the usual in-memory filtering, allowing filters to execute within the database. Here, it constructs an SQL `IN` query that checks if the `taskId` in `TimeEntry` exists in a filtered list of `Task` IDs. - **`filterToRaw`**: This helper translates a standard filter (like `Task.allowedTasks()`) into a raw SQL condition. It processes the custom filter defined in `allowedTasks()` and converts it into SQL, ensuring that our `Task` filtering rules are directly translated into the SQL subquery. 3. **Efficient Filtering**: By translating the `allowedTasks` filter to SQL, we ensure that all filtering happens within the database, reducing memory usage and improving query speed for better performance. ### Try It Out! To see this SQL-based filtering in action: 1. Click **Solve** button to see the try the sql based implementation. 2. Sign in as different users (e.g., Alex and Jane) to observe how access to `TimeEntry` records changes based on the user's roles and permissions. Using SQL-based filters provides an optimized way to manage related access control by leveraging the database’s capabilities, especially useful when dealing with large datasets or complex access rules. # Interactive Tutorial - --- type: lesson title: Sql Relations Filter for User Permissions template: access-control-2 focus: /shared/TimeEntry.ts --- ### SQL Relations Filter for User Permissions :::warn **Experimental Feature:** This API is subject to change in future versions of Remult. ::: The `sqlRelationsFilter` function provides an efficient way to apply row-level filters across related entities directly within SQL, streamlining access control for related rows. In this lesson, we’ll use `sqlRelationsFilter` to apply permissions based on user access to `Task` entities in the `TimeEntry` entity. ### Defining User Permission Filters In this example, we want to ensure that users can only access `TimeEntry` rows associated with `Task` entities they have permission to view. #### TimeEntry Entity Setup ```file:/shared/TimeEntry.ts title="shared/TimeEntry.ts" add={7-8} collapse={11-25} ``` ### Explanation of the Code 1. **Using `sqlRelationsFilter`**: - The `sqlRelationsFilter` function is designed to apply row-level filtering logic directly within the database. - Here, we use it to filter `TimeEntry` rows based on the associated `Task` rows that meet certain user permissions. 2. **Relation-Specific Filtering with `.some()`**: - The `.some()` method is used to match `TimeEntry` rows with related `Task` rows that satisfy the filter in `Task.allowedTasks()`. - By passing `Task.allowedTasks()` to `.some()`, we enforce permissions on `TimeEntry` rows linked to tasks the user is authorized to view. 3. **Simplified Filtering Logic**: - `sqlRelationsFilter` enables us to express complex filtering conditions for related entities with a clear and concise API. - This approach is not only efficient but also significantly reduces code complexity by handling filtering at the SQL level. ### Try It Out 1. **Sign in as Different Users**: Test with various users (e.g., users with and without admin roles) to observe how access to `TimeEntry` records changes based on the user’s permissions for related `Task` entities. 2. **Experiment with Permissions**: Modify the `allowedTasks` filter logic in `Task` to see how different rules impact the visibility of `TimeEntry` entries. By leveraging `sqlRelationsFilter`, you can create highly performant and intuitive access control that directly uses SQL to enforce row-level permissions across related entities. # Interactive Tutorial - --- type: chapter title: Access Control --- # Interactive Tutorial - --- type: lesson title: Field Metadata focus: /shared/Task.ts template: metadata --- # Field Metadata Entities in Remult are not just a single source of truth for storage, APIs, and authentication—they can serve as a central point for managing any entity-related aspect across your application. Let’s explore how to utilize field metadata for consistent UI labeling and data formatting. ### Setting Captions To ensure consistent captions across your app, set the `caption` attribute directly in the field definition. This way, the same caption is automatically used wherever the field appears. ```file:/shared/Task.ts title="shared/Task.ts" collapse={1-6,16-100} add={12} ``` ### Accessing Captions Field metadata, like `caption`, can be accessed using the `fields` property of a repository: ```typescript const titleCaption = repo(Task).fields.title.caption console.log(titleCaption) // Outputs: "The Task Title" ``` Using captions this way allows for a unified UI. For example, in `TodoItem.tsx`: ```file:/frontend/TodoItem.tsx title="frontend/TodoItem.tsx" collapse={15-100} add={6,13} ``` Try changing the caption of `title` in `Task` and observe how the UI updates automatically! ## Display Consistency with `displayValue` To ensure consistent display formatting, especially for literals or dates, use the `displayValue` property. ### Example 1: Displaying a Literal Field For fields with literal values, like `priority`, `displayValue` can ensure consistent capitalization: ```file:/shared/Task.ts title="shared/Task.ts" collapse={1-6,8-14,23-100} add={18-19} ``` In `TodoItem.tsx`, access and use this display formatting: ```file:/frontend/TodoItem.tsx title="frontend/TodoItem.tsx" collapse={1-14,19-100} add={17} ``` ### Example 2: Displaying Dates with Reusable `displayValue` Let’s take a closer look at defining `displayValue` for dates: ```file:/shared/Task.ts title="shared/Task.ts" collapse={1-6,8-25} add={28} ``` In this example, the `displayValue` function is designed to ignore the first parameter (representing the entity) and only use the second parameter, the date value. By focusing on the value alone, this `displayValue` function can be refactored into a standalone utility that works for any date field, not just `createdAt`. #### Refactoring `displayValue` for Reusability You can create a reusable `displayDate` function and use it across different date fields: ```typescript // utils/displayValueHelpers.ts export const displayDate = (_: unknown, date?: Date) => date?.toLocaleDateString() ``` Now, any date field can use this `displayDate` function for consistent date formatting, regardless of the entity: ```typescript import { displayDate } from './utils/displayValueHelpers' @Fields.createdAt({ caption: 'Task Creation Date', displayValue: displayDate, }) createdAt?: Date ``` This approach ensures consistency in date formatting across your application and keeps your code clean and maintainable. You can define similar reusable functions for other field types, ensuring that formatting stays uniform across different entities and fields. ### Extending Field Options with Custom Options Beyond the standard options, fields can also be enhanced with **custom options** tailored to your application's specific needs. These options allow you to store and access any metadata you might need directly within the field definition, making your entity models even more powerful and adaptable. For example, you might want to add a custom validation message, tooltip, or any other metadata to a field. This added flexibility helps you centralize and standardize additional properties that can be useful in various parts of your application, from dynamic UI rendering to custom business logic. To explore more about how to define and use custom options, check out [Enhancing Field and Entity Definitions with Custom Options](https://remult.dev/docs/custom-options#enhancing-field-and-entity-definitions-with-custom-options). ## Leveraging Metadata for Dynamic UI With field metadata, you can abstract your UI components for consistent display across your app. Here’s an example of a dynamic component that uses field metadata: ```solution:/frontend/TodoItem.tsx title="frontend/TodoItem.tsx" add={6-10,14-19} ``` Click **"Solve"** at the top right of the code editor to see this abstraction in action. This dynamic UI approach ensures your fields are displayed with the same metadata-defined captions and formatting throughout the app. --- you can use the `getValueList` function to get the values of a literal field ```tsx add=", getValueList" add={8-10} import { repo, getValueList } from 'remult' return (
{fields.map((field) => (
{field.caption}: {field.displayValue(task)}{' '} {getValueList(field as any) ? `(options: ${getValueList(field as any)})` : ''}
))}
) ``` You can use these capabilities, together with the structured error model to create dynamic forms, dynamic grid and other dynamic uis that leverage # Interactive Tutorial - --- type: lesson title: Forms and Validation focus: /frontend/TodoItem.tsx template: metadata --- ### Forms and Validation This lesson will guide you through creating a form with **field-level error handling** and **dynamic field captions** using metadata. You’ll learn how to capture validation errors, display custom field captions, and dynamically reflect error messages for specific fields. ### Code Example: TodoItem Component Here’s the initial code for the `TodoItem` component: ```file:/frontend/TodoItem.tsx title="frontend/TodoItem.tsx" add={8,11-16,36} collapse={18-29,38-100} ``` ### Code Explanation 1. **ErrorInfo Type**: - `ErrorInfo` captures errors specific to each field in the `Task` entity. If validation errors occur, they populate `modelState`, which contains error messages for each field. - **Example Validation Error Payload**: ```json { "modelState": { "title": "Should not be empty", "priority": "Value must be one of: low, medium, high" }, "message": "The Task Title: Should not be empty" } ``` - Each error message in `modelState` corresponds to a specific field, allowing targeted error display beside the relevant form inputs. - The validations themselves are defined within the entity as part of our single source of truth, ensuring consistent rules and messages across the entire application. - Check out the [validation options in the validation article](https://remult.dev/docs/validation) to see how you can define and extend these validations directly in your entity. 2. **The `save` Function**: - `save` is triggered when the "Save" button is clicked: - It starts by clearing previous errors with `setError(undefined)`. - Then, it tries to save the `state` using `taskRepo.save(state)`. - If an error occurs, `setError(error)` captures it, with field-specific messages provided by `ErrorInfo`. 3. **Displaying Field-Level Errors**: - Error messages are shown directly below each field using `error?.modelState?.title` and `error?.modelState?.priority`. - Optional chaining (`?.`) ensures the UI is protected from undefined values, making error handling efficient and safe. ### Try it Out Clear the `title` field or set an invalid value for `priority` (anything other than "low," "medium," or "high") and see how validation messages appear in real-time, guiding users to correct their inputs. ### Expanding with Field Options To enhance the user experience, let’s switch the `priority` input to a dropdown using the priority options defined in the entity. 1. First, add options to `priority`: ```tsx title="frontend/TodoItem.tsx" add={3} import { getValueList } from 'remult' //... const options = getValueList(priorityField) ``` 2. Use the `options` list to render a dropdown: ```tsx title="frontend/TodoItem.tsx" add={3-12} ``` This approach allows you to keep the `priority` options in the entity as a single source of truth, ensuring consistency across the application. ### Dynamic and Scalable Forms To create a more dynamic form, you can loop through fields directly from the entity, easily building long or short forms without hardcoding field values: ```tsx import { repo, ErrorInfo, getValueList } from 'remult' import { Task } from '../shared/Task.js' import { useState } from 'react' const taskRepo = repo(Task) export function TodoItem({ task }: { task: Task }) { const [state, setState] = useState(task) const [error, setError] = useState>() async function save() { try { setError(undefined) await taskRepo.save(state) } catch (error: any) { setError(error) } } function reset() { setError(undefined) setState(task) } const fields = [taskRepo.fields.title, taskRepo.fields.priority] return (
{fields.map((field) => { const options = getValueList(field) return ( ) })}
) } ``` ### Try the Interactive Example Click the `solve` button at the top right of the code editor to see this in action! This setup ensures consistent validation and display across forms and fields, making your UI scalable and reliable. ### Summary By utilizing field metadata, error handling, and dynamic rendering techniques, you can create reusable, rich forms and UI elements that enhance the consistency and maintainability of your application. These techniques allow you to: - **Centralize Display Logic**: Captions, input types, and validation can all be maintained within the entity definitions, providing a single source of truth that is easily accessible across the application. - **Efficiently Handle Validation**: By capturing and displaying field-level errors dynamically, you can offer immediate, user-friendly feedback, ensuring a smoother user experience. - **Build Scalable, Dynamic Forms**: With access to field metadata and validation options, you can dynamically generate forms that adapt to each field’s specific requirements, reducing code duplication and making it easy to create various form layouts. Together, these strategies make it straightforward to construct forms and other UI components that are consistently styled, validated, and ready for reuse throughout the application. # Interactive Tutorial - --- type: lesson title: Lifecycle Hooks focus: /shared/Task.ts --- ## Introduction to Entity Lifecycle Hooks Entity Lifecycle Hooks in Remult provide a powerful way to add custom actions and validations at key stages of an entity’s lifecycle. These hooks allow you to execute specific logic when saving, updating, or deleting entities, giving you greater control over your data. ### Available Lifecycle Events 1. **Saving** and **Saved**: Triggered when an entity is being saved or after it’s saved. Runs on the backend and can validate, modify data, or log updates. 2. **Deleting** and **Deleted**: Triggered when an entity is being deleted or after deletion. This is useful for cleanup or logging and also runs on the backend. 3. **Validation**: Runs on both the backend and frontend (if possible) and is specifically for validating data before saving. This is often used to enforce constraints across fields. Any exception thrown within `saving` and `deleting` events will be treated as a validation error, preventing the entity from being saved or deleted. In addition to throwing exceptions, you can set an error message directly on a specific field using `field.error`. This displays an error in the UI and aborts the save operation for that entity. ```file:/shared/Task.ts title="shared/Task.ts" add={5-19} collapse={22-100} ``` ### Code Explanation This example defines the following lifecycle hooks for the `Task` entity: - **Saving Hook**: - Runs before an entity is saved. - If the entity is new (indicated by `e.isNew`), it logs the task title with the message "New task." - If the entity is being updated, it iterates over all fields in `e.fields`, checking if any values have changed with `field.valueChanged()`. - When a field has changed, it logs the field’s caption (`field.metadata.caption`), its original value (`field.originalValue`), and its new value (`field.value`). - **Deleting Hook**: - Runs before an entity is deleted. - Logs the title of the task being deleted. This setup allows you to keep track of any task changes and deletions, capturing all field changes when a task is updated and identifying tasks as they are deleted. ### Field-Level Validation and Saving Hooks In addition to entity-level hooks, you can also define validation and saving hooks at the field level. This can be particularly useful when specific logic needs to apply only to a particular field. Field-level hooks execute before the main entity’s lifecycle hook, allowing fine-grained control over individual fields. ### Further Reading and Testing For more information on how to leverage these hooks in your applications, refer to the [Remult Lifecycle Hooks Documentation](https://remult.dev/docs/lifecycle-hooks#entity-lifecycle-hooks). ### Try It Out Click on the `Toggle Terminal` button on the right, make some changes to tasks, and observe the terminal output to see the messages generated in the saving and deleting hooks. # Interactive Tutorial - --- type: lesson title: Select Fields focus: /shared/TaskLight.ts template: select-fields --- ## Introduction Select Fields Select fields in Remult allows you to select only the fields you need from an entity. This is useful when you want to reduce the amount of data groing from client to server. ## How to Create a new entity, and point to an existing table in the database using the `dbName` option. ```file:/shared/TaskLight.ts title="shared/TaskLight.ts" ``` ### Code Explanation - Be aware that it's a complete separate entity, so make sure you set the right access control. - Here, `allowApiCrud` options is set to false, and `allowApiRead` is set to true so this entity is read-only (it's also the default, just wanted to show you can set it explicitly) ### Try it out ```ts // Get Task fields await repo(Task).find() // TaskLight fields only await repo(TaskLight).find() ``` ### See also We showed that you can use `dbName` to point at an existing table in the database. You can also create a dynamic view, [Leveraging Database Capabilities with sqlExpression](https://remult.dev/docs/ref_entity#sqlexpression). # Interactive Tutorial - --- type: lesson title: Extend Entity (add fields) focus: /shared/TaskExtra.ts template: select-fields --- ## Introduction Extend Entity When you application grows, you entities will grow too. At some point, it could be useful to refactor your entities to make them more readable and maintainable by making them smaller. Imagine a `Task` having an `id`, `title`, `completed` and `description` fields _(and way more!)_. It could be good to refactor it to have a `Task` entity with only the `id`, `title` and `completed` fields, and then have a `TaskExtra` entity with the rest of the fields, here `description`. Extending an entity is a powerful feature that allows you to add new fields to an existing entity without changing the base entity. By doing this, you will not overload the base entity with fields that are not relevant all the time. ### How to Create a new entity that extends the base entity and add the new fields to it. ```file:/shared/TaskExtra.ts title="shared/TaskExtra.ts" ``` ### Code Explanation - You need to have a dedicated key for this new entity, here `TaskExtraKey`. - It's to really differentiate between the two entities for remult. - It will also create two different entries in Admin UI. - You need to set the `dbName` option to point to the right database name (same as the base entity). - Yes, by default, remult will use the entity key as the database name. ### Try it out ```ts // Get Task fields await repo(Task).find() // Get Task & TaskExtra fields await repo(TaskExtra).find() ``` # Interactive Tutorial - --- type: chapter title: Entities as a single source of truth --- # Interactive Tutorial - --- type: lesson title: Introduction to Testing with Remult focus: /tests/validations.test.ts --- --- # Introduction to Testing with Remult Remult makes it simple to replace your production database with a test database, enabling you to easily write and execute automated tests. The flexibility of switching between databases allows you to verify logic, validations, and even SQL-related functionality in your entities, streamlining the testing process for development. ## Code Example: Basic Validation Tests The example below sets up tests for the `Task` entity to check basic validation logic: ```file:/tests/validations.test.ts title="tests/validations.test.ts" add={8} collapse={1-5,10-100} ``` ### Code Explanation 1. **Setting the Data Provider for Tests**: - Inside the `beforeEach` hook, the test database is set to `InMemoryDataProvider`, allowing for fast, transient data access without needing a real database connection. 2. **Test Cases**: - **Task with Title**: This test creates a task with a title and verifies that the task count increases by one. - **Task without Title**: This test attempts to insert a task without a title, triggering a validation error. The `expect(error.message)` statement then verifies the validation message. ### Try It Out Click the `Toggle Terminal` button on the right to see the test execution and validation output. --- ## Testing SQL-Based Logic For testing SQL expressions or SQL-based filters, use an in-memory SQLite database, which supports SQL functionality without needing a production database connection. ```solution:/tests/validations.test.ts title="tests/validations.test.ts" collapse={1-6,13-100} add={8-10} ``` ### Explanation of SQL Test Setup - **createSqlite3DataProvider**: Sets an SQLite database in memory, enabling tests for SQL-related code. - **ensureSchema**: This ensures that the table structure matches your entity metadata, automatically creating tables as needed. - **SqlDatabase.LogToConsole**: Setting this to `true` outputs SQL statements to the console, helping verify that SQL operations are working as expected. ### Using Your Actual Database Provider In addition to in-memory testing, you can test with your actual database provider by setting it to `remult.dataProvider`, ensuring compatibility and performance for production scenarios. --- By using these techniques, you can write comprehensive tests covering all entity aspects, from validations to SQL expressions. # Interactive Tutorial - --- type: lesson title: Testing Api Rules focus: /tests/authorization.test.ts --- # Testing API Rules In Remult, automated tests run as if they are executing directly on the backend. This can make it challenging to test API rules, which typically involve permissions and access control that rely on simulating API calls. To address this, you can use the `TestApiDataProvider`, which simulates an API environment for each database operation in your tests. ### Code Example: Authorization Tests with `TestApiDataProvider` The example below demonstrates how to test API rules, including user authentication and authorization, using `TestApiDataProvider`: ```file:/tests/authorization.test.ts title="tests/authorization.test.ts" collapse={1-6,19-100} add={9-12} import { describe, test, expect, beforeEach } from 'vitest' import { remult, repo, InMemoryDataProvider } from 'remult' import { TestApiDataProvider } from 'remult/server' import { createSqlite3DataProvider } from 'remult/remult-sqlite3' import { Task } from '../shared/Task' describe('Test authorization', () => { beforeEach(async () => { remult.dataProvider = TestApiDataProvider({ dataProvider: createSqlite3DataProvider(), }) await repo(Task).insert({ title: 'my task' }) }) test('non-authenticated users cannot delete', async () => { try { remult.user = undefined // Simulate unauthenticated user const task = await repo(Task).findFirst() await repo(Task).delete(task) throw new Error('Should not reach here') } catch (error: any) { expect(error.message).toBe('Forbidden') } }) test('Non-admin users cannot delete', async () => { try { remult.user = { id: '1' } // Simulate authenticated non-admin user const task = await repo(Task).findFirst() await repo(Task).delete(task) throw new Error('Should not reach here') } catch (error: any) { expect(error.message).toBe('Forbidden') } }) test('Admin users can delete', async () => { remult.user = { id: '1', roles: ['admin'] } // Simulate authenticated admin user const task = await repo(Task).findFirst() await repo(Task).delete(task) expect(await repo(Task).count()).toBe(0) }) }) ``` ### Code Explanation 1. **Test Setup with `beforeEach`**: - we set up the test environment to use `TestApiDataProvider`, simulating an API call for each database operation. - We also create an initial task in the database to test authorization logic on existing data. 2. **Testing API Rules**: - Each test simulates different user scenarios to verify the `delete` permission on tasks: - **Non-Authenticated Users**: If `remult.user` is set to `undefined`, the test verifies that unauthenticated users cannot delete tasks. - **Non-Admin Users**: With `remult.user` set to an authenticated but non-admin user, the test expects a `Forbidden` error when attempting deletion. - **Admin Users**: An authenticated admin user should have deletion access, and the test confirms that the task count decreases accordingly. ### Testing SQL-Related Logic For SQL-based tests, you can use the `SqlDatabase.LogToConsole = true` setting to see SQL queries and understand the underlying operations during tests. --- Using these techniques allows you to simulate real API operations within tests, ensuring robust access control and proper handling of user permissions in your application. # Interactive Tutorial - --- type: chapter title: Testing template: tests previews: false terminal: activePanel: 1 panels: - ['output', 'Terminal'] --- # Interactive Tutorial - --- type: part title: In Depth slug: in-depth --- # Interactive Tutorial - --- type: lesson title: Extending Existing Validations focus: /shared/Task.ts --- # Extending Existing Validations In this lesson, you'll learn how to extend and customize existing validations in Remult. Validations in Remult are simply functions that you can call and combine as needed. ### Example: Unique Title Validation Let's extend the existing `unique` validation to check that no two tasks exist with the same title, as long as the title is not empty. ````solution:/shared/Task.ts title="shared/Task.ts" collapse={1-5, 16-99} add={11-13} ``` ```typescript title="shared/Task.ts" add={6-8} export class Task { @Fields.uuid() id = '' @Fields.string({ validate: (task, e) => { if (task.title != '') Validators.unique(task, e) }, }) title = '' //.... } ```` ### Code Explanation - The `validate` function checks if the `title` is not empty. - If the `title` is not empty, the `Validators.unique` function is called to ensure that the task title is unique within the entity. - This approach allows you to combine and extend existing validations to suit your application's needs. ### Try It Out Test this extended validation by trying to add tasks with duplicate titles. Notice that the validation prevents duplicates only when the title is not empty. # Interactive Tutorial - --- type: chapter title: Validations --- # Interactive Tutorial - --- type: part title: Examples --- # Interactive Tutorial - --- type: tutorial mainCommand: ['npm run dev', 'Starting http server'] prepareCommands: - ['npm install', 'Installing dependencies'] editPageLink: https://github.com/remult/remult/blob/main/docs/interactive/src/content/tutorial/${path}?plain=1 ---