Skip to content

Commit 6341de3

Browse files
committed
feat: Add mongoose Document inheritance to identity schemas
1 parent eea63d2 commit 6341de3

File tree

4 files changed

+92
-66
lines changed

4 files changed

+92
-66
lines changed

src/management/identities/_schemas/_parts/additionalFields.part.schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2+
import { Document } from 'mongoose';
23
import { MixedValue } from '~/_common/types/mixed-value.type';
34

45
@Schema({ _id: false })
5-
export class AdditionalFieldsPart {
6+
export class AdditionalFieldsPart extends Document {
67
@Prop({ type: Array, of: String, required: true, default: ['inetOrgPerson'] })
78
objectClasses: string[];
89

src/management/identities/_schemas/_parts/inetOrgPerson.part.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2+
import { Document } from 'mongoose';
23

34
@Schema({ _id: false })
4-
export class inetOrgPerson {
5+
export class inetOrgPerson extends Document {
56
@Prop({ required: true })
67
cn: string;
78

src/management/identities/_schemas/identities.schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export class Identities extends AbstractSchema {
2121

2222
@Prop({ type: AdditionalFieldsPartSchema, required: false, default: {} })
2323
additionalFields: AdditionalFieldsPart;
24+
25+
@Prop({ type: String })
26+
fingerprint: string;
2427
}
2528

2629
export const IdentitiesSchema = SchemaFactory.createForClass(Identities);

src/management/identities/identities.service.ts

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpException, Injectable } from '@nestjs/common';
1+
import { HttpCode, HttpException, HttpStatus, Injectable } from '@nestjs/common';
22
import { InjectModel } from '@nestjs/mongoose';
33
import {
44
Document,
@@ -20,6 +20,8 @@ import { IdentitiesUpsertDto } from './_dto/identities.dto';
2020
import { IdentityState } from './_enums/states.enum';
2121
import { Identities } from './_schemas/identities.schema';
2222
import { IdentitiesValidationService } from './validations/identities.validation.service';
23+
import { createHash } from 'node:crypto';
24+
import { cp } from 'node:fs';
2325

2426
@Injectable()
2527
export class IdentitiesService extends AbstractServiceSchema {
@@ -46,22 +48,15 @@ export class IdentitiesService extends AbstractServiceSchema {
4648
): Promise<ModifyResult<Query<T, T, any, T>>> {
4749
this.logger.log(`Upserting identity with filters ${JSON.stringify(filters)}`);
4850

49-
// const objectClasses = data.additionalFields.objectClasses;
50-
// delete data.additionalFields.objectClasses;
5151
const crushedUpdate = toPlainAndCrush(omit(data || {}, ['$setOnInsert']));
5252
const crushedSetOnInsert = toPlainAndCrush(data.$setOnInsert || {});
53-
// crushedUpdate['additionalFields.objectClasses'] = objectClasses;
54-
55-
// console.log('crushedUpdate', crushedUpdate);
56-
// console.log('crushedSetOnInsert', crushedSetOnInsert);
57-
5853
data = construct({
5954
...crushedUpdate,
6055
...crushedSetOnInsert,
6156
});
6257
data.additionalFields.validations = {};
63-
const logPrefix = `Validation [${data.inetOrgPerson.cn}]:`;
6458

59+
const logPrefix = `Validation [${data.inetOrgPerson.cn}]:`;
6560
try {
6661
this.logger.log(`${logPrefix} Starting additionalFields validation.`);
6762
const validations = await this._validation.validate(data.additionalFields);
@@ -74,68 +69,27 @@ export class IdentitiesService extends AbstractServiceSchema {
7469
crushedUpdate['additionalFields.validations'] = data.additionalFields.validations;
7570
}
7671

77-
return await super.upsert(
72+
const identity = await this.model.findOne(filters).exec();
73+
const fingerprint = await this.previewFingerprint(
74+
construct({
75+
...toPlainAndCrush(identity?.toJSON()),
76+
...crushedUpdate,
77+
}),
78+
);
79+
await this.checkFingerprint(filters, fingerprint);
80+
81+
const upserted = await super.upsert(
7882
filters,
7983
{
8084
$set: crushedUpdate,
8185
$setOnInsert: crushedSetOnInsert,
8286
},
8387
options,
8488
);
85-
}
8689

87-
// public async upsert<T extends AbstractSchema | Document>(
88-
// data?: any,
89-
// options?: QueryOptions<T>,
90-
// ): Promise<ModifyResult<Query<T, T, any, T>>> {
91-
// Logger.log(`Upserting identity: ${JSON.stringify(data)}`);
92-
// const logPrefix = `Validation [${data.inetOrgPerson.cn}]:`;
93-
// // console.log(options);
94-
// const identity = await this._model.findOne({
95-
// 'inetOrgPerson.employeeNumber': data.inetOrgPerson.employeeNumber,
96-
// 'inetOrgPerson.employeeType': data.inetOrgPerson.employeeType,
97-
// });
98-
// // console.log(identity);
99-
// if (!identity && options.errorOnNotFound) {
100-
// this.logger.error(`${logPrefix} Identity not found.`);
101-
// throw new HttpException('Identity not found.', 404);
102-
// }
103-
// data.additionalFields.validations = {};
104-
// try {
105-
// this.logger.log(`${logPrefix} Starting additionalFields validation.`);
106-
// const validations = await this._validation.validate(data.additionalFields);
107-
// this.logger.log(`${logPrefix} AdditionalFields validation successful.`);
108-
// this.logger.log(`Validations : ${validations}`);
109-
// data.state = IdentityState.TO_VALIDATE;
110-
// } catch (error) {
111-
// data = this.handleValidationError(error, data, logPrefix);
112-
// }
113-
114-
// //TODO: ameliorer la logique d'upsert
115-
// if (identity) {
116-
// this.logger.log(`${logPrefix} Identity already exists. Updating.`);
117-
// data.inetOrgPerson = {
118-
// ...identity.inetOrgPerson,
119-
// ...data.inetOrgPerson,
120-
// };
121-
// data.additionalFields.objectClasses = [
122-
// ...new Set([...identity.additionalFields.objectClasses, ...data.additionalFields.objectClasses]),
123-
// ];
124-
// data.additionalFields.attributes = {
125-
// ...identity.additionalFields.attributes,
126-
// ...data.additionalFields.attributes,
127-
// };
128-
// data.additionalFields.validations = {
129-
// ...identity.additionalFields.validations,
130-
// ...data.additionalFields.validations,
131-
// };
132-
// }
133-
134-
// //TODO: rechercher par uid ou employeeNumber + employeeType ?
135-
// const upsert = await super.upsert({ 'inetOrgPerson.uid': data.inetOrgPerson.uid }, data, options);
136-
// return upsert;
137-
// //TODO: add backends service logic here
138-
// }
90+
const identityUpserted = await this._model.findOne({ _id: (upserted as any)._id }).exec();
91+
return await this.generateFingerprint(identityUpserted as unknown as Identities, fingerprint);
92+
}
13993

14094
public async update<T extends AbstractSchema | Document>(
14195
_id: Types.ObjectId | any,
@@ -164,16 +118,18 @@ export class IdentitiesService extends AbstractServiceSchema {
164118
throw error; // Rethrow the original error if it's not one of the handled types.
165119
}
166120
}
121+
167122
// if (update.state === IdentityState.TO_COMPLETE) {
168123
update = { ...update, state: IdentityState.TO_VALIDATE };
124+
169125
// }
170126
// if (update.state === IdentityState.SYNCED) {
171127
// update = { ...update, state: IdentityState.TO_VALIDATE };
172128
// }
173129
//update.state = IdentityState.TO_VALIDATE;
174130
const updated = await super.update(_id, update, options);
175131
//TODO: add backends service logic here (TO_SYNC)
176-
return updated;
132+
return await this.generateFingerprint(updated as unknown as Identities);
177133
}
178134

179135
public async updateState<T extends AbstractSchema | Document>(
@@ -219,6 +175,71 @@ export class IdentitiesService extends AbstractServiceSchema {
219175
return deleted;
220176
}
221177

178+
public async checkFingerprint<T extends AbstractSchema | Document>(
179+
filters: FilterQuery<T>,
180+
fingerprint: string,
181+
): Promise<void> {
182+
const identity = await this.model
183+
.findOne(
184+
{ ...filters, fingerprint },
185+
{
186+
_id: 1,
187+
},
188+
)
189+
.exec();
190+
if (identity) {
191+
this.logger.debug(`Fingerprint matched for <${identity._id}> (${fingerprint}).`);
192+
throw new HttpException('Fingerprint matched.', HttpStatus.NOT_MODIFIED);
193+
}
194+
}
195+
196+
private async generateFingerprint<T extends AbstractSchema | Document>(
197+
identity: Identities,
198+
fingerprint?: string,
199+
): Promise<ModifyResult<Query<T, T, any, T>>> {
200+
if (!fingerprint) {
201+
fingerprint = await this.previewFingerprint(identity.toJSON());
202+
}
203+
204+
const updated = await this.model.findOneAndUpdate(
205+
{ _id: identity._id, fingerprint: { $ne: fingerprint } },
206+
{ fingerprint },
207+
{
208+
new: true,
209+
},
210+
);
211+
212+
if (!updated) {
213+
this.logger.verbose(`Fingerprint already up to date for <${identity._id}>.`);
214+
return identity as unknown as ModifyResult<Query<T, T, any, T>>;
215+
}
216+
217+
this.logger.debug(`Fingerprint updated for <${identity._id}>: ${fingerprint}`);
218+
return updated as unknown as ModifyResult<Query<T, T, any, T>>;
219+
}
220+
221+
private async previewFingerprint(identity: any): Promise<string> {
222+
const additionalFields = omit(identity.additionalFields, ['validations']);
223+
const data = JSON.stringify(
224+
construct(
225+
omit(
226+
toPlainAndCrush({
227+
inetOrgPerson: identity.inetOrgPerson,
228+
additionalFields,
229+
}) as any,
230+
[
231+
//TODO: add configurable fields to exclude
232+
/* 'additionalFields.attributes.supannPerson.supannOIDCGenre' */
233+
],
234+
),
235+
),
236+
);
237+
238+
const hash = createHash('sha256');
239+
hash.update(data);
240+
return hash.digest('hex').toString();
241+
}
242+
222243
private handleValidationError(
223244
error: Error | HttpException,
224245
identity: Identities | IdentitiesUpsertDto,

0 commit comments

Comments
 (0)