@@ -13,14 +13,15 @@ import {
1313 SaveOptions ,
1414 Types ,
1515} from 'mongoose' ;
16+ import { filterSchema , SearchFilterInput } from '@tacxou/nestjs_module_restools/search-filter-schema' ;
1617import { AbstractServiceSchema } from '~/_common/abstracts/abstract.service.schema' ;
1718import { AbstractSchema } from '~/_common/abstracts/schemas/abstract.schema' ;
1819import { normalizeMongoFilterValues } from '~/_common/functions/normalize-mongo-filter-values' ;
1920import { IdentityState } from '~/management/identities/_enums/states.enum' ;
2021import { IdentitiesCrudService } from '~/management/identities/identities-crud.service' ;
2122import { GroupsCreateDto , GroupsUpdateDto } from './_dto/groups.dto' ;
2223import { GroupFamilies } from './_schemas/group-families.schema' ;
23- import { Groups } from './_schemas/groups.schema' ;
24+ import { Groups , GroupType } from './_schemas/groups.schema' ;
2425
2526/**
2627 * Service de gestion des groupes d'identités (type LDAP `groupOfNames`).
@@ -52,18 +53,37 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
5253 ) : Promise < Document < T , any , T > > {
5354 await this . ensureCnAvailable ( data . cn ) ;
5455 await this . ensureMailAvailable ( data . mail ) ;
55- const member = await this . normalizeMembers ( data . member ) ;
5656 const owner = await this . normalizeMembers ( data . owner ) ;
5757 const family = await this . normalizeFamily ( data . family ) ;
58+ const membership =
59+ data . type === GroupType . DYNAMIC
60+ ? await this . buildDynamicMembership ( data . filters )
61+ : { type : GroupType . STATIC , filters : null , member : await this . normalizeMembers ( data . member ) } ;
5862
59- return await super . create < T > ( { ...data , family, member , owner , state : IdentityState . TO_SYNC } , options ) ;
63+ return await super . create < T > ( { ...data , family, owner , ... membership , state : IdentityState . TO_SYNC } , options ) ;
6064 }
6165
6266 public async updateGroup ( _id : Types . ObjectId , data : GroupsUpdateDto ) : Promise < ModifyResult < Query < Groups , Groups > > > {
6367 const $set : Record < string , unknown > = { ...data , state : IdentityState . TO_SYNC } ;
6468 if ( data . cn !== undefined ) await this . ensureCnAvailable ( data . cn , _id ) ;
6569 if ( data . mail !== undefined ) await this . ensureMailAvailable ( data . mail , _id ) ;
66- if ( data . member !== undefined ) $set . member = await this . normalizeMembers ( data . member ) ;
70+ if ( data . type !== undefined || data . filters !== undefined || data . member !== undefined ) {
71+ const current = await this . findById < Groups > ( _id , { type : 1 , filters : 1 } ) ;
72+ const type = data . type ?? current . type ?? GroupType . STATIC ;
73+ if ( type === GroupType . DYNAMIC ) {
74+ // les membres d'un groupe dynamique sont toujours recalculés à partir du filtre
75+ Object . assign (
76+ $set ,
77+ await this . buildDynamicMembership ( data . filters !== undefined ? data . filters : current . filters ) ,
78+ ) ;
79+ } else {
80+ // un groupe redevenu normal conserve ses derniers membres calculés
81+ $set . type = GroupType . STATIC ;
82+ $set . filters = null ;
83+ if ( data . member !== undefined ) $set . member = await this . normalizeMembers ( data . member ) ;
84+ else delete $set . member ;
85+ }
86+ }
6787 if ( data . owner !== undefined ) $set . owner = await this . normalizeMembers ( data . owner ) ;
6888 if ( data . family !== undefined ) $set . family = await this . normalizeFamily ( data . family ) ;
6989
@@ -109,6 +129,7 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
109129 }
110130
111131 public async addMembers ( _id : Types . ObjectId , ids : string [ ] ) : Promise < ModifyResult < Query < Groups , Groups > > > {
132+ await this . ensureStaticGroup ( _id ) ;
112133 const members = await this . normalizeMembers ( ids ) ;
113134 return await this . update < Groups > ( _id , {
114135 $addToSet : { member : { $each : members } } ,
@@ -117,6 +138,7 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
117138 }
118139
119140 public async removeMembers ( _id : Types . ObjectId , ids : string [ ] ) : Promise < ModifyResult < Query < Groups , Groups > > > {
141+ await this . ensureStaticGroup ( _id ) ;
120142 return await this . update < Groups > ( _id , {
121143 $pull : { member : { $in : ids . map ( ( id ) => new Types . ObjectId ( id ) ) } } ,
122144 $set : { state : IdentityState . TO_SYNC } ,
@@ -145,20 +167,28 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
145167 */
146168 public async findMemberOf ( identityId : Types . ObjectId ) : Promise < Groups [ ] > {
147169 return await this . _model
148- . find ( { member : identityId } , { cn : 1 , description : 1 , state : 1 } )
170+ . find ( { member : identityId } , { cn : 1 , description : 1 , state : 1 , type : 1 } )
149171 . sort ( { cn : 1 } )
150172 . lean < Groups [ ] > ( )
151173 . exec ( ) ;
152174 }
153175
154176 /**
155- * Remplace la liste des groupes d'une identité en ne modifiant que les groupes concernés par la différence
177+ * Remplace la liste des groupes d'une identité en ne modifiant que les groupes concernés par la différence.
178+ * Les groupes dynamiques sont ignorés : leur appartenance ne dépend que de leur filtre.
156179 */
157180 public async setIdentityGroups ( identityId : Types . ObjectId , groupIds : string [ ] ) : Promise < Groups [ ] > {
158181 await this . normalizeMembers ( [ identityId . toHexString ( ) ] ) ;
159182
160- const target = new Set ( groupIds ) ;
161- const current = new Set ( ( await this . findMemberOf ( identityId ) ) . map ( ( group ) => group . _id . toHexString ( ) ) ) ;
183+ const dynamicIds = new Set (
184+ ( await this . _model . find ( { type : GroupType . DYNAMIC } , { _id : 1 } ) . lean ( ) . exec ( ) ) . map ( ( group ) =>
185+ group . _id . toHexString ( ) ,
186+ ) ,
187+ ) ;
188+ const target = new Set ( groupIds . filter ( ( id ) => ! dynamicIds . has ( id ) ) ) ;
189+ const current = new Set (
190+ ( await this . findMemberOf ( identityId ) ) . map ( ( group ) => group . _id . toHexString ( ) ) . filter ( ( id ) => ! dynamicIds . has ( id ) ) ,
191+ ) ;
162192 const toAdd = [ ...target ] . filter ( ( id ) => ! current . has ( id ) ) ;
163193 const toRemove = [ ...current ] . filter ( ( id ) => ! target . has ( id ) ) ;
164194
@@ -183,6 +213,37 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
183213 return await this . findMemberOf ( identityId ) ;
184214 }
185215
216+ /**
217+ * Réévalue le filtre de chaque groupe dynamique et met à jour ses membres s'ils ont changé.
218+ * Les groupes modifiés repassent à TO_SYNC ; leurs identifiants sont retournés pour synchronisation.
219+ */
220+ public async refreshDynamicGroups ( ) : Promise < string [ ] > {
221+ const groups = await this . _model
222+ . find ( { type : GroupType . DYNAMIC , deletedFlag : { $ne : true } } , { cn : 1 , filters : 1 , member : 1 } )
223+ . lean < Groups [ ] > ( )
224+ . exec ( ) ;
225+ const changed : string [ ] = [ ] ;
226+
227+ for ( const group of groups ) {
228+ let member : Types . ObjectId [ ] ;
229+ try {
230+ member = await this . resolveDynamicMembers ( group . filters ) ;
231+ } catch ( error ) {
232+ this . logger . error ( `Filtre invalide pour le groupe dynamique <${ group . cn } >: ${ error ?. message || error } ` ) ;
233+ continue ;
234+ }
235+
236+ const before = ( group . member || [ ] ) . map ( ( id ) => id . toString ( ) ) . sort ( ) ;
237+ const after = member . map ( ( id ) => id . toString ( ) ) . sort ( ) ;
238+ if ( before . length === after . length && before . every ( ( id , index ) => id === after [ index ] ) ) continue ;
239+
240+ await this . update < Groups > ( group . _id , { $set : { member, state : IdentityState . TO_SYNC } } ) ;
241+ changed . push ( group . _id . toString ( ) ) ;
242+ }
243+
244+ return changed ;
245+ }
246+
186247 /**
187248 * Passe les groupes donnés à l'état TO_SYNC (action « Mettre à synchroniser » de la liste)
188249 */
@@ -246,6 +307,53 @@ export class GroupsService extends AbstractServiceSchema<Groups> {
246307 }
247308 }
248309
310+ protected async ensureStaticGroup ( _id : Types . ObjectId ) : Promise < void > {
311+ const group = await this . findById < Groups > ( _id , { type : 1 } ) ;
312+ if ( group ?. type === GroupType . DYNAMIC ) {
313+ throw new BadRequestException ( 'Les membres d’un groupe dynamique sont calculés à partir de son filtre' ) ;
314+ }
315+ }
316+
317+ protected async buildDynamicMembership (
318+ filters ?: Record < string , unknown > | null ,
319+ ) : Promise < { type : GroupType ; filters : Record < string , unknown > ; member : Types . ObjectId [ ] } > {
320+ if ( ! filters || ! Object . keys ( filters ) . length ) {
321+ throw new BadRequestException ( 'Un groupe dynamique doit avoir au moins un filtre' ) ;
322+ }
323+ return { type : GroupType . DYNAMIC , filters, member : await this . resolveDynamicMembers ( filters ) } ;
324+ }
325+
326+ public async countDynamicMembers ( filters ?: Record < string , unknown > | null ) : Promise < number > {
327+ return await this . identities . model . countDocuments ( this . buildDynamicFilter ( filters ) ) . exec ( ) ;
328+ }
329+
330+ /**
331+ * Évalue un filtre de groupe dynamique (même format et même périmètre que /identities/count-all)
332+ * et retourne les identifiants des identités sélectionnées, triés
333+ */
334+ protected async resolveDynamicMembers ( filters ?: Record < string , unknown > | null ) : Promise < Types . ObjectId [ ] > {
335+ const found = await this . identities . model
336+ . find ( this . buildDynamicFilter ( filters ) , { _id : 1 } )
337+ . sort ( { _id : 1 } )
338+ . lean ( )
339+ . exec ( ) ;
340+
341+ return found . map ( ( identity ) => new Types . ObjectId ( identity . _id . toString ( ) ) ) ;
342+ }
343+
344+ /**
345+ * Convertit le filtre stocké (clés signées) en filtre Mongo sur les identités non supprimées
346+ */
347+ protected buildDynamicFilter ( filters ?: Record < string , unknown > | null ) : FilterQuery < unknown > {
348+ let filter : FilterQuery < unknown > ;
349+ try {
350+ filter = filterSchema ( ( filters || { } ) as SearchFilterInput ) ;
351+ } catch ( error ) {
352+ throw new BadRequestException ( error ?. message ?? 'Filtre de groupe dynamique invalide' ) ;
353+ }
354+ return normalizeMongoFilterValues ( { ...filter , deletedFlag : { $ne : true } } ) ;
355+ }
356+
249357 protected async ensureCnAvailable ( cn : string , excludeId ?: Types . ObjectId ) : Promise < void > {
250358 const filter : FilterQuery < Groups > = { cn : cn ?. trim ( ) } ;
251359 if ( excludeId ) filter . _id = { $ne : excludeId } ;
0 commit comments