@loomcore/api 0.0.21 → 0.0.22

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.
@@ -2,6 +2,8 @@ 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 createMetaOrg(): Promise<void>;
6
+ declare function deleteMetaOrg(): Promise<void>;
5
7
  declare function setupTestUser(): Promise<{
6
8
  _id: ObjectId;
7
9
  email: string;
@@ -12,17 +14,21 @@ declare function setupTestUser(): Promise<{
12
14
  _updated: Date;
13
15
  _updatedBy: string;
14
16
  }>;
15
- declare function deleteTestUser(): Promise<null>;
17
+ declare function deleteTestUser(): Promise<any[]>;
16
18
  declare function simulateloginWithTestUser(): Promise<string>;
17
19
  declare function getAuthToken(): string;
18
20
  declare function verifyToken(token: string): any;
19
21
  declare function getTestUser(): Partial<IUser>;
20
22
  declare function configureJwtSecret(): void;
21
23
  declare function loginWithTestUser(agent: any): Promise<string>;
24
+ declare function cleanup(): Promise<void>;
22
25
  declare const testUtils: {
26
+ cleanup: typeof cleanup;
23
27
  configureJwtSecret: typeof configureJwtSecret;
24
28
  constDeviceIdCookie: string;
25
29
  createIndexes: typeof createIndexes;
30
+ createMetaOrg: typeof createMetaOrg;
31
+ deleteMetaOrg: typeof deleteMetaOrg;
26
32
  deleteTestUser: typeof deleteTestUser;
27
33
  getAuthToken: typeof getAuthToken;
28
34
  getTestUser: typeof getTestUser;
@@ -43,9 +43,43 @@ async function createIndexes(db) {
43
43
  createIndexes: "users", indexes: [{ key: { email: 1 }, name: 'email_index', unique: true, collation: { locale: 'en', strength: 1 } }]
44
44
  });
45
45
  }
46
+ async function createMetaOrg() {
47
+ if (!db || !collections.organizations) {
48
+ throw new Error('Database not initialized. Call initialize() first.');
49
+ }
50
+ try {
51
+ const existingMetaOrg = await collections.organizations.findOne({ isMetaOrg: true });
52
+ if (!existingMetaOrg) {
53
+ const metaOrgInsertResult = await collections.organizations.insertOne({
54
+ _id: new ObjectId(),
55
+ name: 'Meta Organization',
56
+ isMetaOrg: true,
57
+ _created: new Date(),
58
+ _createdBy: 'system',
59
+ _updated: new Date(),
60
+ _updatedBy: 'system'
61
+ });
62
+ }
63
+ }
64
+ catch (error) {
65
+ console.log('Error in createMetaOrg:', error);
66
+ throw error;
67
+ }
68
+ }
69
+ async function deleteMetaOrg() {
70
+ if (!collections.organizations) {
71
+ return Promise.resolve();
72
+ }
73
+ try {
74
+ await collections.organizations.deleteOne({ isMetaOrg: true });
75
+ }
76
+ catch (error) {
77
+ console.log('Error deleting meta org:', error);
78
+ }
79
+ }
46
80
  async function setupTestUser() {
47
81
  try {
48
- const result = await deleteTestUser();
82
+ await deleteTestUser();
49
83
  return createTestUser();
50
84
  }
51
85
  catch (error) {
@@ -91,11 +125,14 @@ async function createTestUser() {
91
125
  }
92
126
  }
93
127
  function deleteTestUser() {
94
- let promise = Promise.resolve(null);
128
+ let promises = [];
95
129
  if (testUser) {
96
- promise = collections.users.deleteOne({ _id: testUser._id });
130
+ promises.push(collections.users.deleteOne({ _id: testUser._id }));
97
131
  }
98
- return promise;
132
+ if (collections.organizations) {
133
+ promises.push(collections.organizations.deleteOne({ _id: new ObjectId(testOrgId) }));
134
+ }
135
+ return Promise.all(promises);
99
136
  }
100
137
  async function simulateloginWithTestUser() {
101
138
  const req = {
@@ -156,10 +193,22 @@ async function loginWithTestUser(agent) {
156
193
  const authorizationHeaderValue = `Bearer ${response.body?.data?.tokens?.accessToken}`;
157
194
  return authorizationHeaderValue;
158
195
  }
196
+ async function cleanup() {
197
+ try {
198
+ await deleteTestUser();
199
+ await deleteMetaOrg();
200
+ }
201
+ catch (error) {
202
+ console.log('Error during cleanup:', error);
203
+ }
204
+ }
159
205
  const testUtils = {
206
+ cleanup,
160
207
  configureJwtSecret,
161
208
  constDeviceIdCookie,
162
209
  createIndexes,
210
+ createMetaOrg,
211
+ deleteMetaOrg,
163
212
  deleteTestUser,
164
213
  getAuthToken,
165
214
  getTestUser,
@@ -6,7 +6,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server';
6
6
  import { MongoClient } from 'mongodb';
7
7
  import { initializeTypeBox } from '@loomcore/common/validation';
8
8
  import testUtils from './common-test.utils.js';
9
- import { setBaseApiConfig } from '../config/base-api-config.js';
9
+ import { setBaseApiConfig, initSystemUserContext } from '../config/base-api-config.js';
10
10
  import { errorHandler } from '../middleware/error-handler.js';
11
11
  import { ensureUserContext } from '../middleware/ensure-user-context.js';
12
12
  export class TestExpressApp {
@@ -66,7 +66,9 @@ export class TestExpressApp {
66
66
  this.db = this.client.db();
67
67
  testUtils.initialize(this.db);
68
68
  await testUtils.createIndexes(this.db);
69
+ await testUtils.createMetaOrg();
69
70
  }
71
+ await initSystemUserContext(this.db);
70
72
  if (!this.app) {
71
73
  this.app = express();
72
74
  this.app.use(bodyParser.json());
@@ -87,6 +89,7 @@ export class TestExpressApp {
87
89
  this.app.use(errorHandler);
88
90
  }
89
91
  static async cleanup() {
92
+ await testUtils.cleanup();
90
93
  if (this.client) {
91
94
  await this.client.close();
92
95
  }
@@ -1,3 +1,5 @@
1
+ import { Db } from 'mongodb';
1
2
  import { IBaseApiConfig } from '../models/index.js';
2
3
  export declare let config: IBaseApiConfig;
3
4
  export declare function setBaseApiConfig(apiConfig: IBaseApiConfig): void;
5
+ export declare function initSystemUserContext(db: Db): Promise<void>;
@@ -1,5 +1,7 @@
1
+ import { EmptyUserContext, initializeSystemUserContext } from '@loomcore/common/models';
1
2
  export let config;
2
3
  let isConfigSet = false;
4
+ let isSystemUserContextSet = false;
3
5
  function copyOnlyBaseApiConfigProperties(obj) {
4
6
  const baseConfig = {};
5
7
  Object.keys(obj).forEach((key) => {
@@ -16,3 +18,23 @@ export function setBaseApiConfig(apiConfig) {
16
18
  console.warn('BaseApiConfig data has already been set. Ignoring subsequent calls to setBaseApiConfig.');
17
19
  }
18
20
  }
21
+ export async function initSystemUserContext(db) {
22
+ if (!isConfigSet) {
23
+ throw new Error('BaseApiConfig has not been set. Call setBaseApiConfig first.');
24
+ }
25
+ if (!isSystemUserContextSet) {
26
+ const systemEmail = config.email.systemEmailAddress || 'system@example.com';
27
+ let metaOrgId = undefined;
28
+ if (config.app.isMultiTenant) {
29
+ const { OrganizationService } = await import('../services/organization.service.js');
30
+ const organizationService = new OrganizationService(db);
31
+ const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
32
+ metaOrgId = metaOrg._id;
33
+ }
34
+ initializeSystemUserContext(systemEmail, metaOrgId);
35
+ isSystemUserContextSet = true;
36
+ }
37
+ else if (config.env !== 'test') {
38
+ console.warn('SystemUserContext has already been set. Ignoring subsequent calls to initSystemUserContext.');
39
+ }
40
+ }
@@ -26,5 +26,6 @@ export interface IBaseApiConfig {
26
26
  email: {
27
27
  sendGridApiKey?: string;
28
28
  fromAddress?: string;
29
+ systemEmailAddress?: string;
29
30
  };
30
31
  }
@@ -1,7 +1,7 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import moment from 'moment';
3
3
  import crypto from 'crypto';
4
- import { EmptyUserContext, passwordValidator, UserSpec } from '@loomcore/common/models';
4
+ import { EmptyUserContext, passwordValidator, UserSpec, getSystemUserContext } from '@loomcore/common/models';
5
5
  import { entityUtils } from '@loomcore/common/utils';
6
6
  import { BadRequestError, ServerError } from '../errors/index.js';
7
7
  import { JwtService, EmailService } from './index.js';
@@ -49,7 +49,7 @@ export class AuthService extends GenericApiService {
49
49
  refreshToken: refreshTokenObject.token,
50
50
  expiresOn: accessTokenExpiresOn
51
51
  };
52
- this.updateLastLoggedIn(userContext.user._id.toString())
52
+ this.updateLastLoggedIn(userContext.user._id)
53
53
  .catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
54
54
  this.transformSingle(userContext.user);
55
55
  loginResponse = { tokens: tokenResponse, userContext };
@@ -267,7 +267,8 @@ export class AuthService extends GenericApiService {
267
267
  throw new BadRequestError('userId is not a valid ObjectId');
268
268
  }
269
269
  const updates = { _lastLoggedIn: moment().utc().toISOString() };
270
- await this.partialUpdateById(EmptyUserContext, userId, updates);
270
+ const systemUserContext = getSystemUserContext();
271
+ await this.partialUpdateById(systemUserContext, userId, updates);
271
272
  }
272
273
  catch (error) {
273
274
  console.log(`Failed to update lastLoggedIn for user ${userId}: ${error}`);
@@ -337,7 +337,11 @@ export class GenericApiService {
337
337
  const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
338
338
  return transformedEntity;
339
339
  }
340
- stripSenderProvidedSystemProperties(doc) {
340
+ stripSenderProvidedSystemProperties(userContext, doc) {
341
+ const isSystemUser = userContext.user?._id === 'system';
342
+ if (isSystemUser) {
343
+ return;
344
+ }
341
345
  for (const key in doc) {
342
346
  if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
343
347
  delete doc[key];
@@ -356,7 +360,7 @@ export class GenericApiService {
356
360
  }
357
361
  async prepareEntity(userContext, entity, isCreate) {
358
362
  const preparedEntity = _.clone(entity);
359
- this.stripSenderProvidedSystemProperties(preparedEntity);
363
+ this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
360
364
  if (this.modelSpec?.isAuditable) {
361
365
  if (isCreate) {
362
366
  this.auditForCreate(userContext, preparedEntity);
@@ -5,4 +5,5 @@ export declare class OrganizationService extends GenericApiService<IOrganization
5
5
  constructor(db: Db);
6
6
  getAuthTokenByRepoCode(userContext: IUserContext, orgId: string): Promise<string | null | undefined>;
7
7
  validateRepoAuthToken(userContext: IUserContext, orgCode: string, authToken: string): Promise<string | null>;
8
+ getMetaOrg(userContext: IUserContext): Promise<IOrganization>;
8
9
  }
@@ -13,4 +13,8 @@ export class OrganizationService extends GenericApiService {
13
13
  const orgId = org.authToken === authToken ? org._id.toString() : null;
14
14
  return orgId;
15
15
  }
16
+ async getMetaOrg(userContext) {
17
+ const org = await this.findOne(userContext, { isMetaOrg: true });
18
+ return org;
19
+ }
16
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb",
6
6
  "scripts": {
@@ -44,7 +44,7 @@
44
44
  "jsonwebtoken": "^9.0.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "@loomcore/common": "^0.0.9",
47
+ "@loomcore/common": "^0.0.12",
48
48
  "@sinclair/typebox": "^0.34.31",
49
49
  "cookie-parser": "^1.4.6",
50
50
  "cors": "^2.8.5",