Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move data engine to a standalone package without react dependency [LIBS-711] #1392

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions engine/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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',
},
}
5 changes: 5 additions & 0 deletions engine/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# DHIS2 Platform
node_modules
.d2
src/locales
build
9 changes: 9 additions & 0 deletions engine/d2.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = {
type: 'lib',

entryPoints: {
lib: './src/index.ts',
},
}

module.exports = config
36 changes: 36 additions & 0 deletions engine/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@dhis2/data-engine",
"version": "3.11.0-alpha.1",
"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 <[email protected]>",
"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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ 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 { Query } from './types/Query'
import type { DataEngineLink } from './types/DataEngineLink'
import type { QueryExecuteOptions } from './types/ExecuteOptions'
import type { JsonMap, JsonValue } from './types/JsonValue'
import type { Mutation } from './types/Mutation'
import type { Query } from './types/Query'

const reduceResponses = (responses: JsonValue[], names: string[]) =>
responses.reduce<JsonMap>((out, response, idx) => {
Expand All @@ -17,7 +17,7 @@ const reduceResponses = (responses: JsonValue[], names: string[]) =>
}, {})

export class DataEngine {
private link: DataEngineLink
private readonly link: DataEngineLink
public constructor(link: DataEngineLink) {
this.link = link
}
Expand Down
2 changes: 2 additions & 0 deletions engine/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FetchError } from './FetchError'
export { InvalidQueryError } from './InvalidQueryError'
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('getMutationFetchType', () => {
).toBe('delete')
expect(
getMutationFetchType({
id: '123',
type: 'json-patch',
resource: 'test',
data: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InvalidQueryError } from '../types/InvalidQueryError'
import { InvalidQueryError } from '../errors/InvalidQueryError'
import { ResolvedResourceQuery } from '../types/Query'

const validQueryKeys = ['resource', 'id', 'params', 'data']
Expand Down
5 changes: 5 additions & 0 deletions engine/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './DataEngine'
export * from './links'
export * from './errors'

export * from './types'
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FetchError } from '../../engine'
import { FetchError } from '../../errors/FetchError'
import { parseStatus, fetchData, parseContentType } from './fetchData'

describe('networkFetch', () => {
Expand Down Expand Up @@ -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, (type: string) => 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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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() : ''
Expand Down
6 changes: 6 additions & 0 deletions engine/src/links/RestAPILink/path.ts
Original file line number Diff line number Diff line change
@@ -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.replace(/(^\/+)|(\/+$)/g, '')) // Replace leading and trailing slashes for each part
.join('/')
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ResolvedResourceQuery, FetchType } from '../../engine'
import type { FetchType } from '../../types/ExecuteOptions'
import type { ResolvedResourceQuery } from '../../types/Query'
import {
requestContentType,
requestBodyForContentType,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResolvedResourceQuery, FetchType } from '../../../engine'
import type { ResolvedResourceQuery, FetchType } from '../../../types'
import * as multipartFormDataMatchers from './multipartFormDataMatchers'
import * as textPlainMatchers from './textPlainMatchers'
import * as xWwwFormUrlencodedMatchers from './xWwwFormUrlencodedMatchers'
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResolvedResourceQuery, FetchType } from '../../../engine'
import type { ResolvedResourceQuery, FetchType } from '../../../types'

// POST to convert an SVG file
export const isSvgConversion = (
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '<base>',
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)
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading