Fix image-upload, http-client and config

This commit is contained in:
Jerko Steiner 2019-09-15 17:35:31 +07:00
parent 50d36f268f
commit 69dd12375c
27 changed files with 282 additions and 266 deletions

View File

@ -13,14 +13,18 @@ rules:
ignorePattern: '^import .* from '
comma-dangle:
- warn
- always-multiline
- arrays: always-multiline
objects: always-multiline
imports: always-multiline
exports: always-multiline
functions: always-multiline
# semi:
# - warn
# - never
# interface-name-prefix:
'@typescript-eslint/member-delimiter-style':
- error
- warn
- multiline:
delimiter: none
singleline:

View File

@ -0,0 +1,4 @@
extends:
- ../../.eslintrc.yaml
rules:
'@typescript-eslint/no-explicit-any': off

View File

@ -4,7 +4,7 @@ export class Config {
get(key: string) {
let value = this.config
key.split('.').forEach(k => {
if (!value.hasOwnProperty(k)) {
if (!Object.prototype.hasOwnProperty.call(value, k)) {
throw new Error(`Property "${k}" from "${key}" does not exist`)
}
value = value[k]
@ -15,7 +15,7 @@ export class Config {
has(key: string) {
let c = this.config
return key.split('.').every(k => {
const has = c.hasOwnProperty(k)
const has = Object.prototype.hasOwnProperty.call(c, k)
if (has) {
c = c[k]
}

View File

@ -1,6 +1,6 @@
import {ConfigReader} from './ConfigReader'
import {join} from 'path'
import {writeFileSync} from 'fs'
import { writeFileSync } from 'fs'
import { join } from 'path'
import { ConfigReader } from './ConfigReader'
describe('ConfigReader', () => {

View File

@ -1,11 +1,11 @@
import loggerFactory, { Logger } from '@rondo.dev/logger'
import { readFileSync } from 'fs'
import YAML from 'js-yaml'
import { join } from 'path'
import { Config } from './Config'
import { findPackageRoot } from './findPackageRoot'
import loggerFactory, {ILogger} from '@rondo.dev/logger'
const isObject = (value: any) => value !== null && typeof value === 'object'
const isObject = (value: unknown) => value !== null && typeof value === 'object'
export class ConfigReader {
protected readonly config: any = {}
@ -16,7 +16,7 @@ export class ConfigReader {
readonly path: string,
readonly cwd: string | undefined = process.cwd(),
readonly environment = 'CONFIG',
readonly logger: ILogger = loggerFactory.getLogger('config'),
readonly logger: Logger = loggerFactory.getLogger('config'),
) {
const packageRoot = path && findPackageRoot(path)
this.locations = packageRoot ? [packageRoot] : []
@ -93,7 +93,8 @@ export class ConfigReader {
// }
const value = src[key]
if (isObject(value) && !Array.isArray(value)) {
if (!dest.hasOwnProperty(key) ||
if (
!Object.prototype.hasOwnProperty.call(dest, key) ||
Array.isArray(dest[key]) ||
!isObject(dest[key])
) {

View File

@ -1,5 +1,5 @@
import {resolve, join} from 'path'
import {statSync, Stats} from 'fs'
import { Stats, statSync } from 'fs'
import { join, resolve } from 'path'
function findNearestDirectory(
dir: string, filename: string,

View File

@ -1,4 +1,4 @@
export * from './Config'
import {ConfigReader} from './ConfigReader'
import { ConfigReader } from './ConfigReader'
export default ConfigReader

View File

@ -0,0 +1,4 @@
extends:
- ../../.eslintrc.yaml
rules:
'@typescript-eslint/no-explicit-any': off

View File

@ -1,140 +1,51 @@
import axios from 'axios'
import {IHTTPClient} from './IHTTPClient'
import {IHeader} from './IHeader'
import {TMethod, IRoutes} from '@rondo.dev/http-types'
import {URLFormatter} from './URLFormatter'
import {IRequest} from './IRequest'
import {IResponse} from './IResponse'
import {ITypedRequestParams} from './ITypedRequestParams'
import {Method, Routes} from '@rondo.dev/http-types'
import {TypedRequestParams} from './TypedRequestParams'
interface IRequestor {
request: (params: IRequest) => Promise<IResponse>
}
export class HTTPClient<T extends IRoutes> implements IHTTPClient<T> {
protected readonly requestor: IRequestor
protected readonly formatter: URLFormatter
constructor(
protected readonly baseURL = '',
protected readonly headers?: IHeader,
) {
this.requestor = this.createRequestor()
this.formatter = new URLFormatter()
}
protected createRequestor(): IRequestor {
return axios.create({
baseURL: this.baseURL,
headers: this.headers,
})
}
async request<
export interface HTTPClient<T extends Routes> {
request<
P extends keyof T & string,
M extends TMethod,
>(params: ITypedRequestParams<T, P, M>): Promise<T[P][M]['response']> {
const url = this.formatter.format(params.path, params.params)
const response = await this.requestor.request({
method: params.method,
url,
params: params.query,
data: params.body,
})
return response.data
}
M extends Method,
>(params: TypedRequestParams<T, P, M>): Promise<T[P][M]['response']>
get<P extends keyof T & string>(
path: P,
query?: T[P]['get']['query'],
params?: T[P]['get']['params'],
) {
return this.request({
method: 'get',
path,
query,
params,
})
}
): Promise<T[P]['get']['response']>
post<P extends keyof T & string>(
path: P,
body: T[P]['post']['body'],
params?: T[P]['post']['params'],
) {
return this.request({
method: 'post',
path,
body,
params,
})
}
): Promise<T[P]['post']['response']>
put<P extends keyof T & string>(
path: P,
body: T[P]['put']['body'],
params?: T[P]['put']['params'],
) {
return this.request({
method: 'put',
path,
body,
params,
})
}
): Promise<T[P]['put']['response']>
delete<P extends keyof T & string>(
path: P,
body: T[P]['delete']['body'],
params?: T[P]['delete']['params'],
) {
return this.request({
method: 'delete',
path,
body,
params,
})
}
): Promise<T[P]['delete']['response']>
head<P extends keyof T & string>(
path: P,
query?: T[P]['head']['query'],
params?: T[P]['head']['params'],
) {
return this.request({
method: 'head',
path,
params,
query,
})
}
): Promise<T[P]['head']['response']>
options<P extends keyof T & string>(
path: P,
query?: T[P]['options']['query'],
params?: T[P]['options']['params'],
) {
return this.request({
method: 'options',
path,
params,
query,
})
}
): Promise<T[P]['options']['response']>
patch<P extends keyof T & string>(
path: P,
body: T[P]['patch']['body'],
params?: T[P]['patch']['params'],
) {
return this.request({
method: 'patch',
path,
body,
params,
})
}
): Promise<T[P]['patch']['response']>
}

View File

@ -1,32 +1,32 @@
import {HTTPClient} from './HTTPClient'
import {IRequest} from './IRequest'
import {IResponse} from './IResponse'
import {IRoutes, TMethod} from '@rondo.dev/http-types'
import {ITypedRequestParams} from './ITypedRequestParams'
import {SimpleHTTPClient} from './SimpleHTTPClient'
import {Request} from './Request'
import {Response} from './Response'
import {Routes, Method} from '@rondo.dev/http-types'
import {TypedRequestParams} from './TypedRequestParams'
interface IReqRes {
req: IRequest
res: IResponse
interface ReqRes {
req: Request
res: Response
}
export class HTTPClientError extends Error {
constructor(readonly request: IRequest, readonly response: IResponse) {
constructor(readonly request: Request, readonly response: Response) {
super('HTTP Status: ' + response.status)
Error.captureStackTrace(this)
}
}
export interface IRequestStatus {
request: IRequest
export interface RequestStatus {
request: Request
finished: boolean
}
export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
mocks: {[key: string]: IResponse} = {}
requests: IRequestStatus[] = []
export class HTTPClientMock<T extends Routes> extends SimpleHTTPClient<T> {
mocks: {[key: string]: Response} = {}
requests: RequestStatus[] = []
protected waitPromise?: {
resolve: (r: IReqRes) => void
resolve: (r: ReqRes) => void
reject: (err: Error) => void
}
@ -39,15 +39,15 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
*/
createRequestor() {
return {
request: (req: IRequest): Promise<IResponse> => {
const currentRequest: IRequestStatus = {
request: (req: Request): Promise<Response> => {
const currentRequest: RequestStatus = {
request: req,
finished: false,
}
this.requests.push(currentRequest)
return new Promise((resolve, reject) => {
const key = this.serialize(req)
if (!this.mocks.hasOwnProperty(key)) {
if (!Object.prototype.hasOwnProperty.call(this.mocks, key)) {
setImmediate(() => {
const err = new Error(
'No mock for request: ' + key + '\nAvailable mocks: ' +
@ -76,7 +76,7 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
}
}
protected serialize(req: IRequest) {
protected serialize(req: Request) {
return JSON.stringify({
method: req.method,
url: req.url,
@ -90,7 +90,7 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
* replaced. The signature is calculated using the `serialize()` method,
* which just does a `JSON.stringify(req)`.
*/
mockAdd(req: IRequest, data: any, status = 200): this {
mockAdd(req: Request, data: any, status = 200): this {
this.mocks[this.serialize(req)] = {data, status}
return this
}
@ -98,8 +98,8 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
/**
* Adds a new mock with predefined type
*/
mockAddTyped<P extends keyof T & string, M extends TMethod>(
params: ITypedRequestParams<T, P, M>,
mockAddTyped<P extends keyof T & string, M extends Method>(
params: TypedRequestParams<T, P, M>,
response: T[P][M]['response'],
): this {
const url = this.formatter.format(params.path, params.params)
@ -120,7 +120,7 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
return this
}
protected notify(r: IReqRes | Error) {
protected notify(r: ReqRes | Error) {
if (!this.waitPromise) {
return
}
@ -147,12 +147,12 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
* const {req, res} = await httpMock.wait()
* expect(req).toEqual({method:'get', url:'/auth/post', data: {...}})
*/
async wait(): Promise<IReqRes> {
async wait(): Promise<ReqRes> {
if (this.requests.every(r => r.finished)) {
throw new Error('No requests to wait for')
}
expect(this.waitPromise).toBe(undefined)
const result: IReqRes = await new Promise((resolve, reject) => {
const result: ReqRes = await new Promise((resolve, reject) => {
this.waitPromise = {resolve, reject}
})
// TODO think of a better way to do this.

View File

@ -1,3 +1,3 @@
export interface IHeader {
export interface Headers {
readonly [key: string]: string
}

View File

@ -1,51 +0,0 @@
import {TMethod, IRoutes} from '@rondo.dev/http-types'
import {ITypedRequestParams} from './ITypedRequestParams'
export interface IHTTPClient<T extends IRoutes> {
request<
P extends keyof T & string,
M extends TMethod,
>(params: ITypedRequestParams<T, P, M>): Promise<T[P][M]['response']>
get<P extends keyof T & string>(
path: P,
query?: T[P]['get']['query'],
params?: T[P]['get']['params'],
): Promise<T[P]['get']['response']>
post<P extends keyof T & string>(
path: P,
body: T[P]['post']['body'],
params?: T[P]['post']['params'],
): Promise<T[P]['post']['response']>
put<P extends keyof T & string>(
path: P,
body: T[P]['put']['body'],
params?: T[P]['put']['params'],
): Promise<T[P]['put']['response']>
delete<P extends keyof T & string>(
path: P,
body: T[P]['delete']['body'],
params?: T[P]['delete']['params'],
): Promise<T[P]['delete']['response']>
head<P extends keyof T & string>(
path: P,
query?: T[P]['head']['query'],
params?: T[P]['head']['params'],
): Promise<T[P]['head']['response']>
options<P extends keyof T & string>(
path: P,
query?: T[P]['options']['query'],
params?: T[P]['options']['params'],
): Promise<T[P]['options']['response']>
patch<P extends keyof T & string>(
path: P,
body: T[P]['patch']['body'],
params?: T[P]['patch']['params'],
): Promise<T[P]['patch']['response']>
}

View File

@ -1,8 +0,0 @@
import {TMethod} from '@rondo.dev/http-types'
export interface IRequest {
method: TMethod,
url: string,
params?: {[key: string]: any},
data?: any,
}

View File

@ -1,13 +0,0 @@
import {IRoutes, TMethod} from '@rondo.dev/http-types'
export interface ITypedRequestParams<
T extends IRoutes,
P extends keyof T & string,
M extends TMethod,
> {
method: M,
path: P,
params?: T[P][M]['params'],
query?: T[P][M]['query'],
body?: T[P][M]['body'],
}

View File

@ -0,0 +1,8 @@
import {Method} from '@rondo.dev/http-types'
export interface Request {
method: Method
url: string
params?: {[key: string]: any}
data?: any
}

View File

@ -1,3 +1,3 @@
export interface IRequestQuery {
export interface RequestParams {
[key: string]: string | number
}

View File

@ -1,3 +1,3 @@
export interface IRequestParams {
export interface RequestQuery {
[key: string]: string | number
}

View File

@ -1,4 +1,4 @@
export interface IResponse {
export interface Response {
data: any
status: number
}

View File

@ -0,0 +1,140 @@
import axios from 'axios'
import {HTTPClient} from './HTTPClient'
import {Headers} from './Headers'
import {Method, Routes} from '@rondo.dev/http-types'
import {URLFormatter} from './URLFormatter'
import {Request} from './Request'
import {Response} from './Response'
import {TypedRequestParams} from './TypedRequestParams'
interface Requestor {
request: (params: Request) => Promise<Response>
}
export class SimpleHTTPClient<T extends Routes> implements HTTPClient<T> {
protected readonly requestor: Requestor
protected readonly formatter: URLFormatter
constructor(
protected readonly baseURL = '',
protected readonly headers?: Headers,
) {
this.requestor = this.createRequestor()
this.formatter = new URLFormatter()
}
protected createRequestor(): Requestor {
return axios.create({
baseURL: this.baseURL,
headers: this.headers,
})
}
async request<
P extends keyof T & string,
M extends Method,
>(params: TypedRequestParams<T, P, M>): Promise<T[P][M]['response']> {
const url = this.formatter.format(params.path, params.params)
const response = await this.requestor.request({
method: params.method,
url,
params: params.query,
data: params.body,
})
return response.data
}
get<P extends keyof T & string>(
path: P,
query?: T[P]['get']['query'],
params?: T[P]['get']['params'],
) {
return this.request({
method: 'get',
path,
query,
params,
})
}
post<P extends keyof T & string>(
path: P,
body: T[P]['post']['body'],
params?: T[P]['post']['params'],
) {
return this.request({
method: 'post',
path,
body,
params,
})
}
put<P extends keyof T & string>(
path: P,
body: T[P]['put']['body'],
params?: T[P]['put']['params'],
) {
return this.request({
method: 'put',
path,
body,
params,
})
}
delete<P extends keyof T & string>(
path: P,
body: T[P]['delete']['body'],
params?: T[P]['delete']['params'],
) {
return this.request({
method: 'delete',
path,
body,
params,
})
}
head<P extends keyof T & string>(
path: P,
query?: T[P]['head']['query'],
params?: T[P]['head']['params'],
) {
return this.request({
method: 'head',
path,
params,
query,
})
}
options<P extends keyof T & string>(
path: P,
query?: T[P]['options']['query'],
params?: T[P]['options']['params'],
) {
return this.request({
method: 'options',
path,
params,
query,
})
}
patch<P extends keyof T & string>(
path: P,
body: T[P]['patch']['body'],
params?: T[P]['patch']['params'],
) {
return this.request({
method: 'patch',
path,
body,
params,
})
}
}

View File

@ -0,0 +1,13 @@
import {Routes, Method} from '@rondo.dev/http-types'
export interface TypedRequestParams<
T extends Routes,
P extends keyof T & string,
M extends Method,
> {
method: M
path: P
params?: T[P][M]['params']
query?: T[P][M]['query']
body?: T[P][M]['body']
}

View File

@ -1,27 +1,27 @@
import {IRequestParams} from './IRequestParams'
import {IRequestQuery} from './IRequestQuery'
import {RequestParams} from './RequestParams'
import {RequestQuery} from './RequestQuery'
export interface IURLFormatterOptions {
export interface URLFormatterOptions {
readonly baseURL: string
readonly regex: RegExp
}
export class URLFormatter {
constructor(readonly params: IURLFormatterOptions = {
constructor(readonly params: URLFormatterOptions = {
baseURL: '',
regex: /:[a-zA-Z0-9-]+/g,
}) {}
format(
url: string,
params?: IRequestParams,
query?: IRequestQuery,
params?: RequestParams,
query?: RequestQuery,
) {
let formattedUrl = url
if (params) {
formattedUrl = url.replace(this.params.regex, match => {
const key = match.substring(1)
if (!params.hasOwnProperty(key)) {
if (!Object.prototype.hasOwnProperty.call(params, key)) {
throw new Error('Undefined URL paramter: ' + key)
}
return String(params![key])

View File

@ -1,10 +1,10 @@
export * from './HTTPClient'
export * from './HTTPClientMock'
export * from './IHeader'
export * from './IHTTPClient'
export * from './IRequest'
export * from './IRequestParams'
export * from './IRequestQuery'
export * from './IResponse'
export * from './ITypedRequestParams'
export * from './Headers'
export * from './HTTPClient'
export * from './Request'
export * from './RequestParams'
export * from './RequestQuery'
export * from './Response'
export * from './TypedRequestParams'
export * from './URLFormatter'

View File

@ -1,4 +1,6 @@
export type TMethod = 'get'
/* eslint @typescript-eslint/no-explicit-any: 0 */
export type Method = 'get'
| 'post'
| 'put'
| 'delete'
@ -6,14 +8,14 @@ export type TMethod = 'get'
| 'head'
| 'options'
export interface IRoutes {
export interface Routes {
// has to be any because otherwise TypeScript will start
// throwing error and interfaces without an index signature
// would not be usable
[route: string]: any
}
export interface IRoute {
export interface Route {
params: any
query: any
body: any

View File

@ -1,10 +1,9 @@
function attachErrorListener(
reader: FileReader,
file: File,
reject: (err: Error) => void,
) {
reader.onerror = ev => reject(new Error('Error reading file: ' +
reader.onerror = () => reject(new Error('Error reading file: ' +
(reader.error ? reader.error.message : file.name)))
}

View File

@ -1,5 +1,6 @@
export async function createImageFromDataURL(dataURL: string)
: Promise<HTMLImageElement> {
export async function createImageFromDataURL(
dataURL: string,
): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => resolve(image)
@ -8,8 +9,9 @@ export async function createImageFromDataURL(dataURL: string)
})
}
export async function drawCanvasFromDataURL(dataURL: string)
: Promise<HTMLCanvasElement> {
export async function drawCanvasFromDataURL(
dataURL: string,
): Promise<HTMLCanvasElement> {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
const image = await createImageFromDataURL(dataURL)
@ -24,7 +26,7 @@ export async function getCanvasFromArrayBuffer(
width: number,
height: number,
) {
return new Promise((resolve, reject) => {
return new Promise(() => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
const imageData = new ImageData(

View File

@ -3,34 +3,33 @@ import {Resizer} from './Resizer'
import {readAsDataURL} from './Files'
import {drawCanvasFromDataURL} from './Image'
export interface IImage {
export interface Image {
dataURL: string
}
export interface IImageUploadProps {
onChange: (images: IImage[]) => void
export interface ImageUploadProps {
onChange: (images: Image[]) => void
multiple: boolean
}
export class ImageUpload extends React.PureComponent<IImageUploadProps> {
export class ImageUpload extends React.PureComponent<ImageUploadProps> {
fileInput: React.RefObject<HTMLInputElement>
constructor(props: IImageUploadProps) {
constructor(props: ImageUploadProps) {
super(props)
this.fileInput = React.createRef()
}
safeHandleChange = async (event: React.SyntheticEvent<HTMLInputElement>) => {
safeHandleChange = async () => {
try {
await this.handleChange(event)
await this.handleChange()
} catch (err) {
// console.log('Error in handleChange', err)
}
}
handleChange = async (event: React.SyntheticEvent<HTMLInputElement>) => {
const self = this
handleChange = async () => {
const files = Array.from(this.fileInput.current!.files!)
const resized: IImage[] = []
const resized: Image[] = []
for (const file of files) {
const dataURL = await readAsDataURL(file)
const resizedDataURL = await this.resize(dataURL)
@ -44,7 +43,7 @@ export class ImageUpload extends React.PureComponent<IImageUploadProps> {
this.fileInput.current!.parentElement!.appendChild(img)
}
(window as any).resized = resized
// (window as any).resized = resized
this.props.onChange(resized)
}

View File

@ -1,4 +1,4 @@
interface IWorkerParam {
interface WorkerParam {
sourceWidth: number
sourceHeight: number
width: number
@ -6,21 +6,22 @@ interface IWorkerParam {
source: ArrayBuffer
}
interface IWorkerParamMessage {
data: IWorkerParam
interface WorkerParamMessage {
data: WorkerParam
}
interface IWorkerResult {
interface WorkerResult {
target: Uint8ClampedArray
}
interface IWorkerResultMessage {
data: IWorkerResult
interface WorkerResultMessage {
data: WorkerResult
}
function createResizeWorker(root: any = {})
: {onmessage: (event: IWorkerParamMessage) => void} {
root.onmessage = (event: IWorkerParamMessage) => {
function createResizeWorker(
root: any = {}, // eslint-disable-line
): {onmessage: (event: WorkerParamMessage) => void} {
root.onmessage = (event: WorkerParamMessage) => {
const sourceWidth = event.data.sourceWidth
const sourceHeight = event.data.sourceHeight
const width = event.data.width
@ -32,7 +33,7 @@ function createResizeWorker(root: any = {})
const ratioHalfH = Math.ceil(ratioH / 2)
const source = new Uint8ClampedArray(event.data.source)
const sourceH = source.length / sourceWidth / 4
// const sourceH = source.length / sourceWidth / 4
const targetSize = width * height * 4
const targetMemory = new ArrayBuffer(targetSize)
const target = new Uint8ClampedArray(targetMemory, 0, targetSize)
@ -92,10 +93,10 @@ function createResizeWorker(root: any = {})
}
}
const objData: IWorkerResult = {
const objData: WorkerResult = {
target,
}
postMessage(objData, [target.buffer] as any)
postMessage(objData, [target.buffer] as any) // eslint-disable-line
}
return root
}
@ -156,7 +157,7 @@ export class Resizer {
const worker = new Worker(this.workerBlobURL)
activeWorkers += 1
workers[c] = worker
worker.onmessage = (event: IWorkerResultMessage) => {
worker.onmessage = (event: WorkerResultMessage) => {
worker.terminate()
delete workers[c]
activeWorkers -= 1
@ -174,7 +175,7 @@ export class Resizer {
reject(new Error('Error resizing: ' + err.message))
}
const message: IWorkerParam = {
const message: WorkerParam = {
sourceWidth,
sourceHeight: partition.height,
width,