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.
Related
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!
I'm getting a flow error flow: property replace (Property not found in Array) when I'm calling replace on my array. How do I tell flow it is a mobx observable array? I already made the change to my flowconfig [libs] to include mobx
/* #flow */
import { observable } from 'mobx'
export default class GiphyStore {
#observable images = []
async getImageList() {
try {
// make axios network request
const imgs = response.data.data.map(item => {
return { id: item.id, url: item.images.downsized.url }
})
this.images.replace(imgs) // getting error???
} catch (e) {}
}
}
According to the test file provided by mobx you need to;
Use IObservableArray<> type for your arrays
Define your observable without the decorators as flow does not support them yet.
It was a joint effort of finding the answer, thanks a lot.
Let's say I have a service getting a list of properties:
export class PropertyService {
private properties: any;
constructor(#Inject(Http) http) {
this.http.get('/properties.json')
.map((res:Response) => res.json())
.subscribe(
data => this.properties = data
);
}
getProperty(property: string) {
this.properties[property];
}
}
The problem is that properties is not yet loaded when getProperty is called.
How can I make getProperty to wait for the subscription to populate the array?
I would rather not return a subscription to the components.
EDIT:
I tried paulpdaniels answer and worked with pluck. But I got stuck pretty fast.
So basically I have this PropertyService which returns a HOST.
I have an ApiService that uses that host to do another ajax call to get data.
export class PropertyService {
private properties: any;
constructor(#Inject(Http) http) {
this.properties = http.get('/properties.json')
.map((res:Response) => res.json()).cache(1);
}
getProperty(property: string) {
this.properties.pluck(property);
}
}
export class ApiService {
private http: Http;
private host: string;
constructor(#Inject(Http) http, #Inject(PropertyService) propertyService) {
this.http = http;
this.host = propertyServiceService.getProperty("API.URL"); //Is a subscription
}
getData(): any {
//Here I must subscribe to host, and once available,
//use host to do an ajax call that returns another Observable for the component
}
}
How to realize this?
Short answer is you can't, at least not without introducing some level of asynchronicity to your service.
The simple fact is there is no way to force blocking behavior and wait for the Observable to complete, you should return an Observable which your components should then also know how to consume.
export class PropertyService {
private properties: Observable<any>;
constructor(#Inject(Http) http) {
this.properties = this.http.get('/properties.json')
.map((res:Response) => res.json())
//This will hold onto a cached value of the result
//without re-executing
.cache(1);
}
getProperty(property: string) {
this.properties.pluck(property);
}
}
Edit 1
As Frank Herbert once wrote (paraphrasing) "the stream must flow!". If you need to use nested Observables operators like flatMap allow you to flatten those into a single stream, which you can continue to chain.
export class ApiService {
private http: Http;
private host: Observable<string>;
constructor(#Inject(Http) http, #Inject(PropertyService) propertyService) {
this.http = http;
this.host = propertyServiceService.getProperty("API.URL"); //Is an Observable
}
getData(): Observable<any> {
return this.host.flatMap(url => this.http.get(url), (_, res) => res.json());
}
}
What you can do is to return properties at observable itself and then in destination service start from that properties observable and .flatMap a http request after that. That means introducing more asynchronicity as was mentioned in other answers. Othr solution would be to resolve properties before app / component really start executing by retrieving properties in canActivate router guard. In this case you can be sure that when the component calls method of other service which depends on properties being available synchronosuly the properties were already resolved.
I am trying to use angular2-in-memory-web-api in my angular 2 project. I'm having trouble finding any documentation that shows how to return different data objects for different requests with the same collection name. I believe the class I use for my SEED_DATA is off but I'm not sure how to structure it the right way to get what I want.
Here is my main.ts
import {provide} from "#angular/core";
import {bootstrap} from "#angular/platform-browser-dynamic";
import {HTTP_PROVIDERS, XHRBackend} from "#angular/http";
import {AppComponent} from "./app.component";
import {SEED_DATA, InMemoryBackendService} from "angular2-in-memory-web-api/in-memory-backend.service";
import {AppTestData} from "./AppTestData";
bootstrap(AppComponent, [
HTTP_PROVIDERS,
provide(XHRBackend, {useClass: InMemoryBackendService}),
provide(SEED_DATA, {useClass: AppTestData})
]);
My AppTestData.ts file would look something like this
export class AppTestData {
createDb() {
let studentData = {
name: "Student Name",
grade: "B"
};
let otherStudentData = {
name: "Other Student Name",
grade: "A"
};
let httpPaths = {
somepath: {
student: studentData,
anotherPath: {
student: otherStudentData
}
}
}
return httpPaths;
}
}
My attempt at the httpPaths object is off. But the idea would be me calling a get http call to "something/student" and getting back studentData and calling another get http call to "something/anotherPath/student" and getting back otherStudentData.
The following does seem to work but I would like to specify my complete path incase I want post to “something/student” and “something/anotherPath/student” and get different results.
let httpPaths = {
student: studentData
}
I believe you missed the brackets in the return statement:
return {httpPaths};
The developer created an example which can be found here
If you want to "mock" two different databases you have to follow this syntax:
export class AppTestData {
createDb() {
let heroes = {
name: "Batman",
city: "Gotham"
};
let countries = {
name: "America",
code: "GR"
};
return {heroes,countries};
}
}
In your service you can consume it with a Promise and the url for the http request would be :
private httpUrl = '../contacts/contacts';
My file structure is something like:
repo
-app
-shared
myservice.ts // this consumes the data from the InMemory DB
data.service.db.ts // this implements the InMemoryDbService
...
At least that's how I solved the problem.
Indeed the official documentation is poor so far.
Okay, so I am new to working with HTTP and actually getting some data from the server. Been sifting through a lot of tutorials, examples and questions asked here, but I am not finding what I want. All tutorials I've found only shows how to retrieve and add some data.
So based on those examples I've managed to retrieve data:
service:
getCases(){
return this.http.get('someUrl');
}
Case component constructor:
this._service.getCases()
.map((res: Response) => res.json())
.subscribe(cases => this.cases = cases);
Adding cases
service:
public saveCase(case: case) {
let body = JSON.stringify(case);
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post('someUrl', body, options)
.map(this.extractData)
.catch(this.handleError)
.subscribe(case => this.cases.push(case));
}
Case Component:
saveCase() {
let case = new Case(this.id, this.name, this.date)
this._service.saveCase(case);
this.name = '';
}
Okay, so I have and Array "Cases" which contains Case objects. Getting the data from the server displays the cases like I want them to. When I add a new case it gets sent to the server, but how do I get the Array updated when I add a new Case. Because now the new case appears only after I refresh the browser.
Second question is that the user can click a case and it then routes to a detail list where the user can add steps and feedback. If it matters, case has the attributes id, name, date and an array of steps, at this point the array is empty. The step object is it's own class and the object contains an array of feedback. Feedback is also an own class and the object has two attributes, which are both strings. So it's all nested. When I click the case, it does route to the detail page, but there the case name should be printed and it doesn't. It also shows my button for adding steps, but it does nothing. Obviously I'm missing something in my methods, but I have no clue to as what to do. As a comment I can say that before adding the http in my code it all worked as it should. Here are the methods, that are probably missing something:
Case Component:
gotoDetail(case: Case) {
this._router.navigate(['CaseDetail', {"id": case.name}]);
}
Service:
public getById(id: string): Case {
for (let case of this.cases) {
if (case.id === id) {
return case;
}
}
return null;
}
Then there is the matter of syntax for removing cases, haven't found an example that works for me yet, I've tried a bunch... among others the example links provided by #shershen below. None works. The original methods I have, that should be changed to work with http:
Component:
removeSearchCase(case: Case) {
this._service.removeCase(case);
}
Service:
public removeCase(value: Case): void {
let index = this.cases.indexOf(value);
this.cases.splice(index, 1);
}
So the case removal is with post.
And about the backend I can say as much that I only have the following three posts and gets:
getCases (GET), saveCase (also works as updating the case)(POST) and removeCase (POST).
It's hard to debug without sample demo, however the descriptions quite detailed. I am adding some points that may fix the problem while improving the code structure:
First, you should move the request subscription/observing into the service methods; that will encapsulate the request handling logic in service layer:
//service.ts
#Injectable()
export class service {
getCases(){
if (!this.request) {
this.request = this.http.get('/assets/data.json')
.map((response: Response) => response.json())
.map((data: string[]) => {
this.request = null;
return this.names = data;
});
}
return this.request;
}
}
Second, you need to create an instance of your service in your Component's constructor instead of using it as a static method of the service:
//component.ts
import {MyService} from 'PATH_TO_YOUR_SERVICE';
class CaseComponent {
constructor(private _service : MyService){
//other stuff..
}
getData(){
this._service.getCases()
}
}
Additional references:
Official "Getting and Saving Data with HTTP"
Service example with Observables (with Firebase, but still)
Simple service in Angular2 seed project
I think you should put your cases Array in the CaseComponent:
case.component.ts:
cases: Case[];
constructor(private caseService: CaseService){}
getCases() {
this.caseService.getCases()
.subscribe(cases => this.cases = cases);
}
saveCase() {
let case = new Case(this.id, this.name, this.date);
this.caseService.saveCase(case)
.subscribe(case => this.cases = [...this.cases, case]);
}
case.service.ts:
getCases() {
return this.http.get(this.casesUrl)
.map(this.extractData)
.catch(this.handleError);
}
saveCase (case: Case) {
let body = JSON.stringify({ case });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.casesUrl, body, options)
.map(this.extractData)
.catch(this.handleError);
}
Then try to change "name" to "id" in gotoDetail:
gotoDetail(case: Case) {
this._router.navigate(['CaseDetail', {"id": case.id}]);
}