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 {post.title} @@ -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() {
  • {post.title} - {/* Source (array, usually 1 element) */} {post.source?.[0] && ( Source : {post.source[0].title} )} - {/* Tags (array of 0..N elements) */}
    {post.tags?.map(tag => ( {tag.title} @@ -282,7 +255,7 @@ export default async function ActualitesPage() { } ``` -### News detail page (relations always included) +### Page de détail ```js // app/actualites/[slug]/page.js @@ -298,19 +271,14 @@ export default async function ActualiteDetailPage({ params }) {

    {post.title}

    - {/* datetime field: display with time */} - {/* Source — array even with a single element */} {post.source?.[0] && ( -

    - Source : {post.source[0].title} -

    +

    Source : {post.source[0].title}

    )} - {/* Tags */} {post.tags?.length > 0 && (
    {post.tags.map(tag => ( @@ -326,46 +294,7 @@ export default async function ActualiteDetailPage({ params }) { } ``` -### CVE detail page - -```js -// app/cve/[slug]/page.js -export default async function CVEDetailPage({ params }) { - const res = await fetch( - `${process.env.NEXT_PUBLIC_URL}/zen/api/posts/cve/${params.slug}` - ); - const { post } = await res.json(); - - if (!post) notFound(); - - return ( -
    -

    {post.title}

    -

    ID : {post.cve_id}

    -

    Sévérité : {post.severity} — Score : {post.score}

    -

    Produit : {post.product}

    - - {/* 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 |