Store your data
The shop from the first guide returns a total written in the code. This guide stores real products: an entity, a repository, a form to add them, and the same products over REST and GraphQL.
Entities
An entity is a class in entities/ that describes one kind of data:
// fronds/shop/entities/Product.ts
import { entity, primary, text, number } from '@fougere/schema';
export default class Product extends entity({
id: primary(),
title: text({ min: 1, max: 200 }),
cents: number({ min: 0 }),
}) {}
From this class Fougere builds the products table, the validation of every input, the
fields of a form, and the REST and GraphQL types.
Crud(Product) gives a handler the five usual operations — list, findById, create,
update, delete:
// fronds/shop/handlers/ProductHandler.ts
import { Crud } from '@fougere/core';
import Product from '../entities/Product.js';
export default class ProductHandler extends Crud(Product) {}
→ Entities
Migrations
The table does not exist yet, and the app does not create it for you: it refuses to start, and says what is missing.
Fougere boot refused: the database is missing 1 change(s) from the entities:
products — no table
On your machine: fougere migrate --latest --apply
On a shared database: fougere freeze, then fougere migrate --apply
npm run migrate — fougere migrate --latest --apply — creates it on your database. Run it each
time an entity gains a field; the boot tells you when.
A rename is declared on the entity, and the rows follow:
export default class Product extends entity(
{ id: primary(), name: text(), cents: number({ min: 0 }) },
{ previous: { name: 'title' } },
) {}
npm run migrate renames the column on your database. Before the change leaves it, freeze it:
fougere freeze v2
A deployment runs fougere migrate --apply, which applies only what was frozen — a change
nobody froze is refused there, by name. A field that left with no previous: is a drop:
fougere freeze warns that its column goes at the next migrate, and its data with it.
Repositories
A repository is where an entity's rows are read and written. Every entity has one already; write the class when a query deserves a name:
// fronds/shop/repositories/ProductRepository.ts
import { Repository } from '@fougere/core';
import Product from '../entities/Product.js';
export default class ProductRepository extends Repository(Product) {
cheapest(): Promise<Product[]> {
return this.list({ orderBy: 'cents', limit: 5 });
}
}
A repository is a service like Payment: a handler asks for it by type.
// fronds/shop/handlers/CartHandler.ts
import ProductRepository from '../repositories/ProductRepository.js';
export default class CartHandler {
constructor(private products: ProductRepository) {}
async getTotal(): Promise<number> {
const products = await this.products.list();
return products.reduce((sum, product) => sum + product.cents, 0);
}
}
Forms
useFormFor builds a form's state from the entity. It checks in the browser what the
server checks, so an empty title or a negative price is refused before anything is sent:
<script setup lang="ts">
import Product from '@fronds/shop/entities/Product.js';
const { values, errors, submit, loading } = useFormFor(Product);
</script>
<template>
<form @submit.prevent="submit">
<input v-model="values.title" />
<p v-if="errors.title">{{ errors.title }}</p>
<input v-model.number="values.cents" type="number" />
<p v-if="errors.cents">{{ errors.cents }}</p>
<button :disabled="loading">Add</button>
</form>
</template>
submit calls create. After it succeeds, every page listing products refreshes on its
own.
→ Forms
REST and GraphQL
The same products are served over REST and GraphQL when the config says so. Each is a
package of its own: the config imports it, and names it under protocols: to serve it.
npm install @fougere/adapter-rest @fougere/adapter-graphql
pnpm add @fougere/adapter-rest @fougere/adapter-graphql
bun add @fougere/adapter-rest @fougere/adapter-graphql
yarn add @fougere/adapter-rest @fougere/adapter-graphql
// fougere.config.ts
import '@fougere/adapter-rest';
import '@fougere/adapter-graphql';
export default defineFougere({
protocols: { rest: {}, graphql: {} },
});
The REST path is /api/{frond}/{address, plural}, and GraphQL answers on /graphql:
curl http://localhost:3000/api/shop/products
curl -X POST http://localhost:3000/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ products { items { title cents } } }"}'
A list is a page on both: the rows are in items. npx get-graphql-schema http://localhost:3000/graphql prints the whole schema.
→ Surfaces
One more field
Add a field to the entity:
description: optional(text({ max: 500 })),
and run npm run migrate. The column is in the table, description is in every REST answer,
in the GraphQL type and in createProduct and updateProduct, and a form that loops over
useFormFor(Product).fields shows it. 501 characters are refused in the browser, in REST
and in GraphQL, by the same rule. You wrote one line; nothing else could fall behind it.
Next: Shape your topology.