I am using loopback 4 and trying to configure the Model annotation with properties to configure how the collection is created in Mongo.
I have a Model called say Client and I want the collection in Mongo to be called Clients. The cross over with documentation is confusing, as they reference the properties from v3 in v4 docs.
I have tried this:
import {Entity, model, property} from '#loopback/repository';
#model({
settings: {strict: false},
name: 'client',
plural: 'clients',
options: {
mongodb: {
collection: 'clients',
},
},
})
export class Client extends Entity {
#property({
type: 'string',
id: true,
defaultFn: 'uuidv4',
index: true,
})
id: string;
#property({
type: 'string',
required: true,
})
name: string;
#property({
type: 'string',
})
code?: string;
constructor(data?: Partial<Client>) {
super(data);
}
}
With no Joy, still creates the collection as the Class name Client
This is from 2014, but perhaps it still works. Try not putting the mongodb key options
settings: {strict: false},
name: 'client',
plural: 'clients',
mongodb: {
collection: 'clients',
},
Please note that all model settings must be nested inside settings property, LB4 does not support top-level settings yet.
Also the option plural is not used by LB4 as far as I know.
I think the following code should work for you:
#model({
name: 'client',
settings: {
strict: false
mongodb: {
collection: 'clients',
},
},
})
export class Client extends Entity {
// ...
}
UPDATE: I opened a GitHub issue to discuss how to make #model decorator easier to use for users coming from LB3. See https://github.com/strongloop/loopback-next/issues/2142
Related
Sometimes I may need to use single validator class for both inserting and updating resources, as opposed to this statement. Otherwise I may have duplicate codes, which inherently goes against DRY principle.
Consider the following case:
Say, I have a products resource in my app, and users of my app can create, update and delete products. Assume that the product model looks something like this:
export default class Product extends BaseModel {
#column({ isPrimary: true })
public id: number
#column()
public code: string
#column()
public title: string
#column()
public description: string
#column()
public price: number
}
Certainly the migration will be very close to the following:
export default class ProductsSchema extends BaseSchema {
protected tableName = 'products'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id').primary()
table.string('code').unique().notNullable() // <= pay attention that this field is unique
table.string('title').notNullable()
table.string('description', 25).notNullable()
table.double('price').notNullable()
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}
Now users will create a new product. So they will be presented a form, and the validation may look something like:
export default class ProductCreateValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
code: schema.string({ trim: true, escape: true }, [
rules.unique({ table: 'products', column: 'code' }),
]), // <= because this field is unique inside the database
title: schema.string({ trim: true, escape: true }, [
rules.alpha({ allow: ['space'] }),
]),
description: schema.string({ trim: true, escape: true }),
price: schema.number(),
})
public cacheKey = this.ctx.routeKey
public messages = {}
}
The fun begins now! If I create separate class for updating products, most the fields will be the same, except code. So I'll have to duplicate the whole class:
export default class ProductUpdateValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
code: schema.string({ trim: true, escape: true }, [
// rules.unique({ table: 'products', column: 'code' }),
]), // <= I cannot apply unique rule here - because I won't be able to update anymore
title: schema.string({ trim: true, escape: true }, [
rules.alpha({ allow: ['space'] }),
]),
description: schema.string({ trim: true, escape: true }),
price: schema.number(),
})
public cacheKey = this.ctx.routeKey
public messages = {}
}
What if I want to add 3 more fields? With this current setup, I'd have to go to these two class files and add those fields in both of them. And I'd have to go to both these files if I want to adjust some validation logic. It'd be much easier to maintain if I'd be able to use single class for both create and update actions; and it'd automatically cancel the uniqueness check if the particular field of the product that users trying to update hasn't been changed. How could that even possible?
It's very easy to achieve. We need to drop one validator class and modify the other one like so:
export default class ProductValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
code: schema.string({ trim: true, escape: true }, [
rules.unique({
table: 'products',
column: 'code',
whereNot: {
id: this.ctx.request.input('id') || 0 // <= or this may come from route params: this.ctx.params.id
}
}),
]),
title: schema.string({ trim: true, escape: true }, [
rules.alpha({ allow: ['space'] }),
]),
description: schema.string({ trim: true, escape: true }),
price: schema.number(),
})
public cacheKey = this.ctx.routeKey
public messages = {}
}
Let's break this code down:
For inserting new product, the this.ctx.request.input('id') will be undefined, so it'll fallback to 0. So it'll perform SELECT code FROM products WHERE code = ? AND NOT id = ? query with ['<whatever_user_types>', 0]. Since id is the primary key for that table and it cannot be 0, the later condition of the query above will always be TRUE. So the validator will only throw error if the code is found (since the later part is already TRUE). Hence our objective is fulfilled.
For updating existing product, you'll certainly have the ID of the product in hand. Because you're fetching the product from the database, you certainly know its ID. Now put it somewhere of your choice (either inside the update form as <input type="hidden" name="id" value="{{ product.id }}"> or as route param /products/:id/update). Since we have the ID this time around, the this.ctx.request.input('id') (or this.ctx.params.id) will be set. So the query will look like SELECT code FROM products WHERE code = ? AND NOT id = ? query with ['<whatever_user_types>', <product_id>]. This time, the later condition of the query will always be FALSE, so it won't complain if the code matches only with the product we're trying to update and not with any other products. Bingo!
So this is how you can avoid code duplication by utilizing single validator for both create and update actions. Let me know down in comments if you have any other questions.
As said in the title, I'm using Django, GraphQL, Apollo and VueJS in my project.
I'm developping it as a SPA (Single Page Application).
Everything works fine, until I hit the F5 button and refresh the page. Indeed, it shows an unknown page. The thing is it is VueRouter that is managing the SPA and it works fine. But when I press F5, that is Django that tries to serve a page for the current URL and since it doesn't know it, it can't serve the appropriate page.
I know I can set the VueRouter 'history' mode, which I did, and add a URL to Django that serves index.html whatever the URL is.
My problem is the following :
When I'm on a particular form view (i.e : a User form view) my URL is the following :
http://localhost:8000/user
Since I'm using GraphQL for my API, the retrieved data is not based on the URL. In fact, that is my VueJS component that says : Hey Apollo, run that GraphQL to retrieve the User I want.
So when I refresh, yes it serves the User form view..but empty.
The question is : How could I solve this ?
For clarification purposes, here are some code samples :
My Django URLs :
# (... other imports here ...)
from .schema import schema
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), # 'schema' is the main GraphQL schema
path('', TemplateView.as_view(template_name='index.html')),
re_path(r'^.*$', TemplateView.as_view(template_name='index.html')) # I saw that many times to serve the page whatever the URL is when refreshing the page
]
My Vue Router :
export default new Router({
mode: 'history',
routes: [
{ path: '/', name: 'MainApp' },
// ...
{ path: '/users', name: 'UserList', component: UserList },
{ path: '/user/create', name: 'UserFormCreate', component: UserForm, props: true },
{ path: '/user', name: 'UserFormView', component: UserForm, props: true },
{ path: '/user/edit', name: 'UserFormEdit', component: UserForm, props: true },
// Same pattern for other models like 'Group' ...
]
My Example VueJS Component :
<script>
import {
// ...
USER_QUERY,
// ...
} from '../../graphql/base/user.js'
export default {
name: 'UserForm',
props: {
userId: Number,
editing: {
type: Boolean,
default: false
}
},
apollo: {
user: {
query: USER_QUERY,
variables () { return { id: this.userId } },
skip () { return this.userId === undefined },
result ({ data }) {
this.form.username = data.user.username
this.form.firstName = data.user.firstName
this.form.lastName = data.user.lastName
}
}
},
data () {
return {
form: {
username: '',
password: '',
firstName: '',
lastName: ''
},
// ...
}
},
methods: {
// ...
}
I have to mention that I've seen more or less related topics but that doesn't solve my problem.
Thanks in advance for your help !
Edit your route paths to use params. For example:
{ path: '/user/:userId', name: 'UserFormView', component: UserForm, props: true }
Now, the app will interpret any number following the user/ path as a prop called userId. (props: true is important here for using the params as props.)
The only other change you need to make is adjusting your router-links to include the id as well (Ex.: http://localhost:8000/user/1) so that when the page is refreshed, there will be a param to read.
It is the first time that I use this version (4) for development and I have a problem with loopback and mongodb indexing.
Of the two ids that are inside the db loopback it does not collect any.
It's a problem of the API or DB?
Model [Loopback]
import { Entity, model, property } from '#loopback/repository';
#model()
export class Ad extends Entity {
#property({
type: 'number',
id: true,
required: true,
})
id: number;
<...>
constructor(data?: Partial<Ad>) {
super(data);
}
}
Data on Mongo:
{
"_id": {
"$oid": "5c0e9c7730146d2448746834"
},
"id": 110722,
"creation_date": 1492075600000,
"update_date": 1492075921000,
...
}
Response on loopback GET /ads
[{
"id": null,
"creation_date": 1492075600000,
"update_date": 1492075921000,
...
},...]
Hello from the LoopBack team :)
I don't see any obvious problem in the code snippets you posted. What happens when you change id's type from number to string? Will it fix the problem?
Most likely, you have found a bug in LoopBack 4. Please report it via GitHub: https://github.com/strongloop/loopback-next/issues
I fixed the same issue by changing the id=String. Mongodb id contains both string and number. So, if you change the type of id=string (Model) the issue will be fixed.
I'm building an adapter to wrap the Keen.io API, so far I've been able to successfully load the projects resource, however the returned object looks like this:
{
partners_url: "/3.0/projects/<ID>/partners",
name: "Project Name",
url: "/3.0/projects/<ID>",
saved_queries: [ ],
events_url: "/3.0/projects/<ID>/events",
id: "<ID>",
organization_id: "<ORG ID>",
queries_url: "/3.0/projects/<ID>/queries",
api_key: "<API KEY>",
events: [
{
url: "/3.0/projects/<ID>/events/user_signup",
name: "user_signup"
},
{
url: "/3.0/projects/<ID>/events/user_converted",
name: "user_converted"
},
{
url: "/3.0/projects/<ID>/events/user_created_project",
name: "user_created_project"
}
]
}
Excluding a lot of cruft, Ember has no trouble mapping the name and id attributes using the RESTSerializer, but if I add an events relation to my Project model it blows up with:
Error while loading route: TypeError: Cannot set property 'store' of undefined
at Ember.Object.extend.modelFor (http://localhost:3000/assets/ember-data.js?body=1:9813:23)
at Ember.Object.extend.recordForId (http://localhost:3000/assets/ember-data.js?body=1:9266:21)
at deserializeRecordId (http://localhost:3000/assets/ember-data.js?body=1:10197:27)
at deserializeRecordIds (http://localhost:3000/assets/ember-data.js?body=1:10211:9)
at http://localhost:3000/assets/ember-data.js?body=1:10177:11
at http://localhost:3000/assets/ember-data.js?body=1:8518:20
at http://localhost:3000/assets/ember.js?body=1:3404:16
at Object.OrderedSet.forEach (http://localhost:3000/assets/ember.js?body=1:3247:10)
at Object.Map.forEach (http://localhost:3000/assets/ember.js?body=1:3402:10)
at Function.Model.reopenClass.eachRelationship (http://localhost:3000/assets/ember-data.js?body=1:8517:42)
From my investigation this seems to be because it can't find the inverse relation to map an Event back to a Project because there's no parent ID.
Is it possible to create a relation in Ember Data to support this? Or is it possible to modify the Serializer to append a projectId to each event before loading?
I'm using Ember 1.5.0-beta.4 and Ember Data 1.0.0-beta.7+canary.f482da04.
Assuming your Project model is setup the following way:
App.Project = DS.Model.extend({
events: DS.hasMany('event');
});
You need to make sure that the JSON from your API is in a certain shape that Ember-Data expects.
{
"project": {
"id": 1,
"events": ["1", "2"],
},
"events": [{
"id": "1",
"name": "foo"
}, {
"id": "2",
"name": "bar"
}]
}
You can, however, implement extractArrayin your model's serializer to transform the JSON from the server into something similar like the above example.
There's a working example and an explanation in the Ember docs.
I'm getting following (simplified) output from a RESTful API:
{products: [{
product: {
id: 1, name: "T-Shirt red"
},
images: [{
id: 1, size: 'm', url: 'http://db.co/t-shirt-red_m.jpg'
}, {
id: 2, size: 'xl', url: 'http://db.co/t-shirt-red_xl.jpg'
}]
}, {
product: {
id: 2, name: "T-Shirt blue"
},
images: [{
id: 3, size: 'm', url: 'http://db.co/t-shirt-blue_m.jpg'
}, {
id: 4, size: 'xl', url: 'http://db.co/t-shirt-blue_xl.jpg'
}]
}]}
Using Ember version 12, how should the declaration of the Product model looks like and how can I traverse the results? Haven't been able to find any example in that direction.
Following to access the data doesn't work (I just can't find the right syntax):
var products = App.Product.find(); // seems to work
var prodNames = products.getEach('product.name'); // doesn't work
var secondProd = products.getObject(1).get('name'); // doesn't work
Thanks a lot in advance!
claudio.
DS.hasMany and some options for the REST adapter may help you.
I've used a similar setup with MongoDB embedded models. I've attached some examples below.
I didn't want to try to save to the embedded array, so I've used embedded:load, but you can use embedded: 'always' to persist the total object back to the server (although it didn't work quite as I expected) with 'always' if you save the parent object.
DS.RESTAdapter.map 'App.Check',
line_items: { embedded: 'load' }
parties: { embedded: 'load' }
App.Check = DS.Model.extend
description: DS.attr("string")
modified_date: DS.attr("date")
parties: DS.hasMany('App.Party')
App.Party = DS.Model.extend
name: DS.attr("string")
check: DS.belongsTo('App.Party')
You can then reference the item. In a view, I've accessed it as below from a ArrayController where the content is set to an instance of DS.Check.
{{#each party in content.parties }}