Read settings and secrets
Most entities are rows: there are many posts, and a handler reads them through a repository. Some entities have exactly one instance: the address and key of a search engine, the settings of a catalog. Such an entity is declared like any other and asked for by its type:
// entities/Meili.ts
export default class Meili extends entity({
url: url(),
key: text(),
timeoutMs: number({ integer: true, default: 2000 }),
}) {}
// services/Search.ts
export default class Search {
constructor(private meili: Meili) {}
}
Search receives { url, key, timeoutMs }. The code names no variable and no file: where the
value comes from is said in fougere.config.ts.
import '@fougere/adapter-sql';
import '@fougere/adapter-env';
export default defineFougere({
sources: {
main: { type: 'sqlite', path: '.data/app.db' },
env: { type: 'env', values: ['Meili'] },
},
});
An entry of sources: lists two kinds of entities: rows:, the ones it keeps as rows, and
values:, the ones it keeps as one value each. An entity is one or the other, never both.
Where a value comes from
| source | where Meili is read |
|---|---|
env | MEILI_URL, MEILI_KEY, MEILI_TIMEOUT_MS — one variable per field, in SNAKE_CASE |
file | <path>/meili.yaml (or .json, .yml) — one file, beside the directories of rows |
sqlite, pg, mysql, mssql, d1, memory | the row meili of the table fougere_values |
Text is read as the field declares: a number, a boolean (true, false, 1, 0), JSON for
an object or a list. A table of values is part of the schema like any other table:
fougere migrate creates it, and the boot refuses when it is missing.
Production and staging are two deployments, each with its own environment, or its own directory of files:
config: { type: 'file', path: `config/${process.env.APP_ENV}`, values: ['Catalog'] },
Changing what a source gave
A secret kept elsewhere — a vault, a cloud's secret manager — is a link in front of where the value is
read, Env<Meili>:
// fronds/search/services/MeiliFromVault.ts
import { Env } from '@fougere/core';
import Meili from '../entities/Meili.js';
import Vault from './Vault.js';
export default class MeiliFromVault implements Partial<Env<Meili>> {
constructor(private inner: Env<Meili>, private vault: Vault) {}
async read() {
return { ...await this.inner.read(), key: await this.vault.secret('meili') };
}
}
It touches Meili and nothing else: the key is Env<Meili>, as written. What it answers is judged and
completed like what the source holds, once, at boot — a class asking for Meili receives the value, never
the link. The value is read once: a key that changes while the app runs is picked up at the next restart.
What the boot checks
The value is read once, after the schema is checked and before the seeds. The defaults of its fields fill what is missing, and the fields judge the rest. A value that does not hold refuses the boot, one line per field, with the place to fix it:
Fougere boot refused: 2 value(s) do not hold:
meili.url: Does not match format "uri" — sources.env, MEILI_URL
meili.key: Required — sources.env, MEILI_KEY
A value nothing sets is not refused for being absent: it is refused for each required field it lacks, and a value whose fields all have defaults holds with nothing set at all.
A value has no repository and no table of its own. Reaching it as rows — Repository<Meili> —
refuses the boot, and so does asking for a row entity as one value.
Who receives it
A value is registered in the frond that declares its entity, and in the fronds under it. A value several fronds need lives in the frond they all extend: such a frond may hold no rows, and it may hold values.