feat: Typescript for API layer (#817)

Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
Co-authored-by: Zakaria El Asri <33696020+zakariaelas@users.noreply.github.com>
Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
Co-authored-by: Philip Korsholm <philip.korsholm@hotmail.com>
Co-authored-by: Sebastian Rindom <seb@medusa-commerce.com>
This commit is contained in:
Oliver Windall Juhl
2021-11-18 15:19:17 +01:00
committed by GitHub
co-authored by Philip Korsholm Zakaria El Asri Kasper Fabricius Kristensen Philip Korsholm Sebastian Rindom
parent 55e200bf68
commit 373532ecbc
413 changed files with 20957 additions and 10349 deletions
+11 -5
View File
@@ -1,6 +1,6 @@
import { getConfigFile } from "medusa-core-utils"
import path from "path"
import { Column, ColumnOptions, ColumnType } from "typeorm"
import { getConfigFile } from "medusa-core-utils"
const pgSqliteTypeMapping: { [key: string]: ColumnType } = {
increment: "rowid",
@@ -19,7 +19,10 @@ let dbType: string
export function resolveDbType(pgSqlType: ColumnType): ColumnType {
if (!dbType) {
try {
const { configModule } = getConfigFile(path.resolve("."), `medusa-config`)
const { configModule } = getConfigFile(
path.resolve("."),
`medusa-config`
) as any
dbType = configModule.projectConfig.database_type
} catch (error) {
// Default to Postgres to allow for e.g. migrations to run
@@ -27,7 +30,7 @@ export function resolveDbType(pgSqlType: ColumnType): ColumnType {
}
}
if (dbType === "sqlite" && pgSqlType in pgSqliteTypeMapping) {
if (dbType === "sqlite" && (pgSqlType as string) in pgSqliteTypeMapping) {
return pgSqliteTypeMapping[pgSqlType.toString()]
}
return pgSqlType
@@ -38,7 +41,10 @@ export function resolveDbGenerationStrategy(
): "increment" | "uuid" | "rowid" {
if (!dbType) {
try {
const { configModule } = getConfigFile(path.resolve("."), `medusa-config`)
const { configModule } = getConfigFile(
path.resolve("."),
`medusa-config`
) as any
dbType = configModule.projectConfig.database_type
} catch (error) {
// Default to Postgres to allow for e.g. migrations to run
@@ -52,7 +58,7 @@ export function resolveDbGenerationStrategy(
return pgSqlType
}
export function DbAwareColumn(columnOptions: ColumnOptions) {
export function DbAwareColumn(columnOptions: ColumnOptions): PropertyDecorator {
const pre = columnOptions.type
if (columnOptions.type) {
columnOptions.type = resolveDbType(columnOptions.type)
@@ -1,12 +1,22 @@
import { getConfigFile } from "medusa-core-utils"
import path from "path"
import { getConnection } from "typeorm"
import { getConfigFile } from "medusa-core-utils"
export async function manualAutoIncrement(
tableName: string
): Promise<number | null> {
const { configModule } = getConfigFile(path.resolve("."), `medusa-config`)
const dbType = configModule.projectConfig.database_type
let dbType
try {
const { configModule } = getConfigFile(
path.resolve("."),
`medusa-config`
) as any
dbType = configModule.projectConfig.database_type
} catch (error) {
// Default to Postgres to allow for e.g. migrations to run
dbType = "postgres"
}
if (dbType === "sqlite") {
const connection = getConnection()
const [rec] = await connection.query(
+3 -3
View File
@@ -4,10 +4,10 @@ export class ShortenedNamingStrategy extends DefaultNamingStrategy {
eagerJoinRelationAlias(alias: string, propertyPath: string): string {
const path = propertyPath
.split(".")
.map(p => p.substring(0, 2))
.map((p) => p.substring(0, 2))
.join("_")
let out = alias + "_" + path
let match = out.match(/_/g) || []
const out = alias + "_" + path
const match = out.match(/_/g) || []
return out + match.length
}
}
+43
View File
@@ -0,0 +1,43 @@
import { ClassConstructor, plainToClass } from "class-transformer"
import { validate, ValidationError, ValidatorOptions } from "class-validator"
import { MedusaError } from "medusa-core-utils"
const reduceErrorMessages = (errs: ValidationError[]): string[] => {
return errs.reduce((acc: string[], next) => {
if (next.constraints) {
for (const [_, msg] of Object.entries(next.constraints)) {
acc.push(msg)
}
}
if (next.children) {
acc.push(...reduceErrorMessages(next.children))
}
return acc
}, [])
}
export async function validator<T, V>(
typedClass: ClassConstructor<T>,
plain: V,
config: ValidatorOptions = {}
): Promise<T> {
const toValidate = plainToClass(typedClass, plain)
// @ts-ignore
const errors = await validate(toValidate, {
whitelist: true,
forbidNonWhitelisted: true,
...config,
})
const errorMessages = reduceErrorMessages(errors)
if (errors?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
errorMessages.join(", ")
)
}
return toValidate
}
@@ -0,0 +1,2 @@
export const transformDate = ({ value }): Date =>
!isNaN(Date.parse(value)) ? new Date(value) : new Date(Number(value) * 1000)
@@ -0,0 +1,34 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
isDefined,
} from "class-validator"
import { MedusaError } from "medusa-core-utils"
export function IsGreaterThan(
property: string,
validationOptions?: ValidationOptions
) {
return function (object: any, propertyName: string): void {
registerDecorator({
name: "IsGreaterThan",
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints
const relatedValue = args.object[relatedPropertyName]
return relatedValue ? value > relatedValue : isDefined(value)
},
defaultMessage(args?: ValidationArguments): string {
return `"${propertyName}" must be greater than ${JSON.stringify(
args?.constraints[0]
)}`
},
},
})
}
}
@@ -0,0 +1,120 @@
import {
isArray,
isNumber,
isString,
registerDecorator,
ValidationArguments,
ValidationOptions,
} from "class-validator"
import { isDate } from "lodash"
import { MedusaError } from "medusa-core-utils"
import { validator } from "../validator"
async function typeValidator(
typedClass: any,
plain: unknown
): Promise<boolean> {
switch (typedClass) {
case String:
if (!isString(plain)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`String validation failed: ${plain} is not a string`
)
}
return true
case Number:
if (!isNumber(Number(plain))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Number validation failed: ${plain} is not a number`
)
}
return true
case Date:
if (!isDate(new Date(plain as string))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Date validation failed: ${plain} is not a date`
)
}
return true
default:
if (isArray(typedClass) && isArray(plain)) {
const errors: Map<any, string> = new Map()
const result = (
await Promise.all(
(plain as any[]).map(
async (p) =>
await typeValidator(typedClass[0], p).catch((e) => {
errors.set(typedClass[0].name, e.message.split(","))
return false
})
)
)
).some(Boolean)
if (result) {
return true
}
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
Object.fromEntries(errors.entries())
)
}
return (
(await validator(typedClass, plain).then(() => true)) &&
typeof plain === "object"
)
}
}
export function IsType(types: any[], validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string): void {
registerDecorator({
name: "IsType",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
async validate(value: unknown, args: ValidationArguments) {
const errors: Map<any, string> = new Map()
const results = await Promise.all(
types.map(
async (v) =>
await typeValidator(v, value).catch((e) => {
errors.set(v.name, e.message.split(",").filter(Boolean))
return false
})
)
)
if (results.some(Boolean)) {
return true
}
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
JSON.stringify({
message: `${args.property} must be one of: ${types.map(
(t) => `${t.name || (Array.isArray(t) ? t[0]?.name : "")}`
)}`,
details: Object.fromEntries(errors.entries()),
})
)
},
defaultMessage(validationArguments?: ValidationArguments) {
const names = types.map(
(t) => t.name || (isArray(t) ? `${t[0].name}[]` : "")
)
return `${validationArguments?.property} must be one of ${names
.join(", ")
.replace(/, ([^,]*)$/, " or $1")}`
},
},
})
}
}
@@ -0,0 +1,26 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from "class-validator"
export function IsISO8601Duration(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string): void {
registerDecorator({
name: "IsGreaterThan",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const isoDurationRegex =
/^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/
return isoDurationRegex.test(value)
},
defaultMessage(args?: ValidationArguments): string {
return `"${propertyName}" must be a valid ISO 8601 duration`
},
},
})
}
}