Mikro-orm order by ST_Distance_Sphere using MySQL driver - mikro-orm

With MySQL, I am trying to order by ST_Distance_Sphere using QueryBuilder.
I have a entity:
import { Entity, PrimaryKey, Property } from "mikro-orm";
#Entity({ tableName: "studio" })
export default class StudioEntity {
#PrimaryKey()
public id!: number;
#Property()
public name!: string;
#Property({ columnType: "point srid 4326" })
public geometry!: object;
}
And I am trying:
export default class StudioStore {
private studioRepository: EntityRepository<StudioEntity>;
public constructor(ormClient: OrmClient) {
this.studioRepository = ormClient.em.getRepository(StudioEntity);
}
public async findPage(first: number): Promise<StudioEntity[]> {
const query = this.studioRepository.createQueryBuilder().select("*");
query.addSelect(
"ST_Distance_Sphere(`e0`.`geometry`, ST_GeomFromText('POINT(28.612849 77.229883)', 4326)) as distance",
);
query.orderBy({ distance: "ASC" });
return query.limit(first).getResult();
}
}
But I get a ORM error:
Trying to query by not existing property StudioEntity.distance
So, I try to add a property to the entity:
#Property({ persist: false })
public distance?: number;
But now I get a MySQL error:
Unknown column 'e0.distance' in 'order clause'
This is the generated SQL query:
[query] select `e0`.*, ST_Distance_Sphere(`e0`.`geometry`, ST_GeomFromText('POINT(28.612849 77.229883)', 4326)) as distance from `studio` as `e0` order by `e0`.`distance` asc limit 5 [took 4 ms]

You will need to fallback to knex, as QB currently supports only defined property fields in order by. You will also need to define that virtual distance property as you already did, so the value can be mapped to the entity.
https://mikro-orm.io/docs/query-builder/#using-knexjs
const query = this.studioRepository.createQueryBuilder().select("*");
query.addSelect("ST_Distance_Sphere(`e0`.`geometry`, ST_GeomFromText('POINT(28.612849 77.229883)', 4326)) as distance");
query.limit(first);
const knex = query.getKnexQuery();
knex.orderBy('distance', 'asc');
const res = await this.em.getConnection().execute(knex);
const entities = res.map(a => this.em.map(StudioEntity, a));
Not very nice I must say, totally forgot that it is possible to order by computed fields. Will try to address this in v4. I think it could even work as your second approach, as QB could simply check if the property is virtual (has persist: false), and then it would not prefix it.
edit: as of 3.6.6 the approach with persist: false should work out of box

Related

nested route using #ApiParam, how to use a custom validator in nestjs?

I’m looking for some help about custom validator & custom decorator in Nest.
FIRST CASE : working one
A DTO, with class-validator anotations :
import { IsNotEmpty, IsString } from 'class-validator';
import { IsOwnerExisting } from '../decorators/is-owner-existing.decorator';
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#IsOwnerExisting() // custom decorator, calling custom validator, using a service to check in db
ownerEmail: string;
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
I use it in a controller :
#Controller()
#ApiTags('/polls')
export class PollsController {
constructor(private readonly pollsService: PollsService) {}
#Post()
public async create(#Body() createPollDto: CreatePollDto): Promise<Poll> {
return await this.pollsService.create(createPollDto);
}
}
When this endpoint is called, the dto is validating by class-validator, and my custom validator works. If the email doesn’t fit any user in database, a default message is displayed.
That is how I understand it.
SECOND CASE : how to make it work ?
Now, I want to do something similar but in a nested route, with an ApiParam. I’d like to check with a custom validator if the param matches some object in database.
In that case, I can’t use a decorator in the dto, because the dto doesn’t handle the "slug" property, it’s a ManyToOne, and the property is on the other side.
// ENTITIES
export class Choice {
#ManyToOne((type) => Poll)
poll: Poll;
}
export class Poll {
#Column({ unique: true })
slug: string;
#OneToMany((type) => Choice, (choice) => choice.poll, { cascade: true, eager: true })
#JoinColumn()
choices?: Choice[];
}
// DTOs
export class CreateChoiceDto {
#IsNotEmpty()
#IsString()
label: string;
#IsOptional()
#IsString()
imageUrl?: string;
}
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
#IsOptional()
#IsArray()
#ValidateNested({ each: true })
#Type(() => CreateChoiceDto)
choices: CreateChoiceDto[] = [];
}
So where should I hook my validation ?
I’d like to use some decorator directly in the controller. Maybe it’s not the good place, I don’t know. I could do it in the service too.
#Controller()
#ApiTags('/polls/{slug}/choices')
export class ChoicesController {
constructor(private readonly choicesService: ChoicesService) {}
#Post()
#ApiParam({ name: 'slug', type: String })
async create(#Param('slug') slug: string, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
}
As in my first case, I’d like to use something like following, but in the create method of the controller.
#ValidatorConstraint({ async: true })
export class IsSlugMatchingAnyExistingPollConstraint implements ValidatorConstraintInterface {
constructor(#Inject(forwardRef(() => PollsService)) private readonly pollsService: PollsService) {}
public async validate(slug: string, args: ValidationArguments): Promise<boolean> {
return (await this.pollsService.findBySlug(slug)) ? true : false;
}
public defaultMessage(args: ValidationArguments): string {
return `No poll exists with this slug : $value. Use an existing slug, or register one.`;
}
}
Do you understand what I want to do ? Is it feasible ? What is the good way ?
Thanks a lot !
If you're needing to validate the slug with your custom rules you have one of two options
make a custom pipe that doesn't use class-validator and does the validation directly in it.
Use #Param() { slug }: CreatePollDto. This assumes that everything will be sent via URL parameters. You could always make the DTO a simple one such as
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
And then use #Param() { slug }: SlugDto, and now Nest will do the validation via the ValidationPipe for you.
If it didn't work with you with service try to use
getConnection().createQueryBuilder().select().from().where()
I used it in custom decorator to make a isUnique and it works well, but niot with injectable service.
public async validate(slug: string, args: ValidationArguments): Promise<boolean> { return (await getConnection().createQueryBuilder().select(PollsEntityAlias).from(PollsEntity).where('PollsEntity.slug =:slug',{slug}))) ? true : false; }
That’s so greeat! Thanks a lot, it’s working.
I’ve tried something like that, but can’t find the good way.
The deconstructed { slug }: SlugDto, so tricky & clever ! I’ve tried slug : SlugDto, but it couldn’t work, I was like «..hmmm… how to do that… »
Just something else : in the controller method, I was using (as in documentation) #Param('slug'), but with the slugDto, it can’t work. Instead, it must be just #Param().
Finally, my method :
#Post()
#ApiParam({ name: 'slug', type: String })
public async create(#Param() { slug }: SlugDto, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
And the dto :
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
#IsSlugMatchingAnyExistingPoll()
slug: string;
}
Personally, I wouldn't register this as a class-validator decorator, because these are beyond the scopes of Nestjs's dependency injection. Getting a grasp of a service/database connection in order to check the existence of a poll would be troublesome and messy from a validator constraint. Instead, I would suggest implementing this as a pipe.
If you want to only check if the poll exists, you could do something like:
#Injectable()
export class VerifyPollBySlugPipe implements PipeTransform {
constructor(#InjectRepository(Poll) private repository: Repository<Poll>) {}
transform(slug: string, metadata: ArgumentsMetadata): Promise<string> {
let found: Poll = await repository.findOne({
where: { slug }
});
if (!found) throw new NotFoundException(`No poll with slug ${slug} was found`);
return slug;
}
}
But, since you're already fetching the poll entry from the database, maybe you can give it a use in the service, so the same pipe can work to retrieve the entity and throw if not found. I answered a similar question here, you'd just need to add the throwing of the 404 to match your case.
Hope it helps, good luck!

How to delete entities from one-to-many collection with only composite primary keys in MariaDB

#Entity()
export class Job {
#PrimaryKey({ type: BigIntType })
id: string;
#OneToMany(() => JobExperienceLevel,
jobExperienceLevel => jobExperienceLevel.job, {cascade: Cascade.ALL], orphanRemoval: true})
experienceLevels = new Collection<JobExperienceLevel>(this);
}
#Entity()
export class JobExperienceLevel {
#PrimaryKey()
#Enum({
items: () => JobExperienceLevelType
})
experienceLevel: JobExperienceLevelType;
#ManyToOne({nullable:false, primary: true, joinColumn: 'job_id'})
job: Job;
}
export enum JobExperienceLevelType {
ENTRY_LEVEL = 'ENTRY_LEVEL',
JUNIOR = 'JUNIOR',
REGULAR = 'REGULAR',
SENIOR = 'SENIOR'
}
After calling experienceLevels.removeAll() on some job entity it is generating the following query:
delete from `job_experience_level` where `experience_level` = 'SENIOR' and `job_id` is null
The database table 'job_experience_level' contains only composite primary keys (experience_level, job_id)
I have checked that before calling removeAll method there is one entity 'SENIOR' in the collection.
I am using entityrepository with persistAndFlush on the job entity.
The problem is that this query is wrong and it should populate the correct job_id.
I also tried to remove the #PrimaryKey() from experienceLevel property, but then there is no delete query in the transaction at all.
As discussed in the comments, there was a bug with orphan removal and composite keys. Upgrade to v3.6.7 to fix it.
https://github.com/mikro-orm/mikro-orm/blob/master/CHANGELOG.md#367-2020-04-16
Here is a testcase to make sure it really works:
https://github.com/mikro-orm/mikro-orm/commit/94c71c89a648e03fad38a93cced7fa92bbfd7ff7#diff-595473b980e4d4384f667f60dbddde2aR1

Addtional parameter with #ngrx/entity

I want to keep employee array and page loading status in store state. So my initial state will look like this
const initialState = {
isLoading: false,
employees: []
};
Now i want to use #ngrx/entity for employee instead of array. The documentation only show the demo for using entity with entire state.
How can i use entity for only one property rather than entire state?
If it's not possible what is the alternative for above scenario?
See the docs for an example:
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
export interface User {
id: string;
name: string;
}
export interface State extends EntityState<User> {
// additional entities state properties
selectedUserId: number;
}
export const adapter: EntityAdapter<User> = createEntityAdapter<User>();

How to properly query a Firebase list in AngularFire2?

I'm developing an Angular2 application with Firebase as a backend. On a subpage I want to display some tasks for a given week which is specified by a route parameter.
I'm using a BehaviorSubject as a parameter to the AngularFire2 query the following way:
export class PrepareComponent implements OnInit {
private routeSub: any;
weekId = '';
private weekSubject: BehaviorSubject<any> = new BehaviorSubject('weekId');
taskTemplates$: FirebaseListObservable<TaskTemplate[]>;
constructor(private route: ActivatedRoute,
private router: Router,
private angularFire: AngularFire) {
// this.taskTemplates$ = angularFire.database.list("/taskTemplates");
Here is the Firebase query:
this.taskTemplates$ = angularFire.database.list("/taskTemplates", {
query: {
equalTo: this.weekSubject
}
});
}
ngOnInit() {
this.routeSub = this.route.params.map(
(params: Params) => this.weekId = params[ 'weekid' ]
).subscribe(
weekId => this.weekSubject.next(weekId)
);
}
ngOnDestroy() {
this.routeSub.unsubscribe();
}
}
Unfortunately, the Firebase taskTemplates$ observable is not returning any data for the given weekId.
I assumed that once the weekId will be set by querying the route parameters, the list would get it as a query parameter and return the data which has: { weekId: actualWeekId, ...}.
EDIT Added an example of data stored in Firebase:
{
"-Kc_E0U4UOl9PPtxpzCM" : {
"description" : "asdfasdf",
"weekId" : "99f2"
},
"-Kc_E3wv3fhpUt56sM4u" : {
"description" : "another task",
"weekId" : "99f2"
}
}
So what I want to do is to get all records for a given weekId
If anyone is having a hard time like me nowadays, this might help you guys:
db.list('/items', ref => ref.orderByChild('size').equalTo('large'))
Please refer to
https://github.com/angular/angularfire2/blob/master/docs/rtdb/querying-lists.md
for more information!
The core problem appears to be the query that's specified when the list observable is created.
Given the structure of the data, it seems that orderByChild is what should be used, as you appear to be wanting to select the entries that have a particular weekId.
The query in the question's code will be looking for entries that have keys equal to the weekid obtained from the activated route. However, the keys are push keys, like -Kc_E0U4UOl9PPtxpzCM.
You can also simplify the code somewhat by composing the query observable directly from the activated route. Something like this should do what you want:
export class PrepareComponent implements OnInit {
taskTemplates$: FirebaseListObservable<TaskTemplate[]>;
constructor(
private route: ActivatedRoute,
private router: Router,
private angularFire: AngularFire
) {}
ngOnInit() {
this.taskTemplates$ = this.angularFire.database.list("/taskTemplates", {
query: {
orderByChild: 'weekId',
equalTo: this.route.params.map((params: Params) => params['weekid'])
}
});
}
}
If you are using orderByChild you will likely see a console warning about an index - if you've not already created one. You need to use the security rules to do that. It should be explained well enough in the documentation.

Redux - cross-entity state management

I'm using Redux and ImmutableJS to manage the state of my app. I've created the following two Records:
export const OrderRecord = Record({
id: null,
productId: null,
amount: 1,
});
export const ProductRecord = Record({
id: null,
name: '',
price: 0,
});
My global state is normalized based on the normalizr approach like this:
const state = {
entities: {
orders: new OrderedMap(new Map({
1: new OrderRecord(createOrderItem(1, 1)),
})),
products: new OrderedMap(new Map({
1: new ProductRecord(createProductItem(1)),
})),
},
};
I'm using this specification for testing purposes.
Now I'm trying to make some selects with computed fields using Reselect.
export const getVisibleOrders = createSelector(
[getProducts, getOrders],
(products, orders) => {
orders.map(order => {
const product = products.get(order.productId.toString());
if (!product) {
return order;
}
const totalPrice = order.amount * product.price;
order.set('productName', product.name);
order.set('totalPrice', totalPrice);
return order;
});
}
);
, but I get the following error message:
Error: Cannot set unknown key "productName" on Record
I know the reason - Record cannot contain any undefined keys, but my question is: Is there any suggested approach how gracefully solved this problem?
I don't want to extend my Records to support this kind of computed parameters (product.name and totalPrice).
I don't want to keep the static and computed parameters in one place, because for example the 'productName' parametr is from "Product" entity and not from "Order" entity.
Thank you.
The whole point of using Immutable.Record is to not let you add new keys to your record, hence the error message you get. And the whole point of selectors is to expose such "computed" property if you want to consume them outside. In your case, you can simply return a new Map() object or a new record type if you need to use the dotted syntax :
return Map({
productName: 'foobar'
}).merge(order)

Resources