@veap/database 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/casts.js +1 -1
- package/dist/cli/commands/make-migration.d.ts +13 -0
- package/dist/cli/commands/make-migration.js +24 -0
- package/dist/cli/commands/migrate-rollback.d.ts +14 -0
- package/dist/cli/commands/migrate-rollback.js +41 -0
- package/dist/cli/commands/migrate-status.d.ts +16 -0
- package/dist/cli/commands/migrate-status.js +79 -0
- package/dist/cli/commands/migrate.d.ts +13 -0
- package/dist/cli/commands/migrate.js +40 -0
- package/dist/cli/confirmation.d.ts +14 -0
- package/dist/cli/confirmation.js +26 -0
- package/dist/cli/migrations.d.ts +20 -0
- package/dist/cli/migrations.js +41 -0
- package/dist/composition.d.ts +15 -0
- package/dist/composition.js +26 -0
- package/dist/connection.d.ts +1 -1
- package/dist/connection.js +9 -5
- package/dist/factory.d.ts +1 -1
- package/dist/factory.js +1 -1
- package/dist/index.d.ts +18 -16
- package/dist/index.js +18 -16
- package/dist/migration-provider.d.ts +10 -0
- package/dist/migration-provider.js +50 -6
- package/dist/migrations/generator.d.ts +11 -2
- package/dist/migrations/generator.js +40 -5
- package/dist/migrations/index.d.ts +3 -3
- package/dist/migrations/index.js +3 -3
- package/dist/migrations/runner.d.ts +28 -3
- package/dist/migrations/runner.js +45 -29
- package/dist/migrations/schema-builder.js +1 -1
- package/dist/model.d.ts +4 -4
- package/dist/model.js +25 -11
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +62 -8
- package/dist/query-builder.d.ts +6 -6
- package/dist/query-builder.js +40 -6
- package/dist/relations/belongs-to-many.d.ts +3 -3
- package/dist/relations/belongs-to-many.js +2 -2
- package/dist/relations/belongs-to.d.ts +3 -3
- package/dist/relations/belongs-to.js +1 -1
- package/dist/relations/eager-loader.d.ts +1 -1
- package/dist/relations/eager-loader.js +3 -1
- package/dist/relations/has-many.d.ts +3 -3
- package/dist/relations/has-many.js +1 -1
- package/dist/relations/has-one.d.ts +3 -3
- package/dist/relations/has-one.js +1 -1
- package/dist/relations/index.d.ts +11 -11
- package/dist/relations/index.js +11 -11
- package/dist/relations/morph-many.d.ts +3 -3
- package/dist/relations/morph-many.js +2 -2
- package/dist/relations/morph-one.d.ts +3 -3
- package/dist/relations/morph-one.js +2 -2
- package/dist/relations/morph-to-many.d.ts +3 -3
- package/dist/relations/morph-to-many.js +3 -3
- package/dist/relations/morph-to.d.ts +3 -3
- package/dist/relations/morph-to.js +3 -3
- package/dist/relations/relation.d.ts +2 -2
- package/dist/scopes.d.ts +2 -2
- package/dist/seeder.js +2 -2
- package/dist/tokens.d.ts +10 -0
- package/dist/tokens.js +8 -0
- package/package.json +10 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { AppError } from "@veap/kernel";
|
|
3
4
|
const stub = `import type { Schema } from "@veap/database";
|
|
4
5
|
|
|
5
6
|
export default {
|
|
@@ -16,6 +17,8 @@ export default {
|
|
|
16
17
|
},
|
|
17
18
|
};
|
|
18
19
|
`;
|
|
20
|
+
/** Migration names end up in a file name and in a table name, so keep them boring. */
|
|
21
|
+
const NAME_PATTERN = /^[a-zA-Z0-9_]+$/;
|
|
19
22
|
async function updateIndexFile(migrationsDir) {
|
|
20
23
|
const indexFile = path.join(migrationsDir, "index.ts");
|
|
21
24
|
const files = await fs.readdir(migrationsDir);
|
|
@@ -23,28 +26,50 @@ async function updateIndexFile(migrationsDir) {
|
|
|
23
26
|
.filter((f) => f.endsWith(".ts") && f !== "index.ts")
|
|
24
27
|
.sort();
|
|
25
28
|
const imports = [];
|
|
29
|
+
const normalisers = [];
|
|
26
30
|
const exports = [];
|
|
27
31
|
for (const file of migrationFiles) {
|
|
28
32
|
const name = file.replace(".ts", "");
|
|
29
33
|
const varName = `m${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
30
|
-
|
|
34
|
+
// A migration is either the scaffolded `export default { … }` object or a
|
|
35
|
+
// module with named `name`/`up`/`down` exports (every hand-written migration
|
|
36
|
+
// in the shipped apps). Importing the namespace and unwrapping `default`
|
|
37
|
+
// keeps both working, so regenerating the index never silently breaks a
|
|
38
|
+
// migration that was written by hand.
|
|
39
|
+
imports.push(`import * as ${varName}Raw from "./${name}";`);
|
|
40
|
+
normalisers.push(`const ${varName} = (${varName}Raw as { default?: unknown }).default ?? ${varName}Raw;`);
|
|
31
41
|
exports.push(varName);
|
|
32
42
|
}
|
|
33
43
|
const content = `// This file is auto-generated by Veap CLI. Do not edit manually.
|
|
34
44
|
${imports.join("\n")}
|
|
35
45
|
|
|
46
|
+
${normalisers.join("\n")}
|
|
47
|
+
|
|
36
48
|
export const appMigrations = [
|
|
37
49
|
${exports.join(",\n ")}
|
|
38
50
|
];
|
|
39
51
|
`;
|
|
40
52
|
await fs.writeFile(indexFile, content, "utf-8");
|
|
41
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Writes a new migration file and regenerates the directory's `index.ts`.
|
|
56
|
+
*
|
|
57
|
+
* Used by the `veap make:migration` command; kept free of the console so it can
|
|
58
|
+
* be called from anywhere (tests, generators, plugins).
|
|
59
|
+
*/
|
|
42
60
|
export async function generateMigration(name, targetDir) {
|
|
61
|
+
const trimmed = (name ?? "").trim();
|
|
62
|
+
if (!trimmed) {
|
|
63
|
+
throw AppError.BadRequest("A migration name is required.");
|
|
64
|
+
}
|
|
65
|
+
if (!NAME_PATTERN.test(trimmed)) {
|
|
66
|
+
throw AppError.BadRequest(`The migration name "${trimmed}" may only contain letters, digits and underscores.`);
|
|
67
|
+
}
|
|
43
68
|
const timestamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
44
|
-
const fileName = `${timestamp}_${
|
|
45
|
-
let tableName =
|
|
46
|
-
if (
|
|
47
|
-
tableName =
|
|
69
|
+
const fileName = `${timestamp}_${trimmed}`;
|
|
70
|
+
let tableName = trimmed;
|
|
71
|
+
if (trimmed.startsWith("create_") && trimmed.endsWith("_table")) {
|
|
72
|
+
tableName = trimmed.replace("create_", "").replace("_table", "");
|
|
48
73
|
}
|
|
49
74
|
const content = stub
|
|
50
75
|
.replace(/{{name}}/g, fileName)
|
|
@@ -52,6 +77,16 @@ export async function generateMigration(name, targetDir) {
|
|
|
52
77
|
const migrationsDir = targetDir || path.join(process.cwd(), "migrations");
|
|
53
78
|
await fs.mkdir(migrationsDir, { recursive: true });
|
|
54
79
|
const filePath = path.join(migrationsDir, `${fileName}.ts`);
|
|
80
|
+
// Two runs inside the same second would otherwise overwrite the first file.
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(filePath);
|
|
83
|
+
throw AppError.Conflict(`The migration "${fileName}" already exists at ${filePath}.`);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof AppError)
|
|
87
|
+
throw error;
|
|
88
|
+
// ENOENT: nothing to overwrite, keep going.
|
|
89
|
+
}
|
|
55
90
|
await fs.writeFile(filePath, content, "utf-8");
|
|
56
91
|
await updateIndexFile(migrationsDir);
|
|
57
92
|
return { fileName, filePath };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./generator";
|
|
2
|
-
export * from "./runner";
|
|
3
|
-
export * from "./schema-builder";
|
|
1
|
+
export * from "./generator.js";
|
|
2
|
+
export * from "./runner.js";
|
|
3
|
+
export * from "./schema-builder.js";
|
package/dist/migrations/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./generator";
|
|
2
|
-
export * from "./runner";
|
|
3
|
-
export * from "./schema-builder";
|
|
1
|
+
export * from "./generator.js";
|
|
2
|
+
export * from "./runner.js";
|
|
3
|
+
export * from "./schema-builder.js";
|
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
import { Schema } from "./schema-builder";
|
|
1
|
+
import { Schema } from "./schema-builder.js";
|
|
2
2
|
export interface Migration {
|
|
3
3
|
name: string;
|
|
4
4
|
up: (db: any, schema: Schema) => Promise<void>;
|
|
5
5
|
down?: (db: any, schema: Schema) => Promise<void>;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* What one `runMigrations()` call actually did.
|
|
9
|
+
*
|
|
10
|
+
* Returned instead of a bare `void` so a caller — `veap migrate` above all —
|
|
11
|
+
* can report the run instead of guessing: an empty `ran` means nothing was
|
|
12
|
+
* pending, and `batch` is only set when migrations were applied.
|
|
13
|
+
*/
|
|
14
|
+
export interface MigrationRunResult {
|
|
15
|
+
scope: string;
|
|
16
|
+
/** Names of the migrations applied by this call, in execution order. */
|
|
17
|
+
ran: string[];
|
|
18
|
+
/** Batch the migrations of this call were recorded under. */
|
|
19
|
+
batch?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function runMigrations(scope: string, migrations: Migration[]): Promise<MigrationRunResult>;
|
|
22
|
+
/**
|
|
23
|
+
* What one `rollbackMigrations()` call actually undid.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors {@link MigrationRunResult} so `veap migrate:rollback` can report the
|
|
26
|
+
* reversed migrations instead of claiming a rollback that never happened.
|
|
27
|
+
*/
|
|
28
|
+
export interface MigrationRollbackResult {
|
|
29
|
+
scope: string;
|
|
30
|
+
/** Names of the migrations rolled back, in the order their `down()` ran. */
|
|
31
|
+
rolledBack: string[];
|
|
32
|
+
}
|
|
8
33
|
/**
|
|
9
34
|
* Modern static migration rollback runner.
|
|
10
35
|
* Reverts migrations in reverse order.
|
|
11
36
|
*/
|
|
12
|
-
export declare function rollbackMigrations(scope: string, migrations: Migration[]): Promise<
|
|
37
|
+
export declare function rollbackMigrations(scope: string, migrations: Migration[]): Promise<MigrationRollbackResult>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { getKnex } from "../connection";
|
|
3
|
-
import { debug, warn } from "../logger";
|
|
4
|
-
import { Schema } from "./schema-builder";
|
|
2
|
+
import { getKnex } from "../connection.js";
|
|
3
|
+
import { debug, warn } from "../logger.js";
|
|
4
|
+
import { Schema } from "./schema-builder.js";
|
|
5
5
|
async function ensureMigrationsTable(knex = getKnex()) {
|
|
6
6
|
try {
|
|
7
7
|
const hasTable = await knex.schema.hasTable("migrations");
|
|
@@ -27,6 +27,19 @@ async function ensureMigrationsTable(knex = getKnex()) {
|
|
|
27
27
|
warn("veap:migration", "Failed to ensure migrations table:", error);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
async function acquireAdvisoryLockIfPostgres(tx, knex) {
|
|
31
|
+
const clientName = knex?.client?.config?.client || "";
|
|
32
|
+
if (typeof clientName === "string" &&
|
|
33
|
+
(clientName.includes("pg") || clientName.includes("postgres"))) {
|
|
34
|
+
const lockId = 123456;
|
|
35
|
+
try {
|
|
36
|
+
await tx.raw("SELECT pg_advisory_xact_lock(?)", [lockId]);
|
|
37
|
+
}
|
|
38
|
+
catch (_e) {
|
|
39
|
+
debug("veap:migration", "Could not acquire advisory lock, continuing...");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Creates a compatibility DB adapter for migrations that provides `.execute(query)`
|
|
32
45
|
* wrapping `trx.raw(query)`, while also being directly callable as a Knex query builder
|
|
@@ -54,27 +67,22 @@ function createMigrationDbAdapter(trx) {
|
|
|
54
67
|
});
|
|
55
68
|
}
|
|
56
69
|
export async function runMigrations(scope, migrations) {
|
|
70
|
+
let result = { scope, ran: [] };
|
|
57
71
|
if (migrations.length === 0)
|
|
58
|
-
return;
|
|
72
|
+
return result;
|
|
59
73
|
let knex;
|
|
60
74
|
try {
|
|
61
75
|
knex = getKnex();
|
|
62
76
|
}
|
|
63
77
|
catch {
|
|
64
|
-
return;
|
|
78
|
+
return result;
|
|
65
79
|
}
|
|
66
80
|
if (!knex)
|
|
67
|
-
return;
|
|
81
|
+
return result;
|
|
68
82
|
await ensureMigrationsTable(knex);
|
|
69
83
|
await knex.transaction(async (tx) => {
|
|
70
|
-
// Acquire advisory lock
|
|
71
|
-
|
|
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
|
-
}
|
|
84
|
+
// Acquire advisory lock only if running on PostgreSQL
|
|
85
|
+
await acquireAdvisoryLockIfPostgres(tx, knex);
|
|
78
86
|
debug("veap:migration", `Running migrations for scope: ${scope}`);
|
|
79
87
|
// 1. Get executed migrations for this scope
|
|
80
88
|
const executed = await tx("migrations")
|
|
@@ -111,39 +119,42 @@ export async function runMigrations(scope, migrations) {
|
|
|
111
119
|
throw error; // Rollback the whole batch on error
|
|
112
120
|
}
|
|
113
121
|
}
|
|
122
|
+
// Reported only once the whole batch is in: an error above rolls the
|
|
123
|
+
// transaction back, so nothing was applied and `ran` stays empty.
|
|
124
|
+
result = { scope, ran: pending.map((migration) => migration.name), batch };
|
|
114
125
|
});
|
|
126
|
+
return result;
|
|
115
127
|
}
|
|
116
128
|
/**
|
|
117
129
|
* Modern static migration rollback runner.
|
|
118
130
|
* Reverts migrations in reverse order.
|
|
119
131
|
*/
|
|
120
132
|
export async function rollbackMigrations(scope, migrations) {
|
|
133
|
+
let result = { scope, rolledBack: [] };
|
|
121
134
|
if (migrations.length === 0)
|
|
122
|
-
return;
|
|
135
|
+
return result;
|
|
123
136
|
let knex;
|
|
124
137
|
try {
|
|
125
138
|
knex = getKnex();
|
|
126
139
|
}
|
|
127
140
|
catch {
|
|
128
|
-
return;
|
|
141
|
+
return result;
|
|
129
142
|
}
|
|
130
143
|
if (!knex)
|
|
131
|
-
return;
|
|
144
|
+
return result;
|
|
132
145
|
await ensureMigrationsTable(knex);
|
|
133
146
|
await knex.transaction(async (tx) => {
|
|
134
|
-
// Acquire advisory lock
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
debug("veap:migration", "Could not acquire advisory lock, continuing...");
|
|
141
|
-
}
|
|
142
|
-
// 1. Get executed migrations for this scope ordered by batch desc
|
|
147
|
+
// Acquire advisory lock only if running on PostgreSQL
|
|
148
|
+
await acquireAdvisoryLockIfPostgres(tx, knex);
|
|
149
|
+
// 1. The last batch first, and inside it the most recently executed
|
|
150
|
+
// migration first: a rollback has to walk the batch backwards, otherwise a
|
|
151
|
+
// table added to an existing one would be dropped before its own foreign
|
|
152
|
+
// keys, or after a table that depends on it.
|
|
143
153
|
const executed = await tx("migrations")
|
|
144
|
-
.select("name", "batch")
|
|
154
|
+
.select("name", "batch", "executed_at")
|
|
145
155
|
.where("scope", scope)
|
|
146
|
-
.orderBy("batch", "desc")
|
|
156
|
+
.orderBy("batch", "desc")
|
|
157
|
+
.orderBy("executed_at", "desc");
|
|
147
158
|
if (executed.length === 0) {
|
|
148
159
|
debug("veap:migration", `No migrations to rollback for ${scope}.`);
|
|
149
160
|
return;
|
|
@@ -155,7 +166,7 @@ export async function rollbackMigrations(scope, migrations) {
|
|
|
155
166
|
.map((m) => m.name);
|
|
156
167
|
const dbAdapter = createMigrationDbAdapter(tx);
|
|
157
168
|
const schema = new Schema(tx);
|
|
158
|
-
|
|
169
|
+
const rolledBack = [];
|
|
159
170
|
for (const name of toRollbackNames) {
|
|
160
171
|
const migration = migrations.find((m) => m.name === name);
|
|
161
172
|
if (migration && migration.down) {
|
|
@@ -166,6 +177,7 @@ export async function rollbackMigrations(scope, migrations) {
|
|
|
166
177
|
.where("name", name)
|
|
167
178
|
.where("scope", scope)
|
|
168
179
|
.delete();
|
|
180
|
+
rolledBack.push(name);
|
|
169
181
|
debug("veap:migration", `[${scope}] Rolled back ${name}.`);
|
|
170
182
|
}
|
|
171
183
|
catch (error) {
|
|
@@ -174,5 +186,9 @@ export async function rollbackMigrations(scope, migrations) {
|
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
}
|
|
189
|
+
// Only reported once the transaction committed: an error above rolls it
|
|
190
|
+
// back, so nothing was undone.
|
|
191
|
+
result = { scope, rolledBack };
|
|
177
192
|
});
|
|
193
|
+
return result;
|
|
178
194
|
}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type CastType } from "./casts";
|
|
2
|
-
import { ModelQueryBuilder } from "./query-builder";
|
|
3
|
-
import { BelongsTo, BelongsToMany, HasMany, HasOne, MorphedByMany, MorphMany, MorphOne, MorphTo, MorphToMany } from "./relations";
|
|
4
|
-
import type { GlobalScope } from "./scopes";
|
|
1
|
+
import { type CastType } from "./casts.js";
|
|
2
|
+
import { ModelQueryBuilder } from "./query-builder.js";
|
|
3
|
+
import { BelongsTo, BelongsToMany, HasMany, HasOne, MorphedByMany, MorphMany, MorphOne, MorphTo, MorphToMany } from "./relations/index.js";
|
|
4
|
+
import type { GlobalScope } from "./scopes.js";
|
|
5
5
|
export declare abstract class Model<Attributes extends Record<string, any> = Record<string, any>> {
|
|
6
6
|
[key: string]: any;
|
|
7
7
|
/**
|
package/dist/model.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** biome-ignore-all lint/correctness/noConstructorReturn: <ignore> */
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { castAttribute, serializeForStorage } from "./casts";
|
|
4
|
-
import { getKnex } from "./connection";
|
|
5
|
-
import { DatabaseError } from "./errors";
|
|
6
|
-
import { getDatabaseEventDispatcher } from "./events";
|
|
7
|
-
import { Factory } from "./factory";
|
|
8
|
-
import { ModelQueryBuilder } from "./query-builder";
|
|
9
|
-
import { BelongsTo, BelongsToMany, HasMany, HasOne, MorphedByMany, MorphMany, MorphMap, MorphOne, MorphTo, MorphToMany, } from "./relations";
|
|
10
|
-
import { toCamelCase, toSnakeCase } from "./utils";
|
|
3
|
+
import { castAttribute, serializeForStorage } from "./casts.js";
|
|
4
|
+
import { getKnex } from "./connection.js";
|
|
5
|
+
import { DatabaseError } from "./errors.js";
|
|
6
|
+
import { getDatabaseEventDispatcher } from "./events.js";
|
|
7
|
+
import { Factory } from "./factory.js";
|
|
8
|
+
import { ModelQueryBuilder } from "./query-builder.js";
|
|
9
|
+
import { BelongsTo, BelongsToMany, HasMany, HasOne, MorphedByMany, MorphMany, MorphMap, MorphOne, MorphTo, MorphToMany, } from "./relations/index.js";
|
|
10
|
+
import { toCamelCase, toSnakeCase } from "./utils.js";
|
|
11
11
|
export class Model {
|
|
12
12
|
/**
|
|
13
13
|
* Database table name.
|
|
@@ -285,7 +285,7 @@ export class Model {
|
|
|
285
285
|
* Executes a database transaction.
|
|
286
286
|
*/
|
|
287
287
|
static async transaction(callback) {
|
|
288
|
-
const { transaction: runTrx } = await import("./connection");
|
|
288
|
+
const { transaction: runTrx } = await import("./connection.js");
|
|
289
289
|
return await runTrx(callback);
|
|
290
290
|
}
|
|
291
291
|
/**
|
|
@@ -336,12 +336,15 @@ export class Model {
|
|
|
336
336
|
*/
|
|
337
337
|
static async destroy(id) {
|
|
338
338
|
const knex = getKnex();
|
|
339
|
+
const clientType = knex.client?.config?.client;
|
|
339
340
|
if (this.softDeletes) {
|
|
340
341
|
const col = toSnakeCase(this.deletedAtColumn || "deleted_at");
|
|
341
342
|
const count = await knex(this.table)
|
|
342
343
|
.where(this.primaryKey, id)
|
|
343
344
|
.whereNull(col)
|
|
344
|
-
.update({
|
|
345
|
+
.update({
|
|
346
|
+
[col]: serializeForStorage(new Date(), "datetime", clientType),
|
|
347
|
+
});
|
|
345
348
|
return count > 0;
|
|
346
349
|
}
|
|
347
350
|
const count = await knex(this.table).where(this.primaryKey, id).delete();
|
|
@@ -607,7 +610,18 @@ export class Model {
|
|
|
607
610
|
}
|
|
608
611
|
insertData[col] = serializeForStorage(v, castType, clientType);
|
|
609
612
|
}
|
|
610
|
-
await knex(modelClass.table)
|
|
613
|
+
const insertResult = await knex(modelClass.table)
|
|
614
|
+
.insert(insertData)
|
|
615
|
+
.returning(pk);
|
|
616
|
+
if (!this.getAttribute(pk)) {
|
|
617
|
+
if (Array.isArray(insertResult) && insertResult.length > 0) {
|
|
618
|
+
const first = insertResult[0];
|
|
619
|
+
const returnedId = typeof first === "object" && first !== null ? first[pk] : first;
|
|
620
|
+
if (returnedId !== undefined && returnedId !== null) {
|
|
621
|
+
this.setAttribute(pk, returnedId);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
611
625
|
this.isNew = false;
|
|
612
626
|
this._original = { ...this._attributes };
|
|
613
627
|
await this.fireModelEvent("created");
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceProvider } from "@veap/kernel";
|
|
2
|
-
import type { DatabaseConfig } from "./types";
|
|
2
|
+
import type { DatabaseConfig } from "./types.js";
|
|
3
3
|
export interface DatabaseProviderConfig {
|
|
4
4
|
databaseUrl?: string;
|
|
5
5
|
config?: DatabaseConfig;
|
|
@@ -9,4 +9,5 @@ export declare class DatabaseServiceProvider extends ServiceProvider {
|
|
|
9
9
|
constructor(container: any, config?: DatabaseProviderConfig);
|
|
10
10
|
register(): void;
|
|
11
11
|
boot(): Promise<void>;
|
|
12
|
+
shutdown(): Promise<void>;
|
|
12
13
|
}
|
package/dist/provider.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ConfigService,
|
|
1
|
+
import { ConfigService, ServiceProvider } from "@veap/kernel";
|
|
2
2
|
import knex from "knex";
|
|
3
|
-
import { initDatabase, isSqliteDatabase, resolvePostgresSslConfig, resolveSqliteFilename, setKnex, } from "./connection";
|
|
4
|
-
import {
|
|
3
|
+
import { getKnex, initDatabase, isSqliteDatabase, resolvePostgresSslConfig, resolveSqliteFilename, setKnex, } from "./connection.js";
|
|
4
|
+
import { setDatabaseEventDispatcher } from "./events.js";
|
|
5
|
+
import { debug, setDatabaseLogger } from "./logger.js";
|
|
6
|
+
import { DATABASE } from "./tokens.js";
|
|
5
7
|
export class DatabaseServiceProvider extends ServiceProvider {
|
|
6
8
|
config;
|
|
7
9
|
constructor(container, config) {
|
|
@@ -12,13 +14,54 @@ export class DatabaseServiceProvider extends ServiceProvider {
|
|
|
12
14
|
// Port token registration
|
|
13
15
|
}
|
|
14
16
|
async boot() {
|
|
15
|
-
|
|
17
|
+
let configService = null;
|
|
18
|
+
if (this.container.has(ConfigService)) {
|
|
19
|
+
configService = await this.container.resolve(ConfigService);
|
|
20
|
+
}
|
|
21
|
+
else if (this.container.has("ConfigService")) {
|
|
22
|
+
configService = await this.container.resolve("ConfigService");
|
|
23
|
+
}
|
|
16
24
|
const databaseUrl = this.config?.databaseUrl ||
|
|
17
|
-
configService
|
|
25
|
+
configService?.get?.("DATABASE_URL") ||
|
|
18
26
|
process.env.DATABASE_URL ||
|
|
19
27
|
process.env.DATABASE_FILE;
|
|
20
|
-
const isProd = configService
|
|
28
|
+
const isProd = configService?.get?.("NODE_ENV") === "production" ||
|
|
21
29
|
process.env.NODE_ENV === "production";
|
|
30
|
+
// Auto-wire EventDispatcher if bound to container
|
|
31
|
+
if (this.container.has("EventDispatcher")) {
|
|
32
|
+
try {
|
|
33
|
+
const dispatcher = (await this.container.resolve("EventDispatcher"));
|
|
34
|
+
if (dispatcher && typeof dispatcher.publish === "function") {
|
|
35
|
+
setDatabaseEventDispatcher(dispatcher);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Continue without event dispatcher
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Auto-wire Logger if bound to container
|
|
43
|
+
if (this.container.has("LOGGER")) {
|
|
44
|
+
try {
|
|
45
|
+
const logger = (await this.container.resolve("LOGGER"));
|
|
46
|
+
if (logger && typeof logger.info === "function") {
|
|
47
|
+
setDatabaseLogger({
|
|
48
|
+
debug: (ch, msg, ...args) => logger.debug
|
|
49
|
+
? logger.debug(`[${ch}] ${msg}`, ...args)
|
|
50
|
+
: undefined,
|
|
51
|
+
info: (ch, msg, ...args) => logger.info(`[${ch}] ${msg}`, ...args),
|
|
52
|
+
warn: (ch, msg, ...args) => logger.warn
|
|
53
|
+
? logger.warn(`[${ch}] ${msg}`, ...args)
|
|
54
|
+
: console.warn(`[${ch}] ${msg}`, ...args),
|
|
55
|
+
error: (ch, msg, ...args) => logger.error
|
|
56
|
+
? logger.error(`[${ch}] ${msg}`, ...args)
|
|
57
|
+
: console.error(`[${ch}] ${msg}`, ...args),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Continue with default logger
|
|
63
|
+
}
|
|
64
|
+
}
|
|
22
65
|
if (this.config?.config) {
|
|
23
66
|
const instance = initDatabase(this.config.config);
|
|
24
67
|
this.container.register({ token: DATABASE, useValue: instance });
|
|
@@ -32,8 +75,8 @@ export class DatabaseServiceProvider extends ServiceProvider {
|
|
|
32
75
|
const ssl = resolvePostgresSslConfig({
|
|
33
76
|
databaseUrl: resolvedUrl,
|
|
34
77
|
isProd,
|
|
35
|
-
rejectUnauthorizedEnv: configService
|
|
36
|
-
caCert: configService
|
|
78
|
+
rejectUnauthorizedEnv: configService?.get?.("DATABASE_SSL_REJECT_UNAUTHORIZED"),
|
|
79
|
+
caCert: configService?.get?.("DATABASE_SSL_CA"),
|
|
37
80
|
});
|
|
38
81
|
const instance = knex({
|
|
39
82
|
client,
|
|
@@ -55,4 +98,15 @@ export class DatabaseServiceProvider extends ServiceProvider {
|
|
|
55
98
|
useValue: instance,
|
|
56
99
|
});
|
|
57
100
|
}
|
|
101
|
+
async shutdown() {
|
|
102
|
+
try {
|
|
103
|
+
const k = getKnex();
|
|
104
|
+
if (k && typeof k.destroy === "function") {
|
|
105
|
+
await k.destroy();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore if not initialized
|
|
110
|
+
}
|
|
111
|
+
}
|
|
58
112
|
}
|
package/dist/query-builder.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Knex } from "knex";
|
|
2
|
-
import type { Model } from "./model";
|
|
2
|
+
import type { Model } from "./model.js";
|
|
3
|
+
import type { PaginationResult } from "./types.js";
|
|
3
4
|
export declare class ModelQueryBuilder<M extends Model = any> {
|
|
4
5
|
modelClass: any;
|
|
5
6
|
[key: string]: any;
|
|
@@ -65,6 +66,9 @@ export declare class ModelQueryBuilder<M extends Model = any> {
|
|
|
65
66
|
take(count: number): this;
|
|
66
67
|
offset(count: number): this;
|
|
67
68
|
skip(count: number): this;
|
|
69
|
+
groupBy(...columns: any[]): this;
|
|
70
|
+
having(column: any, operatorOrValue?: any, value?: any): this;
|
|
71
|
+
distinct(...columns: any[]): this;
|
|
68
72
|
join(table: string, first: string, operatorOrSecond: string, second?: string): this;
|
|
69
73
|
leftJoin(table: string, first: string, operatorOrSecond: string, second?: string): this;
|
|
70
74
|
/**
|
|
@@ -162,12 +166,8 @@ export declare class ModelQueryBuilder<M extends Model = any> {
|
|
|
162
166
|
/**
|
|
163
167
|
* Paginates results.
|
|
164
168
|
*/
|
|
165
|
-
paginate(page?: number, perPage?: number): Promise<{
|
|
166
|
-
data: M[];
|
|
167
|
-
total: number;
|
|
169
|
+
paginate(page?: number, perPage?: number): Promise<PaginationResult<M> & {
|
|
168
170
|
page: number;
|
|
169
|
-
perPage: number;
|
|
170
|
-
lastPage: number;
|
|
171
171
|
}>;
|
|
172
172
|
/**
|
|
173
173
|
* Bulk updates matching records.
|
package/dist/query-builder.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { serializeForStorage } from "./casts";
|
|
2
|
-
import { getKnex } from "./connection";
|
|
3
|
-
import { DatabaseError } from "./errors";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { toCamelCase, toSnakeCase } from "./utils";
|
|
1
|
+
import { serializeForStorage } from "./casts.js";
|
|
2
|
+
import { getKnex } from "./connection.js";
|
|
3
|
+
import { DatabaseError } from "./errors.js";
|
|
4
|
+
import { eagerLoadRelations } from "./relations/eager-loader.js";
|
|
5
|
+
import { BelongsTo, BelongsToMany, MorphedByMany, MorphMany, MorphOne, MorphToMany, } from "./relations/index.js";
|
|
6
|
+
import { toCamelCase, toSnakeCase } from "./utils.js";
|
|
7
7
|
export class ModelQueryBuilder {
|
|
8
8
|
modelClass;
|
|
9
9
|
knexBuilder;
|
|
@@ -47,6 +47,14 @@ export class ModelQueryBuilder {
|
|
|
47
47
|
: result;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
// 3. Delegate to Knex query builder if method exists (e.g. groupBy, having, distinct, joinRaw)
|
|
51
|
+
if (target.knexBuilder &&
|
|
52
|
+
typeof target.knexBuilder[prop] === "function") {
|
|
53
|
+
return (...args) => {
|
|
54
|
+
const result = target.knexBuilder[prop](...args);
|
|
55
|
+
return result === target.knexBuilder ? receiver : result;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
return Reflect.get(target, prop, receiver);
|
|
52
60
|
},
|
|
@@ -270,6 +278,30 @@ export class ModelQueryBuilder {
|
|
|
270
278
|
skip(count) {
|
|
271
279
|
return this.offset(count);
|
|
272
280
|
}
|
|
281
|
+
groupBy(...columns) {
|
|
282
|
+
const converted = columns.map((col) => typeof col === "string" ? toSnakeCase(col) : col);
|
|
283
|
+
this.knexBuilder.groupBy(...converted);
|
|
284
|
+
return this;
|
|
285
|
+
}
|
|
286
|
+
having(column, operatorOrValue, value) {
|
|
287
|
+
if (value === undefined) {
|
|
288
|
+
if (operatorOrValue === undefined) {
|
|
289
|
+
this.knexBuilder.having(column);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
this.knexBuilder.having(column, "=", operatorOrValue);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.knexBuilder.having(column, operatorOrValue, value);
|
|
297
|
+
}
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
distinct(...columns) {
|
|
301
|
+
const converted = columns.map((col) => typeof col === "string" ? toSnakeCase(col) : col);
|
|
302
|
+
this.knexBuilder.distinct(...converted);
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
273
305
|
join(table, first, operatorOrSecond, second) {
|
|
274
306
|
if (second === undefined) {
|
|
275
307
|
this.knexBuilder.join(table, first, operatorOrSecond);
|
|
@@ -646,6 +678,7 @@ export class ModelQueryBuilder {
|
|
|
646
678
|
data,
|
|
647
679
|
total,
|
|
648
680
|
page,
|
|
681
|
+
currentPage: page,
|
|
649
682
|
perPage,
|
|
650
683
|
lastPage,
|
|
651
684
|
};
|
|
@@ -654,6 +687,7 @@ export class ModelQueryBuilder {
|
|
|
654
687
|
* Bulk updates matching records.
|
|
655
688
|
*/
|
|
656
689
|
async update(attributes) {
|
|
690
|
+
this.applyGlobalScopes();
|
|
657
691
|
const converted = {};
|
|
658
692
|
const casts = this.modelClass?.casts || {};
|
|
659
693
|
const knex = getKnex();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Model } from "../model";
|
|
2
|
-
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
-
import { Relation } from "./relation";
|
|
1
|
+
import type { Model } from "../model.js";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder.js";
|
|
3
|
+
import { Relation } from "./relation.js";
|
|
4
4
|
export declare class BelongsToMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
5
|
pivotTable: string;
|
|
6
6
|
foreignPivotKey: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Model } from "../model";
|
|
2
|
-
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
-
import { Relation } from "./relation";
|
|
1
|
+
import type { Model } from "../model.js";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder.js";
|
|
3
|
+
import { Relation } from "./relation.js";
|
|
4
4
|
export declare class BelongsTo<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
5
|
query(): ModelQueryBuilder<Related>;
|
|
6
6
|
get(): Promise<Related | null>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DatabaseError } from "../errors";
|
|
1
|
+
import { DatabaseError } from "../errors.js";
|
|
2
2
|
/**
|
|
3
3
|
* Loads relations onto an array of model instances in bulk queries, avoiding N+1.
|
|
4
4
|
*/
|
|
@@ -11,6 +11,8 @@ export async function eagerLoadRelations(models, relations) {
|
|
|
11
11
|
for (const rel of relations) {
|
|
12
12
|
const parts = rel.split(".");
|
|
13
13
|
const head = parts[0];
|
|
14
|
+
if (!head)
|
|
15
|
+
continue;
|
|
14
16
|
const tail = parts.slice(1).join(".");
|
|
15
17
|
if (!tree[head]) {
|
|
16
18
|
tree[head] = [];
|