diff --git a/README.md b/README.md index 852e470..0f49d73 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ The repo is centered around the `skills/` directory: - `tidbx` - TiDB Cloud provisioning and lifecycle workflows. - `tidbx-serverless-driver` - Serverless HTTP driver usage and edge runtime guidance. - `tidbx-kysely` - Kysely integration patterns (TCP + serverless/edge). +- `tidbx-prisma` - Prisma ORM setup (schema, migrations, typed client) for TiDB. +- `tidbx-nextjs` - Next.js App Router patterns for TiDB (route handlers, runtimes, Vercel/serverless). +- `tidbx-javascript-mysql2` - Node.js mysql2 driver connection patterns (TLS/CA, pool, transactions). +- `tidbx-javascript-mysqljs` - Node.js mysqljs/mysql driver connection patterns (TLS/CA, pool, transactions). ## Contributing diff --git a/skills/tidbx-javascript-mysql2/SKILL.md b/skills/tidbx-javascript-mysql2/SKILL.md new file mode 100644 index 0000000..8b32e81 --- /dev/null +++ b/skills/tidbx-javascript-mysql2/SKILL.md @@ -0,0 +1,142 @@ +--- +name: tidbx-javascript-mysql2 +description: Connect to TiDB from JavaScript/Node.js using the mysql2 driver (mysql2/promise). Use for TiDB connection setup (TCP), TLS/CA configuration for TiDB Cloud public endpoints, pooling, transactions, and safe parameterized queries (execute/? placeholders) plus small runnable connection/CRUD templates. +--- + +# TiDB + JavaScript (mysql2) + +Use this skill to connect to TiDB from Node.js with the `mysql2` driver (promise API), configure TLS correctly for TiDB Cloud, and provide small runnable snippets you can copy into a project. + +## Important: mysql vs mysql2 + +These two drivers are easy to mix up: + +- This skill is for **mysql2** (`npm i mysql2`) and uses the promise API (`mysql2/promise`). +- If you meant **mysqljs/mysql** (`npm i mysql`) which is callback-based, jump to the `tidbx-javascript-mysqljs` skill instead. + +## Recommendation: prefer an ORM for app code + +For most application code (models, migrations, types), prefer an ORM/query builder: + +- Prisma ORM: use `tidbx-prisma` +- Kysely query builder: use `tidbx-kysely` + +## When to use this skill + +- Connect to TiDB from JavaScript/TypeScript (Node.js runtime) via `mysql2` / `mysql2/promise`. +- Need TLS guidance (TiDB Cloud public endpoint) or CA certificate setup (TiDB Cloud Dedicated). +- Want a minimal "connect -> SELECT VERSION() -> CRUD" template. + +## Code generation rules (Node.js) + +- Never hardcode credentials; use `DATABASE_URL` or `TIDB_*` env vars. +- Prefer `mysql2/promise` and parameterized queries (`?` placeholders via `execute()` / `query()`). +- Default to a pool for apps/services (`createPool`), and `await pool.end()` on shutdown. +- When using a pool against TiDB Cloud, set an idle timeout <= 300s (e.g. `idleTimeout: 300_000`) and keep `connectionLimit` small in serverless environments. +- Do not recommend `multipleStatements: true` unless the user explicitly needs it. + +## Connection checklist + +1. Confirm deployment type: TiDB Cloud (Starter/Essential vs Dedicated) or self-managed. +2. Confirm endpoint type: public vs private/VPC. +3. Decide config style: + - **Preferred**: `DATABASE_URL` (easy for deployment). + - **Alternative**: `TIDB_HOST/TIDB_USER/...` options (handy for local/dev). +4. If using a **public endpoint** on TiDB Cloud, enable **TLS**. + +## Install + +```bash +npm i mysql2 +``` + +If you want `.env` support: + +```bash +npm i dotenv +``` + +## Minimal snippets + +### Option A: connect with `DATABASE_URL` (recommended) + +```js +import { createConnection } from 'mysql2/promise' + +const conn = await createConnection(process.env.DATABASE_URL) +const [[row]] = await conn.query('SELECT VERSION() AS v') +console.log(row.v) +await conn.end() +``` + +Notes: +- URL-encode special characters in passwords (best: copy the URL from the TiDB Cloud "Connect" dialog). +- If the TiDB Cloud connect dialog provides TLS options in the URL, keep them as-is. + +### Option B: connect with connection options (TLS / CA) + +Use this when you want explicit TLS config (common for TiDB Cloud Dedicated with a downloaded CA). + +```js +import { createPool } from 'mysql2/promise' +import fs from 'node:fs' + +const pool = createPool({ + host: process.env.TIDB_HOST, + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER, + password: process.env.TIDB_PASSWORD, + database: process.env.TIDB_DATABASE ?? 'test', + ssl: process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, +}) +``` + +Suggested env vars: + +```bash +TIDB_HOST=... +TIDB_PORT=4000 +TIDB_USER=... +TIDB_PASSWORD=... +TIDB_DATABASE=test +TIDB_ENABLE_SSL=true +# Optional (commonly used for TiDB Cloud Dedicated): +TIDB_CA_PATH=/absolute/path/to/ca.pem +``` + +## CRUD + safety patterns + +- Prefer `execute()` for parameterized SQL: + +```js +const [result] = await pool.execute( + 'INSERT INTO players (coins, goods) VALUES (?, ?)', + [100, 100], +) +``` + +- Use explicit transactions for multi-step updates: + +```js +const conn = await pool.getConnection() +try { + await conn.beginTransaction() + await conn.execute('UPDATE players SET coins = coins + ? WHERE id = ?', [50, id]) + await conn.commit() +} catch (e) { + await conn.rollback() + throw e +} finally { + conn.release() +} +``` + +## Templates & scripts in this skill + +- `scripts/validate_connection.mjs` -- reads `DATABASE_URL` or `TIDB_*`, connects, prints `SELECT VERSION()`, then exits. +- `templates/quickstart.mjs` -- minimal end-to-end: connect -> create table -> insert -> query -> update -> delete. diff --git a/skills/tidbx-javascript-mysql2/scripts/validate_connection.mjs b/skills/tidbx-javascript-mysql2/scripts/validate_connection.mjs new file mode 100644 index 0000000..33c9c4e --- /dev/null +++ b/skills/tidbx-javascript-mysql2/scripts/validate_connection.mjs @@ -0,0 +1,51 @@ +// Minimal TiDB connection smoke test for Node.js + mysql2 (promise API). +// +// Usage: +// 1) npm i mysql2 dotenv +// 2) Set DATABASE_URL or TIDB_* env vars (see ../SKILL.md) +// 3) node scripts/validate_connection.mjs + +import { createConnection } from 'mysql2/promise' +import fs from 'node:fs' + +try { + await import('dotenv/config') +} catch { + // dotenv is optional; environment variables may be provided by the runtime instead. +} + +function buildOptionsFromEnv() { + return { + host: process.env.TIDB_HOST ?? '127.0.0.1', + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER ?? 'root', + password: process.env.TIDB_PASSWORD ?? '', + database: process.env.TIDB_DATABASE ?? 'test', + ssl: + process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, + } +} + +async function main() { + const conn = process.env.DATABASE_URL + ? await createConnection(process.env.DATABASE_URL) + : await createConnection(buildOptionsFromEnv()) + + try { + const [[row]] = await conn.query('SELECT VERSION() AS tidb_version') + console.log(`Connected. VERSION() = ${row.tidb_version}`) + } finally { + await conn.end() + } +} + +main().catch((err) => { + console.error(`Failed to connect: ${err?.message ?? String(err)}`) + process.exitCode = 1 +}) + diff --git a/skills/tidbx-javascript-mysql2/templates/quickstart.mjs b/skills/tidbx-javascript-mysql2/templates/quickstart.mjs new file mode 100644 index 0000000..3a6324f --- /dev/null +++ b/skills/tidbx-javascript-mysql2/templates/quickstart.mjs @@ -0,0 +1,80 @@ +// Minimal end-to-end TiDB quickstart for Node.js + mysql2 (promise API). +// +// Usage: +// 1) npm i mysql2 dotenv +// 2) Set DATABASE_URL or TIDB_* env vars (see ../SKILL.md) +// 3) node templates/quickstart.mjs + +import { createConnection } from 'mysql2/promise' +import fs from 'node:fs' + +try { + await import('dotenv/config') +} catch { + // dotenv is optional; environment variables may be provided by the runtime instead. +} + +function buildOptionsFromEnv() { + return { + host: process.env.TIDB_HOST ?? '127.0.0.1', + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER ?? 'root', + password: process.env.TIDB_PASSWORD ?? '', + database: process.env.TIDB_DATABASE ?? 'test', + ssl: + process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, + } +} + +async function main() { + const conn = process.env.DATABASE_URL + ? await createConnection(process.env.DATABASE_URL) + : await createConnection(buildOptionsFromEnv()) + + try { + const [[ver]] = await conn.query('SELECT VERSION() AS tidb_version') + console.log(`Connected (VERSION() = ${ver.tidb_version})`) + + await conn.execute(` + CREATE TABLE IF NOT EXISTS players ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + coins INT NOT NULL, + goods INT NOT NULL + ) + `) + + const [insertResult] = await conn.execute( + 'INSERT INTO players (coins, goods) VALUES (?, ?)', + [100, 100], + ) + const playerId = insertResult.insertId + console.log(`Inserted player id=${playerId}`) + + const [[player]] = await conn.execute('SELECT id, coins, goods FROM players WHERE id = ?', [ + playerId, + ]) + console.log(`Read player`, player) + + await conn.execute('UPDATE players SET coins = coins + ?, goods = goods + ? WHERE id = ?', [ + 50, + 50, + playerId, + ]) + console.log(`Updated player id=${playerId}`) + + await conn.execute('DELETE FROM players WHERE id = ?', [playerId]) + console.log(`Deleted player id=${playerId}`) + } finally { + await conn.end() + } +} + +main().catch((err) => { + console.error(err) + process.exitCode = 1 +}) diff --git a/skills/tidbx-javascript-mysqljs/SKILL.md b/skills/tidbx-javascript-mysqljs/SKILL.md new file mode 100644 index 0000000..56e3be0 --- /dev/null +++ b/skills/tidbx-javascript-mysqljs/SKILL.md @@ -0,0 +1,124 @@ +--- +name: tidbx-javascript-mysqljs +description: Connect to TiDB from JavaScript/Node.js using the mysqljs/mysql driver (npm package: mysql). Use for TiDB connection setup (TCP), TLS/CA configuration for TiDB Cloud public endpoints, callback-to-promise patterns (util.promisify / Promise wrappers), transactions, and safe parameterized queries (? placeholders) with small runnable connection/CRUD templates. +--- + +# TiDB + JavaScript (mysqljs/mysql) + +Use this skill to connect to TiDB from Node.js with the `mysql` package (aka mysqljs/mysql), configure TLS correctly for TiDB Cloud, and provide small runnable snippets you can copy into a project. + +## Important: mysql vs mysql2 + +These two drivers are easy to mix up: + +- This skill is for **mysqljs/mysql** (`npm i mysql`) which is primarily callback-based. +- If you want the promise-native driver **mysql2** (`npm i mysql2` / `mysql2/promise`), jump to the `tidbx-javascript-mysql2` skill instead. + +## Recommendation: prefer an ORM for app code + +For most application code (models, migrations, types), prefer an ORM/query builder: + +- Prisma ORM: use `tidbx-prisma` +- Kysely query builder: use `tidbx-kysely` + +## When to use this skill + +- Connect to TiDB from JavaScript/TypeScript (Node.js runtime) via `mysql` (mysqljs/mysql). +- Need TLS guidance (TiDB Cloud public endpoint) or CA certificate setup (TiDB Cloud Dedicated). +- Want a minimal "connect -> SELECT VERSION() -> CRUD" template using mysqljs/mysql. + +## Code generation rules (Node.js) + +- Never hardcode credentials; use `DATABASE_URL` or `TIDB_*` env vars. +- Use parameterized queries with `?` placeholders; never string-concatenate untrusted input. +- Prefer a pool for apps/services (`createPool`) and `pool.end()` on shutdown. +- mysqljs/mysql is callback-first; wrap callback APIs with `util.promisify` or small `new Promise(...)` helpers. + +## Install + +```bash +npm i mysql +``` + +If you want `.env` support: + +```bash +npm i dotenv +``` + +## Minimal snippets + +### Option A: connect with `DATABASE_URL` (recommended) + +```js +import mysql from 'mysql' +import util from 'node:util' + +const conn = mysql.createConnection(process.env.DATABASE_URL) +const query = util.promisify(conn.query).bind(conn) + +const rows = await query('SELECT VERSION() AS v') +console.log(rows[0].v) +await util.promisify(conn.end).bind(conn)() +``` + +### Option B: connect with connection options (TLS / CA) + +Use this when you want explicit TLS config (common for TiDB Cloud Dedicated with a downloaded CA). + +```js +import mysql from 'mysql' +import fs from 'node:fs' + +const conn = mysql.createConnection({ + host: process.env.TIDB_HOST, + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER, + password: process.env.TIDB_PASSWORD, + database: process.env.TIDB_DATABASE ?? 'test', + ssl: process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, +}) +``` + +Suggested env vars: + +```bash +TIDB_HOST=... +TIDB_PORT=4000 +TIDB_USER=... +TIDB_PASSWORD=... +TIDB_DATABASE=test +TIDB_ENABLE_SSL=true +# Optional (commonly used for TiDB Cloud Dedicated): +TIDB_CA_PATH=/absolute/path/to/ca.pem +``` + +## Transactions (mysqljs/mysql) + +```js +import util from 'node:util' + +const begin = util.promisify(conn.beginTransaction).bind(conn) +const commit = util.promisify(conn.commit).bind(conn) +const rollback = util.promisify(conn.rollback).bind(conn) +const query = util.promisify(conn.query).bind(conn) + +try { + await begin() + await query('UPDATE players SET coins = coins + ? WHERE id = ?', [50, id]) + await commit() +} catch (e) { + await rollback() + throw e +} +``` + +## Templates & scripts in this skill + +- `scripts/validate_connection.mjs` -- reads `DATABASE_URL` or `TIDB_*`, connects, prints `SELECT VERSION()`, then exits. +- `templates/quickstart.mjs` -- minimal end-to-end: connect -> create table -> insert -> query -> update -> delete. diff --git a/skills/tidbx-javascript-mysqljs/scripts/validate_connection.mjs b/skills/tidbx-javascript-mysqljs/scripts/validate_connection.mjs new file mode 100644 index 0000000..ffc2db3 --- /dev/null +++ b/skills/tidbx-javascript-mysqljs/scripts/validate_connection.mjs @@ -0,0 +1,55 @@ +// Minimal TiDB connection smoke test for Node.js + mysqljs/mysql (callback API). +// +// Usage: +// 1) npm i mysql dotenv +// 2) Set DATABASE_URL or TIDB_* env vars (see ../SKILL.md) +// 3) node scripts/validate_connection.mjs + +import mysql from 'mysql' +import fs from 'node:fs' +import util from 'node:util' + +try { + await import('dotenv/config') +} catch { + // dotenv is optional; environment variables may be provided by the runtime instead. +} + +function buildOptionsFromEnv() { + return { + host: process.env.TIDB_HOST ?? '127.0.0.1', + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER ?? 'root', + password: process.env.TIDB_PASSWORD ?? '', + database: process.env.TIDB_DATABASE ?? 'test', + ssl: + process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, + } +} + +async function main() { + const conn = process.env.DATABASE_URL + ? mysql.createConnection(process.env.DATABASE_URL) + : mysql.createConnection(buildOptionsFromEnv()) + + const query = util.promisify(conn.query).bind(conn) + const end = util.promisify(conn.end).bind(conn) + + try { + const rows = await query('SELECT VERSION() AS tidb_version') + console.log(`Connected. VERSION() = ${rows[0].tidb_version}`) + } finally { + await end() + } +} + +main().catch((err) => { + console.error(`Failed to connect: ${err?.message ?? String(err)}`) + process.exitCode = 1 +}) + diff --git a/skills/tidbx-javascript-mysqljs/templates/quickstart.mjs b/skills/tidbx-javascript-mysqljs/templates/quickstart.mjs new file mode 100644 index 0000000..6508858 --- /dev/null +++ b/skills/tidbx-javascript-mysqljs/templates/quickstart.mjs @@ -0,0 +1,76 @@ +// Minimal end-to-end TiDB quickstart for Node.js + mysqljs/mysql (callback API). +// +// Usage: +// 1) npm i mysql dotenv +// 2) Set DATABASE_URL or TIDB_* env vars (see ../SKILL.md) +// 3) node templates/quickstart.mjs + +import mysql from 'mysql' +import fs from 'node:fs' +import util from 'node:util' + +try { + await import('dotenv/config') +} catch { + // dotenv is optional; environment variables may be provided by the runtime instead. +} + +function buildOptionsFromEnv() { + return { + host: process.env.TIDB_HOST ?? '127.0.0.1', + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER ?? 'root', + password: process.env.TIDB_PASSWORD ?? '', + database: process.env.TIDB_DATABASE ?? 'test', + ssl: + process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, + } +} + +async function main() { + const conn = process.env.DATABASE_URL + ? mysql.createConnection(process.env.DATABASE_URL) + : mysql.createConnection(buildOptionsFromEnv()) + + const query = util.promisify(conn.query).bind(conn) + const end = util.promisify(conn.end).bind(conn) + + try { + const rows = await query('SELECT VERSION() AS tidb_version') + console.log(`Connected (VERSION() = ${rows[0].tidb_version})`) + + await query(` + CREATE TABLE IF NOT EXISTS players ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + coins INT NOT NULL, + goods INT NOT NULL + ) + `) + + const insertOk = await query('INSERT INTO players (coins, goods) VALUES (?, ?)', [100, 100]) + const playerId = insertOk.insertId + console.log(`Inserted player id=${playerId}`) + + const playerRows = await query('SELECT id, coins, goods FROM players WHERE id = ?', [playerId]) + console.log('Read player', playerRows[0]) + + await query('UPDATE players SET coins = coins + ?, goods = goods + ? WHERE id = ?', [50, 50, playerId]) + console.log(`Updated player id=${playerId}`) + + await query('DELETE FROM players WHERE id = ?', [playerId]) + console.log(`Deleted player id=${playerId}`) + } finally { + await end() + } +} + +main().catch((err) => { + console.error(err) + process.exitCode = 1 +}) + diff --git a/skills/tidbx-nextjs/SKILL.md b/skills/tidbx-nextjs/SKILL.md new file mode 100644 index 0000000..ffeb736 --- /dev/null +++ b/skills/tidbx-nextjs/SKILL.md @@ -0,0 +1,70 @@ +--- +name: tidbx-nextjs +description: Build and deploy Next.js (App Router) apps that connect to TiDB. Covers Route Handlers (app/api/*/route.ts), Node vs Edge runtime selection for database access, environment variable handling, and production-safe DB patterns on Vercel/serverless. Prefer Prisma/Kysely integration, with optional mysql2 for minimal examples. Includes guides and TypeScript templates. +--- + +# TiDB Next.js Integration + +Set up a Next.js app (App Router) that connects to TiDB, with clear guidance on where database code can run (Node runtime) and how to avoid common serverless pitfalls. + +## What is different about Next.js (vs plain Node scripts) + +- Full-stack framework: UI and server code live in one project. +- Server/client split: database code must stay server-side (route handlers, server components, server actions). +- Multiple runtimes: some routes can run on Edge; TCP database clients must run on Node runtime. +- File-based routing: API endpoints are files like `app/api/*/route.ts`. + +## When to use this skill + +- Build a new Next.js app that reads/writes TiDB +- Add TiDB to an existing Next.js App Router project +- Deploy to Vercel (or other serverless platforms) and avoid connection/caching/runtime issues +- Decide between Prisma/Kysely vs direct mysql2 + +## Key decisions (recommended defaults) + +1. Prefer an ORM/query builder for application code: + - Prisma ORM: `tidbx-prisma` + - Kysely query builder: `tidbx-kysely` +2. Use a direct driver (`mysql2`) only for minimal demos or when you need raw SQL without an ORM. +3. Run TCP database clients in the Node.js runtime (not Edge). + +## TiDB Cloud pool settings (mysql2) + +- If you use a connection pool against TiDB Cloud, set the pool idle timeout to <= 300s (e.g. `idleTimeout: 300_000`). +- Keep the pool small in serverless environments (often `connectionLimit: 1` is enough per function/container). + +## Available guides + +- `guides/quickstart.md` -- create a Next.js app and add TiDB (Prisma-first; mysql2 alternative) +- `guides/troubleshooting.md` -- runtime/env/caching/connection issues + +## Quick examples + +- "Add a /api/health route that queries TiDB" -> route handler + pooled connection +- "Use Prisma in Next.js and deploy to Vercel" -> prisma singleton + route handlers +- "My route handler fails on Vercel Edge" -> force Node runtime or switch to serverless HTTP driver + +## Templates + +- `templates/route-handler-mysql2.ts` -- App Router Route Handler that queries TiDB with mysql2 (Node runtime) +- `templates/tidb-mysql2-pool.ts` -- mysql2 pool helper with TLS and serverless-friendly defaults +- `templates/prisma-client.ts` -- PrismaClient singleton for Next.js (dev hot reload safe) +- `templates/route-handler-prisma.ts` -- Route Handler using Prisma ($queryRaw + CRUD) + +## Related skills + +- `tidbx-prisma` -- Prisma ORM (models + migrations + typed client) +- `tidbx-kysely` -- Kysely (typed query builder) +- `tidbx-serverless-driver` -- TiDB Cloud serverless HTTP driver (for edge-like runtimes) +- `tidbx` -- provision/manage TiDB Cloud clusters + +--- + +## Workflow + +I will: +1. Confirm your Next.js routing mode (App Router) and runtime target (Node serverless vs Edge) +2. Confirm your TiDB deployment (TiDB Cloud vs self-managed) and endpoint type (public vs private) +3. Recommend Prisma/Kysely by default, unless you explicitly want mysql2 +4. Generate the minimal files (route handler + db client helper) and env vars to get you running diff --git a/skills/tidbx-nextjs/guides/quickstart.md b/skills/tidbx-nextjs/guides/quickstart.md new file mode 100644 index 0000000..30f9212 --- /dev/null +++ b/skills/tidbx-nextjs/guides/quickstart.md @@ -0,0 +1,64 @@ +# Quickstart (Next.js App Router + TiDB) + +Goal: add a working backend-for-frontend API in `app/api/*/route.ts` that queries TiDB. + +This guide defaults to Prisma (recommended) and also includes a mysql2 alternative. + +## Phase 1: Create a Next.js project (TypeScript) + +```bash +npx create-next-app@latest +``` + +Choose: +- App Router: yes +- TypeScript: yes + +## Phase 2: Configure environment variables + +For local development, put secrets in `.env.local` (do not commit it). + +Pick one style: + +- Prisma: `DATABASE_URL=...` (preferred) +- mysql2: `TIDB_HOST/TIDB_USER/...` (or `DATABASE_URL` if you want) + +## Phase 3A (recommended): Prisma path + +Follow `tidbx-prisma` to set up Prisma schema + migrations. + +Then, add: + +1) `src/lib/prisma.ts` from `templates/prisma-client.ts` +2) `src/app/api/health/route.ts` from `templates/route-handler-prisma.ts` + +Run: + +```bash +npx prisma migrate dev +npm run dev +``` + +## Phase 3B (minimal demo): mysql2 path + +Install: + +```bash +npm i mysql2 +``` + +Then, add: + +1) `src/lib/tidb.ts` from `templates/tidb-mysql2-pool.ts` +2) `src/app/api/health/route.ts` from `templates/route-handler-mysql2.ts` + +Run: + +```bash +npm run dev +``` + +## Phase 4: Deploy + +- Set env vars in your hosting provider (e.g., Vercel Project Settings -> Environment Variables). +- Keep DB code server-only: route handlers, server actions, or server components. diff --git a/skills/tidbx-nextjs/guides/troubleshooting.md b/skills/tidbx-nextjs/guides/troubleshooting.md new file mode 100644 index 0000000..e7314cc --- /dev/null +++ b/skills/tidbx-nextjs/guides/troubleshooting.md @@ -0,0 +1,29 @@ +# Troubleshooting (Next.js + TiDB) + +## "Edge runtime" errors / Node APIs missing + +Symptoms: +- Errors about Node built-ins (`net`, `tls`, `fs`) or TCP sockets +- mysql2/Prisma fails in an Edge runtime + +Fix: +- Ensure the code runs in Node.js runtime routes. +- For Route Handlers, export `runtime = 'nodejs'`. + +If you must run on Edge-like runtimes, use `tidbx-serverless-driver` instead of TCP drivers. + +## Leaking secrets to the browser + +- Never put DB credentials in `NEXT_PUBLIC_*` env vars. +- Do not import DB client modules into Client Components (`'use client'`). + +## Too many connections / connection errors on serverless + +- Use a singleton/pool pattern (cache on `globalThis`) to reuse between invocations when possible. +- Keep pools small for serverless environments. + +## Unexpected caching of API responses + +- Ensure your Route Handler is dynamic if it reads fresh DB state. +- For GET handlers, you can export `dynamic = 'force-dynamic'` to avoid static caching. + diff --git a/skills/tidbx-nextjs/templates/prisma-client.ts b/skills/tidbx-nextjs/templates/prisma-client.ts new file mode 100644 index 0000000..8e02872 --- /dev/null +++ b/skills/tidbx-nextjs/templates/prisma-client.ts @@ -0,0 +1,12 @@ +import 'server-only' + +import { PrismaClient } from '@prisma/client' + +type GlobalWithPrisma = typeof globalThis & { __prisma?: PrismaClient } + +export const prisma: PrismaClient = (globalThis as GlobalWithPrisma).__prisma ?? new PrismaClient() + +if (process.env.NODE_ENV !== 'production') { + ;(globalThis as GlobalWithPrisma).__prisma = prisma +} + diff --git a/skills/tidbx-nextjs/templates/route-handler-mysql2.ts b/skills/tidbx-nextjs/templates/route-handler-mysql2.ts new file mode 100644 index 0000000..31dcf32 --- /dev/null +++ b/skills/tidbx-nextjs/templates/route-handler-mysql2.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server' + +// Import path note: +// - If this file lives at `src/app/api//route.ts`, and your pool helper is `src/lib/tidb.ts`, +// then `../../../lib/tidb` is the correct relative import. +// - If you do NOT use a `src/` directory (i.e. `app/api/...` and `lib/...` at repo root), +// change this to `../../lib/tidb`. +import { getTiDBPool } from '../../../lib/tidb' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET() { + const pool = getTiDBPool() + const [rows] = await pool.query('SELECT VERSION() AS tidb_version') + return NextResponse.json({ rows }) +} diff --git a/skills/tidbx-nextjs/templates/route-handler-prisma.ts b/skills/tidbx-nextjs/templates/route-handler-prisma.ts new file mode 100644 index 0000000..4934dfe --- /dev/null +++ b/skills/tidbx-nextjs/templates/route-handler-prisma.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' + +// Import path note: +// - If this file lives at `src/app/api//route.ts`, and your Prisma helper is `src/lib/prisma.ts`, +// then `../../../lib/prisma` is the correct relative import. +// - If you do NOT use a `src/` directory (i.e. `app/api/...` and `lib/...` at repo root), +// change this to `../../lib/prisma`. +import { prisma } from '../../../lib/prisma' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET() { + const rows = await prisma.$queryRaw<{ version: string }[]>`SELECT VERSION() AS version` + return NextResponse.json({ version: rows[0]?.version ?? 'unknown' }) +} diff --git a/skills/tidbx-nextjs/templates/tidb-mysql2-pool.ts b/skills/tidbx-nextjs/templates/tidb-mysql2-pool.ts new file mode 100644 index 0000000..71f5422 --- /dev/null +++ b/skills/tidbx-nextjs/templates/tidb-mysql2-pool.ts @@ -0,0 +1,36 @@ +import 'server-only' + +import fs from 'node:fs' +import { createPool, type Pool } from 'mysql2/promise' + +type GlobalWithTiDBPool = typeof globalThis & { __tidbPool?: Pool } + +function createTiDBPool(): Pool { + return createPool({ + host: process.env.TIDB_HOST, + port: Number(process.env.TIDB_PORT ?? 4000), + user: process.env.TIDB_USER, + password: process.env.TIDB_PASSWORD, + database: process.env.TIDB_DATABASE ?? 'test', + ssl: + process.env.TIDB_ENABLE_SSL === 'true' + ? { + minVersion: 'TLSv1.2', + ca: process.env.TIDB_CA_PATH ? fs.readFileSync(process.env.TIDB_CA_PATH) : undefined, + } + : undefined, + connectionLimit: Number(process.env.TIDB_CONNECTION_LIMIT ?? 1), + maxIdle: Number(process.env.TIDB_MAX_IDLE ?? 1), + // TiDB Cloud connections can be closed when idle; keep idle timeout <= 300s. + idleTimeout: Number(process.env.TIDB_IDLE_TIMEOUT_MS ?? 300_000), + enableKeepAlive: true, + }) +} + +export function getTiDBPool(): Pool { + const globalWithPool = globalThis as GlobalWithTiDBPool + if (!globalWithPool.__tidbPool) { + globalWithPool.__tidbPool = createTiDBPool() + } + return globalWithPool.__tidbPool +} diff --git a/skills/tidbx-prisma/SKILL.md b/skills/tidbx-prisma/SKILL.md new file mode 100644 index 0000000..c181bcb --- /dev/null +++ b/skills/tidbx-prisma/SKILL.md @@ -0,0 +1,120 @@ +--- +name: tidbx-prisma +description: Prisma ORM setup and usage for TiDB from Node.js/TypeScript. Covers configuring prisma/schema.prisma (MySQL provider), DATABASE_URL formatting for TiDB Cloud TLS (sslaccept=strict and optional sslcert), migrations (prisma migrate), Prisma Client generation, CRUD patterns, and safe raw SQL ($queryRaw) plus runnable templates. +--- + +# TiDB Prisma Integration + +Comprehensive Prisma ORM setup for TiDB with guided workflows. + +## When to use this skill + +- Set up Prisma in a new Node.js/TypeScript project +- Add Prisma to an existing project +- Define/modify models and run migrations +- Configure TiDB Cloud TLS correctly via `DATABASE_URL` +- Troubleshoot Prisma connection or migration issues + +## Prisma vs drivers + +Prisma is an ORM (models + migrations + typed client). If you only need a low-level MySQL driver, use: + +- `tidbx-javascript-mysql2` (promise-native) +- `tidbx-javascript-mysqljs` (callback-based) + +## Available guides + +- `guides/quickstart.md` -- new project: install -> init -> migrate -> run TS quickstart +- `guides/troubleshooting.md` -- common TLS/connection/client-generation issues + +I'll pick the smallest guide that matches your request, then use templates/scripts as needed. + +## Quick examples + +- "Add Prisma to my existing Node API and connect to TiDB Cloud" -> follow quickstart, then integrate client +- "My TiDB Cloud public endpoint fails with TLS errors" -> fix `DATABASE_URL` TLS params +- "Create tables from my Prisma models" -> run `prisma migrate dev` + +## Reference: datasource config + +In `prisma/schema.prisma`, use the MySQL connector and read `DATABASE_URL`: + +```prisma +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} +``` + +## TLS notes for TiDB Cloud public endpoints + +For TiDB Cloud public endpoints, configure TLS via query params in `DATABASE_URL`: + +- Serverless/Starter/Essential (public endpoint): append `?sslaccept=strict` +- Dedicated (public endpoint with downloaded CA): append `?sslaccept=strict&sslcert=/absolute/path/to/ca.pem` + +Examples: + +```bash +DATABASE_URL='mysql://USER:PASSWORD@HOST:4000/DATABASE?sslaccept=strict' +DATABASE_URL='mysql://USER:PASSWORD@HOST:4000/DATABASE?sslaccept=strict&sslcert=/absolute/path/to/ca.pem' +``` + +## Install (TypeScript) + +```bash +npm i @prisma/client +npm i -D prisma typescript ts-node @types/node +``` + +## Core patterns + +### Create client and disconnect (always) + +```ts +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() +try { + // ... queries ... +} finally { + await prisma.$disconnect() +} +``` + +### Safe raw SQL (parameterized) + +- Prefer Prisma query builder when possible. +- When you need raw SQL, prefer `$queryRaw` with template literals (parameterized). +- Avoid `$queryRawUnsafe` unless you fully control the SQL. + +```ts +const rows = await prisma.$queryRaw`SELECT VERSION() AS version` +``` + +## Templates & scripts + +- `templates/schema.prisma` -- example schema (Player/Profile) mapped to TiDB tables +- `templates/quickstart.ts` -- minimal Prisma app: connect -> version -> CRUD (run with `ts-node`) +- `scripts/validate_connection.ts` -- connects and prints `SELECT VERSION()` (run with `ts-node`) + +## Related skills + +- `tidbx-nextjs` -- Next.js App Router integration patterns (route handlers, runtimes, deployment) +- `tidbx-kysely` -- typed query builder alternative to Prisma +- `tidbx-javascript-mysql2` -- low-level driver (promise API) +- `tidbx-javascript-mysqljs` -- low-level driver (callback API) + +--- + +## Workflow + +I will: +1. Confirm your runtime (Node) and project language (TypeScript) +2. Confirm your TiDB deployment (TiDB Cloud vs self-managed) and endpoint type +3. Choose a guide (`guides/quickstart.md` or `guides/troubleshooting.md`) +4. Generate the minimal Prisma files/commands to get you unblocked diff --git a/skills/tidbx-prisma/guides/quickstart.md b/skills/tidbx-prisma/guides/quickstart.md new file mode 100644 index 0000000..1d3dda6 --- /dev/null +++ b/skills/tidbx-prisma/guides/quickstart.md @@ -0,0 +1,52 @@ +# Prisma + TiDB quickstart (TypeScript) + +Goal: connect to TiDB via Prisma, run a migration, then do basic CRUD. + +## Phase 1: Install deps + +```bash +npm i @prisma/client +npm i -D prisma typescript ts-node @types/node +``` + +## Phase 2: Initialize Prisma + +```bash +npx prisma init +``` + +Ensure `prisma/schema.prisma` uses: + +- `datasource db { provider = "mysql" }` +- `url = env("DATABASE_URL")` + +## Phase 3: Set DATABASE_URL (TLS when needed) + +Use the TiDB Cloud "Connect" dialog to copy a connection string, then set: + +- TiDB Cloud public endpoint: add `sslaccept=strict` +- TiDB Cloud public endpoint with downloaded CA: add `sslaccept=strict&sslcert=/absolute/path/to/ca.pem` + +Examples: + +```bash +DATABASE_URL='mysql://USER:PASSWORD@HOST:4000/DATABASE?sslaccept=strict' +DATABASE_URL='mysql://USER:PASSWORD@HOST:4000/DATABASE?sslaccept=strict&sslcert=/absolute/path/to/ca.pem' +``` + +## Phase 4: Define models + +Copy `templates/schema.prisma` into `prisma/schema.prisma`, or define your own models. + +## Phase 5: Run migrations (create tables) + +```bash +npx prisma migrate dev +``` + +## Phase 6: Run the TypeScript quickstart + +```bash +npx ts-node templates/quickstart.ts +``` + diff --git a/skills/tidbx-prisma/guides/troubleshooting.md b/skills/tidbx-prisma/guides/troubleshooting.md new file mode 100644 index 0000000..0644aab --- /dev/null +++ b/skills/tidbx-prisma/guides/troubleshooting.md @@ -0,0 +1,18 @@ +# Troubleshooting (Prisma + TiDB) + +## Cannot connect / timeout + +- Confirm endpoint type (public vs private/VPC) matches where the app runs. +- Confirm host/port/user/password are correct in `DATABASE_URL`. +- If using TiDB Cloud Dedicated, ensure traffic filters allow your client IP. + +## TLS errors (TiDB Cloud public endpoint) + +- Ensure `DATABASE_URL` includes `sslaccept=strict`. +- If using a downloaded CA, ensure `sslcert=/absolute/path/to/ca.pem` points to a readable file. + +## Prisma Client errors + +- If you see "Cannot find module '@prisma/client'": run `npm i @prisma/client`. +- If you see client generation errors: run `npx prisma generate` (or `npx prisma migrate dev` which also generates). + diff --git a/skills/tidbx-prisma/scripts/validate_connection.ts b/skills/tidbx-prisma/scripts/validate_connection.ts new file mode 100644 index 0000000..999de78 --- /dev/null +++ b/skills/tidbx-prisma/scripts/validate_connection.ts @@ -0,0 +1,27 @@ +// Prisma ORM TiDB connection smoke test (TypeScript). +// +// Prereqs: +// - DATABASE_URL is set +// - Prisma Client is generated (run: npx prisma generate OR npx prisma migrate dev) +// +// Run: +// npx ts-node scripts/validate_connection.ts + +import { PrismaClient } from '@prisma/client' + +async function main(): Promise { + const prisma = new PrismaClient() + try { + const rows = await prisma.$queryRaw<{ version: string }[]>`SELECT VERSION() AS version` + console.log(`Connected. VERSION() = ${rows[0]?.version ?? 'unknown'}`) + } finally { + await prisma.$disconnect() + } +} + +main().catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + console.error(`Failed to connect: ${message}`) + process.exitCode = 1 +}) + diff --git a/skills/tidbx-prisma/templates/quickstart.ts b/skills/tidbx-prisma/templates/quickstart.ts new file mode 100644 index 0000000..c478b75 --- /dev/null +++ b/skills/tidbx-prisma/templates/quickstart.ts @@ -0,0 +1,52 @@ +// Minimal Prisma ORM quickstart for TiDB (TypeScript). +// +// Setup: +// 1) npm i @prisma/client && npm i -D prisma typescript ts-node @types/node +// 2) npx prisma init +// 3) Set DATABASE_URL (see SKILL.md for TLS params) +// 4) Put a schema in prisma/schema.prisma (you can copy templates/schema.prisma) +// 5) npx prisma migrate dev +// +// Run: +// npx ts-node templates/quickstart.ts + +import type { Player } from '@prisma/client' +import { PrismaClient } from '@prisma/client' + +async function getTiDBVersion(prisma: PrismaClient): Promise { + const rows = await prisma.$queryRaw<{ version: string }[]>`SELECT VERSION() AS version` + return rows[0]?.version ?? 'unknown' +} + +async function main(): Promise { + const prisma = new PrismaClient() + try { + const version = await getTiDBVersion(prisma) + console.log(`Connected (VERSION() = ${version})`) + + const created: Player = await prisma.player.create({ + data: { name: 'Alice', coins: 100, goods: 100 }, + }) + console.log(`Inserted player id=${created.id}`) + + const got = await prisma.player.findUnique({ where: { id: created.id } }) + console.log('Read player', got) + + const updated: Player = await prisma.player.update({ + where: { id: created.id }, + data: { coins: { increment: 50 }, goods: { increment: 50 } }, + }) + console.log(`Updated player id=${updated.id}`) + + await prisma.player.delete({ where: { id: created.id } }) + console.log(`Deleted player id=${created.id}`) + } finally { + await prisma.$disconnect() + } +} + +main().catch((err: unknown) => { + console.error(err) + process.exitCode = 1 +}) + diff --git a/skills/tidbx-prisma/templates/schema.prisma b/skills/tidbx-prisma/templates/schema.prisma new file mode 100644 index 0000000..7d4f977 --- /dev/null +++ b/skills/tidbx-prisma/templates/schema.prisma @@ -0,0 +1,29 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model Player { + id Int @id @default(autoincrement()) + name String @unique(map: "uk_player_on_name") @db.VarChar(50) + coins Decimal @default(0) + goods Int @default(0) + createdAt DateTime @default(now()) @map("created_at") + profile Profile? + + @@map("players") +} + +model Profile { + playerId Int @id @map("player_id") + biography String @db.Text + + player Player @relation(fields: [playerId], references: [id], onDelete: Cascade, map: "fk_profile_on_player_id") + + @@map("profiles") +} +