graphql api tested & user resolver, schema and mongo interfaces tested

This commit is contained in:
Berkay 2025-11-14 10:30:01 +03:00
parent 053586c5cc
commit 45f6b7a1ef
26 changed files with 1211 additions and 489 deletions

View File

@ -2,10 +2,9 @@ FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm install --legacy-peer-deps
COPY . .
# RUN npm run dev
EXPOSE 3000
CMD ["npm", "run", "dev"]

View File

@ -1,9 +1,8 @@
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { NextRequest } from 'next/server';
import { typeDefs, resolvers } from '@/graphql';
const server = new ApolloServer({ typeDefs, resolvers, introspection: process.env.NODE_ENV === 'development' });
const handler = startServerAndCreateNextHandler(server, { context: async (req: NextRequest) => ({ req }) });
const handler = startServerAndCreateNextHandler(server as any, { context: async (req) => ({ req }) });
export { handler as GET, handler as POST };

0
app/api/users/route.ts Normal file
View File

View File

@ -6,12 +6,12 @@ services:
dockerfile: Dockerfile
ports:
- "3000:3000"
networks:
- dbConnect
env_file:
- .env
depends_on:
- storage
environment:
- name=value
volumes:
- .:/app
- /app/node_modules
@ -20,10 +20,20 @@ services:
image: mongo:6
container_name: storage
restart: always
networks:
- dbConnect
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secret123
MONGO_INITDB_DATABASE: evyosdb
ports:
- "27017:27017"
volumes:
- storage_data:/data/db
networks:
dbConnect:
driver: bridge
volumes:
storage_data:

View File

@ -1,6 +1,8 @@
import { mergeTypeDefs, mergeResolvers } from "@graphql-tools/merge";
import { userTypeDefs } from "./schema/userSchema";
import { userTypeDefs } from "./schema/userTypeDefs";
import { userResolvers } from "./resolvers/userResolvers";
import { personTypeDefs } from "./schema/personTypeDefs";
import { personResolvers } from "./resolvers/personResolvers";
export const typeDefs = mergeTypeDefs([userTypeDefs]);
export const resolvers = mergeResolvers([userResolvers]);
export const typeDefs = mergeTypeDefs([userTypeDefs, personTypeDefs]);
export const resolvers = mergeResolvers([userResolvers, personResolvers]);

View File

@ -1,11 +0,0 @@
import clientPromise from '@/lib/mongodb';
export const resolvers = {
Query: {
users: async () => {
const client = await clientPromise;
const db = client.db('test');
return db.collection('users').find().toArray();
},
},
};

View File

@ -0,0 +1,37 @@
import { Person } from "@/models/People";
import { GraphQLError } from "graphql";
export const personResolvers = {
Query: {
persons: async () => {
try {
return await Person.find();
} catch (err: any) {
throw new GraphQLError(err.message);
}
},
person: async (_: any, { id }: { id: string }) => {
try {
const person = await Person.findById(id);
if (!person) throw new GraphQLError("Person not found");
return person;
} catch (err: any) {
throw new GraphQLError(err.message);
}
},
},
Mutation: {
createPerson: async (_: any, { input }: any) => {
try {
const person = await Person.create({
...input,
birthDate: new Date(input.birthDate),
});
return person;
} catch (err: any) {
throw new GraphQLError(err.message);
}
},
},
};

View File

@ -1,19 +1,22 @@
// graphql/resolvers/userResolvers.ts
import { connectDB } from '@/lib/mongodb';
import User from '@/models/User';
import { Users, IUser } from '@/models/Users';
import { Types } from 'mongoose';
export const userResolvers = {
Query: {
users: async () => {
await connectDB();
const users = await User.find().lean();
const users = await Users.find().populate("person").lean();
return users;
},
},
Mutation: {
addUser: async (_: any, { name, email }: { name: string; email: string }) => {
createUser: async (parent: any, args: { input: IUser }) => {
await connectDB();
const user = new User({ name, email });
const user = new Users({
...args.input,
person: new Types.ObjectId(args.input.person),
});
await user.save();
return user;
},

View File

@ -1,12 +0,0 @@
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
_id: ID!
name: String!
}
type Query {
users: [User!]!
}
`;

View File

@ -0,0 +1,49 @@
import { gql } from "graphql-tag";
export const personTypeDefs = gql`
scalar Date
type Person {
id: ID!
firstName: String!
surname: String!
middleName: String!
sexCode: String!
personRef: String!
personTag: String!
fatherName: String!
motherName: String!
countryCode: String!
nationalIdentityId: String!
birthPlace: String!
birthDate: Date!
taxNo: String!
birthname: String!
}
input CreatePersonInput {
firstName: String!
surname: String!
middleName: String!
sexCode: String!
personRef: String!
personTag: String!
fatherName: String!
motherName: String!
countryCode: String!
nationalIdentityId: String!
birthPlace: String!
birthDate: Date!
taxNo: String!
birthname: String!
}
type Query {
persons: [Person!]!
person(id: ID!): Person
}
type Mutation {
createPerson(input: CreatePersonInput!): Person!
}
`;

View File

@ -1,13 +0,0 @@
import { gql } from "graphql-tag";
export const userTypeDefs = gql`
type User {
_id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
}
`;

View File

@ -0,0 +1,75 @@
import { gql } from "graphql-tag";
export const userTypeDefs = gql`
"""Represents a single token entry with a prefix and value"""
type CollectionTokenItem {
prefix: String!
token: String!
}
"""Represents the collection of tokens assigned to a user"""
type CollectionToken {
tokens: [CollectionTokenItem!]!
default: String!
}
type Person {
id: ID!
firstName: String!
surname: String!
middleName: String!
}
"""User model with references and metadata"""
type User {
id: ID!
uuid: String!
expiresAt: String
resetToken: String
password: String!
history: [String!]
tag: String!
email: String!
phone: String!
collectionTokens: CollectionToken!
person: Person!
type: ID
createdAt: String
updatedAt: String
}
"""Input type for a single token entry"""
input CollectionTokenItemInput {
prefix: String!
token: String!
}
"""Input type for a user's token collection"""
input CollectionTokenInput {
tokens: [CollectionTokenItemInput!]!
default: String!
}
"""Input for creating a new user"""
input CreateUserInput {
password: String!
history: [String!]
tag: String!
email: String!
phone: String!
collectionTokens: CollectionTokenInput!
person: ID!
type: ID
}
"""Queries"""
type Query {
users: [User!]!
user(id: ID!): User
}
"""Mutations"""
type Mutation {
createUser(input: CreateUserInput!): User!
}
`;

View File

@ -1,7 +1,14 @@
// lib/mongodb.ts
import mongoose from 'mongoose';
import mongoose from "mongoose";
export const connectDB = async () => {
if (mongoose.connection.readyState >= 1) return;
await mongoose.connect(process.env.MONGO_URI!);
};
declare global { var mongoose: { conn: mongoose.Mongoose | null; promise: Promise<mongoose.Mongoose> | null } }
let cached = global.mongoose;
if (!cached) { cached = global.mongoose = { conn: null, promise: null } }
export async function connectDB() {
if (cached.conn) return cached.conn;
if (!process.env.MONGODB_URI) { throw new Error("Please define MONGODB_URI in your environment variables") }
if (!cached.promise) { cached.promise = mongoose.connect(process.env.MONGODB_URI).then((mongoose) => mongoose) }
cached.conn = await cached.promise;
return cached.conn;
}

61
models/Build.ts Normal file
View File

@ -0,0 +1,61 @@
import mongoose, { Schema, Document, models } from "mongoose";
import { Base } from "./base";
const BuildIbanSchema = new Schema({
iban: { type: String, required: true },
startDate: { type: Date, required: true },
stopDate: { type: Date, required: true, default: new Date("2900-01-01T03:00:00+03:00") },
bankCode: { type: String, required: true, default: "TR0000000000000" },
xcomment: { type: String, required: true, default: "????" },
});
const BuildResponsibleSchema = new Schema({
company: { type: Schema.Types.ObjectId, ref: "Company", required: true },
person: { type: Schema.Types.ObjectId, ref: "Person", required: true },
});
const BuildInfoSchema = new Schema({
govAddressCode: { type: String, required: true },
buildName: { type: String, required: true },
buildNo: { type: String, required: true },
maxFloor: { type: Number, required: true },
undergroundFloor: { type: Number, required: true },
buildDate: { type: Date, required: true },
decisionPeriodDate: { type: Date, required: true },
taxNo: { type: String, required: true },
liftCount: { type: Number, required: true },
heatingSystem: { type: Boolean, required: true },
coolingSystem: { type: Boolean, required: true },
hotWaterSystem: { type: Boolean, required: true },
blockServiceManCount: { type: Number, required: true },
securityServiceManCount: { type: Number, required: true },
garageCount: { type: Number, required: true },
managementRoomId: { type: Number, required: true },
});
interface IBuildInterface extends Base, Document {
buildType: Schema.Types.ObjectId;
collectionToken: string;
info: typeof BuildInfoSchema;
site?: Schema.Types.ObjectId;
address?: Schema.Types.ObjectId;
ibans?: typeof BuildIbanSchema[];
areas?: Schema.Types.ObjectId[];
responsibles?: typeof BuildResponsibleSchema[];
}
const BuildSchema = new Schema<IBuildInterface>({
buildType: { type: Schema.Types.ObjectId, ref: "BuildType", required: true },
collectionToken: { type: String, required: true, unique: true },
info: { type: BuildInfoSchema, required: true },
site: { type: Schema.Types.ObjectId, ref: "Site", required: false },
address: { type: Schema.Types.ObjectId, ref: "BuildAddress", required: false },
areas: [{ type: Schema.Types.ObjectId, ref: "BuildArea", required: false }],
ibans: [{ type: BuildIbanSchema, required: false }],
responsibles: [{ type: BuildResponsibleSchema, required: false }],
}, { timestamps: true });
const Build = models.Build || mongoose.model<IBuildInterface>("Build", BuildSchema);
export { Build };
export type { IBuildInterface };

31
models/BuildAddress.ts Normal file
View File

@ -0,0 +1,31 @@
import mongoose, { Schema, Document, models } from "mongoose";
import { Base } from "./base";
interface IBuildAddressInterface extends Base, Document {
buildNumber: string;
doorNumber: string;
floorNumber: string;
commentAddress: string;
letterAddress: string;
shortLetterAddress: string;
latitude: number;
longitude: number;
street: mongoose.Types.ObjectId;
}
const BuildAddressSchema = new Schema<IBuildAddressInterface>({
buildNumber: { type: String, required: true },
doorNumber: { type: String, required: true },
floorNumber: { type: String, required: true },
commentAddress: { type: String, required: true },
letterAddress: { type: String, required: true },
shortLetterAddress: { type: String, required: true },
latitude: { type: Number, required: true },
longitude: { type: Number, required: true },
street: { type: Schema.Types.ObjectId, ref: "Street", required: true },
}, { timestamps: true });
const BuildAddress = models.BuildAddress || mongoose.model<IBuildAddressInterface>("BuildAddress", BuildAddressSchema);
export { BuildAddress };
export type { IBuildAddressInterface };

25
models/BuildArea.ts Normal file
View File

@ -0,0 +1,25 @@
import mongoose, { Schema, Document, models } from "mongoose";
import { Base } from "./base";
interface IBuildArea extends Base, Document {
build: mongoose.Types.ObjectId;
area: number;
size: number;
type: string;
typeToken: string;
description: string;
}
const BuildAreaSchema = new Schema<IBuildArea>({
build: { type: Schema.Types.ObjectId, ref: "Build", required: true },
area: { type: Number, required: true },
size: { type: Number, required: true },
type: { type: String, required: true },
typeToken: { type: String, required: true },
description: { type: String, required: true },
}, { timestamps: true });
const BuildArea = models.BuildArea || mongoose.model<IBuildArea>("BuildArea", BuildAreaSchema);
export { BuildArea, BuildAreaSchema };
export type { IBuildArea };

37
models/BuildParts.ts Normal file
View File

@ -0,0 +1,37 @@
import mongoose, { Schema, Document, models } from "mongoose";
import { Base } from "./base";
interface IBuildPartsInterface extends Base, Document {
addressGovCode: string;
no: number;
level: number;
code: string;
grossSize: number;
netSize: number;
defaultAccessory: string;
humanLivability: boolean;
key: string;
directionId: Schema.Types.ObjectId;
typeId: Schema.Types.ObjectId;
}
const BuildPartsSchema = new Schema({
buildId: { type: Schema.Types.ObjectId, ref: "Build", required: true },
addressGovCode: { type: String, required: true },
no: { type: Number, required: true },
level: { type: Number, required: true },
code: { type: String, required: true },
grossSize: { type: Number, required: true },
netSize: { type: Number, required: true },
defaultAccessory: { type: String, required: true },
humanLivability: { type: Boolean, required: true },
key: { type: String, required: true },
directionId: { type: Schema.Types.ObjectId, ref: "ApiEnumDropdown", required: true },
typeId: { type: Schema.Types.ObjectId, ref: "ApiEnumDropdown", required: true },
});
const BuildParts = models.BuildParts || mongoose.model<IBuildPartsInterface>("BuildParts", BuildPartsSchema);
export { BuildParts };
export type { IBuildPartsInterface };

7
models/Company.ts Normal file
View File

@ -0,0 +1,7 @@
interface ICompanyInterface {
}
export type { ICompanyInterface };

44
models/LivingSpaces.ts Normal file
View File

@ -0,0 +1,44 @@
import mongoose, { Schema, Model, Document } from 'mongoose';
import { IPerson } from './People';
import { IBuildPartsInterface } from './BuildParts';
import { ICompanyInterface } from './Company';
import { Base } from './base';
import { IUserType } from './UserTypes';
import { randomUUID } from 'crypto';
interface ILivingSpaces extends Base, Document {
part: mongoose.Types.ObjectId | IBuildPartsInterface;
company: mongoose.Types.ObjectId | ICompanyInterface;
person: mongoose.Types.ObjectId | IPerson;
userType: mongoose.Types.ObjectId | IUserType;
}
function getDynamicLivingSpaceModel(collectionToken: string): Model<ILivingSpaces> {
const collectionName = `LivingSpaces${collectionToken}`;
if (mongoose.models[collectionName]) { return mongoose.models[collectionName] as Model<ILivingSpaces> }
const LivingSpacesSchema = new Schema<ILivingSpaces>({
uuid: { type: String, required: false, unique: true, default: () => randomUUID() },
part: { type: Schema.Types.ObjectId, ref: "BuildParts", required: true },
company: { type: Schema.Types.ObjectId, ref: "Company", required: true },
person: { type: Schema.Types.ObjectId, ref: "Person", required: true },
userType: { type: Schema.Types.ObjectId, ref: "UserType", required: true },
expiryStarts: { type: Date, required: false, default: new Date() },
expiryEnds: { type: Date, required: false, default: new Date("2900-01-01T03:00:00+03:00") },
isConfirmed: { type: Boolean, required: false, default: false },
deleted: { type: Boolean, required: false, default: false },
active: { type: Boolean, required: false, default: true },
crypUuId: { type: String, required: false },
createdCredentialsToken: { type: String, required: false },
updatedCredentialsToken: { type: String, required: false },
confirmedCredentialsToken: { type: String, required: false },
isNotificationSend: { type: Boolean, required: false, default: false },
isEmailSend: { type: Boolean, required: false, default: false },
refInt: { type: Number, required: false, default: 0 },
refId: { type: String, required: false },
replicationId: { type: Number, required: false, default: 0 },
}, { timestamps: true });
return mongoose.model<ILivingSpaces>(collectionName, LivingSpacesSchema, collectionName);
}
export { getDynamicLivingSpaceModel };
export type { ILivingSpaces };

41
models/People.ts Normal file
View File

@ -0,0 +1,41 @@
import mongoose, { Schema, models, Document, Model } from "mongoose";
import { Base } from "./base";
interface IPerson extends Base, Document {
firstName: string;
surname: string;
middleName: string;
sexCode: string;
personRef: string;
personTag: string;
fatherName: string;
motherName: string;
countryCode: string;
nationalIdentityId: string;
birthPlace: string;
birthDate: Date;
taxNo: string;
birthname: string;
}
const PersonSchema = new Schema<IPerson>({
firstName: { type: String, required: true },
surname: { type: String, required: true },
middleName: { type: String, required: true },
sexCode: { type: String, required: true },
personRef: { type: String, required: true },
personTag: { type: String, required: true },
fatherName: { type: String, required: true },
motherName: { type: String, required: true },
countryCode: { type: String, required: true },
nationalIdentityId: { type: String, required: true },
birthPlace: { type: String, required: true },
birthDate: { type: Date, required: true },
taxNo: { type: String, required: true },
birthname: { type: String, required: true },
});
const Person: Model<IPerson> = models.Person || mongoose.model<IPerson>("Person", PersonSchema);
export { Person };
export type { IPerson }

View File

@ -1,10 +0,0 @@
import mongoose, { Schema, models } from 'mongoose';
const UserSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
});
const User = models.User || mongoose.model('User', UserSchema);
export default User;

20
models/UserTypes.ts Normal file
View File

@ -0,0 +1,20 @@
import mongoose, { Schema, models } from "mongoose";
interface IUserType {
type: string;
token: string;
typeToken: string;
description: string;
}
const UserTypesSchema = new Schema<IUserType>({
type: { type: String, required: true },
token: { type: String, required: true },
typeToken: { type: String, required: true },
description: { type: String, required: true },
});
const UserTypes = models.UserTypes || mongoose.model("UserTypes", UserTypesSchema);
export { UserTypes };
export type { IUserType };

62
models/Users.ts Normal file
View File

@ -0,0 +1,62 @@
import mongoose, { Schema, models, Document } from "mongoose";
import { BaseSchema, Base } from "./base";
const expiresDate = () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
interface ICollectionToken {
tokens: { prefix: string; token: string }[];
default: string;
}
interface IUser extends Base, Document {
expiresAt?: Date;
resetToken?: string;
password: string;
history?: string[];
tag: string;
email: string;
phone: string;
collectionTokens: ICollectionToken;
person: mongoose.Types.ObjectId;
type?: mongoose.Types.ObjectId;
}
const CollectionTokenSchema = new Schema<ICollectionToken>(
{
tokens: { type: [{ prefix: { type: String, required: true }, token: { type: String, required: true } }], default: [], required: false },
default: { type: String, required: true, default: "" },
},
{ _id: false }
);
const UsersSchema = new Schema<IUser>(
{
...BaseSchema,
expiresAt: { type: Date, default: expiresDate, required: false },
resetToken: { type: String, required: false },
password: { type: String, required: true },
history: {
type: [String],
validate: [(val: string[]) => val.length <= 3, "History can have max 3 items"],
default: [],
required: false,
},
tag: { type: String, required: true },
email: { type: String, required: true, unique: true },
phone: { type: String, required: true, unique: true },
collectionTokens: {
type: CollectionTokenSchema,
default: () => ({ tokens: [], default: "" }),
required: true,
},
person: { type: Schema.Types.ObjectId, ref: "Person", required: true },
type: { type: Schema.Types.ObjectId, ref: "UserType", required: false },
},
{ timestamps: true }
);
const Users = models.Users || mongoose.model("Users", UsersSchema);
export { Users };
export type { IUser, ICollectionToken };

47
models/base.ts Normal file
View File

@ -0,0 +1,47 @@
import { randomUUID } from "crypto";
interface Base {
uuid: string;
expiryStarts: Date;
expiryEnds: Date;
createdAt: Date;
updatedAt: Date;
isConfirmed: boolean;
deleted: boolean;
active: boolean;
crypUuId: string;
createdCredentialsToken: string;
updatedCredentialsToken: string;
confirmedCredentialsToken: string;
isNotificationSend: boolean;
isEmailSend: boolean;
refInt: number;
refId: string;
replicationId: number;
}
const BaseSchema = {
uuid: { type: String, required: false, unique: true, default: randomUUID },
expiryStarts: { type: Date, required: false, default: () => new Date(Date.now()) },
expiryEnds: { type: Date, required: false, default: () => new Date('2099-12-31') },
isConfirmed: { type: Boolean, required: false, default: false },
deleted: { type: Boolean, required: false, default: false },
active: { type: Boolean, required: false, default: true },
crypUuId: { type: String, required: false, default: randomUUID },
createdCredentialsToken: { type: String, required: false, default: randomUUID },
updatedCredentialsToken: { type: String, required: false, default: randomUUID },
confirmedCredentialsToken: { type: String, required: false, default: randomUUID },
isNotificationSend: { type: Boolean, required: false, default: false },
isEmailSend: { type: Boolean, required: false, default: false },
refInt: { type: Number, required: false, default: 0 },
refId: { type: String, required: false, default: randomUUID },
replicationId: { type: Number, required: false, default: 0 },
};
export { BaseSchema };
export type { Base };

1052
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,15 +9,15 @@
"lint": "eslint"
},
"dependencies": {
"@apollo/server": "^5.1.0",
"@as-integrations/next": "^4.1.0",
"@apollo/server": "^4.10.0",
"@as-integrations/next": "^3.0.0",
"@auth/mongodb-adapter": "^3.11.1",
"@graphql-tools/merge": "^9.1.5",
"@tanstack/react-query": "^5.90.8",
"apollo-server-micro": "^3.13.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"graphql": "^16.12.0",
"graphql": "^16.8.1",
"lucide-react": "^0.553.0",
"mongoose": "^8.19.3",
"next": "16.0.2",