create-phoenixjs 0.1.4 → 0.1.6
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 +1 -1
- package/template/config/database.ts +13 -1
- package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
- package/template/database/migrations/20260108165704_TestCliMigration.ts +16 -0
- package/template/database/migrations/2026_01_08_16_57_04_CreateTestMigrationsTable.ts +21 -0
- package/template/framework/cli/artisan.ts +12 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +33 -3
- package/template/framework/database/DatabaseManager.ts +133 -0
- package/template/framework/database/connection/Connection.ts +71 -0
- package/template/framework/database/connection/ConnectionFactory.ts +30 -0
- package/template/framework/database/connection/PostgresConnection.ts +159 -0
- package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
- package/template/framework/database/console/MigrateCommand.ts +32 -0
- package/template/framework/database/console/MigrateResetCommand.ts +31 -0
- package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
- package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
- package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
- package/template/framework/database/migrations/Migration.ts +5 -0
- package/template/framework/database/migrations/MigrationRepository.ts +46 -0
- package/template/framework/database/migrations/Migrator.ts +249 -0
- package/template/framework/database/migrations/index.ts +4 -0
- package/template/framework/database/orm/BelongsTo.ts +246 -0
- package/template/framework/database/orm/BelongsToMany.ts +570 -0
- package/template/framework/database/orm/Builder.ts +160 -0
- package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
- package/template/framework/database/orm/HasMany.ts +303 -0
- package/template/framework/database/orm/HasManyThrough.ts +282 -0
- package/template/framework/database/orm/HasOne.ts +201 -0
- package/template/framework/database/orm/HasOneThrough.ts +281 -0
- package/template/framework/database/orm/Model.ts +1802 -0
- package/template/framework/database/orm/Relation.ts +342 -0
- package/template/framework/database/orm/Scope.ts +14 -0
- package/template/framework/database/orm/SoftDeletes.ts +160 -0
- package/template/framework/database/orm/index.ts +54 -0
- package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
- package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
- package/template/framework/database/pagination/Paginator.ts +110 -0
- package/template/framework/database/pagination/index.ts +2 -0
- package/template/framework/database/query/Builder.ts +918 -0
- package/template/framework/database/query/DB.ts +139 -0
- package/template/framework/database/query/grammars/Grammar.ts +430 -0
- package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
- package/template/framework/database/query/grammars/index.ts +6 -0
- package/template/framework/database/query/index.ts +8 -0
- package/template/framework/database/query/types.ts +196 -0
- package/template/framework/database/schema/Blueprint.ts +478 -0
- package/template/framework/database/schema/Schema.ts +149 -0
- package/template/framework/database/schema/SchemaBuilder.ts +152 -0
- package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
- package/template/framework/database/schema/grammars/index.ts +5 -0
- package/template/framework/database/schema/index.ts +9 -0
- package/template/package.json +4 -1
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
default: '
|
|
2
|
+
default: process.env.DB_CONNECTION || 'postgres',
|
|
3
3
|
|
|
4
4
|
connections: {
|
|
5
|
+
postgres: {
|
|
6
|
+
driver: 'postgres',
|
|
7
|
+
host: process.env.DB_HOST || 'localhost',
|
|
8
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
9
|
+
database: process.env.DB_DATABASE || 'phoenix',
|
|
10
|
+
username: process.env.DB_USERNAME || 'root',
|
|
11
|
+
password: process.env.DB_PASSWORD || '',
|
|
12
|
+
max: 10, // Max connections in pool
|
|
13
|
+
idle_timeout: 20, // Idle timeout in seconds
|
|
14
|
+
connect_timeout: 10, // Connection timeout in seconds
|
|
15
|
+
},
|
|
16
|
+
|
|
5
17
|
prisma: {
|
|
6
18
|
driver: 'prisma',
|
|
7
19
|
// Prisma usually relies on .env DATABASE_URL, but we can put pool config here
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema, Blueprint } from '@framework/database/schema';
|
|
2
|
+
import { Migration } from '@framework/database/migrations';
|
|
3
|
+
|
|
4
|
+
export default class CreateUsersTable implements Migration {
|
|
5
|
+
async up(): Promise<void> {
|
|
6
|
+
await Schema.create('test_users_cli', (table: Blueprint) => {
|
|
7
|
+
table.id();
|
|
8
|
+
table.string('name');
|
|
9
|
+
table.timestamps();
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async down(): Promise<void> {
|
|
14
|
+
await Schema.dropIfExists('test_users_cli');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import { Schema, Blueprint } from '@framework/database/schema';
|
|
3
|
+
import { Migration } from '@framework/database/migrations';
|
|
4
|
+
|
|
5
|
+
export class TestCliMigration implements Migration {
|
|
6
|
+
async up() {
|
|
7
|
+
await Schema.create('test_cli_table', (table) => {
|
|
8
|
+
table.id();
|
|
9
|
+
table.string('name');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async down() {
|
|
14
|
+
await Schema.dropIfExists('test_cli_table');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Schema, Blueprint } from '@framework/database/schema';
|
|
2
|
+
import { Migration } from '@framework/database/migrations';
|
|
3
|
+
|
|
4
|
+
export class CreateTestMigrationsTable implements Migration {
|
|
5
|
+
/**
|
|
6
|
+
* Run the migrations.
|
|
7
|
+
*/
|
|
8
|
+
async up(): Promise<void> {
|
|
9
|
+
// await Schema.create('table_name', (table: Blueprint) => {
|
|
10
|
+
// table.id();
|
|
11
|
+
// table.timestamps();
|
|
12
|
+
// });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reverse the migrations.
|
|
17
|
+
*/
|
|
18
|
+
async down(): Promise<void> {
|
|
19
|
+
// await Schema.dropIfExists('table_name');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -3,6 +3,11 @@ import { MakeControllerCommand } from './commands/MakeControllerCommand';
|
|
|
3
3
|
import { MakeValidatorCommand } from './commands/MakeValidatorCommand';
|
|
4
4
|
import { MakeModelCommand } from './commands/MakeModelCommand';
|
|
5
5
|
import { MakeMiddlewareCommand } from './commands/MakeMiddlewareCommand';
|
|
6
|
+
import { MakeMigrationCommand } from '../database/console/MakeMigrationCommand';
|
|
7
|
+
import { MigrateCommand } from '../database/console/MigrateCommand';
|
|
8
|
+
import { MigrateRollbackCommand } from '../database/console/MigrateRollbackCommand';
|
|
9
|
+
import { MigrateResetCommand } from '../database/console/MigrateResetCommand';
|
|
10
|
+
import { MigrateStatusCommand } from '../database/console/MigrateStatusCommand';
|
|
6
11
|
|
|
7
12
|
const app = new ConsoleApplication();
|
|
8
13
|
|
|
@@ -12,5 +17,12 @@ app.register(new MakeValidatorCommand());
|
|
|
12
17
|
app.register(new MakeModelCommand());
|
|
13
18
|
app.register(new MakeMiddlewareCommand());
|
|
14
19
|
|
|
20
|
+
// Database Commands
|
|
21
|
+
app.register(new MakeMigrationCommand());
|
|
22
|
+
app.register(new MigrateCommand());
|
|
23
|
+
app.register(new MigrateRollbackCommand());
|
|
24
|
+
app.register(new MigrateResetCommand());
|
|
25
|
+
app.register(new MigrateStatusCommand());
|
|
26
|
+
|
|
15
27
|
// Run
|
|
16
28
|
await app.run(process.argv);
|
|
@@ -6,7 +6,7 @@ export class MakeModelCommand extends Command {
|
|
|
6
6
|
signature = 'make:model {name}';
|
|
7
7
|
description = 'Create a new model class';
|
|
8
8
|
|
|
9
|
-
async handle(args: Record<string,
|
|
9
|
+
async handle(args: Record<string, string>): Promise<void> {
|
|
10
10
|
const name = args.name;
|
|
11
11
|
if (!name) {
|
|
12
12
|
console.error('Error: Model name is required.');
|
|
@@ -25,12 +25,42 @@ export class MakeModelCommand extends Command {
|
|
|
25
25
|
|
|
26
26
|
fs.mkdirSync(directory, { recursive: true });
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Generate snake_case table name from class name (e.g., UserProfile -> user_profiles)
|
|
29
|
+
const tableName = this.generateTableName(className);
|
|
30
|
+
|
|
31
|
+
const template = `import { Model } from '@framework/database/orm/Model';
|
|
32
|
+
|
|
33
|
+
export class ${className} extends Model {
|
|
34
|
+
protected table = '${tableName}';
|
|
35
|
+
protected fillable: string[] = [];
|
|
30
36
|
}
|
|
31
37
|
`;
|
|
32
38
|
|
|
33
39
|
fs.writeFileSync(fullPath, template);
|
|
34
40
|
console.log(`Model created successfully: app/models/${relativePath}`);
|
|
35
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a table name from the class name.
|
|
45
|
+
* Converts PascalCase to snake_case and pluralizes.
|
|
46
|
+
* e.g., User -> users, UserProfile -> user_profiles
|
|
47
|
+
*/
|
|
48
|
+
private generateTableName(className: string): string {
|
|
49
|
+
// Convert PascalCase to snake_case
|
|
50
|
+
let result = '';
|
|
51
|
+
for (let i = 0; i < className.length; i++) {
|
|
52
|
+
const char = className[i];
|
|
53
|
+
if (char >= 'A' && char <= 'Z') {
|
|
54
|
+
if (i > 0) {
|
|
55
|
+
result += '_';
|
|
56
|
+
}
|
|
57
|
+
result += char.toLowerCase();
|
|
58
|
+
} else {
|
|
59
|
+
result += char;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Simple pluralization (add 's')
|
|
64
|
+
return result + 's';
|
|
65
|
+
}
|
|
36
66
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM - DatabaseManager
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for database operations.
|
|
5
|
+
* Manages connections, provides default connection, and proxies query methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Connection, ConnectionConfig } from './connection/Connection';
|
|
9
|
+
import { ConnectionFactory } from './connection/ConnectionFactory';
|
|
10
|
+
|
|
11
|
+
export interface DatabaseConfig {
|
|
12
|
+
default: string;
|
|
13
|
+
connections: Record<string, ConnectionConfig>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DatabaseManager {
|
|
17
|
+
private factory: ConnectionFactory;
|
|
18
|
+
private connections: Map<string, Connection> = new Map();
|
|
19
|
+
private config: DatabaseConfig;
|
|
20
|
+
|
|
21
|
+
constructor(config: DatabaseConfig) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.factory = new ConnectionFactory();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get a database connection by name
|
|
28
|
+
* Creates the connection if it doesn't exist
|
|
29
|
+
*/
|
|
30
|
+
connection(name?: string): Connection {
|
|
31
|
+
const connectionName = name || this.config.default;
|
|
32
|
+
|
|
33
|
+
if (!this.connections.has(connectionName)) {
|
|
34
|
+
const connectionConfig = this.config.connections[connectionName];
|
|
35
|
+
if (!connectionConfig) {
|
|
36
|
+
throw new Error(`Database connection [${connectionName}] not configured.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const connection = this.factory.make(connectionConfig);
|
|
40
|
+
this.connections.set(connectionName, connection);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this.connections.get(connectionName)!;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connect to a database by name (or default)
|
|
48
|
+
*/
|
|
49
|
+
async connect(name?: string): Promise<Connection> {
|
|
50
|
+
const conn = this.connection(name);
|
|
51
|
+
await conn.connect();
|
|
52
|
+
return conn;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Disconnect a specific connection
|
|
57
|
+
*/
|
|
58
|
+
async disconnect(name?: string): Promise<void> {
|
|
59
|
+
const connectionName = name || this.config.default;
|
|
60
|
+
const conn = this.connections.get(connectionName);
|
|
61
|
+
if (conn) {
|
|
62
|
+
await conn.disconnect();
|
|
63
|
+
this.connections.delete(connectionName);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Disconnect all connections
|
|
69
|
+
*/
|
|
70
|
+
async disconnectAll(): Promise<void> {
|
|
71
|
+
const disconnectPromises = Array.from(this.connections.values()).map((conn) =>
|
|
72
|
+
conn.disconnect()
|
|
73
|
+
);
|
|
74
|
+
await Promise.all(disconnectPromises);
|
|
75
|
+
this.connections.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute a SELECT query on the default connection
|
|
80
|
+
*/
|
|
81
|
+
async query<T = unknown>(sql: string, bindings?: unknown[]): Promise<T[]> {
|
|
82
|
+
return this.connection().query<T>(sql, bindings);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute an INSERT/UPDATE/DELETE on the default connection
|
|
87
|
+
*/
|
|
88
|
+
async execute(sql: string, bindings?: unknown[]): Promise<number> {
|
|
89
|
+
return this.connection().execute(sql, bindings);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get a single row from the default connection
|
|
94
|
+
*/
|
|
95
|
+
async get<T = unknown>(sql: string, bindings?: unknown[]): Promise<T | null> {
|
|
96
|
+
return this.connection().get<T>(sql, bindings);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Execute a transaction on the default connection
|
|
101
|
+
*/
|
|
102
|
+
async transaction<T>(callback: (tx: unknown) => Promise<T>): Promise<T> {
|
|
103
|
+
return this.connection().transaction(callback);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Begin a fluent query against a database table on the default connection
|
|
108
|
+
*/
|
|
109
|
+
table(table: string): any {
|
|
110
|
+
return this.connection().table(table);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the default connection name
|
|
115
|
+
*/
|
|
116
|
+
getDefaultConnection(): string {
|
|
117
|
+
return this.config.default;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all connection names
|
|
122
|
+
*/
|
|
123
|
+
getConnectionNames(): string[] {
|
|
124
|
+
return Object.keys(this.config.connections);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a connection is active
|
|
129
|
+
*/
|
|
130
|
+
hasConnection(name: string): boolean {
|
|
131
|
+
return this.connections.has(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM - Connection Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstract interface for database connections.
|
|
5
|
+
* All database drivers must implement this interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ConnectionConfig {
|
|
9
|
+
driver: string;
|
|
10
|
+
host?: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
database?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Connection {
|
|
19
|
+
/**
|
|
20
|
+
* Get the driver name
|
|
21
|
+
*/
|
|
22
|
+
readonly driver: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Connect to the database
|
|
26
|
+
*/
|
|
27
|
+
connect(): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Disconnect from the database
|
|
31
|
+
*/
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if connected
|
|
36
|
+
*/
|
|
37
|
+
isConnected(): boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute a SELECT query and return results
|
|
41
|
+
* @param sql SQL query string
|
|
42
|
+
* @param bindings Query parameter bindings
|
|
43
|
+
*/
|
|
44
|
+
query<T = unknown>(sql: string, bindings?: unknown[]): Promise<T[]>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute an INSERT/UPDATE/DELETE query and return affected rows count
|
|
48
|
+
* @param sql SQL statement string
|
|
49
|
+
* @param bindings Query parameter bindings
|
|
50
|
+
*/
|
|
51
|
+
execute(sql: string, bindings?: unknown[]): Promise<number>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a single row
|
|
55
|
+
* @param sql SQL query string
|
|
56
|
+
* @param bindings Query parameter bindings
|
|
57
|
+
*/
|
|
58
|
+
get<T = unknown>(sql: string, bindings?: unknown[]): Promise<T | null>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute operations within a transaction
|
|
62
|
+
* @param callback Transaction callback that receives a transaction SQL instance
|
|
63
|
+
*/
|
|
64
|
+
transaction<T>(callback: (tx: unknown) => Promise<T>): Promise<T>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Begin a fluent query against a database table
|
|
68
|
+
* @param table The table name
|
|
69
|
+
*/
|
|
70
|
+
table(table: string): any; // Return any to avoid circular type dependency for now, or use Builder interface if available
|
|
71
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM - ConnectionFactory
|
|
3
|
+
*
|
|
4
|
+
* Factory class for creating database connections based on configuration.
|
|
5
|
+
* Supports driver-based connection instantiation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Connection, ConnectionConfig } from './Connection';
|
|
9
|
+
import { PostgresConnection, type PostgresConnectionConfig } from './PostgresConnection';
|
|
10
|
+
|
|
11
|
+
export class ConnectionFactory {
|
|
12
|
+
/**
|
|
13
|
+
* Create a connection instance based on the driver configuration
|
|
14
|
+
*/
|
|
15
|
+
make(config: ConnectionConfig): Connection {
|
|
16
|
+
switch (config.driver) {
|
|
17
|
+
case 'postgres':
|
|
18
|
+
return new PostgresConnection(config as PostgresConnectionConfig);
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unsupported database driver: ${config.driver}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get list of supported drivers
|
|
26
|
+
*/
|
|
27
|
+
getSupportedDrivers(): string[] {
|
|
28
|
+
return ['postgres'];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM - PostgresConnection
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL connection implementation using postgres.js (porsager/postgres).
|
|
5
|
+
* Zero-dependency, fastest Postgres driver for Node.js/Bun.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import postgres, { type Sql, type TransactionSql } from 'postgres';
|
|
9
|
+
import type { Connection, ConnectionConfig } from './Connection';
|
|
10
|
+
import { Builder } from '@framework/database/query/Builder';
|
|
11
|
+
import { PostgresGrammar } from '@framework/database/query/grammars/PostgresGrammar';
|
|
12
|
+
|
|
13
|
+
export interface PostgresConnectionConfig extends ConnectionConfig {
|
|
14
|
+
driver: 'postgres';
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
database: string;
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
max?: number; // Max connections in pool
|
|
21
|
+
idle_timeout?: number; // Idle connection timeout in seconds
|
|
22
|
+
connect_timeout?: number; // Connection timeout in seconds
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class PostgresConnection implements Connection {
|
|
26
|
+
readonly driver = 'postgres';
|
|
27
|
+
|
|
28
|
+
private sql: Sql | null = null;
|
|
29
|
+
private connected = false;
|
|
30
|
+
private config: PostgresConnectionConfig;
|
|
31
|
+
|
|
32
|
+
constructor(config: PostgresConnectionConfig) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Connect to the PostgreSQL database
|
|
38
|
+
*/
|
|
39
|
+
async connect(): Promise<void> {
|
|
40
|
+
if (this.connected && this.sql) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.sql = postgres({
|
|
45
|
+
host: this.config.host,
|
|
46
|
+
port: this.config.port,
|
|
47
|
+
database: this.config.database,
|
|
48
|
+
username: this.config.username,
|
|
49
|
+
password: this.config.password,
|
|
50
|
+
max: this.config.max ?? 10,
|
|
51
|
+
idle_timeout: this.config.idle_timeout ?? 20,
|
|
52
|
+
connect_timeout: this.config.connect_timeout ?? 10,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Test the connection
|
|
56
|
+
await this.sql`SELECT 1`;
|
|
57
|
+
this.connected = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Disconnect from the database
|
|
62
|
+
*/
|
|
63
|
+
async disconnect(): Promise<void> {
|
|
64
|
+
if (this.sql) {
|
|
65
|
+
await this.sql.end();
|
|
66
|
+
this.sql = null;
|
|
67
|
+
this.connected = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if connected to the database
|
|
73
|
+
*/
|
|
74
|
+
isConnected(): boolean {
|
|
75
|
+
return this.connected;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute a SELECT query and return results
|
|
80
|
+
*/
|
|
81
|
+
async query<T = unknown>(sql: string, bindings: unknown[] = []): Promise<T[]> {
|
|
82
|
+
this.ensureConnected();
|
|
83
|
+
const result = await this.sql!.unsafe(sql, bindings as (string | number | boolean | null)[]);
|
|
84
|
+
return result as unknown as T[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute an INSERT/UPDATE/DELETE query and return affected rows count
|
|
89
|
+
*/
|
|
90
|
+
async execute(sql: string, bindings: unknown[] = []): Promise<number> {
|
|
91
|
+
this.ensureConnected();
|
|
92
|
+
const result = await this.sql!.unsafe(sql, bindings as (string | number | boolean | null)[]);
|
|
93
|
+
return result.count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get a single row
|
|
98
|
+
*/
|
|
99
|
+
async get<T = unknown>(sql: string, bindings: unknown[] = []): Promise<T | null> {
|
|
100
|
+
const results = await this.query<T>(sql, bindings);
|
|
101
|
+
return results.length > 0 ? results[0] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute operations within a transaction.
|
|
106
|
+
* The callback receives a transaction-scoped SQL instance.
|
|
107
|
+
*/
|
|
108
|
+
async transaction<T>(callback: (sql: TransactionSql) => Promise<T>): Promise<T> {
|
|
109
|
+
this.ensureConnected();
|
|
110
|
+
|
|
111
|
+
const result = await this.sql!.begin(async (txSql) => {
|
|
112
|
+
return callback(txSql);
|
|
113
|
+
});
|
|
114
|
+
return result as T;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Execute a transaction query (for use inside transactions)
|
|
119
|
+
*/
|
|
120
|
+
async txQuery<T = unknown>(
|
|
121
|
+
txSql: TransactionSql,
|
|
122
|
+
sql: string,
|
|
123
|
+
bindings: unknown[] = []
|
|
124
|
+
): Promise<T[]> {
|
|
125
|
+
const result = await txSql.unsafe(sql, bindings as (string | number | boolean | null)[]);
|
|
126
|
+
return result as unknown as T[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Execute a transaction statement (for use inside transactions)
|
|
131
|
+
*/
|
|
132
|
+
async txExecute(txSql: TransactionSql, sql: string, bindings: unknown[] = []): Promise<number> {
|
|
133
|
+
const result = await txSql.unsafe(sql, bindings as (string | number | boolean | null)[]);
|
|
134
|
+
return result.count;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the underlying postgres.js SQL instance
|
|
139
|
+
*/
|
|
140
|
+
getSql(): Sql | null {
|
|
141
|
+
return this.sql;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Begin a fluent query against a database table
|
|
146
|
+
*/
|
|
147
|
+
table(table: string): Builder {
|
|
148
|
+
return new Builder(this, new PostgresGrammar(), table);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ensure we have an active connection
|
|
153
|
+
*/
|
|
154
|
+
private ensureConnected(): void {
|
|
155
|
+
if (!this.connected || !this.sql) {
|
|
156
|
+
throw new Error('Database not connected. Call connect() first.');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from '../../cli/Command';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
export class MakeMigrationCommand extends Command {
|
|
6
|
+
signature = 'make:migration {name}';
|
|
7
|
+
description = 'Create a new migration file';
|
|
8
|
+
|
|
9
|
+
async handle(args: { name: string }): Promise<void> {
|
|
10
|
+
const name = args.name;
|
|
11
|
+
|
|
12
|
+
// Generate timestamp YYYY_MM_DD_HHMMSS
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const timestamp = now.toISOString().replace(/[-T:]/g, '_').split('.')[0];
|
|
15
|
+
|
|
16
|
+
const fileName = `${timestamp}_${name}`;
|
|
17
|
+
const className = this.toPascalCase(name);
|
|
18
|
+
|
|
19
|
+
const content = this.getTemplate(className);
|
|
20
|
+
|
|
21
|
+
const directory = join(process.cwd(), 'database', 'migrations');
|
|
22
|
+
const filePath = join(directory, `${fileName}.ts`);
|
|
23
|
+
|
|
24
|
+
await mkdir(directory, { recursive: true });
|
|
25
|
+
await writeFile(filePath, content);
|
|
26
|
+
|
|
27
|
+
console.log(`Created Migration: ${fileName}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected toPascalCase(str: string): string {
|
|
31
|
+
return str.replace(/(?:^|_)(\w)/g, (_, c) => c.toUpperCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected getTemplate(className: string): string {
|
|
35
|
+
return `import { Schema, Blueprint } from '@framework/database/schema';
|
|
36
|
+
import { Migration } from '@framework/database/migrations';
|
|
37
|
+
|
|
38
|
+
export class ${className} implements Migration {
|
|
39
|
+
/**
|
|
40
|
+
* Run the migrations.
|
|
41
|
+
*/
|
|
42
|
+
async up(): Promise<void> {
|
|
43
|
+
// await Schema.create('table_name', (table: Blueprint) => {
|
|
44
|
+
// table.id();
|
|
45
|
+
// table.timestamps();
|
|
46
|
+
// });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reverse the migrations.
|
|
51
|
+
*/
|
|
52
|
+
async down(): Promise<void> {
|
|
53
|
+
// await Schema.dropIfExists('table_name');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from '../../cli/Command';
|
|
2
|
+
import { Migrator } from '../migrations/Migrator';
|
|
3
|
+
import { DatabaseMigrationRepository } from '../migrations/DatabaseMigrationRepository';
|
|
4
|
+
import { DatabaseManager } from '../DatabaseManager';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
export class MigrateCommand extends Command {
|
|
8
|
+
signature = 'migrate';
|
|
9
|
+
description = 'Run the database migrations';
|
|
10
|
+
|
|
11
|
+
async handle(args: any): Promise<void> {
|
|
12
|
+
const { migrator, connection } = await this.getMigratorAndConnection();
|
|
13
|
+
try {
|
|
14
|
+
await connection.connect();
|
|
15
|
+
await migrator.run();
|
|
16
|
+
} finally {
|
|
17
|
+
await connection.disconnect();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected async getMigratorAndConnection() {
|
|
22
|
+
// This is a bit hacky, normally we'd usage DI container.
|
|
23
|
+
const { default: databaseConfig } = await import(join(process.cwd(), 'config', 'database'));
|
|
24
|
+
const db = new DatabaseManager(databaseConfig);
|
|
25
|
+
const connection = db.connection(); // default connection
|
|
26
|
+
|
|
27
|
+
const repository = new DatabaseMigrationRepository(connection, 'migrations');
|
|
28
|
+
const paths = [join(process.cwd(), 'database', 'migrations')];
|
|
29
|
+
|
|
30
|
+
return { migrator: new Migrator(repository, connection, paths), connection };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from '../../cli/Command';
|
|
2
|
+
import { Migrator } from '../migrations/Migrator';
|
|
3
|
+
import { DatabaseMigrationRepository } from '../migrations/DatabaseMigrationRepository';
|
|
4
|
+
import { DatabaseManager } from '../DatabaseManager';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
export class MigrateResetCommand extends Command {
|
|
8
|
+
signature = 'migrate:reset';
|
|
9
|
+
description = 'Rollback all database migrations';
|
|
10
|
+
|
|
11
|
+
async handle(args: any): Promise<void> {
|
|
12
|
+
const { migrator, connection } = await this.getMigratorAndConnection();
|
|
13
|
+
try {
|
|
14
|
+
await connection.connect();
|
|
15
|
+
await migrator.reset();
|
|
16
|
+
} finally {
|
|
17
|
+
await connection.disconnect();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected async getMigratorAndConnection() {
|
|
22
|
+
const { default: databaseConfig } = await import(join(process.cwd(), 'config', 'database'));
|
|
23
|
+
const db = new DatabaseManager(databaseConfig);
|
|
24
|
+
const connection = db.connection();
|
|
25
|
+
|
|
26
|
+
const repository = new DatabaseMigrationRepository(connection, 'migrations');
|
|
27
|
+
const paths = [join(process.cwd(), 'database', 'migrations')];
|
|
28
|
+
|
|
29
|
+
return { migrator: new Migrator(repository, connection, paths), connection };
|
|
30
|
+
}
|
|
31
|
+
}
|