Skip to content

Commit ed5dd61

Browse files
Merge pull request #23 from Libertech-FR/22-implmentation-mapping
22 implmentation mapping
2 parents 43da6ef + 0a2af8d commit ed5dd61

File tree

13 files changed

+536
-258
lines changed

13 files changed

+536
-258
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ docker/docker-compose.yml
3939

4040
documentation/*
4141
.dev-token.json
42+
*.mongodb.js

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"singleQuote": true,
3-
"trailingComma": "all"
3+
"trailingComma": "all",
4+
"endOfLine": "auto"
45
}

Dockerfile

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
FROM node:18-bookworm-slim as builder
22

33
ENV TIMEZONE=Europe/Paris \
4-
LANGUAGE=fr_FR.UTF-8 \
5-
LANG=fr_FR.UTF-8 \
6-
TERM=xterm \
7-
DEBIAN_FRONTEND=noninteractive
4+
LANGUAGE=fr_FR.UTF-8 \
5+
LANG=fr_FR.UTF-8 \
6+
TERM=xterm \
7+
DEBIAN_FRONTEND=noninteractive
88

99
WORKDIR /data
1010

@@ -16,15 +16,18 @@ RUN yarn install \
1616
--non-interactive \
1717
--production=false
1818

19+
RUN apt-get update -y \
20+
&& apt-get install -y python3
21+
1922
RUN yarn run build
2023

2124
FROM node:18-bookworm-slim AS production
2225

2326
ENV TIMEZONE=Europe/Paris \
24-
LANGUAGE=fr_FR.UTF-8 \
25-
LANG=fr_FR.UTF-8 \
26-
TERM=xterm \
27-
DEBIAN_FRONTEND=noninteractive
27+
LANGUAGE=fr_FR.UTF-8 \
28+
LANG=fr_FR.UTF-8 \
29+
TERM=xterm \
30+
DEBIAN_FRONTEND=noninteractive
2831

2932
ARG NODE_ENV=production
3033
ENV NODE_ENV=${NODE_ENV}
@@ -35,25 +38,25 @@ ADD package.json .
3538
ADD *.lock .
3639

3740
RUN apt clean -y \
38-
&& apt update -y \
39-
&& apt upgrade -y \
40-
&& apt install -y locales \
41-
&& export LANGUAGE=${LANGUAGE} \
42-
&& export LANG=${LANG} \
43-
&& export LC_ALL=${LC_ALL} \
44-
&& locale-gen ${LANG} \
45-
&& dpkg-reconfigure --frontend ${DEBIAN_FRONTEND} locales \
46-
&& apt install --no-install-recommends -yq \
47-
git \
48-
jq \
49-
nano \
50-
openssl
41+
&& apt update -y \
42+
&& apt upgrade -y \
43+
&& apt install -y locales \
44+
&& export LANGUAGE=${LANGUAGE} \
45+
&& export LANG=${LANG} \
46+
&& export LC_ALL=${LC_ALL} \
47+
&& locale-gen ${LANG} \
48+
&& dpkg-reconfigure --frontend ${DEBIAN_FRONTEND} locales \
49+
&& apt install --no-install-recommends -yq \
50+
git \
51+
jq \
52+
nano \
53+
openssl
5154

5255
RUN yarn install \
53-
--prefer-offline \
54-
--pure-lockfile \
55-
--non-interactive \
56-
--production=true \
56+
--prefer-offline \
57+
--pure-lockfile \
58+
--non-interactive \
59+
--production=true \
5760
&& yarn cache clean \
5861
&& yarn autoclean --init \
5962
&& yarn autoclean --force

Makefile

Lines changed: 10 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:
@@ -19,8 +18,8 @@ dev: ## Start development environment
1918
-e NODE_ENV=development \
2019
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \
2120
--add-host host.docker.internal:host-gateway \
22-
--network dev \
2321
--platform $(PLATFORM) \
22+
--network dev \
2423
--name $(APP_NAME) \
2524
-p $(APP_PORT):4000 \
2625
-v $(CURDIR):/data \
@@ -61,9 +60,9 @@ dbs: ## Start databases
6160
--health-timeout=3s \
6261
--health-start-period=5s \
6362
--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
63+
--health-cmd="mongosh --eval \"db.stats().ok\" || exit 1" \
64+
mongo:7.0 --replSet rs0 --wiredTigerCacheSizeGB 1.5 --bind_ip localhost,$(BASE_NAME)-mongodb || true
65+
@docker volume create $(BASE_NAME)-redis
6766
@docker run -d --rm \
6867
--name $(BASE_NAME)-redis \
6968
-v $(BASE_NAME)-redis:/data \
@@ -76,13 +75,18 @@ dbs: ## Start databases
7675
--health-retries=3 \
7776
--health-cmd="redis-cli ping || exit 1" \
7877
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
78+
@docker exec -it $(BASE_NAME)-mongodb mongosh --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: '127.0.0.1:27017'}]})" || true
8079

8180
stop: ## Stop the container
8281
@docker stop $(APP_NAME) || true
8382
@docker stop $(BASE_NAME)-mongodb || true
8483
@docker stop $(BASE_NAME)-redis || true
8584

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"@streamkits/nestjs_module_scrud": "^0.0.16",
6060
"ajv": "^8.12.0",
6161
"ajv-errors": "^3.0.0",
62-
"argon2": "^0.31.2",
62+
"argon2": "^0.40.1",
6363
"bullmq": "^4.14.0",
6464
"class-transformer": "^0.5.1",
6565
"class-validator": "^0.14.0",
@@ -91,8 +91,8 @@
9191
"@nestjs/schematics": "^10.0.2",
9292
"@nestjs/swagger": "^7.1.16",
9393
"@nestjs/testing": "^10.1.3",
94-
"@swc/cli": "^0.1.63",
95-
"@swc/core": "^1.3.99",
94+
"@swc/cli": "^0.3.10",
95+
"@swc/core": "^1.4.6",
9696
"@types/cookie-parser": "^1.4.6",
9797
"@types/express": "^4.17.17",
9898
"@types/jest": "^29.5.2",

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

Lines changed: 59 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,63 @@ 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+
let result = await this._model
267+
.findOneAndUpdate<Query<T | null, T, any, T>>(
268+
filter,
269+
{
270+
...update,
271+
$set: {
272+
...(update?.$set || {}),
273+
'metadata.lastUpdatedBy': this.request?.user?.username || 'anonymous',
274+
'metadata.lastUpdatedAt': new Date(),
275+
}
276+
},
277+
{
278+
upsert: true,
279+
new: true,
280+
runValidators: true,
281+
...options,
282+
} as QueryOptions<T> & { includeResultMetadata: true },
283+
)
284+
.exec();
285+
286+
if (this.eventEmitter) {
287+
const afterEvents = await this.eventEmitter?.emitAsync(
288+
[this.moduleName.toLowerCase(), this.serviceName.toLowerCase(), 'service', 'afterUpsert'].join(EventEmitterSeparator),
289+
{ result },
290+
);
291+
for (const afterEvent of afterEvents) {
292+
if (afterEvent?.result) result = { ...result, ...afterEvent.result };
293+
}
294+
}
295+
296+
if (!result) {
297+
throw new NotFoundException();
298+
}
299+
300+
return result;
246301
}
302+
247303

248304
public async delete<T extends AbstractSchema | Document>(_id: Types.ObjectId | any, options?: QueryOptions<T> | null | undefined): Promise<Query<T, T, any, T>> {
249305
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: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,68 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { HttpException, Injectable, Logger } from '@nestjs/common';
22
import { InjectModel } from '@nestjs/mongoose';
33
import { Identities } from './_schemas/identities.schema';
44
import { Document, Model, ModifyResult, Query, QueryOptions, SaveOptions, Types, UpdateQuery } from 'mongoose';
55
import { AbstractServiceSchema } from '~/_common/abstracts/abstract.service.schema';
66
import { AbstractSchema } from '~/_common/abstracts/schemas/abstract.schema';
7+
import { IdentitiesValidationService } from './validations/identities.validation.service';
8+
import { ValidationConfigException, ValidationSchemaException } from '~/_common/errors/ValidationException';
9+
import { IdentityState } from './_enums/states.enum';
710

811
@Injectable()
912
export class IdentitiesService extends AbstractServiceSchema {
10-
constructor(@InjectModel(Identities.name) protected _model: Model<Identities>) {
13+
constructor(
14+
@InjectModel(Identities.name) protected _model: Model<Identities>,
15+
protected readonly _validation: IdentitiesValidationService,
16+
) {
1117
super();
1218
}
1319

1420
public async create<T extends AbstractSchema | Document>(
1521
data?: any,
1622
options?: SaveOptions,
1723
): Promise<Document<T, any, T>> {
18-
// noinspection UnnecessaryLocalVariableJS
1924
const created: Document<T, any, T> = await super.create(data, options);
20-
//TODO: add backends service logic here (TO_SYNC)
2125
return created;
26+
//TODO: add backends service logic here
27+
}
28+
29+
public async upsert<T extends AbstractSchema | Document>(
30+
data?: any,
31+
options?: QueryOptions<T>,
32+
): Promise<ModifyResult<Query<T, T, any, T>>> {
33+
Logger.log(`Upserting identity: ${JSON.stringify(data)}`);
34+
const logPrefix = `Validation [${data.inetOrgPerson.cn}]:`;
35+
data.additionalFields.validations = {};
36+
try {
37+
Logger.log(`${logPrefix} Starting additionalFields validation.`);
38+
const validations = await this._validation.validate(data.additionalFields);
39+
Logger.log(`${logPrefix} AdditionalFields validation successful.`);
40+
Logger.log(`Validations : ${validations}`);
41+
data.state = IdentityState.TO_VALIDATE;
42+
} catch (error) {
43+
data = this.handleValidationError(error, data, logPrefix);
44+
}
45+
46+
//TODO: ameliorer la logique d'upsert
47+
const identity = await this._model.findOne({ 'inetOrgPerson.uid': data.inetOrgPerson.uid });
48+
if (identity) {
49+
Logger.log(`${logPrefix} Identity already exists. Updating.`);
50+
data.additionalFields.objectClasses = [
51+
...new Set([...identity.additionalFields.objectClasses, ...data.additionalFields.objectClasses]),
52+
];
53+
data.additionalFields.attributes = {
54+
...identity.additionalFields.attributes,
55+
...data.additionalFields.attributes,
56+
};
57+
data.additionalFields.validations = {
58+
...identity.additionalFields.validations,
59+
...data.additionalFields.validations,
60+
};
61+
}
62+
63+
const upsert = await super.upsert({ 'inetOrgPerson.uid': data.inetOrgPerson.uid }, data, options);
64+
return upsert;
65+
//TODO: add backends service logic here
2266
}
2367

2468
public async update<T extends AbstractSchema | Document>(
@@ -42,4 +86,27 @@ export class IdentitiesService extends AbstractServiceSchema {
4286
//TODO: add backends service logic here (TO_SYNC)
4387
return deleted;
4488
}
89+
90+
private handleValidationError(error: Error | HttpException, identity: Identities, logPrefix: string) {
91+
if (error instanceof ValidationConfigException) {
92+
Logger.error(`${logPrefix} Validation config error. ${JSON.stringify(error.getValidations())}`);
93+
throw new ValidationConfigException(error.getPayload());
94+
}
95+
96+
if (error instanceof ValidationSchemaException) {
97+
Logger.warn(`${logPrefix} Validation schema error. ${JSON.stringify(error.getValidations())}`);
98+
identity.additionalFields.validations = error.getValidations();
99+
if (identity.state === IdentityState.TO_CREATE) {
100+
Logger.warn(`${logPrefix} State set to TO_COMPLETE.`);
101+
identity.state = IdentityState.TO_COMPLETE;
102+
return identity;
103+
} else {
104+
Logger.error(`${logPrefix} Validation schema error. ${JSON.stringify(error.getValidations())}`);
105+
throw new ValidationSchemaException(error.getPayload());
106+
}
107+
} else {
108+
Logger.error(`${logPrefix} Unhandled error: ${error.message}`);
109+
throw error; // Rethrow the original error if it's not one of the handled types.
110+
}
111+
}
45112
}

0 commit comments

Comments
 (0)