@loomcore/api 0.0.21 → 0.0.23

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
+ }
@@ -1,6 +1,6 @@
1
1
  import { Application, NextFunction, Request, Response } from 'express';
2
2
  import { TSchema } from '@sinclair/typebox';
3
- import { IEntity, IModelSpec } from '@loomcore/common/models';
3
+ import { IEntity, IModelSpec, IUserContext } from '@loomcore/common/models';
4
4
  import { IGenericApiService } from '../services/index.js';
5
5
  export declare abstract class ApiController<T extends IEntity> {
6
6
  protected app: Application;
@@ -11,6 +11,12 @@ export declare abstract class ApiController<T extends IEntity> {
11
11
  protected publicSchema?: TSchema;
12
12
  protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSchema?: TSchema);
13
13
  mapRoutes(app: Application): void;
14
+ protected validate(entity: any, isPartial?: boolean): void;
15
+ protected validateMany(entities: any[], isPartial?: boolean): void;
16
+ protected prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
17
+ protected prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
18
+ protected prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
19
+ protected prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
14
20
  getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
15
21
  get(req: Request, res: Response, next: NextFunction): Promise<void>;
16
22
  getById(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { entityUtils } from '@loomcore/common/utils';
1
2
  import { isAuthenticated } from '../middleware/index.js';
2
3
  import { apiUtils } from '../utils/index.js';
3
4
  export class ApiController {
@@ -26,6 +27,17 @@ export class ApiController {
26
27
  app.patch(`/api/${this.slug}/:id`, isAuthenticated, this.partialUpdateById.bind(this));
27
28
  app.delete(`/api/${this.slug}/:id`, isAuthenticated, this.deleteById.bind(this));
28
29
  }
30
+ validate(entity, isPartial = false) {
31
+ const validationErrors = this.service.validate(entity, isPartial);
32
+ entityUtils.handleValidationResult(validationErrors, `ApiController.validate for ${this.slug}`);
33
+ }
34
+ validateMany(entities, isPartial = false) {
35
+ const validationErrors = this.service.validateMany(entities, isPartial);
36
+ entityUtils.handleValidationResult(validationErrors, `ApiController.validateMany for ${this.slug}`);
37
+ }
38
+ async prepareDataForDb(userContext, entity, isCreate = false) {
39
+ return await this.service.prepareDataForDb(userContext, entity, isCreate);
40
+ }
29
41
  async getAll(req, res, next) {
30
42
  res.set('Content-Type', 'application/json');
31
43
  const entities = await this.service.getAll(req.userContext);
@@ -50,17 +62,23 @@ export class ApiController {
50
62
  }
51
63
  async create(req, res, next) {
52
64
  res.set('Content-Type', 'application/json');
53
- const entity = await this.service.create(req.userContext, req.body);
65
+ this.validate(req.body);
66
+ const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, true);
67
+ const entity = await this.service.create(req.userContext, preparedEntity);
54
68
  apiUtils.apiResponse(res, 201, { data: entity || undefined }, this.modelSpec, this.publicSchema);
55
69
  }
56
70
  async fullUpdateById(req, res, next) {
57
71
  res.set('Content-Type', 'application/json');
58
- const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, req.body);
72
+ this.validate(req.body);
73
+ const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, false);
74
+ const updateResult = await this.service.fullUpdateById(req.userContext, req.params.id, preparedEntity);
59
75
  apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
60
76
  }
61
77
  async partialUpdateById(req, res, next) {
62
78
  res.set('Content-Type', 'application/json');
63
- const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, req.body);
79
+ this.validate(req.body, true);
80
+ const preparedEntity = await this.prepareDataForDb(req.userContext, req.body, false);
81
+ const updateResult = await this.service.partialUpdateById(req.userContext, req.params.id, preparedEntity);
64
82
  apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSchema);
65
83
  }
66
84
  async deleteById(req, res, next) {
@@ -1,4 +1,5 @@
1
- import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSchema, UserContextSpec } from '@loomcore/common/models';
1
+ import { LoginResponseSpec, TokenResponseSpec, UserSpec, PublicUserSchema, UserContextSpec, passwordValidator } from '@loomcore/common/models';
2
+ import { entityUtils } from '@loomcore/common/utils';
2
3
  import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
3
4
  import { isAuthenticated } from '../middleware/index.js';
4
5
  import { apiUtils } from '../utils/index.js';
@@ -28,7 +29,10 @@ export class AuthController {
28
29
  async registerUser(req, res) {
29
30
  const userContext = req.userContext;
30
31
  const body = req.body;
31
- const user = await this.authService.createUser(userContext, body);
32
+ const validationErrors = this.authService.validate(body);
33
+ entityUtils.handleValidationResult(validationErrors, 'AuthController.registerUser');
34
+ const preparedUser = await this.authService.prepareDataForDb(userContext, body, true);
35
+ const user = await this.authService.createUser(userContext, preparedUser);
32
36
  apiUtils.apiResponse(res, 201, { data: user || undefined }, UserSpec, PublicUserSchema);
33
37
  }
34
38
  async requestTokenUsingRefreshToken(req, res, next) {
@@ -51,6 +55,8 @@ export class AuthController {
51
55
  async changePassword(req, res) {
52
56
  const userContext = req.userContext;
53
57
  const body = req.body;
58
+ const validationErrors = entityUtils.validate(passwordValidator, { password: body.password });
59
+ entityUtils.handleValidationResult(validationErrors, 'AuthController.changePassword');
54
60
  const updateResult = await this.authService.changeLoggedInUsersPassword(userContext, body);
55
61
  apiUtils.apiResponse(res, 200, { data: updateResult });
56
62
  }
@@ -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, 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 };
@@ -88,8 +88,6 @@ export class AuthService extends GenericApiService {
88
88
  return tokens;
89
89
  }
90
90
  async changeLoggedInUsersPassword(userContext, body) {
91
- const validationResult = entityUtils.validate(passwordValidator, { password: body.password });
92
- entityUtils.handleValidationResult(validationResult, 'AuthService.changePassword');
93
91
  const queryObject = { _id: new ObjectId(userContext.user._id) };
94
92
  const result = await this.changePassword(userContext, queryObject, body.password);
95
93
  return result;
@@ -266,8 +264,9 @@ export class AuthService extends GenericApiService {
266
264
  if (!entityUtils.isValidObjectId(userId)) {
267
265
  throw new BadRequestError('userId is not a valid ObjectId');
268
266
  }
269
- const updates = { _lastLoggedIn: moment().utc().toISOString() };
270
- await this.partialUpdateById(EmptyUserContext, userId, updates);
267
+ const updates = { _lastLoggedIn: moment().utc().toDate() };
268
+ const systemUserContext = getSystemUserContext();
269
+ await this.partialUpdateById(systemUserContext, userId, updates);
271
270
  }
272
271
  catch (error) {
273
272
  console.log(`Failed to update lastLoggedIn for user ${userId}: ${error}`);
@@ -4,6 +4,10 @@ import { IUserContext, IEntity, IPagedResult, QueryOptions } from '@loomcore/com
4
4
  export interface IGenericApiService<T extends IEntity> {
5
5
  validate(doc: any, isPartial?: boolean): ValueError[] | null;
6
6
  validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
7
+ prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
8
+ prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
9
+ prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
10
+ prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
7
11
  getAll(userContext: IUserContext): Promise<T[]>;
8
12
  get(userContext: IUserContext, queryOptions: QueryOptions): Promise<IPagedResult<T>>;
9
13
  getById(userContext: IUserContext, id: string): Promise<T>;
@@ -30,7 +30,7 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
30
30
  auditForCreate(userContext: IUserContext, doc: any): void;
31
31
  auditForUpdate(userContext: IUserContext, doc: any): void;
32
32
  onBeforeCreate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
33
- onAfterCreate<E extends T | T[]>(userContext: IUserContext | undefined, entities: E): Promise<E | E[]>;
33
+ onAfterCreate<E extends T | T[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
34
34
  onBeforeUpdate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entities: E): Promise<E | E[]>;
35
35
  onAfterUpdate<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext | undefined, entities: E): Promise<E>;
36
36
  onBeforeDelete(userContext: IUserContext, queryObject: any): Promise<any>;
@@ -38,7 +38,10 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
38
38
  transformList(list: any[]): T[];
39
39
  transformSingle(single: any): T;
40
40
  private stripSenderProvidedSystemProperties;
41
- protected preparePayload<E extends T | T[] | Partial<T> | Partial<T>[]>(userContext: IUserContext, entity: E, isCreate?: boolean): Promise<E | E[]>;
41
+ prepareDataForDb(userContext: IUserContext, entity: T, isCreate?: boolean): Promise<T>;
42
+ prepareDataForDb(userContext: IUserContext, entity: Partial<T>, isCreate?: boolean): Promise<Partial<T>>;
43
+ prepareDataForDb(userContext: IUserContext, entity: T[], isCreate?: boolean): Promise<T[]>;
44
+ prepareDataForDb(userContext: IUserContext, entity: Partial<T>[], isCreate?: boolean): Promise<Partial<T>[]>;
42
45
  protected prepareEntity(userContext: IUserContext, entity: T | Partial<T>, isCreate: boolean): Promise<T | Partial<T>>;
43
46
  protected prepareQuery(userContext: IUserContext | undefined, query: any): any;
44
47
  protected prepareQueryOptions(userContext: IUserContext | undefined, queryOptions: QueryOptions): QueryOptions;
@@ -135,8 +135,6 @@ export class GenericApiService {
135
135
  return count;
136
136
  }
137
137
  async create(userContext, entity) {
138
- const validationErrors = this.validate(entity);
139
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.create');
140
138
  let createdEntity = null;
141
139
  try {
142
140
  const preparedEntity = await this.onBeforeCreate(userContext, entity);
@@ -160,8 +158,6 @@ export class GenericApiService {
160
158
  let createdEntities = [];
161
159
  if (entities.length) {
162
160
  try {
163
- const validationErrors = this.validateMany(entities);
164
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.createMany');
165
161
  const preparedEntities = await this.onBeforeCreate(userContext, entities);
166
162
  const insertResult = await this.collection.insertMany(preparedEntities);
167
163
  if (insertResult.insertedIds) {
@@ -182,8 +178,6 @@ export class GenericApiService {
182
178
  if (!entityUtils.isValidObjectId(id)) {
183
179
  throw new BadRequestError('id is not a valid ObjectId');
184
180
  }
185
- const validationErrors = this.validate(entity);
186
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.fullUpdateById');
187
181
  const baseQuery = { _id: new ObjectId(id) };
188
182
  const query = this.prepareQuery(userContext, baseQuery);
189
183
  const existingEntity = await this.collection.findOne(query);
@@ -194,13 +188,13 @@ export class GenericApiService {
194
188
  _created: existingEntity._created,
195
189
  _createdBy: existingEntity._createdBy,
196
190
  };
197
- const clone = await this.onBeforeUpdate(userContext, entity);
198
- Object.assign(clone, auditProperties);
199
- const mongoUpdateResult = await this.collection.replaceOne(query, clone);
191
+ const preparedEntity = await this.onBeforeUpdate(userContext, entity);
192
+ Object.assign(preparedEntity, auditProperties);
193
+ const mongoUpdateResult = await this.collection.replaceOne(query, preparedEntity);
200
194
  if (mongoUpdateResult?.matchedCount <= 0) {
201
195
  throw new IdNotFoundError();
202
196
  }
203
- await this.onAfterUpdate(userContext, clone);
197
+ await this.onAfterUpdate(userContext, preparedEntity);
204
198
  const updatedEntity = await this.collection.findOne(query);
205
199
  return this.transformSingle(updatedEntity);
206
200
  }
@@ -208,12 +202,10 @@ export class GenericApiService {
208
202
  if (!entityUtils.isValidObjectId(id)) {
209
203
  throw new BadRequestError('id is not a valid ObjectId');
210
204
  }
211
- const validationErrors = this.validate(entity, true);
212
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.partialUpdateById');
213
- const clone = await this.onBeforeUpdate(userContext, entity);
205
+ const preparedEntity = await this.onBeforeUpdate(userContext, entity);
214
206
  const baseQuery = { _id: new ObjectId(id) };
215
207
  const query = this.prepareQuery(userContext, baseQuery);
216
- const updatedEntity = await this.collection.findOneAndUpdate(query, { $set: clone }, { returnDocument: 'after' });
208
+ const updatedEntity = await this.collection.findOneAndUpdate(query, { $set: preparedEntity }, { returnDocument: 'after' });
217
209
  if (!updatedEntity) {
218
210
  throw new IdNotFoundError();
219
211
  }
@@ -227,12 +219,9 @@ export class GenericApiService {
227
219
  if (!entityUtils.isValidObjectId(id)) {
228
220
  throw new BadRequestError('id is not a valid ObjectId');
229
221
  }
230
- const validationErrors = this.validate(entity, true);
231
- entityUtils.handleValidationResult(validationErrors, 'GenericApiService.partialUpdateByIdWithoutBeforeAndAfter');
232
- const preparedEntity = this.preparePayload(userContext, entity, false);
233
222
  const baseQuery = { _id: new ObjectId(id) };
234
223
  const query = this.prepareQuery(userContext, baseQuery);
235
- const modifyResult = await this.collection.findOneAndUpdate(query, { $set: preparedEntity }, { returnDocument: 'after' });
224
+ const modifyResult = await this.collection.findOneAndUpdate(query, { $set: entity }, { returnDocument: 'after' });
236
225
  let updatedEntity = null;
237
226
  if (modifyResult?.ok === 1) {
238
227
  updatedEntity = modifyResult.value;
@@ -248,13 +237,13 @@ export class GenericApiService {
248
237
  return this.transformSingle(updatedEntity);
249
238
  }
250
239
  async update(userContext, queryObject, entity) {
251
- const clone = await this.onBeforeUpdate(userContext, entity);
240
+ const preparedEntity = await this.onBeforeUpdate(userContext, entity);
252
241
  const query = this.prepareQuery(userContext, queryObject);
253
- const mongoUpdateResult = await this.collection.updateMany(query, { $set: clone });
242
+ const mongoUpdateResult = await this.collection.updateMany(query, { $set: preparedEntity });
254
243
  if (mongoUpdateResult?.matchedCount <= 0) {
255
244
  throw new NotFoundError('No records found matching update query');
256
245
  }
257
- await this.onAfterUpdate(userContext, clone);
246
+ await this.onAfterUpdate(userContext, preparedEntity);
258
247
  const updatedEntities = await this.collection.find(query).toArray();
259
248
  return this.transformList(updatedEntities);
260
249
  }
@@ -304,15 +293,13 @@ export class GenericApiService {
304
293
  doc._updatedBy = userId;
305
294
  }
306
295
  async onBeforeCreate(userContext, entities) {
307
- const preparedEntities = await this.preparePayload(userContext, entities, true);
308
- return Promise.resolve(preparedEntities);
296
+ return Promise.resolve(entities);
309
297
  }
310
298
  async onAfterCreate(userContext, entities) {
311
299
  return Promise.resolve(entities);
312
300
  }
313
301
  async onBeforeUpdate(userContext, entities) {
314
- const preparedEntities = await this.preparePayload(userContext, entities, false);
315
- return Promise.resolve(preparedEntities);
302
+ return Promise.resolve(entities);
316
303
  }
317
304
  onAfterUpdate(userContext, entities) {
318
305
  return Promise.resolve(entities);
@@ -337,26 +324,28 @@ export class GenericApiService {
337
324
  const transformedEntity = dbUtils.convertObjectIdsToStrings(single, this.modelSpec.fullSchema);
338
325
  return transformedEntity;
339
326
  }
340
- stripSenderProvidedSystemProperties(doc) {
327
+ stripSenderProvidedSystemProperties(userContext, doc) {
328
+ const isSystemUser = userContext.user?._id === 'system';
329
+ if (isSystemUser) {
330
+ return;
331
+ }
341
332
  for (const key in doc) {
342
333
  if (Object.prototype.hasOwnProperty.call(doc, key) && key.startsWith('_') && key !== '_orgId') {
343
334
  delete doc[key];
344
335
  }
345
336
  }
346
337
  }
347
- async preparePayload(userContext, entity, isCreate = false) {
348
- let result;
338
+ async prepareDataForDb(userContext, entity, isCreate = false) {
349
339
  if (Array.isArray(entity)) {
350
- result = await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
340
+ return await Promise.all(entity.map(item => this.prepareEntity(userContext, item, isCreate)));
351
341
  }
352
342
  else {
353
- result = await this.prepareEntity(userContext, entity, isCreate);
343
+ return await this.prepareEntity(userContext, entity, isCreate);
354
344
  }
355
- return result;
356
345
  }
357
346
  async prepareEntity(userContext, entity, isCreate) {
358
347
  const preparedEntity = _.clone(entity);
359
- this.stripSenderProvidedSystemProperties(preparedEntity);
348
+ this.stripSenderProvidedSystemProperties(userContext, preparedEntity);
360
349
  if (this.modelSpec?.isAuditable) {
361
350
  if (isCreate) {
362
351
  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.23",
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",