mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 04:06:44 +02:00
- Serveur Rust (server-rs/, axum + sqlx) : les 148 adresses de l'API, l'interface, les migrations de démarrage et les tâches de fond ; même comportement que Node (banc : 2027 cas, 0 écart hors écarts déclarés USERS-02, RAPPRO-21, R4, R5). - Node retiré : server/, dépendances serveur, image Docker sans Node (51 Mo), docker-compose à un seul service applicatif. - R4 : écritures d'un autre site refusées (Sec-Fetch-Site / Origin). - R5 : « aujourd'hui » et sauvegarde automatique à l'heure de Paris. - R6 : mainteneur dans .github/CODEOWNERS. - Menu latéral : Accueil puis Calendrier en tête. - Plan et état : docs/PLAN-MIGRATION-RUST.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ke2qyS8rgacP3ap5iezDbM
225 lines
8.0 KiB
TypeScript
225 lines
8.0 KiB
TypeScript
// Génère server-rs/src/schema.rs à partir de shared/schema.ts :
|
|
// - une structure par table, avec les mêmes clés JSON, dans le même ordre et
|
|
// aux mêmes formats que ce que Drizzle renvoie à l'interface ;
|
|
// - le schéma de validation que drizzle-zod (createInsertSchema) en tire.
|
|
// Les colonnes nulles dans la base réelle le sont aussi en Rust (DATABASE_URL).
|
|
//
|
|
// Usage : DATABASE_URL=postgres://… npx tsx scripts/gen-rust-schema.ts
|
|
import { execFileSync } from "child_process";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import pg from "pg";
|
|
import { getTableColumns } from "drizzle-orm";
|
|
import { getTableConfig, PgTable } from "drizzle-orm/pg-core";
|
|
import * as schema from "../shared/schema";
|
|
|
|
const OUTPUT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../server-rs/src/schema.rs");
|
|
|
|
const RUST_KEYWORDS = new Set(["type", "match", "ref", "move", "use", "loop", "where", "self", "struct", "enum"]);
|
|
|
|
function pascal(name: string): string {
|
|
return name.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
}
|
|
|
|
interface ColumnInfo {
|
|
jsKey: string;
|
|
name: string;
|
|
columnType: string;
|
|
notNull: boolean;
|
|
hasDefault: boolean;
|
|
length?: number;
|
|
scale?: number;
|
|
}
|
|
|
|
// Type SQL vers lequel le texte d'un paramètre est converti (comme node-pg, qui
|
|
// envoie tous les paramètres en texte)
|
|
function sqlType(column: ColumnInfo): string {
|
|
switch (column.columnType) {
|
|
case "PgSerial":
|
|
case "PgInteger":
|
|
return "int4";
|
|
case "PgBigInt53":
|
|
return "int8";
|
|
case "PgVarchar":
|
|
case "PgText":
|
|
return "text";
|
|
case "PgBoolean":
|
|
return "bool";
|
|
case "PgTimestamp":
|
|
return "timestamp";
|
|
case "PgDateString":
|
|
return "date";
|
|
case "PgNumeric":
|
|
return "numeric";
|
|
case "PgJsonb":
|
|
return "jsonb";
|
|
default:
|
|
throw new Error(`Type de colonne non géré : ${column.columnType} (${column.name})`);
|
|
}
|
|
}
|
|
|
|
function rustType(column: ColumnInfo): string {
|
|
switch (column.columnType) {
|
|
case "PgSerial":
|
|
case "PgInteger":
|
|
return "i32";
|
|
case "PgBigInt53":
|
|
return "i64";
|
|
case "PgVarchar":
|
|
case "PgText":
|
|
return "String";
|
|
case "PgBoolean":
|
|
return "bool";
|
|
case "PgTimestamp":
|
|
return "NaiveDateTime";
|
|
case "PgDateString":
|
|
return "NaiveDate";
|
|
case "PgNumeric":
|
|
return "Decimal";
|
|
case "PgJsonb":
|
|
return "serde_json::Value";
|
|
default:
|
|
throw new Error(`Type de colonne non géré : ${column.columnType} (${column.name})`);
|
|
}
|
|
}
|
|
|
|
// Même correspondance que drizzle-zod 0.7 (columnToSchema)
|
|
function zodField(column: ColumnInfo): string {
|
|
let field: string;
|
|
switch (column.columnType) {
|
|
case "PgSerial":
|
|
case "PgInteger":
|
|
field = `Field::int32("${column.jsKey}")`;
|
|
break;
|
|
case "PgBigInt53":
|
|
field = `Field::safe_integer("${column.jsKey}")`;
|
|
break;
|
|
case "PgVarchar":
|
|
field = column.length ? `Field::string("${column.jsKey}").max_len(${column.length})` : `Field::string("${column.jsKey}")`;
|
|
break;
|
|
case "PgText":
|
|
case "PgDateString":
|
|
case "PgNumeric":
|
|
field = `Field::string("${column.jsKey}")`;
|
|
break;
|
|
case "PgBoolean":
|
|
field = `Field::boolean("${column.jsKey}")`;
|
|
break;
|
|
case "PgTimestamp":
|
|
field = `Field::date("${column.jsKey}")`;
|
|
break;
|
|
case "PgJsonb":
|
|
field = `Field::json("${column.jsKey}")`;
|
|
break;
|
|
default:
|
|
throw new Error(`Type de colonne non géré : ${column.columnType}`);
|
|
}
|
|
if (!column.notNull) field += ".nullable()";
|
|
if (!column.notNull || column.hasDefault) field += ".optional()";
|
|
return field;
|
|
}
|
|
|
|
async function main() {
|
|
const nullableInDb = new Set<string>();
|
|
if (process.env.DATABASE_URL) {
|
|
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
await client.connect();
|
|
const { rows } = await client.query(
|
|
`SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'public' AND is_nullable = 'YES'`,
|
|
);
|
|
for (const row of rows) nullableInDb.add(`${row.table_name}.${row.column_name}`);
|
|
await client.end();
|
|
} else {
|
|
console.warn("DATABASE_URL absent : nullabilité tirée de shared/schema.ts seulement");
|
|
}
|
|
|
|
const tables = new Map<string, ColumnInfo[]>();
|
|
for (const value of Object.values(schema)) {
|
|
if (!(value instanceof PgTable)) continue;
|
|
const config = getTableConfig(value);
|
|
if (tables.has(config.name)) continue;
|
|
const columns = Object.entries(getTableColumns(value)).map(([jsKey, column]: [string, any]) => ({
|
|
jsKey,
|
|
name: column.name,
|
|
columnType: column.columnType,
|
|
notNull: column.notNull,
|
|
hasDefault: column.hasDefault,
|
|
length: column.length,
|
|
scale: column.scale,
|
|
}));
|
|
tables.set(config.name, columns);
|
|
}
|
|
|
|
const out: string[] = [];
|
|
out.push("// Généré par scripts/gen-rust-schema.ts à partir de shared/schema.ts : ne pas modifier à la main.");
|
|
out.push("// Une structure par table (clés, ordre et formats JSON de Drizzle) et son schéma");
|
|
out.push("// d'insertion drizzle-zod (createInsertSchema).");
|
|
out.push("#![allow(dead_code)]");
|
|
out.push("");
|
|
out.push("use chrono::{NaiveDate, NaiveDateTime};");
|
|
out.push("use rust_decimal::Decimal;");
|
|
out.push("use serde::Serialize;");
|
|
out.push("use sqlx::FromRow;");
|
|
out.push("");
|
|
out.push("use crate::db::{Col, Table};");
|
|
out.push("use crate::zod::{Field, Object};");
|
|
out.push("");
|
|
|
|
for (const [table, columns] of [...tables].sort(([a], [b]) => a.localeCompare(b))) {
|
|
const name = `${pascal(table)}Row`;
|
|
out.push(`/// Table ${table}`);
|
|
out.push("#[derive(Debug, Clone, Serialize, FromRow)]");
|
|
out.push(`pub struct ${name} {`);
|
|
for (const column of columns) {
|
|
const nullable = !column.notNull || nullableInDb.has(`${table}.${column.name}`);
|
|
const base = rustType(column);
|
|
const type = nullable ? `Option<${base}>` : base;
|
|
const attributes: string[] = [];
|
|
const field = RUST_KEYWORDS.has(column.name) ? `${column.name}_` : column.name;
|
|
if (field !== column.name) attributes.push(`#[sqlx(rename = "${column.name}")]`);
|
|
const serde = [`rename = "${column.jsKey}"`];
|
|
if (base === "NaiveDateTime") serde.push(`serialize_with = "${nullable ? "crate::json::ts_opt" : "crate::json::ts"}"`);
|
|
if (base === "Decimal") {
|
|
// Échelle de la colonne : PostgreSQL rend "0.00", sqlx lit 0
|
|
if (typeof column.scale !== "number") throw new Error(`Échelle inconnue : ${table}.${column.name}`);
|
|
serde.push(`serialize_with = "${nullable ? "crate::json::numeric_opt" : "crate::json::numeric"}::<${column.scale}, _>"`);
|
|
}
|
|
attributes.push(`#[serde(${serde.join(", ")})]`);
|
|
for (const attribute of attributes) out.push(` ${attribute}`);
|
|
out.push(` pub ${field}: ${type},`);
|
|
}
|
|
out.push("}");
|
|
out.push("");
|
|
out.push(`impl Table for ${name} {`);
|
|
out.push(` const TABLE: &'static str = "${table}";`);
|
|
out.push(` const COLUMNS: &'static str = "${columns.map((c) => `\\"${c.name}\\"`).join(", ")}";`);
|
|
out.push(" const FIELDS: &'static [Col] = &[");
|
|
for (const column of columns) {
|
|
out.push(` Col { key: "${column.jsKey}", column: "${column.name}", sql_type: "${sqlType(column)}" },`);
|
|
}
|
|
out.push(" ];");
|
|
out.push("}");
|
|
out.push("");
|
|
out.push(`impl ${name} {`);
|
|
out.push(` /// createInsertSchema(${table})`);
|
|
out.push(" pub fn insert_schema() -> Object {");
|
|
out.push(" Object::new(vec![");
|
|
for (const column of columns) out.push(` ${zodField(column)},`);
|
|
out.push(" ])");
|
|
out.push(" }");
|
|
out.push("}");
|
|
out.push("");
|
|
}
|
|
|
|
fs.writeFileSync(OUTPUT, out.join("\n"));
|
|
// Même mise en forme que cargo fmt (server-rs/rustfmt.toml)
|
|
execFileSync("rustfmt", ["--edition", "2024", "--config-path", path.dirname(path.dirname(OUTPUT)), OUTPUT]);
|
|
console.log(`${tables.size} tables écrites dans ${path.relative(process.cwd(), OUTPUT)}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|