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.

→ How a schema moves

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);
  }
}

→ Repositories

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
// 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.

Built with Fougere and Nuxt, hosted on GitHub Pages.