-
-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add experimental entity-{uniqueness,property-names,join-columns…
…} helpers (#1062) * feat: add entity-{uniqueness,property-names,relation-columns} helpers * fix: renamed entity-relation-columns to entity-join-columns & added js docs
- Loading branch information
Showing
26 changed files
with
525 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { TypeormExtensionError } from '../../errors'; | ||
|
||
type TypeormRelationLookupErrorOptions = { | ||
message: string, | ||
relation: string, | ||
columns: string[] | ||
}; | ||
|
||
export class EntityRelationLookupError extends TypeormExtensionError { | ||
/** | ||
* The property name of the relation. | ||
*/ | ||
public relation: string; | ||
|
||
/** | ||
* The property names of the join columns. | ||
*/ | ||
public columns: string[]; | ||
|
||
constructor(options: TypeormRelationLookupErrorOptions) { | ||
super(options.message); | ||
|
||
this.relation = options.relation; | ||
this.columns = options.columns; | ||
} | ||
|
||
static notReferenced(relation: string, columns: string[]) { | ||
return new EntityRelationLookupError({ | ||
message: `${relation} entity is not referenced by ${columns.join(', ')}`, | ||
relation, | ||
columns, | ||
}); | ||
} | ||
|
||
static notFound(relation: string, columns: string[]) { | ||
return new EntityRelationLookupError({ | ||
message: `Can't find ${relation} entity by ${columns.join(', ')}`, | ||
relation, | ||
columns, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './error'; | ||
export * from './property-names'; | ||
export * from './metadata'; | ||
export * from './join-columns'; | ||
export * from './uniqueness'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import type { ObjectLiteral } from 'rapiq'; | ||
import type { DataSource, EntityTarget, FindOptionsWhere } from 'typeorm'; | ||
import { useDataSource } from '../../data-source'; | ||
import { EntityRelationLookupError } from './error'; | ||
import { getEntityMetadata } from './metadata'; | ||
|
||
type EntityRelationColumnsValidateOptions<T> = { | ||
dataSource?: DataSource, | ||
entityTarget: EntityTarget<T>, | ||
}; | ||
|
||
/** | ||
* Validate join columns of a given entity. | ||
* It will look up and append the referenced entities to the input entity. | ||
* | ||
* @experimental | ||
* @param entity | ||
* @param options | ||
*/ | ||
export async function validateEntityJoinColumns<T extends ObjectLiteral>( | ||
entity: Partial<T>, | ||
options: EntityRelationColumnsValidateOptions<T>, | ||
) { | ||
const dataSource = options.dataSource || await useDataSource(); | ||
const entityMetadata = await getEntityMetadata(options.entityTarget, dataSource); | ||
|
||
const relations : Partial<T> = {}; | ||
for (let i = 0; i < entityMetadata.relations.length; i++) { | ||
const relation = entityMetadata.relations[i]; | ||
|
||
const where : FindOptionsWhere<ObjectLiteral> = {}; | ||
const columns : string[] = []; | ||
for (let j = 0; j < relation.joinColumns.length; j++) { | ||
const joinColumn = relation.joinColumns[j]; | ||
if (typeof entity[joinColumn.propertyName] === 'undefined') { | ||
continue; | ||
} | ||
|
||
if (joinColumn.referencedColumn) { | ||
where[joinColumn.referencedColumn.propertyName] = entity[joinColumn.propertyName]; | ||
columns.push(joinColumn.propertyName); | ||
} else { | ||
throw EntityRelationLookupError.notReferenced( | ||
relation.propertyName, | ||
[joinColumn.propertyName], | ||
); | ||
} | ||
} | ||
|
||
if (columns.length === 0) { | ||
continue; | ||
} | ||
|
||
const repository = dataSource.getRepository(relation.type); | ||
const item = await repository.findOne({ | ||
where, | ||
}); | ||
|
||
if (!item) { | ||
throw EntityRelationLookupError.notFound(relation.propertyName, columns); | ||
} | ||
|
||
relations[relation.propertyName as keyof T] = item as T[keyof T]; | ||
} | ||
|
||
const relationKeys = Object.keys(relations); | ||
for (let i = 0; i < relationKeys.length; i++) { | ||
const relationKey = relationKeys[i]; | ||
|
||
entity[relationKey as keyof T] = relations[relationKey] as T[keyof T]; | ||
} | ||
|
||
return entity; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Repository } from 'typeorm'; | ||
import type { | ||
DataSource, EntityMetadata, EntityTarget, | ||
ObjectLiteral, | ||
} from 'typeorm'; | ||
import { useDataSource } from '../../data-source'; | ||
|
||
/** | ||
* Receive metadata for a given repository or entity-target. | ||
* | ||
* @experimental | ||
* @param input | ||
* @param dataSource | ||
*/ | ||
export async function getEntityMetadata<T extends ObjectLiteral>( | ||
input: Repository<T> | EntityTarget<T>, | ||
dataSource?: DataSource, | ||
): Promise<EntityMetadata> { | ||
if (input instanceof Repository) { | ||
return input.metadata; | ||
} | ||
|
||
dataSource = dataSource || await useDataSource(); | ||
|
||
const index = dataSource.entityMetadatas.findIndex( | ||
(entityMetadata) => entityMetadata.target === input, | ||
); | ||
|
||
if (index === -1) { | ||
throw new Error(`The entity ${input} is not registered.`); | ||
} | ||
|
||
return dataSource.entityMetadatas[index]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type { ObjectLiteral } from 'rapiq'; | ||
import { Repository } from 'typeorm'; | ||
import type { DataSource, EntityMetadata, EntityTarget } from 'typeorm'; | ||
import { getEntityMetadata } from './metadata'; | ||
|
||
/** | ||
* Get (relation-) property names of a given entity. | ||
* | ||
* @experimental | ||
* @param input | ||
* @param dataSource | ||
*/ | ||
export async function getEntityPropertyNames<T extends ObjectLiteral>( | ||
input: EntityTarget<T> | Repository<T>, | ||
dataSource?: DataSource, | ||
) : Promise<string[]> { | ||
let entityMetadata : EntityMetadata; | ||
if (input instanceof Repository) { | ||
entityMetadata = input.metadata; | ||
} else { | ||
entityMetadata = await getEntityMetadata(input, dataSource); | ||
} | ||
|
||
const items : string[] = []; | ||
|
||
for (let i = 0; i < entityMetadata.columns.length; i++) { | ||
items.push(entityMetadata.columns[i].propertyName); | ||
} | ||
|
||
for (let i = 0; i < entityMetadata.relations.length; i++) { | ||
items.push(entityMetadata.relations[i].propertyName); | ||
} | ||
|
||
return items; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { FilterComparisonOperator } from 'rapiq'; | ||
import type { FiltersParseOutputElement } from 'rapiq'; | ||
import { Brackets } from 'typeorm'; | ||
import type { | ||
DataSource, EntityTarget, ObjectLiteral, WhereExpressionBuilder, | ||
} from 'typeorm'; | ||
import { useDataSource } from '../../data-source'; | ||
import { applyFiltersTransformed, transformParsedFilters } from '../../query'; | ||
import { pickRecord } from '../../utils'; | ||
import { getEntityMetadata } from './metadata'; | ||
|
||
type EntityUniquenessCheckOptions<T> = { | ||
entityTarget: EntityTarget<T>, | ||
entity: Partial<T>, | ||
entityExisting?: Partial<T>, | ||
dataSource?: DataSource | ||
}; | ||
|
||
function transformUndefinedToNull<T>(input: undefined | T) : T { | ||
if (typeof input === 'undefined') { | ||
return null as T; | ||
} | ||
|
||
return input; | ||
} | ||
|
||
function applyWhereExpression( | ||
qb: WhereExpressionBuilder, | ||
data: Record<string, any>, | ||
type: 'source' | 'target', | ||
) { | ||
const elements : FiltersParseOutputElement[] = []; | ||
|
||
const keys = Object.keys(data); | ||
for (let i = 0; i < keys.length; i++) { | ||
elements.push({ | ||
key: keys[i], | ||
value: transformUndefinedToNull(data[keys[i]]), | ||
operator: type === 'target' ? | ||
FilterComparisonOperator.EQUAL : | ||
FilterComparisonOperator.NOT_EQUAL, | ||
}); | ||
} | ||
|
||
const queryFilters = transformParsedFilters(elements, { | ||
bindingKey(key) { | ||
if (type === 'source') { | ||
return `filter_source_${key}`; | ||
} | ||
|
||
return `filter_target_${key}`; | ||
}, | ||
}); | ||
|
||
applyFiltersTransformed(qb, queryFilters); | ||
|
||
return queryFilters; | ||
} | ||
|
||
/** | ||
* Check if a given entity does not already exist. | ||
* Composite unique keys on a null column can only be present once. | ||
* | ||
* @experimental | ||
* @param options | ||
*/ | ||
export async function isEntityUnique<T extends ObjectLiteral>( | ||
options: EntityUniquenessCheckOptions<T>, | ||
) : Promise<boolean> { | ||
const dataSource = options.dataSource || await useDataSource(); | ||
|
||
const metadata = await getEntityMetadata(options.entityTarget, dataSource); | ||
|
||
const repository = dataSource.getRepository(metadata.target); | ||
|
||
const primaryColumnNames = metadata.primaryColumns.map((c) => c.propertyName); | ||
|
||
for (let i = 0; i < metadata.ownUniques.length; i++) { | ||
const uniqueColumnNames = metadata.ownUniques[i].columns.map( | ||
(column) => column.propertyName, | ||
); | ||
|
||
const queryBuilder = repository.createQueryBuilder('entity'); | ||
queryBuilder.where(new Brackets((qb) => { | ||
applyWhereExpression(qb, pickRecord(options.entity, uniqueColumnNames), 'target'); | ||
})); | ||
|
||
queryBuilder.andWhere(new Brackets((qb) => { | ||
if (options.entityExisting) { | ||
applyWhereExpression(qb, pickRecord(options.entityExisting, primaryColumnNames), 'source'); | ||
} | ||
})); | ||
|
||
const entity = await queryBuilder.getOne(); | ||
if (entity) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './entity'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
export * from './errors'; | ||
export * from './query'; | ||
export * from './cli/commands'; | ||
export * from './database'; | ||
export * from './data-source'; | ||
export * from './env'; | ||
export * from './database'; | ||
export * from './helpers'; | ||
export * from './seeder'; | ||
export * from './utils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export function pickRecord(data: Record<string, any>, keys: string[]) { | ||
const output : Record<string, any> = {}; | ||
for (let i = 0; i < keys.length; i++) { | ||
output[keys[i]] = data[keys[i]]; | ||
} | ||
|
||
return output; | ||
} |
Oops, something went wrong.