First create generator and datasource entry in your .prisma
file like below:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid()) // ID type column whose default value is cuid
createdAt DateTime @default(now()) // Date type column whose default value is current date and time
modifiedAt DateTime @default(now()) // Date type column whose default value is current date and time
email String @unique // String type column whose value must be unique
name String? // String type column whose value is optional
}
model Question {
id String @id @default(cuid()) // ID type column whose default value is cuid
createdAt DateTime @default(now()) // Date type column whose default value is current date and time
modifiedAt DateTime @default(now()) // Date type column whose default value is current date and time
text String // String type column
options String[] // Column value should be a list of string
answer String // String type column
marks Int @default(0) // Integer type column whose default value is null
negativeMarking Boolean @default(false) // Boolean type value whose default value is false
}
It is very important to remember there is never a prisma model relationship. Relationships are always both sided.
model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
email String @unique
projects Project[] // Model User has many relationship with Project model
name String?
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
users User[] // Model Project has many relationship with User model
name String
slug String @unique
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
}
model TestPaper {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
title String
description String
expireInDays Int @default(0)
maxAttempt Int @default(1)
negativeMarking Boolean @default(false)
totalTime Int @default(0)
results Result[] // Model TestPaper has many relationship on Result model
}
model Result {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
testId String
test TestPaper @relation(fields: [testId], references: [id]) // Model Result has one relationship with TestPaper model
score Int
}
model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
email String @unique
projects Project[]
name String?
testsTaken Result[]
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
users User[]
name String
slug String @unique
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
}
model Question {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
text String
options String[]
answer String
marks Int @default(0)
papers TestPaper[]
}
model TestPaper {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
title String
description String
expireInDays Int @default(0)
maxAttempt Int @default(1)
questions Question[]
negativeMarking Boolean @default(false)
totalTime Int @default(0)
results Result[]
}
model Result {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
testId String
test TestPaper @relation(fields: [testId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
answers String[]
score Int
}
/* eslint-disable no-return-await */
import {
extendType,
intArg,
list,
nonNull,
objectType,
stringArg,
} from 'nexus';
import prisma from '../../db/prisma';
const Question = objectType({
name: 'Question',
definition(t) {
t.nonNull.string('id'); // non-null string column with name `id`
t.nonNull.string('text'); // non-null string column with name `text`
t.nonNull.list.nonNull.string('options'); // non-nullable list of string column with name `options`
t.nonNull.string('answer'); // non-null string column with name `answer`
t.nonNull.int('marks'); // non-null integer column with name `marks`
t.nullable.int('expireInDays'); // nullable integer type column
t.nullable.int('maxAttempt'); // nullable integer type column
t.nullable.boolean('negativeMarking'); // nullable boolean type column
t.nullable.int('totalTime'); // nullable integer type column
},
});
You must first define the model then create mutation for the model.
const mutations = extendType({
type: 'Mutation',
definition: (t) => {
t.nullable.field('createQuestion', {
type: 'Question',
args: {
text: nonNull(stringArg()),
answer: nonNull(stringArg()),
marks: nonNull(intArg()),
options: nonNull(list(nonNull('String'))),
},
// Create data in model from args
resolve: async (_, args, ctx) => {
return await prisma.question.create({
data: {
text: args.text,
answer: args.answer,
marks: args.marks,
options: args.options,
},
});
},
});
t.nullable.field('updateQuestion', {
type: 'Question',
args: {
id: nonNull(stringArg()),
text: nonNull(stringArg()),
answer: nonNull(stringArg()),
marks: nonNull(intArg()),
options: nonNull(list(nonNull('String'))),
},
// Updates data in question model by ID
resolve: async (_, args, ctx) => {
return await prisma.question.update({
where: { id: args.id },
data: {
text: args.text,
answer: args.answer,
marks: args.marks,
options: args.options,
},
});
},
});
},
});
Graphql mutation query for above mutation will look like below:
mutation CreateQuestion(
$text: String!
$answer: String!
$marks: Int!
$options: [String!]!
$topicId: ID
) {
createQuestion(
text: $text
answer: $answer
marks: $marks
options: $options
topicId: $topicId
) {
id
}
}
mutation UpdateQuestion(
$id: String!
$text: String!
$answer: String!
$marks: Int!
$options: [String!]!
) {
updateQuestion(
id: $id
text: $text
answer: $answer
marks: $marks
options: $options
) {
id
}
}
import {
arg,
booleanArg,
extendType,
intArg,
list,
nonNull,
objectType,
stringArg,
} from 'nexus';
const queries = extendType({
type: 'Query',
definition: (t) => {
t.field('question', {
type: 'Question',
args: {
id: nonNull(stringArg()),
},
// Query data from question model by ID from args
resolve: (_, { id }: any, ctx) => {
return prisma.question.findUnique({
where: {
id,
},
});
},
});
},
});
The GraphQuery query object for above will look like below:
query GetQuestion($id: String!) {
question(id: $id) {
id
text
options
answer
marks
}
}
Here the question
in query is same as name of field in our query definition.
const queries = extendType({
type: 'Query',
definition: (t) => {
// get all papers
t.list.field('papers', {
type: 'TestPaper',
resolve: (_root, _args, ctx) => {
if (!ctx.user?.id) return null;
return prisma.testPaper.findMany();
},
});
},
});
The GraphQuery query object for above will look like below:
query GetPaperList {
papers {
id
title
description
expireInDays
maxAttempt
negativeMarking
totalTime
}
}
Here the papers
in query is same as name of field in our query definition.
import {
arg,
booleanArg,
extendType,
intArg,
list,
nonNull,
objectType,
stringArg,
} from 'nexus';
import prisma from '../../db/prisma';
const TestPaper = objectType({
name: 'TestPaper',
definition(t) {
t.nonNull.string('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nullable.int('expireInDays');
t.nullable.int('maxAttempt');
t.nullable.boolean('negativeMarking');
t.nullable.int('totalTime');
// questions field will be of type question ID
// which will then be resolved from question table records using ID. It will be a list of ID
t.nonNull.list.nonNull.field('questions', {
type: 'Question',
resolve: (parent, _, ctx) => ctx.prisma.testPaper
.findUnique({ where: { id: parent.id } })
.questions(),
});
},
});
The prisma model for above model will look like below:
model TestPaper {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
title String
description String
expireInDays Int @default(0)
maxAttempt Int @default(1)
questions Question[]
negativeMarking Boolean @default(false)
totalTime Int @default(0)
}
const Question = objectType({
name: 'Question',
definition(t) {
t.nonNull.string('id');
t.nonNull.string('text');
t.nonNull.list.nonNull.string('options');
t.nonNull.string('answer');
t.nonNull.int('marks');
// topicId field will be of type topic ID
// which will then be resolved from topic table records
t.nonNull.field('topic', {
type: 'Topic',
// eslint-disable-next-line max-len
resolve: (parent: any, _: any, ctx: any) => ctx.prisma.question.findUnique({ where: { id: parent.id } }).topic(),
});
},
});
The prisma model for above model will look like below:
model Question {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
text String
options String[]
answer String
marks Int @default(0)
papers TestPaper[]
topicId String
topic Topic @relation(fields: [topicId], references: [id])
}
Nexus mutation of above model will look like below:
const mutations = extendType({
type: 'Mutation',
definition: (t) => {
t.nullable.field('createQuestion', {
type: 'Question',
args: {
text: nonNull(stringArg()),
answer: nonNull(stringArg()),
marks: nonNull(intArg()),
options: nonNull(list(nonNull('String'))),
// topic ID of type ID which links to topic in model
topicId: arg({
type: 'ID',
}),
},
resolve: async (_, args: any, ctx) => {
if (!ctx.user?.id) return null;
return await prisma.question.create({
data: {
text: args.text,
answer: args.answer,
marks: args.marks,
options: args.options,
topic: {
connect: { id: args.topicId } || null,
},
},
});
},
});
t.nullable.field('updateQuestion', {
type: 'Question',
args: {
id: nonNull(stringArg()),
text: nonNull(stringArg()),
answer: nonNull(stringArg()),
marks: nonNull(intArg()),
options: nonNull(list(nonNull('String'))),
},
resolve: async (_, args, ctx) => {
if (!ctx.user?.id) return null;
return await prisma.question.update({
where: { id: args.id },
data: {
text: args.text,
answer: args.answer,
marks: args.marks,
options: args.options,
},
});
},
});
},
});
const mutations = extendType({
type: 'Mutation',
definition: (t) => {
t.nullable.field('createTestPaper', {
type: 'TestPaper',
args: {
title: nonNull(stringArg()),
description: nonNull(stringArg()),
expireInDays: intArg(),
maxAttempt: intArg(),
negativeMarking: booleanArg(),
totalTime: intArg(),
// Questions will contain any array of ID
questions: arg({
type: list('ID'),
}),
},
resolve: async (_, args: any, ctx) => {
if (!ctx.user?.id) return null;
return await prisma.testPaper.create({
data: {
title: args.title,
description: args.description,
expireInDays: args.expireInDays || 0,
maxAttempt: args.maxAttempt || 1,
negativeMarking: args.negativeMarking || false,
totalTime: args.totalTime || 0, // 0 = infinite time
// connecting question ID list in args with question table records
questions: {
connect: args.questions.map((que) => ({ id: que })) || [],
},
},
});
},
});
},
});
Graphql mutation query for above mutation will look like below:
mutation CreateTestPaper(
$title: String!
$description: String!
$expireInDays: Int
$maxAttempt: Int
$negativeMarking: Boolean
$totalTime: Int
$questions: [ID!]!
) {
createTestPaper(
title: $title
description: $description
expireInDays: $expireInDays
maxAttempt: $maxAttempt
negativeMarking: $negativeMarking
totalTime: $totalTime
questions: $questions
) {
id
}
}
Nexus object model for the data model:
const TestPaper = objectType({
name: 'TestPaper',
definition(t) {
t.nonNull.string('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nullable.int('expireInDays');
t.nullable.int('maxAttempt');
t.nullable.boolean('negativeMarking');
t.nullable.int('totalTime');
// questions field will be of type question ID
// which will then be resolved from question table records
t.nonNull.list.nonNull.field('questions', {
type: 'Question',
resolve: (parent, _, ctx) => ctx.prisma.testPaper
.findUnique({ where: { id: parent.id } })
.questions(),
});
},
});
const queries = extendType({
type: 'Query',
definition: (t) => {
// get all papers
t.list.field('papers', {
type: 'TestPaper',
resolve: (_root, _args, ctx) => {
if (!ctx.user?.id) return null;
// Include questions in query response
return prisma.testPaper.findMany({
include: { questions: true },
});
},
});
// Get paper by ID with question
t.field('paperWithQuestion', {
type: 'TestPaper',
args: {
id: nonNull(stringArg()),
},
resolve: (_, { id }: any, ctx) => {
if (!ctx.user?.id) return null;
// Include questions in query response
return prisma.testPaper.findUnique({
where: {
id,
},
include: { questions: true },
});
},
});
},
});
Graphql query for above query object will look like below:
Return only question ID list with query response along with test paper details:
query GetPaperList {
papers {
id
title
description
expireInDays
maxAttempt
negativeMarking
totalTime
questions {
id
}
}
}
Return question info list with query response along with test paper details:
query GetPaperWithQuestion($id: String!) {
paperWithQuestion(id: $id) {
id
title
description
expireInDays
maxAttempt
negativeMarking
totalTime
questions {
id
text
options
answer
marks
}
}
}
Update model with deleted field like below:
model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
modifiedAt DateTime @default(now())
email String @unique
name String?
deleted Boolean @default(false)
}
Update nexus model object like below:
const User = objectType({
name: 'User',
definition(t) {
t.nonNull.string('id');
t.nullable.string('name');
t.nonNull.string('email');
t.nullable.boolean('deleted');
},
});
Update mutation of model like below:
t.field('deleteUserById', {
type: 'User',
args: {
id: nonNull(stringArg()),
},
resolve: async (_root, { id }, ctx) => {
if (!ctx.user?.id) return null;
return await prisma.user.delete({
where: { id },
});
},
});
Add prisma middleware for soft deletion and not returning deleted data:
/* eslint-disable vars-on-top,import/no-mutable-exports */
import { PrismaClient } from '@prisma/client';
// Make global.cachedPrisma work with TypeScript
declare global {
// NOTE: This actually needs to be a "var", let/const don't work here.
// eslint-disable-next-line no-var
var cachedPrisma: PrismaClient;
}
// Workaround to make Prisma Client work well during "next dev"
// @see https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.cachedPrisma) {
global.cachedPrisma = new PrismaClient();
}
prisma = global.cachedPrisma;
}
/** ******************************** */
/* SOFT DELETE MIDDLEWARE */
/** ******************************** */
prisma.$use(async (paramData, next) => {
const params = { ...paramData };
// Check incoming query type
if (params.action === 'delete') {
// Delete queries
// Change action to an update
params.action = 'update';
params.args.data = { deleted: true };
}
if (params.action === 'deleteMany') {
// Delete many queries
params.action = 'updateMany';
if (params.args.data !== undefined) {
params.args.data.deleted = true;
} else {
params.args.data = { deleted: true };
}
}
if (paramData.action === 'findFirst' || paramData.action === 'findMany' || paramData.action === 'findUnique') {
const conditions = paramData.args.where || {};
params.args.where = { ...conditions, deleted: false };
}
return next(params);
});
export default prisma;
Add inputObjectType
for defining array of JSON inputs.
const Sprints = inputObjectType({
name: 'Sprints',
definition(t) {
t.nullable.string('id');
t.nonNull.string('taskId');
t.nonNull.int('timeSpent');
t.nullable.boolean('isCompleted');
},
});
Use custom input type in mutation args.
t.nullable.list.field('createDailySprints', {
type: 'DailySprint',
args: {
sprints: nonNull(list(Sprints)),
},
resolve: async (_, args, ctx) => {
if (!ctx.user?.id) return null;
const results = [];
args.sprints.map((sprint) => {
const query = {
timeSpent: sprint.timeSpent,
isCompleted: sprint.isCompleted,
};
if (sprint.id) {
const dailySprint = prisma.dailySprint.findFirst({
where: {
id: sprint.id,
},
});
if (dailySprint && dailySprint.userId !== ctx.user.id) {
return;
}
if (dailySprint) {
results.push(prisma.dailySprint.update({
where: { id: sprint.id },
data: query,
}));
}
}
results.push(prisma.dailySprint.create({
data: {
...query,
id: sprint.id || cuid(),
user: {
connect: { id: ctx.user?.id },
},
task: {
connect: { id: sprint.taskId },
},
},
}));
});
return await Promise.all(results);
},
});
Use input type in GraphQL mutation.
mutation CreateDailySprints(
$sprints: [Sprints!]!
) {
createDailySprints(sprints: $sprints) {
id
timeSpent
isCompleted
task {
id
date
subject
status
category
}
}
}