Aller au contenu

Writing a service

Copy of sumaris-app/.agents/rules/service.md (rule written for AI coding agents, when creating or modifying Angular services). Do not edit here: run npm run site:sync-rules to update it.

  • Use the file suffix .service.ts;
  • Use @Injectable({ providedIn: 'root' }) for application-wide singleton services, or @Injectable() if the service is provided in a specific module or component;

Base classes

  • For a full CRUD entity service with GraphQL, extend BaseEntityService<T, F, ID>:
    • T is the entity type (extends Entity);
    • F is the filter type (extends EntityFilter);
    • ID is the identifier type (default number);
    • This base class provides load(), loadAll(), watchAll(), save(), saveAll(), deleteAll(), listenChanges() out of the box;
  • For a simpler service with manual GraphQL queries, extend BaseGraphqlService<T, F, ID>;
  • Both base classes already inject GraphqlService internally (exposed as this.graphql) and, for BaseEntityService, PlatformService (exposed as this.platform) — do not pass them through your subclass's constructor;

GraphQL queries and mutations

  • Define GraphQL queries and mutations as constants above the service class, using gql from apollo-angular or @apollo/client/core:
    const Queries: BaseEntityGraphqlQueries = {
      loadAll: gql`...`,
      loadAllWithTotal: gql`...`,
      load: gql`...`,
    };
    const Mutations: BaseEntityGraphqlMutations = {
      save: gql`...`,
      saveAll: gql`...`,
      deleteAll: gql`...`,
    };
    
  • Use GraphQL fragments to share field definitions across queries and mutations;
  • Pass queries and mutations to the base class via the options object in super();

Constructor

  • Never redeclare graphql: GraphqlService (or, for BaseEntityService subclasses, platform: PlatformService) as a constructor parameter, and never call inject(GraphqlService) directly. The bare GraphqlService class has no provider anymore (it's only provided through the internal APP_GRAPHQL_SERVICE token) — redeclaring/shadowing it this way causes NG0201: No provider found for GraphqlService at runtime as soon as the service is instantiated;
  • Inject all other dependencies via inject(), declared as class fields above the constructor (not as constructor parameters), and keep the constructor empty except for the super() call and any setup logic:
    @Injectable({ providedIn: 'root' })
    export class MyEntityService extends BaseEntityService<MyEntity, MyEntityFilter> {
      protected accountService = inject(AccountService);
      protected network = inject(NetworkService);
    
      constructor() {
        super(MyEntity, MyEntityFilter, {
          queries: Queries,
          mutations: Mutations,
        });
        this._logPrefix = '[my-entity-service] ';
        this._debug = !environment.production;
      }
    }
    
  • For BaseGraphqlService (when not using BaseEntityService), the pattern is the same, but super() only takes an optional BaseGraphqlServiceOptions:
    @Injectable({ providedIn: 'root' })
    export class MyService extends BaseGraphqlService {
      protected accountService = inject(AccountService);
    
      constructor() {
        super({ production: environment.production });
      }
    }
    
  • If a service does not extend BaseGraphqlService/BaseEntityService but still needs GraphQL access, inject the token directly: protected graphql = inject(APP_GRAPHQL_SERVICE); (still never inject(GraphqlService));

Key methods to implement or override

  • load(id, opts?): Load a single entity by ID;
  • save(entity, opts?): Save an entity (validate, convert to JSON with asObject(), call GraphQL mutation, then update the entity with copyIdAndUpdateDate());
  • delete(entity, opts?): Delete an entity;
  • canUserWrite(data, opts?): Return whether the current user can write the entity;
  • fillDefaultProperties(entity): Set default values (e.g. statusId) before saving;
  • copyIdAndUpdateDate(source, target): Copy id and updateDate from the GraphQL response back to the entity;
  • listenChanges(id, opts?): Return an Observable to watch real-time changes via GraphQL subscriptions;

Save pattern

  • A typical save() method follows this pattern:

    async save(entity: MyEntity, opts?: EntityServiceLoadOptions): Promise<MyEntity> {
      // Validate required fields
      if (isNilOrBlank(entity?.label)) throw new Error('Missing a required label');
    
      // Fill defaults
      this.fillDefaultProperties(entity);
    
      // Convert to JSON
      const json = this.asObject(SAVE_AS_OBJECT_OPTIONS);
    
      // Execute mutation
      await this.graphql.mutate<{ data: any }>({
        mutation: Mutations.save,
        variables: { data: json },
        error: { code: ErrorCodes.SAVE_DATA_ERROR, message: 'ERROR.SAVE_DATA_ERROR' },
        update: (proxy, { data }) => {
          const savedEntity = data?.data;
          if (savedEntity) {
            this.copyIdAndUpdateDate(savedEntity, entity);
          }
        },
      });
    
      return entity;
    }
    

  • A typical asObject() method follows this pattern:

    protected asObject(source: MyEntity, opts?: DataEntityAsObjectOptions): any {
      opts = { ...MINIFY_OPTIONS, ...opts };
      const target: any = source.asObject({ ...MINIFY_OPTIONS, ...opts });
    
      // Additional serialization override (e.g. delete some properties, that not exists in the server)
      // delete target.someProperty;
    
      return target;
    }
    

Error handling

  • Use error codes from ErrorCodes and i18n message keys for GraphQL error options;
  • Throw structured errors { code, message } for business rule violations;

Debugging

  • Set this._debug = !environment.production in the constructor;
  • Use this._logPrefix for consistent log prefixes: [my-entity-service];
  • Guard debug logs with if (this._debug) console.debug(...);

Reference examples

  • Simple service (manual queries): src/app/referential/services/parameter.service.ts;
  • Full CRUD service (BaseEntityService): src/app/data/position/device/device-position.service.ts;
  • Offline-capable service (RootDataSynchroService): See offline data management;