ts-mysql-ddl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +323 -0
- package/bin/ts-mysql-ddl.cjs +16 -0
- package/dist/build.d.ts +19 -0
- package/dist/build.js +53 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +29 -0
- package/dist/column-def.d.ts +51 -0
- package/dist/column-def.js +29 -0
- package/dist/column-ref.d.ts +30 -0
- package/dist/column-ref.js +35 -0
- package/dist/define-table.d.ts +36 -0
- package/dist/define-table.js +50 -0
- package/dist/generate-ddl.d.ts +16 -0
- package/dist/generate-ddl.js +149 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -0
- package/dist/mysql-type.d.ts +30 -0
- package/dist/mysql-type.js +35 -0
- package/dist/scan-tables.d.ts +22 -0
- package/dist/scan-tables.js +59 -0
- package/dist/table-def.d.ts +38 -0
- package/dist/table-def.js +8 -0
- package/package.json +30 -0
- package/src/build.ts +65 -0
- package/src/cli.ts +29 -0
- package/src/column-def.ts +112 -0
- package/src/column-ref.ts +42 -0
- package/src/define-table.ts +62 -0
- package/src/generate-ddl.ts +196 -0
- package/src/index.ts +28 -0
- package/src/mysql-type.ts +63 -0
- package/src/scan-tables.ts +81 -0
- package/src/table-def.ts +50 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate MySQL DDL from TableSchema array.
|
|
3
|
+
*
|
|
4
|
+
* Output format matches the style of 001_init.sql:
|
|
5
|
+
* - Full CREATE TABLE with IF NOT EXISTS
|
|
6
|
+
* - Backtick-quoted identifiers
|
|
7
|
+
* - Consistent indentation (2 spaces)
|
|
8
|
+
* - Constraints after column definitions
|
|
9
|
+
* - ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
10
|
+
*/
|
|
11
|
+
import type { TableSchema } from './table-def';
|
|
12
|
+
/**
|
|
13
|
+
* Generate DDL for multiple tables.
|
|
14
|
+
* Returns a single SQL string with CREATE TABLE statements separated by blank lines.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateDdl(tables: TableSchema[]): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate MySQL DDL from TableSchema array.
|
|
3
|
+
*
|
|
4
|
+
* Output format matches the style of 001_init.sql:
|
|
5
|
+
* - Full CREATE TABLE with IF NOT EXISTS
|
|
6
|
+
* - Backtick-quoted identifiers
|
|
7
|
+
* - Consistent indentation (2 spaces)
|
|
8
|
+
* - Constraints after column definitions
|
|
9
|
+
* - ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
10
|
+
*/
|
|
11
|
+
import { columnSqlType } from './column-def.js';
|
|
12
|
+
/** Resolve a ColumnRef or lazy ColumnRef getter */
|
|
13
|
+
function resolveRef(ref) {
|
|
14
|
+
return typeof ref === 'function' ? ref() : ref;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve column name from a ColumnRef */
|
|
17
|
+
function colName(ref) {
|
|
18
|
+
return ref._column || '?';
|
|
19
|
+
}
|
|
20
|
+
/** Resolve multiple column refs to names */
|
|
21
|
+
function colNames(refs) {
|
|
22
|
+
return Array.isArray(refs) ? refs.map(colName) : [colName(refs)];
|
|
23
|
+
}
|
|
24
|
+
/** Safe access to `.default` — only some variants have it */
|
|
25
|
+
function getDefault(def) {
|
|
26
|
+
if ('default' in def)
|
|
27
|
+
return def.default;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function isIntColumn(def) {
|
|
31
|
+
const t = def.type;
|
|
32
|
+
return t === 'TINYINT' || t === 'SMALLINT' || t === 'INT' || t === 'BIGINT';
|
|
33
|
+
}
|
|
34
|
+
function renderDefault(def) {
|
|
35
|
+
const val = getDefault(def);
|
|
36
|
+
if (val === undefined)
|
|
37
|
+
return '';
|
|
38
|
+
if (val === 'CURRENT_TIMESTAMP')
|
|
39
|
+
return ' DEFAULT CURRENT_TIMESTAMP';
|
|
40
|
+
if (isIntColumn(def)) {
|
|
41
|
+
return ` DEFAULT ${val}`;
|
|
42
|
+
}
|
|
43
|
+
return ` DEFAULT '${val}'`;
|
|
44
|
+
}
|
|
45
|
+
function renderNullable(def) {
|
|
46
|
+
if ('autoIncrement' in def && def.autoIncrement)
|
|
47
|
+
return ' NOT NULL';
|
|
48
|
+
return def.nullable === true ? '' : ' NOT NULL';
|
|
49
|
+
}
|
|
50
|
+
function renderExtra(def) {
|
|
51
|
+
if ('autoIncrement' in def && def.autoIncrement)
|
|
52
|
+
return ' AUTO_INCREMENT';
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
function renderComment(def) {
|
|
56
|
+
return def.comment ? ` COMMENT '${def.comment.replace(/'/g, "\\'")}'` : '';
|
|
57
|
+
}
|
|
58
|
+
function renderColumnLine(col) {
|
|
59
|
+
const { name, def } = col;
|
|
60
|
+
const sqlType = columnSqlType(def);
|
|
61
|
+
const nullable = renderNullable(def);
|
|
62
|
+
const default_ = renderDefault(def);
|
|
63
|
+
const extra = renderExtra(def);
|
|
64
|
+
const comment = renderComment(def);
|
|
65
|
+
return ` \`${name}\` ${sqlType}${nullable}${default_}${extra}${comment}`;
|
|
66
|
+
}
|
|
67
|
+
function renderPrimaryKey(schema, columns) {
|
|
68
|
+
const pk = schema._schema.primaryKey;
|
|
69
|
+
if (pk) {
|
|
70
|
+
const names = Array.isArray(pk) ? pk.map(colName) : [colName(pk)];
|
|
71
|
+
return ` PRIMARY KEY (\`${names.join('`, `')}\`)`;
|
|
72
|
+
}
|
|
73
|
+
// Auto-detect autoIncrement column
|
|
74
|
+
const autoIncCol = columns.find(c => 'autoIncrement' in c.def && c.def.autoIncrement);
|
|
75
|
+
if (autoIncCol) {
|
|
76
|
+
return ` PRIMARY KEY (\`${autoIncCol.name}\`)`;
|
|
77
|
+
}
|
|
78
|
+
// Check for column-level primaryKey: true
|
|
79
|
+
const pkCols = columns.filter(c => 'primaryKey' in c.def && c.def.primaryKey);
|
|
80
|
+
if (pkCols.length > 0) {
|
|
81
|
+
return ` PRIMARY KEY (\`${pkCols.map(c => c.name).join('`, `')}\`)`;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function renderInlineConstraints(schema, columns) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
// UNIQUE constraints declared inline on columns
|
|
88
|
+
for (const col of columns) {
|
|
89
|
+
if ('unique' in col.def && col.def.unique) {
|
|
90
|
+
lines.push(` UNIQUE KEY \`uk_${schema._schema.name}_${col.name}\` (\`${col.name}\`)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// INDEX constraints declared inline on columns
|
|
94
|
+
for (const col of columns) {
|
|
95
|
+
if ('index' in col.def && col.def.index) {
|
|
96
|
+
lines.push(` KEY \`idx_${schema._schema.name}_${col.name}\` (\`${col.name}\`)`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Explicit indexes from table.indexes
|
|
100
|
+
const indexes = schema._schema.indexes;
|
|
101
|
+
if (indexes) {
|
|
102
|
+
for (const idx of indexes) {
|
|
103
|
+
const cols = idx.columns.map(c => `\`${colName(c)}\``).join(', ');
|
|
104
|
+
const name = idx.name || `idx_${idx.columns.map(c => colName(c)).join('_')}`;
|
|
105
|
+
const keyword = idx.unique ? 'UNIQUE KEY' : 'KEY';
|
|
106
|
+
lines.push(` ${keyword} \`${name}\` (${cols})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
111
|
+
// ── Public API ──
|
|
112
|
+
/**
|
|
113
|
+
* Generate DDL for multiple tables.
|
|
114
|
+
* Returns a single SQL string with CREATE TABLE statements separated by blank lines.
|
|
115
|
+
*/
|
|
116
|
+
export function generateDdl(tables) {
|
|
117
|
+
const parts = [];
|
|
118
|
+
for (const table of tables) {
|
|
119
|
+
const schema = table._schema;
|
|
120
|
+
const columns = Object.entries(schema.columns).map(([name, def]) => ({ name, def }));
|
|
121
|
+
const lines = [];
|
|
122
|
+
// 1. Column definitions
|
|
123
|
+
for (const col of columns) {
|
|
124
|
+
lines.push(renderColumnLine(col));
|
|
125
|
+
}
|
|
126
|
+
// 2. Primary key
|
|
127
|
+
const pkLine = renderPrimaryKey(table, columns);
|
|
128
|
+
if (pkLine)
|
|
129
|
+
lines.push(pkLine);
|
|
130
|
+
// 3. Constraints + indexes
|
|
131
|
+
const constraintLines = renderInlineConstraints(table, columns);
|
|
132
|
+
lines.push(...constraintLines);
|
|
133
|
+
// 4. Foreign keys
|
|
134
|
+
if (schema.foreignKeys) {
|
|
135
|
+
for (const [name, fk] of Object.entries(schema.foreignKeys)) {
|
|
136
|
+
const cols = colNames(fk.columns).map(c => `\`${c}\``).join(', ');
|
|
137
|
+
const resolved = resolveRef(fk.ref);
|
|
138
|
+
const onDelete = fk.onDelete ? ` ON DELETE ${fk.onDelete}` : '';
|
|
139
|
+
lines.push(` CONSTRAINT \`${name}\` FOREIGN KEY (${cols}) REFERENCES \`${resolved._table}\`(\`${resolved._column}\`)${onDelete}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 5. Join with commas
|
|
143
|
+
const body = lines.join(',\n');
|
|
144
|
+
const comment = schema.comment ? `-- ${schema.comment}\n` : '';
|
|
145
|
+
const ddl = `${comment}CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n${body}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
|
|
146
|
+
parts.push(ddl);
|
|
147
|
+
}
|
|
148
|
+
return parts.join('\n\n');
|
|
149
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { MysqlType } from './mysql-type';
|
|
2
|
+
export type { MysqlType as MysqlTypeT } from './mysql-type';
|
|
3
|
+
export { columnSqlType } from './column-def';
|
|
4
|
+
export type { ColumnDef, EnumShape, IntColumn, DecimalColumn, StringColumn, TextColumn, DateColumn, DateTimeColumn, } from './column-def';
|
|
5
|
+
export { ColumnRef, col } from './column-ref';
|
|
6
|
+
export type { ForeignKey, IndexDef, TableSchema } from './table-def';
|
|
7
|
+
export { defineTable } from './define-table';
|
|
8
|
+
export type { DefineTableInput } from './define-table';
|
|
9
|
+
export { generateDdl } from './generate-ddl';
|
|
10
|
+
export { scanTables } from './scan-tables';
|
|
11
|
+
export { buildSchema } from './build';
|
|
12
|
+
export type { BuildSchemaOptions } from './build';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MysqlType } from './mysql-type.js';
|
|
2
|
+
export { columnSqlType } from './column-def.js';
|
|
3
|
+
export { ColumnRef, col } from './column-ref.js';
|
|
4
|
+
export { defineTable } from './define-table.js';
|
|
5
|
+
export { generateDdl } from './generate-ddl.js';
|
|
6
|
+
export { scanTables } from './scan-tables.js';
|
|
7
|
+
export { buildSchema } from './build.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL data type constants and type guard.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { MysqlType } from '@fastify-api/ts-mysql-ddl';
|
|
6
|
+
* const col = { type: MysqlType.VARCHAR, length: 100 };
|
|
7
|
+
*/
|
|
8
|
+
export declare const MysqlType: {
|
|
9
|
+
readonly TINYINT: "TINYINT";
|
|
10
|
+
readonly SMALLINT: "SMALLINT";
|
|
11
|
+
readonly INT: "INT";
|
|
12
|
+
readonly BIGINT: "BIGINT";
|
|
13
|
+
readonly DECIMAL: "DECIMAL";
|
|
14
|
+
readonly CHAR: "CHAR";
|
|
15
|
+
readonly VARCHAR: "VARCHAR";
|
|
16
|
+
readonly TEXT: "TEXT";
|
|
17
|
+
readonly DATE: "DATE";
|
|
18
|
+
readonly DATETIME: "DATETIME";
|
|
19
|
+
readonly TIMESTAMP: "TIMESTAMP";
|
|
20
|
+
};
|
|
21
|
+
export type MysqlType = (typeof MysqlType)[keyof typeof MysqlType];
|
|
22
|
+
/** Integer type family guard */
|
|
23
|
+
export type IntMysqlType = typeof MysqlType.TINYINT | typeof MysqlType.SMALLINT | typeof MysqlType.INT | typeof MysqlType.BIGINT;
|
|
24
|
+
/** String type family guard */
|
|
25
|
+
export type StringMysqlType = typeof MysqlType.CHAR | typeof MysqlType.VARCHAR;
|
|
26
|
+
/** Date/time type family guard */
|
|
27
|
+
export type DateTimeMysqlType = typeof MysqlType.DATE | typeof MysqlType.DATETIME | typeof MysqlType.TIMESTAMP;
|
|
28
|
+
export declare function isIntType(t: MysqlType): t is IntMysqlType;
|
|
29
|
+
export declare function isStringType(t: MysqlType): t is StringMysqlType;
|
|
30
|
+
export declare function isDateTimeType(t: MysqlType): t is DateTimeMysqlType;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL data type constants and type guard.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { MysqlType } from '@fastify-api/ts-mysql-ddl';
|
|
6
|
+
* const col = { type: MysqlType.VARCHAR, length: 100 };
|
|
7
|
+
*/
|
|
8
|
+
export const MysqlType = {
|
|
9
|
+
// Integers
|
|
10
|
+
TINYINT: 'TINYINT',
|
|
11
|
+
SMALLINT: 'SMALLINT',
|
|
12
|
+
INT: 'INT',
|
|
13
|
+
BIGINT: 'BIGINT',
|
|
14
|
+
// Decimals
|
|
15
|
+
DECIMAL: 'DECIMAL',
|
|
16
|
+
// Strings
|
|
17
|
+
CHAR: 'CHAR',
|
|
18
|
+
VARCHAR: 'VARCHAR',
|
|
19
|
+
// Text
|
|
20
|
+
TEXT: 'TEXT',
|
|
21
|
+
// Date / Time
|
|
22
|
+
DATE: 'DATE',
|
|
23
|
+
DATETIME: 'DATETIME',
|
|
24
|
+
TIMESTAMP: 'TIMESTAMP',
|
|
25
|
+
};
|
|
26
|
+
export function isIntType(t) {
|
|
27
|
+
return t === MysqlType.TINYINT || t === MysqlType.SMALLINT
|
|
28
|
+
|| t === MysqlType.INT || t === MysqlType.BIGINT;
|
|
29
|
+
}
|
|
30
|
+
export function isStringType(t) {
|
|
31
|
+
return t === MysqlType.CHAR || t === MysqlType.VARCHAR;
|
|
32
|
+
}
|
|
33
|
+
export function isDateTimeType(t) {
|
|
34
|
+
return t === MysqlType.DATE || t === MysqlType.DATETIME || t === MysqlType.TIMESTAMP;
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discover table definitions from a directory of *.table.ts files.
|
|
3
|
+
*
|
|
4
|
+
* Each file exports at minimum `columns` (Record<string, ColumnRef>).
|
|
5
|
+
* Optional exports: `indexes`, `foreignKeys`, `primaryKey`, `comment`.
|
|
6
|
+
*
|
|
7
|
+
* Two-phase initialization ensures FK refs between tables resolve correctly
|
|
8
|
+
* regardless of file processing order:
|
|
9
|
+
* Phase 1 — set _table/_column on all ColumnRefs
|
|
10
|
+
* Phase 2 — build full TableSchema with FKs, indexes, etc.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const tables = await scanTables('./schema');
|
|
14
|
+
* const sql = generateDdl(tables);
|
|
15
|
+
* console.log(sql);
|
|
16
|
+
*/
|
|
17
|
+
import type { TableSchema } from './table-def';
|
|
18
|
+
/**
|
|
19
|
+
* Scan a directory for *.table.ts files, import each, and return TableSchema[].
|
|
20
|
+
* Table name is derived from filename: "order.table.ts" → "order".
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanTables(dir: string): Promise<TableSchema[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discover table definitions from a directory of *.table.ts files.
|
|
3
|
+
*
|
|
4
|
+
* Each file exports at minimum `columns` (Record<string, ColumnRef>).
|
|
5
|
+
* Optional exports: `indexes`, `foreignKeys`, `primaryKey`, `comment`.
|
|
6
|
+
*
|
|
7
|
+
* Two-phase initialization ensures FK refs between tables resolve correctly
|
|
8
|
+
* regardless of file processing order:
|
|
9
|
+
* Phase 1 — set _table/_column on all ColumnRefs
|
|
10
|
+
* Phase 2 — build full TableSchema with FKs, indexes, etc.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const tables = await scanTables('./schema');
|
|
14
|
+
* const sql = generateDdl(tables);
|
|
15
|
+
* console.log(sql);
|
|
16
|
+
*/
|
|
17
|
+
import { readdirSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { pathToFileURL } from 'node:url';
|
|
20
|
+
import { defineTable } from './define-table.js';
|
|
21
|
+
/**
|
|
22
|
+
* Scan a directory for *.table.ts files, import each, and return TableSchema[].
|
|
23
|
+
* Table name is derived from filename: "order.table.ts" → "order".
|
|
24
|
+
*/
|
|
25
|
+
export async function scanTables(dir) {
|
|
26
|
+
const absDir = path.resolve(dir);
|
|
27
|
+
// 1. Discover files
|
|
28
|
+
const files = readdirSync(absDir)
|
|
29
|
+
.filter((f) => f.endsWith('.table.ts'))
|
|
30
|
+
.sort(); // deterministic order
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
throw new Error(`No *.table.ts files found in ${absDir}`);
|
|
33
|
+
}
|
|
34
|
+
// 2. Import all modules
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const modulePath = path.join(absDir, file);
|
|
38
|
+
const tableName = file.replace(/\.table\.ts$/, '');
|
|
39
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
40
|
+
if (!mod.columns) {
|
|
41
|
+
console.warn(`Skipping ${file}: no 'columns' export`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
entries.push({ name: tableName, mod });
|
|
45
|
+
}
|
|
46
|
+
// 3. Phase 1 — set column identity on all refs
|
|
47
|
+
for (const { name, mod } of entries) {
|
|
48
|
+
for (const [colName, ref] of Object.entries(mod.columns)) {
|
|
49
|
+
ref._table = name;
|
|
50
|
+
ref._column = colName;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 4. Phase 2 — build full schemas
|
|
54
|
+
const schemas = [];
|
|
55
|
+
for (const { name, mod } of entries) {
|
|
56
|
+
schemas.push(defineTable(name, mod));
|
|
57
|
+
}
|
|
58
|
+
return schemas;
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table definition interfaces.
|
|
3
|
+
*
|
|
4
|
+
* Index and FK columns use ColumnRef for compile-time checking:
|
|
5
|
+
* indexes: [{ columns: [columns.status, columns.deleted] }]
|
|
6
|
+
* foreignKeys: { fk_x: { columns: [columns.mer_id], ref: merchant.columns.id } }
|
|
7
|
+
*/
|
|
8
|
+
import type { ColumnDef } from './column-def';
|
|
9
|
+
import { ColumnRef } from './column-ref';
|
|
10
|
+
export { ColumnRef, col } from './column-ref';
|
|
11
|
+
export interface IndexDef {
|
|
12
|
+
columns: ColumnRef[];
|
|
13
|
+
name?: string;
|
|
14
|
+
unique?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ForeignKey {
|
|
17
|
+
/** Local column(s) */
|
|
18
|
+
columns: ColumnRef | ColumnRef[];
|
|
19
|
+
/** Referenced column — direct ref or lazy for circular deps */
|
|
20
|
+
ref: ColumnRef | (() => ColumnRef);
|
|
21
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
22
|
+
}
|
|
23
|
+
export interface TableSchema {
|
|
24
|
+
/** Internal table definition for DDL generation */
|
|
25
|
+
readonly _schema: InternalTableDef;
|
|
26
|
+
/** Column references for FK/index declarations, e.g. merchant.columns.id */
|
|
27
|
+
readonly columns: Record<string, ColumnRef>;
|
|
28
|
+
}
|
|
29
|
+
/** @internal — not exported directly */
|
|
30
|
+
export interface InternalTableDef {
|
|
31
|
+
name: string;
|
|
32
|
+
/** Raw column definitions keyed by column name */
|
|
33
|
+
columns: Record<string, ColumnDef>;
|
|
34
|
+
primaryKey?: ColumnRef | ColumnRef[];
|
|
35
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
36
|
+
indexes?: IndexDef[];
|
|
37
|
+
comment?: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table definition interfaces.
|
|
3
|
+
*
|
|
4
|
+
* Index and FK columns use ColumnRef for compile-time checking:
|
|
5
|
+
* indexes: [{ columns: [columns.status, columns.deleted] }]
|
|
6
|
+
* foreignKeys: { fk_x: { columns: [columns.mer_id], ref: merchant.columns.id } }
|
|
7
|
+
*/
|
|
8
|
+
export { ColumnRef, col } from './column-ref.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-mysql-ddl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript DSL for MySQL schema definition — define tables in TS, generate DDL SQL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ts-mysql-ddl": "bin/ts-mysql-ddl.cjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"bin"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json && node scripts/fix-imports.mjs",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"typecheck": "npx tsc --noEmit",
|
|
20
|
+
"cli": "npx tsx src/cli.ts"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^26.1.1",
|
|
24
|
+
"typescript": "^5.7.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"tsx": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build schema: scan *.table.ts directory → generate DDL → optionally write to file.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const sql = await buildSchema({ schemaDir: './schema' });
|
|
6
|
+
* await buildSchema({ schemaDir: './schema', outFile: './001_init.sql' });
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFileSync } from 'node:fs';
|
|
10
|
+
import { scanTables } from './scan-tables';
|
|
11
|
+
import { generateDdl } from './generate-ddl';
|
|
12
|
+
import type { TableSchema } from './table-def';
|
|
13
|
+
|
|
14
|
+
export interface BuildSchemaOptions {
|
|
15
|
+
/** Directory to scan for *.table.ts files */
|
|
16
|
+
schemaDir: string;
|
|
17
|
+
/** Optional output file path — writes generated SQL if provided */
|
|
18
|
+
outFile?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Topological sort by FK dependency — referenced tables come first */
|
|
22
|
+
function sortByDependency(tables: TableSchema[]): TableSchema[] {
|
|
23
|
+
const byName = new Map(tables.map(t => [t._schema.name, t]));
|
|
24
|
+
const visited = new Set<string>();
|
|
25
|
+
const sorted: TableSchema[] = [];
|
|
26
|
+
|
|
27
|
+
function visit(name: string) {
|
|
28
|
+
if (visited.has(name)) return;
|
|
29
|
+
visited.add(name);
|
|
30
|
+
const table = byName.get(name);
|
|
31
|
+
if (!table) return;
|
|
32
|
+
// Visit FK refs first
|
|
33
|
+
const fks = table._schema.foreignKeys;
|
|
34
|
+
if (fks) {
|
|
35
|
+
for (const fk of Object.values(fks)) {
|
|
36
|
+
const ref = typeof fk.ref === 'function' ? fk.ref() : fk.ref;
|
|
37
|
+
if (ref._table && ref._table !== name) {
|
|
38
|
+
visit(ref._table);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
sorted.push(table);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const t of tables) visit(t._schema.name);
|
|
46
|
+
return sorted;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scan schema directory, resolve FK dependency order, generate DDL.
|
|
51
|
+
* Optionally writes result to `outFile`.
|
|
52
|
+
* Returns the generated SQL string.
|
|
53
|
+
*/
|
|
54
|
+
export async function buildSchema(options: BuildSchemaOptions): Promise<string> {
|
|
55
|
+
const { schemaDir, outFile } = options;
|
|
56
|
+
const tables = sortByDependency(await scanTables(schemaDir));
|
|
57
|
+
const sql = generateDdl(tables);
|
|
58
|
+
|
|
59
|
+
if (outFile) {
|
|
60
|
+
writeFileSync(outFile, sql + '\n', 'utf-8');
|
|
61
|
+
console.log(`Generated ${tables.length} tables → ${outFile}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return sql;
|
|
65
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI for ts-mysql-ddl: scan *.table.ts → generate DDL → write SQL file.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ts-mysql-ddl --src ./schema --dest ./001_init.sql
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { buildSchema } from './build';
|
|
10
|
+
|
|
11
|
+
function parseArgs(): { src: string; dest: string } {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
let src = '';
|
|
14
|
+
let dest = '';
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
if (arg === '--src' && i + 1 < args.length) { src = args[++i]!; }
|
|
19
|
+
if (arg === '--dest' && i + 1 < args.length) { dest = args[++i]!; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!src) throw new Error('Missing required flag: --src <schema_dir>');
|
|
23
|
+
if (!dest) throw new Error('Missing required flag: --dest <output_file>');
|
|
24
|
+
|
|
25
|
+
return { src, dest };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { src, dest } = parseArgs();
|
|
29
|
+
await buildSchema({ schemaDir: src, outFile: dest });
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column definition — discriminated union by MysqlType.
|
|
3
|
+
*
|
|
4
|
+
* Each type family only allows the parameters that make sense for that type.
|
|
5
|
+
* Compile-time errors if you try to use `length` on INT or `autoIncrement` on VARCHAR.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Shared types ──
|
|
9
|
+
|
|
10
|
+
interface BaseColumn {
|
|
11
|
+
nullable?: boolean;
|
|
12
|
+
comment?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Any TypeScript enum value — string or numeric */
|
|
16
|
+
export type EnumShape = Record<string, string | number>;
|
|
17
|
+
|
|
18
|
+
// ── Integer: TINYINT | SMALLINT | INT | BIGINT ──
|
|
19
|
+
|
|
20
|
+
export interface IntColumn extends BaseColumn {
|
|
21
|
+
type: typeof import('./mysql-type').MysqlType.TINYINT
|
|
22
|
+
| typeof import('./mysql-type').MysqlType.SMALLINT
|
|
23
|
+
| typeof import('./mysql-type').MysqlType.INT
|
|
24
|
+
| typeof import('./mysql-type').MysqlType.BIGINT;
|
|
25
|
+
autoIncrement?: true;
|
|
26
|
+
primaryKey?: true;
|
|
27
|
+
default?: number;
|
|
28
|
+
enumRef?: EnumShape; // real TS enum, e.g. CouponStatus
|
|
29
|
+
index?: boolean;
|
|
30
|
+
unique?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Decimal: DECIMAL ──
|
|
34
|
+
|
|
35
|
+
export interface DecimalColumn extends BaseColumn {
|
|
36
|
+
type: typeof import('./mysql-type').MysqlType.DECIMAL;
|
|
37
|
+
precision: number;
|
|
38
|
+
scale: number;
|
|
39
|
+
default?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── String: CHAR | VARCHAR ──
|
|
43
|
+
|
|
44
|
+
export interface StringColumn extends BaseColumn {
|
|
45
|
+
type: typeof import('./mysql-type').MysqlType.CHAR
|
|
46
|
+
| typeof import('./mysql-type').MysqlType.VARCHAR;
|
|
47
|
+
length: number;
|
|
48
|
+
enumRef?: EnumShape; // real TS enum, e.g. CouponStatus
|
|
49
|
+
primaryKey?: true;
|
|
50
|
+
default?: string;
|
|
51
|
+
index?: boolean;
|
|
52
|
+
unique?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Text: TEXT ──
|
|
56
|
+
|
|
57
|
+
export interface TextColumn extends BaseColumn {
|
|
58
|
+
type: typeof import('./mysql-type').MysqlType.TEXT;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Date / DateTime ──
|
|
62
|
+
|
|
63
|
+
export interface DateColumn extends BaseColumn {
|
|
64
|
+
type: typeof import('./mysql-type').MysqlType.DATE;
|
|
65
|
+
default?: 'CURRENT_TIMESTAMP';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DateTimeColumn extends BaseColumn {
|
|
69
|
+
type: typeof import('./mysql-type').MysqlType.DATETIME
|
|
70
|
+
| typeof import('./mysql-type').MysqlType.TIMESTAMP;
|
|
71
|
+
default?: 'CURRENT_TIMESTAMP';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Union ──
|
|
75
|
+
|
|
76
|
+
export type ColumnDef =
|
|
77
|
+
| IntColumn
|
|
78
|
+
| DecimalColumn
|
|
79
|
+
| StringColumn
|
|
80
|
+
| TextColumn
|
|
81
|
+
| DateColumn
|
|
82
|
+
| DateTimeColumn;
|
|
83
|
+
|
|
84
|
+
// ── Helpers ──
|
|
85
|
+
|
|
86
|
+
function isIntType(t: string): boolean {
|
|
87
|
+
return t === 'TINYINT' || t === 'SMALLINT' || t === 'INT' || t === 'BIGINT';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isStringType(t: string): boolean {
|
|
91
|
+
return t === 'CHAR' || t === 'VARCHAR';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get the SQL type name with parameters, e.g. "VARCHAR(100)" or "DECIMAL(10,2)" */
|
|
95
|
+
export function columnSqlType(def: ColumnDef): string {
|
|
96
|
+
const t = def.type;
|
|
97
|
+
|
|
98
|
+
if (isIntType(t)) return t;
|
|
99
|
+
|
|
100
|
+
if (t === 'DECIMAL') {
|
|
101
|
+
const d = def as DecimalColumn;
|
|
102
|
+
return `${t}(${d.precision},${d.scale})`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isStringType(t)) {
|
|
106
|
+
const d = def as StringColumn;
|
|
107
|
+
return `${t}(${d.length})`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// TEXT, DATE, DATETIME, TIMESTAMP — no params
|
|
111
|
+
return t;
|
|
112
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColumnRef — a column definition that also carries its table/column identity.
|
|
3
|
+
*
|
|
4
|
+
* Created via `col()` factory, identity is set by `defineTable()`.
|
|
5
|
+
* `toString()` enables use in template literals:
|
|
6
|
+
* name: `idx_${columns.status}`
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ColumnDef } from './column-def';
|
|
10
|
+
|
|
11
|
+
export class ColumnRef {
|
|
12
|
+
/** Underlying column definition (immutable) */
|
|
13
|
+
readonly def: ColumnDef;
|
|
14
|
+
|
|
15
|
+
/** Table name — set by defineTable() */
|
|
16
|
+
_table: string = '';
|
|
17
|
+
/** Column name — set by defineTable() */
|
|
18
|
+
_column: string = '';
|
|
19
|
+
|
|
20
|
+
constructor(def: ColumnDef) {
|
|
21
|
+
this.def = def;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** For template literals: `idx_${col}` → "idx_status" */
|
|
25
|
+
toString(): string {
|
|
26
|
+
return this._column || '?';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Factory to create a ColumnRef from a ColumnDef.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* const columns = {
|
|
35
|
+
* id: col({ type: MysqlType.INT, autoIncrement: true }),
|
|
36
|
+
* name: col({ type: MysqlType.VARCHAR, length: 50 }),
|
|
37
|
+
* status: col({ type: MysqlType.VARCHAR, length: 20 }),
|
|
38
|
+
* };
|
|
39
|
+
*/
|
|
40
|
+
export function col(def: ColumnDef): ColumnRef {
|
|
41
|
+
return new ColumnRef(def);
|
|
42
|
+
}
|