Basic transaction system
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT DEFAULT 'No description provided',
|
||||
"availableStock" INTEGER NOT NULL DEFAULT 0,
|
||||
"imageUrl" TEXT NOT NULL DEFAULT '/no_image.png',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"hashedPassword" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL,
|
||||
"resetToken" TEXT,
|
||||
"resetTokenExpiresAt" DATETIME,
|
||||
"roles" TEXT NOT NULL DEFAULT 'user'
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
@@ -0,0 +1,41 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT DEFAULT 'No description provided',
|
||||
"availableStock" INTEGER NOT NULL DEFAULT 0,
|
||||
"imageUrl" TEXT NOT NULL DEFAULT '/no_image.png',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Part_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"hashedPassword" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL,
|
||||
"resetToken" TEXT,
|
||||
"resetTokenExpiresAt" TIMESTAMP(3),
|
||||
"roles" TEXT NOT NULL DEFAULT 'user',
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Transaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
18
api/db/migrations/20231107152332_transaction/migration.sql
Normal file
18
api/db/migrations/20231107152332_transaction/migration.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `type` to the `Transaction` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionType" AS ENUM ('in', 'out');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Part" ADD COLUMN "transactionId" INTEGER;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Transaction" ADD COLUMN "quantities" INTEGER[],
|
||||
ADD COLUMN "type" "TransactionType" NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Part" ADD CONSTRAINT "Part_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
27
api/db/migrations/20231107163803_transaction/migration.sql
Normal file
27
api/db/migrations/20231107163803_transaction/migration.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `quantities` on the `Transaction` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Part" DROP CONSTRAINT "Part_transactionId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Transaction" DROP COLUMN "quantities";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartTransaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"partId" INTEGER NOT NULL,
|
||||
"transactionId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "PartTransaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartTransaction" ADD CONSTRAINT "PartTransaction_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartTransaction" ADD CONSTRAINT "PartTransaction_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
24
api/db/migrations/20231107165858_del/migration.sql
Normal file
24
api/db/migrations/20231107165858_del/migration.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `PartTransaction` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Transaction` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartTransaction" DROP CONSTRAINT "PartTransaction_partId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartTransaction" DROP CONSTRAINT "PartTransaction_transactionId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "PartTransaction";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Transaction";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "TransactionType";
|
||||
16
api/db/migrations/20231107224945_transaction/migration.sql
Normal file
16
api/db/migrations/20231107224945_transaction/migration.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionType" AS ENUM ('in', 'out');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Transaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"type" "TransactionType" NOT NULL,
|
||||
"parts" JSONB[],
|
||||
|
||||
CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
@@ -1,5 +1,5 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
@@ -15,16 +15,32 @@ model Part {
|
||||
availableStock Int @default(0)
|
||||
imageUrl String @default("/no_image.png")
|
||||
createdAt DateTime @default(now())
|
||||
transactionId Int?
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
firstName String
|
||||
lastName String
|
||||
email String @unique
|
||||
email String @unique
|
||||
hashedPassword String
|
||||
salt String
|
||||
resetToken String?
|
||||
resetTokenExpiresAt DateTime?
|
||||
roles String @default("user")
|
||||
roles String @default("user")
|
||||
transactions Transaction[]
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
in
|
||||
out
|
||||
}
|
||||
|
||||
model Transaction {
|
||||
id Int @id @default(autoincrement())
|
||||
date DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
type TransactionType
|
||||
parts Json[] // { part: Part, quantity: Int }[]
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@redwoodjs/api": "6.3.2",
|
||||
"@redwoodjs/auth-dbauth-api": "6.3.2",
|
||||
"@redwoodjs/graphql-server": "6.3.2",
|
||||
"@redwoodjs/api": "6.3.3",
|
||||
"@redwoodjs/auth-dbauth-api": "6.3.3",
|
||||
"@redwoodjs/graphql-server": "6.3.3",
|
||||
"filestack-js": "^3.27.0"
|
||||
}
|
||||
}
|
||||
|
||||
58
api/src/graphql/transactions.sdl.ts
Normal file
58
api/src/graphql/transactions.sdl.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export const schema = gql`
|
||||
type Transaction {
|
||||
id: Int!
|
||||
date: DateTime!
|
||||
user: User!
|
||||
userId: Int!
|
||||
type: TransactionType!
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
in
|
||||
out
|
||||
}
|
||||
|
||||
enum FilterTransactionsByType {
|
||||
in
|
||||
out
|
||||
both
|
||||
}
|
||||
|
||||
type UserTransactions {
|
||||
transactions: [Transaction!]!
|
||||
filter: FilterTransactionsByType!
|
||||
}
|
||||
|
||||
type Query {
|
||||
transactions(filter: FilterTransactionsByType!): UserTransactions!
|
||||
@requireAuth(roles: "admin")
|
||||
transaction(id: Int!): Transaction @requireAuth(roles: "admin")
|
||||
userTransactions(
|
||||
userId: Int!
|
||||
filter: FilterTransactionsByType!
|
||||
): UserTransactions! @requireAuth
|
||||
}
|
||||
|
||||
input CreateTransactionInput {
|
||||
date: DateTime!
|
||||
userId: Int!
|
||||
type: TransactionType!
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
input UpdateTransactionInput {
|
||||
date: DateTime
|
||||
userId: Int
|
||||
type: TransactionType
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createTransaction(input: CreateTransactionInput!): Transaction! @requireAuth
|
||||
returnTransaction(id: Int!, userId: Int!): Transaction! @requireAuth
|
||||
updateTransaction(id: Int!, input: UpdateTransactionInput!): Transaction!
|
||||
@requireAuth(roles: "admin")
|
||||
deleteTransaction(id: Int!): Transaction! @requireAuth(roles: "admin")
|
||||
}
|
||||
`
|
||||
48
api/src/graphql/users.sdl.ts
Normal file
48
api/src/graphql/users.sdl.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export const schema = gql`
|
||||
type User {
|
||||
id: Int!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
hashedPassword: String!
|
||||
salt: String!
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String!
|
||||
transactions: [Transaction]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
users: [User!]! @requireAuth(roles: "admin")
|
||||
user(id: Int!): User @requireAuth(roles: "admin")
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
hashedPassword: String!
|
||||
salt: String!
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String!
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
firstName: String
|
||||
lastName: String
|
||||
email: String
|
||||
hashedPassword: String
|
||||
salt: String
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): User! @requireAuth(roles: "admin")
|
||||
updateUser(id: Int!, input: UpdateUserInput!): User!
|
||||
@requireAuth(roles: "admin")
|
||||
deleteUser(id: Int!): User! @requireAuth(roles: "admin")
|
||||
}
|
||||
`
|
||||
@@ -27,7 +27,7 @@ export const getCurrentUser = async (session: Decoded) => {
|
||||
|
||||
return await db.user.findUnique({
|
||||
where: { id: session.id },
|
||||
select: { id: true, firstName: true, roles: true },
|
||||
select: { id: true, firstName: true, roles: true, transactions: true },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,22 @@ export const partPage = ({
|
||||
}
|
||||
|
||||
export const createPart: MutationResolvers['createPart'] = ({ input }) => {
|
||||
input.description =
|
||||
input.description.length == 0
|
||||
? 'No description provided'
|
||||
: input.description
|
||||
|
||||
return db.part.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePart: MutationResolvers['updatePart'] = ({ id, input }) => {
|
||||
input.description =
|
||||
input.description.length == 0
|
||||
? 'No description provided'
|
||||
: input.description
|
||||
|
||||
return db.part.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
|
||||
39
api/src/services/transactions/transactions.scenarios.ts
Normal file
39
api/src/services/transactions/transactions.scenarios.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Prisma, Transaction } from '@prisma/client'
|
||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
||||
|
||||
export const standard = defineScenario<Prisma.TransactionCreateArgs>({
|
||||
transaction: {
|
||||
one: {
|
||||
data: {
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
user: {
|
||||
create: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String2420106',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
two: {
|
||||
data: {
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
user: {
|
||||
create: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String1596322',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type StandardScenario = ScenarioData<Transaction, 'transaction'>
|
||||
68
api/src/services/transactions/transactions.test.ts
Normal file
68
api/src/services/transactions/transactions.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Transaction } from '@prisma/client'
|
||||
|
||||
import {
|
||||
transactions,
|
||||
transaction,
|
||||
createTransaction,
|
||||
updateTransaction,
|
||||
deleteTransaction,
|
||||
} from './transactions'
|
||||
import type { StandardScenario } from './transactions.scenarios'
|
||||
|
||||
// Generated boilerplate tests do not account for all circumstances
|
||||
// and can fail without adjustments, e.g. Float.
|
||||
// Please refer to the RedwoodJS Testing Docs:
|
||||
// https://redwoodjs.com/docs/testing#testing-services
|
||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||
|
||||
describe('transactions', () => {
|
||||
scenario('returns all transactions', async (scenario: StandardScenario) => {
|
||||
const result = await transactions()
|
||||
|
||||
expect(result.length).toEqual(Object.keys(scenario.transaction).length)
|
||||
})
|
||||
|
||||
scenario(
|
||||
'returns a single transaction',
|
||||
async (scenario: StandardScenario) => {
|
||||
const result = await transaction({ id: scenario.transaction.one.id })
|
||||
|
||||
expect(result).toEqual(scenario.transaction.one)
|
||||
}
|
||||
)
|
||||
|
||||
scenario('creates a transaction', async (scenario: StandardScenario) => {
|
||||
const result = await createTransaction({
|
||||
input: {
|
||||
userId: scenario.transaction.two.userId,
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.userId).toEqual(scenario.transaction.two.userId)
|
||||
expect(result.type).toEqual('in')
|
||||
expect(result.parts).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
scenario('updates a transaction', async (scenario: StandardScenario) => {
|
||||
const original = (await transaction({
|
||||
id: scenario.transaction.one.id,
|
||||
})) as Transaction
|
||||
const result = await updateTransaction({
|
||||
id: original.id,
|
||||
input: { type: 'out' },
|
||||
})
|
||||
|
||||
expect(result.type).toEqual('out')
|
||||
})
|
||||
|
||||
scenario('deletes a transaction', async (scenario: StandardScenario) => {
|
||||
const original = (await deleteTransaction({
|
||||
id: scenario.transaction.one.id,
|
||||
})) as Transaction
|
||||
const result = await transaction({ id: original.id })
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
||||
158
api/src/services/transactions/transactions.ts
Normal file
158
api/src/services/transactions/transactions.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type {
|
||||
QueryResolvers,
|
||||
MutationResolvers,
|
||||
TransactionRelationResolvers,
|
||||
Part,
|
||||
} from 'types/graphql'
|
||||
|
||||
import { UserInputError } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const transactions: QueryResolvers['transactions'] = async ({
|
||||
filter,
|
||||
}) => {
|
||||
const transactions =
|
||||
filter == 'both'
|
||||
? await db.transaction.findMany({
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
: await db.transaction.findMany({
|
||||
where: {
|
||||
type: filter,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
transactions,
|
||||
filter,
|
||||
}
|
||||
}
|
||||
|
||||
export const userTransactions: QueryResolvers['userTransactions'] = async ({
|
||||
userId,
|
||||
filter,
|
||||
}) => {
|
||||
return {
|
||||
transactions:
|
||||
filter == 'both'
|
||||
? await db.transaction.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
: await db.transaction.findMany({
|
||||
where: {
|
||||
AND: {
|
||||
userId,
|
||||
type: filter,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
}),
|
||||
filter,
|
||||
}
|
||||
}
|
||||
|
||||
export const transaction: QueryResolvers['transaction'] = ({ id }) => {
|
||||
return db.transaction.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const returnTransaction: MutationResolvers['returnTransaction'] =
|
||||
async ({ id, userId }) => {
|
||||
const transaction = await db.transaction.findUnique({ where: { id } })
|
||||
|
||||
if (transaction.type == 'out' && userId == transaction.userId) {
|
||||
for (const partRaw of transaction.parts) {
|
||||
const transactionPart = JSON.parse(partRaw['part']) as Part
|
||||
const part = await db.part.findUnique({
|
||||
where: { id: transactionPart.id },
|
||||
})
|
||||
|
||||
await db.part.update({
|
||||
where: { id: part.id },
|
||||
data: {
|
||||
availableStock: {
|
||||
increment: partRaw['quantity'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return await db.transaction.update({
|
||||
where: { id: transaction.id },
|
||||
data: { type: 'in' },
|
||||
})
|
||||
} else return transaction
|
||||
}
|
||||
|
||||
export const createTransaction: MutationResolvers['createTransaction'] =
|
||||
async ({ input }) => {
|
||||
const basket = input.parts.map((item) => {
|
||||
const part: Part = JSON.parse(item['part']) as Part
|
||||
return { part: part.id, quantity: item['quantity'] }
|
||||
})
|
||||
|
||||
for (const item of basket) {
|
||||
const part = await db.part.findUnique({ where: { id: item.part } })
|
||||
|
||||
if (!part) throw new UserInputError(`Part ${item.part} does not exist`)
|
||||
|
||||
if (input.type == 'out') {
|
||||
if (part.availableStock < item.quantity)
|
||||
throw new UserInputError(
|
||||
`Cannot take out more than available stock (${part.availableStock} available, ${item.quantity} requested)`
|
||||
)
|
||||
|
||||
await db.part.update({
|
||||
where: { id: item.part },
|
||||
data: { availableStock: { decrement: item.quantity } },
|
||||
})
|
||||
} else if (input.type == 'in') {
|
||||
await db.part.update({
|
||||
where: { id: item.part },
|
||||
data: { availableStock: { increment: item.quantity } },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return db.transaction.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTransaction: MutationResolvers['updateTransaction'] = ({
|
||||
id,
|
||||
input,
|
||||
}) => {
|
||||
return db.transaction.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTransaction: MutationResolvers['deleteTransaction'] = ({
|
||||
id,
|
||||
}) => {
|
||||
return db.transaction.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Transaction: TransactionRelationResolvers = {
|
||||
user: (_obj, { root }) => {
|
||||
return db.transaction.findUnique({ where: { id: root?.id } }).user()
|
||||
},
|
||||
}
|
||||
27
api/src/services/users/users.scenarios.ts
Normal file
27
api/src/services/users/users.scenarios.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Prisma, User } from '@prisma/client'
|
||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
||||
|
||||
export const standard = defineScenario<Prisma.UserCreateArgs>({
|
||||
user: {
|
||||
one: {
|
||||
data: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String6743396',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
two: {
|
||||
data: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String6958264',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type StandardScenario = ScenarioData<User, 'user'>
|
||||
59
api/src/services/users/users.test.ts
Normal file
59
api/src/services/users/users.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { User } from '@prisma/client'
|
||||
|
||||
import { users, user, createUser, updateUser, deleteUser } from './users'
|
||||
import type { StandardScenario } from './users.scenarios'
|
||||
|
||||
// Generated boilerplate tests do not account for all circumstances
|
||||
// and can fail without adjustments, e.g. Float.
|
||||
// Please refer to the RedwoodJS Testing Docs:
|
||||
// https://redwoodjs.com/docs/testing#testing-services
|
||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||
|
||||
describe('users', () => {
|
||||
scenario('returns all users', async (scenario: StandardScenario) => {
|
||||
const result = await users()
|
||||
|
||||
expect(result.length).toEqual(Object.keys(scenario.user).length)
|
||||
})
|
||||
|
||||
scenario('returns a single user', async (scenario: StandardScenario) => {
|
||||
const result = await user({ id: scenario.user.one.id })
|
||||
|
||||
expect(result).toEqual(scenario.user.one)
|
||||
})
|
||||
|
||||
scenario('creates a user', async () => {
|
||||
const result = await createUser({
|
||||
input: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String3921297',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.firstName).toEqual('String')
|
||||
expect(result.lastName).toEqual('String')
|
||||
expect(result.email).toEqual('String3921297')
|
||||
expect(result.hashedPassword).toEqual('String')
|
||||
expect(result.salt).toEqual('String')
|
||||
})
|
||||
|
||||
scenario('updates a user', async (scenario: StandardScenario) => {
|
||||
const original = (await user({ id: scenario.user.one.id })) as User
|
||||
const result = await updateUser({
|
||||
id: original.id,
|
||||
input: { firstName: 'String2' },
|
||||
})
|
||||
|
||||
expect(result.firstName).toEqual('String2')
|
||||
})
|
||||
|
||||
scenario('deletes a user', async (scenario: StandardScenario) => {
|
||||
const original = (await deleteUser({ id: scenario.user.one.id })) as User
|
||||
const result = await user({ id: original.id })
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
||||
42
api/src/services/users/users.ts
Normal file
42
api/src/services/users/users.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
QueryResolvers,
|
||||
MutationResolvers,
|
||||
UserRelationResolvers,
|
||||
} from 'types/graphql'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const users: QueryResolvers['users'] = () => {
|
||||
return db.user.findMany()
|
||||
}
|
||||
|
||||
export const user: QueryResolvers['user'] = ({ id }) => {
|
||||
return db.user.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createUser: MutationResolvers['createUser'] = ({ input }) => {
|
||||
return db.user.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => {
|
||||
return db.user.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => {
|
||||
return db.user.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const User: UserRelationResolvers = {
|
||||
transactions: (_obj, { root }) => {
|
||||
return db.user.findUnique({ where: { id: root?.id } }).transactions()
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user