forj 0.0.3 → 0.0.5
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/package.json +13 -2
- package/src/index.ts +1 -0
- package/src/migrations/blueprint.ts +170 -0
- package/src/migrations/builder.ts +167 -0
- package/src/migrations/column.ts +45 -0
- package/src/migrations/foreign-key.ts +25 -0
- package/src/migrations/index.ts +5 -0
- package/src/migrations/migration.ts +5 -0
- package/src/migrations/migrator.ts +110 -0
- package/src/migrations/schema.ts +151 -0
- package/src/migrations/types.ts +64 -0
- package/src/model.ts +1 -1
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forj",
|
|
3
3
|
"description": "SQLite ORM and Query Builder whitout dependencies",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"files": ["src"],
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
10
|
"./d1": "./src/d1/index.ts",
|
|
11
|
-
"./d1/types": "./src/d1/types.ts"
|
|
11
|
+
"./d1/types": "./src/d1/types.ts",
|
|
12
|
+
"./dynamodb": "./src/dynamodb/index.ts",
|
|
13
|
+
"./dynamodb/types": "./src/dynamodb/types.ts",
|
|
14
|
+
"./migrate": "./src/migrations/index.ts"
|
|
12
15
|
},
|
|
13
16
|
"-exports": {
|
|
14
17
|
".": {
|
|
@@ -59,6 +62,8 @@
|
|
|
59
62
|
"resources",
|
|
60
63
|
"support",
|
|
61
64
|
"sqlite",
|
|
65
|
+
"migration",
|
|
66
|
+
"schema",
|
|
62
67
|
"orm",
|
|
63
68
|
"query",
|
|
64
69
|
"builder",
|
|
@@ -71,9 +76,15 @@
|
|
|
71
76
|
"postgres",
|
|
72
77
|
"postgresql",
|
|
73
78
|
"typescript",
|
|
79
|
+
"serverless",
|
|
80
|
+
"dynamo",
|
|
81
|
+
"dynamodb",
|
|
74
82
|
"aws",
|
|
75
83
|
"lambda",
|
|
76
84
|
"llrt",
|
|
85
|
+
"d1",
|
|
86
|
+
"cloudflare",
|
|
87
|
+
"workers",
|
|
77
88
|
"deno",
|
|
78
89
|
"bun",
|
|
79
90
|
"nodejs"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import Column from './column'
|
|
2
|
+
import ForeignKey from './foreign-key'
|
|
3
|
+
import type {
|
|
4
|
+
ColumnDefinition, IndexDefinition, ForeignKeyDefinition,
|
|
5
|
+
} from './types'
|
|
6
|
+
|
|
7
|
+
export class Blueprint {
|
|
8
|
+
#table: string
|
|
9
|
+
#columns: ColumnDefinition[] = []
|
|
10
|
+
#indexes: IndexDefinition[] = []
|
|
11
|
+
#foreignKeys: ForeignKeyDefinition[] = []
|
|
12
|
+
#dropColumns: string[] = []
|
|
13
|
+
#renameColumns: Map<string, string> = new Map()
|
|
14
|
+
|
|
15
|
+
constructor(table: string) {
|
|
16
|
+
this.#table = table
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#column(definition: ColumnDefinition) {
|
|
20
|
+
const column: ColumnDefinition = definition
|
|
21
|
+
this.#columns.push(column)
|
|
22
|
+
return new Column(column)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
id(name: string = 'id') { // Auto-increment ID (bigint unsigned)
|
|
26
|
+
return this.#column({ name, type: 'BIGINT', unsigned: true, autoIncrement: true, primary: true, nullable: false })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
string(name: string, length: number = 255) {
|
|
30
|
+
return this.#column({ name, type: 'VARCHAR', length, nullable: false })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
text(name: string) {
|
|
34
|
+
return this.#column({ name, type: 'TEXT', nullable: false })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
integer(name: string) {
|
|
38
|
+
return this.#column({ name, type: 'INTEGER', nullable: false })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
bigInteger(name: string) {
|
|
42
|
+
return this.#column({ name, type: 'BIGINT', nullable: false })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tinyInteger(name: string) {
|
|
46
|
+
return this.#column({ name, type: 'TINYINT', nullable: false })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
boolean(name: string) {
|
|
50
|
+
return this.#column({ name, type: 'BOOLEAN', nullable: false })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
decimal(name: string, precision: number = 8, scale: number = 2) {
|
|
54
|
+
return this.#column({ name, type: `DECIMAL(${precision},${scale})`, nullable: false })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
float(name: string) {
|
|
58
|
+
return this.#column({ name, type: 'FLOAT', nullable: false })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
double(name: string) {
|
|
62
|
+
return this.#column({ name, type: 'DOUBLE', nullable: false })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
date(name: string) {
|
|
66
|
+
return this.#column({ name, type: 'DATE', nullable: false })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dateTime(name: string) {
|
|
70
|
+
return this.#column({ name, type: 'DATETIME', nullable: false })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
timestamp(name: string) {
|
|
74
|
+
return this.#column({ name, type: 'TIMESTAMP', nullable: false })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
time(name: string) {
|
|
78
|
+
return this.#column({ name, type: 'TIME', nullable: false })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
json(name: string) {
|
|
82
|
+
return this.#column({ name, type: 'JSON', nullable: false })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
enum(name: string, values: string[]) {
|
|
86
|
+
return this.#column({ name, type: `ENUM(${values.map(v => `'${v}'`).join(', ')})`, nullable: false })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
timestamps() {
|
|
90
|
+
this.timestamp('created_at')
|
|
91
|
+
this.timestamp('updated_at')
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
softDelete(name: string = 'deleted_at') {
|
|
96
|
+
this.timestamp(name).nullable()
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
softDeletes(name: string = 'deleted_at') {
|
|
101
|
+
return this.softDelete(name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
foreign(column: string) {
|
|
105
|
+
const fk: ForeignKeyDefinition = { column, references: '', on: '' }
|
|
106
|
+
this.#foreignKeys.push(fk)
|
|
107
|
+
return new ForeignKey(fk)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
index(columns: string | string[], name?: string): this {
|
|
111
|
+
this.#indexes.push({
|
|
112
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
113
|
+
type: 'index',
|
|
114
|
+
name
|
|
115
|
+
})
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
unique(columns: string | string[], name?: string): this {
|
|
120
|
+
this.#indexes.push({
|
|
121
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
122
|
+
type: 'unique',
|
|
123
|
+
name
|
|
124
|
+
})
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
primary(columns: string | string[], name?: string): this {
|
|
129
|
+
this.#indexes.push({
|
|
130
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
131
|
+
type: 'primary',
|
|
132
|
+
name
|
|
133
|
+
})
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
dropColumn(...name: string[] | string[][]): this {
|
|
138
|
+
this.#dropColumns.push(...name.flat(Infinity) as string[])
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
renameColumn(from: string, to: string): this {
|
|
143
|
+
this.#renameColumns.set(from, to)
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get table(): string {
|
|
148
|
+
return this.#table
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get columns() {
|
|
152
|
+
return this.#columns
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get indexes() {
|
|
156
|
+
return this.#indexes
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get foreignKeys() {
|
|
160
|
+
return this.#foreignKeys
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get dropColumns() {
|
|
164
|
+
return this.#dropColumns
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get renameColumns() {
|
|
168
|
+
return this.#renameColumns
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Blueprint } from './blueprint'
|
|
2
|
+
import type { ColumnDefinition, IndexDefinition, ForeignKeyDefinition } from './types'
|
|
3
|
+
|
|
4
|
+
export default class SchemaBuilder {
|
|
5
|
+
static create(blueprint: Blueprint): string {
|
|
6
|
+
const tableName = blueprint.table
|
|
7
|
+
const columns = blueprint.columns
|
|
8
|
+
const indexes = blueprint.indexes
|
|
9
|
+
const foreignKeys = blueprint.foreignKeys
|
|
10
|
+
|
|
11
|
+
const columnDefinitions = columns.map(col => this.#column(col))
|
|
12
|
+
const indexDefinitions = indexes.map(idx => this.#index(idx, tableName))
|
|
13
|
+
const foreignKeyDefinitions = foreignKeys.map(fk => this.#foreignKey(fk))
|
|
14
|
+
|
|
15
|
+
const allDefinitions = [
|
|
16
|
+
...columnDefinitions,
|
|
17
|
+
...indexDefinitions,
|
|
18
|
+
...foreignKeyDefinitions
|
|
19
|
+
].filter(Boolean)
|
|
20
|
+
|
|
21
|
+
return `CREATE TABLE ${tableName} (\n ${allDefinitions.join(',\n ')}\n);`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static alter(blueprint: Blueprint): string[] {
|
|
25
|
+
const tableName = blueprint.table
|
|
26
|
+
const statements: string[] = []
|
|
27
|
+
|
|
28
|
+
const columns = blueprint.columns
|
|
29
|
+
if (columns.length > 0)
|
|
30
|
+
columns.forEach(col => statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.#column(col)};`))
|
|
31
|
+
|
|
32
|
+
const dropColumns = blueprint.dropColumns
|
|
33
|
+
dropColumns.forEach(col => statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${col};`))
|
|
34
|
+
|
|
35
|
+
const renameColumns = blueprint.renameColumns
|
|
36
|
+
renameColumns.forEach((newName, oldName) => statements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${oldName} TO ${newName};`))
|
|
37
|
+
|
|
38
|
+
const indexes = blueprint.indexes
|
|
39
|
+
indexes.forEach(idx => {
|
|
40
|
+
const indexSql = this.#indexStatement(idx, tableName)
|
|
41
|
+
if (indexSql) statements.push(indexSql)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const foreignKeys = blueprint.foreignKeys
|
|
45
|
+
foreignKeys.forEach(fk => statements.push(`ALTER TABLE ${tableName} ADD ${this.#foreignKey(fk)};`))
|
|
46
|
+
|
|
47
|
+
return statements
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static #column(column: ColumnDefinition) {
|
|
51
|
+
let sql = `${column.name} ${column.type}`
|
|
52
|
+
|
|
53
|
+
if (column.length && !column.type.includes('('))
|
|
54
|
+
sql += `(${column.length})`
|
|
55
|
+
|
|
56
|
+
if (column.unsigned)
|
|
57
|
+
sql += ' UNSIGNED'
|
|
58
|
+
|
|
59
|
+
if (column.nullable) {
|
|
60
|
+
sql += ' NULL'
|
|
61
|
+
} else {
|
|
62
|
+
sql += ' NOT NULL'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (column.autoIncrement)
|
|
66
|
+
sql += ' AUTO_INCREMENT'
|
|
67
|
+
|
|
68
|
+
if (column.default !== undefined) {
|
|
69
|
+
if (column.default === null) {
|
|
70
|
+
sql += ' DEFAULT NULL'
|
|
71
|
+
} else if (typeof column.default === 'string') {
|
|
72
|
+
sql += ` DEFAULT '${column.default}'`
|
|
73
|
+
} else if (typeof column.default === 'boolean') {
|
|
74
|
+
sql += ` DEFAULT ${column.default ? 1 : 0}`
|
|
75
|
+
} else {
|
|
76
|
+
sql += ` DEFAULT ${column.default}`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (column.unique)
|
|
81
|
+
sql += ' UNIQUE'
|
|
82
|
+
|
|
83
|
+
if (column.primary)
|
|
84
|
+
sql += ' PRIMARY KEY'
|
|
85
|
+
|
|
86
|
+
if (column.comment)
|
|
87
|
+
sql += ` COMMENT '${column.comment.replace(/'/g, "''")}'`
|
|
88
|
+
|
|
89
|
+
return sql
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static #index(index: IndexDefinition, tableName: string): string {
|
|
93
|
+
const indexName = index.name || `${tableName}_${index.columns.join('_')}_${index.type}`
|
|
94
|
+
const columns = index.columns.join(', ')
|
|
95
|
+
|
|
96
|
+
switch (index.type) {
|
|
97
|
+
case 'primary':
|
|
98
|
+
return `PRIMARY KEY (${columns})`
|
|
99
|
+
case 'unique':
|
|
100
|
+
return `UNIQUE KEY ${indexName} (${columns})`
|
|
101
|
+
case 'index':
|
|
102
|
+
return `KEY ${indexName} (${columns})`
|
|
103
|
+
default:
|
|
104
|
+
return ''
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static #indexStatement(index: IndexDefinition, tableName: string): string {
|
|
109
|
+
const indexName = index.name || `${tableName}_${index.columns.join('_')}_${index.type}`
|
|
110
|
+
const columns = index.columns.join(', ')
|
|
111
|
+
|
|
112
|
+
switch (index.type) {
|
|
113
|
+
case 'primary':
|
|
114
|
+
return `ALTER TABLE ${tableName} ADD PRIMARY KEY (${columns});`
|
|
115
|
+
case 'unique':
|
|
116
|
+
return `CREATE UNIQUE INDEX ${indexName} ON ${tableName} (${columns});`
|
|
117
|
+
case 'index':
|
|
118
|
+
return `CREATE INDEX ${indexName} ON ${tableName} (${columns});`
|
|
119
|
+
default:
|
|
120
|
+
return ''
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static #foreignKey(fk: ForeignKeyDefinition): string {
|
|
125
|
+
let sql = `FOREIGN KEY (${fk.column}) REFERENCES ${fk.on}(${fk.references})`
|
|
126
|
+
|
|
127
|
+
if (fk.onDelete)
|
|
128
|
+
sql += ` ON DELETE ${fk.onDelete.toUpperCase()}`
|
|
129
|
+
|
|
130
|
+
if (fk.onUpdate)
|
|
131
|
+
sql += ` ON UPDATE ${fk.onUpdate.toUpperCase()}`
|
|
132
|
+
|
|
133
|
+
return sql
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static drop(tableName: string) {
|
|
137
|
+
return `DROP TABLE ${tableName};`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static dropIfExists(tableName: string) {
|
|
141
|
+
return `DROP TABLE IF EXISTS ${tableName};`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static rename(from: string, to: string) {
|
|
145
|
+
return `ALTER TABLE ${from} RENAME TO ${to};`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static hasTable(tableName: string) {
|
|
149
|
+
return `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}';`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static hasColumn(tableName: string, columnName: string) { // TODO refactor..
|
|
153
|
+
return `PRAGMA table_info(${tableName});`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getAllTables() {
|
|
157
|
+
return `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static getColumns(tableName: string): string {
|
|
161
|
+
return `PRAGMA table_info(${tableName});`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static dropAllTables(tables: string[]) {
|
|
165
|
+
return tables.map(table => `DROP TABLE IF EXISTS ${table};`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ColumnDefinition } from './types'
|
|
2
|
+
|
|
3
|
+
export default class Column {
|
|
4
|
+
constructor(private column: ColumnDefinition) {}
|
|
5
|
+
|
|
6
|
+
nullable() {
|
|
7
|
+
this.column.nullable = true
|
|
8
|
+
return this
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
unique() {
|
|
12
|
+
this.column.unique = true
|
|
13
|
+
return this
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
default(value: any) {
|
|
17
|
+
this.column.default = value
|
|
18
|
+
return this
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
unsigned() {
|
|
22
|
+
this.column.unsigned = true
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
index() {
|
|
27
|
+
this.column.index = true
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
comment(text: string) {
|
|
32
|
+
this.column.comment = text
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
primary() {
|
|
37
|
+
this.column.primary = true
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
autoIncrement() {
|
|
42
|
+
this.column.autoIncrement = true
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ForeignKeyDefinition } from './types'
|
|
2
|
+
|
|
3
|
+
export default class ForeignKey {
|
|
4
|
+
constructor(private fk: ForeignKeyDefinition) {}
|
|
5
|
+
|
|
6
|
+
references(column: string) {
|
|
7
|
+
this.fk.references = column
|
|
8
|
+
return this
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
on(table: string) {
|
|
12
|
+
this.fk.on = table
|
|
13
|
+
return this
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
onDelete(action: 'cascade' | 'set null' | 'restrict' | 'no action') {
|
|
17
|
+
this.fk.onDelete = action;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onUpdate(action: 'cascade' | 'set null' | 'restrict' | 'no action') {
|
|
22
|
+
this.fk.onUpdate = action
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import glob from 'tiny-glob'
|
|
2
|
+
import { mkdirSync, existsSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
import { Datte, IMPORT } from 't0n'
|
|
5
|
+
import { Schema } from './schema'
|
|
6
|
+
import { MigrationInfo, MigrationClass } from './types'
|
|
7
|
+
|
|
8
|
+
const __root = resolve(dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
export class Migrator {
|
|
11
|
+
static #dir: string = ''
|
|
12
|
+
static #folder: string = 'migrations'
|
|
13
|
+
static #sqlFolder: string = 'sql'
|
|
14
|
+
|
|
15
|
+
static dir(dir: string) {
|
|
16
|
+
this.#dir = dir
|
|
17
|
+
return this
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static folder(dir: string) {
|
|
21
|
+
this.#dir = dir
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async toSql(outputDir: string = '') {
|
|
26
|
+
const dir = this.#dir || join(__root, this.#folder)
|
|
27
|
+
outputDir ||= join(dir, this.#sqlFolder)
|
|
28
|
+
|
|
29
|
+
this.#ensureDir(dir)
|
|
30
|
+
this.#ensureDir(outputDir)
|
|
31
|
+
const files = (await glob(join(dir, '/*.{ts,js}'))).filter(file => !file.includes('.d.')) // TODO: sort
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const info = await this.#info(file)
|
|
35
|
+
if (!info)
|
|
36
|
+
continue // TODO: trigger a warn
|
|
37
|
+
|
|
38
|
+
const sql = await this.run(info.handler)
|
|
39
|
+
const path = join(outputDir, info.name +'.sql')
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
writeFileSync(path, `-- Migration: ${info.name}\n\n${sql}\n`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async run(handler: MigrationClass) {
|
|
47
|
+
// try {
|
|
48
|
+
// await migrationClass.up()
|
|
49
|
+
// } catch (error) {
|
|
50
|
+
// // Try run() if up() is not implemented
|
|
51
|
+
// if (migrationClass.run) {
|
|
52
|
+
// await migrationClass.run()
|
|
53
|
+
// } else {
|
|
54
|
+
// throw error
|
|
55
|
+
// }
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
if (!handler?.run) return ''
|
|
59
|
+
Schema.clearStatements()
|
|
60
|
+
await handler.run()
|
|
61
|
+
return Schema.sql
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static async #info(fileName: string): Promise<MigrationInfo | null> {
|
|
65
|
+
const name = fileName.replace(/\.[jt]s$/, '')
|
|
66
|
+
const match = name.match(/\/(\d{4})_(\d{2})_(\d{2})_(\d{2})(\d{2})(\d{2})_(.+)$/)
|
|
67
|
+
|
|
68
|
+
if (!match) return null
|
|
69
|
+
const [, year, month, day, hour, minute, second, slugName] = match
|
|
70
|
+
|
|
71
|
+
const mod = await IMPORT(join(__root, fileName))
|
|
72
|
+
const handler = mod.default as MigrationClass
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
timestamp: new Date(
|
|
76
|
+
parseInt(year),
|
|
77
|
+
parseInt(month) - 1, // Js months are 0-indexed
|
|
78
|
+
parseInt(day),
|
|
79
|
+
parseInt(hour),
|
|
80
|
+
parseInt(minute),
|
|
81
|
+
parseInt(second)
|
|
82
|
+
).getTime(),
|
|
83
|
+
name: name.split('/').at(-1) as string,
|
|
84
|
+
fileName,
|
|
85
|
+
className: this.#toClassName(slugName),
|
|
86
|
+
handler,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static #toClassName(name: string) {
|
|
91
|
+
return name
|
|
92
|
+
.split(/[-_.]/)
|
|
93
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
|
|
94
|
+
.join('')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static fileName(name: string) {
|
|
98
|
+
return (
|
|
99
|
+
name.replace(/([A-Z])/g, '_$1') // snake_case
|
|
100
|
+
.replace(/\s+/g, '_')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
+ '_'
|
|
103
|
+
+ Datte.dateTime().replace(/[:.-]/g, '_').replace(/_+/g, '_')
|
|
104
|
+
).replace(/^_+|_+$/g, '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static #ensureDir(dir: string) {
|
|
108
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
import { Blueprint } from './blueprint'
|
|
3
|
+
import Builder from './builder'
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
BlueprintFn,
|
|
7
|
+
// SchemaConnection,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
export class Schema {
|
|
11
|
+
// static #c: SchemaConnection | null = null
|
|
12
|
+
static #statements: string[] = []
|
|
13
|
+
static #foreignKeyConstraintsEnabled = true
|
|
14
|
+
|
|
15
|
+
// static setConnection(connection: SchemaConnection) {
|
|
16
|
+
// this.#c = connection
|
|
17
|
+
// return this
|
|
18
|
+
// }
|
|
19
|
+
|
|
20
|
+
static get statements() {
|
|
21
|
+
return this.#statements
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static clearStatements() {
|
|
25
|
+
this.#statements = []
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static #addStatement(...sql: string[] | string[][]) {
|
|
30
|
+
this.#statements.push(...sql.flat(Infinity) as string[])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// mudei tudo this.#executeSql -> this.#addStatement
|
|
34
|
+
// static async #executeSql(...sql: string[] | string[][]): Promise<void> {
|
|
35
|
+
// sql = sql.flat(Infinity) as string[]
|
|
36
|
+
// // if (this.#c) {
|
|
37
|
+
// // for (const statement of sql) {
|
|
38
|
+
// // await this.#c.execute(statement)
|
|
39
|
+
// // }
|
|
40
|
+
// // }
|
|
41
|
+
// this.#addStatement(...sql)
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
static async create(tableName: string, fn: BlueprintFn): Promise<void> {
|
|
45
|
+
const blueprint = new Blueprint(tableName)
|
|
46
|
+
fn(blueprint)
|
|
47
|
+
await this.#addStatement(Builder.create(blueprint))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async table(tableName: string, fn: BlueprintFn): Promise<void> {
|
|
51
|
+
const blueprint = new Blueprint(tableName)
|
|
52
|
+
fn(blueprint)
|
|
53
|
+
await this.#addStatement(Builder.alter(blueprint))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static async drop(tableName: string): Promise<void> {
|
|
57
|
+
await this.#addStatement(Builder.drop(tableName))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static async dropIfExists(tableName: string): Promise<void> {
|
|
61
|
+
await this.#addStatement(Builder.dropIfExists(tableName))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static async rename(from: string, to: string): Promise<void> {
|
|
65
|
+
await this.#addStatement(Builder.rename(from, to))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async disableForeignKeyConstraints(): Promise<void> {
|
|
69
|
+
this.#foreignKeyConstraintsEnabled = false
|
|
70
|
+
await this.#addStatement('PRAGMA foreign_keys = OFF;')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static async enableForeignKeyConstraints(): Promise<void> {
|
|
74
|
+
this.#foreignKeyConstraintsEnabled = true
|
|
75
|
+
await this.#addStatement('PRAGMA foreign_keys = ON;')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static isForeignKeyConstraintsEnabled(): boolean {
|
|
79
|
+
return this.#foreignKeyConstraintsEnabled
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static get sql() {
|
|
83
|
+
return this.#statements.join('\n\n')
|
|
84
|
+
}
|
|
85
|
+
static get raw() {
|
|
86
|
+
return this.sql
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// static async dropAllTables(): Promise<void> {
|
|
90
|
+
// const tables = await this.getAllTables()
|
|
91
|
+
// const sql = Builder.dropAllTables(tables)
|
|
92
|
+
// await this.#addStatement(sql)
|
|
93
|
+
// }
|
|
94
|
+
|
|
95
|
+
// static async hasTable(tableName: string): Promise<boolean> {
|
|
96
|
+
// if (!this.#c)
|
|
97
|
+
// throw new Error('Database connection not set')
|
|
98
|
+
|
|
99
|
+
// const sql = Builder.hasTable(tableName)
|
|
100
|
+
// const result = await this.#c.query(sql)
|
|
101
|
+
// return result.length > 0
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
// static async hasColumn(tableName: string, columnName: string): Promise<boolean> {
|
|
105
|
+
// if (!this.#c)
|
|
106
|
+
// throw new Error('Database connection not set')
|
|
107
|
+
|
|
108
|
+
// const sql = Builder.hasColumn(tableName, columnName)
|
|
109
|
+
// const result = await this.#c.query(sql)
|
|
110
|
+
// return result.some((row: any) => row.name === columnName)
|
|
111
|
+
// }
|
|
112
|
+
|
|
113
|
+
// static async hasColumns(tableName: string, ...columnNames: string[]): Promise<boolean> {
|
|
114
|
+
// if (!this.#c)
|
|
115
|
+
// throw new Error('Database connection not set')
|
|
116
|
+
|
|
117
|
+
// const sql = Builder.hasColumn(tableName, '')
|
|
118
|
+
// const result = await this.#c.query(sql)
|
|
119
|
+
// const existingColumns = result.map((row: any) => row.name)
|
|
120
|
+
|
|
121
|
+
// return columnNames.every(col => existingColumns.includes(col))
|
|
122
|
+
// }
|
|
123
|
+
|
|
124
|
+
// static async getAllTables(): Promise<string[]> {
|
|
125
|
+
// if (!this.#c)
|
|
126
|
+
// throw new Error('Database connection not set')
|
|
127
|
+
|
|
128
|
+
// const sql = Builder.getAllTables()
|
|
129
|
+
// const result = await this.#c.query(sql)
|
|
130
|
+
// return result.map((row: any) => row.name)
|
|
131
|
+
// }
|
|
132
|
+
|
|
133
|
+
// static async getColumns(tableName: string): Promise<any[]> {
|
|
134
|
+
// if (!this.#c)
|
|
135
|
+
// throw new Error('Database connection not set')
|
|
136
|
+
|
|
137
|
+
// const sql = Builder.getColumns(tableName)
|
|
138
|
+
// return await this.#c.query(sql)
|
|
139
|
+
// }
|
|
140
|
+
|
|
141
|
+
// static async getColumnType(tableName: string, columnName: string): Promise<string | null> {
|
|
142
|
+
// if (!this.#c) {
|
|
143
|
+
// throw new Error('Database connection not set')
|
|
144
|
+
// }
|
|
145
|
+
|
|
146
|
+
// const sql = Builder.getColumns(tableName)
|
|
147
|
+
// const result = await this.#c.query(sql)
|
|
148
|
+
// const column = result.find((row: any) => row.name === columnName)
|
|
149
|
+
// return column ? column.type : null
|
|
150
|
+
// }
|
|
151
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Blueprint } from './blueprint'
|
|
2
|
+
import { Migration } from './migration'
|
|
3
|
+
|
|
4
|
+
export type BlueprintFn = (table: Blueprint) => void
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// TODO: refactor bellow
|
|
8
|
+
|
|
9
|
+
export interface ColumnDefinition {
|
|
10
|
+
name: string,
|
|
11
|
+
type: string,
|
|
12
|
+
length?: number,
|
|
13
|
+
nullable?: boolean,
|
|
14
|
+
unique?: boolean,
|
|
15
|
+
primary?: boolean,
|
|
16
|
+
autoIncrement?: boolean,
|
|
17
|
+
default?: any,
|
|
18
|
+
unsigned?: boolean,
|
|
19
|
+
index?: boolean,
|
|
20
|
+
comment?: string,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IndexDefinition {
|
|
24
|
+
columns: string[],
|
|
25
|
+
type: 'index' | 'unique' | 'primary',
|
|
26
|
+
name?: string,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ForeignKeyDefinition {
|
|
30
|
+
column: string,
|
|
31
|
+
references: string,
|
|
32
|
+
on: string,
|
|
33
|
+
onDelete?: 'cascade' | 'set null' | 'restrict' | 'no action',
|
|
34
|
+
onUpdate?: 'cascade' | 'set null' | 'restrict' | 'no action',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MigrationInfo {
|
|
38
|
+
timestamp: number,
|
|
39
|
+
name: string,
|
|
40
|
+
fileName: string,
|
|
41
|
+
className: string,
|
|
42
|
+
handler: MigrationClass,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MigrationRecord {
|
|
46
|
+
migration: string;
|
|
47
|
+
batch: number;
|
|
48
|
+
executed_at: Date;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type MigrationClass = {
|
|
52
|
+
// new (): MigrationClass;
|
|
53
|
+
// new(): Migration;
|
|
54
|
+
// up(): Promise<void>;
|
|
55
|
+
// down(): Promise<void>;
|
|
56
|
+
run(): Promise<void>
|
|
57
|
+
// toSQL?(): Promise<string>;
|
|
58
|
+
// getMigrationName?(): string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// export interface SchemaConnection {
|
|
62
|
+
// execute(sql: string): Promise<any>,
|
|
63
|
+
// query(sql: string): Promise<any[]>,
|
|
64
|
+
// }
|
package/src/model.ts
CHANGED
|
@@ -15,7 +15,7 @@ export default abstract class Model<TB extends keyof DB, DB> {
|
|
|
15
15
|
static $schema?: DBSchema
|
|
16
16
|
|
|
17
17
|
static pipe<S, T>(): Pipe<S, T> {
|
|
18
|
-
throw new Error(`Database connection not provided.`) // improv this message
|
|
18
|
+
throw new Error(`Database connection not provided.`) // TODO: improv this message
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
static builder<S, T>() {
|