import { client } from "@logilab/cwclientlibjs"; import { RqlQuery } from "@logilab/cwclientlibjs/build/client"; 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 }) => { const attributeUpdates: string[] = []; const relationUpdates: string[] = []; const restrictionsUpdate: string[] = [`X is ${resource}, X eid ${id}`]; let counter = 0; Object.entries(data).forEach(([key, value]) => { if (key in schema.entities[resource]) { attributeUpdates.push(`X ${key} ${JSON.stringify(value)}`); } else if (subjectRelationsNames(resource).includes(key)) { if (!Array.isArray(value)) { throw new Error(`An array is expected for relation ${key}.`); } relationUpdates.push(`X ${key} Y${counter}`); if (value.length > 0) { restrictionsUpdate.push(`Y${counter} eid IN (${value.join(", ")})`); } counter += 1; } else if ( key.startsWith("reverse_") && objectRelationsNames(resource).includes(key.replace("reverse_", "")) ) { if (!Array.isArray(value)) { throw new Error(`An array is expected for relation ${key}.`); } relationUpdates.push(`Y${counter} ${key.replace("reverse_", "")} X`); if (value.length > 0) { restrictionsUpdate.push(`Y${counter} eid IN (${value.join(", ")})`); } counter += 1; } }); const queries: RqlQuery[] = [ [ ` SET ${[...attributeUpdates, ...relationUpdates].join(", ")} WHERE ${restrictionsUpdate.join(", ")}`, {}, ], ]; if (relationUpdates.length !== 0) { queries.unshift([ ` DELETE ${relationUpdates.join(", ")} WHERE X is ${resource}, X eid ${id}`, {}, ]); } await rqlClient.transactionV2(queries); return Promise.resolve({ data: { ...data, id } }); }; const updateMany: DataProvider["updateMany"] = (_resource, _params) => Promise.reject("Not implemented"); const create: DataProvider["create"] = async (resource, { data }) => { 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, }; }