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,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
|
+
}
|
package/src/table-def.ts
ADDED
|
@@ -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
|
+
}
|