@loomcore/api 0.1.42 → 0.1.46

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.
@@ -0,0 +1 @@
1
+ export type DbType = 'postgres' | 'mongodb';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
- export * from './mongo-db/index.js';
2
- export * from './postgres/index.js';
1
+ export * from './db-type.type.js';
2
+ export * from './migrations/migration-runner.js';
3
3
  export * from './models/index.js';
4
+ export * from './mongo-db/index.js';
4
5
  export * from './operations/index.js';
5
- export * from './migrations/migration-runner.js';
6
+ export * from './postgres/index.js';
@@ -1,5 +1,6 @@
1
- export * from './mongo-db/index.js';
2
- export * from './postgres/index.js';
1
+ export * from './db-type.type.js';
2
+ export * from './migrations/migration-runner.js';
3
3
  export * from './models/index.js';
4
+ export * from './mongo-db/index.js';
4
5
  export * from './operations/index.js';
5
- export * from './migrations/migration-runner.js';
6
+ export * from './postgres/index.js';
@@ -1,16 +1,14 @@
1
- export type DbType = 'postgres' | 'mongo';
2
- export interface MigrationConfig {
3
- dbType: DbType;
4
- dbUrl: string;
5
- migrationsDir: string;
6
- }
1
+ import { IBaseApiConfig } from '../../models/base-api-config.interface.js';
7
2
  export declare class MigrationRunner {
8
- private config;
3
+ private dbType;
4
+ private dbUrl;
5
+ private migrationsDir;
9
6
  private mongoClient;
10
- constructor(config: MigrationConfig);
7
+ constructor(config: IBaseApiConfig);
11
8
  private parseSql;
12
9
  private getMigrator;
13
10
  private wipeDatabase;
14
11
  run(command: 'up' | 'down' | 'reset'): Promise<void>;
15
12
  private closeConnection;
13
+ create(name: string): Promise<void>;
16
14
  }
@@ -3,11 +3,21 @@ import { Pool } from 'pg';
3
3
  import { MongoClient } from 'mongodb';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
+ import { buildMongoUrl } from '../mongo-db/utils/build-mongo-url.util.js';
7
+ import { buildPostgresUrl } from '../postgres/utils/build-postgres-url.util.js';
8
+ const getTimestamp = () => {
9
+ const now = new Date();
10
+ return now.toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
11
+ };
6
12
  export class MigrationRunner {
7
- config;
13
+ dbType;
14
+ dbUrl;
15
+ migrationsDir;
8
16
  mongoClient = null;
9
17
  constructor(config) {
10
- this.config = config;
18
+ this.dbType = config.app.dbType || 'mongodb';
19
+ this.dbUrl = this.dbType === 'postgres' ? buildPostgresUrl(config) : buildMongoUrl(config);
20
+ this.migrationsDir = path.join(process.cwd(), 'database', 'migrations');
11
21
  }
12
22
  parseSql(content) {
13
23
  const upMatch = content.match(/--\s*up\s([\s\S]+?)(?=--\s*down|$)/i);
@@ -18,12 +28,11 @@ export class MigrationRunner {
18
28
  };
19
29
  }
20
30
  async getMigrator() {
21
- const { dbType, dbUrl, migrationsDir } = this.config;
22
- if (dbType === 'postgres') {
23
- const pool = new Pool({ connectionString: dbUrl });
31
+ if (this.dbType === 'postgres') {
32
+ const pool = new Pool({ connectionString: this.dbUrl });
24
33
  return new Umzug({
25
34
  migrations: {
26
- glob: path.join(migrationsDir, '*.sql'),
35
+ glob: path.join(this.migrationsDir, '*.sql'),
27
36
  resolve: ({ name, path: filePath, context }) => {
28
37
  const content = fs.readFileSync(filePath, 'utf8');
29
38
  const { up, down } = this.parseSql(content);
@@ -51,24 +60,23 @@ export class MigrationRunner {
51
60
  logger: console,
52
61
  });
53
62
  }
54
- else if (dbType === 'mongo') {
55
- const client = await MongoClient.connect(dbUrl);
63
+ else if (this.dbType === 'mongodb') {
64
+ const client = await MongoClient.connect(this.dbUrl);
56
65
  this.mongoClient = client;
57
66
  const db = client.db();
58
67
  return new Umzug({
59
- migrations: { glob: path.join(migrationsDir, '*.ts') },
68
+ migrations: { glob: path.join(this.migrationsDir, '*.ts') },
60
69
  context: db,
61
70
  storage: new MongoDBStorage({ collection: db.collection('migrations') }),
62
71
  logger: console,
63
72
  });
64
73
  }
65
- throw new Error(`Unsupported DB_TYPE: ${dbType}`);
74
+ throw new Error(`Unsupported DB_TYPE: ${this.dbType}`);
66
75
  }
67
76
  async wipeDatabase() {
68
- const { dbType, dbUrl } = this.config;
69
- console.log(`⚠️ Wiping ${dbType} database...`);
70
- if (dbType === 'postgres') {
71
- const pool = new Pool({ connectionString: dbUrl });
77
+ console.log(`⚠️ Wiping ${this.dbType} database...`);
78
+ if (this.dbType === 'postgres') {
79
+ const pool = new Pool({ connectionString: this.dbUrl });
72
80
  try {
73
81
  await pool.query('DROP SCHEMA public CASCADE; CREATE SCHEMA public;');
74
82
  }
@@ -76,8 +84,8 @@ export class MigrationRunner {
76
84
  await pool.end();
77
85
  }
78
86
  }
79
- else if (dbType === 'mongo') {
80
- const client = await MongoClient.connect(dbUrl);
87
+ else if (this.dbType === 'mongodb') {
88
+ const client = await MongoClient.connect(this.dbUrl);
81
89
  await client.db().dropDatabase();
82
90
  await client.close();
83
91
  }
@@ -113,12 +121,56 @@ export class MigrationRunner {
113
121
  }
114
122
  }
115
123
  async closeConnection(migrator) {
116
- if (this.config.dbType === 'postgres') {
124
+ if (this.dbType === 'postgres') {
117
125
  await migrator.context.end();
118
126
  }
119
- else if (this.config.dbType === 'mongo' && this.mongoClient) {
127
+ else if (this.dbType === 'mongodb' && this.mongoClient) {
120
128
  await this.mongoClient.close();
121
129
  this.mongoClient = null;
122
130
  }
123
131
  }
132
+ async create(name) {
133
+ if (!name) {
134
+ throw new Error('Migration name is required');
135
+ }
136
+ const safeName = name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
137
+ const filename = `${getTimestamp()}_${safeName}`;
138
+ let extension = '';
139
+ let content = '';
140
+ if (this.dbType === 'postgres') {
141
+ extension = 'sql';
142
+ content = `-- Migration: ${safeName}
143
+ -- Created: ${new Date().toISOString()}
144
+
145
+ -- up
146
+ -- Write your CREATE/ALTER statements here...
147
+
148
+
149
+ -- down
150
+ -- Write your DROP/UNDO statements here...
151
+ `;
152
+ }
153
+ else {
154
+ extension = 'ts';
155
+ content = `import { Db } from 'mongodb';
156
+
157
+ // Migration: ${safeName}
158
+ // Created: ${new Date().toISOString()}
159
+
160
+ export const up = async ({ context: db }: { context: Db }) => {
161
+ // await db.collection('...')....
162
+ };
163
+
164
+ export const down = async ({ context: db }: { context: Db }) => {
165
+ // await db.collection('...')....
166
+ };
167
+ `;
168
+ }
169
+ const fullPath = path.join(this.migrationsDir, `${filename}.${extension}`);
170
+ if (!fs.existsSync(this.migrationsDir)) {
171
+ fs.mkdirSync(this.migrationsDir, { recursive: true });
172
+ }
173
+ fs.writeFileSync(fullPath, content);
174
+ console.log(`✅ Created migration:\n ${fullPath}`);
175
+ }
124
176
  }
@@ -0,0 +1,2 @@
1
+ import { IBaseApiConfig } from "../../../models/base-api-config.interface.js";
2
+ export declare function buildMongoUrl(config: IBaseApiConfig): string;
@@ -0,0 +1,13 @@
1
+ export function buildMongoUrl(config) {
2
+ const { database } = config;
3
+ if (!database) {
4
+ throw new Error("Database configuration is required to build the MongoDB URL.");
5
+ }
6
+ const { user, password, host, port, name } = database;
7
+ if (!user || !password || !host || !port || !name) {
8
+ throw new Error("Database configuration must include user, password, host, port, and name to build the MongoDB URL.");
9
+ }
10
+ const encodedUser = encodeURIComponent(user);
11
+ const encodedPassword = encodeURIComponent(password);
12
+ return `mongodb://${encodedUser}:${encodedPassword}@${host}:${port}/${name}`;
13
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './build-find-options.util.js';
2
+ export * from './build-mongo-url.util.js';
2
3
  export * from './build-no-sql-match.util.js';
3
4
  export * from './convert-object-ids-to-strings.util.js';
4
5
  export * from './convert-operations-to-pipeline.util.js';
@@ -1,4 +1,5 @@
1
1
  export * from './build-find-options.util.js';
2
+ export * from './build-mongo-url.util.js';
2
3
  export * from './build-no-sql-match.util.js';
3
4
  export * from './convert-object-ids-to-strings.util.js';
4
5
  export * from './convert-operations-to-pipeline.util.js';
@@ -0,0 +1,2 @@
1
+ import { IBaseApiConfig } from "../../../models/base-api-config.interface.js";
2
+ export declare function buildPostgresUrl(config: IBaseApiConfig): string;
@@ -0,0 +1,13 @@
1
+ export function buildPostgresUrl(config) {
2
+ const { database } = config;
3
+ if (!database) {
4
+ throw new Error("Database configuration is required to build the PostgreSQL URL.");
5
+ }
6
+ const { user, password, host, port, name } = database;
7
+ if (!user || !password || !host || !port || !name) {
8
+ throw new Error("Database configuration must include user, password, host, port, and name to build the PostgreSQL URL.");
9
+ }
10
+ const encodedUser = encodeURIComponent(user);
11
+ const encodedPassword = encodeURIComponent(password);
12
+ return `postgresql://${encodedUser}:${encodedPassword}@${host}:${port}/${name}`;
13
+ }
@@ -1 +1,2 @@
1
+ export * from './build-postgres-url.util.js';
1
2
  export * from './does-table-exist.util.js';
@@ -1 +1,2 @@
1
+ export * from './build-postgres-url.util.js';
1
2
  export * from './does-table-exist.util.js';
@@ -1,3 +1,4 @@
1
1
  export * from './error-handler.js';
2
2
  export * from './is-authorized.js';
3
3
  export * from './ensure-user-context.js';
4
+ export * from './request-lifecycle.js';
@@ -1,3 +1,4 @@
1
1
  export * from './error-handler.js';
2
2
  export * from './is-authorized.js';
3
3
  export * from './ensure-user-context.js';
4
+ export * from './request-lifecycle.js';
@@ -0,0 +1,6 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export interface RequestLifecycleOptions {
3
+ onRequestStart?: (req: Request, res: Response) => void | Promise<void>;
4
+ onRequestEnd?: (req: Request, res: Response, error?: Error) => void | Promise<void>;
5
+ }
6
+ export declare const requestLifecycle: (options?: RequestLifecycleOptions) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -0,0 +1,45 @@
1
+ export const requestLifecycle = (options = {}) => {
2
+ const { onRequestStart, onRequestEnd } = options;
3
+ return async (req, res, next) => {
4
+ req.startTime = Date.now();
5
+ if (onRequestStart) {
6
+ try {
7
+ await onRequestStart(req, res);
8
+ }
9
+ catch (error) {
10
+ console.error('Error in onRequestStart callback:', error);
11
+ }
12
+ }
13
+ let endCallbackCalled = false;
14
+ const callOnRequestEnd = async (error) => {
15
+ if (endCallbackCalled)
16
+ return;
17
+ endCallbackCalled = true;
18
+ if (onRequestEnd) {
19
+ try {
20
+ await onRequestEnd(req, res, error);
21
+ }
22
+ catch (callbackError) {
23
+ console.error('Error in onRequestEnd callback:', callbackError);
24
+ }
25
+ }
26
+ };
27
+ res.on('finish', () => {
28
+ const error = req.lifecycleError;
29
+ callOnRequestEnd(error);
30
+ });
31
+ res.on('close', () => {
32
+ if (!res.writableEnded && !endCallbackCalled) {
33
+ const error = req.lifecycleError || new Error('Request closed before completion');
34
+ callOnRequestEnd(error);
35
+ }
36
+ });
37
+ try {
38
+ await next();
39
+ }
40
+ catch (error) {
41
+ req.lifecycleError = error instanceof Error ? error : new Error(String(error));
42
+ throw error;
43
+ }
44
+ };
45
+ };
@@ -1,3 +1,4 @@
1
+ import { DbType } from "../databases/db-type.type.js";
1
2
  export interface IBaseApiConfig {
2
3
  appName: string;
3
4
  env: string;
@@ -5,6 +6,10 @@ export interface IBaseApiConfig {
5
6
  clientSecret: string;
6
7
  database: {
7
8
  name?: string;
9
+ host?: string;
10
+ port?: number;
11
+ user?: string;
12
+ password?: string;
8
13
  };
9
14
  externalPort?: number;
10
15
  internalPort?: number;
@@ -19,6 +24,7 @@ export interface IBaseApiConfig {
19
24
  isMultiTenant: boolean;
20
25
  metaOrgName?: string;
21
26
  metaOrgCode?: string;
27
+ dbType?: DbType;
22
28
  };
23
29
  adminUser?: {
24
30
  email: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.42",
3
+ "version": "0.1.46",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
6
  "scripts": {