import { injectable } from 'inversify';
import { entities } from './decorators';
import { EntityQueryBuilder } from './EntityQueryBuilder';
import { QueryPartField } from './QueryPartField';
import { Field } from './EntityDescriptor';
import { QueryOptions, ValidSuffix } from './QueryBuilder';
import { NotFoundError } from './NotFoundError';

@injectable()
export class FirebirdQueryBuilder {
  public queryEntity<
    T,
    F extends keyof T,
    V = {
      [K in F]: T[K];
    },
  >({
    entity,
    fields,
    where,
    groupBy,
    first,
    skip,
  }: QueryBuilderOptions<T, F>): {
    query: QueryPartField<V>;
  } {
    const dbFields = this.getDatabaseFields({
      fields,
      entity,
    });
    const { tableName, from } = this.getEntity(entity);
    const allColumnsAreOnTableName = dbFields.every((item) =>
      item.columnName.startsWith(${tableName}.),
    );

    const queryOptions: QueryOptions = {
      from: allColumnsAreOnTableName ? tableName : from,
      select: dbFields.map((item) => item.fieldName),
      where,
      groupBy,
      first,
      skip,
    };
    const options = new EntityQueryBuilder(this.getEntity(entity));
    const queryPart = options.buildQuery(queryOptions);
    const query = new QueryPartField<V>(queryPart.sql, queryPart.parameters, dbFields);

    return {
      query,
    };
  }

  private getField<T>(entity: Constructor<T>, field: keyof T) {
    const value = this.getEntity(entity);
    const item = value.ownFields.find((item) => item.fieldName === field.toString().split('_')[0]);
    if (item) {
      return item;
    } else {
      throw new NotFoundError(Did not find ${String(field)} firebird column on entity ${entity}.);
    }
  }

  private getEntity<T>(entity: Constructor<T>) {
    const value = entities.get(entity.prototype);
    if (!value) {
      throw new NotFoundError('Entity not found');
    }
    return value;
  }

  private getDatabaseFields<T, F extends keyof T>({
    fields,
    entity,
  }: {
    fields: F[] | '*';
    entity: new () => T;
  }): Field[] {
    return fields === '*'
      ? this.getEntity(entity).fields
      : fields.map((item) => this.getField(entity, item));
  }
}

export interface QueryBuilderOptions<T, F extends keyof T> {
  entity: new () => T;
  fields: F[] | '*';
  where?: ChainedAndOr<T>;
  groupBy?: string;
  first?: number;
  skip?: number;
}

export type ChainedAndOr<T> =
  | PartialRecord<T>
  | {
      AND?: Array<PartialRecord<T> | ChainedAndOr<T>>;
      OR?: Array<PartialRecord<T> | ChainedAndOr<T>>;
    };

export type PartialRecord<T> =
  | Partial<Record<keyof T | 'SQLCONDITION', T[keyof T]>>
  | Partial<{
      [K in SuffixKey<keyof T>]: K extends ${infer Field}_in ? string[] : T[keyof T];
    }>;

type Constructor<T> = new () => T;
type SuffixKey<K> = ${string & K}_${ValidSuffix};
import DataLoader from 'dataloader';
import { injectable } from 'inversify';
import sortObject from 'sort-object-keys';
import { ObjectLiteral } from 'typeorm';
import {
  ChainedAndOr,
  FirebirdQueryBuilder,
  PartialRecord,
  QueryBuilderOptions,
} from './FirebirdQueryBuilder';
import { FirebirdConnector } from './FirebirdConnector';
import { NotFoundError } from './NotFoundError';

@injectable()
export class FirebirdQueryBuilderDataFetcher {
  constructor(
    private firebirdQueryBuilder: FirebirdQueryBuilder,
    private firebirdConnector: FirebirdConnector,
  ) {}

  private getAllEntityByIdDataLoader: Partial<Record<string, DataLoader<unknown, unknown>>> = {};

  private getEntityByIdDataLoader: Partial<Record<string, DataLoader<unknown, unknown>>> = {};

  public async getById<T extends { id: string }, F extends keyof T>({
    entity,
    fields,
    id,
  }: {
    entity: Entity<T>;
    fields: ['id', ...Array<keyof T>] | '*';
    id: string;
  }): Promise<{ [K in F]: T[K] }> {
    const dataloader = this.createOrGetEntityByIdDataLoader({
      entity,
    });
    const item = await dataloader.load({
      fields,
      id,
    });
    if (item === null) {
      throw new NotFoundError();
    }

    const constructedEntity = new entity();
    Object.assign(constructedEntity, item);
    return constructedEntity;
  }

  public async getAll<T extends ObjectLiteral, F extends keyof T>({
    entity,
    fields,
    where,
    groupBy,
  }: QueryBuilderOptions<T, F>) {
    const dataloader = this.createOrGetAllEntityDataLoader({
      entity,
    });
    const results = await dataloader.load({
      fields,
      where,
      groupBy,
    });

    return results.map((item) => {
      const constructedEntity = new entity();
      Object.assign(constructedEntity, item);
      return constructedEntity;
    });
  }

  public clearAllCache<T>(entity: Entity<T>) {
    this.getAllEntityByIdDataLoader[entity.name]?.clearAll();
    this.getEntityByIdDataLoader[entity.name]?.clearAll();
  }

  private createOrGetEntityByIdDataLoader<
    T extends { id: string },
    F extends keyof T,
    V = {
      [K in F]: T[K];
    },
  >({ entity }: { entity: Entity<T> }): SingleLoader<F, V> {
    if (this.getEntityByIdDataLoader[entity.name]) {
      return this.getEntityByIdDataLoader[entity.name] as SingleLoader<F, V>;
    }

    const cacheKeyFn = (item: SingleLoaderKey<F>): string => {
      return ${item.id}-${item.fields};
    };

    this.getEntityByIdDataLoader[entity.name] = new DataLoader<
      SingleLoaderKey<F>,
      V | null,
      string
    >(
      async (keys) => {
        const sharedBatchFields = keys[0].fields;
        const where: PartialRecord<Record<string, 'id'>> = {
          id_in: keys.map((item) => item.id.toString()),
        };
        const { query } = this.firebirdQueryBuilder.queryEntity<T, F, V>({
          entity: entity,
          fields: sharedBatchFields,
          where: where as unknown as PartialRecord<T>,
        });

        const results = query.convert(
          await this.firebirdConnector.executeQuery(query.sql, query.parameters),
        ) as Array<{ id: string }>;

        const ids = keys.map((where) => {
          const row = results.find((item) => item.id === where.id);
          if (row) {
            return row as V;
          } else {
            return null;
          }
        });
        return ids;
      },
      {
        maxBatchSize: 100,
        cacheKeyFn,
      },
    );

    return this.getEntityByIdDataLoader[entity.name] as SingleLoader<F, V>;
  }

  private createOrGetAllEntityDataLoader<
    T,
    F extends keyof T,
    V = {
      [K in F]: T[K];
    },
  >({ entity }: { entity: Entity<T> }): MultiLoader<F, T, V> {
    if (this.getAllEntityByIdDataLoader[entity.name]) {
      return this.getAllEntityByIdDataLoader[entity.name] as MultiLoader<F, T, V>;
    }

    const cacheKeyFn = (item: MultiLoaderKey<F, T>): string => {
      const fields = typeof item.fields === 'string' ? item.fields : item.fields.join('');
      const object = item.where || {};
      return ${fields}-${item.groupBy}-${JSON.stringify(sortObject(object))};
    };

    this.getAllEntityByIdDataLoader[entity.name] = new DataLoader<
      MultiLoaderKey<F, T>,
      V[],
      string
    >(
      async (keys) => {
        return Promise.all(
          keys.map(async (item) => {
            const { query } = this.firebirdQueryBuilder.queryEntity<T, F, V>({
              entity: entity,
              fields: item.fields,
              where: item.where,
              groupBy: item.groupBy,
            });

            return query.convert(
              await this.firebirdConnector.executeQuery(query.sql, query.parameters),
            );
          }),
        );
      },
      {
        maxBatchSize: 100,
        cacheKeyFn,
      },
    );

    return this.getAllEntityByIdDataLoader[entity.name] as MultiLoader<F, T, V>;
  }
}
type MultiLoader<F, T, V> = DataLoader<MultiLoaderKey<F, T>, V[]>;
type MultiLoaderKey<F, T> = { fields: F[] | '*'; where?: ChainedAndOr<T>; groupBy?: string };

type SingleLoader<F, V> = DataLoader<SingleLoaderKey<F>, V | null>;
type SingleLoaderKey<F> = {
  fields: F[] | '*';
  id: string;
};
type Entity<T> = new () => T;


---
      [K in SuffixKey<keyof T>]: K extends ${infer Field}_in ? string[] : T[keyof T];
'Field' is declared but its value is never read.ts(6133)

do you know why this error happens? what is the fix? i can see that when i change for example:
          idnotexists_in: keys.map((item) => item.id.toString()), 
then i do not get a typescript error, and i also do not get autocomplete. is it bugged? why does this happen? what is the fix? please educate me, and also explain how this is supposed to work.