import { getMutationFetchType } from './helpers/getMutationFetchType' import { resolveDynamicQuery } from './helpers/resolveDynamicQuery' import { validateResourceQuery, validateResourceQueries, } from './helpers/validate' import { requestOptionsToFetchType } from './links/RestAPILink/queryToRequestOptions' import type { DataEngineLink } from './types/DataEngineLink' import type { QueryExecuteOptions } from './types/ExecuteOptions' import { JSON_PATCH_CONTENT_TYPE, type JSONPatch } from './types/JSONPatch' import type { JsonMap, JsonValue } from './types/JsonValue' import type { Mutation } from './types/Mutation' import type { Query, ResourceQuery } from './types/Query' const reduceResponses = (responses: JsonValue[], names: string[]) => responses.reduce((out, response, idx) => { out[names[idx]] = response return out }, {}) export class DataEngine { private readonly link: DataEngineLink public constructor(link: DataEngineLink) { this.link = link } // Overload 1: When no generic is provided, accept any Query and return inferred type public query( query: Query, options?: QueryExecuteOptions ): Promise> // Overload 2: When generic is provided, enforce that query keys match the generic keys public query>( query: Record, options?: QueryExecuteOptions ): Promise public query>( query: Query, { variables = {}, signal, onComplete, onError, }: QueryExecuteOptions = {} ): Promise> { const names = Object.keys(query) const queries = names .map((name) => query[name]) .map((q) => resolveDynamicQuery(q, variables)) validateResourceQueries(queries, names) return Promise.all( queries.map((q) => { return this.link.executeResourceQuery('read', q, { signal, }) }) ) .then((results) => { const data = reduceResponses(results, names) onComplete?.(data) return data as TResult | Record }) .catch((error) => { onError?.(error) throw error }) } public mutate( mutation: Mutation, { variables = {}, signal, onComplete, onError, }: QueryExecuteOptions = {} ): Promise { const query = resolveDynamicQuery(mutation, variables) const type = getMutationFetchType(mutation) validateResourceQuery(type, query) const result = this.link.executeResourceQuery(type, query, { signal, }) return result .then((data) => { onComplete?.(data) return data }) .catch((error) => { onError?.(error) throw error }) } public async fetch( path: string, init: RequestInit = {}, executeOptions?: QueryExecuteOptions ): Promise { const type = requestOptionsToFetchType(init) if (path.includes('://')) { throw new Error( 'Absolute URLs are not supported by the DHIS2 DataEngine fetch interface' ) } const uri = new URL(path, 'http://dummybaseurl') const [, resource, id] = /^\/([^/]+)(?:\/([^?]*))?/.exec(uri.pathname) ?? [] const params = Object.fromEntries(uri.searchParams) if (type === 'read') { const queryResult = await this.query( { result: { resource, id, params, } as ResourceQuery, }, executeOptions ) return queryResult.result } return this.mutate( { type, resource, id, params, partial: type === 'update' && init.method === 'PATCH', data: init.body?.valueOf(), } as Mutation, executeOptions ) } public get(path: string, executeOptions?: QueryExecuteOptions) { return this.fetch(path, { method: 'GET' }, executeOptions) } public post(path: string, body: any, executeOptions?: QueryExecuteOptions) { return this.fetch(path, { method: 'POST', body }, executeOptions) } public put(path: string, body: any, executeOptions?: QueryExecuteOptions) { return this.fetch(path, { method: 'PUT', body }, executeOptions) } public patch( path: string, body: any, executeOptions?: QueryExecuteOptions ) { return this.fetch(path, { method: 'PATCH', body }, executeOptions) } public jsonPatch( path: string, patches: JSONPatch, executeOptions?: QueryExecuteOptions ) { return this.fetch( path, { method: 'PATCH', body: patches as any, headers: { 'Content-Type': JSON_PATCH_CONTENT_TYPE }, }, executeOptions ) } public delete(path: string, executeOptions?: QueryExecuteOptions) { return this.fetch(path, { method: 'DELETE' }, executeOptions) } } export default DataEngine