ytn – Paket-Nutzungsleitfaden
ytn (whY Try Not) bietet portable, in JS kompilierbare TypeScript-Pakete für Schema-Validierung, SQL-Generierung und CLI-Routing. Alle Pakete sind reines ESM, striktes TypeScript und haben wo möglich null externe Abhängigkeiten.
Pakete auf einen Blick
| Paket | npm | Installieren | Verwende es, wenn ... |
|---|---|---|---|
@ytrynot/dna | @ytrynot/dna | npm install @ytrynot/dna | Du eine Zod-ähnliche Schema-Validierung mit serialisierbarem Bytecode oder eigenständigen kompilierten Validatoren benötigst |
@ytrynot/schvalid | @ytrynot/schvalid | npm install @ytrynot/schvalid | Du ein JSON Schema 2020-12 hast und ultraschnelle Validierung benötigst |
@ytrynot/qb | @ytrynot/qb | npm install @ytrynot/qb | Du SQLite-SQL-Strings aus einer flüssigen API oder einem Zod/DNA-Schema erstellen musst |
@ytrynot/cli | @ytrynot/cli | npm install @ytrynot/cli | Du einen CLI-Router mit DNA-validierten Routen, Auto-Hilfe und AOT-Kompilierung benötigst |
@ytrynot/dna – Schema-Builder + DNA-Laufzeit
Installation
npm install @ytrynot/dna
Ein Schema definieren (Zod-ähnliche flüssige API)
import { dna } from "@ytrynot/dna";
const user = dna.object({
name: dna.string().min(2),
email: dna.string().email(),
age: dna.number().int().min(0).optional(),
role: dna.enum(["admin", "user"]).default("user"),
});
type User = dna.infer<typeof user>;
// { name: string; email: string; age?: number; role: "admin" | "user" }
Validieren / Parsen
// Boolesche Validierung (schnell, Fail-Fast)
user.validate({ name: "John", email: "[email protected]" }); // true
// Sicheres Parsen (kein Wurf)
const result = user.safeParse({ name: "J", email: "bad" });
// { success: false, errors: [...] }
// Parsen (wirft Fehler bei Fehler)
const data = user.parse({ name: "John", email: "[email protected]" });
In eigenständiges JS kompilieren (keine Laufzeit nötig)
import { dna, validator, parser } from "@ytrynot/dna";
const schema = dna.object({ name: dna.string().min(2), age: dna.number() });
const bytecode = schema.toDna(); // serialisierbarer DNA-Bytecode
// Einmal kompilieren, die Funktion wiederverwenden
const validate = validator(bytecode);
validate({ name: "John", age: 30 }); // true
const parse = parser(bytecode);
parse({ name: "John", age: 30 }); // { success: true, data: { name: "John", age: 30 } }
Objektmodi
dna.object({ name: dna.string() }) // standard — entfernt unbekannte Schlüssel
dna.strictObject({ name: dna.string() }) // strikt — lehnt unbekannte Schlüssel ab
dna.looseObject({ name: dna.string() }) // locker — lässt unbekannte Schlüssel durch
Häufige Schema-Typen
dna.string().min(1).max(100).email().uuid().url().regex(/^[a-z]+$/)
dna.number().int().positive().negative().min(0).max(100).multipleOf(2)
dna.boolean()
dna.date()
dna.bigint()
dna.literal("active")
dna.enum(["a", "b", "c"])
dna.array(dna.string()).min(1).max(10).nonempty()
dna.object({ ... })
dna.tuple([dna.string(), dna.number()])
dna.union([dna.string(), dna.number()])
dna.discriminatedUnion("type", [shapeA, shapeB])
dna.record(dna.string(), dna.boolean())
dna.map(dna.string(), dna.string())
dna.set(dna.string())
dna.optional() // dna.string().optional()
dna.nullable() // dna.string().nullable()
dna.default("fallback")
dna.catch("fallback")
dna.preprocess((v) => Number(v), dna.number())
dna.coerce.number().parse("123") // 123
dna.instanceof(UserClass)
dna.templateLiteral(["https://", dna.string(), ".", dna.enum(["com", "net"])])
dna.function().input([dna.string()]).output(dna.number()).implement((s) => s.length)
dna.lazy(() => dna.object({ children: dna.array(recursiveSchema) }))
dna.brand<"MyBrand">()
Verfeinerungen (Refinements)
dna.string().refine((s) => s.length > 3, "Zu kurz");
dna.array(dna.string()).superRefine((val, ctx) => {
if (val.length > 10) ctx.addIssue({ code: "too_big", maximum: 10, message: "Zu viele" });
});
Transformationen + Pipe
const schema = dna.string().transform((s) => s.length).pipe(dna.number());
schema.parse("Hallo"); // 5
Rekursion
// Muster 1: dna.lazy()
const TreeSchema: DnaLazy<Tree> = dna.lazy(() =>
dna.object({ value: dna.number(), children: dna.array(TreeSchema) })
);
// Muster 2: Objekt-Getter (Selbstreferenz)
const NodeSchema = dna.object({
value: dna.number(),
get children() { return dna.array(NodeSchema); },
});
Metadaten
const field = dna.string().meta({ title: "E-Mail", description: "Benutzer-E-Mail" });
const field2 = dna.string().describe("Vollständiger Name des Benutzers");
Weiterführende Lektüre
- README — vollständige API, Einstiegspunkte, CLI Union, Externals
- Rezepte — 15+ Copy-Paste-Rezepte (Objektmodi, Rekursion, Pipe, Verfeinerungen, Koerzion, Template-Literale, Funktions-Schemas, Records/Maps/Sets, Defaults, Branding, Metadaten)
- Technische Dokumentation — DNA-Opcodes, Architektur, Codegen-Interna
- Zod-Vergleich — DNA vs. Zod Feature-Parität
- Externals — Externals-Mechanismus für Transformationen
- Serialisierung — DNA-Serialisierungsformat
- Opcode-Muster — Opcode-Nutzungsmuster
@ytrynot/schvalid – JSON-Schema-zu-DNA-Compiler
Installation
npm install @ytrynot/schvalid
JSON-Schema in DNA umwandeln + validieren
import { jschemaToDna, validator, parser } from "@ytrynot/schvalid";
const jsonSchema = {
type: "object",
properties: {
name: { type: "string", minLength: 3 },
age: { type: "number", minimum: 0 },
},
required: ["name"],
};
const dna = jschemaToDna(jsonSchema);
// Schnelle boolesche Validierung
const validate = validator(dna);
validate({ name: "John", age: 30 }); // true
validate({ name: "Jo" }); // false (minLength 3)
// Vollständiges Parsen mit Fehlersammlung
const parse = parser(dna);
parse({ name: "John", age: 30 }); // { success: true, data: { name: "John", age: 30 } }
parse({ name: "Jo" }); // { success: false, errors: [...] }
Einmal kompilieren, viele Validierungen (Builder-API)
import { schvalid } from "@ytrynot/schvalid";
const compiler = schvalid("validation");
const validate = compiler.compile(jsonSchema);
validate({ name: "John", age: 30 }); // true
// Vier Modi:
schvalid("validation"); // boolescher Validator (Fail-Fast)
schvalid("parser"); // Parser mit Fehlersammlung + frischem Ausgabeobjekt
schvalid("fast"); // Hybrid: zuerst validieren, nur bei Fehler parsen (data === input bei Erfolg)
schvalid("all"); // { validate, parse, parseFast } — alle einmal kompiliert, gemeinsame Instanzen
Unterstützte JSON-Schema-2020-12-Features
- Typen: string, number, integer, boolean, null, object, array
- Einschränkungen: properties, required, items, additionalProperties, minItems, maxItems, minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, pattern, format, const, enum
- Logik: anyOf, allOf, oneOf, if/then/else, not
- OpenAPI 3.1: discriminator
- Referenzen: interne
$refnur (keine externen URIs)
Einschränkungen
- Keine externen
$ref(HTTP-URIs, URNs, externe Dateien) - Keine benutzerdefinierten Formate (nur Standardformate: E-Mail, UUID, URI usw.)
- Keine asynchrone Validierung, kein
$data, keine Typkoerzion, keine Standardwert-Einfügung
Weiterführende Lektüre
- README — vollständige API, Modi, Leistung
- AJV-Vergleich — schvalid vs. AJV Feature-für-Feature
@ytrynot/qb – SQLite-Query-Builder
Installation
npm install @ytrynot/qb
# Optionale Peer-Abhängigkeiten für schema-gesteuerte DDL:
npm install zod # für Zod v4 Schema-Introspection
npm install @ytrynot/dna # für DNA-Schema-Introspection
Flüssige DML-Abfragen
import { QueryBuilder as QB } from "@ytrynot/qb";
const sql = QB.table("users", "u")
.select(["u.id", "u.name", "u.email"])
.where(["u.active"])
.orderBy("u.created_at", "DESC")
.limit(10)
.toSQL();
// SELECT u.id, u.name, u.email FROM users AS u WHERE u.active = @active ORDER BY u.created_at DESC LIMIT 10
Eine Tabelle aus einem Zod-Schema definieren
import { z } from "zod";
import { QueryBuilder as QB } from "@ytrynot/qb";
const UserSchema = z.object({
id: z.string().uuid().meta({ pk: true }),
email: z.string().email().meta({ unique: true }),
name: z.string(),
age: z.number().int(),
created_at: z.date().optional(),
});
const users = QB.defTable("users", UserSchema);
users.createTable;
// CREATE TABLE IF NOT EXISTS users (
// id TEXT PRIMARY KEY,
// email TEXT UNIQUE NOT NULL,
// name TEXT NOT NULL,
// age INTEGER NOT NULL,
// created_at DATETIME
// );
users.getById; // SELECT * FROM users WHERE id = @id
users.insert; // INSERT INTO users (id, email, name, age, created_at) VALUES (@id, @email, @name, @age, @created_at)
users.update; // UPDATE users SET email = @email, name = @name, age = @age, created_at = @created_at WHERE id = @id
users.delete; // DELETE FROM users WHERE id = @id
users.upsert; // INSERT INTO users (...) VALUES (...) ON CONFLICT(id) DO UPDATE SET ...
Eine Tabelle aus einem DNA-Schema definieren
import { dna } from "@ytrynot/dna";
import { QueryBuilder as QB } from "@ytrynot/qb";
const UserSchema = dna.object({
id: dna.string().uuid().meta({ pk: true }),
email: dna.string().email().meta({ unique: true }),
name: dna.string(),
age: dna.int(),
created_at: dna.date().optional(),
});
const users = QB.defTable("users", UserSchema);
// Gleiche SQL-Ausgabe wie die Zod-Version
Benutzerdefinierte Abfragen mit .req / .q
users.req.select("id", "name").where("id").toSQL();
// SELECT id, name FROM users WHERE id = @id
users.req.upsert("email", "name").toSQL();
// INSERT INTO users (email, name) VALUES (@email, @name)
// ON CONFLICT(id, email) DO UPDATE SET name = excluded.name
Mit einem Treiber ausführen
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("app.db");
db.exec(QB.enableForeignKeys()); // PRAGMA foreign_keys = ON
db.exec(users.createTable);
const stmt = db.prepare(users.insert);
stmt.run({ id: crypto.randomUUID(), email: "[email protected]", name: "Alice", age: 30, created_at: null });
Metadatenschlüssel für DDL
| Schlüssel | Wirkung |
|---|---|
pk: true | PRIMARY KEY |
pkauto: true | PRIMARY KEY AUTOINCREMENT |
unique: true | UNIQUE |
default: value | DEFAULT value |
fk: { table, column } | REFERENCES table(column) |
Weiterführende Lektüre
- Schnellstart — vollständiges Tutorial, Ende-zu-Ende-CRUD
- How-to: Abfragen — SELECT, INSERT, UPDATE, DELETE, UPSERT, WHERE, JOINs, Sortierung, Limits, Paginierung, Textsuche, GROUP BY
- How-to: DDL — alle Schema-Quellen, Metadatenschlüssel, Fremdschlüssel, Indizes, zusammengesetzte PK
- How-to: Erweitert — EXISTS, CASE WHEN, Unterabfragen, Fensterfunktionen, PragmaBuilder
- Funktionsreferenz — vollständige Methodeninventur, SQLite-Versionsmatrix
@ytrynot/cli – DNA-validierter CLI-Router
Installation
npm install @ytrynot/cli
Routen als DNA-Schemas definieren
import { dna } from "@ytrynot/dna";
import { createContract, execute } from "@ytrynot/cli";
const buildRoute = dna.object({
cmd: dna.literal("build"),
files: dna.array(dna.string()).optional().meta({ description: "Zu erstellende Dateien" }),
}).meta({ cli: { routeId: "build" }, description: "Das Projekt erstellen" });
const deployRoute = dna.object({
cmd: dna.literal("deploy"),
target: dna.string().optional().meta({ description: "Bereitstellungsziel" }),
}).meta({ cli: { routeId: "deploy" }, description: "Das Projekt bereitstellen" });
const contract = createContract({
name: "mycli",
description: "Ein Demo-CLI",
targets: [buildRoute, deployRoute],
cli: { positionals: ["cmd", "files"] },
});
Ausführen (Ebene 1 — synchrones Routing + Validierung)
const result = execute(contract, ["build", "a.ts", "b.ts"]);
// { success: true, route: "build", payload: { cmd: "build", files: ["a.ts", "b.ts"] } }
execute(contract, ["deploy", "--target", "prod"]);
// { success: true, route: "deploy", payload: { cmd: "deploy", target: "prod" } }
execute(contract, ["unbekannt"]);
// { success: false, errors: [...] }
Vollständiges CLI mit Handlern + Formatierer + process.exit
import { executeContract, cliFactory, fullCli } from "@ytrynot/cli";
import type { ts } from "@ytrynot/cli";
const handlers: ts.Handlers = {
build: (payload) => ({ success: true, data: `${payload.files?.length ?? 0} Dateien erstellt` }),
deploy: (payload) => ({ success: true, data: `Nach ${payload.target ?? "Standard"} bereitgestellt` }),
};
const formatter: ts.FormatterFn = (result) => {
if (result.success) return { exit: 0, message: String(result.data ?? "") };
return { exit: 1, message: `Fehler: ${result.error}` };
};
const run = fullCli(cliFactory(executeContract(contract, handlers), formatter));
await run(); // liest process.argv.slice(2), gibt aus, beendet sich
--help / --version hinzufügen
import { helpRoute, versionRoute } from "@ytrynot/cli";
const contract = createContract({
name: "mycli",
description: "Ein Demo-CLI",
version: "1.0.0",
targets: [buildRoute, deployRoute, helpRoute, versionRoute],
cli: { positionals: ["cmd", "files"] },
});
// --help und --version werden automatisch zu den Routen hinzugefügt
CLI-Union (gemeinsame n-äre Flaggen)
const contract = createContract({
name: "mycli",
targets: [buildRoute, deployRoute],
cli: {
positionals: ["cmd", "...args"],
union: dna.object({
verbose: dna.boolean().default(false).meta({ description: "Ausführliche Ausgabe anzeigen" }),
color: dna.boolean().default(true).meta({ description: "Farben deaktivieren" }),
config: dna.string().optional().meta({ description: "Pfad zur Konfigurationsdatei" }),
}),
unionPosition: "before", // Position von Union-Flaggen: "before" | "after" Routen-Positionalen
},
});
AOT-Kompilierung (Ahead-of-Time)
import { compileContractToFile } from "@ytrynot/cli";
compileContractToFile(contract, "./dist/cli.js");
// Erzeugt eine eigenständige JS-Datei, die ohne das CLI-Paket ausgeführt werden kann
Die AOT-Datei minimiert die Startzeit und kann in Produktions-Images ohne TypeScript-Abhängigkeiten eingebettet werden.
Weiterführende Lektüre
- README — vollständige API, AOT, CLI-Union, benutzerdefinierte Formatierer
- How-to: CLI — Ende-zu-Ende-CLI-Einrichtung mit AOT
- How-to: Union — gemeinsame Flaggen, CI/CD-Integration, Umgebungsvariablen
Lizenz
ytn ist unter der MIT-Lizenz lizenziert.