How to use single validator class for both resource creation and modification in AdonisJS 5? - adonis.js

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.

Related

How to configure apollo cache to uniquely identify a child elements based on their parent primary key

What is the proper way to configure apollo's cache normalization for a child array fields that do not have an ID of their own but are unique in the structure of their parent?
Let's say we have the following schema:
type Query {
clients: [Client!]!
}
type Client {
clientId: ID
name: String!
events: [Events!]!
}
type Events {
month: String!
year: Int!
day: Int!
clients: [Client!]!
}
At first I thought I can use multiple keyFields to achieve a unique identifier like this:
const createCache = () => new InMemoryCache({
typePolicies: {
Event: {
keyFields: ['year', 'month', 'name'],
},
},
});
There would never be more than 1 event per day so it's safe to say that the event is unique for a client based on date
But the created cache entries lack a clientId (in the cache key) so 2 events that are on the same date but for different clients cannot be distinguished
Is there a proper way to configure typePolicies for this relationship?
For example the key field can be set to use a subfield:
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["title", "author", ["name"]],
},
},
});
The Book type above uses a subfield as part of its primary key. The ["name"] item indicates that the name field of the previous field in the array (author) is part of the primary key. The Book's author field must be an object that includes a name field for this to be valid.
In my case I'd like to use a parent field as part of the primary key
If you can't add a unique event id, then the fallback is to disable normalization:
Objects that are not normalized are instead embedded within their parent object in the cache. You can't access these objects directly, but you can access them via their parent.
To do this you set keyFields to false:
const createCache = () => new InMemoryCache({
typePolicies: {
Event: {
keyFields: false
},
},
});
Essentially each Event object will be stored in the cache under its parent Client object.

How to resolve custom nested graphql query with Apollo CacheRedirects

We are using apollo-client in a react project. We made a cursor level on top of any list queries. For example:
query MediaList($mediaIds: [ID!], $type: [MediaType!], $userId: ID!) {
user {
id
medias_cursor(all_medias: true, active: true, ids: $mediaIds) {
medias {
id
type
category
name
}
}
}
}
Now for different MediaList query, the Media Objects might already exist in cache but we can not use it to skip network query. For example:
After we query medias_cursor({"all_medias":true,"active":true,"ids":["361","362","363"]}),
we've already got the three Media objects here - (Media:361, Media:362, Media:363).
So when we try to query medias_cursor({"all_medias":true,"active":true,"ids":["361","363"]}, we should have everything we need in the cache already. But right now, the apollo default behavior will just pass the cache and hit the network.
We tried to add a cacheRedirects config to solve this problem like this:
const cache = new InMemoryCache({
cacheRedirects: {
User: {
medias_cursor: (_, { ids }, { getCacheKey }) => {
if (!ids) return undefined
return {
medias: map(ids, id => {
return getCacheKey({ __typename: 'Media', id: id })
})
}
},
},
},
})
We are expecting that the cacheRedirects would help us to use the cache when it's available, but now it will skip the cache anyway.

TypeORM: How to set ForeignKey explicitly without having property for loading relations?

I don't want to create a property for loading relation into it (as shown in all the examples). The only thing I need is to have an explicit foreign key property so that the migration will be able to create appropriate constraints for it in the database. The closest decorator to the one I need is #RelationId but it still requires the presence of a property of the relational class.
For clarity let's take the example from the documentation:
#Entity()
export class Post {
#ManyToOne(type => Category)
category: Category;
#RelationId((post: Post) => post.category) // it still requires the presence of the `category` proeprty
categoryId: number;
}
I don't need the category property here. I want to have the categoryId property and mark it as foreign key to Category.Id. It should look like this:
#Entity()
export class Post {
#ForeignKey((category: Category) => category.Id) // it's a foreign key to Category.Id
categoryId: number;
}
Is it possible?
"I need is to have an explicit foreign key property"...
No, you could not. TypeOrm will automatically create foreign key property when you use #ManyToOne decorator. Just combine #ManyToOne and #JoinColumn decorators together like this:
#ManyToOne(type => Category)
#JoinColumn({ name: 'custom_field_name_if_you_want' })
category: Category;
Maybe you can create and write your own migration and use it like this :
const queryRunner = connection.createQueryRunner();
await queryRunner.createTable(new Table({
name: "question",
columns: [
{
name: "id",
type: "int",
isPrimary: true
},
{
name: "name",
type: "varchar",
}
]
}), true);
await queryRunner.createTable(new Table({
name: "answer",
columns: [
{
name: "id",
type: "int",
isPrimary: true
},
{
name: "name",
type: "varchar",
},
{
name: "questionId",
isUnique: connection.driver instanceof CockroachDriver, // CockroachDB requires UNIQUE constraints on referenced columns
type: "int",
}
]
}), true);
// clear sqls in memory to avoid removing tables when down queries executed.
queryRunner.clearSqlMemory();
const foreignKey = new TableForeignKey({
columnNames: ["questionId"],
referencedColumnNames: ["id"],
referencedTableName: "question",
onDelete: "CASCADE"
});
await queryRunner.createForeignKey("answer", foreignKey);
This code snippet is extracted from the functional test of type orm and you can use it to create your own constraint on the database I think.
It's actually possible to do so:
#Entity()
export class Post {
// this will add categoryId
#ManyToOne(type => Category)
category: Category;
// and you can use this for accessing post.categoryId
// only column you mark with #Column decorator will be mapped to a database column
// Ref: https://typeorm.io/#/entities
categoryId: number;
}
The added categoryId won't be mapped to column and will then be use for setting explicitly the id or for accessing its value as in:
post.categoryId = 1;
// or
const id = post.categoryId
Check with these two places Auth module(JwtModule.register()) and JWT strategy(super({...})). Make sure you have secret /secretOrKey is set to the same key. In my case "secret: process.env.JWT_SECRET_KEY" & "secretOrKey: process.env.JWT_SECRET_KEY"
I have encountered the same problem recently.
I still use the Entity but only with the primary key value of the referenced entity.
i.e. I do not query the database for the referenced entity.
Suppose your category entity looks like this:
#Entity()
export class Category{
#PrimaryGeneratedColumn()
id: number;
// ... other stuff
}
Now using your codes as example.
Dircely assigning relation using a foreign key value would be like.
// You wish to assign category #12 to a certain post
post.category = { id: 12 } as Category

Loopback4 Model Definition options for mongodb collection name

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

Apollo: Refetch queries that have multiple variable permutations after mutation

Let's say I have a table that lists a bunch of Posts using a query like:
const PostsQuery = gql`
query posts($name: string) {
posts {
id
name
status
}
}
`;
const query = apolloClient.watchQuery({query: PostsQuery});
query.subscribe({
next: (posts) => console.log(posts) // [ {name: "Post 1", id: '1', status: 'pending' }, { name: "Paul's Post", id: '2', status: 'pending'} ]
});
Then later my user comes along and enters a value in a search field and calls this code:
query.setVariables({name: 'Paul'})
It fetches the filtered posts and logs it out fine.
// [ { name: "Paul's Post", id: '2', status: 'pending'} ]
Now, in my table there is a button that changes the status of a post from 'Pending' to 'Active'. The user clicks that and it calls code like:
const PostsMutation = gql`
mutation activatePost($id: ID!) {
activatePost(id: $id) {
ok
object {
id
name
status
}
}
}
`;
apolloClient.mutate({mutation: PostsMutation});
All is well with the mutation, but now I want to refetch the table data so it has the latest, so I make a change:
apolloClient.mutate({
mutation: PostsMutation,
refetchQueries: [{query: PostsQuery, variables: {name: 'Paul'}]
});
Hurray, it works!
// [ { name: "Paul's Post", id: '2', status: 'active'} ]
But... now my user clears the search query, expecting the results to update.
query.setVariables({});
// [ {name: "Post 1", id: '1', status: 'pending' }, { name: "Paul's Post", id: '2', status: 'pending'} ]
Oh no! Because the data was not refetched in our mutation with our "original" variables (meaning none), we are getting stale data!
So how do you handle a situation where you have a mutation that may affect a query that could have many permutations of variables?
I had a similar issue, I am using Apollo with Angular, so I am not sure if this method will work with React Client, but it should.
If you look closely at refetchQueries properties on the mutate method, you will see that the function can also return a string array of query names to refetch. By returning just the query name as a string, you do not need to worry about the variables. Be advised that this will refetch all the queries matching the name. So if you had a lot queries with different variables it could end up being a large request. But, in my case it is worth the trade off. If this is a problem, you could also get access to the queryManager through apolloClient.queryManager which you could use to do some more fine grained control of what to refetch. I didn't implement it, but it looked very possible. I found the solution below fits my needs fine.
In your code, what you need to do is:
apolloClient.mutate({
mutation: PostsMutation,
refetchQueries: (mutationResult) => ['PostQueries']
});
This will refetch any query with the name 'PostQueries'. Again, it is possible to only refetch a subset of them if you dig into the queryManager and do some filtering on the active watch queries. But, that is another exercise.