Skip to content

Commit 12a7b14

Browse files
committed
Feat/channel folders
1 parent d99f522 commit 12a7b14

18 files changed

+377
-20
lines changed

.gitlab-ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ lint:
2727
tags:
2828
- beep-runner
2929
stage: test
30-
image: node:20.18
30+
image: node:21.7.3
3131
before_script:
3232
- corepack enable
3333
- pnpm config set store-dir .pnpm-store
@@ -45,7 +45,7 @@ test:
4545
stage: test
4646
tags:
4747
- beep-runner
48-
image: node:20.18
48+
image: node:21.7.3
4949
services:
5050
- name: postgres
5151
alias: postgres

adonisrc.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default defineConfig({
8585
{
8686
files: ['tests/unit/**/*.spec(.ts|.js)'],
8787
name: 'unit',
88-
timeout: 2000,
88+
timeout: 10000,
8989
},
9090
{
9191
files: ['tests/functional/**/*.spec(.ts|.js)'],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Exception } from '@adonisjs/core/exceptions'
2+
import { HttpContext } from '@adonisjs/core/http'
3+
4+
export default class ChannelWithIncoherentHierarchyException extends Exception {
5+
async handle(error: this, ctx: HttpContext) {
6+
ctx.response.status(error.status).send({
7+
message: error.message,
8+
error: error.code,
9+
})
10+
}
11+
}

apps/channels/models/channel.ts

+13
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export default class Channel extends BaseModel {
3636
@column()
3737
declare position: number
3838

39+
@column({
40+
columnName: 'parent_id',
41+
})
42+
declare parentId: string | null
43+
3944
@manyToMany(() => User, {
4045
pivotTable: 'channels_users',
4146
})
@@ -47,6 +52,14 @@ export default class Channel extends BaseModel {
4752
@belongsTo(() => Server)
4853
declare server: BelongsTo<typeof Server>
4954

55+
@belongsTo(() => Channel)
56+
declare parent: BelongsTo<typeof Channel>
57+
58+
@hasMany(() => Channel, {
59+
foreignKey: 'parentId',
60+
})
61+
declare childrens: HasMany<typeof Channel>
62+
5063
@column.dateTime({ autoCreate: true })
5164
declare createdAt: DateTime
5265

apps/channels/models/channel_type.ts

+14
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,18 @@ export enum ChannelType {
22
TEXT_SERVER = 0,
33
VOICE_SERVER = 1,
44
PRIVATE_CHAT = 2,
5+
FOLDER_SERVER = 3,
6+
}
7+
8+
export function channelTypeToString(type: ChannelType): string {
9+
switch (type) {
10+
case ChannelType.TEXT_SERVER:
11+
return 'text_server'
12+
case ChannelType.VOICE_SERVER:
13+
return 'voice_server'
14+
case ChannelType.PRIVATE_CHAT:
15+
return 'private_chat'
16+
case ChannelType.FOLDER_SERVER:
17+
return 'folder_server'
18+
}
519
}

apps/channels/services/channel_service.ts

+64-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ChannelNotFoundException from '#apps/channels/exceptions/channel_not_found_exception'
22
import Channel from '#apps/channels/models/channel'
3-
import { ChannelType } from '#apps/channels/models/channel_type'
3+
import { ChannelType, channelTypeToString } from '#apps/channels/models/channel_type'
44
import { CachedUser, OccupiedChannel } from '#apps/channels/models/occupied_channels'
55
import {
66
CreateChannelSchema,
@@ -20,6 +20,7 @@ import logger from '@adonisjs/core/services/logger'
2020
import redis from '@adonisjs/redis/services/main'
2121
import transmit from '@adonisjs/transmit/services/main'
2222
import jwt from 'jsonwebtoken'
23+
import ChannelWithIncoherentHierarchyException from '../exceptions/channel_cant_be_children_exception.js'
2324

2425
export interface PayloadJWTSFUConnection {
2526
channelSn?: string
@@ -54,6 +55,14 @@ export default class ChannelService {
5455
return server.channels
5556
}
5657

58+
async findAllChannelsByServerWithChildren(serverId: string): Promise<Channel[]> {
59+
const channels = await Channel.query()
60+
.whereNull('parentId')
61+
.where('serverId', serverId)
62+
.preload('childrens')
63+
return channels
64+
}
65+
5766
async findPrivateOrderedForUserOrFail(userId: string): Promise<Channel[]> {
5867
await User.findOrFail(userId).catch(() => {
5968
throw new UserNotFoundException('User not found', { status: 404, code: 'E_ROW_NOT_FOUND' })
@@ -160,20 +169,63 @@ export default class ChannelService {
160169
serverId: string,
161170
userId: string
162171
): Promise<Channel> {
163-
const sn = generateSnowflake()
164172
const type = newChannel.type as ChannelType
173+
if (newChannel.parentId) {
174+
if (type === ChannelType.PRIVATE_CHAT || type === ChannelType.FOLDER_SERVER) {
175+
throw new ChannelWithIncoherentHierarchyException(
176+
`Channel with type ${channelTypeToString(type)} can't have a parent channel`,
177+
{
178+
status: 422,
179+
code: 'E_WRONG_HIERARCHY',
180+
}
181+
)
182+
}
183+
184+
let parent: Channel
185+
try {
186+
parent = await Channel.findOrFail(newChannel.parentId)
187+
} catch (e) {
188+
logger.error(e)
189+
throw new ChannelNotFoundException('Parent channel not found', {
190+
status: 404,
191+
code: 'E_ROWNOTFOUND',
192+
})
193+
}
194+
195+
if ((parent.type as ChannelType) !== ChannelType.FOLDER_SERVER) {
196+
throw new ChannelWithIncoherentHierarchyException(
197+
`Parent channel is not of type FOLDER_SERVER`,
198+
{
199+
status: 422,
200+
code: 'E_WRONG_HIERARCHY',
201+
}
202+
)
203+
}
204+
}
205+
206+
const sn = generateSnowflake()
165207
const firstChannel = await Channel.query()
166-
.where('serverId', serverId)
208+
.where('server_id', serverId)
167209
.orderBy('position')
168210
.first()
169211
const position = firstChannel != null ? firstChannel.position - 1 : 0
170-
const channel = await Channel.create({
171-
name: newChannel.name,
172-
type: type,
173-
serverId: serverId,
174-
serialNumber: sn,
175-
position,
176-
})
212+
213+
const channel = newChannel.parentId
214+
? await Channel.create({
215+
name: newChannel.name,
216+
type: type,
217+
serverId: serverId,
218+
serialNumber: sn,
219+
position,
220+
parentId: newChannel.parentId,
221+
})
222+
: await Channel.create({
223+
name: newChannel.name,
224+
type: type,
225+
serverId: serverId,
226+
serialNumber: sn,
227+
position,
228+
})
177229
logger.info('Created new channel : ', channel)
178230
await channel.related('users').attach([userId])
179231
return channel
@@ -186,7 +238,9 @@ export default class ChannelService {
186238
code: 'E_ROWNOTFOUND',
187239
})
188240
})
241+
logger.info('Updating channel : ', payload)
189242
channel.merge(payload)
243+
logger.info(channel.toJSON())
190244
await redis.del(`channel:${id}`)
191245
return channel.save()
192246
}

apps/channels/validators/channel.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const createChannelValidator = vine.compile(
1010
vine.object({
1111
name: vine.string().minLength(1), // name is not empty and must be given
1212
type: vine.enum(ChannelType),
13+
parentId: vine.string().optional(), // Set the parent id but only if type is TEXT_SERVER or VOICE_SERVER and parent is FOLDER_SERVER
1314
})
1415
)
1516

@@ -23,6 +24,7 @@ export const updateChannelValidator = vine.compile(
2324
name: vine.string().minLength(1).optional(), // The name is optional for the update, but if provided, it must be non-empty
2425
description: vine.string().minLength(1).optional(), // Same principle applies to the description
2526
position: vine.number().optional(),
27+
parentId: vine.string().optional().nullable(), // Set the parent id but only if type is TEXT_SERVER or VOICE_SERVER and parent is FOLDER_SERVER
2628
})
2729
)
2830

apps/servers/controllers/server_channels_controller.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { Payload } from '#apps/authentication/contracts/payload'
2+
import Channel from '#apps/channels/models/channel'
23
import ChannelService from '#apps/channels/services/channel_service'
34
import { createChannelValidator, updateChannelValidator } from '#apps/channels/validators/channel'
45
import ServerChannelPolicy from '#apps/servers/policies/server_channel_policy'
56
import { mutedValidator } from '#apps/users/validators/muted_validator'
67
import { inject } from '@adonisjs/core'
78
import type { HttpContext } from '@adonisjs/core/http'
9+
import { findChannelServerValidator } from '../validators/server.js'
810

911
@inject()
1012
export default class ServerChannelsController {
1113
constructor(private channelService: ChannelService) {}
1214

13-
//recupere les channels d'un server
14-
async findByServerId({ params, bouncer }: HttpContext) {
15+
async findByServerId({ request, params, bouncer }: HttpContext) {
16+
const { group } = await request.validateUsing(findChannelServerValidator)
1517
await bouncer.with(ServerChannelPolicy).authorize('view' as never, params.serverId)
16-
return this.channelService.findAllByServer(params.serverId)
18+
let channels: Channel[]
19+
if (group) {
20+
channels = await this.channelService.findAllChannelsByServerWithChildren(params.serverId)
21+
} else {
22+
channels = await this.channelService.findAllByServer(params.serverId)
23+
}
24+
return channels
1725
}
1826

1927
async findByChannelId({ params, bouncer }: HttpContext) {

apps/servers/validators/server.ts

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export const updatePictureValidator = vine.compile(
5959
})
6060
)
6161

62+
export const findChannelServerValidator = vine.compile(
63+
vine.object({
64+
group: vine.boolean().optional(),
65+
})
66+
)
67+
6268
export type UpdateBannerSchema = Infer<typeof updateBannerValidator>
6369
export type CreateServerSchema = Infer<typeof createServerValidator>
6470
export type UpdateServerSchema = Infer<typeof updateServerValidator>

database/factories/channel_factory.ts

+13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export const ChannelFactory = factory
1515
.state('private_channel', async (channel) => {
1616
channel.type = ChannelType.PRIVATE_CHAT
1717
})
18+
.state('folder_channel', async (channel) => {
19+
channel.type = ChannelType.FOLDER_SERVER
20+
})
21+
.state('voice_channel', async (channel) => {
22+
channel.type = ChannelType.VOICE_SERVER
23+
})
24+
.state('text_channel', async (channel) => {
25+
//for declarative setup
26+
channel.type = ChannelType.TEXT_SERVER
27+
})
1828
.relation('server', () => ServerFactory)
1929
.relation('users', () => UserFactory)
2030
.build()
@@ -29,4 +39,7 @@ export const ChannelFactoryWithServer = (serverId: string) =>
2939
serverId: serverId,
3040
})
3141
})
42+
.state('folder_channel', async (channel) => {
43+
channel.type = ChannelType.FOLDER_SERVER
44+
})
3245
.build()

database/factories/role_factory.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import factory from '@adonisjs/lucid/factories'
33
import { ServerFactory } from '#database/factories/server_factory'
44
import { DEFAULT_ROLE_SERVER_PERMISSION } from '#apps/shared/constants/default_role_permission'
55
import { DEFAULT_ROLE_SERVER } from '#apps/shared/constants/default_role_server'
6+
import { Permissions } from '#apps/shared/enums/permissions'
67

78
export const RoleFactory = factory
89
.define(Role, async ({ faker }) => {
@@ -18,6 +19,10 @@ export const RoleFactory = factory
1819
role.permissions = DEFAULT_ROLE_SERVER_PERMISSION
1920
role.name = DEFAULT_ROLE_SERVER
2021
})
22+
.state('admin_role', async (role) => {
23+
role.permissions = Permissions.ADMINISTRATOR
24+
role.name = 'Admin'
25+
})
2126
.build()
2227

2328
export const RoleWithServerFactory = (serverId: string) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Channel from '#apps/channels/models/channel'
2+
import { BaseSchema } from '@adonisjs/lucid/schema'
3+
4+
export default class extends BaseSchema {
5+
protected tableName = 'channels'
6+
7+
async up() {
8+
this.schema.alterTable(this.tableName, (table) => {
9+
table
10+
.string('parent_id')
11+
.references('id')
12+
.inTable(Channel.table)
13+
.onDelete('CASCADE')
14+
.onUpdate('CASCADE')
15+
.nullable() // if null then "leaf" of the folder tree
16+
})
17+
}
18+
19+
async down() {
20+
this.schema.alterTable(this.tableName, (table) => {
21+
table.dropColumn('parent_id')
22+
})
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Server from '#apps/servers/models/server'
2+
import { BaseSeeder } from '@adonisjs/lucid/seeders'
3+
import { ChannelType } from '#apps/channels/models/channel_type'
4+
import logger from '@adonisjs/core/services/logger'
5+
import Channel from '#apps/channels/models/channel'
6+
7+
export default class extends BaseSeeder {
8+
async run() {
9+
const server = await Server.findBy('name', 'Beep')
10+
11+
const channel = await Channel.findBy('name', 'Folder Channel')
12+
const channel2 = await Channel.findBy('name', 'Children')
13+
14+
if (channel || channel2) {
15+
logger.info('Channels already exists')
16+
return
17+
}
18+
19+
if (!server) {
20+
throw new Error('Server not found')
21+
}
22+
23+
try {
24+
const parent = await Channel.create({
25+
name: 'Folder Channel',
26+
type: ChannelType.FOLDER_SERVER,
27+
serverId: server.id,
28+
serialNumber: '1',
29+
position: 0,
30+
})
31+
32+
logger.info('Parent channel created', parent.id)
33+
34+
await Channel.create({
35+
name: 'Children',
36+
type: ChannelType.TEXT_SERVER,
37+
serverId: server.id,
38+
serialNumber: '2',
39+
position: 0,
40+
parentId: parent.id,
41+
})
42+
} catch (e) {
43+
logger.error(e)
44+
}
45+
}
46+
}

database/seeders/server_seeder.ts

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const serverService = await app.container.make(ServerService)
99
export default class extends BaseSeeder {
1010
static environment: string[] = ['development']
1111
async run() {
12-
// Write your database queries inside the run method
1312
//remove all servers before seeding
1413
await Server.query().delete()
1514
const admin = await User.findByOrFail('username', 'admin')

0 commit comments

Comments
 (0)