# Known patterns — executable examples

Real code anchored to the `iam/tag` module. When you copy a structure from one of these sections,
verify against the current file on disk — generated files may change.

---

## List flow (iam/tag)

### 1. List config — single source of truth

`frontend/src/app/domains/admin/bounded-contexts/iam/tag/data-access/tag.list-config.ts`

```typescript
import { deleteByIdMutation, fields, paginationQuery } from './tag.graphql';

export const TAG_LIST_CONFIG = {
  queryPagination: paginationQuery,
  paginationKey: 'pagination',
  mutationDeleteObjectById: deleteByIdMutation,
  fields,
} as const;
```

Both the resolver and the component consume this object. The component via the facade
`useGraphqlList(TAG_LIST_CONFIG)`; the resolver via destructuring.

### 2. List component — composable + useDataTable + builders

Excerpt of `tag-list.component.ts` (trimmed to the data-layer wiring):

```typescript
import {
  buildFilterWhere, buildSearchWhere, buildSortOrder,
  DEFAULT_PAGE_SIZE, useDataTable, useGraphqlList,
  type ColumnFilter, type DataTableData,
} from '@aurora';
import { Operator } from '@aurorajs.dev/core-common';
import { TAG_LIST_CONFIG } from '../data-access/tag.list-config';

export default class TagListComponent {
  private readonly route = inject(ActivatedRoute);

  // composable facade — paginate + delete + loading
  private readonly list = useGraphqlList<IamTag>(TAG_LIST_CONFIG);

  // local state
  private readonly searchValue = signal('');
  private readonly columnFilters = signal<ColumnFilter[]>([]);
  readonly page = signal<DataTableData<IamTag>>(this.route.snapshot.data['pagination']);

  // TanStack table instance
  private readonly dataTable = useDataTable<IamTag>({
    data: () => this.page()?.rows ?? [],
    columns: this.columns,
    totalRows: () => this.page()?.total ?? 0,
    pageSize: DEFAULT_PAGE_SIZE,
    manualSorting: true,
    manualPagination: true,
    onSortingChange: (sorting) => this.fetchPage({ sorting }),
    onPaginationChange: (p) =>
      this.fetchPage({ offset: p.pageIndex * p.pageSize, limit: p.pageSize }),
  });
  protected readonly table = this.dataTable.table;

  // refetch — translates current state into fetcher variables
  private async fetchPage(params: { sorting?: SortingState; offset?: number; limit?: number } = {}) {
    const state = this.table.getState();
    const order = buildSortOrder(params.sorting ?? state.sorting);
    const offset = params.offset ?? state.pagination.pageIndex * state.pagination.pageSize;
    const limit = params.limit ?? state.pagination.pageSize;
    const searchWhere = buildSearchWhere(this.searchValue(), this.searchColumns);
    const filterWhere = buildFilterWhere(this.columnFilters());
    const where =
      searchWhere && filterWhere
        ? { [Operator.and]: [searchWhere, filterWhere] }
        : (searchWhere ?? filterWhere);

    const fresh = await lastValueFrom(
      this.list.paginate({ query: { where, order, offset, limit } }),
    );
    this.page.set(fresh);
  }

  async confirmDelete() {
    const tag = this.tagToDelete();
    if (!tag) return;
    await lastValueFrom(this.list.deleteById(tag.id));
    await this.fetchPage();
  }
}
```

Key things to notice:

- The composable (`this.list`) is instantiated ONCE in the class body and reused for refetch and
  delete.
- The initial page comes from the resolver (`route.snapshot.data['pagination']`) — the component
  does NOT call `paginate()` on init.
- `Operator.and` is used as the dynamic key (NOT `'[and]'`).

---

## Detail flow (iam/tag)

### 1. Detail config — single source of truth

`frontend/src/app/domains/admin/bounded-contexts/iam/tag/data-access/tag.detail-config.ts`

```typescript
import { createMutation, fields, findByIdQuery, updateByIdMutation } from './tag.graphql';

export const TAG_DETAIL_CONFIG = {
  queryObject: findByIdQuery,
  objectKey: 'object',
  mutationCreateObject: createMutation,
  mutationUpdateObject: updateByIdMutation,
  fields,
} as const;
```

### 2. Detail component — useGraphqlDetail + pickModel + signalForm

Excerpt of `tag-detail.component.ts` (trimmed to the data-layer wiring):

```typescript
import { pickModel, useGraphqlDetail } from '@aurora';
import { TAG_DETAIL_CONFIG } from '../data-access/tag.detail-config';

export default class TagDetailComponent {
  private readonly route = inject(ActivatedRoute);
  private readonly router = inject(Router);
  private readonly fb = inject(FormBuilder);

  readonly mode = signal<'new' | 'edit'>(this.route.snapshot.data['mode']);
  readonly detail = useGraphqlDetail<IamTag>(TAG_DETAIL_CONFIG);

  // pickModel = safe projection: never leaks extra keys from source,
  // fills missing keys with the defaults provided.
  readonly signalModel = pickModel(this.route.snapshot.data['item'], {
    id: '',
    name: '',
  });

  readonly signalForm = this.fb.group({
    id: [
      this.signalModel.id,
      [Validators.required, Validators.minLength(36), Validators.maxLength(36)],
    ],
    name: [this.signalModel.name, [Validators.required, Validators.maxLength(64)]],
  });

  async onSubmit() {
    if (this.signalForm.invalid) return;

    if (this.mode() === 'new') {
      await lastValueFrom(
        this.detail.create(this.signalForm.value as Record<string, unknown>),
      );
    } else {
      await lastValueFrom(
        this.detail.update(this.signalForm.value as Record<string, unknown>),
      );
    }

    this.router.navigate(['/iam/tag']);
  }
}
```

Key things to notice:

- `pickModel(item, defaults)` converts the resolver-loaded record into the exact form shape. The
  defaults object is also documentation of the form schema.
- `useGraphqlDetail` exposes `create` and `update` as methods; both return `Observable` consumed
  with `lastValueFrom`.
- `mode` comes from `route.snapshot.data['mode']` — the route config distinguishes `new` from
  `edit`.

---

## Resolver flow (iam/tag)

`frontend/src/app/domains/admin/bounded-contexts/iam/tag/data-access/tag.resolvers.ts`

```typescript
import { type ResolveFn } from '@angular/router';
import {
  buildSortOrder,
  DEFAULT_PAGE_SIZE,
  queryFindById,
  queryPaginate,
  type DataTableData,
} from '@aurora';
import { type IamTag } from '@src/src/app/aurora.types';
import { TAG_DETAIL_CONFIG } from './tag.detail-config';
import { TAG_LIST_CONFIG } from './tag.list-config';

export const tagListResolver: ResolveFn<DataTableData<IamTag>> = () => {
  const { queryPagination, paginationKey, fields } = TAG_LIST_CONFIG;
  return queryPaginate<IamTag>(queryPagination, paginationKey, fields, {
    query: {
      offset: 0,
      limit: DEFAULT_PAGE_SIZE,
      order: buildSortOrder([]),
    },
  });
};

export const tagNewResolver: ResolveFn<boolean> = () => {
  // placeholder for pre-loading relations, enums, or other data needed to create a tag
  return true;
};

export const tagDetailResolver: ResolveFn<IamTag | null> = (route) => {
  const id = route.paramMap.get('id');
  if (!id) return null;
  const { queryObject, objectKey, fields } = TAG_DETAIL_CONFIG;
  return queryFindById<IamTag>(queryObject, objectKey, id, fields);
};
```

Key things to notice:

- Imports from `@aurora` are **fetchers** (`queryPaginate`, `queryFindById`) — NO `useGraphql*`.
- The resolver destructures from the config object so the query, key, and fields stay in sync
  with the component.
- The list resolver uses `buildSortOrder([])` to produce an empty order (valid default for "no
  sort yet").
- The returned `Observable` is what the router awaits.

---

## Escape hatch — `inject(Apollo)` directly

Reserved for cases where neither a fetcher nor a composable fits. Examples: custom cache policy
tuning, `watchQuery` with a live cache subscription, or you are implementing a new fetcher /
composable and are by definition underneath the layer.

```typescript
import { inject, signal } from '@angular/core';
import { Apollo } from 'apollo-angular';
import { map } from 'rxjs';
import { MY_CUSTOM_QUERY } from './my-custom.graphql';

// Minimal example — a live watchQuery that updates as the cache changes.
export class MyCustomComponent {
  private readonly apollo = inject(Apollo);
  readonly state = signal<MyData | null>(null);

  constructor() {
    this.apollo
      .watchQuery<{ myData: MyData }>({
        query: MY_CUSTOM_QUERY,
        fetchPolicy: 'cache-and-network',
      })
      .valueChanges.pipe(map((r) => r.data?.myData ?? null))
      .subscribe((value) => this.state.set(value));
  }
}
```

Before using this pattern, check `fetchers/index.ts`. Nine times out of ten, a fetcher already
covers what you need.
