import { SelectQueryBuilder } from 'typeorm' import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata' /** * Joins 2 keys as `K`, `K.P`, `K.(P` or `K.P)` * The parenthesis notation is included for embedded columns */ type Join = K extends string ? P extends string ? `${K}${'' extends P ? '' : '.'}${P | `(${P}` | `${P})`}` : never : never /** * Get the previous number between 0 and 10. Examples: * Prev[3] = 2 * Prev[0] = never. * Prev[20] = 0 */ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]] /** * Unwrap Promise to T */ type UnwrapPromise = T extends Promise ? UnwrapPromise : T /** * Unwrap Array to T */ type UnwrapArray = T extends Array ? UnwrapArray : T /** * Find all the dotted path properties for a given column. * * T: The column * D: max depth */ // v Have we reached max depth? export type Column = [D] extends [never] ? // yes, stop recursing never : // Are we extending something with keys? T extends Record ? { // For every keyof T, find all possible properties as a string union [K in keyof T]-?: K extends string ? // Is it string or number (includes enums)? T[K] extends string | number ? // yes, add just the key `${K}` : // Is it a Date? T[K] extends Date ? // yes, add just the key `${K}` : // no, is it an array? T[K] extends Array ? // yes, unwrap it, and recurse deeper `${K}` | Join, Prev[D]>> : // no, is it a promise? T[K] extends Promise ? // yes, try to infer its return type and recurse U extends Array ? `${K}` | Join, Prev[D]>> : `${K}` | Join, Prev[D]>> : // no, we have no more special cases, so treat it as an // object and recurse deeper on its keys `${K}` | Join> : never // Join all the string unions of each keyof T into a single string union }[keyof T] : '' export type RelationColumn = Extract< Column, { [K in Column]: K extends `${infer R}.${string}` ? R : never }[Column] > export type Order = [Column, 'ASC' | 'DESC'] export type SortBy = Order[] export function isEntityKey(entityColumns: Column[], column: string): column is Column { return !!entityColumns.find((c) => c === column) } export const positiveNumberOrDefault = (value: number | undefined, defaultValue: number, minValue: 0 | 1 = 0) => value === undefined || value < minValue ? defaultValue : value export type ColumnProperties = { propertyPath?: string; propertyName: string; isNested: boolean; column: string } export function getPropertiesByColumnName(column: string): ColumnProperties { const propertyPath = column.split('.') if (propertyPath.length > 1) { const propertyNamePath = propertyPath.slice(1) let isNested = false, propertyName = propertyNamePath.join('.') if (!propertyName.startsWith('(') && propertyNamePath.length > 1) { isNested = true } propertyName = propertyName.replace('(', '').replace(')', '') return { propertyPath: propertyPath[0], propertyName, // the join is in case of an embedded entity isNested, column: `${propertyPath[0]}.${propertyName}`, } } else { return { propertyName: propertyPath[0], isNested: false, column: propertyPath[0] } } } export function extractVirtualProperty( qb: SelectQueryBuilder, columnProperties: ColumnProperties ): { isVirtualProperty: boolean; query?: ColumnMetadata['query'] } { const metadata = columnProperties.propertyPath ? qb?.expressionMap?.mainAlias?.metadata?.findColumnWithPropertyPath(columnProperties.propertyPath) ?.referencedColumn?.entityMetadata // on relation : qb?.expressionMap?.mainAlias?.metadata return ( metadata?.columns?.find((column) => column.propertyName === columnProperties.propertyName) || { isVirtualProperty: false, query: undefined, } ) } export function includesAllPrimaryKeyColumns(qb: SelectQueryBuilder, propertyPath: string[]): boolean { if (!qb || !propertyPath) { return false } return qb.expressionMap.mainAlias?.metadata?.primaryColumns .map((column) => column.propertyPath) .every((column) => propertyPath.includes(column)) } export function hasColumnWithPropertyPath( qb: SelectQueryBuilder, columnProperties: ColumnProperties ): boolean { if (!qb || !columnProperties) { return false } return !!qb.expressionMap.mainAlias?.metadata?.hasColumnWithPropertyPath(columnProperties.propertyName) } export function checkIsRelation(qb: SelectQueryBuilder, propertyPath: string): boolean { if (!qb || !propertyPath) { return false } return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath) } export function checkIsEmbedded(qb: SelectQueryBuilder, propertyPath: string): boolean { if (!qb || !propertyPath) { return false } return !!qb?.expressionMap?.mainAlias?.metadata?.hasEmbeddedWithPropertyPath(propertyPath) } export function checkIsArray(qb: SelectQueryBuilder, propertyName: string): boolean { if (!qb || !propertyName) { return false } return !!qb?.expressionMap?.mainAlias?.metadata.findColumnWithPropertyName(propertyName)?.isArray } // This function is used to fix the column alias when using relation, embedded or virtual properties export function fixColumnAlias( properties: ColumnProperties, alias: string, isRelation = false, isVirtualProperty = false, isEmbedded = false, query?: ColumnMetadata['query'] ): string { if (isRelation) { if (isVirtualProperty && query) { return `(${query(`${alias}_${properties.propertyPath}`)})` // () is needed to avoid parameter conflict } else if ((isVirtualProperty && !query) || properties.isNested) { return `${alias}_${properties.propertyPath}_${properties.propertyName}` } else { return `${alias}_${properties.propertyPath}.${properties.propertyName}` } } else if (isVirtualProperty) { return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}` } else if (isEmbedded) { return `${alias}.${properties.propertyPath}.${properties.propertyName}` } else { return `${alias}.${properties.propertyName}` } } export function getQueryUrlComponents(path: string): { queryOrigin: string; queryPath: string } { const r = new RegExp('^(?:[a-z+]+:)?//', 'i') let queryOrigin = '' let queryPath = '' if (r.test(path)) { const url = new URL(path) queryOrigin = url.origin queryPath = url.pathname } else { queryPath = path } return { queryOrigin, queryPath } } const isoDateRegExp = new RegExp( /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/ ) export function isISODate(str: string): boolean { return isoDateRegExp.test(str) }