@loomcore/api 0.0.5 → 0.0.7

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.
@@ -1,8 +1,17 @@
1
- import { Db } from 'mongodb';
1
+ import { Db, ObjectId } from 'mongodb';
2
2
  import { IUser, IUserContext } from '@loomcore/common/models';
3
3
  declare function initialize(database: Db): void;
4
4
  declare function createIndexes(db: Db): Promise<void>;
5
- declare function setupTestUser(): Promise<Partial<IUser>>;
5
+ declare function setupTestUser(): Promise<{
6
+ _id: ObjectId;
7
+ email: string;
8
+ password: string;
9
+ _orgId: string;
10
+ _created: Date;
11
+ _createdBy: string;
12
+ _updated: Date;
13
+ _updatedBy: string;
14
+ }>;
6
15
  declare function deleteTestUser(): Promise<null>;
7
16
  declare function simulateloginWithTestUser(): Promise<string>;
8
17
  declare function getAuthToken(): string;
@@ -71,7 +71,7 @@ async function createTestUser() {
71
71
  });
72
72
  }
73
73
  const localTestUser = {
74
- _id: testUserId,
74
+ _id: new ObjectId(testUserId),
75
75
  email: testUserEmail,
76
76
  password: hashedAndSaltedTestUserPassword,
77
77
  _orgId: testOrgId,
@@ -82,7 +82,7 @@ async function createTestUser() {
82
82
  };
83
83
  const insertResult = await collections.users.insertOne(localTestUser);
84
84
  delete localTestUser['password'];
85
- testUser = localTestUser;
85
+ testUser = { ...localTestUser, _id: localTestUser._id.toString() };
86
86
  return localTestUser;
87
87
  }
88
88
  catch (error) {
@@ -1,3 +1,3 @@
1
1
  import { IBaseApiConfig } from '../models/index.js';
2
2
  export declare let config: IBaseApiConfig;
3
- export declare function setBaseApiConfig(baseApiConfig: IBaseApiConfig): void;
3
+ export declare function setBaseApiConfig(apiConfig: IBaseApiConfig): void;
@@ -7,9 +7,9 @@ function copyOnlyBaseApiConfigProperties(obj) {
7
7
  });
8
8
  return baseConfig;
9
9
  }
10
- export function setBaseApiConfig(baseApiConfig) {
10
+ export function setBaseApiConfig(apiConfig) {
11
11
  if (!isConfigSet) {
12
- config = copyOnlyBaseApiConfigProperties(baseApiConfig);
12
+ config = copyOnlyBaseApiConfigProperties(apiConfig);
13
13
  isConfigSet = true;
14
14
  }
15
15
  else if (config.env !== 'test') {
@@ -1,2 +1,4 @@
1
1
  export * from './api.controller.js';
2
2
  export * from './auth.controller.js';
3
+ export * from './organizations.controller.js';
4
+ export * from './users.controller.js';
@@ -1,2 +1,4 @@
1
1
  export * from './api.controller.js';
2
2
  export * from './auth.controller.js';
3
+ export * from './organizations.controller.js';
4
+ export * from './users.controller.js';
@@ -0,0 +1,12 @@
1
+ import { Application, NextFunction, Request, Response } from 'express';
2
+ import { Db } from 'mongodb';
3
+ import { IOrganization } from '@loomcore/common/models';
4
+ import { ApiController } from './api.controller.js';
5
+ import { OrganizationService } from '../services/index.js';
6
+ export declare class OrganizationsController extends ApiController<IOrganization> {
7
+ orgService: OrganizationService;
8
+ constructor(app: Application, db: Db);
9
+ mapRoutes(app: Application): void;
10
+ getByName(req: Request, res: Response, next: NextFunction): Promise<void>;
11
+ getByCode(req: Request, res: Response, next: NextFunction): Promise<void>;
12
+ }
@@ -0,0 +1,47 @@
1
+ import { ApiController } from './api.controller.js';
2
+ import { isAuthenticated } from '../middleware/index.js';
3
+ import { apiUtils } from '../utils/index.js';
4
+ import { BadRequestError } from '../errors/index.js';
5
+ import { OrganizationService } from '../services/index.js';
6
+ export class OrganizationsController extends ApiController {
7
+ orgService;
8
+ constructor(app, db) {
9
+ const orgService = new OrganizationService(db);
10
+ super('organizations', app, orgService);
11
+ this.orgService = orgService;
12
+ }
13
+ mapRoutes(app) {
14
+ super.mapRoutes(app);
15
+ app.get(`/api/${this.slug}/get-by-name/:name`, isAuthenticated, this.getByName.bind(this));
16
+ app.get(`/api/${this.slug}/get-by-code/:code`, isAuthenticated, this.getByCode.bind(this));
17
+ }
18
+ async getByName(req, res, next) {
19
+ console.log('in OrganizationController.getByName');
20
+ let name = req.params?.name;
21
+ try {
22
+ res.set('Content-Type', 'application/json');
23
+ const entity = await this.orgService.findOne(req.userContext, { name: { $regex: new RegExp(`^${name}$`, 'i') } });
24
+ if (!entity)
25
+ throw new BadRequestError('Name not found');
26
+ apiUtils.apiResponse(res, 200, { data: entity });
27
+ }
28
+ catch (err) {
29
+ next(err);
30
+ return;
31
+ }
32
+ }
33
+ async getByCode(req, res, next) {
34
+ let code = req.params?.code;
35
+ try {
36
+ res.set('Content-Type', 'application/json');
37
+ const entity = await this.orgService.findOne(req.userContext, { code: code });
38
+ if (!entity)
39
+ throw new BadRequestError('Code not found');
40
+ apiUtils.apiResponse(res, 200, { data: entity });
41
+ }
42
+ catch (err) {
43
+ next(err);
44
+ return;
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,9 @@
1
+ import { Db } from 'mongodb';
2
+ import { Application } from 'express';
3
+ import { IUser } from '@loomcore/common/models';
4
+ import { ApiController } from './api.controller.js';
5
+ export declare class UsersController extends ApiController<IUser> {
6
+ private userService;
7
+ constructor(app: Application, db: Db);
8
+ mapRoutes(app: Application): void;
9
+ }
@@ -0,0 +1,22 @@
1
+ import { UserSpec, PublicUserSchema } from '@loomcore/common/models';
2
+ import { ApiController } from './api.controller.js';
3
+ import { isAuthenticated } from '../middleware/index.js';
4
+ import { UserService } from '../services/index.js';
5
+ export class UsersController extends ApiController {
6
+ userService;
7
+ constructor(app, db) {
8
+ const userService = new UserService(db);
9
+ super('users', app, userService, 'user', UserSpec, PublicUserSchema);
10
+ this.userService = userService;
11
+ }
12
+ mapRoutes(app) {
13
+ app.get(`/api/${this.slug}`, isAuthenticated, this.get.bind(this));
14
+ app.get(`/api/${this.slug}/all`, isAuthenticated, this.getAll.bind(this));
15
+ app.get(`/api/${this.slug}/find`, isAuthenticated, this.get.bind(this));
16
+ app.get(`/api/${this.slug}/count`, isAuthenticated, this.getCount.bind(this));
17
+ app.get(`/api/${this.slug}/:id`, isAuthenticated, this.getById.bind(this));
18
+ app.post(`/api/${this.slug}`, isAuthenticated, this.create.bind(this));
19
+ app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
20
+ app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
21
+ }
22
+ }
@@ -4,5 +4,7 @@ export * from './generic-api.service.js';
4
4
  export * from './generic-api-service.interface.js';
5
5
  export * from './jwt.service.js';
6
6
  export * from './multi-tenant-api.service.js';
7
+ export * from './organization.service.js';
7
8
  export * from './password-reset-token.service.js';
8
9
  export * from './tenant-query-decorator.js';
10
+ export * from './user.service.js';
@@ -4,5 +4,7 @@ export * from './generic-api.service.js';
4
4
  export * from './generic-api-service.interface.js';
5
5
  export * from './jwt.service.js';
6
6
  export * from './multi-tenant-api.service.js';
7
+ export * from './organization.service.js';
7
8
  export * from './password-reset-token.service.js';
8
9
  export * from './tenant-query-decorator.js';
10
+ export * from './user.service.js';
@@ -0,0 +1,8 @@
1
+ import { Db } from 'mongodb';
2
+ import { GenericApiService } from './generic-api.service.js';
3
+ import { IOrganization, IUserContext } from '@loomcore/common/models';
4
+ export declare class OrganizationService extends GenericApiService<IOrganization> {
5
+ constructor(db: Db);
6
+ getAuthTokenByRepoCode(userContext: IUserContext, orgId: string): Promise<string | null | undefined>;
7
+ validateRepoAuthToken(userContext: IUserContext, orgCode: string, authToken: string): Promise<string | null>;
8
+ }
@@ -0,0 +1,16 @@
1
+ import { GenericApiService } from './generic-api.service.js';
2
+ import { OrganizationSpec } from '@loomcore/common/models';
3
+ export class OrganizationService extends GenericApiService {
4
+ constructor(db) {
5
+ super(db, 'organizations', 'organization', OrganizationSpec);
6
+ }
7
+ async getAuthTokenByRepoCode(userContext, orgId) {
8
+ const org = await this.getById(userContext, orgId);
9
+ return org ? org.authToken : null;
10
+ }
11
+ async validateRepoAuthToken(userContext, orgCode, authToken) {
12
+ const org = await this.findOne(userContext, { code: orgCode });
13
+ const orgId = org.authToken === authToken ? org._id.toString() : null;
14
+ return orgId;
15
+ }
16
+ }
@@ -0,0 +1,10 @@
1
+ import { Db } from 'mongodb';
2
+ import { IUser, IUserContext } from '@loomcore/common/models';
3
+ import { MultiTenantApiService } from '../services/index.js';
4
+ export declare class UserService extends MultiTenantApiService<IUser> {
5
+ constructor(db: Db);
6
+ fullUpdateById(userContext: IUserContext, id: string, entity: IUser): Promise<any>;
7
+ protected prepareEntity(userContext: IUserContext, entity: IUser, isCreate: boolean): Promise<IUser | Partial<IUser>>;
8
+ transformList(users: IUser[]): IUser[];
9
+ transformSingle(user: IUser): IUser;
10
+ }
@@ -0,0 +1,25 @@
1
+ import { Value } from '@sinclair/typebox/value';
2
+ import { UserSpec, PublicUserSchema } from '@loomcore/common/models';
3
+ import { MultiTenantApiService } from '../services/index.js';
4
+ import { ServerError } from '../errors/index.js';
5
+ export class UserService extends MultiTenantApiService {
6
+ constructor(db) {
7
+ super(db, 'users', 'user', UserSpec);
8
+ }
9
+ async fullUpdateById(userContext, id, entity) {
10
+ throw new ServerError('Cannot full update a user. Either use PATCH or /auth/change-password to update password.');
11
+ }
12
+ async prepareEntity(userContext, entity, isCreate) {
13
+ const preparedEntity = await super.prepareEntity(userContext, entity, isCreate);
14
+ if (!isCreate) {
15
+ return Value.Clean(PublicUserSchema, preparedEntity);
16
+ }
17
+ return preparedEntity;
18
+ }
19
+ transformList(users) {
20
+ return super.transformList(users);
21
+ }
22
+ transformSingle(user) {
23
+ return super.transformSingle(user);
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ import { Application } from 'express';
2
+ import { Db, MongoClient } from "mongodb";
3
+ import { Server } from "http";
4
+ import { IBaseApiConfig } from "../models/index.js";
5
+ type RouteSetupFunction = (app: Application, db: Db, config: IBaseApiConfig) => void;
6
+ declare function setupExpressApp(db: Db, config: IBaseApiConfig, setupRoutes: RouteSetupFunction): Application;
7
+ declare function performGracefulShutdown(event: any, mongoClient: MongoClient | null, externalServer: Server | null, internalServer: Server | null): void;
8
+ export declare const expressUtils: {
9
+ setupExpressApp: typeof setupExpressApp;
10
+ performGracefulShutdown: typeof performGracefulShutdown;
11
+ };
12
+ export {};
@@ -0,0 +1,96 @@
1
+ import express from 'express';
2
+ import cookieParser from "cookie-parser";
3
+ import bodyParser from "body-parser";
4
+ import cors from "cors";
5
+ import { NotFoundError } from "../errors/not-found.error.js";
6
+ import { errorHandler } from "../middleware/error-handler.js";
7
+ import { ensureUserContext } from '../middleware/ensure-user-context.js';
8
+ function setupExpressApp(db, config, setupRoutes) {
9
+ const app = express();
10
+ app.use((req, res, next) => {
11
+ if (req.path !== '/api/health' && process.env.NODE_ENV !== 'test') {
12
+ console.log(`[${new Date().toLocaleString('en-US', { timeZone: 'America/Chicago' })}] INCOMING REQUEST: ${req.method} ${req.path}`);
13
+ }
14
+ next();
15
+ });
16
+ app.use(bodyParser.json());
17
+ app.use(cookieParser());
18
+ app.use(cors({
19
+ origin: config.corsAllowedOrigins,
20
+ credentials: true
21
+ }));
22
+ app.use(ensureUserContext);
23
+ setupRoutes(app, db, config);
24
+ app.all('*', async (req, res) => {
25
+ throw new NotFoundError(`Requested path, ${req.path}, Not Found`);
26
+ });
27
+ app.use(errorHandler);
28
+ return app;
29
+ }
30
+ function performGracefulShutdown(event, mongoClient, externalServer, internalServer) {
31
+ const closeMongoConnection = async () => {
32
+ if (mongoClient) {
33
+ console.log('closing mongodb connection');
34
+ try {
35
+ await mongoClient.close();
36
+ console.log('MongoDB connection closed successfully');
37
+ }
38
+ catch (err) {
39
+ console.error('Error closing MongoDB connection:', err);
40
+ }
41
+ }
42
+ };
43
+ const shutdownServers = new Promise((resolve) => {
44
+ let serversClosedCount = 0;
45
+ const totalServers = (externalServer ? 1 : 0) + (internalServer ? 1 : 0);
46
+ const onServerClosed = () => {
47
+ serversClosedCount++;
48
+ if (serversClosedCount >= totalServers) {
49
+ resolve();
50
+ }
51
+ };
52
+ if (totalServers === 0) {
53
+ resolve();
54
+ return;
55
+ }
56
+ if (externalServer) {
57
+ console.log('Closing external HTTP server...');
58
+ externalServer.close((err) => {
59
+ if (err)
60
+ console.error('Error closing external server:', err);
61
+ console.log('External HTTP server closed');
62
+ onServerClosed();
63
+ });
64
+ }
65
+ if (internalServer) {
66
+ console.log('Closing internal HTTP server...');
67
+ internalServer.close((err) => {
68
+ if (err)
69
+ console.error('Error closing internal server:', err);
70
+ console.log('Internal HTTP server closed');
71
+ onServerClosed();
72
+ });
73
+ }
74
+ setTimeout(() => {
75
+ console.log('Server shutdown timeout reached, proceeding with MongoDB cleanup');
76
+ resolve();
77
+ }, 5000);
78
+ });
79
+ Promise.race([
80
+ shutdownServers.then(() => closeMongoConnection()),
81
+ new Promise(resolve => {
82
+ setTimeout(async () => {
83
+ console.log('Ensuring MongoDB connection is closed before exit');
84
+ await closeMongoConnection();
85
+ resolve();
86
+ }, 6000);
87
+ })
88
+ ]).then(() => {
89
+ console.log('Cleanup complete, exiting process');
90
+ process.exit(0);
91
+ });
92
+ }
93
+ export const expressUtils = {
94
+ setupExpressApp,
95
+ performGracefulShutdown,
96
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb",
6
6
  "scripts": {
@@ -47,6 +47,7 @@
47
47
  "@loomcore/common": "^0.0.7",
48
48
  "@sinclair/typebox": "^0.34.31",
49
49
  "cookie-parser": "^1.4.6",
50
+ "cors": "^2.8.5",
50
51
  "express": "^5.1.0",
51
52
  "lodash": "^4.17.21",
52
53
  "moment": "^2.30.1",
@@ -54,6 +55,7 @@
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/cookie-parser": "^1.4.7",
58
+ "@types/cors": "^2.8.18",
57
59
  "@types/express": "^5.0.1",
58
60
  "@types/jsonwebtoken": "^9.0.9",
59
61
  "@types/lodash": "^4.17.13",