@zerotal/orm 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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +58 -0
- package/src/casts/Cast.ts +200 -0
- package/src/commands/DbSeedCommand.ts +71 -0
- package/src/commands/MakeFactoryCommand.ts +59 -0
- package/src/commands/MakeMigrationCommand.ts +109 -0
- package/src/commands/MakeModelCommand.ts +83 -0
- package/src/commands/MakeSeederCommand.ts +50 -0
- package/src/commands/MigrateCommand.ts +60 -0
- package/src/commands/MigrateFreshCommand.ts +41 -0
- package/src/commands/MigrateGenerateCommand.ts +110 -0
- package/src/commands/MigrateRollbackCommand.ts +43 -0
- package/src/commands/MigrateStatusCommand.ts +49 -0
- package/src/commands/_loadMigrations.ts +34 -0
- package/src/commands/index.ts +30 -0
- package/src/config.ts +182 -0
- package/src/conventions.ts +67 -0
- package/src/db/DB.ts +486 -0
- package/src/db/NPlusOneDetector.ts +176 -0
- package/src/db/QueryBuilder.ts +2458 -0
- package/src/db/ReadWriteRouter.ts +96 -0
- package/src/db/TransactionContext.ts +13 -0
- package/src/db/dialects/MysqlDialect.ts +57 -0
- package/src/db/dialects/PostgresDialect.ts +55 -0
- package/src/db/dialects/SqliteDialect.ts +54 -0
- package/src/db/dialects/index.ts +25 -0
- package/src/db/dialects/types.ts +67 -0
- package/src/db/resolver.ts +30 -0
- package/src/db/sql-types.ts +12 -0
- package/src/db/types.ts +296 -0
- package/src/errors/MassAssignmentError.ts +25 -0
- package/src/errors/MigrationError.ts +18 -0
- package/src/errors/ModelNotFoundError.ts +21 -0
- package/src/errors/NPlusOneError.ts +6 -0
- package/src/errors/RelationNotLoadedError.ts +19 -0
- package/src/errors/StateError.ts +18 -0
- package/src/errors/TransactionError.ts +13 -0
- package/src/errors/UnsupportedDialectError.ts +18 -0
- package/src/errors/index.ts +7 -0
- package/src/events.ts +112 -0
- package/src/global.d.ts +17 -0
- package/src/implicitBinding.ts +73 -0
- package/src/index.ts +255 -0
- package/src/model/BaseModel.ts +2499 -0
- package/src/model/ModelQueryBuilder.ts +1808 -0
- package/src/model/Observer.ts +73 -0
- package/src/model/OrmContext.ts +71 -0
- package/src/model/ReactiveProxy.ts +53 -0
- package/src/model/SoftDeletes.ts +108 -0
- package/src/model/State.ts +290 -0
- package/src/model/decorators/_metadata.ts +211 -0
- package/src/model/decorators/_registerRelation.ts +20 -0
- package/src/model/decorators/belongsTo.ts +38 -0
- package/src/model/decorators/column.ts +278 -0
- package/src/model/decorators/hasMany.ts +34 -0
- package/src/model/decorators/hasManyThrough.ts +50 -0
- package/src/model/decorators/hasOne.ts +34 -0
- package/src/model/decorators/hasOneThrough.ts +40 -0
- package/src/model/decorators/manyToMany.ts +55 -0
- package/src/model/decorators/morphMany.ts +38 -0
- package/src/model/decorators/morphOne.ts +38 -0
- package/src/model/decorators/morphTo.ts +51 -0
- package/src/model/decorators/morphToMany.ts +49 -0
- package/src/model/decorators/morphedByMany.ts +46 -0
- package/src/model/decorators/table.ts +124 -0
- package/src/model/hooks/HookRegistry.ts +110 -0
- package/src/model/mixins.ts +536 -0
- package/src/model/payload.ts +114 -0
- package/src/model/relations/RelationRegistry.ts +184 -0
- package/src/observability.ts +210 -0
- package/src/provider/DatabaseProvider.ts +266 -0
- package/src/schema/Blueprint.ts +900 -0
- package/src/schema/ColumnDefinition.ts +517 -0
- package/src/schema/Migration.ts +34 -0
- package/src/schema/MigrationCodegen.ts +108 -0
- package/src/schema/MigrationRunner.ts +351 -0
- package/src/schema/ModelInspector.ts +133 -0
- package/src/schema/Schema.ts +140 -0
- package/src/schema/SchemaDiffer.ts +137 -0
- package/src/schema/SchemaInspector.ts +164 -0
- package/src/schema/__test_migrations__/001_create_test_table.ts +15 -0
- package/src/schema/autoMigrate.ts +154 -0
- package/src/schema/index.ts +28 -0
- package/src/seeding/Seeder.ts +46 -0
- package/src/support/identifiers.ts +62 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command, Str } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
export type MigrationFileResult = {
|
|
4
|
+
path: string;
|
|
5
|
+
created: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a migration name (e.g. `create_posts_table`) into a PascalCase
|
|
10
|
+
* class name (e.g. `CreatePostsTable`) for use in the generated stub.
|
|
11
|
+
*/
|
|
12
|
+
export function toMigrationClassName(name: string): string {
|
|
13
|
+
return Str.pascalCase(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the source text of a blank migration file whose default-exported
|
|
18
|
+
* class is named `className`.
|
|
19
|
+
*/
|
|
20
|
+
export function migrationStub(className: string): string {
|
|
21
|
+
return `import { Migration, Schema } from '@zerotal/orm';
|
|
22
|
+
|
|
23
|
+
export default class ${className} extends Migration {
|
|
24
|
+
override async up(): Promise<void> {
|
|
25
|
+
await Schema.create('table_name', (table) => {
|
|
26
|
+
table.increments('id');
|
|
27
|
+
table.timestamps();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override async down(): Promise<void> {
|
|
32
|
+
await Schema.drop('table_name');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lists the existing `database/migrations/*.ts` file paths, used to derive the
|
|
40
|
+
* next zero-padded numeric prefix.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
async function listMigrationFiles(): Promise<string[]> {
|
|
45
|
+
const glob = new Bun.Glob("database/migrations/*.ts");
|
|
46
|
+
const files: string[] = [];
|
|
47
|
+
for await (const file of glob.scan({ cwd: process.cwd() })) {
|
|
48
|
+
files.push(file);
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Writes a new numbered migration file for `name` under `database/migrations/`,
|
|
55
|
+
* returning its path and whether it was created (`false` if one already exists).
|
|
56
|
+
*/
|
|
57
|
+
export async function createMigrationFile(name: string): Promise<MigrationFileResult> {
|
|
58
|
+
const files = await listMigrationFiles();
|
|
59
|
+
const nextNumber = files.length + 1;
|
|
60
|
+
const prefix = String(nextNumber).padStart(3, "0");
|
|
61
|
+
const fileName = `${prefix}_${name}.ts`;
|
|
62
|
+
const path = `database/migrations/${fileName}`;
|
|
63
|
+
|
|
64
|
+
if (await Bun.file(path).exists()) {
|
|
65
|
+
return { path, created: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Bun.write() creates any missing parent directories, so no mkdir is needed.
|
|
69
|
+
await Bun.write(path, migrationStub(toMigrationClassName(name)));
|
|
70
|
+
return { path, created: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Scaffolds a new migration file (`bun zt make:migration`).
|
|
75
|
+
*
|
|
76
|
+
* Writes a blank migration with `up()`/`down()` stubs to
|
|
77
|
+
* `database/migrations/`, prefixed with the next zero-padded sequence number.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```bash
|
|
81
|
+
* bun zt make:migration create_posts_table
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @category Scaffolding (make:*)
|
|
85
|
+
*/
|
|
86
|
+
export class MakeMigrationCommand extends Command {
|
|
87
|
+
static commandName = "make:migration";
|
|
88
|
+
static description = "Create a new migration file";
|
|
89
|
+
static needsApp = false;
|
|
90
|
+
static args = [
|
|
91
|
+
{
|
|
92
|
+
name: "name",
|
|
93
|
+
required: true,
|
|
94
|
+
description: "Migration name (e.g. create_posts_table)",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
async run(): Promise<void> {
|
|
99
|
+
const name = this.args["name"]!;
|
|
100
|
+
const result = await createMigrationFile(name);
|
|
101
|
+
|
|
102
|
+
if (!result.created) {
|
|
103
|
+
this.error(`File already exists: ${result.path}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.info(`Created: ${result.path}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Command, Str } from "@zerotal/core";
|
|
2
|
+
import { createMigrationFile } from "./MakeMigrationCommand.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the source text of a model class named `name` bound to `tableName`.
|
|
6
|
+
*/
|
|
7
|
+
export function modelStub(name: string, tableName: string): string {
|
|
8
|
+
return `import { BaseModel, column, table } from '@zerotal/orm';
|
|
9
|
+
|
|
10
|
+
@table('${tableName}').withTimestamps()
|
|
11
|
+
export class ${name} extends BaseModel {
|
|
12
|
+
// Models guard every attribute by default. List the columns that may be
|
|
13
|
+
// mass-assigned from user input via create() / fill().
|
|
14
|
+
static override fillable: string[] = ['name'];
|
|
15
|
+
|
|
16
|
+
@column() name!: string;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scaffolds a new model class (`bun zt make:model`).
|
|
23
|
+
*
|
|
24
|
+
* Writes an `app/models/<Name>.ts` file containing a `BaseModel` subclass whose
|
|
25
|
+
* table name is the snake-cased model name. Passing `--migration` (`-m`) also
|
|
26
|
+
* generates a matching `create_<name>_table` migration.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```bash
|
|
30
|
+
* bun zt make:model Post
|
|
31
|
+
* bun zt make:model Post --migration
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @category Scaffolding (make:*)
|
|
35
|
+
*/
|
|
36
|
+
export class MakeModelCommand extends Command {
|
|
37
|
+
static commandName = "make:model";
|
|
38
|
+
static description = "Create a new model class";
|
|
39
|
+
static needsApp = false;
|
|
40
|
+
static args = [
|
|
41
|
+
{
|
|
42
|
+
name: "name",
|
|
43
|
+
required: true,
|
|
44
|
+
description: "Model name (e.g. User)",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
static flags = [
|
|
48
|
+
{
|
|
49
|
+
name: "migration",
|
|
50
|
+
short: "m",
|
|
51
|
+
type: "boolean" as const,
|
|
52
|
+
description: "Also create a migration file",
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
async run(): Promise<void> {
|
|
58
|
+
const name = this.args["name"]!;
|
|
59
|
+
const path = `app/models/${name}.ts`;
|
|
60
|
+
|
|
61
|
+
if (await Bun.file(path).exists()) {
|
|
62
|
+
this.error(`File already exists: ${path}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tableName = Str.snakeCase(name);
|
|
67
|
+
|
|
68
|
+
// Bun.write() creates any missing parent directories, so no mkdir is needed.
|
|
69
|
+
await Bun.write(path, modelStub(name, tableName));
|
|
70
|
+
this.info(`Created: ${path}`);
|
|
71
|
+
|
|
72
|
+
const withMigration = this.flags["migration"] as boolean;
|
|
73
|
+
if (withMigration) {
|
|
74
|
+
const migrationName = `create_${Str.snakeCase(name)}_table`;
|
|
75
|
+
const result = await createMigrationFile(migrationName);
|
|
76
|
+
if (!result.created) {
|
|
77
|
+
this.error(`File already exists: ${result.path}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.info(`Created: ${result.path}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scaffolds a new database seeder class (`bun zt make:seeder`).
|
|
5
|
+
*
|
|
6
|
+
* Writes `database/seeders/<Name>.ts` containing a `Seeder` subclass with an
|
|
7
|
+
* empty `run()` method to be filled in with seeding logic.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```bash
|
|
11
|
+
* bun zt make:seeder UserSeeder
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @category Scaffolding (make:*)
|
|
15
|
+
*/
|
|
16
|
+
export class MakeSeederCommand extends Command {
|
|
17
|
+
static commandName = "make:seeder";
|
|
18
|
+
static description = "Create a new database seeder class";
|
|
19
|
+
static needsApp = false;
|
|
20
|
+
static args = [
|
|
21
|
+
{ name: "name", required: true, description: "Seeder class name (e.g. UserSeeder)" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async run(): Promise<void> {
|
|
25
|
+
const name = this.args["name"]!;
|
|
26
|
+
const path = `database/seeders/${name}.ts`;
|
|
27
|
+
|
|
28
|
+
if (await Bun.file(path).exists()) {
|
|
29
|
+
this.error(`File already exists: ${path}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await Bun.write(path, seederStub(name));
|
|
34
|
+
this.info(`Created: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the source text of a seeder class file named `name`.
|
|
40
|
+
*/
|
|
41
|
+
export function seederStub(name: string): string {
|
|
42
|
+
return `import { Seeder } from '@zerotal/orm';
|
|
43
|
+
|
|
44
|
+
export class ${name} extends Seeder {
|
|
45
|
+
async run(): Promise<void> {
|
|
46
|
+
// Seed your database here
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
import type { MigrationEntry } from "../schema/MigrationRunner.ts";
|
|
3
|
+
import { MigrationRunner } from "../schema/MigrationRunner.ts";
|
|
4
|
+
import { _getConnection } from "../db/DB.ts";
|
|
5
|
+
import { loadMigrations } from "./_loadMigrations.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs all pending database migrations (`bun zt migrate`).
|
|
9
|
+
*
|
|
10
|
+
* Loads every migration under `database/migrations/`, then applies those not
|
|
11
|
+
* yet run. Passing `--fresh` first drops all tables and re-runs every
|
|
12
|
+
* migration from scratch.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```bash
|
|
16
|
+
* bun zt migrate
|
|
17
|
+
* bun zt migrate --fresh
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @category Migrations
|
|
21
|
+
*/
|
|
22
|
+
export class MigrateCommand extends Command {
|
|
23
|
+
static commandName = "migrate";
|
|
24
|
+
static aliases = ["db:migrate"];
|
|
25
|
+
static description = "Run all pending database migrations";
|
|
26
|
+
static needsApp = true;
|
|
27
|
+
static flags = [
|
|
28
|
+
{
|
|
29
|
+
name: "fresh",
|
|
30
|
+
type: "boolean" as const,
|
|
31
|
+
description: "Drop all tables and re-run all migrations",
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
async run(): Promise<void> {
|
|
37
|
+
const fresh = this.flags["fresh"] as boolean;
|
|
38
|
+
const records = await loadMigrations();
|
|
39
|
+
const entries: MigrationEntry[] = records.map((r) => ({
|
|
40
|
+
name: r.name,
|
|
41
|
+
migration: r.instance,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const runner = new MigrationRunner({ connection: _getConnection() });
|
|
45
|
+
|
|
46
|
+
if (fresh) {
|
|
47
|
+
await runner.reset(entries);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ran = await runner.run(entries);
|
|
51
|
+
|
|
52
|
+
if (ran.length === 0) {
|
|
53
|
+
this.info("Nothing to migrate.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.info(`Migrated ${ran.length} migration(s).`);
|
|
58
|
+
this.table(ran.map((name) => [name, "ran"]));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
import type { MigrationEntry } from "../schema/MigrationRunner.ts";
|
|
3
|
+
import { MigrationRunner } from "../schema/MigrationRunner.ts";
|
|
4
|
+
import { _getConnection } from "../db/DB.ts";
|
|
5
|
+
import { loadMigrations } from "./_loadMigrations.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Rolls back every migration, then re-runs them from scratch (`bun zt migrate:fresh`).
|
|
9
|
+
*
|
|
10
|
+
* Resets the database by reversing all applied migrations and then re-applying
|
|
11
|
+
* the full set, giving a clean, fully-migrated schema in one step.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* bun zt migrate:fresh
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @category Migrations
|
|
19
|
+
*/
|
|
20
|
+
export class MigrateFreshCommand extends Command {
|
|
21
|
+
static commandName = "migrate:fresh";
|
|
22
|
+
static description = "Roll back every migration, then re-run them from scratch";
|
|
23
|
+
static needsApp = true;
|
|
24
|
+
|
|
25
|
+
async run(): Promise<void> {
|
|
26
|
+
const records = await loadMigrations();
|
|
27
|
+
const entries: MigrationEntry[] = records.map((r) => ({
|
|
28
|
+
name: r.name,
|
|
29
|
+
migration: r.instance,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const runner = new MigrationRunner({ connection: _getConnection() });
|
|
33
|
+
await runner.reset(entries);
|
|
34
|
+
const ran = await runner.run(entries);
|
|
35
|
+
|
|
36
|
+
this.info(`Database refreshed — ran ${ran.length} migration(s).`);
|
|
37
|
+
if (ran.length > 0) {
|
|
38
|
+
this.table(ran.map((name) => [name, "migrated"]));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Command } from "@zerotal/core";
|
|
3
|
+
import { ModelInspector } from "../schema/ModelInspector.ts";
|
|
4
|
+
import { SchemaDiffer } from "../schema/SchemaDiffer.ts";
|
|
5
|
+
import { generateMigrationContent } from "../schema/MigrationCodegen.ts";
|
|
6
|
+
import { toMigrationClassName } from "./MakeMigrationCommand.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lists the existing `database/migrations/*.ts` file paths, used to derive the
|
|
10
|
+
* next zero-padded numeric prefix for the generated migration.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
async function listMigrationFiles(): Promise<string[]> {
|
|
15
|
+
const glob = new Bun.Glob("database/migrations/*.ts");
|
|
16
|
+
const files: string[] = [];
|
|
17
|
+
for await (const file of glob.scan({ cwd: process.cwd() })) {
|
|
18
|
+
files.push(file);
|
|
19
|
+
}
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Auto-generates a migration from model schema changes (`bun zt migrate:generate`).
|
|
25
|
+
*
|
|
26
|
+
* Loads the model files matching the `--models` glob, diffs their `@column()`
|
|
27
|
+
* schemas against the live database, and writes a numbered migration under
|
|
28
|
+
* `database/migrations/` describing the new tables and columns. Reports "No
|
|
29
|
+
* schema changes detected" when the models and database already agree.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```bash
|
|
33
|
+
* bun zt migrate:generate create_posts_table
|
|
34
|
+
* bun zt migrate:generate add_slug_to_posts --models "app/models/**\/*.ts"
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @category Migrations
|
|
38
|
+
*/
|
|
39
|
+
export class MigrateGenerateCommand extends Command {
|
|
40
|
+
static override commandName = "migrate:generate";
|
|
41
|
+
static override description = "Auto-generate a migration from model schema changes";
|
|
42
|
+
static override needsApp = false;
|
|
43
|
+
|
|
44
|
+
static override args = [
|
|
45
|
+
{
|
|
46
|
+
name: "name",
|
|
47
|
+
required: true,
|
|
48
|
+
description: "Migration name (e.g. create_posts_table)",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
static override flags = [
|
|
53
|
+
{
|
|
54
|
+
name: "models",
|
|
55
|
+
type: "string" as const,
|
|
56
|
+
description: "Glob pattern for model files to inspect",
|
|
57
|
+
default: "app/models/**/*.ts",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
async run(): Promise<void> {
|
|
62
|
+
const name = this.args["name"]!;
|
|
63
|
+
const modelsPattern = (this.flags["models"] as string | undefined) ?? "app/models/**/*.ts";
|
|
64
|
+
|
|
65
|
+
// ── 1. Import model files ────────────────────────────────────────────────
|
|
66
|
+
this.dim(`Scanning models: ${modelsPattern}`);
|
|
67
|
+
await ModelInspector.load(modelsPattern);
|
|
68
|
+
|
|
69
|
+
const schemas = ModelInspector.all();
|
|
70
|
+
if (schemas.length === 0) {
|
|
71
|
+
this.warn(`No models found matching: ${modelsPattern}`);
|
|
72
|
+
this.dim("Ensure models use @column() decorators and set static table.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.dim(`Found ${schemas.length} model(s)`);
|
|
76
|
+
|
|
77
|
+
// ── 2. Diff against live DB ──────────────────────────────────────────────
|
|
78
|
+
const diff = await SchemaDiffer.diff(schemas);
|
|
79
|
+
|
|
80
|
+
if (SchemaDiffer.isEmpty(diff)) {
|
|
81
|
+
this.info("No schema changes detected — nothing to generate.");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── 3. Generate migration content ────────────────────────────────────────
|
|
86
|
+
const className = toMigrationClassName(name);
|
|
87
|
+
const content = generateMigrationContent(className, diff);
|
|
88
|
+
|
|
89
|
+
// ── 4. Write the migration file ──────────────────────────────────────────
|
|
90
|
+
const existing = await listMigrationFiles();
|
|
91
|
+
const nextNumber = existing.length + 1;
|
|
92
|
+
const prefix = String(nextNumber).padStart(3, "0");
|
|
93
|
+
const filePath = path.join("database", "migrations", `${prefix}_${name}.ts`);
|
|
94
|
+
const absPath = path.resolve(process.cwd(), filePath);
|
|
95
|
+
|
|
96
|
+
// Bun.write() creates any missing parent directories, so no mkdir is needed.
|
|
97
|
+
await Bun.write(absPath, content);
|
|
98
|
+
|
|
99
|
+
// ── 5. Report ────────────────────────────────────────────────────────────
|
|
100
|
+
this.info(`Generated: ${filePath}`);
|
|
101
|
+
|
|
102
|
+
if (diff.newTables.length > 0) {
|
|
103
|
+
this.line(` + New tables: ${diff.newTables.map((t) => t.schema.table).join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
if (diff.newColumns.length > 0) {
|
|
106
|
+
const cols = diff.newColumns.map((c) => `${c.table}.${c.column.name}`);
|
|
107
|
+
this.line(` + New columns: ${cols.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
import type { MigrationEntry } from "../schema/MigrationRunner.ts";
|
|
3
|
+
import { MigrationRunner } from "../schema/MigrationRunner.ts";
|
|
4
|
+
import { _getConnection } from "../db/DB.ts";
|
|
5
|
+
import { loadMigrations } from "./_loadMigrations.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Rolls back the most recent migration batch (`bun zt migrate:rollback`).
|
|
9
|
+
*
|
|
10
|
+
* Reverses the migrations applied in the last batch by calling their `down()`
|
|
11
|
+
* methods; reports "Nothing to roll back." when no batch remains.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* bun zt migrate:rollback
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @category Migrations
|
|
19
|
+
*/
|
|
20
|
+
export class MigrateRollbackCommand extends Command {
|
|
21
|
+
static commandName = "migrate:rollback";
|
|
22
|
+
static description = "Roll back the most recent migration batch";
|
|
23
|
+
static needsApp = true;
|
|
24
|
+
|
|
25
|
+
async run(): Promise<void> {
|
|
26
|
+
const records = await loadMigrations();
|
|
27
|
+
const entries: MigrationEntry[] = records.map((r) => ({
|
|
28
|
+
name: r.name,
|
|
29
|
+
migration: r.instance,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const runner = new MigrationRunner({ connection: _getConnection() });
|
|
33
|
+
const rolledBack = await runner.rollback(entries);
|
|
34
|
+
|
|
35
|
+
if (rolledBack.length === 0) {
|
|
36
|
+
this.info("Nothing to roll back.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.info(`Rolled back ${rolledBack.length} migration(s).`);
|
|
41
|
+
this.table(rolledBack.map((name) => [name, "rolled back"]));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from "@zerotal/core";
|
|
2
|
+
import type { MigrationEntry } from "../schema/MigrationRunner.ts";
|
|
3
|
+
import { MigrationRunner } from "../schema/MigrationRunner.ts";
|
|
4
|
+
import { _getConnection } from "../db/DB.ts";
|
|
5
|
+
import { loadMigrations } from "./_loadMigrations.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shows the status of each migration (`bun zt migrate:status`).
|
|
9
|
+
*
|
|
10
|
+
* Prints a table listing every known migration and whether it has run, along
|
|
11
|
+
* with its batch number and the timestamp it was applied.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* bun zt migrate:status
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @category Migrations
|
|
19
|
+
*/
|
|
20
|
+
export class MigrateStatusCommand extends Command {
|
|
21
|
+
static commandName = "migrate:status";
|
|
22
|
+
static description = "Show the status of each migration";
|
|
23
|
+
static needsApp = true;
|
|
24
|
+
|
|
25
|
+
async run(): Promise<void> {
|
|
26
|
+
const records = await loadMigrations();
|
|
27
|
+
const entries: MigrationEntry[] = records.map((r) => ({
|
|
28
|
+
name: r.name,
|
|
29
|
+
migration: r.instance,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const runner = new MigrationRunner({ connection: _getConnection() });
|
|
33
|
+
const statuses = await runner.status(entries);
|
|
34
|
+
|
|
35
|
+
const rows: [string, string][] = [
|
|
36
|
+
["name", "ran | batch | ranAt"],
|
|
37
|
+
...statuses.map((status): [string, string] => {
|
|
38
|
+
if (!status.ran) {
|
|
39
|
+
return [status.name, "no | - | -"];
|
|
40
|
+
}
|
|
41
|
+
const batch = status.batch ?? "-";
|
|
42
|
+
const ranAt = status.ranAt ? status.ranAt.toISOString() : "-";
|
|
43
|
+
return [status.name, `yes | ${batch} | ${ranAt}`];
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
this.table(rows);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MigrationRecord } from "../schema/MigrationRunner.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Globs `database/migrations/*.ts`, imports each file, and returns a
|
|
5
|
+
* {@link MigrationRecord} array of instantiated migrations. Files are sorted by
|
|
6
|
+
* filename so `001_` runs before `002_`; files without a default export are
|
|
7
|
+
* skipped.
|
|
8
|
+
*
|
|
9
|
+
* Shared by the `migrate`, `migrate:fresh`, `migrate:rollback`, and
|
|
10
|
+
* `migrate:status` commands.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export async function loadMigrations(): Promise<MigrationRecord[]> {
|
|
15
|
+
const glob = new Bun.Glob("database/migrations/*.ts");
|
|
16
|
+
const files: string[] = [];
|
|
17
|
+
|
|
18
|
+
for await (const file of glob.scan({ cwd: process.cwd() })) {
|
|
19
|
+
files.push(file);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
files.sort();
|
|
23
|
+
|
|
24
|
+
const records: MigrationRecord[] = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const mod = await import(`${process.cwd()}/${file}`);
|
|
27
|
+
const Ctor = mod.default as (new () => MigrationRecord["instance"]) | undefined;
|
|
28
|
+
if (!Ctor) continue;
|
|
29
|
+
const name = file.replace(/^database\/migrations\//, "").replace(/\.ts$/, "");
|
|
30
|
+
records.push({ name, instance: new Ctor() });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for the Zerotal ORM: database migrations and code scaffolding.
|
|
3
|
+
*
|
|
4
|
+
* This entry point bundles the `Command`-derived classes that back the
|
|
5
|
+
* ORM's `bun zt` sub-commands — running, refreshing, rolling back, and
|
|
6
|
+
* inspecting migrations, seeding the database, and generating models,
|
|
7
|
+
* migrations, factories, and seeders. Register these with the CLI runner to
|
|
8
|
+
* expose the `migrate:*`, `db:*`, and `make:*` commands in an application.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```bash
|
|
12
|
+
* # Run all pending migrations
|
|
13
|
+
* bun zt migrate
|
|
14
|
+
*
|
|
15
|
+
* # Scaffold a new model class
|
|
16
|
+
* bun zt make:model Post
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
export { MigrateCommand } from "./MigrateCommand.ts";
|
|
22
|
+
export { MigrateRollbackCommand } from "./MigrateRollbackCommand.ts";
|
|
23
|
+
export { MigrateStatusCommand } from "./MigrateStatusCommand.ts";
|
|
24
|
+
export { MigrateFreshCommand } from "./MigrateFreshCommand.ts";
|
|
25
|
+
export { MakeMigrationCommand } from "./MakeMigrationCommand.ts";
|
|
26
|
+
export { MigrateGenerateCommand } from "./MigrateGenerateCommand.ts";
|
|
27
|
+
export { MakeModelCommand } from "./MakeModelCommand.ts";
|
|
28
|
+
export { DbSeedCommand } from "./DbSeedCommand.ts";
|
|
29
|
+
export { MakeSeederCommand } from "./MakeSeederCommand.ts";
|
|
30
|
+
export { MakeFactoryCommand } from "./MakeFactoryCommand.ts";
|