diff --git a/engine/.eslintrc.js b/engine/.eslintrc.js new file mode 100644 index 000000000..89b605713 --- /dev/null +++ b/engine/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: [ + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + ], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'import/no-unresolved': 'off', + }, +} diff --git a/engine/.gitignore b/engine/.gitignore new file mode 100644 index 000000000..6570aa5cb --- /dev/null +++ b/engine/.gitignore @@ -0,0 +1,5 @@ +# DHIS2 Platform +node_modules +.d2 +src/locales +build \ No newline at end of file diff --git a/engine/d2.config.js b/engine/d2.config.js new file mode 100644 index 000000000..84bec20f1 --- /dev/null +++ b/engine/d2.config.js @@ -0,0 +1,9 @@ +const config = { + type: 'lib', + + entryPoints: { + lib: './src/index.ts', + }, +} + +module.exports = config diff --git a/engine/package.json b/engine/package.json new file mode 100644 index 000000000..ac9795184 --- /dev/null +++ b/engine/package.json @@ -0,0 +1,36 @@ +{ + "name": "@dhis2/data-engine", + "version": "3.14.6", + "description": "A standalone data query engine for DHIS2 REST API", + "main": "./build/cjs/index.js", + "module": "./build/es/index.js", + "types": "./build/types/index.d.ts", + "exports": { + "import": "./build/es/index.js", + "require": "./build/cjs/index.js", + "types": "./build/types/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/dhis2/app-runtime.git", + "directory": "engine" + }, + "author": "Austin McGee ", + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "files": [ + "build/**" + ], + "scripts": { + "build:types": "tsc --emitDeclarationOnly --outDir ./build/types", + "build:package": "d2-app-scripts build", + "build": "concurrently -n build,types \"yarn build:package\" \"yarn build:types\"", + "watch": "NODE_ENV=development concurrently -n build,types \"yarn build:package --watch\" \"yarn build:types --watch\"", + "type-check": "tsc --noEmit --allowJs --checkJs", + "type-check:watch": "yarn type-check --watch", + "test": "d2-app-scripts test", + "coverage": "yarn test --coverage" + } +} diff --git a/services/data/src/engine/DataEngine.test.ts b/engine/src/DataEngine.test.ts similarity index 100% rename from services/data/src/engine/DataEngine.test.ts rename to engine/src/DataEngine.test.ts diff --git a/engine/src/DataEngine.ts b/engine/src/DataEngine.ts new file mode 100644 index 000000000..11496cf62 --- /dev/null +++ b/engine/src/DataEngine.ts @@ -0,0 +1,181 @@ +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 diff --git a/services/data/src/engine/types/FetchError.test.ts b/engine/src/errors/FetchError.test.ts similarity index 100% rename from services/data/src/engine/types/FetchError.test.ts rename to engine/src/errors/FetchError.test.ts diff --git a/services/data/src/engine/types/FetchError.ts b/engine/src/errors/FetchError.ts similarity index 100% rename from services/data/src/engine/types/FetchError.ts rename to engine/src/errors/FetchError.ts diff --git a/services/data/src/engine/types/InvalidQueryError.ts b/engine/src/errors/InvalidQueryError.ts similarity index 100% rename from services/data/src/engine/types/InvalidQueryError.ts rename to engine/src/errors/InvalidQueryError.ts diff --git a/engine/src/errors/index.ts b/engine/src/errors/index.ts new file mode 100644 index 000000000..c3d94a4fe --- /dev/null +++ b/engine/src/errors/index.ts @@ -0,0 +1,2 @@ +export { FetchError } from './FetchError' +export { InvalidQueryError } from './InvalidQueryError' diff --git a/services/data/src/engine/helpers/getMutationFetchType.test.ts b/engine/src/helpers/getMutationFetchType.test.ts similarity index 97% rename from services/data/src/engine/helpers/getMutationFetchType.test.ts rename to engine/src/helpers/getMutationFetchType.test.ts index 227982244..af1a49072 100644 --- a/services/data/src/engine/helpers/getMutationFetchType.test.ts +++ b/engine/src/helpers/getMutationFetchType.test.ts @@ -10,6 +10,7 @@ describe('getMutationFetchType', () => { ).toBe('delete') expect( getMutationFetchType({ + id: '123', type: 'json-patch', resource: 'test', data: {}, diff --git a/services/data/src/engine/helpers/getMutationFetchType.ts b/engine/src/helpers/getMutationFetchType.ts similarity index 54% rename from services/data/src/engine/helpers/getMutationFetchType.ts rename to engine/src/helpers/getMutationFetchType.ts index 0845c6742..b1e4d9818 100644 --- a/services/data/src/engine/helpers/getMutationFetchType.ts +++ b/engine/src/helpers/getMutationFetchType.ts @@ -1,9 +1,9 @@ import { FetchType } from '../types/ExecuteOptions' import { Mutation } from '../types/Mutation' -export const getMutationFetchType = (mutation: Mutation): FetchType => - mutation.type === 'update' - ? mutation.partial - ? 'update' - : 'replace' - : mutation.type +export const getMutationFetchType = (mutation: Mutation): FetchType => { + if (mutation.type === 'update') { + return mutation.partial ? 'update' : 'replace' + } + return mutation.type +} diff --git a/services/data/src/engine/helpers/resolveDynamicQuery.test.ts b/engine/src/helpers/resolveDynamicQuery.test.ts similarity index 100% rename from services/data/src/engine/helpers/resolveDynamicQuery.test.ts rename to engine/src/helpers/resolveDynamicQuery.test.ts diff --git a/services/data/src/engine/helpers/resolveDynamicQuery.ts b/engine/src/helpers/resolveDynamicQuery.ts similarity index 100% rename from services/data/src/engine/helpers/resolveDynamicQuery.ts rename to engine/src/helpers/resolveDynamicQuery.ts diff --git a/services/data/src/engine/helpers/validate.test.ts b/engine/src/helpers/validate.test.ts similarity index 100% rename from services/data/src/engine/helpers/validate.test.ts rename to engine/src/helpers/validate.test.ts diff --git a/services/data/src/engine/helpers/validate.ts b/engine/src/helpers/validate.ts similarity index 87% rename from services/data/src/engine/helpers/validate.ts rename to engine/src/helpers/validate.ts index 4630d3529..d419f9886 100644 --- a/services/data/src/engine/helpers/validate.ts +++ b/engine/src/helpers/validate.ts @@ -1,21 +1,21 @@ -import { InvalidQueryError } from '../types/InvalidQueryError' +import { InvalidQueryError } from '../errors/InvalidQueryError' import { ResolvedResourceQuery } from '../types/Query' -const validQueryKeys = ['resource', 'id', 'params', 'data'] -const validTypes = [ +const validQueryKeys = new Set(['resource', 'id', 'params', 'data']) +const validTypes = new Set([ 'read', 'create', 'update', 'replace', 'delete', 'json-patch', -] +]) export const getResourceQueryErrors = ( type: string, query: ResolvedResourceQuery ): string[] => { - if (!validTypes.includes(type)) { + if (!validTypes.has(type)) { return [`Unknown query or mutation type ${type}`] } if (typeof query !== 'object') { @@ -47,12 +47,10 @@ export const getResourceQueryErrors = ( "Mutation type 'json-patch' requires property 'data' to be of type Array" ) } - const invalidKeys = Object.keys(query).filter( - (k) => !validQueryKeys.includes(k) - ) - invalidKeys.forEach((k) => { + const invalidKeys = Object.keys(query).filter((k) => !validQueryKeys.has(k)) + for (const k of invalidKeys) { errors.push(`Property ${k} is not supported`) - }) + } return errors } diff --git a/engine/src/index.ts b/engine/src/index.ts new file mode 100644 index 000000000..e82eeff3b --- /dev/null +++ b/engine/src/index.ts @@ -0,0 +1,5 @@ +export * from './DataEngine' +export * from './links' +export * from './errors' + +export * from './types' diff --git a/services/data/src/links/CustomDataLink.test.ts b/engine/src/links/CustomDataLink.test.ts similarity index 100% rename from services/data/src/links/CustomDataLink.test.ts rename to engine/src/links/CustomDataLink.test.ts diff --git a/services/data/src/links/CustomDataLink.ts b/engine/src/links/CustomDataLink.ts similarity index 81% rename from services/data/src/links/CustomDataLink.ts rename to engine/src/links/CustomDataLink.ts index 79465767b..1e6fdccbc 100644 --- a/services/data/src/links/CustomDataLink.ts +++ b/engine/src/links/CustomDataLink.ts @@ -1,10 +1,10 @@ -import { +import type { DataEngineLink, DataEngineLinkExecuteOptions, - FetchType, - JsonValue, - ResolvedResourceQuery, -} from '../engine' +} from '../types/DataEngineLink' +import type { FetchType } from '../types/ExecuteOptions' +import type { JsonValue } from '../types/JsonValue' +import type { ResolvedResourceQuery } from '../types/Query' export type CustomResourceFactory = ( type: FetchType, @@ -21,9 +21,9 @@ export interface CustomLinkOptions { } export class CustomDataLink implements DataEngineLink { - private failOnMiss: boolean - private loadForever: boolean - private data: CustomData + private readonly failOnMiss: boolean + private readonly loadForever: boolean + private readonly data: CustomData public constructor( customData: CustomData, { failOnMiss = true, loadForever = false }: CustomLinkOptions = {} @@ -49,7 +49,7 @@ export class CustomDataLink implements DataEngineLink { `No data provided for resource type ${query.resource}!` ) } - return Promise.resolve(null) + return null } switch (typeof customResource) { @@ -60,7 +60,7 @@ export class CustomDataLink implements DataEngineLink { return customResource case 'function': { const result = await customResource(type, query, options) - if (typeof result === 'undefined' && this.failOnMiss) { + if (result === undefined && this.failOnMiss) { throw new Error( `The custom function for resource ${query.resource} must always return a value but returned ${result}` ) diff --git a/services/data/src/links/ErrorLink.ts b/engine/src/links/ErrorLink.ts similarity index 58% rename from services/data/src/links/ErrorLink.ts rename to engine/src/links/ErrorLink.ts index 8fcaa7a70..f4fad6995 100644 --- a/services/data/src/links/ErrorLink.ts +++ b/engine/src/links/ErrorLink.ts @@ -1,12 +1,12 @@ -import { DataEngineLink } from '../engine' +import type { DataEngineLink } from '../types/DataEngineLink' export class ErrorLink implements DataEngineLink { - private errorMessage: string + private readonly errorMessage: string public constructor(errorMessage: string) { this.errorMessage = errorMessage } public executeResourceQuery() { console.error(this.errorMessage) - return Promise.reject(this.errorMessage) + return Promise.reject(new Error(this.errorMessage)) } } diff --git a/services/data/src/links/RestAPILink.test.ts b/engine/src/links/RestAPILink.test.ts similarity index 100% rename from services/data/src/links/RestAPILink.test.ts rename to engine/src/links/RestAPILink.test.ts diff --git a/services/data/src/links/RestAPILink.ts b/engine/src/links/RestAPILink.ts similarity index 74% rename from services/data/src/links/RestAPILink.ts rename to engine/src/links/RestAPILink.ts index 35a5c4f7f..f759853f8 100644 --- a/services/data/src/links/RestAPILink.ts +++ b/engine/src/links/RestAPILink.ts @@ -1,22 +1,22 @@ -import type { Config } from '@dhis2/app-service-config' -import { +import type { DataEngineConfig } from '../types/DataEngineConfig' +import type { DataEngineLink, DataEngineLinkExecuteOptions, - FetchType, - JsonValue, - ResolvedResourceQuery, -} from '../engine/' +} from '../types/DataEngineLink' +import type { FetchType } from '../types/ExecuteOptions' +import type { JsonValue } from '../types/JsonValue' +import type { ResolvedResourceQuery } from '../types/Query' import { fetchData } from './RestAPILink/fetchData' import { joinPath } from './RestAPILink/path' import { queryToRequestOptions } from './RestAPILink/queryToRequestOptions' import { queryToResourcePath } from './RestAPILink/queryToResourcePath' export class RestAPILink implements DataEngineLink { - public readonly config: Config + public readonly config: DataEngineConfig public readonly versionedApiPath: string public readonly unversionedApiPath: string - public constructor(config: Config) { + public constructor(config: DataEngineConfig) { this.config = config this.versionedApiPath = joinPath('api', String(config.apiVersion)) this.unversionedApiPath = joinPath('api') diff --git a/services/data/src/links/RestAPILink/fetchData.test.ts b/engine/src/links/RestAPILink/fetchData.test.ts similarity index 92% rename from services/data/src/links/RestAPILink/fetchData.test.ts rename to engine/src/links/RestAPILink/fetchData.test.ts index 5b47b5dd5..863cdb5f6 100644 --- a/services/data/src/links/RestAPILink/fetchData.test.ts +++ b/engine/src/links/RestAPILink/fetchData.test.ts @@ -1,4 +1,4 @@ -import { FetchError } from '../../engine' +import { FetchError } from '../../errors/FetchError' import { parseStatus, fetchData, parseContentType } from './fetchData' describe('networkFetch', () => { @@ -106,19 +106,24 @@ describe('networkFetch', () => { }) }) + const toContentTypeHeader = (type: string) => { + if (type === 'json') { + return 'application/json' + } + if (type === 'text') { + return 'text/plain' + } + + return 'some/other-content-type' + } describe('fetchData', () => { const headers: Record string> = { - 'Content-Type': (type) => - type === 'json' - ? 'application/json' - : type === 'text' - ? 'text/plain' - : 'some/other-content-type', + 'Content-Type': (type) => toContentTypeHeader(type), } const mockFetch = jest.fn(async (url) => ({ status: 200, headers: { - get: (name: string) => headers[name] && headers[name](url), + get: (name: string) => headers[name]?.(url), }, json: async () => ({ foo: 'bar' }), text: async () => 'foobar', diff --git a/services/data/src/links/RestAPILink/fetchData.ts b/engine/src/links/RestAPILink/fetchData.ts similarity index 93% rename from services/data/src/links/RestAPILink/fetchData.ts rename to engine/src/links/RestAPILink/fetchData.ts index 6ac7bdbbc..443e63d1a 100644 --- a/services/data/src/links/RestAPILink/fetchData.ts +++ b/engine/src/links/RestAPILink/fetchData.ts @@ -1,4 +1,6 @@ -import { FetchError, FetchErrorDetails, JsonValue } from '../../engine' +import { FetchError } from '../../errors/FetchError' +import type { FetchErrorDetails } from '../../errors/FetchError' +import type { JsonValue } from '../../types/JsonValue' export const parseContentType = (contentType: string | null) => contentType ? contentType.split(';')[0].trim().toLowerCase() : '' diff --git a/services/data/src/links/RestAPILink/metadataResources.ts b/engine/src/links/RestAPILink/metadataResources.ts similarity index 100% rename from services/data/src/links/RestAPILink/metadataResources.ts rename to engine/src/links/RestAPILink/metadataResources.ts diff --git a/services/data/src/links/RestAPILink/path.test.ts b/engine/src/links/RestAPILink/path.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/path.test.ts rename to engine/src/links/RestAPILink/path.test.ts diff --git a/engine/src/links/RestAPILink/path.ts b/engine/src/links/RestAPILink/path.ts new file mode 100644 index 000000000..87f129e08 --- /dev/null +++ b/engine/src/links/RestAPILink/path.ts @@ -0,0 +1,6 @@ +export const joinPath = (...parts: (string | undefined | null)[]): string => { + const realParts = parts.filter((part) => !!part) as string[] + return realParts + .map((part) => part.replaceAll(/(^\/+)|(\/+$)/g, '')) // Replace leading and trailing slashes for each part + .join('/') +} diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions.test.ts b/engine/src/links/RestAPILink/queryToRequestOptions.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/queryToRequestOptions.test.ts rename to engine/src/links/RestAPILink/queryToRequestOptions.test.ts diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions.ts b/engine/src/links/RestAPILink/queryToRequestOptions.ts similarity index 52% rename from services/data/src/links/RestAPILink/queryToRequestOptions.ts rename to engine/src/links/RestAPILink/queryToRequestOptions.ts index c20ca3796..2a5b72a65 100644 --- a/services/data/src/links/RestAPILink/queryToRequestOptions.ts +++ b/engine/src/links/RestAPILink/queryToRequestOptions.ts @@ -1,4 +1,6 @@ -import { ResolvedResourceQuery, FetchType } from '../../engine' +import type { FetchType } from '../../types/ExecuteOptions' +import { JSON_PATCH_CONTENT_TYPE } from '../../types/JSONPatch' +import type { ResolvedResourceQuery } from '../../types/Query' import { requestContentType, requestBodyForContentType, @@ -23,6 +25,27 @@ const getMethod = (type: FetchType): string => { } } +export const requestOptionsToFetchType = (init: RequestInit): FetchType => { + const method = init.method ?? 'GET' + const headers = Object.fromEntries(new Headers(init.headers).entries()) + const contentType = headers['content-type'] + switch (method) { + case 'GET': + return 'read' + case 'POST': + return 'create' + case 'PATCH': + if (contentType === JSON_PATCH_CONTENT_TYPE) { + return 'json-patch' + } + return 'update' + case 'DELETE': + return 'delete' + default: + throw new Error(`Unsupported request method ${method}`) + } +} + export const queryToRequestOptions = ( type: FetchType, query: ResolvedResourceQuery, diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.ts b/engine/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.ts diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts b/engine/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts similarity index 95% rename from services/data/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts index 40ae0419e..025b4fcc2 100644 --- a/services/data/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts +++ b/engine/src/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.ts @@ -1,4 +1,4 @@ -import { ResolvedResourceQuery, FetchType } from '../../../engine' +import type { ResolvedResourceQuery, FetchType } from '../../../types' /* * Requests that expect a "multipart/form-data" Content-Type have been collected by scanning diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/requestContentType.test.ts b/engine/src/links/RestAPILink/queryToRequestOptions/requestContentType.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/queryToRequestOptions/requestContentType.test.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/requestContentType.test.ts diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts b/engine/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts similarity index 91% rename from services/data/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts index ba0af9aaf..14332b26e 100644 --- a/services/data/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts +++ b/engine/src/links/RestAPILink/queryToRequestOptions/requestContentType.ts @@ -1,11 +1,12 @@ -import { ResolvedResourceQuery, FetchType } from '../../../engine' +import type { ResolvedResourceQuery, FetchType } from '../../../types' +import { JSON_PATCH_CONTENT_TYPE } from '../../../types/JSONPatch' import * as multipartFormDataMatchers from './multipartFormDataMatchers' import * as textPlainMatchers from './textPlainMatchers' import * as xWwwFormUrlencodedMatchers from './xWwwFormUrlencodedMatchers' type RequestContentType = | 'application/json' - | 'application/json-patch+json' + | typeof JSON_PATCH_CONTENT_TYPE | 'text/plain' | 'multipart/form-data' | 'application/x-www-form-urlencoded' @@ -62,7 +63,7 @@ export const requestContentType = ( } if (type === 'json-patch') { - return 'application/json-patch+json' + return JSON_PATCH_CONTENT_TYPE } if (resourceExpectsTextPlain(type, query)) { @@ -101,13 +102,13 @@ export const requestBodyForContentType = ( contentType: RequestContentType, { data }: ResolvedResourceQuery ): undefined | string | FormData | URLSearchParams => { - if (typeof data === 'undefined') { + if (data === undefined) { return undefined } if ( contentType === 'application/json' || - contentType === 'application/json-patch+json' + contentType === JSON_PATCH_CONTENT_TYPE ) { return JSON.stringify(data) } diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.test.ts b/engine/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.test.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.test.ts diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts b/engine/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts similarity index 97% rename from services/data/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts index 6a5171187..6ed00164e 100644 --- a/services/data/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts +++ b/engine/src/links/RestAPILink/queryToRequestOptions/textPlainMatchers.ts @@ -1,4 +1,4 @@ -import { ResolvedResourceQuery, FetchType } from '../../../engine' +import type { ResolvedResourceQuery, FetchType } from '../../../types' /* * Requests that expect a "text/plain" Content-Type have been collected by scanning @@ -112,7 +112,7 @@ export const addOrUpdateConfigurationProperty = ( ): boolean => { // NOTE: The corsWhitelist property does expect "application/json" const pattern = /^(configuration)\/([a-zA-Z]{1,50})$/ - const match = resource.match(pattern) + const match = pattern.exec(resource) return type === 'create' && !!match && match[2] !== 'corsWhitelist' } diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.ts b/engine/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.ts similarity index 100% rename from services/data/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.ts diff --git a/services/data/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts b/engine/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts similarity index 75% rename from services/data/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts rename to engine/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts index 86c8de148..defa9652f 100644 --- a/services/data/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts +++ b/engine/src/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.ts @@ -1,4 +1,4 @@ -import { ResolvedResourceQuery, FetchType } from '../../../engine' +import type { ResolvedResourceQuery, FetchType } from '../../../types' // POST to convert an SVG file export const isSvgConversion = ( diff --git a/services/data/src/links/RestAPILink/queryToResourcePath.test.ts b/engine/src/links/RestAPILink/queryToResourcePath.test.ts similarity index 94% rename from services/data/src/links/RestAPILink/queryToResourcePath.test.ts rename to engine/src/links/RestAPILink/queryToResourcePath.test.ts index 9d7e0dbcd..6e8d5e2ec 100644 --- a/services/data/src/links/RestAPILink/queryToResourcePath.test.ts +++ b/engine/src/links/RestAPILink/queryToResourcePath.test.ts @@ -1,16 +1,17 @@ -import { Config } from '@dhis2/app-service-config' -import { ResolvedResourceQuery } from '../../engine' +import type { DataEngineConfig } from '../../types/DataEngineConfig' +import type { ResolvedResourceQuery } from '../../types/Query' import { RestAPILink } from '../RestAPILink' import { queryToResourcePath } from './queryToResourcePath' -const createLink = (config) => new RestAPILink(config) -const defaultConfig: Config = { - basePath: '', - apiVersion: '37', +const createLink = (config: DataEngineConfig) => new RestAPILink(config) +const defaultConfig: DataEngineConfig = { + baseUrl: 'http://localhost:8080', + apiVersion: 37, serverVersion: { major: 2, minor: 37, patch: 11, + full: '2.37.11', }, } const link = createLink(defaultConfig) @@ -190,12 +191,13 @@ describe('queryToResourcePath', () => { const query: ResolvedResourceQuery = { resource: 'tracker', } - const v38config: Config = { + const v38config: DataEngineConfig = { ...defaultConfig, serverVersion: { major: 2, minor: 38, patch: 0, + full: '2.38.0', }, } expect(queryToResourcePath(createLink(v38config), query, 'read')).toBe( diff --git a/services/data/src/links/RestAPILink/queryToResourcePath.ts b/engine/src/links/RestAPILink/queryToResourcePath.ts similarity index 87% rename from services/data/src/links/RestAPILink/queryToResourcePath.ts rename to engine/src/links/RestAPILink/queryToResourcePath.ts index 2fdc48db9..ce518932f 100644 --- a/services/data/src/links/RestAPILink/queryToResourcePath.ts +++ b/engine/src/links/RestAPILink/queryToResourcePath.ts @@ -1,10 +1,10 @@ -import type { Config } from '@dhis2/app-service-config' -import { - ResolvedResourceQuery, +import type { DataEngineConfig } from '../../types/DataEngineConfig' +import type { FetchType } from '../../types/ExecuteOptions' +import type { ResolvedResourceQuery } from '../../types/Query' +import type { QueryParameters, QueryParameterValue, - FetchType, -} from '../../engine' +} from '../../types/QueryParameters' import { RestAPILink } from '../RestAPILink' import { joinPath } from './path' import { validateResourceQuery } from './validateQuery' @@ -68,10 +68,13 @@ const isAction = (resource: string) => resource.startsWith(actionPrefix) const makeActionPath = (resource: string) => joinPath( 'dhis-web-commons', - `${resource.substr(actionPrefix.length)}.action` + `${resource.substring(actionPrefix.length)}.action` ) -const skipApiVersion = (resource: string, config: Config): boolean => { +const skipApiVersion = ( + resource: string, + config: DataEngineConfig +): boolean => { if (resource === 'tracker' || resource.startsWith('tracker/')) { if (!config.serverVersion?.minor || config.serverVersion?.minor < 38) { return true diff --git a/services/data/src/links/RestAPILink/validateQuery.test.ts b/engine/src/links/RestAPILink/validateQuery.test.ts similarity index 93% rename from services/data/src/links/RestAPILink/validateQuery.test.ts rename to engine/src/links/RestAPILink/validateQuery.test.ts index 085227935..cb1f00d18 100644 --- a/services/data/src/links/RestAPILink/validateQuery.test.ts +++ b/engine/src/links/RestAPILink/validateQuery.test.ts @@ -15,7 +15,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery( { @@ -25,7 +25,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery( { @@ -39,7 +39,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() }) it('Should return true for mutations', () => { @@ -55,15 +55,15 @@ describe('validateQuery', () => { 'create' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery({ resource: 'visualizations' }, 'update') ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect(validateResourceQuery({ resource: 'maps' }, 'delete')).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() }) it('Should skip validation for non-normative and non-metadata resources', () => { @@ -76,7 +76,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery( @@ -84,7 +84,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery( @@ -92,7 +92,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() expect( validateResourceQuery( @@ -100,13 +100,13 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() process.env.NODE_ENV = 'test' }) it('Should return true and not warn in production mode', () => { - console.warn = jest.fn() + const warn = (console.warn = jest.fn()) process.env.NODE_ENV = 'production' expect( @@ -118,7 +118,7 @@ describe('validateQuery', () => { 'read' ) ).toBe(true) - expect(console.warn).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() process.env.NODE_ENV = 'test' }) diff --git a/services/data/src/links/RestAPILink/validateQuery.ts b/engine/src/links/RestAPILink/validateQuery.ts similarity index 91% rename from services/data/src/links/RestAPILink/validateQuery.ts rename to engine/src/links/RestAPILink/validateQuery.ts index 81f9b63e5..9e918f114 100644 --- a/services/data/src/links/RestAPILink/validateQuery.ts +++ b/engine/src/links/RestAPILink/validateQuery.ts @@ -1,4 +1,5 @@ -import { FetchType, ResolvedResourceQuery } from '../../engine' +import type { FetchType } from '../../types/ExecuteOptions' +import type { ResolvedResourceQuery } from '../../types/Query' import { normativeMetadataResources } from './metadataResources' const validatePagination = ( @@ -38,7 +39,7 @@ const validateDeclarativeFields = ( } else if (Array.isArray(query.params.fields)) { fields = query.params.fields.map((field) => String(field).trim()) } - if (fields?.find((field) => field.match(/(^\*$|^:.+)/))) { + if (fields?.find((field) => /(^\*$|^:.+)/.test(field))) { warn( 'Data queries should not use wildcard or dynamic field groups', query.params.fields, diff --git a/services/data/src/links/index.ts b/engine/src/links/index.ts similarity index 100% rename from services/data/src/links/index.ts rename to engine/src/links/index.ts diff --git a/engine/src/types/DataEngineConfig.ts b/engine/src/types/DataEngineConfig.ts new file mode 100644 index 000000000..e60c1a8be --- /dev/null +++ b/engine/src/types/DataEngineConfig.ts @@ -0,0 +1,10 @@ +export interface DataEngineConfig { + baseUrl: string + apiVersion: number + serverVersion?: { + major: number + minor: number + patch?: number + full: string + } +} diff --git a/services/data/src/engine/types/DataEngineLink.ts b/engine/src/types/DataEngineLink.ts similarity index 65% rename from services/data/src/engine/types/DataEngineLink.ts rename to engine/src/types/DataEngineLink.ts index 7055e0148..949ebb3a9 100644 --- a/services/data/src/engine/types/DataEngineLink.ts +++ b/engine/src/types/DataEngineLink.ts @@ -1,6 +1,6 @@ -import { FetchType } from './ExecuteOptions' -import { JsonValue } from './JsonValue' -import { ResolvedResourceQuery } from './Query' +import type { FetchType } from './ExecuteOptions' +import type { JsonValue } from './JsonValue' +import type { ResolvedResourceQuery } from './Query' export interface DataEngineLinkExecuteOptions { signal?: AbortSignal diff --git a/services/data/src/engine/types/ExecuteOptions.ts b/engine/src/types/ExecuteOptions.ts similarity index 74% rename from services/data/src/engine/types/ExecuteOptions.ts rename to engine/src/types/ExecuteOptions.ts index c7ddb37e7..49d6ba812 100644 --- a/services/data/src/engine/types/ExecuteOptions.ts +++ b/engine/src/types/ExecuteOptions.ts @@ -1,5 +1,5 @@ -import { FetchError } from './FetchError' -import { QueryVariables } from './Query' +import type { FetchError } from '../errors/FetchError' +import type { QueryVariables } from './Query' export type FetchType = | 'create' diff --git a/engine/src/types/JSONPatch.ts b/engine/src/types/JSONPatch.ts new file mode 100644 index 000000000..7e2444e41 --- /dev/null +++ b/engine/src/types/JSONPatch.ts @@ -0,0 +1,32 @@ +export const JSON_PATCH_CONTENT_TYPE = 'application/json-patch+json' + +export type JSONPatchOpAdd = { + op: 'add' + path: string + value: any +} + +export type JSONPatchOpRemove = { + op: 'remove' + path: string +} + +export type JSONPatchOpReplace = { + op: 'replace' + path: string + value: any +} + +export type JSONPatchOpRemoveById = { + op: 'remove-by-id' + path: string + id: string +} + +export type JSONPatchOp = + | JSONPatchOpAdd + | JSONPatchOpRemove + | JSONPatchOpReplace + | JSONPatchOpRemoveById + +export type JSONPatch = JSONPatchOp[] diff --git a/services/data/src/engine/types/JsonValue.ts b/engine/src/types/JsonValue.ts similarity index 100% rename from services/data/src/engine/types/JsonValue.ts rename to engine/src/types/JsonValue.ts diff --git a/services/data/src/engine/types/Mutation.ts b/engine/src/types/Mutation.ts similarity index 87% rename from services/data/src/engine/types/Mutation.ts rename to engine/src/types/Mutation.ts index 16de02332..2678be877 100644 --- a/services/data/src/engine/types/Mutation.ts +++ b/engine/src/types/Mutation.ts @@ -1,5 +1,5 @@ -import { FetchError } from './FetchError' -import { ResourceQuery, QueryVariables } from './Query' +import type { FetchError } from '../errors/FetchError' +import type { ResourceQuery, QueryVariables } from './Query' export type MutationType = | 'create' diff --git a/services/data/src/engine/types/PossiblyDynamic.ts b/engine/src/types/PossiblyDynamic.ts similarity index 100% rename from services/data/src/engine/types/PossiblyDynamic.ts rename to engine/src/types/PossiblyDynamic.ts diff --git a/services/data/src/engine/types/Query.ts b/engine/src/types/Query.ts similarity index 76% rename from services/data/src/engine/types/Query.ts rename to engine/src/types/Query.ts index e058b53c6..f59e5d07a 100644 --- a/services/data/src/engine/types/Query.ts +++ b/engine/src/types/Query.ts @@ -1,7 +1,7 @@ -import { FetchError } from './FetchError' -import { JsonMap } from './JsonValue' -import { PossiblyDynamic } from './PossiblyDynamic' -import { QueryParameters } from './QueryParameters' +import type { FetchError } from '../errors/FetchError' +import type { JsonMap } from './JsonValue' +import type { PossiblyDynamic } from './PossiblyDynamic' +import type { QueryParameters } from './QueryParameters' export type QueryVariables = Record diff --git a/services/data/src/engine/types/QueryParameters.ts b/engine/src/types/QueryParameters.ts similarity index 100% rename from services/data/src/engine/types/QueryParameters.ts rename to engine/src/types/QueryParameters.ts diff --git a/engine/src/types/index.ts b/engine/src/types/index.ts new file mode 100644 index 000000000..36826bbf0 --- /dev/null +++ b/engine/src/types/index.ts @@ -0,0 +1,8 @@ +export * from './DataEngineConfig' +export * from './DataEngineLink' +export * from './ExecuteOptions' +export * from './JsonValue' +export * from './Mutation' +export * from './PossiblyDynamic' +export * from './Query' +export * from './QueryParameters' diff --git a/engine/tsconfig.json b/engine/tsconfig.json new file mode 100644 index 000000000..a719f9bc3 --- /dev/null +++ b/engine/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "files": ["src/index.ts"], + "include": ["src/**/*"], + "exclude": [] +} diff --git a/examples/cra/yarn.lock b/examples/cra/yarn.lock index 228f96e46..4d4664d8e 100644 --- a/examples/cra/yarn.lock +++ b/examples/cra/yarn.lock @@ -1047,34 +1047,42 @@ integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== "@dhis2/app-runtime@file:../../runtime": - version "3.14.2" + version "3.14.6" dependencies: - "@dhis2/app-service-alerts" "3.14.2" - "@dhis2/app-service-config" "3.14.2" - "@dhis2/app-service-data" "3.14.2" - "@dhis2/app-service-offline" "3.14.2" - "@dhis2/app-service-plugin" "3.14.2" + "@dhis2/app-service-alerts" "3.14.6" + "@dhis2/app-service-config" "3.14.6" + "@dhis2/app-service-data" "3.14.6" + "@dhis2/app-service-offline" "3.14.6" + "@dhis2/app-service-plugin" "3.14.6" + prop-types "^15.7.2" -"@dhis2/app-service-alerts@3.14.2", "@dhis2/app-service-alerts@file:../../services/alerts": - version "3.14.2" +"@dhis2/app-service-alerts@3.14.6", "@dhis2/app-service-alerts@file:../../services/alerts": + version "3.14.6" + dependencies: + prop-types "^15.7.2" -"@dhis2/app-service-config@3.14.2", "@dhis2/app-service-config@file:../../services/config": - version "3.14.2" +"@dhis2/app-service-config@3.14.6", "@dhis2/app-service-config@file:../../services/config": + version "3.14.6" + dependencies: + prop-types "^15.7.2" -"@dhis2/app-service-data@3.14.2", "@dhis2/app-service-data@file:../../services/data": - version "3.14.2" +"@dhis2/app-service-data@3.14.6", "@dhis2/app-service-data@file:../../services/data": + version "3.14.6" dependencies: "@tanstack/react-query" "^4.36.1" + prop-types "^15.7.2" -"@dhis2/app-service-offline@3.14.2", "@dhis2/app-service-offline@file:../../services/offline": - version "3.14.2" +"@dhis2/app-service-offline@3.14.6", "@dhis2/app-service-offline@file:../../services/offline": + version "3.14.6" dependencies: lodash "^4.17.21" + prop-types "^15.7.2" -"@dhis2/app-service-plugin@3.14.2", "@dhis2/app-service-plugin@file:../../services/plugin": - version "3.14.2" +"@dhis2/app-service-plugin@3.14.6", "@dhis2/app-service-plugin@file:../../services/plugin": + version "3.14.6" dependencies: post-robot "^10.0.46" + prop-types "^15.7.2" "@hapi/address@2.x.x": version "2.1.4" diff --git a/examples/query-playground/package.json b/examples/query-playground/package.json index 74f861798..b5df95de3 100644 --- a/examples/query-playground/package.json +++ b/examples/query-playground/package.json @@ -30,6 +30,7 @@ "@dhis2/app-service-config": "file:../../services/config", "@dhis2/app-service-data": "file:../../services/data", "@dhis2/app-service-offline": "file:../../services/offline", - "@dhis2/app-service-plugin": "file:../../services/plugin" + "@dhis2/app-service-plugin": "file:../../services/plugin", + "@dhis2/data-engine": "file:../../engine" } } diff --git a/examples/query-playground/src/App.jsx b/examples/query-playground/src/App.jsx index 3ca74f235..9224ea75b 100644 --- a/examples/query-playground/src/App.jsx +++ b/examples/query-playground/src/App.jsx @@ -1,8 +1,18 @@ +import { useConfig } from '@dhis2/app-runtime' +import { DataEngine, RestAPILink } from '@dhis2/data-engine' import { CssVariables } from '@dhis2/ui' -import React from 'react' +import React, { useEffect } from 'react' import { QueryRepl } from './QueryRepl.jsx' export default function App() { + const config = useConfig() + + useEffect(() => { + // Initialize a new global DataEngine (window.engine) which can be used + // to test the programmatic non-react interface using the browser console + window.engine = new DataEngine(new RestAPILink(config)) + }, [config]) + return ( <> diff --git a/examples/query-playground/yarn.lock b/examples/query-playground/yarn.lock index 7ec6a155c..1851469a5 100644 --- a/examples/query-playground/yarn.lock +++ b/examples/query-playground/yarn.lock @@ -1702,34 +1702,42 @@ moment "^2.24.0" "@dhis2/app-runtime@*", "@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@file:../../runtime": - version "3.14.2" + version "3.14.6" dependencies: - "@dhis2/app-service-alerts" "3.14.2" - "@dhis2/app-service-config" "3.14.2" - "@dhis2/app-service-data" "3.14.2" - "@dhis2/app-service-offline" "3.14.2" - "@dhis2/app-service-plugin" "3.14.2" + "@dhis2/app-service-alerts" "3.14.6" + "@dhis2/app-service-config" "3.14.6" + "@dhis2/app-service-data" "3.14.6" + "@dhis2/app-service-offline" "3.14.6" + "@dhis2/app-service-plugin" "3.14.6" + prop-types "^15.7.2" -"@dhis2/app-service-alerts@3.14.2", "@dhis2/app-service-alerts@file:../../services/alerts": - version "3.14.2" +"@dhis2/app-service-alerts@3.14.6", "@dhis2/app-service-alerts@file:../../services/alerts": + version "3.14.6" + dependencies: + prop-types "^15.7.2" -"@dhis2/app-service-config@3.14.2", "@dhis2/app-service-config@file:../../services/config": - version "3.14.2" +"@dhis2/app-service-config@3.14.6", "@dhis2/app-service-config@file:../../services/config": + version "3.14.6" + dependencies: + prop-types "^15.7.2" -"@dhis2/app-service-data@3.14.2", "@dhis2/app-service-data@file:../../services/data": - version "3.14.2" +"@dhis2/app-service-data@3.14.6", "@dhis2/app-service-data@file:../../services/data": + version "3.14.6" dependencies: "@tanstack/react-query" "^4.36.1" + prop-types "^15.7.2" -"@dhis2/app-service-offline@3.14.2", "@dhis2/app-service-offline@file:../../services/offline": - version "3.14.2" +"@dhis2/app-service-offline@3.14.6", "@dhis2/app-service-offline@file:../../services/offline": + version "3.14.6" dependencies: lodash "^4.17.21" + prop-types "^15.7.2" -"@dhis2/app-service-plugin@3.14.2", "@dhis2/app-service-plugin@file:../../services/plugin": - version "3.14.2" +"@dhis2/app-service-plugin@3.14.6", "@dhis2/app-service-plugin@file:../../services/plugin": + version "3.14.6" dependencies: post-robot "^10.0.46" + prop-types "^15.7.2" "@dhis2/app-shell@12.0.0": version "12.0.0" @@ -1835,6 +1843,9 @@ i18next "^10.3" moment "^2.24.0" +"@dhis2/data-engine@file:../../engine": + version "3.14.6" + "@dhis2/multi-calendar-dates@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-2.0.0.tgz#febf04f873670960804d38c9ebaa1cadf8050db3" @@ -2632,13 +2643,20 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== dependencies: "@babel/types" "^7.20.7" +"@types/babel__traverse@^7.0.4": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" diff --git a/package.json b/package.json index 3ea5f638b..5963f7dd9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "private": true, "workspaces": [ "runtime", + "engine", "services/*" ], "devDependencies": { @@ -41,10 +42,12 @@ "build:config": "cd services/config && yarn build", "build:services": "yarn build:config && loop \"yarn build\" --cwd ./services --exclude config --exit-on-error", "build:runtime": "cd runtime && yarn build", - "build": "yarn build:services && yarn build:runtime && yarn install:examples", + "build:engine": "cd engine && yarn build", + "build": "yarn build:engine && yarn build:services && yarn build:runtime && yarn install:examples", "test:services": "loop \"yarn test\" --cwd ./services --exit-on-error", "test:runtime": "cd runtime && yarn test", - "test": "yarn test:services && yarn test:runtime", + "test:engine": "cd engine && yarn test", + "test": "yarn test:engine && yarn test:services && yarn test:runtime", "format": "d2-style apply js --all --no-stage && d2-style apply text --all --no-stage", "start": "yarn build && cd examples/query-playground && yarn start", "docs:build": "mkdir -p dist && cp docs/index.html dist/ && yarn build:playground", diff --git a/runtime/src/Provider.tsx b/runtime/src/Provider.tsx index 312af148c..a10da31cb 100644 --- a/runtime/src/Provider.tsx +++ b/runtime/src/Provider.tsx @@ -1,6 +1,6 @@ import { AlertsProvider } from '@dhis2/app-service-alerts' import { ConfigProvider } from '@dhis2/app-service-config' -import { Config } from '@dhis2/app-service-config/build/types/types' +import type { Config } from '@dhis2/app-service-config' import { DataProvider } from '@dhis2/app-service-data' import { OfflineProvider } from '@dhis2/app-service-offline' import React from 'react' diff --git a/services/config/package.json b/services/config/package.json index 4e6a77699..586b56d26 100644 --- a/services/config/package.json +++ b/services/config/package.json @@ -3,7 +3,7 @@ "version": "3.14.6", "main": "./build/cjs/index.js", "module": "./build/es/index.js", - "types": "build/types/index.d.ts", + "types": "./build/types/index.d.ts", "exports": { "import": "./build/es/index.js", "require": "./build/cjs/index.js", diff --git a/services/data/package.json b/services/data/package.json index 94c33bc67..0f5d0cb4b 100644 --- a/services/data/package.json +++ b/services/data/package.json @@ -3,7 +3,7 @@ "version": "3.14.6", "main": "./build/cjs/index.js", "module": "./build/es/index.js", - "types": "build/types/index.d.ts", + "types": "./build/types/index.d.ts", "exports": { "import": "./build/es/index.js", "require": "./build/cjs/index.js", @@ -38,6 +38,7 @@ }, "peerDependencies": { "@dhis2/app-service-config": "3.14.6", + "@dhis2/data-engine": "3.14.6", "react": "^16.8.6 || ^18", "react-dom": "^16.8.6 || ^18" } diff --git a/services/data/src/engine/DataEngine.ts b/services/data/src/engine/DataEngine.ts deleted file mode 100644 index f50535d89..000000000 --- a/services/data/src/engine/DataEngine.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { getMutationFetchType } from './helpers/getMutationFetchType' -import { resolveDynamicQuery } from './helpers/resolveDynamicQuery' -import { - validateResourceQuery, - validateResourceQueries, -} from './helpers/validate' -import { DataEngineLink } from './types/DataEngineLink' -import { QueryExecuteOptions } from './types/ExecuteOptions' -import { JsonMap, JsonValue } from './types/JsonValue' -import { 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 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 && onComplete(data) - return data as TResult | Record - }) - .catch((error) => { - onError && 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 && onComplete(data) - return data - }) - .catch((error) => { - onError && onError(error) - throw error - }) - } -} - -export default DataEngine diff --git a/services/data/src/engine/index.ts b/services/data/src/engine/index.ts deleted file mode 100644 index 790c2b74c..000000000 --- a/services/data/src/engine/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './DataEngine' -export * from './types/DataEngineLink' -export * from './types/ExecuteOptions' -export * from './types/FetchError' -export * from './types/JsonValue' -export * from './types/Mutation' -export * from './types/PossiblyDynamic' -export * from './types/Query' -export * from './types/QueryParameters' diff --git a/services/data/src/index.ts b/services/data/src/index.ts index 87d18c445..5bba96092 100644 --- a/services/data/src/index.ts +++ b/services/data/src/index.ts @@ -1,12 +1,5 @@ -export { - DataQuery, - DataMutation, - DataProvider, - CustomDataProvider, -} from './react' +export * from './react' -export { useDataEngine, useDataQuery, useDataMutation } from './react' - -export { FetchError } from './engine' +export { FetchError } from '@dhis2/data-engine' export type * from './types' diff --git a/services/data/src/links/RestAPILink/path.ts b/services/data/src/links/RestAPILink/path.ts deleted file mode 100644 index 8143741dc..000000000 --- a/services/data/src/links/RestAPILink/path.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const joinPath = (...parts: (string | undefined | null)[]): string => { - const realParts = parts.filter((part) => !!part) as string[] - return realParts.map((part) => part.replace(/^\/+|\/+$/g, '')).join('/') -} diff --git a/services/data/src/react/components/CustomDataProvider.tsx b/services/data/src/react/components/CustomDataProvider.tsx index de8b08f4c..a4e1f2607 100644 --- a/services/data/src/react/components/CustomDataProvider.tsx +++ b/services/data/src/react/components/CustomDataProvider.tsx @@ -1,7 +1,7 @@ +import { DataEngine, CustomDataLink } from '@dhis2/data-engine' +import type { CustomData, CustomLinkOptions } from '@dhis2/data-engine' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React from 'react' -import { DataEngine } from '../../engine' -import { CustomDataLink, CustomData, CustomLinkOptions } from '../../links' import { DataContext } from '../context/DataContext' import { queryClientOptions as queryClientDefaults } from './DataProvider' diff --git a/services/data/src/react/components/DataMutation.tsx b/services/data/src/react/components/DataMutation.tsx index fd7c3b48a..0105fcfc3 100644 --- a/services/data/src/react/components/DataMutation.tsx +++ b/services/data/src/react/components/DataMutation.tsx @@ -1,4 +1,4 @@ -import { Mutation, MutationOptions } from '../../engine' +import type { Mutation, MutationOptions } from '@dhis2/data-engine' import { MutationRenderInput } from '../../types' import { useDataMutation } from '../hooks/useDataMutation' diff --git a/services/data/src/react/components/DataProvider.test.tsx b/services/data/src/react/components/DataProvider.test.tsx index e16d6f20a..f60c0d3a5 100644 --- a/services/data/src/react/components/DataProvider.test.tsx +++ b/services/data/src/react/components/DataProvider.test.tsx @@ -1,7 +1,6 @@ +import { DataEngine, RestAPILink } from '@dhis2/data-engine' import { render } from '@testing-library/react' import React from 'react' -import { DataEngine } from '../../engine' -import { RestAPILink } from '../../links' import { DataContext } from '../context/DataContext' import { DataProvider } from './DataProvider' diff --git a/services/data/src/react/components/DataProvider.tsx b/services/data/src/react/components/DataProvider.tsx index 1ce7ac092..c44cd6a1b 100644 --- a/services/data/src/react/components/DataProvider.tsx +++ b/services/data/src/react/components/DataProvider.tsx @@ -1,14 +1,13 @@ /* eslint-disable react/no-unused-prop-types */ import { useConfig } from '@dhis2/app-service-config' +import { DataEngine, RestAPILink } from '@dhis2/data-engine' import { QueryClient, QueryClientProvider, type QueryClientConfig, } from '@tanstack/react-query' import React from 'react' -import { DataEngine } from '../../engine' -import { RestAPILink } from '../../links' import { DataContext } from '../context/DataContext' export interface ProviderInput { diff --git a/services/data/src/react/components/DataQuery.tsx b/services/data/src/react/components/DataQuery.tsx index e9cb089dc..4448c9f96 100644 --- a/services/data/src/react/components/DataQuery.tsx +++ b/services/data/src/react/components/DataQuery.tsx @@ -1,4 +1,4 @@ -import { Query, QueryOptions } from '../../engine' +import type { Query, QueryOptions } from '@dhis2/data-engine' import { QueryRenderInput } from '../../types' import { useDataQuery } from '../hooks/useDataQuery' diff --git a/services/data/src/react/components/index.ts b/services/data/src/react/components/index.ts new file mode 100644 index 000000000..f309ce350 --- /dev/null +++ b/services/data/src/react/components/index.ts @@ -0,0 +1,4 @@ +export { CustomDataProvider } from './CustomDataProvider' +export { DataMutation } from './DataMutation' +export { DataProvider } from './DataProvider' +export { DataQuery } from './DataQuery' diff --git a/services/data/src/react/context/DataContext.tsx b/services/data/src/react/context/DataContext.tsx index 7a4102da2..41f0e938f 100644 --- a/services/data/src/react/context/DataContext.tsx +++ b/services/data/src/react/context/DataContext.tsx @@ -1,5 +1,5 @@ import React from 'react' import { ContextType } from '../../types' -import { defaultContext } from './defaultContext' +import { defaultDataContext } from './defaultDataContext' -export const DataContext = React.createContext(defaultContext) +export const DataContext = React.createContext(defaultDataContext) diff --git a/services/data/src/react/context/defaultContext.test.ts b/services/data/src/react/context/defaultDataContext.test.ts similarity index 90% rename from services/data/src/react/context/defaultContext.test.ts rename to services/data/src/react/context/defaultDataContext.test.ts index 6d54f71ca..9acc659a5 100644 --- a/services/data/src/react/context/defaultContext.test.ts +++ b/services/data/src/react/context/defaultDataContext.test.ts @@ -1,4 +1,4 @@ -import { defaultContext } from './defaultContext' +import { defaultDataContext } from './defaultDataContext' describe('defaultContext', () => { const originalError = console.error @@ -10,7 +10,7 @@ describe('defaultContext', () => { afterEach(() => (console.error = originalError)) it('Should throw if query is called', () => { - const context = defaultContext + const context = defaultDataContext expect( context.engine.query({ test: { @@ -28,7 +28,7 @@ describe('defaultContext', () => { }) it('Should throw and log if mutate is called', () => { - const context = defaultContext + const context = defaultDataContext expect( context.engine.mutate({ type: 'create', diff --git a/services/data/src/react/context/defaultContext.ts b/services/data/src/react/context/defaultDataContext.ts similarity index 63% rename from services/data/src/react/context/defaultContext.ts rename to services/data/src/react/context/defaultDataContext.ts index 8b794ed3b..81f9ecddd 100644 --- a/services/data/src/react/context/defaultContext.ts +++ b/services/data/src/react/context/defaultDataContext.ts @@ -1,5 +1,4 @@ -import { DataEngine } from '../../engine' -import { ErrorLink } from '../../links' +import { DataEngine, ErrorLink } from '@dhis2/data-engine' const errorMessage = 'DHIS2 data context must be initialized, please ensure that you include a in your application' @@ -7,4 +6,4 @@ const errorMessage = const link = new ErrorLink(errorMessage) const engine = new DataEngine(link) -export const defaultContext = { engine } +export const defaultDataContext = { engine } diff --git a/services/data/src/react/hooks/index.ts b/services/data/src/react/hooks/index.ts new file mode 100644 index 000000000..d8ac6d3fd --- /dev/null +++ b/services/data/src/react/hooks/index.ts @@ -0,0 +1,3 @@ +export { useDataEngine } from './useDataEngine' +export { useDataMutation } from './useDataMutation' +export { useDataQuery } from './useDataQuery' diff --git a/services/data/src/react/hooks/mergeAndCompareVariables.ts b/services/data/src/react/hooks/mergeAndCompareVariables.ts index cbec87ab0..baea0d367 100644 --- a/services/data/src/react/hooks/mergeAndCompareVariables.ts +++ b/services/data/src/react/hooks/mergeAndCompareVariables.ts @@ -1,4 +1,4 @@ -import type { QueryVariables } from '../../engine' +import type { QueryVariables } from '@dhis2/data-engine' import { stableVariablesHash } from './stableVariablesHash' export const mergeAndCompareVariables = ( diff --git a/services/data/src/react/hooks/stableVariablesHash.test.ts b/services/data/src/react/hooks/stableVariablesHash.test.ts index b396fda8a..c5332b60e 100644 --- a/services/data/src/react/hooks/stableVariablesHash.test.ts +++ b/services/data/src/react/hooks/stableVariablesHash.test.ts @@ -39,7 +39,7 @@ describe('stableVariablesHash', () => { }) it('throws a clear error when the variables contain a circular reference', () => { - const unserializable = { + const unserializable: any = { value: 'value', } unserializable.circular = unserializable diff --git a/services/data/src/react/hooks/useDataMutation.test.tsx b/services/data/src/react/hooks/useDataMutation.test.tsx index 34394cb62..bf2349566 100644 --- a/services/data/src/react/hooks/useDataMutation.test.tsx +++ b/services/data/src/react/hooks/useDataMutation.test.tsx @@ -1,6 +1,6 @@ +import type { CreateMutation, UpdateMutation } from '@dhis2/data-engine' import { renderHook, act, waitFor } from '@testing-library/react' import * as React from 'react' -import { CreateMutation, UpdateMutation } from '../../engine/types/Mutation' import { CustomDataProvider } from '../components/CustomDataProvider' import { useDataEngine } from './useDataEngine' import { useDataMutation } from './useDataMutation' @@ -164,7 +164,7 @@ describe('useDataMutation', () => { id: ({ id }) => id, data: { answer: '?' }, } - const answerSpy = jest.fn(() => 42) + const answerSpy = jest.fn(() => Promise.resolve(42)) const data = { answer: answerSpy } const wrapper = ({ children }) => ( {children} diff --git a/services/data/src/react/hooks/useDataMutation.ts b/services/data/src/react/hooks/useDataMutation.ts index 7e385703a..75d2c9174 100644 --- a/services/data/src/react/hooks/useDataMutation.ts +++ b/services/data/src/react/hooks/useDataMutation.ts @@ -1,5 +1,9 @@ +import type { + QueryOptions, + Mutation, + QueryExecuteOptions, +} from '@dhis2/data-engine' import { useCallback } from 'react' -import { QueryOptions, Mutation, QueryExecuteOptions } from '../../engine' import { MutationRenderInput } from '../../types' import { useDataEngine } from './useDataEngine' import { useQueryExecutor } from './useQueryExecutor' diff --git a/services/data/src/react/hooks/useDataQuery.test.tsx b/services/data/src/react/hooks/useDataQuery.test.tsx index 370db8163..e27d43abb 100644 --- a/services/data/src/react/hooks/useDataQuery.test.tsx +++ b/services/data/src/react/hooks/useDataQuery.test.tsx @@ -85,6 +85,7 @@ describe('useDataQuery', () => { case two: return Promise.resolve(resultTwo) } + return Promise.resolve(undefined) }) const data = { answer: mockSpy, @@ -245,7 +246,7 @@ describe('useDataQuery', () => { describe('internal: deduplication', () => { it('Should deduplicate identical requests', async () => { - const mockSpy = jest.fn(() => 42) + const mockSpy = jest.fn(() => Promise.resolve(42)) const data = { answer: mockSpy, } @@ -414,7 +415,7 @@ describe('useDataQuery', () => { let count = 0 const spy = jest.fn(() => { count++ - return count + return Promise.resolve(count) }) const data = { answer: spy, @@ -469,10 +470,10 @@ describe('useDataQuery', () => { it('Should only trigger a single request when refetch is called on a lazy query with new variables', async () => { const spy = jest.fn((type, query) => { if (query.id === '1') { - return 42 + return Promise.resolve(42) } - return 0 + return Promise.resolve(0) }) const data = { answer: spy, @@ -511,10 +512,10 @@ describe('useDataQuery', () => { it('Should only trigger a single request when refetch is called on a lazy query with identical variables', async () => { const spy = jest.fn((type, query) => { if (query.id === '1') { - return 42 + return Promise.resolve(42) } - return 0 + return Promise.resolve(0) }) const data = { answer: spy, @@ -553,7 +554,7 @@ describe('useDataQuery', () => { it('Should have a stable identity if the variables have not changed', async () => { const data = { - answer: () => 42, + answer: () => Promise.resolve(42), } const query = { x: { resource: 'answer' } } const wrapper = ({ children }) => ( @@ -750,7 +751,7 @@ describe('useDataQuery', () => { params: ({ one, two, three }) => ({ one, two, three }), }, } - const spy = jest.fn(() => 42) + const spy = jest.fn(() => Promise.resolve(42)) const data = { answer: spy } const wrapper = ({ children }) => ( {children} @@ -917,6 +918,7 @@ describe('useDataQuery', () => { case two: return Promise.resolve(resultTwo) } + return Promise.resolve(undefined) }) const data = { answer: mockSpy, diff --git a/services/data/src/react/hooks/useDataQuery.ts b/services/data/src/react/hooks/useDataQuery.ts index eb4986caa..d83fad5e3 100644 --- a/services/data/src/react/hooks/useDataQuery.ts +++ b/services/data/src/react/hooks/useDataQuery.ts @@ -1,12 +1,12 @@ -import { useQuery } from '@tanstack/react-query' -import { useState, useRef, useCallback, useDebugValue } from 'react' import type { Query, QueryOptions, QueryResult, QueryVariables, -} from '../../engine' -import type { FetchError } from '../../engine/types/FetchError' + FetchError, +} from '@dhis2/data-engine' +import { useQuery } from '@tanstack/react-query' +import { useState, useRef, useCallback, useDebugValue } from 'react' import type { QueryRenderInput, QueryRefetchFunction } from '../../types' import { mergeAndCompareVariables } from './mergeAndCompareVariables' import { useDataEngine } from './useDataEngine' diff --git a/services/data/src/react/hooks/useQueryExecutor.ts b/services/data/src/react/hooks/useQueryExecutor.ts index bf9d31ce6..e9931ea96 100644 --- a/services/data/src/react/hooks/useQueryExecutor.ts +++ b/services/data/src/react/hooks/useQueryExecutor.ts @@ -1,5 +1,6 @@ +import { FetchError } from '@dhis2/data-engine' +import type { QueryExecuteOptions } from '@dhis2/data-engine' import { useState, useCallback, useRef, useEffect } from 'react' -import { FetchError, QueryExecuteOptions } from '../../engine' import { ExecuteHookInput, ExecuteHookResult } from '../../types' import { useStaticInput } from './useStaticInput' diff --git a/services/data/src/react/index.ts b/services/data/src/react/index.ts index 709274199..7aea380be 100644 --- a/services/data/src/react/index.ts +++ b/services/data/src/react/index.ts @@ -1,13 +1,2 @@ -export { CustomDataProvider } from './components/CustomDataProvider' -export { DataMutation } from './components/DataMutation' -export { DataProvider } from './components/DataProvider' -export { DataQuery } from './components/DataQuery' - -export { DataContext } from './context/DataContext' -export { defaultContext } from './context/defaultContext' - -export { useDataEngine } from './hooks/useDataEngine' -export { useDataMutation } from './hooks/useDataMutation' -export { useDataQuery } from './hooks/useDataQuery' -export { useQueryExecutor } from './hooks/useQueryExecutor' -export { useStaticInput } from './hooks/useStaticInput' +export * from './components' +export * from './hooks' diff --git a/services/data/src/types.ts b/services/data/src/types.ts index 8ed6ae042..e623343a4 100644 --- a/services/data/src/types.ts +++ b/services/data/src/types.ts @@ -1,11 +1,14 @@ -import DataEngine from './engine/DataEngine' -import { QueryExecuteOptions } from './engine/types/ExecuteOptions' -import { FetchError } from './engine/types/FetchError' -import { JsonValue } from './engine/types/JsonValue' -import { QueryVariables, QueryResult } from './engine/types/Query' +import type { + DataEngine, + QueryExecuteOptions, + FetchError, + JsonValue, + QueryVariables, + QueryResult, +} from '@dhis2/data-engine' + +export type { Mutation, Query } from '@dhis2/data-engine' -export type { Mutation } from './engine/types/Mutation' -export type { Query } from './engine/types/Query' export interface ContextType { engine: DataEngine } diff --git a/services/offline/package.json b/services/offline/package.json index 7302e4083..95637d3eb 100644 --- a/services/offline/package.json +++ b/services/offline/package.json @@ -4,7 +4,7 @@ "version": "3.14.6", "main": "./build/cjs/index.js", "module": "./build/es/index.js", - "types": "build/types/index.d.ts", + "types": "./build/types/index.d.ts", "exports": { "import": "./build/es/index.js", "require": "./build/cjs/index.js", diff --git a/services/plugin/package.json b/services/plugin/package.json index b691cd3e0..f92d068a2 100644 --- a/services/plugin/package.json +++ b/services/plugin/package.json @@ -3,7 +3,7 @@ "version": "3.14.6", "main": "./build/cjs/index.js", "module": "./build/es/index.js", - "types": "build/types/index.d.ts", + "types": "./build/types/index.d.ts", "exports": { "import": "./build/es/index.js", "require": "./build/cjs/index.js", diff --git a/services/plugin/src/types.ts b/services/plugin/src/types.ts index f00b281de..8aae1bce8 100644 --- a/services/plugin/src/types.ts +++ b/services/plugin/src/types.ts @@ -1,3 +1 @@ -import { ReactNode } from 'react' - // file is a placeholder to allow build