@loomcore/api 0.0.20 → 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 };
@@ -266,8 +266,9 @@ export class AuthService extends GenericApiService {
266
266
  if (!entityUtils.isValidObjectId(userId)) {
267
267
  throw new BadRequestError('userId is not a valid ObjectId');
268
268
  }
269
- const updates = { _lastLoggedIn: moment().utc().toDate() };
270
- await this.partialUpdateById(EmptyUserContext, userId, updates);
269
+ const updates = { _lastLoggedIn: moment().utc().toISOString() };
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}`);
@@ -1,6 +1,9 @@
1
1
  import { DeleteResult, Document, FindOptions } from 'mongodb';
2
+ import { ValueError } from '@sinclair/typebox/errors';
2
3
  import { IUserContext, IEntity, IPagedResult, QueryOptions } from '@loomcore/common/models';
3
4
  export interface IGenericApiService<T extends IEntity> {
5
+ validate(doc: any, isPartial?: boolean): ValueError[] | null;
6
+ validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
4
7
  getAll(userContext: IUserContext): Promise<T[]>;
5
8
  get(userContext: IUserContext, queryOptions: QueryOptions): Promise<IPagedResult<T>>;
6
9
  getById(userContext: IUserContext, id: string): Promise<T>;
@@ -10,6 +10,7 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
10
10
  protected modelSpec?: IModelSpec;
11
11
  constructor(db: Db, pluralResourceName: string, singularResourceName: string, modelSpec?: IModelSpec);
12
12
  validate(doc: any, isPartial?: boolean): ValueError[] | null;
13
+ validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
13
14
  protected getAdditionalPipelineStages(): any[];
14
15
  protected createAggregationPipeline(userContext: IUserContext, query: any, queryOptions?: QueryOptions): any[];
15
16
  getAll(userContext: IUserContext): Promise<T[]>;
@@ -25,6 +25,20 @@ export class GenericApiService {
25
25
  const validator = isPartial ? this.modelSpec.partialValidator : this.modelSpec.validator;
26
26
  return entityUtils.validate(validator, doc);
27
27
  }
28
+ validateMany(docs, isPartial = false) {
29
+ if (!this.modelSpec) {
30
+ return null;
31
+ }
32
+ const validator = isPartial ? this.modelSpec.partialValidator : this.modelSpec.validator;
33
+ let allErrors = [];
34
+ for (const doc of docs) {
35
+ const errors = entityUtils.validate(validator, doc);
36
+ if (errors && errors.length > 0) {
37
+ allErrors.push(...errors);
38
+ }
39
+ }
40
+ return allErrors.length > 0 ? allErrors : null;
41
+ }
28
42
  getAdditionalPipelineStages() {
29
43
  return [];
30
44
  }
@@ -146,10 +160,8 @@ export class GenericApiService {
146
160
  let createdEntities = [];
147
161
  if (entities.length) {
148
162
  try {
149
- for (const entity of entities) {
150
- const validationErrors = this.validate(entity);
151
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
152
- }
163
+ const validationErrors = this.validateMany(entities);
164
+ entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
153
165
  const preparedEntities = await this.onBeforeCreate(userContext, entities);
154
166
  const insertResult = await this.collection.insertMany(preparedEntities);
155
167
  if (insertResult.insertedIds) {
@@ -325,9 +337,13 @@ export class GenericApiService {
325
337
  const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
326
338
  return transformedEntity;
327
339
  }
328
- stripSenderProvidedSystemProperties(doc) {
340
+ stripSenderProvidedSystemProperties(userContext, doc) {
341
+ const isSystemUser = userContext.user?._id === 'system';
342
+ if (isSystemUser) {
343
+ return;
344
+ }
329
345
  for (const key in doc) {
330
- if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_')) {
346
+ if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
331
347
  delete doc[key];
332
348
  }
333
349
  }
@@ -344,7 +360,7 @@ export class GenericApiService {
344
360
  }
345
361
  async prepareEntity(userContext, entity, isCreate) {
346
362
  const preparedEntity = _.clone(entity);
347
- this.stripSenderProvidedSystemProperties(preparedEntity);
363
+ this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
348
364
  if (this.modelSpec?.isAuditable) {
349
365
  if (isCreate) {
350
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.20",
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",