@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.
Files changed (63) hide show
  1. package/README.md +3 -2
  2. package/dist/casts.js +1 -1
  3. package/dist/cli/commands/make-migration.d.ts +13 -0
  4. package/dist/cli/commands/make-migration.js +24 -0
  5. package/dist/cli/commands/migrate-rollback.d.ts +14 -0
  6. package/dist/cli/commands/migrate-rollback.js +41 -0
  7. package/dist/cli/commands/migrate-status.d.ts +16 -0
  8. package/dist/cli/commands/migrate-status.js +79 -0
  9. package/dist/cli/commands/migrate.d.ts +13 -0
  10. package/dist/cli/commands/migrate.js +40 -0
  11. package/dist/cli/confirmation.d.ts +14 -0
  12. package/dist/cli/confirmation.js +26 -0
  13. package/dist/cli/migrations.d.ts +20 -0
  14. package/dist/cli/migrations.js +41 -0
  15. package/dist/composition.d.ts +15 -0
  16. package/dist/composition.js +26 -0
  17. package/dist/connection.d.ts +1 -1
  18. package/dist/connection.js +9 -5
  19. package/dist/factory.d.ts +1 -1
  20. package/dist/factory.js +1 -1
  21. package/dist/index.d.ts +18 -16
  22. package/dist/index.js +18 -16
  23. package/dist/migration-provider.d.ts +10 -0
  24. package/dist/migration-provider.js +50 -6
  25. package/dist/migrations/generator.d.ts +11 -2
  26. package/dist/migrations/generator.js +40 -5
  27. package/dist/migrations/index.d.ts +3 -3
  28. package/dist/migrations/index.js +3 -3
  29. package/dist/migrations/runner.d.ts +28 -3
  30. package/dist/migrations/runner.js +45 -29
  31. package/dist/migrations/schema-builder.js +1 -1
  32. package/dist/model.d.ts +4 -4
  33. package/dist/model.js +25 -11
  34. package/dist/provider.d.ts +2 -1
  35. package/dist/provider.js +62 -8
  36. package/dist/query-builder.d.ts +6 -6
  37. package/dist/query-builder.js +40 -6
  38. package/dist/relations/belongs-to-many.d.ts +3 -3
  39. package/dist/relations/belongs-to-many.js +2 -2
  40. package/dist/relations/belongs-to.d.ts +3 -3
  41. package/dist/relations/belongs-to.js +1 -1
  42. package/dist/relations/eager-loader.d.ts +1 -1
  43. package/dist/relations/eager-loader.js +3 -1
  44. package/dist/relations/has-many.d.ts +3 -3
  45. package/dist/relations/has-many.js +1 -1
  46. package/dist/relations/has-one.d.ts +3 -3
  47. package/dist/relations/has-one.js +1 -1
  48. package/dist/relations/index.d.ts +11 -11
  49. package/dist/relations/index.js +11 -11
  50. package/dist/relations/morph-many.d.ts +3 -3
  51. package/dist/relations/morph-many.js +2 -2
  52. package/dist/relations/morph-one.d.ts +3 -3
  53. package/dist/relations/morph-one.js +2 -2
  54. package/dist/relations/morph-to-many.d.ts +3 -3
  55. package/dist/relations/morph-to-many.js +3 -3
  56. package/dist/relations/morph-to.d.ts +3 -3
  57. package/dist/relations/morph-to.js +3 -3
  58. package/dist/relations/relation.d.ts +2 -2
  59. package/dist/scopes.d.ts +2 -2
  60. package/dist/seeder.js +2 -2
  61. package/dist/tokens.d.ts +10 -0
  62. package/dist/tokens.js +8 -0
  63. 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
- imports.push(`import { default as ${varName} } from "./${name}";`);
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}_${name}`;
45
- let tableName = name;
46
- if (name.startsWith("create_") && name.endsWith("_table")) {
47
- tableName = name.replace("create_", "").replace("_table", "");
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";
@@ -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
- export declare function runMigrations(scope: string, migrations: Migration[]): Promise<void>;
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<void>;
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 (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
- }
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
- 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
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
- // Rollback in reverse order
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
  }
@@ -1,4 +1,4 @@
1
- import { getKnex } from "../connection";
1
+ import { getKnex } from "../connection.js";
2
2
  /**
3
3
  * Lightweight SQL helper for migrations, compatible with sql`...` template strings.
4
4
  */
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({ [col]: new Date() });
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).insert(insertData);
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");
@@ -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, DATABASE, ServiceProvider } from "@veap/kernel";
1
+ import { ConfigService, ServiceProvider } from "@veap/kernel";
2
2
  import knex from "knex";
3
- import { initDatabase, isSqliteDatabase, resolvePostgresSslConfig, resolveSqliteFilename, setKnex, } from "./connection";
4
- import { debug } from "./logger";
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
- const configService = await this.container.resolve(ConfigService);
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.get("DATABASE_URL") ||
25
+ configService?.get?.("DATABASE_URL") ||
18
26
  process.env.DATABASE_URL ||
19
27
  process.env.DATABASE_FILE;
20
- const isProd = configService.get("NODE_ENV") === "production" ||
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.get("DATABASE_SSL_REJECT_UNAUTHORIZED"),
36
- caCert: configService.get("DATABASE_SSL_CA"),
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
  }
@@ -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.
@@ -1,9 +1,9 @@
1
- import { serializeForStorage } from "./casts";
2
- import { getKnex } from "./connection";
3
- import { DatabaseError } from "./errors";
4
- import { BelongsTo, BelongsToMany, MorphedByMany, MorphMany, MorphOne, MorphToMany, } from "./relations";
5
- import { eagerLoadRelations } from "./relations/eager-loader";
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,5 +1,5 @@
1
- import { getKnex } from "../connection";
2
- import { Relation } from "./relation";
1
+ import { getKnex } from "../connection.js";
2
+ import { Relation } from "./relation.js";
3
3
  export class BelongsToMany extends Relation {
4
4
  pivotTable;
5
5
  foreignPivotKey;
@@ -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 { Relation } from "./relation";
1
+ import { Relation } from "./relation.js";
2
2
  export class BelongsTo extends Relation {
3
3
  query() {
4
4
  const foreignKeyValue = this.parent.getAttribute(this.foreignKey);
@@ -1,4 +1,4 @@
1
- import type { Model } from "../model";
1
+ import type { Model } from "../model.js";
2
2
  /**
3
3
  * Loads relations onto an array of model instances in bulk queries, avoiding N+1.
4
4
  */
@@ -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] = [];