Skip to content
Snippets Groups Projects
createDataProvider.ts 8.44 KiB
Newer Older
import { client } from "@logilab/cwclientlibjs";
import { AuthProvider, DataProvider, DeleteResult } from "ra-core";
import { Schema } from "./Schema";
import { deduplicate } from "./utils/deduplicate";

// import authProvider from './authProvider';
// see https://marmelab.com/react-admin/Authentication.html
export const authProvider: AuthProvider = {
  // authentication
  login: (_params) => Promise.resolve(),
  checkError: (_error) => Promise.resolve(),
  checkAuth: (_params) => Promise.resolve(),
  logout: () => Promise.resolve(),
  getIdentity: () => Promise.resolve({ id: 0 }),
  // authorization
  getPermissions: (_params) => Promise.resolve(),
};

export function createDataProvider<S extends Schema<string>>(
  rqlClient: client.CwRqlClient,
  schema: S
): DataProvider {
  function subjectRelationsNames(entityType: string) {
    return deduplicate(
      schema.relationsDefinitions
        .filter(({ subject }) => subject === entityType)
        .map(({ name }) => name)
    );
  }

  function objectRelationsNames(entityType: string) {
    return deduplicate(
      schema.relationsDefinitions
        .filter(({ object }) => object === entityType)
        .map(({ name }) => name)
    );
  }

  const getList: DataProvider["getList"] = async (
    resource: string,
    { pagination, sort, filter }
  ) => {
    const sortAttribute = sort.field === "id" ? "eid" : sort.field;
    const attributesNames = ["eid", ...Object.keys(schema.entities[resource])];
    const selection: string[] = [];
    const restrictions: string[] = [];
    let sortvariable = null;
    attributesNames.forEach((key, idx) => {
      const variable = `X${idx}`;
      selection.push(variable);
      restrictions.push(`X ${key} ${variable}`);
      if (key === sortAttribute) {
        sortvariable = variable;
      }
    });

    // Handle filters
    restrictions.push(
      ...Object.entries(filter).map(([attrName, attrValue]) => {
        return `EXISTS(X ${attrName} ~= '%${attrValue}%')`;
      })
    );

    const total = await rqlClient
      .queryRows(`Any Count(${selection[0]}) WHERE ${restrictions.join(",")}`)
      .then((rows) => rows[0][0]);
    return rqlClient
      .queryRows(
        `Any ${selection.join(", ")}  ORDERBY ${sortvariable} ${
          sort.order
        } LIMIT ${pagination.perPage} OFFSET ${
          (pagination.page - 1) * pagination.perPage
        } WHERE ${restrictions.join(", ")}`,
        {}
      )
      .then((rows) => {
        return {
          data: rows.map((row) =>
            row.reduce(
              (agg, attributeValue, idx) => ({
                [attributesNames[idx]]: attributeValue,
                ...agg,
              }),
              { id: row[0] }
            )
          ),
          total,
        };
      });
  };

  const getOne: DataProvider["getOne"] = async (resource: string, params) => {
    // Getting attributes
    const attributesNames = [...Object.keys(schema.entities[resource])];
    const selection: string[] = [];
    const restrictions: string[] = [];
    attributesNames.forEach((key, idx) => {
      const variable = `X${idx}`;
      selection.push(variable);
      restrictions.push(`X ${key} ${variable}`);
    });

    const result = await rqlClient.queryRows(
      `Any ${selection.join(",")} Where ${restrictions.join(",")}, X eid ${
        params.id
      }`
    );
    const row = result[0];
    const entity = row.reduce(
      (agg, attributeValue, idx) => ({
        [attributesNames[idx]]: attributeValue,
        ...agg,
      }),
      { eid: params.id, id: params.id }
    );

    // Getting subject relations
    const subjectRelations = schema.relationsDefinitions.filter(
      ({ subject }) => subject === resource
    );
    for (const relation of subjectRelations) {
      const result = await rqlClient.queryRows(
        `Any TARGET Where X ${relation.name} TARGET, X eid ${params.id}, TARGET is ${relation.object}`
      );
      (entity[relation.name] ??= []).push(...result.map((row) => row[0]));
    }

    // Getting object relations
    const objectRelations = schema.relationsDefinitions.filter(
      ({ object }) => object === resource
    );
    for (const relation of objectRelations) {
      const result = await rqlClient.queryRows(
        `Any TARGET Where TARGET ${relation.name} X, X eid ${params.id}, TARGET is ${relation.subject}`
      );
      (entity[`reverse_${relation.name}`] ??= []).push(
        ...result.map((row) => row[0])
      );
    }
    return { data: entity };
  };

  const getMany: DataProvider["getMany"] = async (resource, params) => {
    // FIXME handle several params id and refactor code to use GetOne
    // Getting attributes
    const attributesNames = [...Object.keys(schema.entities[resource])];
    const selection: string[] = [];
    const restrictions: string[] = [];
    attributesNames.forEach((key, idx) => {
      const variable = `X${idx}`;
      selection.push(variable);
      restrictions.push(`X ${key} ${variable}`);
    });
    const result = await rqlClient.queryRows(
      `Any ${selection.join(",")} Where ${restrictions.join(
        ","
      )}, X eid IN (${params.ids.join(",")})`
    );
    const entities = result.map((row, rowIdx) =>
      row.reduce(
        (agg, attributeValue, idx) => ({
          [attributesNames[idx]]: attributeValue,
          ...agg,
        }),
        { id: params.ids[rowIdx] }
      )
    );

    return { data: entities };
  };

  const getManyReference: DataProvider["getManyReference"] = (
    _resource,
    _params
  ) => Promise.reject("Not implemented");

  const update: DataProvider["update"] = async (resource, { data, id }) => {
    // FIXME update relations
    const attributesUpdates: string[] = [];
    Object.entries(data).forEach(([key, value]) => {
      if (key in schema.entities[resource]) {
        attributesUpdates.push(`X ${key} ${JSON.stringify(value)}`);
      }
    });
    await rqlClient.queryRows(`
        SET ${attributesUpdates.join(", ")} WHERE X is ${resource}, X eid ${id}
      `);
    return Promise.resolve({ data: { ...data, id } });
  };

  const updateMany: DataProvider["updateMany"] = (_resource, _params) =>
    Promise.reject("Not implemented");

  const create: DataProvider["create"] = async (resource, { data }) => {
    // FIXME create relations
    const assignments: string[] = [];
    const restrictions: string[] = [];
    Object.entries(data).forEach(([key, value]) => {
      if (key in schema.entities[resource]) {
        assignments.push(`X ${key} ${JSON.stringify(value)}`);
      }
    });

    let counter = 0;
    function addRelation(
      relationName: string,
      resourceRole: "subject" | "object",
      targetId: number
    ) {
      const relationTargetVariable = `Y${counter++}`;
      if (resourceRole === "subject") {
        assignments.push(`X ${relationName} ${relationTargetVariable}`);
      } else {
        assignments.push(`${relationTargetVariable} ${relationName} X`);
      }
      restrictions.push(`${relationTargetVariable} eid ${targetId}`);
    }

    function toArray(idOrIds: number | number[] | undefined) {
      if (Array.isArray(idOrIds)) {
        return idOrIds;
      }
      if (idOrIds === undefined) {
        return [];
      }
      return [idOrIds];
    }
    for (const relationName of subjectRelationsNames(resource)) {
      const ids = toArray(data[relationName]);
      ids.forEach((id) => addRelation(relationName, "subject", id));
    }

    for (const relationName of objectRelationsNames(resource)) {
      const ids = toArray(data[`reverse_${relationName}`]);
      ids.forEach((id) => addRelation(relationName, "object", id));
    }

    const result = await rqlClient.queryRows(`
        INSERT ${resource} X:
           ${assignments.join(", ")}
        ${restrictions.length > 0 ? "WHERE " + restrictions.join(", ") : ""}
    `);

    const eid = result[0][0];
    return Promise.resolve({ data: { ...data, id: eid } });
  };

  const _delete: DataProvider["delete"] = async (resource, data) => {
    await rqlClient.queryRows(`
      DELETE ${resource} X WHERE X eid ${data.id}
    `);
    // FIXME: find how to correctly specify type without "any"
    return { data: data.previousData } as DeleteResult<any>;
  };
  const deleteMany: DataProvider["deleteMany"] = async (resource, data) => {
    await rqlClient.queryRows(`
      DELETE ${resource} X WHERE X eid IN (${data.ids.join(", ")})
    `);
    return { data: data.ids };
  };

  return {
    getList,
    getOne,
    getMany,
    getManyReference,
    update,
    updateMany,
    create,
    delete: _delete,
    deleteMany,
  };
}