rayman1104 / ra-data-nestjsx-crud

Data provider which integrates React Admin with NestJS CRUD library
MIT License
100 stars 27 forks source link

Is it compatible with react-admin 5.1.0 and @dataui/crud? #51

Open IbrahemHadidy opened 1 month ago

IbrahemHadidy commented 1 month ago

I am using react-admin 5.1.0, @dataui/crud 5.3.4, @dataui/crud-typeorm 5.3.4 but the problem is the sorting and filters doesn't seem to work

what i noticed is the sent syntax is: http://localhost:4000/api/admin/developers?&filter%5B0%5D=s%7C%7C%24contL%7C%7Csomename&filter%5B1%5D=name%7C%7C%24contL%7C%7Cas&limit=10&page=1&sort%5B0%5D=id%2CDESC&offset=0

decoded: http://localhost:4000/api/admin/developers?&filter[0]=s||$contL||somename&filter[1]=name||$contL||as&limit=10&page=1&sort[0]=id,DESC&offset=0

this is swagger documentation of the GET request (using @dataui/crud): image

if there is a fix/solution to this problem or mention any other package that suits this docs I would be grateful.

IbrahemHadidy commented 1 month ago

I made my own edit/solution (not sure if it is stable yet)

import { CondOperator } from '@dataui/crud-request';
import omitBy from 'lodash.omitby';
import { fetchUtils } from 'react-admin';

import type {
  DataProvider as AdminDataProvider,
  CreateParams,
  CreateResult,
  DeleteManyParams,
  DeleteManyResult,
  DeleteParams,
  DeleteResult,
  GetListParams,
  GetListResult,
  GetManyParams,
  GetManyReferenceParams,
  GetManyReferenceResult,
  GetManyResult,
  GetOneParams,
  GetOneResult,
  UpdateManyParams,
  UpdateManyResult,
  UpdateParams,
  UpdateResult,
} from 'react-admin';

class DataProvider implements AdminDataProvider {
  private apiUrl: string;
  private httpClient: typeof fetchUtils.fetchJson;

  constructor(apiUrl: string, httpClient = fetchUtils.fetchJson) {
    this.apiUrl = apiUrl;
    this.httpClient = httpClient;
  }

  private countDiff = (
    o1: Record<string, unknown>,
    o2: Record<string, unknown>
  ): Record<string, unknown> => omitBy(o1, (v: unknown, k: string | number) => o2[k] === v);

  private composeFilter = (paramsFilter: unknown): string => {
    const flatFilter = fetchUtils.flattenObject(paramsFilter);
    return Object.keys(flatFilter)
      .map((key) => {
        const splitKey = key.split(/\|\||:/);
        const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/gi;

        let field = splitKey[0];
        let ops = splitKey[1];
        if (!ops) {
          if (
            typeof flatFilter[key] === 'boolean' ||
            typeof flatFilter[key] === 'number' ||
            (typeof flatFilter[key] === 'string' && flatFilter[key].match(/^\d+$/)) ||
            flatFilter[key].match(uuidRegex)
          ) {
            ops = CondOperator.EQUALS;
          } else {
            ops = CondOperator.CONTAINS_LOW;
          }
        }

        if (field.startsWith('_') && field.includes('.')) {
          field = field.split(/\.(.+)/)[1];
        }
        return `filter=${field}||${ops}||${flatFilter[key]}`;
      })
      .join('&');
  };

  private composeQueryParams = (params: GetListParams): string => {
    const queryParams: { [key: string]: string | number } = {};

    if (params.pagination) {
      queryParams.limit = params.pagination.perPage;
      queryParams.offset = (params.pagination.page - 1) * params.pagination.perPage;
    }

    if (params.sort) {
      queryParams.sort = `${params.sort.field},${params.sort.order}`;
    }

    const filterString = this.composeFilter(params.filter || {});
    const queryString = Object.entries(queryParams)
      .map(([key, value]) => `${key}=${value}`)
      .join('&');

    return filterString ? `${queryString}&${filterString}` : queryString;
  };

  public getList = async (resource: string, params: GetListParams): Promise<GetListResult> => {
    const queryStringParams = this.composeQueryParams(params);

    const url = `${this.apiUrl}/${resource}?${queryStringParams}`;
    const { json } = await this.httpClient(url);
    return {
      data: json.data,
      total: json.total,
    };
  };

  public getOne = async (resource: string, params: GetOneParams): Promise<GetOneResult> => {
    const { json } = await this.httpClient(`${this.apiUrl}/${resource}/${params.id}`);
    return { data: json };
  };

  public getMany = async (resource: string, params: GetManyParams): Promise<GetManyResult> => {
    const query = `filter=id||${CondOperator.IN}||${params.ids.join(',')}`;
    const url = `${this.apiUrl}/${resource}?${query}`;

    const { json } = await this.httpClient(url);
    return { data: json.data || json };
  };

  public getManyReference = async (
    resource: string,
    params: GetManyReferenceParams
  ): Promise<GetManyReferenceResult> => {
    const queryParams = this.composeQueryParams(params);
    const queryStringParams =
      queryParams + `&filter=${params.target}||${CondOperator.EQUALS}||${params.id}`;

    const url = `${this.apiUrl}/${resource}?${queryStringParams}`;
    const { json } = await this.httpClient(url);
    return {
      data: json.data,
      total: json.total,
    };
  };

  public update = async (resource: string, params: UpdateParams): Promise<UpdateResult> => {
    const data = this.countDiff(params.data, params.previousData);
    const { json } = await this.httpClient(`${this.apiUrl}/${resource}/${params.id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
    return { data: json };
  };

  public updateMany = async (
    resource: string,
    params: UpdateManyParams
  ): Promise<UpdateManyResult> => {
    const responses = await Promise.all(
      params.ids.map(async (id) => {
        const { json } = await this.httpClient(`${this.apiUrl}/${resource}/${id}`, {
          method: 'PUT',
          body: JSON.stringify(params.data),
        });
        return json;
      })
    );

    return {
      data: responses,
    };
  };

  public create = async (resource: string, params: CreateParams): Promise<CreateResult> => {
    const { json } = await this.httpClient(`${this.apiUrl}/${resource}`, {
      method: 'POST',
      body: JSON.stringify(params.data),
    });
    return {
      data: { ...json },
    };
  };

  public delete = async (resource: string, params: DeleteParams): Promise<DeleteResult> => {
    const { json } = await this.httpClient(`${this.apiUrl}/${resource}/${params.id}`, {
      method: 'DELETE',
    });
    return { data: { ...json, id: params.id } };
  };

  public deleteMany = async (
    resource: string,
    params: DeleteManyParams
  ): Promise<DeleteManyResult> => {
    const responses = await Promise.all(
      params.ids.map(async (id) => {
        const { json } = await this.httpClient(`${this.apiUrl}/${resource}/${id}`, {
          method: 'DELETE',
        });
        return json;
      })
    );
    return { data: responses };
  };
}

const dataProvider = (apiUrl: string, httpClient = fetchUtils.fetchJson) =>
  new DataProvider(apiUrl, httpClient);
export default dataProvider;