I am learning how to use the Adonis framework (v5) and as the tradition dictates it, I am creating a todo list api to test it out.
The problem I am having is regarding the relationship between the User and Todo entities.
Here are the two models:
// file: app/Models/Todo.ts
export default class Todo extends BaseModel {
#column({ isPrimary: true })
public id: number
#belongsTo(() => User, {
foreignKey: 'id',
})
public author: BelongsTo<typeof User>
#column()
public completed: boolean
#column()
public title: string
#column()
public description: string | null
#column.dateTime({ autoCreate: true })
public createdAt: DateTime
#column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}
// file: app/Models/User.ts
export default class User extends BaseModel {
#column({ isPrimary: true })
public id: number
#column()
public username: string
#column({ serializeAs: null })
public password: string
#hasMany(() => Todo, {
foreignKey: 'author',
})
public todos: HasMany<typeof Todo>
#column.dateTime({ autoCreate: true })
public createdAt: DateTime
#column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
#beforeSave()
public static async hashPassword(user: User) {
if (user.$dirty.password) {
// Only hash password if required
user.password = await Hash.make(user.password)
}
}
}
I did not include the migrations files but I will edit this question if they are needed.
What I am expecting is that I should be able to create and save Users into my database, the same for Todo entries and link the todo entries to their author. The documentation provides us with an example but I don't see where I am doing something wrong.
So to test it out I am using the node ace repl command as follow:
// log of running the commands in the AdonisJS v5 REPL
> loadModels()
recursively reading models from "app/Models"
Loaded models module. You can access it using the "models" variable
> undefined
> const testUser = await models.User.create({ username: 'testUser', password: 'password' })
undefined
> await testUser.related('todos').create({ title: 'Example todo entry' })
Uncaught:
Exception: E_MISSING_MODEL_ATTRIBUTE: "User.todos" expects "author" to exist on "Todo" model, but is missing
at <my-app-directory>\REPL23:1:39
at Proxy.related (<my-app-directory>\node_modules\#adonisjs\lucid\build\src\Orm\BaseModel\index.js:1436:18)
at HasMany.boot (<my-app-directory>\node_modules\#adonisjs\lucid\build\src\Orm\Relations\HasMany\index.js:74:12)
at KeysExtractor.extract (<my-app-directory>\node_modules\#adonisjs\lucid\build\src\Orm\Relations\KeysExtractor.js:28:39)
at Array.reduce (<anonymous>)
at <my-app-directory>\node_modules\#adonisjs\lucid\build\src\Orm\Relations\KeysExtractor.js:32:23
>
I don't understand the error message since author actually exists on the Todo model.
How can I solve this problem and get my todo app up and running?
Thank you by advance!
The error is that you are missing a field inside your model.
You need to define all fields with the #column() decorator. In your example, you are not defining the column author.
When creating a relation, you must have one column (the FK) and one relation.
If we assume that you have a column user_id inside your todos table, then you need to add the column user_id inside your Todo model.
Here is a correct example:
class User extends BaseModel {
// ...
#hasMany(() => Todo)
todos: HasMany<typeof Todo>
}
class Todo extends BaseModel {
#column()
user_id: number
#belongsTo(() => User)
author: BelongsTo<typeof User>
}
I'm trying to delete some records from a entity, but typeorm is not using the #Column definition os the entity, thus performing a invalid sql:
#Entity('table')
export class TableEntity implements Table {
#Column({ name: 'user_id' })
#PrimaryColumn()
userId: number;
...
}
this.repository.delete({
userId: 1
})
DELETE FROM "table" WHERE "userId" = $1
and not
DELETE FROM "table" where "user_id" = $1
as I expected. Why?
I am wondering if it is possible to pass multiple IDs to a useQuery for apollo hook. Or run a single query per ID, and if so how would I go about doing so.
I have the following query
const DECOR_SHEET = gql`
query GetDecorSheet($id: ID!) {
findDecorSheetByID(id:$id){
refIdArray
sheetName
_id
}
}
`;
and the following useQuery hook
const { loading, error, data: sheetData } = useQuery(DECOR_SHEET, {
variables: { id: id },
context: {
headers: {
authorization: cookieBearer,
},
},
});
I have the following IDs 293164663883956749, 293526016787218952 and I would like to return into one object in order to render into a component.
I am using Fauna DB and this is the input graphQL schema
type Catalog {
decor: Boolean
clothing: Boolean
supplies: Boolean
furniture: Boolean
owner: User
}
type Decor {
description: String
pieces: Int
purchaser: String
alterations: Boolean
cost: Int
purchaseDate: Date
category: String
image: String
itemNum: Int
owner: User!
visible: Boolean
}
type DecorSheet {
sheetName: String
refIdArray: String
owner: User!
}
type User {
email: String! #unique
catalog: Catalog
decor: [Decor!] #relation
decorSheet: [DecorSheet!] #relation
}
and this is the generated schema
directive #embedded on OBJECT
directive #collection(name: String!) on OBJECT
directive #index(name: String!) on FIELD_DEFINITION
directive #resolver(
name: String
paginated: Boolean! = false
) on FIELD_DEFINITION
directive #relation(name: String) on FIELD_DEFINITION
directive #unique(index: String) on FIELD_DEFINITION
type Catalog {
_id: ID!
decor: Boolean
clothing: Boolean
supplies: Boolean
owner: User
furniture: Boolean
_ts: Long!
}
input CatalogInput {
decor: Boolean
clothing: Boolean
supplies: Boolean
furniture: Boolean
owner: CatalogOwnerRelation
}
input CatalogOwnerRelation {
create: UserInput
connect: ID
disconnect: Boolean
}
scalar Date
type Decor {
purchaseDate: Date
visible: Boolean
image: String
description: String
_id: ID!
alterations: Boolean
cost: Int
pieces: Int
category: String
owner: User!
purchaser: String
itemNum: Int
_ts: Long!
}
input DecorInput {
description: String
pieces: Int
purchaser: String
alterations: Boolean
cost: Int
purchaseDate: Date
category: String
image: String
itemNum: Int
owner: DecorOwnerRelation
visible: Boolean
}
input DecorOwnerRelation {
create: UserInput
connect: ID
}
type DecorPage {
data: [Decor]!
after: String
before: String
}
type DecorSheet {
refIdArray: String
_id: ID!
sheetName: String
owner: User!
_ts: Long!
}
input DecorSheetInput {
sheetName: String
refIdArray: String
owner: DecorSheetOwnerRelation
}
input DecorSheetOwnerRelation {
create: UserInput
connect: ID
}
type DecorSheetPage {
data: [DecorSheet]!
after: String
before: String
}
scalar Long
type Mutation {
updateUser(
id: ID!
data: UserInput!
): User
createUser(data: UserInput!): User!
createDecorSheet(data: DecorSheetInput!): DecorSheet!
createDecor(data: DecorInput!): Decor!
deleteCatalog(id: ID!): Catalog
updateCatalog(
id: ID!
data: CatalogInput!
): Catalog
updateDecor(
id: ID!
data: DecorInput!
): Decor
updateDecorSheet(
id: ID!
data: DecorSheetInput!
): DecorSheet
deleteDecor(id: ID!): Decor
deleteUser(id: ID!): User
createCatalog(data: CatalogInput!): Catalog!
deleteDecorSheet(id: ID!): DecorSheet
}
type Query {
findUserByID(id: ID!): User
findCatalogByID(id: ID!): Catalog
findDecorByID(id: ID!): Decor
findDecorSheetByID(id: ID!): DecorSheet
}
scalar Time
type User {
catalog: Catalog
email: String!
_id: ID!
decor(
_size: Int
_cursor: String
): DecorPage!
decorSheet(
_size: Int
_cursor: String
): DecorSheetPage!
_ts: Long!
}
input UserCatalogRelation {
create: CatalogInput
connect: ID
disconnect: Boolean
}
input UserDecorRelation {
create: [DecorInput]
connect: [ID]
disconnect: [ID]
}
input UserDecorSheetRelation {
create: [DecorSheetInput]
connect: [ID]
disconnect: [ID]
}
input UserInput {
email: String!
catalog: UserCatalogRelation
decor: UserDecorRelation
decorSheet: UserDecorSheetRelation
}
There is an option to query with Fauna's FQL which may have a way of querying multiple IDs I will have to look into that, but would prefer to do this in graphQL with apollo if possible.
Thanks ahead of time
Thanks to support from FaunaDB
The following query and UDF does the trick
Query
type Query {
getMultipleDecors(DecorId: [ID!]): [Decor]
#resolver(name: "get_multiple_decors")
}
udf named get_multiple_decors
Query(
Lambda(
["input"],
Let(
{
data: Map(
Var("input"),
Lambda("x", Get(Ref(Collection("Decor"), Var("x"))))
)
},
Var("data")
)
)
)
If it's always exactly two ids, you can fetch both objects in a single query easily using field aliases:
const DECOR_SHEET = gql`
query GetDecorSheet($firstId: ID!, $secondId: ID!) {
firstDecorSheet: findDecorSheetByID(id: $firstId) {
refIdArray
sheetName
_id
}
secondDecorSheet: findDecorSheetByID(id: $secondId) {
refIdArray
sheetName
_id
}
}
`;
So I have this query:
START TRANSACTION;
CREATE TABLE `users` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`registration_date` timestamp NOT NULL,
`address` varchar(255),
`description` varchar(1000),
`studio` varchar(255),
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`is_looking_for_work` boolean,
`last_active` timestamp,
`last_login` timestamp,
`profile_photo` varchar(2000)
);
CREATE TABLE `availability` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`start_time` timestamp NOT NULL,
`end_time` timestamp NOT NULL,
`description` varchar(500),
`user_id` int NOT NULL
);
CREATE TABLE `chats` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`initiator_id` int NOT NULL,
`invited_id` int NOT NULL
);
CREATE TABLE `messages` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`chat_id` int NOT NULL,
`author_id` int NOT NULL,
`content` varchar(1000) NOT NULL,
`creation_date` timestamp NOT NULL
);
CREATE TABLE `offers` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`author_id` int,
`colors` ENUM ('white', 'red'),
`project_image` varchar(10000),
`finished_image` varchar(10000),
`price` int NOT NULL,
`description` varchar(500),
`time_needed` int NOT NULL,
`is_available` boolean NOT NULL,
`is_in_portfolio` boolean
);
CREATE TABLE `meetings` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`provider` int NOT NULL,
`client` int NOT NULL,
`date` timestamp NOT NULL,
`duration` int NOT NULL,
`description` varchar(1000) NOT NULL,
`offer_id` int NOT NULL
);
CREATE TABLE `tags` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`tag` varchar(255) NOT NULL,
`description` varchar(1000),
`is_style` boolean NOT NULL
);
CREATE TABLE `tags_to_offers` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`offer_id` int NOT NULL,
`tag_id` int NOT NULL
);
CREATE TABLE `tags_to_users` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`tag_id` int NOT NULL
);
ALTER TABLE `offers` ADD FOREIGN KEY (`author_id`) REFERENCES `users` (`id`);
ALTER TABLE `availability` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
ALTER TABLE `chats` ADD FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`);
ALTER TABLE `chats` ADD FOREIGN KEY (`invited_id`) REFERENCES `users` (`id`);
ALTER TABLE `messages` ADD FOREIGN KEY (`chat_id`) REFERENCES `chats` (`id`);
ALTER TABLE `messages` ADD FOREIGN KEY (`author_id`) REFERENCES `users` (`id`);
ALTER TABLE `tags_to_offers` ADD FOREIGN KEY (`offer_id`) REFERENCES `offers` (`id`);
ALTER TABLE `tags_to_offers` ADD FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`);
ALTER TABLE `tags_to_users` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
ALTER TABLE `tags_to_users` ADD FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`);
ALTER TABLE `meetings` ADD FOREIGN KEY (`provider`) REFERENCES `users` (`id`);
ALTER TABLE `meetings` ADD FOREIGN KEY (`client`) REFERENCES `users` (`id`);
ALTER TABLE `meetings` ADD FOREIGN KEY (`offer_id`) REFERENCES `offers` (`id`);
COMMIT;
It causes prisma to create this schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model availability {
id Int #id #default(autoincrement())
start_time DateTime
end_time DateTime
description String?
user_id Int
users users #relation(fields: [user_id], references: [id])
##index([user_id], name: "user_id")
}
model chats {
id Int #id #default(autoincrement())
initiator_id Int
invited_id Int
users_chats_initiator_idTousers users #relation("chats_initiator_idTousers", fields: [initiator_id], references: [id])
users_chats_invited_idTousers users #relation("chats_invited_idTousers", fields: [invited_id], references: [id])
messages messages[]
##index([initiator_id], name: "initiator_id")
##index([invited_id], name: "invited_id")
}
model meetings {
id Int #id #default(autoincrement())
provider Int
client Int
date DateTime
duration Int
description String
offer_id Int
users_meetings_clientTousers users #relation("meetings_clientTousers", fields: [client], references: [id])
offers offers #relation(fields: [offer_id], references: [id])
users_meetings_providerTousers users #relation("meetings_providerTousers", fields: [provider], references: [id])
##index([client], name: "client")
##index([offer_id], name: "offer_id")
##index([provider], name: "provider")
}
model messages {
id Int #id #default(autoincrement())
chat_id Int
author_id Int
content String
creation_date DateTime
users users #relation(fields: [author_id], references: [id])
chats chats #relation(fields: [chat_id], references: [id])
##index([author_id], name: "author_id")
##index([chat_id], name: "chat_id")
}
model offers {
id Int #id #default(autoincrement())
author_id Int?
colors offers_colors?
project_image String?
finished_image String?
price Int
description String?
time_needed Int
is_available Boolean
is_in_portfolio Boolean?
users users? #relation(fields: [author_id], references: [id])
meetings meetings[]
tags_to_offers tags_to_offers[]
##index([author_id], name: "author_id")
}
model tags {
id Int #id #default(autoincrement())
tag String
description String?
is_style Boolean
tags_to_offers tags_to_offers[]
tags_to_users tags_to_users[]
}
model tags_to_offers {
id Int #id #default(autoincrement())
offer_id Int
tag_id Int
offers offers #relation(fields: [offer_id], references: [id])
tags tags #relation(fields: [tag_id], references: [id])
##index([offer_id], name: "offer_id")
##index([tag_id], name: "tag_id")
}
model tags_to_users {
id Int #id #default(autoincrement())
user_id Int
tag_id Int
tags tags #relation(fields: [tag_id], references: [id])
users users #relation(fields: [user_id], references: [id])
##index([tag_id], name: "tag_id")
##index([user_id], name: "user_id")
}
model users {
id Int #id #default(autoincrement())
name String
registration_date DateTime
address String?
description String?
studio String?
email String
password String
is_looking_for_work Boolean?
last_active DateTime?
last_login DateTime?
profile_photo String?
availability availability[]
chats_chats_initiator_idTousers chats[] #relation("chats_initiator_idTousers")
chats_chats_invited_idTousers chats[] #relation("chats_invited_idTousers")
meetings_meetings_clientTousers meetings[] #relation("meetings_clientTousers")
meetings_meetings_providerTousers meetings[] #relation("meetings_providerTousers")
messages messages[]
offers offers[]
tags_to_users tags_to_users[]
}
enum offers_colors {
white
red
}
Which, is not what I was going for. I wanted something like this (GraphQL types):
type User {
id: Int!
name: String!
registration_date: String!
address: String
description: String
studio: String
email: String!
password: String!
is_looking_for_work: Boolean
last_active: String
last_login: String
profile_photo: String
availability: [Availability]
chats: [Chat]
meetings: [Meeting]
tags: [Tag]
}
type Offer {
id: Int!
author_id: Int!
colors: String
project_image: String!
finished_image: String
price: Int!
description: String
time_needed: Int!
is_available: Boolean!
is_in_portfolio: Boolean
tags: [Tag]
}
type Availability {
id: Int!
start_time: String!
end_time: String!
description: String
user_id: Int!
}
type Chat {
id: Int!
initiator_id: Int!
invited_id: Int!
messages: [Message]
}
type Message {
id: Int!
chat_id: Int!
author_id: Int!
content: Int!
creation_date: Int!
}
type Meeting {
id: Int!
providerId: Int!
clientId: Int!
date: String!
duration: Int!
description: String!
offer_id: Int!
}
type Tag {
id: Int!
tag: String!
description: String
is_style: Boolean!
}
For example let's look at user for simplicity:
SQL
CREATE TABLE `users` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`registration_date` timestamp NOT NULL,
`address` varchar(255),
`description` varchar(1000),
`studio` varchar(255),
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`is_looking_for_work` boolean,
`last_active` timestamp,
`last_login` timestamp,
`profile_photo` varchar(2000)
);
Prisma
model users {
id Int #id #default(autoincrement())
name String
registration_date DateTime
address String?
description String?
studio String?
email String
password String
is_looking_for_work Boolean?
last_active DateTime?
last_login DateTime?
profile_photo String?
availability availability[]
chats_chats_initiator_idTousers chats[] #relation("chats_initiator_idTousers")
chats_chats_invited_idTousers chats[] #relation("chats_invited_idTousers")
meetings_meetings_clientTousers meetings[] #relation("meetings_clientTousers")
meetings_meetings_providerTousers meetings[] #relation("meetings_providerTousers")
// the 4 above: Why are they not in objects?
messages messages[]
offers offers[]
tags_to_users tags_to_users[]
}
What I was hoping for
type User {
id: Int!
name: String!
registration_date: String!
address: String
description: String
studio: String
email: String!
password: String!
is_looking_for_work: Boolean
last_active: String
last_login: String
profile_photo: String
availability: [Availability]
chats: [Chat]
meetings: [Meeting]
// meeting and chats. Why are they not like this?
tags: [Tag]
}
And as you can see
chats_chats_initiator_idTousers chats[] #relation("chats_initiator_idTousers")
chats_chats_invited_idTousers chats[] #relation("chats_invited_idTousers")
meetings_meetings_clientTousers meetings[] #relation("meetings_clientTousers")
meetings_meetings_providerTousers meetings[] #relation("meetings_providerTousers")
This is how prisma made the chats and meethings. Also it added objects. But why did it add the 4 above?
What did I mess up in my schema? Or can I create a db schema from my graphql types?
As Documented on TypeOrm FrameWork Repository.save should save/insert new values and ignore/update the existing once,
But now I'm facing a problem that it's thrown a duplication error on existing value and stoping the whole inserting! ( I have a unique column called key )
My entity:
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { Source } from '../sources/source.entity';
#Entity({ name: 'articles' })
export class Article {
#PrimaryGeneratedColumn()
id: number;
#Column()
title: string;
#Column({
nullable: true
})
image: string | null;
#Column({
type: "text",
})
url: string;
#Column({
type: "varchar",
unique: true
})
key: string;
#Column({
type: 'datetime',
nullable: true
})
date: Date | null;
#ManyToOne(type => Source, source => source.articles, {eager: true})
#JoinColumn({name: 'source'})
source: Source;
#Column({
type: `text`,
nullable: true
})
description: string | null
}
My Service:
constructor(
#InjectRepository(Article) private readonly articleRepository: Repository<Article>,
private readonly articlesScraper: BlogScraperService
) {
}
async clonningFromScraper() {
let articles = await this.articlesScraper.articles('1');
articles = articles.map(article => ({ ...article, key: decodeURIComponent(article.url).substring(0, 255) }));
return this.articleRepository
.save(articles);
}
I have ended up solving this by RAW SQL query using the following
return this.articleRepository.query(
"INSERT IGNORE INTO articles ( title, date, url, image, source, description, _key ) VALUES ?", [querableArticles]);