Skip to content

Commit 6b185e8

Browse files
Ajouter la méthode upsert dans le contrôleur IdentitiesController et la méthode upsert dans le service AbstractServiceSchema.
1 parent 03bfdec commit 6b185e8

File tree

4 files changed

+155
-13
lines changed

4 files changed

+155
-13
lines changed

Makefile

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ APP_PORT = 4002
33
IMG_NAME = "ghcr.io/libertech-fr/sesame-orchestrator"
44
BASE_NAME = "sesame"
55
APP_NAME = "sesame-orchestrator"
6-
PLATFORM = "linux/amd64"
76

87
.DEFAULT_GOAL := help
98
help:
@@ -20,7 +19,6 @@ dev: ## Start development environment
2019
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \
2120
--add-host host.docker.internal:host-gateway \
2221
--network dev \
23-
--platform $(PLATFORM) \
2422
--name $(APP_NAME) \
2523
-p $(APP_PORT):4000 \
2624
-v $(CURDIR):/data \
@@ -61,9 +59,9 @@ dbs: ## Start databases
6159
--health-timeout=3s \
6260
--health-start-period=5s \
6361
--health-retries=3 \
64-
--health-cmd="mongo --eval \"db.stats().ok\" || exit 1" \
65-
mongo:7.0 --replSet rs0 --wiredTigerCacheSizeGB 1.5 || true
66-
@docker volume create $(APP_NAME)-redis
62+
--health-cmd="mongosh --eval \"db.stats().ok\" || exit 1" \
63+
mongo:7.0 --replSet rs0 --wiredTigerCacheSizeGB 1.5 --bind_ip localhost,$(BASE_NAME)-mongodb || true
64+
@docker volume create $(BASE_NAME)-redis
6765
@docker run -d --rm \
6866
--name $(BASE_NAME)-redis \
6967
-v $(BASE_NAME)-redis:/data \
@@ -76,13 +74,18 @@ dbs: ## Start databases
7674
--health-retries=3 \
7775
--health-cmd="redis-cli ping || exit 1" \
7876
redis || true
79-
@docker exec -it $(BASE_NAME)-mongodb mongo --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: '127.0.0.1:27017'}]})" || true
77+
@docker exec -it $(BASE_NAME)-mongodb mongosh --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: '127.0.0.1:27017'}]})" || true
8078

8179
stop: ## Stop the container
8280
@docker stop $(APP_NAME) || true
8381
@docker stop $(BASE_NAME)-mongodb || true
8482
@docker stop $(BASE_NAME)-redis || true
8583

84+
stop-dbs: ## Stop databases
85+
@docker stop $(BASE_NAME)-mongodb || true
86+
@docker stop $(BASE_NAME)-redis || true
87+
88+
8689
run-test: ## Run tests
8790
act --container-architecture="linux/arm64" -j test
8891

src/_common/abstracts/abstract.service.schema.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Injectable, NotFoundException } from '@nestjs/common';
2-
import { AbstractSchema } from './schemas/abstract.schema';
32
import {
43
Document,
54
FilterQuery,
@@ -12,9 +11,10 @@ import {
1211
Types,
1312
UpdateQuery,
1413
} from 'mongoose';
14+
import { EventEmitterSeparator } from '~/_common/constants/event-emitter.constant';
1515
import { AbstractService, AbstractServiceContext } from './abstract.service';
1616
import { ServiceSchemaInterface } from './interfaces/service.schema.interface';
17-
import { EventEmitterSeparator } from '~/_common/constants/event-emitter.constant';
17+
import { AbstractSchema } from './schemas/abstract.schema';
1818

1919
@Injectable()
2020
export abstract class AbstractServiceSchema extends AbstractService implements ServiceSchemaInterface {
@@ -191,7 +191,7 @@ export abstract class AbstractServiceSchema extends AbstractService implements S
191191
public async update<T extends AbstractSchema | Document>(
192192
_id: Types.ObjectId | any,
193193
update: UpdateQuery<T>,
194-
options?: QueryOptions<T> & { rawResult: true },
194+
options?: QueryOptions<T>,
195195
): Promise<ModifyResult<Query<T, T, any, T>>> {
196196
const logInfos = Object.values({
197197
...arguments,
@@ -243,7 +243,65 @@ export abstract class AbstractServiceSchema extends AbstractService implements S
243243
}
244244
}
245245
return updated
246+
}
247+
248+
public async upsert<T extends AbstractSchema | Document>(
249+
filter: FilterQuery<T>,
250+
update: UpdateQuery<T>,
251+
options?: QueryOptions<T>
252+
): Promise<ModifyResult<Query<T, T, any, T>>> {
253+
this.logger.debug(['upsert', JSON.stringify(Object.values(arguments))].join(' '));
254+
if (this.eventEmitter) {
255+
const beforeEvents = await this.eventEmitter?.emitAsync(
256+
[this.moduleName.toLowerCase(), this.serviceName.toLowerCase(), 'service', 'beforeUpsert'].join(EventEmitterSeparator),
257+
{ filter, update, options },
258+
);
259+
for (const beforeEvent of beforeEvents) {
260+
if (beforeEvent?.stop) throw beforeEvent?.stop;
261+
if (beforeEvent?.filter) filter = { ...filter, ...beforeEvent.filter };
262+
if (beforeEvent?.update) update = { ...update, ...beforeEvent.update };
263+
if (beforeEvent?.options) options = { ...options, ...beforeEvent.options };
264+
}
265+
}
266+
const validation = await this._model.validate(update)
267+
console.log('validation', validation)
268+
let result = await this._model
269+
.findOneAndUpdate<Query<T | null, T, any, T>>(
270+
filter,
271+
{
272+
...update,
273+
$set: {
274+
...(update?.$set || {}),
275+
'metadata.lastUpdatedBy': this.request?.user?.username || 'anonymous',
276+
'metadata.lastUpdatedAt': new Date(),
277+
}
278+
},
279+
{
280+
upsert: true,
281+
new: true,
282+
runValidators: true,
283+
...options,
284+
} as QueryOptions<T> & { includeResultMetadata: true },
285+
)
286+
.exec();
287+
288+
if (this.eventEmitter) {
289+
const afterEvents = await this.eventEmitter?.emitAsync(
290+
[this.moduleName.toLowerCase(), this.serviceName.toLowerCase(), 'service', 'afterUpsert'].join(EventEmitterSeparator),
291+
{ result },
292+
);
293+
for (const afterEvent of afterEvents) {
294+
if (afterEvent?.result) result = { ...result, ...afterEvent.result };
295+
}
296+
}
297+
298+
if (!result) {
299+
throw new NotFoundException();
300+
}
301+
302+
return result;
246303
}
304+
247305

248306
public async delete<T extends AbstractSchema | Document>(_id: Types.ObjectId | any, options?: QueryOptions<T> | null | undefined): Promise<Query<T, T, any, T>> {
249307
this.logger.debug(['delete', JSON.stringify(Object.values(arguments))].join(' '))

src/management/identities/identities.controller.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,40 @@ export class IdentitiesController extends AbstractController {
8181
});
8282
}
8383

84+
@Post('upsert')
85+
@ApiCreateDecorator(IdentitiesCreateDto, IdentitiesDto)
86+
public async upsert(
87+
@Res()
88+
res: Response,
89+
@Body() body: IdentitiesCreateDto,
90+
): Promise<
91+
Response<
92+
{
93+
statusCode: number;
94+
data?: Document<Identities, any, Identities>;
95+
message?: string;
96+
validations?: MixedValue;
97+
},
98+
any
99+
>
100+
> {
101+
let statusCode = HttpStatus.CREATED;
102+
let message = null;
103+
const data = await this._service.upsert<Identities>(body);
104+
// If the state is TO_COMPLETE, the identity is created but additional fields are missing or invalid
105+
// Else the state is TO_VALIDATE, we return a 201 status code
106+
if ((data as unknown as Identities).state === IdentityState.TO_COMPLETE) {
107+
statusCode = HttpStatus.ACCEPTED;
108+
message = 'Identitée créée avec succès, mais des champs additionnels sont manquants ou invalides.';
109+
}
110+
111+
return res.status(statusCode).json({
112+
statusCode,
113+
data,
114+
message,
115+
});
116+
}
117+
84118
@Get()
85119
@ApiPaginatedDecorator(PickProjectionHelper(IdentitiesDto, IdentitiesController.projection))
86120
public async search(

src/management/identities/identities.service.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,48 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { additionalFieldsPartDto } from './_dto/_parts/additionalFields.dto';
2+
import { HttpException, Injectable, Logger } from '@nestjs/common';
23
import { InjectModel } from '@nestjs/mongoose';
34
import { Identities } from './_schemas/identities.schema';
45
import { Document, Model, ModifyResult, Query, QueryOptions, SaveOptions, Types, UpdateQuery } from 'mongoose';
56
import { AbstractServiceSchema } from '~/_common/abstracts/abstract.service.schema';
67
import { AbstractSchema } from '~/_common/abstracts/schemas/abstract.schema';
8+
import { IdentitiesValidationService } from './validations/identities.validation.service';
9+
import { ValidationConfigException, ValidationSchemaException } from '~/_common/errors/ValidationException';
10+
import { IdentityState } from './_enums/states.enum';
711

812
@Injectable()
913
export class IdentitiesService extends AbstractServiceSchema {
10-
constructor(@InjectModel(Identities.name) protected _model: Model<Identities>) {
14+
constructor(
15+
@InjectModel(Identities.name) protected _model: Model<Identities>,
16+
protected readonly _validation: IdentitiesValidationService,
17+
) {
1118
super();
1219
}
1320

1421
public async create<T extends AbstractSchema | Document>(
1522
data?: any,
1623
options?: SaveOptions,
1724
): Promise<Document<T, any, T>> {
18-
// noinspection UnnecessaryLocalVariableJS
1925
const created: Document<T, any, T> = await super.create(data, options);
20-
//TODO: add backends service logic here (TO_SYNC)
2126
return created;
27+
//TODO: add backends service logic here
28+
}
29+
30+
public async upsert<T extends AbstractSchema | Document>(
31+
data?: any,
32+
options?: QueryOptions<T>,
33+
): Promise<ModifyResult<Query<T, T, any, T>>> {
34+
const logPrefix = `Validation [${data.inetOrgPerson.cn}]:`;
35+
try {
36+
Logger.log(`${logPrefix} Starting additionalFields validation.`);
37+
await this._validation.validate(data.additionalFields);
38+
Logger.log(`${logPrefix} AdditionalFields validation successful.`);
39+
data.state = IdentityState.TO_VALIDATE;
40+
} catch (error) {
41+
data = this.handleValidationError(error, data, logPrefix);
42+
}
43+
const upsert = await super.upsert({ 'inetOrgPerson.uid': data.inetOrgPerson.uid }, data, options);
44+
return upsert;
45+
//TODO: add backends service logic here
2246
}
2347

2448
public async update<T extends AbstractSchema | Document>(
@@ -42,4 +66,27 @@ export class IdentitiesService extends AbstractServiceSchema {
4266
//TODO: add backends service logic here (TO_SYNC)
4367
return deleted;
4468
}
69+
70+
private handleValidationError(error: Error | HttpException, identity: Identities, logPrefix: string) {
71+
if (error instanceof ValidationConfigException) {
72+
Logger.error(`${logPrefix} Validation config error. ${JSON.stringify(error.getValidations())}`);
73+
throw new ValidationConfigException(error.getPayload());
74+
}
75+
76+
if (error instanceof ValidationSchemaException) {
77+
Logger.warn(`${logPrefix} Validation schema error. ${JSON.stringify(error.getValidations())}`);
78+
if (identity.state === IdentityState.TO_CREATE) {
79+
Logger.warn(`${logPrefix} State set to TO_COMPLETE.`);
80+
identity.state = IdentityState.TO_COMPLETE;
81+
identity.additionalFields.validations = error.getValidations();
82+
return identity;
83+
} else {
84+
Logger.error(`${logPrefix} Validation schema error. ${JSON.stringify(error.getValidations())}`);
85+
throw new ValidationSchemaException(error.getPayload());
86+
}
87+
} else {
88+
Logger.error(`${logPrefix} Unhandled error: ${error.message}`);
89+
throw error; // Rethrow the original error if it's not one of the handled types.
90+
}
91+
}
4592
}

0 commit comments

Comments
 (0)