@veap/database 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +176 -2
- package/dist/casts.d.ts +9 -0
- package/dist/casts.js +78 -0
- package/dist/connection.d.ts +49 -0
- package/dist/connection.js +189 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +7 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +161 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +21 -0
- package/dist/migration-provider.d.ts +5 -0
- package/dist/migration-provider.js +15 -0
- package/dist/migrations/generator.d.ts +4 -0
- package/dist/migrations/generator.js +58 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/runner.d.ts +12 -0
- package/dist/migrations/runner.js +178 -0
- package/dist/migrations/schema-builder.d.ts +84 -0
- package/dist/migrations/schema-builder.js +409 -0
- package/dist/model.d.ts +379 -0
- package/dist/model.js +899 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.js +58 -0
- package/dist/query-builder.d.ts +197 -0
- package/dist/query-builder.js +715 -0
- package/dist/relations/belongs-to-many.d.ts +15 -0
- package/dist/relations/belongs-to-many.js +80 -0
- package/dist/relations/belongs-to.d.ts +8 -0
- package/dist/relations/belongs-to.js +37 -0
- package/dist/relations/eager-loader.d.ts +5 -0
- package/dist/relations/eager-loader.js +66 -0
- package/dist/relations/has-many.d.ts +9 -0
- package/dist/relations/has-many.js +48 -0
- package/dist/relations/has-one.d.ts +8 -0
- package/dist/relations/has-one.js +37 -0
- package/dist/relations/index.d.ts +11 -0
- package/dist/relations/index.js +11 -0
- package/dist/relations/morph-many.d.ts +14 -0
- package/dist/relations/morph-many.js +65 -0
- package/dist/relations/morph-map.d.ts +32 -0
- package/dist/relations/morph-map.js +73 -0
- package/dist/relations/morph-one.d.ts +14 -0
- package/dist/relations/morph-one.js +65 -0
- package/dist/relations/morph-to-many.d.ts +44 -0
- package/dist/relations/morph-to-many.js +215 -0
- package/dist/relations/morph-to.d.ts +19 -0
- package/dist/relations/morph-to.js +90 -0
- package/dist/relations/relation.d.ts +22 -0
- package/dist/relations/relation.js +13 -0
- package/dist/scopes.d.ts +16 -0
- package/dist/scopes.js +1 -0
- package/dist/seeder.d.ts +25 -0
- package/dist/seeder.js +49 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +20 -0
- package/package.json +50 -4
package/dist/factory.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { DatabaseError } from "./errors";
|
|
2
|
+
export class Factory {
|
|
3
|
+
static get registry() {
|
|
4
|
+
const g = globalThis;
|
|
5
|
+
if (!g.__VEAP_FACTORY_REGISTRY__) {
|
|
6
|
+
g.__VEAP_FACTORY_REGISTRY__ = new Map();
|
|
7
|
+
}
|
|
8
|
+
return g.__VEAP_FACTORY_REGISTRY__;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Registers a factory class/instance for a model.
|
|
12
|
+
*/
|
|
13
|
+
static register(modelClass, factoryOrFactoryFn) {
|
|
14
|
+
this.registry.set(modelClass, factoryOrFactoryFn);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Gets the registered factory for a model if one exists.
|
|
18
|
+
*/
|
|
19
|
+
static getForModel(modelClass) {
|
|
20
|
+
return this.registry.get(modelClass);
|
|
21
|
+
}
|
|
22
|
+
modelClass;
|
|
23
|
+
countNumber = 1;
|
|
24
|
+
states = [];
|
|
25
|
+
sequenceItems = [];
|
|
26
|
+
constructor(modelClass) {
|
|
27
|
+
if (modelClass) {
|
|
28
|
+
this.modelClass = modelClass;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sets the number of models that should be generated.
|
|
33
|
+
*/
|
|
34
|
+
count(amount) {
|
|
35
|
+
this.countNumber = Math.max(1, amount);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Applies an attribute state transformation to the factory.
|
|
40
|
+
*/
|
|
41
|
+
state(override) {
|
|
42
|
+
this.states.push(override);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Applies sequential states rotating through the given items.
|
|
47
|
+
*/
|
|
48
|
+
sequence(...items) {
|
|
49
|
+
this.sequenceItems.push(...items);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generates the raw attributes dictionary for a single model instance.
|
|
54
|
+
*/
|
|
55
|
+
async raw(override = {}, index = 0) {
|
|
56
|
+
let attributes = await this.definition();
|
|
57
|
+
// Apply sequence if provided
|
|
58
|
+
if (this.sequenceItems.length > 0) {
|
|
59
|
+
const seqState = this.sequenceItems[index % this.sequenceItems.length];
|
|
60
|
+
attributes = { ...attributes, ...seqState };
|
|
61
|
+
}
|
|
62
|
+
// Apply states
|
|
63
|
+
for (const stateItem of this.states) {
|
|
64
|
+
if (typeof stateItem === "function") {
|
|
65
|
+
attributes = { ...attributes, ...stateItem(attributes) };
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
attributes = { ...attributes, ...stateItem };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Apply direct overrides
|
|
72
|
+
attributes = { ...attributes, ...override };
|
|
73
|
+
return attributes;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a single model instance in memory without saving to database.
|
|
77
|
+
*/
|
|
78
|
+
async makeOne(override = {}, index = 0) {
|
|
79
|
+
if (!this.modelClass) {
|
|
80
|
+
throw DatabaseError.Internal("[veap:Factory] Model class is not defined for this factory.");
|
|
81
|
+
}
|
|
82
|
+
const attributes = await this.raw(override, index);
|
|
83
|
+
return new this.modelClass(attributes, true);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Creates model instance(s) in memory without saving to database.
|
|
87
|
+
*/
|
|
88
|
+
async make(override = {}) {
|
|
89
|
+
if (this.countNumber === 1) {
|
|
90
|
+
return await this.makeOne(override, 0);
|
|
91
|
+
}
|
|
92
|
+
const instances = [];
|
|
93
|
+
for (let i = 0; i < this.countNumber; i++) {
|
|
94
|
+
instances.push(await this.makeOne(override, i));
|
|
95
|
+
}
|
|
96
|
+
return instances;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates and persists a single model instance to the database.
|
|
100
|
+
*/
|
|
101
|
+
async createOne(override = {}, index = 0) {
|
|
102
|
+
const instance = await this.makeOne(override, index);
|
|
103
|
+
await instance.save();
|
|
104
|
+
return instance;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates and persists model instance(s) to the database.
|
|
108
|
+
*/
|
|
109
|
+
async create(override = {}) {
|
|
110
|
+
if (this.countNumber === 1) {
|
|
111
|
+
return await this.createOne(override, 0);
|
|
112
|
+
}
|
|
113
|
+
const instances = [];
|
|
114
|
+
for (let i = 0; i < this.countNumber; i++) {
|
|
115
|
+
instances.push(await this.createOne(override, i));
|
|
116
|
+
}
|
|
117
|
+
return instances;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generic factory implementation used by defineFactory helper.
|
|
122
|
+
*/
|
|
123
|
+
class GenericFactory extends Factory {
|
|
124
|
+
definitionFn;
|
|
125
|
+
constructor(modelClass, definitionFn) {
|
|
126
|
+
super(modelClass);
|
|
127
|
+
this.definitionFn = definitionFn;
|
|
128
|
+
}
|
|
129
|
+
async definition() {
|
|
130
|
+
return await this.definitionFn();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Convenient helper to define a Model Factory in a single concise call.
|
|
135
|
+
*/
|
|
136
|
+
export function defineFactory(modelClass, definitionFn) {
|
|
137
|
+
const FactoryClass = class extends GenericFactory {
|
|
138
|
+
constructor() {
|
|
139
|
+
super(modelClass, definitionFn);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const factoryCallable = function (count) {
|
|
143
|
+
const instance = new FactoryClass();
|
|
144
|
+
if (count !== undefined) {
|
|
145
|
+
instance.count(count);
|
|
146
|
+
}
|
|
147
|
+
return instance;
|
|
148
|
+
};
|
|
149
|
+
// Attach static factory shortcuts
|
|
150
|
+
factoryCallable.count = (amount) => factoryCallable().count(amount);
|
|
151
|
+
factoryCallable.state = (override) => factoryCallable().state(override);
|
|
152
|
+
factoryCallable.sequence = (...items) => factoryCallable().sequence(...items);
|
|
153
|
+
factoryCallable.make = (override) => factoryCallable().make(override);
|
|
154
|
+
factoryCallable.makeOne = (override) => factoryCallable().makeOne(override);
|
|
155
|
+
factoryCallable.create = (override) => factoryCallable().create(override);
|
|
156
|
+
factoryCallable.createOne = (override) => factoryCallable().createOne(override);
|
|
157
|
+
// Allow `new UserFactory()`
|
|
158
|
+
Object.setPrototypeOf(factoryCallable, FactoryClass);
|
|
159
|
+
factoryCallable.prototype = FactoryClass.prototype;
|
|
160
|
+
return factoryCallable;
|
|
161
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as knex, type Knex } from "knex";
|
|
2
|
+
export * from "./casts";
|
|
3
|
+
export * from "./connection";
|
|
4
|
+
export * from "./errors";
|
|
5
|
+
export * from "./events";
|
|
6
|
+
export * from "./factory";
|
|
7
|
+
export * from "./logger";
|
|
8
|
+
export * from "./migration-provider";
|
|
9
|
+
export * from "./migrations/index";
|
|
10
|
+
export * from "./model";
|
|
11
|
+
export * from "./provider";
|
|
12
|
+
export * from "./query-builder";
|
|
13
|
+
export * from "./relations/index";
|
|
14
|
+
export * from "./scopes";
|
|
15
|
+
export * from "./seeder";
|
|
16
|
+
export * from "./types";
|
|
17
|
+
export * from "./utils";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as knex } from "knex";
|
|
2
|
+
export * from "./casts";
|
|
3
|
+
export * from "./connection";
|
|
4
|
+
export * from "./errors";
|
|
5
|
+
export * from "./events";
|
|
6
|
+
export * from "./factory";
|
|
7
|
+
export * from "./logger";
|
|
8
|
+
export * from "./migration-provider";
|
|
9
|
+
export * from "./migrations/index";
|
|
10
|
+
export * from "./model";
|
|
11
|
+
export * from "./provider";
|
|
12
|
+
export * from "./query-builder";
|
|
13
|
+
export * from "./relations/index";
|
|
14
|
+
export * from "./scopes";
|
|
15
|
+
export * from "./seeder";
|
|
16
|
+
export * from "./types";
|
|
17
|
+
export * from "./utils";
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DatabaseLogger {
|
|
2
|
+
debug(channel: string, message: string, ...args: any[]): void;
|
|
3
|
+
info(channel: string, message: string, ...args: any[]): void;
|
|
4
|
+
warn(channel: string, message: string, ...args: any[]): void;
|
|
5
|
+
error(channel: string, message: string, ...args: any[]): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function setDatabaseLogger(logger: DatabaseLogger): void;
|
|
8
|
+
export declare function debug(channel: string, message: string, ...args: any[]): void;
|
|
9
|
+
export declare function info(channel: string, message: string, ...args: any[]): void;
|
|
10
|
+
export declare function warn(channel: string, message: string, ...args: any[]): void;
|
|
11
|
+
export declare function error(channel: string, message: string, ...args: any[]): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let activeLogger = {
|
|
2
|
+
debug: (_channel, _message, ..._args) => { },
|
|
3
|
+
info: (channel, message, ..._args) => console.info(`[${channel}] ${message}`),
|
|
4
|
+
warn: (channel, message, ..._args) => console.warn(`[${channel}] ${message}`),
|
|
5
|
+
error: (channel, message, ..._args) => console.error(`[${channel}] ${message}`),
|
|
6
|
+
};
|
|
7
|
+
export function setDatabaseLogger(logger) {
|
|
8
|
+
activeLogger = logger;
|
|
9
|
+
}
|
|
10
|
+
export function debug(channel, message, ...args) {
|
|
11
|
+
activeLogger.debug(channel, message, ...args);
|
|
12
|
+
}
|
|
13
|
+
export function info(channel, message, ...args) {
|
|
14
|
+
activeLogger.info(channel, message, ...args);
|
|
15
|
+
}
|
|
16
|
+
export function warn(channel, message, ...args) {
|
|
17
|
+
activeLogger.warn(channel, message, ...args);
|
|
18
|
+
}
|
|
19
|
+
export function error(channel, message, ...args) {
|
|
20
|
+
activeLogger.error(channel, message, ...args);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ServiceProvider } from "@veap/kernel";
|
|
2
|
+
import { runMigrations } from "./migrations/runner";
|
|
3
|
+
export class MigrationServiceProvider extends ServiceProvider {
|
|
4
|
+
register() {
|
|
5
|
+
// No bindings needed
|
|
6
|
+
}
|
|
7
|
+
async boot() {
|
|
8
|
+
if (this.container.has("AppMigrations")) {
|
|
9
|
+
const appMigrations = await this.container.resolve("AppMigrations");
|
|
10
|
+
if (appMigrations?.length) {
|
|
11
|
+
await runMigrations("app", appMigrations);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const stub = `import type { Schema } from "@veap/database";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "{{name}}",
|
|
7
|
+
async up(_db: any, schema: Schema) {
|
|
8
|
+
await schema.createTable("{{tableName}}", (table) => {
|
|
9
|
+
table.increments("id");
|
|
10
|
+
|
|
11
|
+
table.timestamps();
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
async down(_db: any, schema: Schema) {
|
|
15
|
+
await schema.dropTableIfExists("{{tableName}}");
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
`;
|
|
19
|
+
async function updateIndexFile(migrationsDir) {
|
|
20
|
+
const indexFile = path.join(migrationsDir, "index.ts");
|
|
21
|
+
const files = await fs.readdir(migrationsDir);
|
|
22
|
+
const migrationFiles = files
|
|
23
|
+
.filter((f) => f.endsWith(".ts") && f !== "index.ts")
|
|
24
|
+
.sort();
|
|
25
|
+
const imports = [];
|
|
26
|
+
const exports = [];
|
|
27
|
+
for (const file of migrationFiles) {
|
|
28
|
+
const name = file.replace(".ts", "");
|
|
29
|
+
const varName = `m${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
30
|
+
imports.push(`import { default as ${varName} } from "./${name}";`);
|
|
31
|
+
exports.push(varName);
|
|
32
|
+
}
|
|
33
|
+
const content = `// This file is auto-generated by Veap CLI. Do not edit manually.
|
|
34
|
+
${imports.join("\n")}
|
|
35
|
+
|
|
36
|
+
export const appMigrations = [
|
|
37
|
+
${exports.join(",\n ")}
|
|
38
|
+
];
|
|
39
|
+
`;
|
|
40
|
+
await fs.writeFile(indexFile, content, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
export async function generateMigration(name, targetDir) {
|
|
43
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
44
|
+
const fileName = `${timestamp}_${name}`;
|
|
45
|
+
let tableName = name;
|
|
46
|
+
if (name.startsWith("create_") && name.endsWith("_table")) {
|
|
47
|
+
tableName = name.replace("create_", "").replace("_table", "");
|
|
48
|
+
}
|
|
49
|
+
const content = stub
|
|
50
|
+
.replace(/{{name}}/g, fileName)
|
|
51
|
+
.replace(/{{tableName}}/g, tableName);
|
|
52
|
+
const migrationsDir = targetDir || path.join(process.cwd(), "migrations");
|
|
53
|
+
await fs.mkdir(migrationsDir, { recursive: true });
|
|
54
|
+
const filePath = path.join(migrationsDir, `${fileName}.ts`);
|
|
55
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
56
|
+
await updateIndexFile(migrationsDir);
|
|
57
|
+
return { fileName, filePath };
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Schema } from "./schema-builder";
|
|
2
|
+
export interface Migration {
|
|
3
|
+
name: string;
|
|
4
|
+
up: (db: any, schema: Schema) => Promise<void>;
|
|
5
|
+
down?: (db: any, schema: Schema) => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function runMigrations(scope: string, migrations: Migration[]): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Modern static migration rollback runner.
|
|
10
|
+
* Reverts migrations in reverse order.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rollbackMigrations(scope: string, migrations: Migration[]): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { getKnex } from "../connection";
|
|
3
|
+
import { debug, warn } from "../logger";
|
|
4
|
+
import { Schema } from "./schema-builder";
|
|
5
|
+
async function ensureMigrationsTable(knex = getKnex()) {
|
|
6
|
+
try {
|
|
7
|
+
const hasTable = await knex.schema.hasTable("migrations");
|
|
8
|
+
if (!hasTable) {
|
|
9
|
+
await knex.schema.createTable("migrations", (table) => {
|
|
10
|
+
table.text("id").primary();
|
|
11
|
+
table.text("name").notNullable();
|
|
12
|
+
table.integer("batch").notNullable();
|
|
13
|
+
table.timestamp("executed_at").defaultTo(knex.fn.now()).notNullable();
|
|
14
|
+
table.text("scope").notNullable().defaultTo("global");
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const hasScope = await knex.schema.hasColumn("migrations", "scope");
|
|
19
|
+
if (!hasScope) {
|
|
20
|
+
await knex.schema.alterTable("migrations", (table) => {
|
|
21
|
+
table.text("scope").notNullable().defaultTo("global");
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
warn("veap:migration", "Failed to ensure migrations table:", error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a compatibility DB adapter for migrations that provides `.execute(query)`
|
|
32
|
+
* wrapping `trx.raw(query)`, while also being directly callable as a Knex query builder
|
|
33
|
+
* on the active migration transaction `trx(tableName)`.
|
|
34
|
+
*/
|
|
35
|
+
function createMigrationDbAdapter(trx) {
|
|
36
|
+
const adapter = (tableName) => trx(tableName);
|
|
37
|
+
adapter.raw = (query, ...bindings) => trx.raw(query, ...bindings);
|
|
38
|
+
adapter.execute = (query) => {
|
|
39
|
+
const sqlString = typeof query === "string"
|
|
40
|
+
? query
|
|
41
|
+
: query?.query || query?.text || String(query);
|
|
42
|
+
return trx.raw(sqlString);
|
|
43
|
+
};
|
|
44
|
+
return new Proxy(adapter, {
|
|
45
|
+
get(target, prop, receiver) {
|
|
46
|
+
if (prop in target) {
|
|
47
|
+
return Reflect.get(target, prop, receiver);
|
|
48
|
+
}
|
|
49
|
+
return Reflect.get(trx, prop);
|
|
50
|
+
},
|
|
51
|
+
apply(target, thisArg, argArray) {
|
|
52
|
+
return trx(...argArray);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function runMigrations(scope, migrations) {
|
|
57
|
+
if (migrations.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
let knex;
|
|
60
|
+
try {
|
|
61
|
+
knex = getKnex();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!knex)
|
|
67
|
+
return;
|
|
68
|
+
await ensureMigrationsTable(knex);
|
|
69
|
+
await knex.transaction(async (tx) => {
|
|
70
|
+
// Acquire advisory lock (unique ID for migrations in Postgres)
|
|
71
|
+
const lockId = 123456;
|
|
72
|
+
try {
|
|
73
|
+
await tx.raw("SELECT pg_advisory_xact_lock(?)", [lockId]);
|
|
74
|
+
}
|
|
75
|
+
catch (_e) {
|
|
76
|
+
debug("veap:migration", "Could not acquire advisory lock, continuing...");
|
|
77
|
+
}
|
|
78
|
+
debug("veap:migration", `Running migrations for scope: ${scope}`);
|
|
79
|
+
// 1. Get executed migrations for this scope
|
|
80
|
+
const executed = await tx("migrations")
|
|
81
|
+
.select("name")
|
|
82
|
+
.where("scope", scope);
|
|
83
|
+
const executedNames = executed.map((m) => m.name);
|
|
84
|
+
// 2. Find pending migrations
|
|
85
|
+
const pending = migrations.filter((m) => !executedNames.includes(m.name));
|
|
86
|
+
if (pending.length === 0) {
|
|
87
|
+
debug("veap:migration", `No new migrations for ${scope}.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// 3. Get next batch number
|
|
91
|
+
const batchResult = await tx("migrations").max("batch as maxBatch").first();
|
|
92
|
+
const batch = (Number(batchResult?.maxBatch) || 0) + 1;
|
|
93
|
+
const dbAdapter = createMigrationDbAdapter(tx);
|
|
94
|
+
const schema = new Schema(tx);
|
|
95
|
+
// 4. Execute pending
|
|
96
|
+
for (const migration of pending) {
|
|
97
|
+
debug("veap:migration", `[${scope}] Running migration: ${migration.name}...`);
|
|
98
|
+
try {
|
|
99
|
+
await migration.up(dbAdapter, schema);
|
|
100
|
+
await tx("migrations").insert({
|
|
101
|
+
id: crypto.randomUUID(),
|
|
102
|
+
name: migration.name,
|
|
103
|
+
scope: scope,
|
|
104
|
+
batch: batch,
|
|
105
|
+
executed_at: new Date(),
|
|
106
|
+
});
|
|
107
|
+
debug("veap:migration", `[${scope}] Successfully ran ${migration.name}.`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
warn("veap:migration", `[${scope}] Failed to run ${migration.name}:`, error);
|
|
111
|
+
throw error; // Rollback the whole batch on error
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Modern static migration rollback runner.
|
|
118
|
+
* Reverts migrations in reverse order.
|
|
119
|
+
*/
|
|
120
|
+
export async function rollbackMigrations(scope, migrations) {
|
|
121
|
+
if (migrations.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
let knex;
|
|
124
|
+
try {
|
|
125
|
+
knex = getKnex();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!knex)
|
|
131
|
+
return;
|
|
132
|
+
await ensureMigrationsTable(knex);
|
|
133
|
+
await knex.transaction(async (tx) => {
|
|
134
|
+
// Acquire advisory lock
|
|
135
|
+
const lockId = 123456;
|
|
136
|
+
try {
|
|
137
|
+
await tx.raw("SELECT pg_advisory_xact_lock(?)", [lockId]);
|
|
138
|
+
}
|
|
139
|
+
catch (_e) {
|
|
140
|
+
debug("veap:migration", "Could not acquire advisory lock, continuing...");
|
|
141
|
+
}
|
|
142
|
+
// 1. Get executed migrations for this scope ordered by batch desc
|
|
143
|
+
const executed = await tx("migrations")
|
|
144
|
+
.select("name", "batch")
|
|
145
|
+
.where("scope", scope)
|
|
146
|
+
.orderBy("batch", "desc");
|
|
147
|
+
if (executed.length === 0) {
|
|
148
|
+
debug("veap:migration", `No migrations to rollback for ${scope}.`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Rollback the latest batch
|
|
152
|
+
const latestBatch = executed[0].batch;
|
|
153
|
+
const toRollbackNames = executed
|
|
154
|
+
.filter((m) => m.batch === latestBatch)
|
|
155
|
+
.map((m) => m.name);
|
|
156
|
+
const dbAdapter = createMigrationDbAdapter(tx);
|
|
157
|
+
const schema = new Schema(tx);
|
|
158
|
+
// Rollback in reverse order
|
|
159
|
+
for (const name of toRollbackNames) {
|
|
160
|
+
const migration = migrations.find((m) => m.name === name);
|
|
161
|
+
if (migration && migration.down) {
|
|
162
|
+
debug("veap:migration", `[${scope}] Rolling back migration: ${name}...`);
|
|
163
|
+
try {
|
|
164
|
+
await migration.down(dbAdapter, schema);
|
|
165
|
+
await tx("migrations")
|
|
166
|
+
.where("name", name)
|
|
167
|
+
.where("scope", scope)
|
|
168
|
+
.delete();
|
|
169
|
+
debug("veap:migration", `[${scope}] Rolled back ${name}.`);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
warn("veap:migration", `[${scope}] Failed to rollback ${name}:`, error);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight SQL helper for migrations, compatible with sql`...` template strings.
|
|
3
|
+
*/
|
|
4
|
+
export declare function sql(strings: TemplateStringsArray | string, ...values: any[]): any;
|
|
5
|
+
export declare namespace sql {
|
|
6
|
+
var raw: (str: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export type DatabaseDialect = "postgres" | "sqlite" | "mysql";
|
|
9
|
+
export declare class ColumnBuilder {
|
|
10
|
+
type: string;
|
|
11
|
+
name: string;
|
|
12
|
+
private _notNull;
|
|
13
|
+
private _primaryKey;
|
|
14
|
+
private _unique;
|
|
15
|
+
private _default;
|
|
16
|
+
private _defaultNow;
|
|
17
|
+
private _references;
|
|
18
|
+
private _onDelete;
|
|
19
|
+
private _onUpdate;
|
|
20
|
+
constructor(type: string, name: string);
|
|
21
|
+
notNull(): this;
|
|
22
|
+
primaryKey(): this;
|
|
23
|
+
unique(): this;
|
|
24
|
+
default(value: any): this;
|
|
25
|
+
defaultNow(): this;
|
|
26
|
+
defaultFn(fn: any): this;
|
|
27
|
+
references(table: string, column?: string): this;
|
|
28
|
+
onDelete(action: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION"): this;
|
|
29
|
+
onUpdate(action: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION"): this;
|
|
30
|
+
toSql(dialect?: DatabaseDialect): string;
|
|
31
|
+
}
|
|
32
|
+
export declare class Blueprint {
|
|
33
|
+
private tableName;
|
|
34
|
+
private columns;
|
|
35
|
+
private _primaryKeys;
|
|
36
|
+
private _uniques;
|
|
37
|
+
constructor(tableName: string);
|
|
38
|
+
id(name?: string): ColumnBuilder;
|
|
39
|
+
uuid(name?: string): ColumnBuilder;
|
|
40
|
+
increments(name: string): ColumnBuilder;
|
|
41
|
+
text(name: string): ColumnBuilder;
|
|
42
|
+
varchar(name: string, length?: number): ColumnBuilder;
|
|
43
|
+
string(name: string, length?: number): ColumnBuilder;
|
|
44
|
+
integer(name: string): ColumnBuilder;
|
|
45
|
+
smallint(name: string): ColumnBuilder;
|
|
46
|
+
bigint(name: string): ColumnBuilder;
|
|
47
|
+
boolean(name: string): ColumnBuilder;
|
|
48
|
+
timestamp(name: string, precision?: number): ColumnBuilder;
|
|
49
|
+
timestamptz(name: string, precision?: number): ColumnBuilder;
|
|
50
|
+
timestamps(): void;
|
|
51
|
+
softDeletes(name?: string): ColumnBuilder;
|
|
52
|
+
date(name: string): ColumnBuilder;
|
|
53
|
+
json(name: string): ColumnBuilder;
|
|
54
|
+
jsonb(name: string): ColumnBuilder;
|
|
55
|
+
bytea(name: string): ColumnBuilder;
|
|
56
|
+
blob(name: string): ColumnBuilder;
|
|
57
|
+
decimal(name: string, precision?: number, scale?: number): ColumnBuilder;
|
|
58
|
+
float(name: string): ColumnBuilder;
|
|
59
|
+
double(name: string): ColumnBuilder;
|
|
60
|
+
custom(type: string, name: string): ColumnBuilder;
|
|
61
|
+
unique(columns: string[]): this;
|
|
62
|
+
primary(columns: string[]): this;
|
|
63
|
+
private addColumn;
|
|
64
|
+
toSql(dialect?: DatabaseDialect): string;
|
|
65
|
+
toAlterSql(dialect?: DatabaseDialect): string[];
|
|
66
|
+
}
|
|
67
|
+
export declare class Schema {
|
|
68
|
+
private db?;
|
|
69
|
+
constructor(db?: any | undefined);
|
|
70
|
+
getDialect(): DatabaseDialect;
|
|
71
|
+
get knex(): any;
|
|
72
|
+
private getKnexInstance;
|
|
73
|
+
hasTable(name: string): Promise<boolean>;
|
|
74
|
+
hasColumn(tableName: string, columnName: string): Promise<boolean>;
|
|
75
|
+
dropColumn(tableName: string, columnName: string): Promise<void>;
|
|
76
|
+
private runQuery;
|
|
77
|
+
createTable(name: string, callback: (table: Blueprint) => void): Promise<void>;
|
|
78
|
+
table(name: string, callback: (table: Blueprint) => void): Promise<void>;
|
|
79
|
+
dropTable(name: string): Promise<void>;
|
|
80
|
+
dropTableIfExists(name: string): Promise<void>;
|
|
81
|
+
createEnum(name: string, values: string[]): Promise<void>;
|
|
82
|
+
dropEnum(name: string): Promise<void>;
|
|
83
|
+
raw(query: string): Promise<void>;
|
|
84
|
+
}
|