diff --git a/docs/dev/GUIDE.md b/docs/dev/GUIDE.md
index b7ee815..f9f20a0 100644
--- a/docs/dev/GUIDE.md
+++ b/docs/dev/GUIDE.md
@@ -6,7 +6,6 @@ Tout ce qui est **code** est en **anglais**, sans exception :
- Noms de fichiers (sauf dossiers de routes Next.js, voir ci-dessous)
- Variables, fonctions, classes, composants
- Commentaires dans le code
-- README.md
- Props, événements, constantes, types
- Git commit
@@ -15,7 +14,7 @@ Tout ce qui est **code** est en **anglais**, sans exception :
Tout ce qui est **visible par l'utilisateur** est en **français** :
- Textes, titres, descriptions, labels
- Slugs et noms de dossiers qui correspondent à des routes URL
-- Documentations
+- Documentations, README.md
## Messages de commit Git
diff --git a/src/modules/README.md b/src/modules/README.md
deleted file mode 100644
index 81f70c1..0000000
--- a/src/modules/README.md
+++ /dev/null
@@ -1,284 +0,0 @@
-# Module System
-
-Modules are self-contained features that can be enabled/disabled via environment variables.
-
-## File Structure
-
-```
-src/modules/your-module/
-├── module.config.js # Required — navigation, pages, widgets
-├── db.js # Database schema (createTables / dropTables)
-├── crud.js # CRUD operations
-├── actions.js # Server actions (for public pages)
-├── metadata.js # SEO metadata generators
-├── api.js # API route handlers
-├── cron.config.js # Scheduled tasks
-├── index.js # Public API re-exports
-├── .env.example # Environment variable documentation
-├── admin/ # Admin pages (lazy-loaded)
-│ └── index.js # Re-exports admin components
-├── pages/ # Public pages (lazy-loaded)
-│ └── index.js
-├── dashboard/ # Dashboard widgets
-│ ├── statsActions.js
-│ └── Widget.js
-└── sub-feature/ # Optional sub-modules (e.g. items/, categories/)
- ├── db.js
- ├── crud.js
- └── admin/
-```
-
-> Not all files are required. Only create what the module actually needs.
-
----
-
-## Step 1 — Create `module.config.js`
-
-```javascript
-import { lazy } from 'react';
-
-export default {
- // Module identity
- name: 'your-module',
- displayName: 'Your Module',
- version: '1.0.0',
- description: 'Description of your module',
-
- // Other modules this one depends on (must be enabled too)
- dependencies: ['clients'],
-
- // Environment variables this module uses (documentation only)
- envVars: [
- 'YOUR_MODULE_API_KEY',
- ],
-
- // Admin navigation — single section object or array of section objects
- navigation: {
- id: 'your-module',
- title: 'Your Module',
- icon: 'SomeIcon', // String icon name from shared/Icons.js
- items: [
- { name: 'Items', href: '/admin/your-module/items', icon: 'PackageIcon' },
- { name: 'New', href: '/admin/your-module/items/new', icon: 'PlusSignIcon' },
- ],
- },
-
- // Admin pages — path → lazy component
- adminPages: {
- '/admin/your-module/items': lazy(() => import('./admin/ItemsListPage.js')),
- '/admin/your-module/items/new': lazy(() => import('./admin/ItemCreatePage.js')),
- '/admin/your-module/items/edit': lazy(() => import('./admin/ItemEditPage.js')),
- },
-
- // (Optional) Custom resolver for dynamic paths not known at build time.
- // Called before the adminPages map. Return the lazy component or null.
- pageResolver(path) {
- const parts = path.split('/').filter(Boolean);
- // example: /admin/your-module/{type}/list
- if (parts[2] === 'list') return lazy(() => import('./admin/ItemsListPage.js'));
- return null;
- },
-
- // Public pages — keyed by 'default' (one component handles all public routes)
- publicPages: {
- default: lazy(() => import('./pages/YourModulePublicPages.js')),
- },
-
- // Public route patterns for SEO/route matching (relative to /zen/your-module/)
- publicRoutes: [
- { pattern: ':id', description: 'View item' },
- { pattern: ':id/pdf', description: 'PDF viewer' },
- ],
-
- // Dashboard widgets (lazy-loaded, rendered on the admin dashboard)
- dashboardWidgets: [
- lazy(() => import('./dashboard/Widget.js')),
- ],
-};
-```
-
-### Navigation as multiple sections
-
-When a module provides several distinct sections (like the `posts` module with one section per post type), set `navigation` to an array:
-
-```javascript
-navigation: [
- { id: 'your-module-foo', title: 'Foo', icon: 'Book02Icon', items: [...] },
- { id: 'your-module-bar', title: 'Bar', icon: 'Layers01Icon', items: [...] },
-],
-```
-
----
-
-## Step 2 — Create `db.js`
-
-Every module that uses a database must expose a `createTables` function:
-
-```javascript
-import { query } from '@hykocx/zen/database';
-
-export async function createTables() {
- const created = [];
- const skipped = [];
-
- const exists = await query(`
- SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = $1
- )`, ['zen_your_module']);
-
- if (!exists.rows[0].exists) {
- await query(`
- CREATE TABLE zen_your_module (
- id SERIAL PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
- )
- `);
- created.push('zen_your_module');
- } else {
- skipped.push('zen_your_module');
- }
-
- return { created, skipped };
-}
-
-export async function dropTables() {
- await query(`DROP TABLE IF EXISTS zen_your_module CASCADE`);
-}
-```
-
-> **Never create migrations.** Instead, provide the SQL to the user so they can run it manually.
-
----
-
-## Step 3 — Create `.env.example`
-
-Document every environment variable the module reads:
-
-```bash
-#################################
-# MODULE YOUR-MODULE
-ZEN_MODULE_YOUR_MODULE=false
-
-ZEN_MODULE_YOUR_MODULE_API_KEY=
-ZEN_MODULE_YOUR_MODULE_SOME_OPTION=default_value
-#################################
-```
-
----
-
-## Step 4 — Create `cron.config.js` (optional)
-
-Only needed if the module requires scheduled tasks:
-
-```javascript
-import { doSomething } from './reminders.js';
-
-export default {
- jobs: [
- {
- name: 'your-module-task',
- description: 'Description of what this job does',
- schedule: '*/5 * * * *', // cron expression
- handler: doSomething,
- timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
- },
- ],
-};
-```
-
----
-
-## Step 5 — Register the module in 5 files
-
-### `modules/modules.registry.js` — add the module name
-
-```javascript
-export const AVAILABLE_MODULES = [
- 'clients',
- 'invoice',
- 'your-module',
-];
-```
-
-### `modules/modules.pages.js` — import the config
-
-```javascript
-'use client';
-
-import yourModuleConfig from './your-module/module.config.js';
-
-const MODULE_CONFIGS = {
- // ...existing modules...
- 'your-module': yourModuleConfig,
-};
-```
-
-### `modules/modules.actions.js` — import server actions (if public pages or dashboard widgets)
-
-```javascript
-import { yourPublicAction } from './your-module/actions.js';
-import { getYourModuleDashboardStats } from './your-module/dashboard/statsActions.js';
-
-export const MODULE_ACTIONS = {
- // ...existing modules...
- 'your-module': { yourPublicAction },
-};
-
-export const MODULE_DASHBOARD_ACTIONS = {
- // ...existing modules...
- 'your-module': getYourModuleDashboardStats,
-};
-```
-
-### `modules/modules.metadata.js` — import metadata generators (if SEO needed)
-
-```javascript
-import * as yourModuleMetadata from './your-module/metadata.js';
-
-export const MODULE_METADATA = {
- // ...existing modules...
- 'your-module': yourModuleMetadata,
-};
-```
-
-### `modules/init.js` — register the database initializer
-
-```javascript
-import { createTables as createYourModuleTables } from './your-module/db.js';
-
-const MODULE_DB_INITIALIZERS = {
- // ...existing modules...
- 'your-module': createYourModuleTables,
-};
-```
-
----
-
-## Step 6 — Enable the module
-
-```bash
-ZEN_MODULE_YOUR_MODULE=true
-```
-
-The environment variable is derived from the module name: `ZEN_MODULE_` + module name uppercased (hyphens become underscores).
-
----
-
-## Sub-modules
-
-For complex modules, group related features into sub-directories. Each sub-module follows the same pattern (`db.js`, `crud.js`, `admin/`). The parent `module.config.js` registers all sub-module admin pages, and the parent `index.js` re-exports everything publicly.
-
-See `src/modules/invoice/` for a complete example with `items/`, `categories/`, `transactions/`, and `recurrences/` sub-modules.
-
----
-
-## Reference implementations
-
-| Module | Features demonstrated |
-|--------|-----------------------|
-| `src/modules/invoice/` | Sub-modules, public pages, cron jobs, Stripe, email, PDF, dashboard widgets, metadata |
-| `src/modules/posts/` | Dynamic config from env vars, `pageResolver`, multiple navigation sections |
-| `src/modules/clients/` | Simple module, dependencies, no public pages |
diff --git a/src/modules/posts/README.md b/src/modules/posts/README.md
index 4b13f3a..13c5bbe 100644
--- a/src/modules/posts/README.md
+++ b/src/modules/posts/README.md
@@ -1,99 +1,102 @@
-# Posts Module
+# Module Posts
-Configurable Custom Post Types via environment variables. Inspired by the WordPress CPT concept: each project declares its own content types (blog, CVE, job, event, etc.) with the fields it needs, without modifying code.
+Types de contenus configurables via variables d'environnement. Chaque projet déclare ses propres types (blogue, CVE, emploi, événement...) avec les champs dont il a besoin, sans toucher au code.
---
-## Features
+## Configuration
-- **Multiple post types** in a single module (blog, CVE, job...)
-- **Dynamic fields** per type: title, slug, text, markdown, date, datetime, color, category, image, relation
-- **Generic admin**: forms adapt automatically to the config
-- **Public API** per type for integration in a Next.js site
-- **Optional categories** per type (enabled if a `category` field is defined)
-- **Relations** between types (many-to-many, e.g. CVE → Tags)
-- **Unique slugs** per type (scoped: `blogue/mon-article` ≠ `cve/mon-article`)
+### Variables d'environnement
----
-
-## Installation
-
-### 1. Environment variables
-
-Copy variables from [`.env.example`](.env.example) into your `.env`:
-
-> If no label is provided (`ZEN_MODULE_POSTS_TYPES=blogue`), the display name will be the key with the first letter capitalized (`Blogue`).
-
-**Optional (images):**
-
-If one of your types uses the `image` field, configure Zen storage in your main `.env` (`ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`).
-
-### 2. Available field types
-
-| Type | `.env` syntax | Description |
-|---|---|---|
-| `title` | `name:title` | Main text field — auto-generates the slug |
-| `slug` | `name:slug` | Unique URL slug per type — auto-filled from title |
-| `text` | `name:text` | Free text area (textarea) |
-| `markdown` | `name:markdown` | Markdown editor with preview |
-| `date` | `name:date` | Date picker only (YYYY-MM-DD) |
-| `datetime` | `name:datetime` | Date **and time** picker (YYYY-MM-DDTHH:MM) |
-| `color` | `name:color` | Color picker — stores a hex code `#rrggbb` |
-| `category` | `name:category` | Dropdown linked to the category table |
-| `image` | `name:image` | Image upload to Zen storage |
-| `relation` | `name:relation:target_type` | Multi-select to posts of another type |
-
-> **Rule:** each type must have at least one `title` field and one `slug` field. The `category` field automatically creates the `zen_posts_category` table. The `relation` field automatically creates the `zen_posts_relations` table.
-
-#### `date` vs `datetime`
-
-- `date` → stores `"2026-03-14"` — sufficient for blog posts, events
-- `datetime` → stores `"2026-03-14T10:30:00.000Z"` (ISO 8601, UTC) — needed for CVEs, security bulletins, precise schedules
-
-### `relation` field — Linking posts together
-
-The `relation` field associates multiple posts of another type (many-to-many). Example: news posts referencing a source and tags.
+Copier les variables de [`.env.example`](.env.example) dans votre `.env`.
```bash
-ZEN_MODULE_POSTS_TYPE_ACTUALITE=title:title|slug:slug|date:datetime|resume:text|content:markdown|image:image|source:relation:source|tags:relation:tag
+# Liste des types (séparés par |, en minuscules)
+# Format optionnel avec label : cle:Label
+ZEN_MODULE_POSTS_TYPES=blogue:Blogue|cve:CVE|emploi:Emplois
+
+# Champs par type : nom:type|nom:type|...
+ZEN_MODULE_POSTS_TYPE_BLOGUE=title:title|slug:slug|category:category|date:date|resume:text|content:markdown|image:image
+ZEN_MODULE_POSTS_TYPE_CVE=title:title|slug:slug|cve_id:text|severity:text|date:datetime|description:markdown|tags:relation:tag
+ZEN_MODULE_POSTS_TYPE_TAG=title:title|slug:slug
+```
+
+Si aucun label n'est fourni (`ZEN_MODULE_POSTS_TYPES=blogue`), le nom affiché sera la clé avec la première lettre en majuscule.
+
+### Types de champs
+
+| Type | Syntaxe `.env` | Description |
+|---|---|---|
+| `title` | `nom:title` | Champ texte principal, génère le slug automatiquement |
+| `slug` | `nom:slug` | Slug unique par type, pré-rempli depuis le titre |
+| `text` | `nom:text` | Zone de texte libre |
+| `markdown` | `nom:markdown` | Éditeur Markdown avec prévisualisation |
+| `date` | `nom:date` | Sélecteur de date (YYYY-MM-DD) |
+| `datetime` | `nom:datetime` | Date et heure (ISO 8601, UTC) |
+| `color` | `nom:color` | Sélecteur de couleur, stocke un code hex `#rrggbb` |
+| `category` | `nom:category` | Menu déroulant lié à la table des catégories |
+| `image` | `nom:image` | Upload d'image vers le stockage Zen |
+| `relation` | `nom:relation:type_cible` | Sélection multiple vers des posts d'un autre type |
+
+Chaque type doit avoir au moins un champ `title` et un champ `slug`.
+
+**`date` ou `datetime` ?**
+
+- `date` stocke `"2026-03-14"`. Suffisant pour un billet de blogue ou un événement.
+- `datetime` stocke `"2026-03-14T10:30:00.000Z"`. Nécessaire pour les CVE ou tout contenu avec une heure précise.
+
+### Champ `relation`
+
+Le champ `relation` associe plusieurs posts d'un autre type (many-to-many).
+
+```bash
+ZEN_MODULE_POSTS_TYPE_ACTUALITE=title:title|slug:slug|date:datetime|resume:text|content:markdown|source:relation:source|tags:relation:tag
ZEN_MODULE_POSTS_TYPE_TAG=title:title|slug:slug
ZEN_MODULE_POSTS_TYPE_SOURCE=title:title|slug:slug
```
-- The name before `relation` (`source`, `tags`) is the field name in the form and in the API response
-- The value after `relation` (`source`, `tag`) is the target post type
-- Selection is **multiple** (multi-select with real-time search)
-- Relations are stored in `zen_posts_relations` (junction table)
-- In the API, relations are returned as an array of objects containing **all fields** of the linked post: `tags: [{ id, slug, title, color, ... }]`
+- Le nom avant `relation` (`source`, `tags`) est le nom du champ dans le formulaire et dans la réponse API.
+- La valeur après `relation` (`source`, `tag`) est le type cible.
+- La sélection est multiple avec recherche en temps réel.
+- Les relations sont stockées dans `zen_posts_relations`.
+- Dans l'API, les relations sont retournées comme un tableau d'objets avec tous les champs du post lié.
-### 3. Database tables
+### Images
-Tables are created automatically with `npx zen-db init`. For reference, here are the module tables:
+Si un type utilise le champ `image`, configurer le stockage Zen dans le `.env` principal : `ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`.
+
+---
+
+## Base de données
+
+Les tables sont créées automatiquement avec `npx zen-db init`.
| Table | Description |
|---|---|
-| `zen_posts` | Posts (all types — custom fields in `data JSONB`) |
-| `zen_posts_category` | Categories per type (if a `category` field is defined) |
-| `zen_posts_relations` | Relations between posts (if a `relation` field is defined) |
+| `zen_posts` | Posts de tous les types (champs personnalisés dans `data JSONB`) |
+| `zen_posts_category` | Catégories par type (créée si un champ `category` est défini) |
+| `zen_posts_relations` | Relations entre posts (créée si un champ `relation` est défini) |
-> **Design:** all custom fields are stored in the `data JSONB` column. Adding or removing a field in `.env` requires no SQL migration.
+Tous les champs personnalisés sont dans la colonne `data JSONB`. Ajouter ou retirer un champ dans le `.env` ne nécessite aucune migration SQL.
---
-## Admin interface
+## Interface d'administration
| Page | URL |
|---|---|
-| Post list for a type | `/admin/posts/{type}/list` |
-| Create a post | `/admin/posts/{type}/new` |
-| Edit a post | `/admin/posts/{type}/edit/{id}` |
-| Category list for a type | `/admin/posts/{type}/categories` |
-| Create a category | `/admin/posts/{type}/categories/new` |
-| Edit a category | `/admin/posts/{type}/categories/edit/{id}` |
+| Liste des posts | `/admin/posts/{type}/list` |
+| Créer un post | `/admin/posts/{type}/new` |
+| Modifier un post | `/admin/posts/{type}/edit/{id}` |
+| Liste des catégories | `/admin/posts/{type}/categories` |
+| Créer une catégorie | `/admin/posts/{type}/categories/new` |
+| Modifier une catégorie | `/admin/posts/{type}/categories/edit/{id}` |
---
-## Public API (no authentication)
+## API publique
+
+Pas d'authentification requise.
### Config
@@ -101,28 +104,27 @@ Tables are created automatically with `npx zen-db init`. For reference, here are
GET /zen/api/posts/config
```
-Returns the list of all configured types with their fields.
+Retourne la liste de tous les types configurés avec leurs champs.
-### Post list
+### Liste de posts
```
GET /zen/api/posts/{type}
```
-**Query parameters:**
-
-| Parameter | Default | Description |
+| Paramètre | Défaut | Description |
|---|---|---|
-| `page` | `1` | Current page |
-| `limit` | `20` | Results per page |
-| `category_id` | — | Filter by category |
-| `sortBy` | `created_at` | Sort by (field name of the type) |
-| `sortOrder` | `DESC` | `ASC` or `DESC` |
-| `withRelations` | `false` | `true` to include relation fields in each post |
+| `page` | `1` | Page courante |
+| `limit` | `20` | Résultats par page |
+| `category_id` | — | Filtrer par catégorie |
+| `sortBy` | `created_at` | Trier par (nom de champ du type) |
+| `sortOrder` | `DESC` | `ASC` ou `DESC` |
+| `withRelations` | `false` | `true` pour inclure les champs relation |
-> **Performance:** `withRelations=true` runs an additional SQL query per post. Use with a reasonable `limit` (≤ 20). On a detail page, prefer `/posts/{type}/{slug}` which always loads relations.
+`withRelations=true` exécute une requête SQL supplémentaire par post. Garder un `limit` raisonnable (20 maximum). Sur une page de détail, préférer `/posts/{type}/{slug}` qui charge toujours les relations.
+
+**Réponse sans relations (défaut) :**
-**Response without `withRelations` (default):**
```json
{
"success": true,
@@ -146,7 +148,8 @@ GET /zen/api/posts/{type}
}
```
-**Response with `withRelations=true`:**
+**Réponse avec `withRelations=true` :**
+
```json
{
"success": true,
@@ -160,61 +163,33 @@ GET /zen/api/posts/{type}
{ "id": 3, "slug": "cert-fr", "post_type": "source", "title": "CERT-FR" }
],
"tags": [
- { "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" },
- { "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" }
+ { "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" },
+ { "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" }
]
}
]
}
```
-### Single post by slug
+### Post par slug
```
GET /zen/api/posts/{type}/{slug}
```
-Relations are **always included** on a single post.
+Les relations sont toujours incluses sur un post individuel.
-**Response for a news post:**
-```json
-{
- "success": true,
- "post": {
- "id": 1,
- "post_type": "actualite",
- "slug": "faille-critique-openssh",
- "title": "Faille critique dans OpenSSH",
- "date": "2026-03-14T10:30:00.000Z",
- "resume": "Une faille critique a été découverte...",
- "content": "# Détails\n\n...",
- "image": "blog/1234567890-image.webp",
-
- "source": [
- { "id": 3, "slug": "cert-fr", "post_type": "source", "title": "CERT-FR" }
- ],
- "tags": [
- { "id": 7, "slug": "openssh", "post_type": "tag", "title": "OpenSSH" },
- { "id": 8, "slug": "vulnerabilite", "post_type": "tag", "title": "Vulnérabilité" }
- ],
-
- "created_at": "2026-03-14T12:00:00Z",
- "updated_at": "2026-03-14T12:00:00Z"
- }
-}
-```
-
-### Categories
+### Catégories
```
GET /zen/api/posts/{type}/categories
```
-Returns the list of active categories for the type (to populate a filter).
+Retourne les catégories actives du type (pour alimenter un filtre).
### Images
-Image keys are built as follows: `/zen/api/storage/{image_field_value}`
+Les clés d'image s'utilisent avec la route de stockage :
```jsx
@@ -222,9 +197,9 @@ Image keys are built as follows: `/zen/api/storage/{image_field_value}`
---
-## Next.js integration examples
+## Intégration Next.js
-### News list (without relations)
+### Liste de posts
```js
// app/actualites/page.js
@@ -248,7 +223,7 @@ export default async function ActualitesPage() {
}
```
-### News list with tags and source (withRelations)
+### Liste avec relations
```js
// app/actualites/page.js
@@ -264,12 +239,10 @@ export default async function ActualitesPage() {
-
- {/* Disclosure date and time */}
-
-
- {/* Associated tags */}
- {post.tags?.length > 0 && (
-
- {post.tags.map(tag => (
- {tag.title}
- ))}
-
- )}
-
-
{post.description}
-
- );
-}
-```
-
-### Dynamic SEO metadata
+### Métadonnées SEO dynamiques
```js
// app/actualites/[slug]/page.js
@@ -390,63 +319,74 @@ export async function generateMetadata({ params }) {
---
-## Adding a new post type
+## Ajouter ou modifier un type
-Edit `.env` only — no database restart needed:
+**Ajouter un type :** modifier uniquement le `.env`, pas besoin de redémarrer la base.
```bash
-# Before
-ZEN_MODULE_POSTS_TYPES=cve:CVEs|actualite:Actualités
+# Avant
+ZEN_MODULE_POSTS_TYPES=cve:CVE|actualite:Actualités
-# After — adding the 'evenement' type
-ZEN_MODULE_POSTS_TYPES=cve:CVEs|actualite:Actualités|evenement:Événements
+# Après
+ZEN_MODULE_POSTS_TYPES=cve:CVE|actualite:Actualités|evenement:Événements
ZEN_MODULE_POSTS_TYPE_EVENEMENT=title:title|slug:slug|date:datetime|location:text|description:markdown|image:image
```
-Restart the server. Tables are unchanged (new fields use the existing JSONB).
+Redémarrer le serveur. Les tables ne changent pas (les nouveaux champs utilisent le JSONB existant).
-## Modifying fields on an existing type
-
-Update the `ZEN_MODULE_POSTS_TYPE_*` variable and restart. Existing posts keep their data in JSONB even if a field is removed from the config — it simply won't appear in the form anymore.
+**Modifier les champs d'un type existant :** mettre à jour la variable `ZEN_MODULE_POSTS_TYPE_*` et redémarrer. Les posts existants conservent leurs données en JSONB, même si un champ est retiré de la config.
---
-## Programmatic usage (importers / fetchers)
+## Utilisation programmatique
-CRUD functions are directly importable server-side. No need to go through the HTTP API — ideal for cron jobs, import scripts, or automated fetchers.
+Les fonctions CRUD sont importables côté serveur. Idéal pour les cron jobs, scripts d'import ou fetchers automatisés.
-### Available functions
+### Fonctions disponibles
```js
import {
- createPost, // Create a post
- updatePost, // Update a post
- getPostBySlug, // Find by slug
- getPostByField, // Find by any JSONB field
- upsertPost, // Create or update (idempotent, for importers)
- getPosts, // List with pagination
- deletePost, // Delete
+ createPost, // Créer un post
+ updatePost, // Modifier un post
+ getPostBySlug, // Chercher par slug
+ getPostByField, // Chercher par n'importe quel champ JSONB
+ upsertPost, // Créer ou mettre à jour (idempotent)
+ getPosts, // Liste avec pagination
+ deletePost, // Supprimer
} from '@hykocx/zen/modules/posts/crud';
```
### `upsertPost(postType, rawData, uniqueField)`
-Key function for importers: creates the post if it doesn't exist, updates it otherwise.
+Crée le post s'il n'existe pas, le met à jour sinon.
-- `postType`: the post type (`'cve'`, `'actualite'`, etc.)
-- `rawData`: post data (same fields as for `createPost`)
-- `uniqueField`: the deduplication key field (`'slug'` by default, or `'cve_id'`, etc.)
+- `postType` : le type de post (`'cve'`, `'actualite'`...)
+- `rawData` : les données du post (mêmes champs que pour `createPost`)
+- `uniqueField` : le champ de déduplication (`'slug'` par défaut)
-Returns `{ post, created: boolean }`.
+Retourne `{ post, created: boolean }`.
-### Example — CVE fetcher (cron job)
+### Champs `relation` dans `rawData`
+
+Les champs `relation` reçoivent un **tableau d'IDs** de posts existants.
+
+```js
+// Correct
+{ tags: [7, 8, 12], source: [3] }
+
+// Incorrect
+{ tags: ['openssh', 'vuln'], source: { id: 3 } }
+```
+
+Si les posts liés n'existent pas encore, les créer d'abord avec `upsertPost` puis utiliser leurs IDs.
+
+### Exemple : fetcher de CVE
```js
// src/cron/fetch-cves.js
import { upsertPost } from '@hykocx/zen/modules/posts/crud';
export async function fetchAndImportCVEs() {
- // 1. Fetch data from an external source
const response = await fetch('https://api.example.com/cves/recent');
const { cves } = await response.json();
@@ -454,24 +394,24 @@ export async function fetchAndImportCVEs() {
for (const cve of cves) {
try {
- // 2. Resolve relations — ensure tags exist
+ // Résoudre les relations : s'assurer que les tags existent
const tagIds = [];
for (const tagName of (cve.tags || [])) {
const { post: tag } = await upsertPost('tag', { title: tagName }, 'slug');
tagIds.push(tag.id);
}
- // 3. Upsert the CVE (deduplicated on cve_id)
+ // Upsert du CVE, dédupliqué sur cve_id
const { created } = await upsertPost('cve', {
- title: cve.title, // title field
- cve_id: cve.id, // text field
- severity: cve.severity, // text field
- score: String(cve.cvssScore), // text field
- product: cve.affectedProduct, // text field
- date: cve.publishedAt, // datetime field (ISO 8601)
- description: cve.description, // markdown field
- tags: tagIds, // relation:tag field — array of IDs
- }, 'cve_id'); // deduplicate on cve_id
+ title: cve.title,
+ cve_id: cve.id,
+ severity: cve.severity,
+ score: String(cve.cvssScore),
+ product: cve.affectedProduct,
+ date: cve.publishedAt,
+ description: cve.description,
+ tags: tagIds,
+ }, 'cve_id');
created ? results.created++ : results.updated++;
} catch (err) {
@@ -485,17 +425,14 @@ export async function fetchAndImportCVEs() {
}
```
-### Example — News fetcher with source
+### Exemple : fetcher d'actualités avec source
```js
-import { upsertPost, getPostByField } from '@hykocx/zen/modules/posts/crud';
+import { upsertPost } from '@hykocx/zen/modules/posts/crud';
-export async function fetchAndImportActualites(sourceName, sourceUrl, articles) {
- // 1. Ensure the source exists
- const { post: source } = await upsertPost('source', {
- title: sourceName,
- color: '#3b82f6',
- }, 'slug');
+export async function fetchAndImportActualites(sourceName, articles) {
+ // S'assurer que la source existe
+ const { post: source } = await upsertPost('source', { title: sourceName }, 'slug');
for (const article of articles) {
await upsertPost('actualite', {
@@ -503,45 +440,31 @@ export async function fetchAndImportActualites(sourceName, sourceUrl, articles)
date: article.publishedAt,
resume: article.summary,
content: article.content,
- source: [source.id], // relation:source — array of IDs
- tags: [], // relation:tag
+ source: [source.id],
+ tags: [],
}, 'slug');
}
}
```
-### Rules for relation fields in `rawData`
-
-`relation` fields must receive an **array of IDs** of existing posts:
-
-```js
-// Correct
-{ tags: [7, 8, 12], source: [3] }
-
-// Incorrect — no slugs or objects
-{ tags: ['openssh', 'vuln'], source: { id: 3 } }
-```
-
-If the linked posts don't exist yet, create them first with `upsertPost` then use their IDs.
-
---
-## Admin API (authentication required)
+## API d'administration
-These routes require an active admin session.
+Authentification requise.
-| Method | Route | Description |
+| Méthode | Route | Description |
|---|---|---|
-| `GET` | `/zen/api/admin/posts/config` | Full config for all types |
-| `GET` | `/zen/api/admin/posts/search?type={type}&q={query}` | Search posts for the relation picker |
-| `GET` | `/zen/api/admin/posts/posts?type={type}` | Post list for a type |
-| `GET` | `/zen/api/admin/posts/posts?type={type}&withRelations=true` | List with relations included |
-| `GET` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Post by ID (relations always included) |
-| `POST` | `/zen/api/admin/posts/posts?type={type}` | Create a post |
-| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Update a post |
-| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Delete a post |
-| `POST` | `/zen/api/admin/posts/upload-image` | Upload an image |
-| `GET` | `/zen/api/admin/posts/categories?type={type}` | Category list |
-| `POST` | `/zen/api/admin/posts/categories?type={type}` | Create a category |
-| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Update a category |
-| `DELETE` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Delete a category |
+| `GET` | `/zen/api/admin/posts/config` | Config complète de tous les types |
+| `GET` | `/zen/api/admin/posts/search?type={type}&q={query}` | Recherche pour le sélecteur de relation |
+| `GET` | `/zen/api/admin/posts/posts?type={type}` | Liste des posts d'un type |
+| `GET` | `/zen/api/admin/posts/posts?type={type}&withRelations=true` | Liste avec relations |
+| `GET` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Post par ID (relations toujours incluses) |
+| `POST` | `/zen/api/admin/posts/posts?type={type}` | Créer un post |
+| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Modifier un post |
+| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Supprimer un post |
+| `POST` | `/zen/api/admin/posts/upload-image` | Upload d'image |
+| `GET` | `/zen/api/admin/posts/categories?type={type}` | Liste des catégories |
+| `POST` | `/zen/api/admin/posts/categories?type={type}` | Créer une catégorie |
+| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Modifier une catégorie |
+| `DELETE` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Supprimer une catégorie |