@loomcore/api 0.1.36 → 0.1.38

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,15 @@
1
+ export type DbType = 'postgres' | 'mongo';
2
+ export interface MigrationConfig {
3
+ dbType: DbType;
4
+ dbUrl: string;
5
+ migrationsDir: string;
6
+ }
7
+ export declare class MigrationRunner {
8
+ private config;
9
+ constructor(config: MigrationConfig);
10
+ private parseSql;
11
+ private getMigrator;
12
+ private wipeDatabase;
13
+ run(command: 'up' | 'down' | 'reset'): Promise<void>;
14
+ private closeConnection;
15
+ }
@@ -0,0 +1,118 @@
1
+ import { Umzug, MongoDBStorage } from 'umzug';
2
+ import { Pool } from 'pg';
3
+ import { MongoClient } from 'mongodb';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ export class MigrationRunner {
7
+ config;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ parseSql(content) {
12
+ const upMatch = content.match(/--\s*up\s([\s\S]+?)(?=--\s*down|$)/i);
13
+ const downMatch = content.match(/--\s*down\s([\s\S]+)/i);
14
+ return {
15
+ up: upMatch ? upMatch[1].trim() : '',
16
+ down: downMatch ? downMatch[1].trim() : ''
17
+ };
18
+ }
19
+ async getMigrator() {
20
+ const { dbType, dbUrl, migrationsDir } = this.config;
21
+ if (dbType === 'postgres') {
22
+ const pool = new Pool({ connectionString: dbUrl });
23
+ return new Umzug({
24
+ migrations: {
25
+ glob: path.join(migrationsDir, '*.sql'),
26
+ resolve: ({ name, path: filePath, context }) => {
27
+ const content = fs.readFileSync(filePath, 'utf8');
28
+ const { up, down } = this.parseSql(content);
29
+ return {
30
+ name,
31
+ up: async () => context.query(up),
32
+ down: async () => context.query(down)
33
+ };
34
+ }
35
+ },
36
+ context: pool,
37
+ storage: {
38
+ async executed({ context }) {
39
+ await context.query(`CREATE TABLE IF NOT EXISTS migrations (name text)`);
40
+ const result = await context.query(`SELECT name FROM migrations`);
41
+ return result.rows.map((r) => r.name);
42
+ },
43
+ async logMigration({ name, context }) {
44
+ await context.query(`INSERT INTO migrations (name) VALUES ($1)`, [name]);
45
+ },
46
+ async unlogMigration({ name, context }) {
47
+ await context.query(`DELETE FROM migrations WHERE name = $1`, [name]);
48
+ }
49
+ },
50
+ logger: console,
51
+ });
52
+ }
53
+ else if (dbType === 'mongo') {
54
+ const client = await MongoClient.connect(dbUrl);
55
+ const db = client.db();
56
+ return new Umzug({
57
+ migrations: { glob: path.join(migrationsDir, '*.ts') },
58
+ context: db,
59
+ storage: new MongoDBStorage({ collection: db.collection('migrations') }),
60
+ logger: console,
61
+ });
62
+ }
63
+ throw new Error(`Unsupported DB_TYPE: ${dbType}`);
64
+ }
65
+ async wipeDatabase() {
66
+ const { dbType, dbUrl } = this.config;
67
+ console.log(`⚠️ Wiping ${dbType} database...`);
68
+ if (dbType === 'postgres') {
69
+ const pool = new Pool({ connectionString: dbUrl });
70
+ try {
71
+ await pool.query('DROP SCHEMA public CASCADE; CREATE SCHEMA public;');
72
+ }
73
+ finally {
74
+ await pool.end();
75
+ }
76
+ }
77
+ else if (dbType === 'mongo') {
78
+ const client = await MongoClient.connect(dbUrl);
79
+ await client.db().dropDatabase();
80
+ await client.close();
81
+ }
82
+ console.log('✅ Database wiped.');
83
+ }
84
+ async run(command) {
85
+ try {
86
+ if (command === 'reset') {
87
+ await this.wipeDatabase();
88
+ console.log('🚀 Restarting migrations...');
89
+ const migrator = await this.getMigrator();
90
+ await migrator.up();
91
+ await this.closeConnection(migrator);
92
+ console.log('✅ Reset complete.');
93
+ return;
94
+ }
95
+ const migrator = await this.getMigrator();
96
+ switch (command) {
97
+ case 'up':
98
+ await migrator.up();
99
+ console.log('✅ Migrations up to date.');
100
+ break;
101
+ case 'down':
102
+ await migrator.down();
103
+ console.log('✅ Reverted last migration.');
104
+ break;
105
+ }
106
+ await this.closeConnection(migrator);
107
+ }
108
+ catch (err) {
109
+ console.error('❌ Migration failed:', err);
110
+ process.exit(1);
111
+ }
112
+ }
113
+ async closeConnection(migrator) {
114
+ if (this.config.dbType === 'postgres') {
115
+ await migrator.context.end();
116
+ }
117
+ }
118
+ }
@@ -11,6 +11,7 @@ import { OrganizationService } from './organization.service.js';
11
11
  import { passwordUtils } from '../utils/index.js';
12
12
  import { config } from '../config/index.js';
13
13
  import { refreshTokenModelSpec } from '../models/refresh-token.model.js';
14
+ import { getUserContextAuthorizations } from './utils/getUserContextAuthorizations.util.js';
14
15
  export class AuthService extends MultiTenantApiService {
15
16
  refreshTokenService;
16
17
  passwordResetTokenService;
@@ -34,10 +35,11 @@ export class AuthService extends MultiTenantApiService {
34
35
  if (!passwordsMatch) {
35
36
  throw new BadRequestError('Invalid Credentials');
36
37
  }
38
+ const authorizations = await getUserContextAuthorizations(this.database, user);
37
39
  const userContext = {
38
40
  user: user,
39
41
  organization: organization ?? undefined,
40
- authorizations: []
42
+ authorizations: authorizations
41
43
  };
42
44
  const deviceId = this.getAndSetDeviceIdCookie(req, res);
43
45
  const loginResponse = await this.logUserIn(userContext, deviceId);
@@ -68,20 +70,19 @@ export class AuthService extends MultiTenantApiService {
68
70
  if (!rawUser) {
69
71
  return null;
70
72
  }
71
- const dbPostprocessed = this.database.postprocessEntity(rawUser, this.modelSpec.fullSchema);
72
- return dbPostprocessed;
73
+ return this.database.postprocessEntity(rawUser, this.modelSpec.fullSchema);
73
74
  }
74
75
  async createUser(userContext, user) {
75
- if (userContext.user?._id === 'system') {
76
+ if (userContext.user._id === 'system') {
77
+ if (config.app.isMultiTenant && userContext.organization?._id !== user._orgId) {
78
+ throw new BadRequestError('User is not authorized to create a user in this organization');
79
+ }
76
80
  if (user._orgId) {
77
81
  const org = await this.organizationService.findOne(userContext, { filters: { _id: { eq: user._orgId } } });
78
82
  if (!org) {
79
83
  throw new BadRequestError('The specified organization does not exist');
80
84
  }
81
85
  }
82
- if (config.app.isMultiTenant && userContext.organization?._id !== user._orgId) {
83
- throw new BadRequestError('User is not authorized to create a user in this organization');
84
- }
85
86
  }
86
87
  if (user.email) {
87
88
  const existingUser = await this.getUserByEmail(user.email);
@@ -98,10 +99,11 @@ export class AuthService extends MultiTenantApiService {
98
99
  const systemUserContext = getSystemUserContext();
99
100
  const user = await this.getById(systemUserContext, activeRefreshToken.userId);
100
101
  const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
102
+ const authorizations = await getUserContextAuthorizations(this.database, user);
101
103
  const userContext = {
102
104
  user: user,
103
105
  organization: organization ?? undefined,
104
- authorizations: []
106
+ authorizations: authorizations
105
107
  };
106
108
  tokens = await this.createNewTokens(userContext, activeRefreshToken);
107
109
  }
@@ -1,12 +1,8 @@
1
- import { IUser, IUserContext, IPagedResult, IQueryOptions } from '@loomcore/common/models';
1
+ import { IUser, IUserContext } from '@loomcore/common/models';
2
2
  import { MultiTenantApiService } from './index.js';
3
3
  import { IDatabase } from '../databases/models/index.js';
4
4
  export declare class UserService extends MultiTenantApiService<IUser> {
5
5
  constructor(database: IDatabase);
6
6
  fullUpdateById(userContext: IUserContext, id: string, entity: IUser): Promise<IUser>;
7
- getById(userContext: IUserContext, id: string): Promise<IUser>;
8
- get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<IUser>>;
9
- getAll(userContext: IUserContext): Promise<IUser[]>;
10
7
  preprocessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean, allowId?: boolean): Promise<Partial<IUser>>;
11
- private getUserContextAuthorizations;
12
8
  }
@@ -1,8 +1,7 @@
1
1
  import { Value } from '@sinclair/typebox/value';
2
2
  import { UserSpec, PublicUserSchema } from '@loomcore/common/models';
3
3
  import { MultiTenantApiService } from './index.js';
4
- import { IdNotFoundError, ServerError } from '../errors/index.js';
5
- import { PostgresDatabase } from '../databases/postgres/postgres.database.js';
4
+ import { ServerError } from '../errors/index.js';
6
5
  export class UserService extends MultiTenantApiService {
7
6
  constructor(database) {
8
7
  super(database, 'users', 'user', UserSpec);
@@ -10,28 +9,6 @@ export class UserService extends MultiTenantApiService {
10
9
  async fullUpdateById(userContext, id, entity) {
11
10
  throw new ServerError('Cannot full update a user. Either use PATCH or /auth/change-password to update password.');
12
11
  }
13
- async getById(userContext, id) {
14
- const { operations, queryObject } = this.prepareQuery(userContext, {}, []);
15
- const user = await this.database.getById(operations, queryObject, id, this.pluralResourceName);
16
- if (!user) {
17
- throw new IdNotFoundError();
18
- }
19
- return this.postprocessEntity(userContext, user);
20
- }
21
- async get(userContext, queryOptions) {
22
- const { operations, queryObject } = this.prepareQuery(userContext, queryOptions, []);
23
- const pagedResult = await this.database.get(operations, queryObject, this.modelSpec, this.pluralResourceName);
24
- const transformedEntities = (pagedResult.entities || []).map(entity => this.postprocessEntity(userContext, entity));
25
- return {
26
- ...pagedResult,
27
- entities: transformedEntities
28
- };
29
- }
30
- async getAll(userContext) {
31
- const { operations } = this.prepareQuery(userContext, {}, []);
32
- const users = await this.database.getAll(operations, this.pluralResourceName);
33
- return users.map(user => this.postprocessEntity(userContext, user));
34
- }
35
12
  async preprocessEntity(userContext, entity, isCreate, allowId = false) {
36
13
  const preparedEntity = await super.preprocessEntity(userContext, entity, isCreate);
37
14
  if (preparedEntity.email) {
@@ -42,12 +19,4 @@ export class UserService extends MultiTenantApiService {
42
19
  }
43
20
  return preparedEntity;
44
21
  }
45
- async getUserContextAuthorizations(user) {
46
- if (!(this.database instanceof PostgresDatabase)) {
47
- return [];
48
- }
49
- const orgId = user._orgId;
50
- const authorizations = await this.database.getUserAuthorizations(user._id, orgId);
51
- return authorizations;
52
- }
53
22
  }
@@ -0,0 +1,3 @@
1
+ import { IUser, IUserContextAuthorization } from "@loomcore/common/models";
2
+ import { IDatabase } from "../../databases/models/index.js";
3
+ export declare function getUserContextAuthorizations(database: IDatabase, user: IUser): Promise<IUserContextAuthorization[]>;
@@ -0,0 +1,9 @@
1
+ import { PostgresDatabase } from "../../databases/postgres/postgres.database.js";
2
+ export async function getUserContextAuthorizations(database, user) {
3
+ if (!(database instanceof PostgresDatabase)) {
4
+ return [];
5
+ }
6
+ const orgId = user._orgId;
7
+ const authorizations = await database.getUserAuthorizations(user._id, orgId);
8
+ return authorizations;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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": {
@@ -62,7 +62,8 @@
62
62
  "moment": "^2.30.1",
63
63
  "mongodb": "^6.16.0",
64
64
  "pg": "^8.15.6",
65
- "rxjs": "^7.8.0"
65
+ "rxjs": "^7.8.0",
66
+ "umzug": "^3.8.2"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@types/cookie-parser": "^1.4.7",