From df9c50561340da274833eb58274ea2fa601564fc Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Wed, 29 Jul 2026 15:56:06 +0900 Subject: [PATCH 1/2] BREAKING: `modelService.delete()` does not `refetchObservableQueries()` #12584 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The most common use case for `modelService.delete()` is from a page detail, and after deletion the user is redirected to the page listing. So there is no need to `refetchObservableQueries()` unconditionally. # Other use cases For other use cases we can reload specific queries like those examples. ## Dialog From a dialog on top of the listing page: ```ts this.itemService .delete([id], { // Wait till we refresh the list under the dialog before closing the dialog, to avoid re-clicking on the just deleted item refetchQueries: [itemsQuery], awaitRefetchQueries: true, }) .subscribe({ next: () => { this.alertService.info(`Supprimé`); this.dialogRef.close(); } }); ``` ## Inline deletion in listing From a listing page where deletion is directly accessible on the row: ```ts this.itemService.delete([id], { refetchQueries: [itemsQuery], }).subscribe({ next: () => { this.alertService.info(`Supprimé`); }, error: () => this.deleting.delete(id), }); ``` ## Bulk deleting Bulk deleting via `NaturalAbstractList.bulkDelete()` still automatically refetch the listing of items, but no other queries anymore. --- .../src/lib/classes/abstract-detail.ts | 2 +- .../natural/src/lib/classes/abstract-list.ts | 18 ++++++---- .../modules/relations/relations.component.ts | 2 +- .../services/abstract-model.service.spec.ts | 36 ------------------- .../lib/services/abstract-model.service.ts | 26 ++++++++------ 5 files changed, 28 insertions(+), 56 deletions(-) diff --git a/projects/natural/src/lib/classes/abstract-detail.ts b/projects/natural/src/lib/classes/abstract-detail.ts index 8a65f83a..21db6c7c 100644 --- a/projects/natural/src/lib/classes/abstract-detail.ts +++ b/projects/natural/src/lib/classes/abstract-detail.ts @@ -50,7 +50,7 @@ export class NaturalAbstractDetail< any, any, any, - unknown, + any, any >, ExtraResolve extends Literal = Record, diff --git a/projects/natural/src/lib/classes/abstract-list.ts b/projects/natural/src/lib/classes/abstract-list.ts index 757032c9..0014f9fc 100644 --- a/projects/natural/src/lib/classes/abstract-list.ts +++ b/projects/natural/src/lib/classes/abstract-list.ts @@ -569,7 +569,7 @@ export class NaturalAbstractList< } /** - * Delete multiple items at once + * Delete multiple items at once, and then refresh the list of items automatically */ protected bulkDelete(): Observable { const subject = new Subject(); @@ -581,12 +581,16 @@ export class NaturalAbstractList< // never call this method. const selection = this.selection.selected as {id: string}[]; - this.service.delete(selection).subscribe(() => { - this.selection.clear(); - this.alertService.info($localize`Supprimé`); - subject.next(); - subject.complete(); - }); + this.service + .delete(selection, { + refetchQueries: this.service.allQuery ? [this.service.allQuery] : [], + }) + .subscribe(() => { + this.selection.clear(); + this.alertService.info($localize`Supprimé`); + subject.next(); + subject.complete(); + }); } }); diff --git a/projects/natural/src/lib/modules/relations/relations.component.ts b/projects/natural/src/lib/modules/relations/relations.component.ts index 0c081677..4a83447b 100644 --- a/projects/natural/src/lib/modules/relations/relations.component.ts +++ b/projects/natural/src/lib/modules/relations/relations.component.ts @@ -94,7 +94,7 @@ export class NaturalRelationsComponent< any, unknown, any, - unknown, + any, any >, > diff --git a/projects/natural/src/lib/services/abstract-model.service.spec.ts b/projects/natural/src/lib/services/abstract-model.service.spec.ts index ef26162a..2cac7c7a 100644 --- a/projects/natural/src/lib/services/abstract-model.service.spec.ts +++ b/projects/natural/src/lib/services/abstract-model.service.spec.ts @@ -8,7 +8,6 @@ import {type Literal} from '../types/types'; import {NullService} from '../testing/null.service'; import {Apollo} from 'apollo-angular'; import {takeWhile} from 'rxjs/operators'; -import {type ObservableQuery} from '@apollo/client'; const observableError = 'Cannot use Observable as variables. Instead you should use .subscribe() to call the method with a real value'; @@ -27,41 +26,6 @@ describe('NaturalAbstractModelService', () => { service = TestBed.inject(PostService); }); - it('should be delay deleted resolving', fakeAsync(() => { - const apollo = TestBed.inject(Apollo); - - let resolveMyPromise: (value: ObservableQuery.Result[]) => void; - apollo.client.refetchObservableQueries = () => - new Promise[]>(resolve => { - resolveMyPromise = resolve; - }); - - let resolved = false; - let completed = false; - service.delete([{id: '123'}]).subscribe({ - next: () => { - resolved = true; - }, - complete: () => { - completed = true; - }, - }); - - expect(resolved).toBeFalse(); - expect(completed).toBeFalse(); - - tick(2000); - - expect(resolved).toBeFalse(); - expect(completed).toBeFalse(); - - resolveMyPromise!([]); - tick(2000); - - expect(resolved).toBeTrue(); - expect(completed).toBeTrue(); - })); - it('should be created', () => { expect(service).toBeTruthy(); }); diff --git a/projects/natural/src/lib/services/abstract-model.service.ts b/projects/natural/src/lib/services/abstract-model.service.ts index 48f38963..6f4d9187 100644 --- a/projects/natural/src/lib/services/abstract-model.service.ts +++ b/projects/natural/src/lib/services/abstract-model.service.ts @@ -10,7 +10,7 @@ import { import {type DocumentNode} from 'graphql'; import {merge, pick} from 'es-toolkit'; import {defaults} from 'es-toolkit/compat'; -import {catchError, combineLatest, EMPTY, first, from, Observable, of, type OperatorFunction} from 'rxjs'; +import {catchError, combineLatest, EMPTY, first, Observable, of, type OperatorFunction} from 'rxjs'; import {debounceTime, filter, map, shareReplay, startWith, switchMap, takeWhile, tap} from 'rxjs/operators'; import {NaturalQueryVariablesManager, type QueryVariables} from '../classes/query-variable-manager'; import {type Literal} from '../types/types'; @@ -32,6 +32,10 @@ export type FormControls = Record; export type WithId = {id: string} & T; +export type MutateOptionsWithoutVariables = Vdelete extends never + ? never + : Omit, 'mutation' | 'variables'>; + export abstract class NaturalAbstractModelService< Tone, Vone extends {id: string}, @@ -68,7 +72,7 @@ export abstract class NaturalAbstractModelService< public constructor( protected readonly name: string, protected readonly oneQuery: DocumentNode | null, - protected readonly allQuery: DocumentNode | null, + public readonly allQuery: DocumentNode | null, protected readonly createMutation: DocumentNode | null, protected readonly updateMutation: DocumentNode | null, protected readonly deleteMutation: DocumentNode | null, @@ -430,7 +434,13 @@ export abstract class NaturalAbstractModelService< /** * Delete objects and then refetch the list of objects */ - public delete(objects: {id: string}[]): Observable { + public delete( + objects: {id: string}[], + options: MutateOptionsWithoutVariables = {} as MutateOptionsWithoutVariables< + Tdelete, + Vdelete + >, + ): Observable { this.throwIfObservable(objects); this.throwIfNotQuery(this.deleteMutation); @@ -449,17 +459,11 @@ export abstract class NaturalAbstractModelService< return this.apollo .mutate({ + ...options, mutation: this.deleteMutation, variables: variables, }) - .pipe( - // Delay the observable until Apollo refetch is completed - switchMap(result => { - const mappedResult = this.mapDelete(result); - - return from(this.apollo.client.refetchObservableQueries()).pipe(map(() => mappedResult)); - }), - ); + .pipe(map(result => this.mapDelete(result))); } /** From 737fa14f7e6c89d2fb77c3b9bdaa1bd0df7028c3 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Thu, 30 Jul 2026 14:46:51 +0900 Subject: [PATCH 2/2] BREAKING: `modelService.create()` does not `refetchObservableQueries()` #12584 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The most common use case for `modelService.create()` is from a page detail for creation, and after the creation the user is redirected to the page detail for edition. So there is no need to `refetchObservableQueries()` unconditionally. # Other use cases For other use cases we can reload specific queries like those examples. ## Dialog From a dialog on top of the listing page: ```ts this.itemService .create({...}, { // Refresh the list under the dialog refetchQueries: [itemsQuery] }) .subscribe(newItem => { this.alertService.info('Créé'); this.dialogRef.close(newItem); }); ``` ## Inline creation in listing From a listing page where row creation is inline, there is nothing to do, because the row already exists before we even mutate the server. --- .../src/lib/classes/abstract-detail.ts | 2 +- .../modules/relations/relations.component.ts | 2 +- .../lib/services/abstract-model.service.ts | 37 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/projects/natural/src/lib/classes/abstract-detail.ts b/projects/natural/src/lib/classes/abstract-detail.ts index 21db6c7c..8a65f83a 100644 --- a/projects/natural/src/lib/classes/abstract-detail.ts +++ b/projects/natural/src/lib/classes/abstract-detail.ts @@ -50,7 +50,7 @@ export class NaturalAbstractDetail< any, any, any, - any, + unknown, any >, ExtraResolve extends Literal = Record, diff --git a/projects/natural/src/lib/modules/relations/relations.component.ts b/projects/natural/src/lib/modules/relations/relations.component.ts index 4a83447b..0c081677 100644 --- a/projects/natural/src/lib/modules/relations/relations.component.ts +++ b/projects/natural/src/lib/modules/relations/relations.component.ts @@ -94,7 +94,7 @@ export class NaturalRelationsComponent< any, unknown, any, - any, + unknown, any >, > diff --git a/projects/natural/src/lib/services/abstract-model.service.ts b/projects/natural/src/lib/services/abstract-model.service.ts index 6f4d9187..8a315b41 100644 --- a/projects/natural/src/lib/services/abstract-model.service.ts +++ b/projects/natural/src/lib/services/abstract-model.service.ts @@ -1,5 +1,11 @@ import {Apollo, gql, onlyCompleteData} from 'apollo-angular'; -import {type ApolloLink, NetworkStatus, type ObservableQuery, type WatchQueryFetchPolicy} from '@apollo/client'; +import { + type ApolloLink, + NetworkStatus, + type ObservableQuery, + type OperationVariables, + type WatchQueryFetchPolicy, +} from '@apollo/client'; import { type AbstractControl, type AsyncValidatorFn, @@ -32,9 +38,10 @@ export type FormControls = Record; export type WithId = {id: string} & T; -export type MutateOptionsWithoutVariables = Vdelete extends never - ? never - : Omit, 'mutation' | 'variables'>; +export type MutateOptionsWithoutVariables = Omit< + Apollo.MutateOptions, + 'mutation' | 'variables' +>; export abstract class NaturalAbstractModelService< Tone, @@ -366,9 +373,12 @@ export abstract class NaturalAbstractModelService< } /** - * Create an object in DB and then refetch the list of objects + * Create an object in DB */ - public create(object: Vcreate['input']): Observable { + public create( + object: Vcreate['input'], + options: MutateOptionsWithoutVariables = {}, + ): Observable { this.throwIfObservable(object); this.throwIfNotQuery(this.createMutation); @@ -379,15 +389,11 @@ export abstract class NaturalAbstractModelService< return this.apollo .mutate({ + ...(options as MutateOptionsWithoutVariables), mutation: this.createMutation, variables: variables, }) - .pipe( - map(result => { - this.apollo.client.refetchObservableQueries(); - return this.mapCreation(result); - }), - ); + .pipe(map(result => this.mapCreation(result))); } /** @@ -436,10 +442,7 @@ export abstract class NaturalAbstractModelService< */ public delete( objects: {id: string}[], - options: MutateOptionsWithoutVariables = {} as MutateOptionsWithoutVariables< - Tdelete, - Vdelete - >, + options: MutateOptionsWithoutVariables = {}, ): Observable { this.throwIfObservable(objects); this.throwIfNotQuery(this.deleteMutation); @@ -459,7 +462,7 @@ export abstract class NaturalAbstractModelService< return this.apollo .mutate({ - ...options, + ...(options as MutateOptionsWithoutVariables), mutation: this.deleteMutation, variables: variables, })