Scaffold Apollo backend domain API
This commit is contained in:
10
src/context.js
Normal file
10
src/context.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { prisma } from './prisma-client.js';
|
||||
|
||||
export async function buildContext(req) {
|
||||
const userId = req.headers['x-user-id'];
|
||||
const user = userId
|
||||
? await prisma.user.findUnique({ where: { id: String(userId) } })
|
||||
: null;
|
||||
|
||||
return { prisma, user };
|
||||
}
|
||||
11
src/prisma-client.js
Normal file
11
src/prisma-client.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL is required for Prisma connection');
|
||||
}
|
||||
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
|
||||
export const prisma = new PrismaClient({ adapter });
|
||||
553
src/resolvers.js
Normal file
553
src/resolvers.js
Normal file
@@ -0,0 +1,553 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
||||
|
||||
const ACTIVE_ORDER_STATUSES = ['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS'];
|
||||
|
||||
function toFloat(value) {
|
||||
return value == null ? null : Number(value);
|
||||
}
|
||||
|
||||
function requireUser(context) {
|
||||
if (!context.user) {
|
||||
throw new Error('Authentication required. Pass x-user-id header.');
|
||||
}
|
||||
return context.user;
|
||||
}
|
||||
|
||||
function requireRole(context, role) {
|
||||
const user = requireUser(context);
|
||||
if (user.role !== role) {
|
||||
throw new Error(`Only ${role} can perform this operation.`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async function appendOrderEvent(prisma, orderId, status, actorUserId, note = null) {
|
||||
return prisma.orderStatusEvent.create({
|
||||
data: {
|
||||
orderId,
|
||||
status,
|
||||
actorUserId,
|
||||
note,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function orderCode() {
|
||||
return `FR-${Date.now()}-${crypto.randomInt(1000, 9999)}`;
|
||||
}
|
||||
|
||||
function invitationToken() {
|
||||
return crypto.randomBytes(24).toString('hex');
|
||||
}
|
||||
|
||||
export const resolvers = {
|
||||
DateTime: dateTimeScalar,
|
||||
JSON: jsonScalar,
|
||||
|
||||
Query: {
|
||||
healthcheck: () => 'ok',
|
||||
|
||||
me: (_, __, context) => context.user,
|
||||
|
||||
clientProducts: (_, __, context) =>
|
||||
context.prisma.product.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
inventory: {
|
||||
include: { warehouse: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
|
||||
myOrders: (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.order.findMany({
|
||||
where: user.role === 'MANAGER' ? { managerId: user.id } : { customerId: user.id },
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
myCurrentOrders: (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.order.findMany({
|
||||
where: {
|
||||
...(user.role === 'MANAGER' ? { managerId: user.id } : { customerId: user.id }),
|
||||
status: { in: ACTIVE_ORDER_STATUSES },
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
managerOrders: (_, { status }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.order.findMany({
|
||||
where: {
|
||||
managerId: manager.id,
|
||||
...(status ? { status } : {}),
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
registrationRequests: (_, { status }, context) => {
|
||||
requireRole(context, 'MANAGER');
|
||||
return context.prisma.registrationRequest.findMany({
|
||||
where: status ? { status } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
referralStats: async (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
const [links, transactions, pendingWithdrawals] = await Promise.all([
|
||||
context.prisma.referralLink.count({ where: { referrerId: user.id } }),
|
||||
context.prisma.bonusTransaction.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
context.prisma.rewardWithdrawalRequest.findMany({
|
||||
where: {
|
||||
requesterId: user.id,
|
||||
status: 'PENDING',
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const txSum = transactions.reduce((acc, tx) => acc + Number(tx.amount), 0);
|
||||
const pendingSum = pendingWithdrawals.reduce((acc, tx) => acc + Number(tx.amount), 0);
|
||||
|
||||
return {
|
||||
referrerId: user.id,
|
||||
availableBalance: txSum - pendingSum,
|
||||
referralsCount: links,
|
||||
transactions,
|
||||
pendingWithdrawals,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
registerSelf: (_, { input }, context) =>
|
||||
context.prisma.registrationRequest.create({
|
||||
data: {
|
||||
companyName: input.companyName,
|
||||
inn: input.inn,
|
||||
contactName: input.contactName,
|
||||
email: input.email,
|
||||
status: 'PENDING',
|
||||
},
|
||||
}),
|
||||
|
||||
reviewRegistrationRequest: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.registrationRequest.update({
|
||||
where: { id: input.requestId },
|
||||
data: {
|
||||
status: input.decision === 'APPROVE' ? 'APPROVED' : 'REJECTED',
|
||||
rejectionReason: input.decision === 'REJECT' ? input.rejectionReason ?? 'Rejected by manager' : null,
|
||||
reviewedById: manager.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
createInvitation: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const expiresInDays = input.expiresInDays > 0 ? input.expiresInDays : 7;
|
||||
const expiresAt = new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
return context.prisma.invitation.create({
|
||||
data: {
|
||||
token: invitationToken(),
|
||||
email: input.email,
|
||||
companyName: input.companyName,
|
||||
managerId: manager.id,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
acceptInvitation: async (_, { input }, context) => {
|
||||
const invitation = await context.prisma.invitation.findUnique({ where: { token: input.token } });
|
||||
if (!invitation) {
|
||||
throw new Error('Invitation token is invalid.');
|
||||
}
|
||||
if (invitation.acceptedAt) {
|
||||
throw new Error('Invitation has already been used.');
|
||||
}
|
||||
if (invitation.expiresAt < new Date()) {
|
||||
throw new Error('Invitation has expired.');
|
||||
}
|
||||
|
||||
const company = await context.prisma.company.upsert({
|
||||
where: { name: invitation.companyName },
|
||||
update: {},
|
||||
create: { name: invitation.companyName },
|
||||
});
|
||||
|
||||
const user = await context.prisma.user.create({
|
||||
data: {
|
||||
email: invitation.email,
|
||||
fullName: input.fullName,
|
||||
role: 'CLIENT',
|
||||
companyId: company.id,
|
||||
},
|
||||
});
|
||||
|
||||
await context.prisma.invitation.update({
|
||||
where: { id: invitation.id },
|
||||
data: {
|
||||
acceptedById: user.id,
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
connectMessenger: (_, { input }, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.messengerConnection.upsert({
|
||||
where: {
|
||||
userId_type_channelId: {
|
||||
userId: user.id,
|
||||
type: input.type,
|
||||
channelId: input.channelId,
|
||||
},
|
||||
},
|
||||
update: { isActive: true },
|
||||
create: {
|
||||
userId: user.id,
|
||||
type: input.type,
|
||||
channelId: input.channelId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
submitReadyOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
if (!input.items.length) {
|
||||
throw new Error('Order must contain at least one item.');
|
||||
}
|
||||
|
||||
const productIds = input.items.map((item) => item.productId);
|
||||
const products = await context.prisma.product.findMany({
|
||||
where: { id: { in: productIds }, isActive: true },
|
||||
});
|
||||
|
||||
if (products.length !== input.items.length) {
|
||||
throw new Error('Some products are invalid or inactive.');
|
||||
}
|
||||
|
||||
const productMap = new Map(products.map((product) => [product.id, product]));
|
||||
|
||||
const order = await context.prisma.order.create({
|
||||
data: {
|
||||
code: orderCode(),
|
||||
kind: 'READY',
|
||||
customerId: customer.id,
|
||||
status: 'NEW',
|
||||
items: {
|
||||
create: input.items.map((item) => {
|
||||
const product = productMap.get(item.productId);
|
||||
return {
|
||||
productId: item.productId,
|
||||
productName: product.name,
|
||||
quantity: item.quantity,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: true,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'NEW', customer.id, 'Ready order created by client');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
submitCalculationOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
const order = await context.prisma.order.create({
|
||||
data: {
|
||||
code: orderCode(),
|
||||
kind: 'CALCULATION',
|
||||
customerId: customer.id,
|
||||
status: 'NEW',
|
||||
calculationPayload: input.parameters,
|
||||
items: {
|
||||
create: [
|
||||
{
|
||||
productName: input.productName,
|
||||
quantity: input.quantity,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: true,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'NEW', customer.id, 'Calculation request created by client');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
managerSetOrderOffer: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'WAITING_DOUBLE_CONFIRM',
|
||||
deliveryTerms: input.deliveryTerms,
|
||||
deliveryFee: input.deliveryFee,
|
||||
totalPrice: input.totalPrice,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'WAITING_DOUBLE_CONFIRM', manager.id, 'Offer is published by manager');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
clientReviewOrder: async (_, { orderId, decision }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
|
||||
if (!order || order.customerId !== customer.id) {
|
||||
throw new Error('Order is not available for this client.');
|
||||
}
|
||||
|
||||
const status = decision === 'REJECT'
|
||||
? 'CLIENT_REJECTED'
|
||||
: order.managerApproved
|
||||
? 'CONFIRMED'
|
||||
: 'WAITING_DOUBLE_CONFIRM';
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
clientApproved: decision === 'APPROVE',
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(
|
||||
context.prisma,
|
||||
updated.id,
|
||||
status,
|
||||
customer.id,
|
||||
decision === 'APPROVE' ? 'Client approved offer' : 'Client rejected offer',
|
||||
);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
managerFinalizeOrder: async (_, { orderId, decision }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order) {
|
||||
throw new Error('Order was not found.');
|
||||
}
|
||||
|
||||
const status = decision === 'REJECT'
|
||||
? 'MANAGER_REJECTED'
|
||||
: order.clientApproved
|
||||
? 'CONFIRMED'
|
||||
: 'WAITING_DOUBLE_CONFIRM';
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
managerApproved: decision === 'APPROVE',
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(
|
||||
context.prisma,
|
||||
updated.id,
|
||||
status,
|
||||
manager.id,
|
||||
decision === 'APPROVE' ? 'Manager approved order' : 'Manager rejected order',
|
||||
);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
blockOrder: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'MANAGER_BLOCKED',
|
||||
blockReason: input.reason,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'MANAGER_BLOCKED', manager.id, input.reason);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
startOrderWork: async (_, { orderId }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order || order.status !== 'CONFIRMED') {
|
||||
throw new Error('Only confirmed order can be started.');
|
||||
}
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'IN_PROGRESS',
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'IN_PROGRESS', manager.id, 'Order moved to in-progress');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
completeOrder: async (_, { orderId }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order || order.status !== 'IN_PROGRESS') {
|
||||
throw new Error('Only in-progress order can be completed.');
|
||||
}
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'COMPLETED',
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'COMPLETED', manager.id, 'Order completed');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
createReferral: (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.referralLink.create({
|
||||
data: {
|
||||
referrerId: manager.id,
|
||||
refereeId: input.refereeUserId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addBonusTransaction: (_, { input }, context) => {
|
||||
requireRole(context, 'MANAGER');
|
||||
return context.prisma.bonusTransaction.create({
|
||||
data: {
|
||||
userId: input.userId,
|
||||
amount: input.amount,
|
||||
reason: input.reason,
|
||||
orderId: input.orderId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
requestRewardWithdrawal: (_, { input }, context) => {
|
||||
const client = requireRole(context, 'CLIENT');
|
||||
if (input.amount < 100) {
|
||||
throw new Error('Minimum withdrawal amount is 100.');
|
||||
}
|
||||
|
||||
return context.prisma.rewardWithdrawalRequest.create({
|
||||
data: {
|
||||
requesterId: client.id,
|
||||
amount: input.amount,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
reviewRewardWithdrawal: (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.rewardWithdrawalRequest.update({
|
||||
where: { id: input.withdrawalId },
|
||||
data: {
|
||||
reviewedById: manager.id,
|
||||
status: input.decision === 'APPROVE' ? 'APPROVED' : 'REJECTED',
|
||||
reviewComment: input.reviewComment,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
Product: {
|
||||
availableInWarehouses: (product) =>
|
||||
product.inventory.map((stock) => ({
|
||||
warehouse: stock.warehouse,
|
||||
availableQty: toFloat(stock.availableQty),
|
||||
})),
|
||||
},
|
||||
|
||||
Order: {
|
||||
deliveryFee: (order) => toFloat(order.deliveryFee),
|
||||
totalPrice: (order) => toFloat(order.totalPrice),
|
||||
},
|
||||
|
||||
OrderItem: {
|
||||
quantity: (item) => toFloat(item.quantity),
|
||||
},
|
||||
|
||||
BonusTransaction: {
|
||||
amount: (tx) => toFloat(tx.amount),
|
||||
},
|
||||
|
||||
RewardWithdrawalRequest: {
|
||||
amount: (tx) => toFloat(tx.amount),
|
||||
},
|
||||
};
|
||||
44
src/scalars.js
Normal file
44
src/scalars.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { GraphQLError, GraphQLScalarType, Kind } from 'graphql';
|
||||
|
||||
function parseJsonLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) return ast.value;
|
||||
if (ast.kind === Kind.BOOLEAN) return ast.value;
|
||||
if (ast.kind === Kind.INT || ast.kind === Kind.FLOAT) return Number(ast.value);
|
||||
if (ast.kind === Kind.NULL) return null;
|
||||
if (ast.kind === Kind.OBJECT) {
|
||||
return Object.fromEntries(ast.fields.map((field) => [field.name.value, parseJsonLiteral(field.value)]));
|
||||
}
|
||||
if (ast.kind === Kind.LIST) {
|
||||
return ast.values.map(parseJsonLiteral);
|
||||
}
|
||||
throw new GraphQLError('Unsupported JSON literal');
|
||||
}
|
||||
|
||||
export const dateTimeScalar = new GraphQLScalarType({
|
||||
name: 'DateTime',
|
||||
serialize(value) {
|
||||
return new Date(value).toISOString();
|
||||
},
|
||||
parseValue(value) {
|
||||
return new Date(value);
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new GraphQLError('DateTime must be a string');
|
||||
}
|
||||
return new Date(ast.value);
|
||||
},
|
||||
});
|
||||
|
||||
export const jsonScalar = new GraphQLScalarType({
|
||||
name: 'JSON',
|
||||
serialize(value) {
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
return parseJsonLiteral(ast);
|
||||
},
|
||||
});
|
||||
294
src/schema.graphql
Normal file
294
src/schema.graphql
Normal file
@@ -0,0 +1,294 @@
|
||||
scalar DateTime
|
||||
scalar JSON
|
||||
|
||||
enum UserRole {
|
||||
CLIENT
|
||||
MANAGER
|
||||
}
|
||||
|
||||
enum MessengerType {
|
||||
TELEGRAM
|
||||
MAX
|
||||
}
|
||||
|
||||
enum RegistrationStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum OrderKind {
|
||||
READY
|
||||
CALCULATION
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
NEW
|
||||
MANAGER_PROCESSING
|
||||
WAITING_DOUBLE_CONFIRM
|
||||
CLIENT_REJECTED
|
||||
MANAGER_REJECTED
|
||||
MANAGER_BLOCKED
|
||||
CONFIRMED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum WithdrawalStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum Decision {
|
||||
APPROVE
|
||||
REJECT
|
||||
}
|
||||
|
||||
type Company {
|
||||
id: ID!
|
||||
name: String!
|
||||
inn: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
fullName: String!
|
||||
role: UserRole!
|
||||
company: Company
|
||||
}
|
||||
|
||||
type Invitation {
|
||||
id: ID!
|
||||
token: String!
|
||||
email: String!
|
||||
companyName: String!
|
||||
managerId: ID!
|
||||
acceptedById: ID
|
||||
expiresAt: DateTime!
|
||||
acceptedAt: DateTime
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RegistrationRequest {
|
||||
id: ID!
|
||||
companyName: String!
|
||||
inn: String
|
||||
contactName: String!
|
||||
email: String!
|
||||
status: RegistrationStatus!
|
||||
rejectionReason: String
|
||||
reviewedById: ID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type MessengerConnection {
|
||||
id: ID!
|
||||
userId: ID!
|
||||
type: MessengerType!
|
||||
channelId: String!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type Warehouse {
|
||||
id: ID!
|
||||
code: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type ProductWarehouseBalance {
|
||||
warehouse: Warehouse!
|
||||
availableQty: Float!
|
||||
}
|
||||
|
||||
type Product {
|
||||
id: ID!
|
||||
sku: String!
|
||||
name: String!
|
||||
description: String
|
||||
isCustomizable: Boolean!
|
||||
isActive: Boolean!
|
||||
availableInWarehouses: [ProductWarehouseBalance!]!
|
||||
}
|
||||
|
||||
type OrderItem {
|
||||
id: ID!
|
||||
productId: ID
|
||||
productName: String!
|
||||
quantity: Float!
|
||||
}
|
||||
|
||||
type OrderStatusEvent {
|
||||
id: ID!
|
||||
status: OrderStatus!
|
||||
actorUserId: ID!
|
||||
note: String
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Order {
|
||||
id: ID!
|
||||
code: String!
|
||||
kind: OrderKind!
|
||||
status: OrderStatus!
|
||||
customerId: ID!
|
||||
managerId: ID
|
||||
clientApproved: Boolean
|
||||
managerApproved: Boolean
|
||||
blockReason: String
|
||||
deliveryTerms: String
|
||||
deliveryFee: Float
|
||||
totalPrice: Float
|
||||
calculationPayload: JSON
|
||||
items: [OrderItem!]!
|
||||
history: [OrderStatusEvent!]!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type ReferralLink {
|
||||
id: ID!
|
||||
referrerId: ID!
|
||||
refereeId: ID!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type BonusTransaction {
|
||||
id: ID!
|
||||
userId: ID!
|
||||
amount: Float!
|
||||
reason: String!
|
||||
orderId: ID
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RewardWithdrawalRequest {
|
||||
id: ID!
|
||||
requesterId: ID!
|
||||
amount: Float!
|
||||
status: WithdrawalStatus!
|
||||
reviewedById: ID
|
||||
reviewComment: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type ReferralStats {
|
||||
referrerId: ID!
|
||||
availableBalance: Float!
|
||||
referralsCount: Int!
|
||||
transactions: [BonusTransaction!]!
|
||||
pendingWithdrawals: [RewardWithdrawalRequest!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
healthcheck: String!
|
||||
me: User
|
||||
clientProducts: [Product!]!
|
||||
myOrders: [Order!]!
|
||||
myCurrentOrders: [Order!]!
|
||||
managerOrders(status: OrderStatus): [Order!]!
|
||||
registrationRequests(status: RegistrationStatus): [RegistrationRequest!]!
|
||||
referralStats: ReferralStats!
|
||||
}
|
||||
|
||||
input RegisterSelfInput {
|
||||
companyName: String!
|
||||
inn: String
|
||||
contactName: String!
|
||||
email: String!
|
||||
}
|
||||
|
||||
input ReviewRegistrationRequestInput {
|
||||
requestId: ID!
|
||||
decision: Decision!
|
||||
rejectionReason: String
|
||||
}
|
||||
|
||||
input CreateInvitationInput {
|
||||
email: String!
|
||||
companyName: String!
|
||||
expiresInDays: Int = 7
|
||||
}
|
||||
|
||||
input AcceptInvitationInput {
|
||||
token: String!
|
||||
fullName: String!
|
||||
}
|
||||
|
||||
input ConnectMessengerInput {
|
||||
type: MessengerType!
|
||||
channelId: String!
|
||||
}
|
||||
|
||||
input ReadyOrderItemInput {
|
||||
productId: ID!
|
||||
quantity: Float!
|
||||
}
|
||||
|
||||
input SubmitReadyOrderInput {
|
||||
items: [ReadyOrderItemInput!]!
|
||||
}
|
||||
|
||||
input SubmitCalculationOrderInput {
|
||||
productName: String!
|
||||
quantity: Float!
|
||||
parameters: JSON!
|
||||
}
|
||||
|
||||
input SetOrderOfferInput {
|
||||
orderId: ID!
|
||||
deliveryTerms: String!
|
||||
deliveryFee: Float!
|
||||
totalPrice: Float!
|
||||
}
|
||||
|
||||
input BlockOrderInput {
|
||||
orderId: ID!
|
||||
reason: String!
|
||||
}
|
||||
|
||||
input CreateReferralInput {
|
||||
refereeUserId: ID!
|
||||
}
|
||||
|
||||
input AddBonusTransactionInput {
|
||||
userId: ID!
|
||||
amount: Float!
|
||||
reason: String!
|
||||
orderId: ID
|
||||
}
|
||||
|
||||
input RequestRewardWithdrawalInput {
|
||||
amount: Float!
|
||||
}
|
||||
|
||||
input ReviewRewardWithdrawalInput {
|
||||
withdrawalId: ID!
|
||||
decision: Decision!
|
||||
reviewComment: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
registerSelf(input: RegisterSelfInput!): RegistrationRequest!
|
||||
reviewRegistrationRequest(input: ReviewRegistrationRequestInput!): RegistrationRequest!
|
||||
createInvitation(input: CreateInvitationInput!): Invitation!
|
||||
acceptInvitation(input: AcceptInvitationInput!): User!
|
||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||
|
||||
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
||||
submitCalculationOrder(input: SubmitCalculationOrderInput!): Order!
|
||||
managerSetOrderOffer(input: SetOrderOfferInput!): Order!
|
||||
clientReviewOrder(orderId: ID!, decision: Decision!): Order!
|
||||
managerFinalizeOrder(orderId: ID!, decision: Decision!): Order!
|
||||
blockOrder(input: BlockOrderInput!): Order!
|
||||
startOrderWork(orderId: ID!): Order!
|
||||
completeOrder(orderId: ID!): Order!
|
||||
|
||||
createReferral(input: CreateReferralInput!): ReferralLink!
|
||||
addBonusTransaction(input: AddBonusTransactionInput!): BonusTransaction!
|
||||
requestRewardWithdrawal(input: RequestRewardWithdrawalInput!): RewardWithdrawalRequest!
|
||||
reviewRewardWithdrawal(input: ReviewRewardWithdrawalInput!): RewardWithdrawalRequest!
|
||||
}
|
||||
50
src/server.js
Normal file
50
src/server.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@as-integrations/express5';
|
||||
|
||||
import { buildContext } from './context.js';
|
||||
import { prisma } from './prisma-client.js';
|
||||
import { resolvers } from './resolvers.js';
|
||||
|
||||
const typeDefs = readFileSync(new URL('./schema.graphql', import.meta.url), 'utf8');
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '1mb' }));
|
||||
|
||||
app.get('/healthz', (_, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
expressMiddleware(server, {
|
||||
context: async ({ req }) => buildContext(req),
|
||||
}),
|
||||
);
|
||||
|
||||
const port = Number(process.env.PORT ?? 4000);
|
||||
app.listen(port, () => {
|
||||
console.log(`apollo-backend running at http://localhost:${port}/graphql`);
|
||||
});
|
||||
|
||||
async function shutdown() {
|
||||
await server.stop();
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user