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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Factory that creates a TableSchema from column refs and metadata.
3
+ *
4
+ * Input columns are ColumnRef objects (created via `col()`).
5
+ * defineTable sets their _table and _column identity.
6
+ *
7
+ * Usage:
8
+ * const columns = {
9
+ * id: col({ type: MysqlType.VARCHAR, length: 12, primaryKey: true }),
10
+ * name: col({ type: MysqlType.VARCHAR, length: 100 }),
11
+ * };
12
+ *
13
+ * export const merchant = defineTable('merchant', { columns });
14
+ *
15
+ * export const coupon = defineTable('coupon', {
16
+ * columns: {
17
+ * mer_id: col({ type: MysqlType.VARCHAR, length: 20 }),
18
+ * },
19
+ * foreignKeys: {
20
+ * fk_coupon_merchant: {
21
+ * columns: [columns.mer_id],
22
+ * ref: merchant.columns.id,
23
+ * },
24
+ * },
25
+ * });
26
+ */
27
+
28
+ import { ColumnRef } from './column-ref';
29
+ import type { InternalTableDef, TableSchema, ForeignKey, IndexDef } from './table-def';
30
+
31
+ export interface DefineTableInput {
32
+ columns: Record<string, ColumnRef>;
33
+ primaryKey?: ColumnRef | ColumnRef[];
34
+ foreignKeys?: Record<string, ForeignKey>;
35
+ indexes?: IndexDef[];
36
+ comment?: string;
37
+ }
38
+
39
+ export function defineTable(name: string, input: DefineTableInput): TableSchema {
40
+ const colDefs: Record<string, import('./column-def').ColumnDef> = {};
41
+ const refs: Record<string, ColumnRef> = {};
42
+
43
+ for (const [colName, colRef] of Object.entries(input.columns)) {
44
+ // Set column identity on the ref
45
+ colRef._table = name;
46
+ colRef._column = colName;
47
+
48
+ colDefs[colName] = colRef.def;
49
+ refs[colName] = colRef;
50
+ }
51
+
52
+ const internal: InternalTableDef = {
53
+ name,
54
+ columns: colDefs,
55
+ };
56
+ if (input.primaryKey) internal.primaryKey = input.primaryKey;
57
+ if (input.foreignKeys) internal.foreignKeys = input.foreignKeys;
58
+ if (input.indexes) internal.indexes = input.indexes;
59
+ if (input.comment) internal.comment = input.comment;
60
+
61
+ return { _schema: internal, columns: refs };
62
+ }
@@ -0,0 +1,196 @@
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
+
12
+ import type { ColumnDef } from './column-def';
13
+ import { columnSqlType } from './column-def';
14
+ import type { TableSchema, ColumnRef } from './table-def';
15
+
16
+ // ── Internal helpers ──
17
+
18
+ interface ColumnMeta {
19
+ name: string;
20
+ def: ColumnDef;
21
+ }
22
+
23
+ /** Resolve a ColumnRef or lazy ColumnRef getter */
24
+ function resolveRef(ref: ColumnRef | (() => ColumnRef)): ColumnRef {
25
+ return typeof ref === 'function' ? ref() : ref;
26
+ }
27
+
28
+ /** Resolve column name from a ColumnRef */
29
+ function colName(ref: ColumnRef): string {
30
+ return ref._column || '?';
31
+ }
32
+
33
+ /** Resolve multiple column refs to names */
34
+ function colNames(refs: ColumnRef | ColumnRef[]): string[] {
35
+ return Array.isArray(refs) ? refs.map(colName) : [colName(refs)];
36
+ }
37
+
38
+ /** Safe access to `.default` — only some variants have it */
39
+ function getDefault(def: ColumnDef): string | number | undefined {
40
+ if ('default' in def) return def.default as string | number | undefined;
41
+ return undefined;
42
+ }
43
+
44
+ function isIntColumn(def: ColumnDef): def is IntColumn {
45
+ const t = def.type;
46
+ return t === 'TINYINT' || t === 'SMALLINT' || t === 'INT' || t === 'BIGINT';
47
+ }
48
+
49
+ function renderDefault(def: ColumnDef): string {
50
+ const val = getDefault(def);
51
+ if (val === undefined) return '';
52
+
53
+ if (val === 'CURRENT_TIMESTAMP') return ' DEFAULT CURRENT_TIMESTAMP';
54
+
55
+ if (isIntColumn(def)) {
56
+ return ` DEFAULT ${val}`;
57
+ }
58
+
59
+ return ` DEFAULT '${val}'`;
60
+ }
61
+
62
+ function renderNullable(def: ColumnDef): string {
63
+ if ('autoIncrement' in def && def.autoIncrement) return ' NOT NULL';
64
+ return def.nullable === true ? '' : ' NOT NULL';
65
+ }
66
+
67
+ function renderExtra(def: ColumnDef): string {
68
+ if ('autoIncrement' in def && def.autoIncrement) return ' AUTO_INCREMENT';
69
+ return '';
70
+ }
71
+
72
+ type IntColumn = import('./column-def').IntColumn;
73
+
74
+ function renderComment(def: ColumnDef): string {
75
+ return def.comment ? ` COMMENT '${def.comment.replace(/'/g, "\\'")}'` : '';
76
+ }
77
+
78
+ function renderColumnLine(col: ColumnMeta): string {
79
+ const { name, def } = col;
80
+ const sqlType = columnSqlType(def);
81
+ const nullable = renderNullable(def);
82
+ const default_ = renderDefault(def);
83
+ const extra = renderExtra(def);
84
+ const comment = renderComment(def);
85
+ return ` \`${name}\` ${sqlType}${nullable}${default_}${extra}${comment}`;
86
+ }
87
+
88
+ function renderPrimaryKey(schema: { _schema: InternalTableDef }, columns: ColumnMeta[]): string | null {
89
+ const pk = schema._schema.primaryKey;
90
+
91
+ if (pk) {
92
+ const names = Array.isArray(pk) ? pk.map(colName) : [colName(pk)];
93
+ return ` PRIMARY KEY (\`${names.join('`, `')}\`)`;
94
+ }
95
+
96
+ // Auto-detect autoIncrement column
97
+ const autoIncCol = columns.find(c => 'autoIncrement' in c.def && c.def.autoIncrement);
98
+ if (autoIncCol) {
99
+ return ` PRIMARY KEY (\`${autoIncCol.name}\`)`;
100
+ }
101
+
102
+ // Check for column-level primaryKey: true
103
+ const pkCols = columns.filter(c => 'primaryKey' in c.def && c.def.primaryKey);
104
+ if (pkCols.length > 0) {
105
+ return ` PRIMARY KEY (\`${pkCols.map(c => c.name).join('`, `')}\`)`;
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ interface InternalTableDef {
112
+ name: string;
113
+ primaryKey?: ColumnRef | ColumnRef[];
114
+ indexes?: import('./table-def').IndexDef[];
115
+ foreignKeys?: Record<string, import('./table-def').ForeignKey>;
116
+ comment?: string;
117
+ }
118
+
119
+ function renderInlineConstraints(schema: { _schema: InternalTableDef }, columns: ColumnMeta[]): string[] {
120
+ const lines: string[] = [];
121
+
122
+ // UNIQUE constraints declared inline on columns
123
+ for (const col of columns) {
124
+ if ('unique' in col.def && col.def.unique) {
125
+ lines.push(` UNIQUE KEY \`uk_${schema._schema.name}_${col.name}\` (\`${col.name}\`)`);
126
+ }
127
+ }
128
+
129
+ // INDEX constraints declared inline on columns
130
+ for (const col of columns) {
131
+ if ('index' in col.def && col.def.index) {
132
+ lines.push(` KEY \`idx_${schema._schema.name}_${col.name}\` (\`${col.name}\`)`);
133
+ }
134
+ }
135
+
136
+ // Explicit indexes from table.indexes
137
+ const indexes = schema._schema.indexes;
138
+ if (indexes) {
139
+ for (const idx of indexes) {
140
+ const cols = idx.columns.map(c => `\`${colName(c)}\``).join(', ');
141
+ const name = idx.name || `idx_${idx.columns.map(c => colName(c)).join('_')}`;
142
+ const keyword = idx.unique ? 'UNIQUE KEY' : 'KEY';
143
+ lines.push(` ${keyword} \`${name}\` (${cols})`);
144
+ }
145
+ }
146
+
147
+ return lines;
148
+ }
149
+
150
+ // ── Public API ──
151
+
152
+ /**
153
+ * Generate DDL for multiple tables.
154
+ * Returns a single SQL string with CREATE TABLE statements separated by blank lines.
155
+ */
156
+ export function generateDdl(tables: TableSchema[]): string {
157
+ const parts: string[] = [];
158
+
159
+ for (const table of tables) {
160
+ const schema = table._schema;
161
+ const columns: ColumnMeta[] = Object.entries(schema.columns).map(([name, def]) => ({ name, def }));
162
+ const lines: string[] = [];
163
+
164
+ // 1. Column definitions
165
+ for (const col of columns) {
166
+ lines.push(renderColumnLine(col));
167
+ }
168
+
169
+ // 2. Primary key
170
+ const pkLine = renderPrimaryKey(table, columns);
171
+ if (pkLine) lines.push(pkLine);
172
+
173
+ // 3. Constraints + indexes
174
+ const constraintLines = renderInlineConstraints(table, columns);
175
+ lines.push(...constraintLines);
176
+
177
+ // 4. Foreign keys
178
+ if (schema.foreignKeys) {
179
+ for (const [name, fk] of Object.entries(schema.foreignKeys)) {
180
+ const cols = colNames(fk.columns).map(c => `\`${c}\``).join(', ');
181
+ const resolved = resolveRef(fk.ref);
182
+ const onDelete = fk.onDelete ? ` ON DELETE ${fk.onDelete}` : '';
183
+ lines.push(` CONSTRAINT \`${name}\` FOREIGN KEY (${cols}) REFERENCES \`${resolved._table}\`(\`${resolved._column}\`)${onDelete}`);
184
+ }
185
+ }
186
+
187
+ // 5. Join with commas
188
+ const body = lines.join(',\n');
189
+
190
+ const comment = schema.comment ? `-- ${schema.comment}\n` : '';
191
+ const ddl = `${comment}CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n${body}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
192
+ parts.push(ddl);
193
+ }
194
+
195
+ return parts.join('\n\n');
196
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export { MysqlType } from './mysql-type';
2
+ export type { MysqlType as MysqlTypeT } from './mysql-type';
3
+
4
+ export { columnSqlType } from './column-def';
5
+ export type {
6
+ ColumnDef,
7
+ EnumShape,
8
+ IntColumn,
9
+ DecimalColumn,
10
+ StringColumn,
11
+ TextColumn,
12
+ DateColumn,
13
+ DateTimeColumn,
14
+ } from './column-def';
15
+
16
+ export { ColumnRef, col } from './column-ref';
17
+
18
+ export type { ForeignKey, IndexDef, TableSchema } from './table-def';
19
+
20
+ export { defineTable } from './define-table';
21
+ export type { DefineTableInput } from './define-table';
22
+
23
+ export { generateDdl } from './generate-ddl';
24
+
25
+ export { scanTables } from './scan-tables';
26
+
27
+ export { buildSchema } from './build';
28
+ export type { BuildSchemaOptions } from './build';
@@ -0,0 +1,63 @@
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
+
9
+ export const MysqlType = {
10
+ // Integers
11
+ TINYINT: 'TINYINT',
12
+ SMALLINT: 'SMALLINT',
13
+ INT: 'INT',
14
+ BIGINT: 'BIGINT',
15
+
16
+ // Decimals
17
+ DECIMAL: 'DECIMAL',
18
+
19
+ // Strings
20
+ CHAR: 'CHAR',
21
+ VARCHAR: 'VARCHAR',
22
+
23
+ // Text
24
+ TEXT: 'TEXT',
25
+
26
+ // Date / Time
27
+ DATE: 'DATE',
28
+ DATETIME: 'DATETIME',
29
+ TIMESTAMP: 'TIMESTAMP',
30
+ } as const;
31
+
32
+ export type MysqlType = (typeof MysqlType)[keyof typeof MysqlType];
33
+
34
+ /** Integer type family guard */
35
+ export type IntMysqlType =
36
+ | typeof MysqlType.TINYINT
37
+ | typeof MysqlType.SMALLINT
38
+ | typeof MysqlType.INT
39
+ | typeof MysqlType.BIGINT;
40
+
41
+ /** String type family guard */
42
+ export type StringMysqlType =
43
+ | typeof MysqlType.CHAR
44
+ | typeof MysqlType.VARCHAR;
45
+
46
+ /** Date/time type family guard */
47
+ export type DateTimeMysqlType =
48
+ | typeof MysqlType.DATE
49
+ | typeof MysqlType.DATETIME
50
+ | typeof MysqlType.TIMESTAMP;
51
+
52
+ export function isIntType(t: MysqlType): t is IntMysqlType {
53
+ return t === MysqlType.TINYINT || t === MysqlType.SMALLINT
54
+ || t === MysqlType.INT || t === MysqlType.BIGINT;
55
+ }
56
+
57
+ export function isStringType(t: MysqlType): t is StringMysqlType {
58
+ return t === MysqlType.CHAR || t === MysqlType.VARCHAR;
59
+ }
60
+
61
+ export function isDateTimeType(t: MysqlType): t is DateTimeMysqlType {
62
+ return t === MysqlType.DATE || t === MysqlType.DATETIME || t === MysqlType.TIMESTAMP;
63
+ }
@@ -0,0 +1,81 @@
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
+
18
+ import { readdirSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { pathToFileURL } from 'node:url';
21
+ import { defineTable } from './define-table';
22
+ import { ColumnRef } from './column-ref';
23
+ import type { ForeignKey, IndexDef, TableSchema } from './table-def';
24
+
25
+ /** Module shape expected from each *.table.ts file */
26
+ interface TableModule {
27
+ columns: Record<string, ColumnRef>;
28
+ primaryKey?: ColumnRef | ColumnRef[];
29
+ foreignKeys?: Record<string, ForeignKey>;
30
+ indexes?: IndexDef[];
31
+ comment?: string;
32
+ }
33
+
34
+ /**
35
+ * Scan a directory for *.table.ts files, import each, and return TableSchema[].
36
+ * Table name is derived from filename: "order.table.ts" → "order".
37
+ */
38
+ export async function scanTables(dir: string): Promise<TableSchema[]> {
39
+ const absDir = path.resolve(dir);
40
+
41
+ // 1. Discover files
42
+ const files = readdirSync(absDir)
43
+ .filter((f: string) => f.endsWith('.table.ts'))
44
+ .sort(); // deterministic order
45
+
46
+ if (files.length === 0) {
47
+ throw new Error(`No *.table.ts files found in ${absDir}`);
48
+ }
49
+
50
+ // 2. Import all modules
51
+ const entries: { name: string; mod: TableModule }[] = [];
52
+
53
+ for (const file of files) {
54
+ const modulePath = path.join(absDir, file);
55
+ const tableName = file.replace(/\.table\.ts$/, '');
56
+ const mod: TableModule = await import(pathToFileURL(modulePath).href);
57
+
58
+ if (!mod.columns) {
59
+ console.warn(`Skipping ${file}: no 'columns' export`);
60
+ continue;
61
+ }
62
+
63
+ entries.push({ name: tableName, mod });
64
+ }
65
+
66
+ // 3. Phase 1 — set column identity on all refs
67
+ for (const { name, mod } of entries) {
68
+ for (const [colName, ref] of Object.entries(mod.columns)) {
69
+ ref._table = name;
70
+ ref._column = colName;
71
+ }
72
+ }
73
+
74
+ // 4. Phase 2 — build full schemas
75
+ const schemas: TableSchema[] = [];
76
+ for (const { name, mod } of entries) {
77
+ schemas.push(defineTable(name, mod));
78
+ }
79
+
80
+ return schemas;
81
+ }
@@ -0,0 +1,50 @@
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
+
9
+ import type { ColumnDef } from './column-def';
10
+ import { ColumnRef } from './column-ref';
11
+
12
+ export { ColumnRef, col } from './column-ref';
13
+
14
+ // ── Index ──
15
+
16
+ export interface IndexDef {
17
+ columns: ColumnRef[];
18
+ name?: string;
19
+ unique?: boolean;
20
+ }
21
+
22
+ // ── Foreign key ──
23
+
24
+ export interface ForeignKey {
25
+ /** Local column(s) */
26
+ columns: ColumnRef | ColumnRef[];
27
+ /** Referenced column — direct ref or lazy for circular deps */
28
+ ref: ColumnRef | (() => ColumnRef);
29
+ onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
30
+ }
31
+
32
+ // ── Table schema (returned by defineTable) ──
33
+
34
+ export interface TableSchema {
35
+ /** Internal table definition for DDL generation */
36
+ readonly _schema: InternalTableDef;
37
+ /** Column references for FK/index declarations, e.g. merchant.columns.id */
38
+ readonly columns: Record<string, ColumnRef>;
39
+ }
40
+
41
+ /** @internal — not exported directly */
42
+ export interface InternalTableDef {
43
+ name: string;
44
+ /** Raw column definitions keyed by column name */
45
+ columns: Record<string, ColumnDef>;
46
+ primaryKey?: ColumnRef | ColumnRef[];
47
+ foreignKeys?: Record<string, ForeignKey>;
48
+ indexes?: IndexDef[];
49
+ comment?: string;
50
+ }