diff --git a/.cursor/rules/mongo-lib.mdc b/.cursor/rules/mongo-lib.mdc new file mode 100644 index 0000000..710da37 --- /dev/null +++ b/.cursor/rules/mongo-lib.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: +alwaysApply: true +--- + +# Your rule content + +- This Project is a private npm package. it's for all projects to interact with mongo db. +- Always use clean code and design patterns. +- Alwyas propose the codes base on the current strctour. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e220994..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,6 +0,0 @@ -# [3.11.0](https://github.com/TogetherCrew/mongo-lib/compare/v3.10.0...v3.11.0) (2025-05-28) - - -### Features - -* add upsert repo ([a20f695](https://github.com/TogetherCrew/mongo-lib/commit/a20f695d91b8e70a4e86fe60074e6936c68a4f15)) diff --git a/__tests__/unit/models/thread.model.test.ts b/__tests__/unit/models/thread.model.test.ts new file mode 100644 index 0000000..2893fa0 --- /dev/null +++ b/__tests__/unit/models/thread.model.test.ts @@ -0,0 +1,96 @@ +import { IThread } from '../../../src/interfaces'; +import { Thread } from '../../../src/models'; + +describe('Thread model', () => { + describe('thread validation', () => { + let thread: IThread; + beforeEach(() => { + thread = { + id: '123456789012345678', + type: 11, + guild_id: '987654321098765432', + parent_id: '111111111111111111', + owner_id: '222222222222222222', + name: 'Test Thread', + last_message_id: '333333333333333333', + message_count: 5, + member_count: 3, + rate_limit_per_user: 0, + thread_metadata: { + archived: false, + auto_archive_duration: 1440, + archive_timestamp: '2024-01-01T00:00:00.000Z', + locked: false, + invitable: true, + }, + total_message_sent: 5, + flags: 0, + applied_tags: ['444444444444444444'], + nsfw: false, + }; + }); + + test('should correctly validate a valid Thread data', async () => { + await expect(new Thread(thread).validate()).resolves.toBeUndefined(); + }); + + test('should fail validation with invalid thread type', async () => { + thread.type = 5 as any; + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should fail validation with invalid auto_archive_duration', async () => { + thread.thread_metadata.auto_archive_duration = 999; + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should fail validation with name too long', async () => { + thread.name = 'x'.repeat(101); + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should fail validation with negative message_count', async () => { + thread.message_count = -1; + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should fail validation with member_count over limit', async () => { + thread.member_count = 51; + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should fail validation with rate_limit_per_user over limit', async () => { + thread.rate_limit_per_user = 21601; + await expect(new Thread(thread).validate()).rejects.toThrow(); + }); + + test('should handle ANNOUNCEMENT_THREAD type', async () => { + thread.type = 10; + await expect(new Thread(thread).validate()).resolves.toBeUndefined(); + }); + + test('should handle PRIVATE_THREAD type', async () => { + thread.type = 12; + thread.thread_metadata.invitable = false; + await expect(new Thread(thread).validate()).resolves.toBeUndefined(); + }); + + test('should handle minimal required data', async () => { + const minimalThread = { + id: '123456789012345678', + type: 11, + guild_id: '987654321098765432', + parent_id: '111111111111111111', + owner_id: '222222222222222222', + name: 'Minimal Thread', + thread_metadata: { + archived: false, + auto_archive_duration: 1440, + archive_timestamp: '2024-01-01T00:00:00.000Z', + locked: false, + }, + }; + await expect(new Thread(minimalThread).validate()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/src/interfaces/Announcement.interface.ts b/src/interfaces/Announcement.interface.ts index b239198..af6aa64 100644 --- a/src/interfaces/Announcement.interface.ts +++ b/src/interfaces/Announcement.interface.ts @@ -1,4 +1,4 @@ -import { type Model, type Types, type ObjectId } from 'mongoose'; +import { Model, ObjectId, Types } from 'mongoose'; interface IDiscordOptions { channelIds?: string[]; @@ -30,10 +30,6 @@ export interface IAnnouncement { data: Array>; } -export interface IAnnouncementMethods { - softDelete: (userId: ObjectId) => void; -} - -export interface AnnouncementModel extends Model, IAnnouncementMethods> { +export interface AnnouncementModel extends Model { paginate: (filter: object, options: object) => any; } diff --git a/src/interfaces/Channel.interface.ts b/src/interfaces/Channel.interface.ts index fcf6601..5453e93 100644 --- a/src/interfaces/Channel.interface.ts +++ b/src/interfaces/Channel.interface.ts @@ -25,7 +25,7 @@ export interface IChannelUpdateBody { type?: number; } export interface IChannelMethods { - softDelete: () => void; + softDelete: () => Promise; } export interface ChannelPayload { diff --git a/src/interfaces/GuildMember.interface.ts b/src/interfaces/GuildMember.interface.ts index 573f84c..002ebb4 100644 --- a/src/interfaces/GuildMember.interface.ts +++ b/src/interfaces/GuildMember.interface.ts @@ -47,7 +47,7 @@ export interface GuildMemberPayload { } export interface IGuildMemberMethods { - softDelete: () => void; + softDelete: () => Promise; } export interface GuildMemberModel extends Model, IGuildMemberMethods> { diff --git a/src/interfaces/Platfrom.interface.ts b/src/interfaces/Platfrom.interface.ts index 0a2d274..ebdab4f 100644 --- a/src/interfaces/Platfrom.interface.ts +++ b/src/interfaces/Platfrom.interface.ts @@ -1,5 +1,6 @@ -import { type Model, type Types } from 'mongoose'; -import { type PlatformNames } from '../config/enums'; +import { Model, Types } from 'mongoose'; + +import { PlatformNames } from '../config/enums'; export interface IPlatform { name: PlatformNames; diff --git a/src/interfaces/Role.interface.ts b/src/interfaces/Role.interface.ts index b0b0b1c..5ae3e2c 100644 --- a/src/interfaces/Role.interface.ts +++ b/src/interfaces/Role.interface.ts @@ -15,7 +15,7 @@ export interface IRoleUpdateBody { } export interface IRoleMethods { - softDelete: () => void; + softDelete: () => Promise; } export interface RolePayload { diff --git a/src/interfaces/Thread.interface.ts b/src/interfaces/Thread.interface.ts new file mode 100644 index 0000000..e108e76 --- /dev/null +++ b/src/interfaces/Thread.interface.ts @@ -0,0 +1,51 @@ +import { Snowflake } from 'discord.js'; +import { Model } from 'mongoose'; + +export interface IThreadMetadata { + archived: boolean; + auto_archive_duration: number; + archive_timestamp: string; + locked: boolean; + invitable?: boolean; + create_timestamp?: string; +} + +export interface IThread { + id: Snowflake; + type: 10 | 11 | 12; + guild_id: Snowflake; + parent_id: Snowflake; + owner_id: Snowflake; + name: string; + last_message_id?: Snowflake | null; + message_count?: number; + member_count?: number; + rate_limit_per_user?: number; + thread_metadata: IThreadMetadata; + total_message_sent?: number; + flags?: number; + applied_tags?: Snowflake[]; + nsfw?: boolean; + deletedAt?: Date | null; +} + +export interface IThreadUpdateBody { + name?: string; + archived?: boolean; + auto_archive_duration?: number; + locked?: boolean; + invitable?: boolean; + rate_limit_per_user?: number; + flags?: number; + applied_tags?: Snowflake[]; + nsfw?: boolean; + deletedAt?: Date | null; +} + +export interface IThreadMethods { + softDelete: () => Promise; +} + +export interface ThreadModel extends Model, IThreadMethods> { + paginate: (filter: object, options: object) => any; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 3bfa5fb..0d3d1b9 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -11,3 +11,4 @@ export * from './Community.interface'; export * from './Platfrom.interface'; export * from './Announcement.interface'; export * from './Module.interface'; +export * from './Thread.interface'; diff --git a/src/models/Thread.model.ts b/src/models/Thread.model.ts new file mode 100644 index 0000000..6947cb2 --- /dev/null +++ b/src/models/Thread.model.ts @@ -0,0 +1,6 @@ +import { model } from 'mongoose'; + +import { IThread, ThreadModel } from '../interfaces'; +import { threadSchema } from './schemas'; + +export default model('Thread', threadSchema); diff --git a/src/models/index.ts b/src/models/index.ts index 3d7c110..496dc19 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,15 +1,17 @@ -import User from './User.model'; -import Token from './Token.model'; -import HeatMap from './HeatMap.model'; -import RawInfo from './RawInfo.model'; -import MemberActivity from './memberActivity.model'; -import GuildMember from './GuildMember.model'; +import Announcement from './Announcement.model'; import Channel from './Channel.model'; -import Role from './Role.model'; import Community from './Community.model'; -import Platform from './Platfrom.model'; -import Announcement from './Announcement.model'; +import GuildMember from './GuildMember.model'; +import HeatMap from './HeatMap.model'; +import MemberActivity from './memberActivity.model'; import Module from './Module.model'; +import Platform from './Platfrom.model'; +import RawInfo from './RawInfo.model'; +import Role from './Role.model'; +import Thread from './Thread.model'; +import Token from './Token.model'; +import User from './User.model'; + export { User, Token, @@ -23,4 +25,5 @@ export { Platform, Announcement, Module, + Thread, }; diff --git a/src/models/schemas/Channel.schema.ts b/src/models/schemas/Channel.schema.ts index 7e87cd1..511a4bf 100644 --- a/src/models/schemas/Channel.schema.ts +++ b/src/models/schemas/Channel.schema.ts @@ -1,6 +1,7 @@ import { Schema } from 'mongoose'; -import { toJSON, paginate } from './plugins'; -import { type IChannel, type ChannelModel } from '../../interfaces'; + +import { ChannelModel, IChannel } from '../../interfaces'; +import { paginate, toJSON } from './plugins'; const channelSchema = new Schema({ channelId: { @@ -18,7 +19,7 @@ const channelSchema = new Schema({ }, permissionOverwrites: [ { - id: String, // or use mongoose.Schema.Types.ObjectId if Snowflake is an ObjectId + id: String, type: { type: Number, enum: [0, 1], diff --git a/src/models/schemas/Thread.schema.ts b/src/models/schemas/Thread.schema.ts new file mode 100644 index 0000000..002fd72 --- /dev/null +++ b/src/models/schemas/Thread.schema.ts @@ -0,0 +1,130 @@ +import { Schema } from 'mongoose'; + +import { IThread, ThreadModel } from '../../interfaces'; +import { paginate, toJSON } from './plugins'; + +const threadMetadataSchema = new Schema( + { + archived: { + type: Boolean, + required: true, + default: false, + }, + auto_archive_duration: { + type: Number, + required: true, + enum: [60, 1440, 4320, 10080], + }, + archive_timestamp: { + type: String, + required: true, + }, + locked: { + type: Boolean, + required: true, + default: false, + }, + invitable: { + type: Boolean, + default: null, + }, + create_timestamp: { + type: String, + default: null, + }, + }, + { _id: false }, +); + +const threadSchema = new Schema( + { + id: { + type: String, + required: true, + unique: true, + }, + type: { + type: Number, + required: true, + enum: [10, 11, 12], + }, + guild_id: { + type: String, + required: true, + }, + parent_id: { + type: String, + required: true, + }, + owner_id: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + minlength: 1, + maxlength: 100, + }, + last_message_id: { + type: String, + default: null, + }, + message_count: { + type: Number, + default: 0, + min: 0, + }, + member_count: { + type: Number, + default: 1, + min: 0, + max: 50, + }, + rate_limit_per_user: { + type: Number, + default: 0, + min: 0, + max: 21600, + }, + thread_metadata: { + type: threadMetadataSchema, + required: true, + }, + total_message_sent: { + type: Number, + default: 0, + min: 0, + }, + flags: { + type: Number, + default: 0, + }, + applied_tags: { + type: [String], + default: [], + }, + nsfw: { + type: Boolean, + default: false, + }, + deletedAt: { + type: Date, + default: null, + }, + }, + { + timestamps: true, + }, +); + +threadSchema.method('softDelete', async function softDelete() { + this.deletedAt = new Date(); + await this.save(); +}); + +// Plugins +threadSchema.plugin(toJSON); +threadSchema.plugin(paginate); + +export default threadSchema; diff --git a/src/models/schemas/index.ts b/src/models/schemas/index.ts index 8bab6bb..84de680 100644 --- a/src/models/schemas/index.ts +++ b/src/models/schemas/index.ts @@ -1,15 +1,17 @@ -import userSchema from './User.schema'; -import tokenSchema from './Token.schema'; -import heatMapSchema from './HeatMap.schema'; -import rawInfoSchema from './RawInfo.schema'; -import MemberActivitySchema from './MemberActivity.schema'; -import guildMemberSchema from './GuildMember.schema'; +import announcementSchema, { announcementEmitter } from './Announcement.schema'; import channelSchema from './Channel.schema'; -import roleSchema from './Role.schema'; import communitySchema from './Community.schema'; -import platformSchema from './Platform.schema'; -import announcementSchema, { announcementEmitter } from './Announcement.schema'; +import guildMemberSchema from './GuildMember.schema'; +import heatMapSchema from './HeatMap.schema'; +import MemberActivitySchema from './MemberActivity.schema'; import moduleSchema from './Module.schema'; +import platformSchema from './Platform.schema'; +import rawInfoSchema from './RawInfo.schema'; +import roleSchema from './Role.schema'; +import threadSchema from './Thread.schema'; +import tokenSchema from './Token.schema'; +import userSchema from './User.schema'; + export { userSchema, tokenSchema, @@ -24,4 +26,5 @@ export { announcementSchema, announcementEmitter, moduleSchema, + threadSchema, }; diff --git a/src/repositories/thread.repository.ts b/src/repositories/thread.repository.ts new file mode 100644 index 0000000..65e6d87 --- /dev/null +++ b/src/repositories/thread.repository.ts @@ -0,0 +1,12 @@ +import { Connection } from 'mongoose'; + +import { IThread } from '../interfaces'; +import { BaseRepository } from './base.repository'; + +export class ThreadRepository extends BaseRepository { + constructor(connection: Connection) { + super(connection.model('Thread')); + } +} + +export const makeThreadRepository = (connection: Connection) => new ThreadRepository(connection);