From 2f826fbf362e57d7aaa6cc7622d4ed3d68f0e4b9 Mon Sep 17 00:00:00 2001 From: Michel Fedde Date: Sun, 25 May 2025 16:07:09 +0200 Subject: [PATCH] Adds Event system and automatic messages --- package-lock.json | 20 +++ package.json | 2 + source/Container/Container.ts | 6 +- source/Container/Services.ts | 3 + .../Discord/CommandPartials/GroupSelection.ts | 2 +- source/Discord/Commands/Playdates.ts | 31 +++-- source/Events/DefaultEvents.ts | 27 ++++ source/Events/ElementCreatedEvent.ts | 10 ++ source/Events/EventHandler.ts | 48 +++++++ .../Handlers/SendCreatedNotification.ts | 75 +++++++++++ source/Events/ReminderEvent.ts | 122 ++++++++++++++++++ source/Groups/GroupConfigurationHandler.ts | 8 +- source/Groups/GroupConfigurationRenderer.ts | 32 +++++ .../Groups/GroupConfigurationTransformers.ts | 9 +- source/Groups/RuntimeGroupConfiguration.d.ts | 8 ++ source/Icons/IconCache.ts | 4 +- source/Repositories/PlaydateRepository.ts | 24 ++++ source/Repositories/Repository.ts | 10 +- source/main.ts | 3 + source/types/Class.ts | 2 + 20 files changed, 428 insertions(+), 18 deletions(-) create mode 100644 source/Events/DefaultEvents.ts create mode 100644 source/Events/ElementCreatedEvent.ts create mode 100644 source/Events/EventHandler.ts create mode 100644 source/Events/Handlers/SendCreatedNotification.ts create mode 100644 source/Events/ReminderEvent.ts create mode 100644 source/types/Class.ts diff --git a/package-lock.json b/package-lock.json index bc934be..2b918b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "discord.js": "^14.18.0", "dotenv": "^16.4.7", "esbuild": "^0.25.0", + "is-plain-object": "^5.0.0", "log4js": "^6.9.1", + "node-cron": "^4.0.7", "object-path-set": "^1.0.2", "svg2img": "^1.0.0-beta.2" } @@ -1913,6 +1915,15 @@ "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", "license": "MIT" }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jimp": { "version": "0.16.13", "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.13.tgz", @@ -2086,6 +2097,15 @@ "node": ">=10" } }, + "node_modules/node-cron": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.0.7.tgz", + "integrity": "sha512-A37UUDpxRT/kWanELr/oMayCWQFk9Zx9BEUoXrAKuKwKzH4XuAX+vMixMBPkgZBkADgJwXv91w5cMRTNSVP/mA==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-path-set": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/object-path-set/-/object-path-set-1.0.2.tgz", diff --git a/package.json b/package.json index b729483..4d221ba 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "discord.js": "^14.18.0", "dotenv": "^16.4.7", "esbuild": "^0.25.0", + "is-plain-object": "^5.0.0", "log4js": "^6.9.1", + "node-cron": "^4.0.7", "object-path-set": "^1.0.2", "svg2img": "^1.0.0-beta.2" } diff --git a/source/Container/Container.ts b/source/Container/Container.ts index ace9c92..8909c24 100644 --- a/source/Container/Container.ts +++ b/source/Container/Container.ts @@ -1,15 +1,17 @@ +import {Class} from "../types/Class"; + export class Container { static instance: Container; private instances: Map = new Map(); - public set(instance: T, name: string|null = null): void + public set(instance: T, name: string|null = null): void { const settingName = name ?? instance.constructor.name; this.instances.set(settingName.toLowerCase(), instance); } - public get(name: string): T + public get(name: string): T { return this.instances.get(name.toLowerCase()); } diff --git a/source/Container/Services.ts b/source/Container/Services.ts index 591208a..cdc26d5 100644 --- a/source/Container/Services.ts +++ b/source/Container/Services.ts @@ -9,6 +9,7 @@ import {GuildEmojiRoleManager} from "discord.js"; import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository"; import {DiscordClient} from "../Discord/DiscordClient"; import {IconCache} from "../Icons/IconCache"; +import {EventHandler} from "../Events/EventHandler"; export enum ServiceHint { App, @@ -30,6 +31,8 @@ export class Services { const iconCache = new IconCache(discordClient); container.set(iconCache); + container.set(new EventHandler()); + // @ts-ignore configure({ appenders: { diff --git a/source/Discord/CommandPartials/GroupSelection.ts b/source/Discord/CommandPartials/GroupSelection.ts index b12bf42..46b8ef1 100644 --- a/source/Discord/CommandPartials/GroupSelection.ts +++ b/source/Discord/CommandPartials/GroupSelection.ts @@ -30,7 +30,7 @@ export class GroupSelection { ) } - public static getGroup(interaction: CommandInteraction): GroupModel { + public static getGroup(interaction: CommandInteraction|AutocompleteInteraction): GroupModel { const groupname = interaction.options.get("group", true); if (!groupname) { throw new UserError("No group name provided"); diff --git a/source/Discord/Commands/Playdates.ts b/source/Discord/Commands/Playdates.ts index 0425a05..319eb72 100644 --- a/source/Discord/Commands/Playdates.ts +++ b/source/Discord/Commands/Playdates.ts @@ -4,7 +4,7 @@ import { CommandInteraction, AutocompleteInteraction, GuildMember, - EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields + EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields, time, User } from "discord.js"; import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command"; import {Container} from "../../Container/Container"; @@ -93,17 +93,32 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter throw new UserError("No date or invalid date format for the to parameter."); } + if (fromDate > toDate) { + throw new UserError("The to-date can't be earlier than the from-date"); + } + + const playdateRepo = Container.get(PlaydateRepository.name); + + const collidingTimes = playdateRepo.findPlaydatesInRange(fromDate, toDate, group); + if (collidingTimes.length > 0) { + throw new UserError("The playdate collides with another playdate. Please either remove the old one or choose a different time.") + } + const playdate: Partial = { group: group, from_time: new Date(fromDate), to_time: new Date(toDate), } - const id = Container.get(PlaydateRepository.name).create(playdate); + const id = playdateRepo.create(playdate); const embed = new EmbedBuilder() .setTitle("Created a play-date.") .setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.") + .setFields({ + name: "Created playdate", + value: `${time(new Date(fromDate),'F')} - ${time(new Date(toDate), 'F')}`, + }) .setFooter({ text: `Group: ${group.name}` }) @@ -127,12 +142,8 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter return; } - const groupname = interaction.options.getString("group") - const group = Container.get(GroupRepository.name).findGroupByName((groupname ?? '').toString()); - if (!group) { - throw new UserError("No group found"); - } - + const group = GroupSelection.getGroup(interaction); + const playdates = Container.get(PlaydateRepository.name).findFromGroup(group); await interaction.respond( playdates.map(playdate => { @@ -153,8 +164,8 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter playdates.map((playdate) => { return { - name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`, - value: `` + name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`, + value: `${time(playdate.from_time, 'R')}` } }) ) diff --git a/source/Events/DefaultEvents.ts b/source/Events/DefaultEvents.ts new file mode 100644 index 0000000..caee973 --- /dev/null +++ b/source/Events/DefaultEvents.ts @@ -0,0 +1,27 @@ +import {EventHandler, TimedEvent} from "./EventHandler"; +import {Container} from "../Container/Container"; +import {ReminderEvent} from "./ReminderEvent"; +import {ElementCreatedEvent} from "./ElementCreatedEvent"; +import {ClassNamed} from "../types/Class"; +import {sendCreatedNotificationEventHandler} from "./Handlers/SendCreatedNotification"; +import {PlaydateModel} from "../Models/PlaydateModel"; + +export class DefaultEvents { + public static setupTimed() { + const events: TimedEvent[] = [ + new ReminderEvent() + ] + + const eventHandler = Container.get(EventHandler.name); + + events.forEach((event) => { + eventHandler.addTimed(event); + }) + } + + public static setupHandlers() { + const eventHandler = Container.get(EventHandler.name); + + eventHandler.addHandler>(ElementCreatedEvent.name, sendCreatedNotificationEventHandler); + } +} \ No newline at end of file diff --git a/source/Events/ElementCreatedEvent.ts b/source/Events/ElementCreatedEvent.ts new file mode 100644 index 0000000..9f2f149 --- /dev/null +++ b/source/Events/ElementCreatedEvent.ts @@ -0,0 +1,10 @@ +import {Model} from "../Models/Model"; + +export class ElementCreatedEvent { + constructor( + public readonly tableName: string, + public readonly instanceValues: Partial, + public readonly instanceId: number + ) { + } +} \ No newline at end of file diff --git a/source/Events/EventHandler.ts b/source/Events/EventHandler.ts new file mode 100644 index 0000000..cbbe1fc --- /dev/null +++ b/source/Events/EventHandler.ts @@ -0,0 +1,48 @@ +import cron from "node-cron"; +import {Nullable} from "../types/Nullable"; +import {Class, ClassNamed} from "../types/Class"; + +export type EventConfiguration = { + name: string, + maxExecutions?: number, +} + +export interface TimedEvent { + configuration: EventConfiguration, + cronExpression: string, + execute: () => void +} + +export class EventHandler { + private eventHandlers: Map = new Map(); + + constructor() { + } + + public addHandler(eventName: string, handler: (event: T) => void) { + if (!this.eventHandlers.has(eventName)) { + this.eventHandlers.set(eventName, []); + } + + this.eventHandlers.get(eventName)?.push(handler); + } + + public dispatch(event: T) { + const eventName = event.constructor.name; + if (!this.eventHandlers.has(eventName)) { + return; + } + + this.eventHandlers.get(eventName)?.forEach((handler) => { + handler(event); + }) + } + + public addTimed(event: TimedEvent) { + if (!cron.validate(event.cronExpression)) { + throw new Error(`Can't create event with name '${event.configuration.name}': Invalid cron expression.`) + } + + cron.schedule(event.cronExpression, event.execute.bind(event), event.configuration); + } +} \ No newline at end of file diff --git a/source/Events/Handlers/SendCreatedNotification.ts b/source/Events/Handlers/SendCreatedNotification.ts new file mode 100644 index 0000000..73bd302 --- /dev/null +++ b/source/Events/Handlers/SendCreatedNotification.ts @@ -0,0 +1,75 @@ +import {ElementCreatedEvent} from "../ElementCreatedEvent"; +import {DefaultHandler} from "../DefaultEvents"; +import {PlaydateModel} from "../../Models/PlaydateModel"; +import PlaydateTableConfiguration from "../../Database/tables/Playdate"; +import {EmbedBuilder, roleMention, time} from "discord.js"; +import {ArrayUtils} from "../../Utilities/ArrayUtils"; +import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler"; +import {Container} from "../../Container/Container"; +import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRenderer"; +import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository"; +import {DiscordClient} from "../../Discord/DiscordClient"; + +const NEW_PLAYDATE_MESSAGES = [ + 'A new playdate was added. Lets hope, your GM has not planned to kill you. >:]', + 'Oh look. A new playdate... neat.', + 'A new playdate. Lets polish the dice.' +]; + +export async function sendCreatedNotificationEventHandler(event: ElementCreatedEvent) { + if (event.tableName !== PlaydateTableConfiguration.name) { + return; + } + + const playdate = event.instanceValues; + if (!playdate.group || !playdate.from_time || !playdate.to_time) { + return; + } + + + const configurationHandler = new GroupConfigurationHandler( + Container.get(GroupConfigurationRepository.name), + playdate.group + ); + + const targetChannel = configurationHandler.getConfigurationByPath('channels.newPlaydates'); + if (!targetChannel) { + return; + } + + const channel = await Container.get(DiscordClient.name).Client.channels.fetch(targetChannel) + if (!channel) { + return; + } + + if (!channel.isTextBased()) { + return; + } + + if (!channel.isSendable()) { + return; + } + + const embed = new EmbedBuilder() + .setTitle("New Playdate added") + .setDescription( + ArrayUtils.chooseRandom(NEW_PLAYDATE_MESSAGES) + ) + .addFields({ + name: "Playdate:", + value: `${time(playdate.from_time, "F")} - ${time(playdate.to_time, 'F')}`, + }) + .setFooter({ + text: `Group: ${playdate.group.name}` + }); + + channel.send({ + content: roleMention(playdate.group.role.roleid), + embeds: [ + embed + ], + allowedMentions: { + roles: [ playdate.group.role.roleid ] + } + }) +} \ No newline at end of file diff --git a/source/Events/ReminderEvent.ts b/source/Events/ReminderEvent.ts new file mode 100644 index 0000000..c09ec64 --- /dev/null +++ b/source/Events/ReminderEvent.ts @@ -0,0 +1,122 @@ +import {CronExpression, Event, EventConfiguration, TimedEvent} from "./EventHandler"; +import {Container} from "../Container/Container"; +import Playdate from "../Database/tables/Playdate"; +import {PlaydateRepository} from "../Repositories/PlaydateRepository"; +import {GroupConfigurationHandler} from "../Groups/GroupConfigurationHandler"; +import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository"; +import {PlaydateModel} from "../Models/PlaydateModel"; +import {ChannelId} from "../types/DiscordTypes"; +import {DiscordClient} from "../Discord/DiscordClient"; +import {EmbedBuilder, MessageFlags, roleMention, time} from "discord.js"; +import {ArrayUtils} from "../Utilities/ArrayUtils"; + +export class ReminderEvent implements TimedEvent { + private static REMINDER_INTERVALS = [ + 1, + 7 + ]; + + private static REMINDER_NOTIFICATIONS = [ + 'The darkness approaches. Get ready!', + 'Your aid is requested once again.', + 'Grab your dice and show them evil-doers how its done!', + ] + + + configuration: EventConfiguration = { + name: "Reminders", + } + + cronExpression: CronExpression = "0 9 * * *" + + private groupConfigurationRepository: GroupConfigurationRepository + private playdateRepository: PlaydateRepository + private discordClient: DiscordClient + + constructor() { + this.playdateRepository = Container.get(PlaydateRepository.name); + this.groupConfigurationRepository = Container.get(GroupConfigurationRepository.name); + this.discordClient = Container.get(DiscordClient.name); + } + + async execute() { + const today = new Date(); + today.setHours(0,0,0,0); + + const playdates = ReminderEvent.REMINDER_INTERVALS.flatMap((interval) => { + const fromDate = new Date(today.valueOf()) + fromDate.setDate(fromDate.getDate() + interval); + + const toDate = new Date(today.valueOf()) + toDate.setDate(toDate.getDate() + interval); + toDate.setHours(23,59,59,999); + + return this.playdateRepository.findPlaydatesInRange(fromDate, toDate); + }, this) + + const promises = playdates + .map((playdate) => { + if (!playdate.group) { + return Promise.resolve(); + } + + const configurationHandler = new GroupConfigurationHandler( + this.groupConfigurationRepository, + playdate.group + ); + + const config = configurationHandler.getConfiguration(); + const targetChannel = config.channels?.playdateReminders; + + if (!targetChannel) { + return Promise.resolve(); + } + + return this.sendReminder(playdate, targetChannel, config.locale); + }, this) + + await Promise.all(promises); + } + + private async sendReminder(playdate: PlaydateModel, targetChannel: ChannelId, locale: Intl.Locale) { + if (!playdate.group) { + return; + } + + const channel = await this.discordClient.Client.channels.fetch(targetChannel) + if (!channel) { + return; + } + + if (!channel.isTextBased()) { + return; + } + + if (!channel.isSendable()) { + return; + } + + const embed = new EmbedBuilder() + .setTitle("Playdate reminder") + .setDescription( + ArrayUtils.chooseRandom(ReminderEvent.REMINDER_NOTIFICATIONS) + ) + .addFields({ + name: "Next Playdate:", + value: `${time(playdate.from_time, "F")} - ${time(playdate.from_time, 'R')}`, + }) + .setFooter({ + text: `Group: ${playdate.group.name}` + }); + + channel.send({ + content: roleMention(playdate.group.role.roleid), + embeds: [ + embed + ], + allowedMentions: { + roles: [ playdate.group.role.roleid ] + } + }) + } +} \ No newline at end of file diff --git a/source/Groups/GroupConfigurationHandler.ts b/source/Groups/GroupConfigurationHandler.ts index 92fb69e..2cd4286 100644 --- a/source/Groups/GroupConfigurationHandler.ts +++ b/source/Groups/GroupConfigurationHandler.ts @@ -6,11 +6,15 @@ import {GroupConfigurationResult, GroupConfigurationTransformers} from "./GroupC import setPath from 'object-path-set'; import deepmerge from "deepmerge"; import {Nullable} from "../types/Nullable"; +import {isPlainObject} from "is-plain-object"; export class GroupConfigurationHandler { private static DEFAULT_CONFIGURATION: RuntimeGroupConfiguration = { channels: null, locale: new Intl.Locale('en-GB'), + permissions: { + allowMemberManagingPlaydates: false + } } private readonly transformers: GroupConfigurationTransformers = new GroupConfigurationTransformers(); @@ -42,7 +46,9 @@ export class GroupConfigurationHandler { } public getConfiguration(): RuntimeGroupConfiguration { - return deepmerge(GroupConfigurationHandler.DEFAULT_CONFIGURATION, this.getDatabaseConfiguration()); + return deepmerge(GroupConfigurationHandler.DEFAULT_CONFIGURATION, this.getDatabaseConfiguration(), { + isMergeableObject: isPlainObject + }); } public getConfigurationByPath(path: string): Nullable { diff --git a/source/Groups/GroupConfigurationRenderer.ts b/source/Groups/GroupConfigurationRenderer.ts index 6b8156c..c53f424 100644 --- a/source/Groups/GroupConfigurationRenderer.ts +++ b/source/Groups/GroupConfigurationRenderer.ts @@ -34,6 +34,7 @@ import {UserError} from "../Discord/UserError"; import {RuntimeGroupConfiguration} from "./RuntimeGroupConfiguration"; import {ChannelId} from "../types/DiscordTypes"; import {IconCache} from "../Icons/IconCache"; +import {ifError} from "node:assert"; type UIElementCollection = Record; type UIElement = { @@ -76,6 +77,19 @@ export class GroupConfigurationRenderer { key: 'locale', description: "Provides locale to be used for this group. This mostly sets how the dates are displayed, but this can also be later used for translations.", isConfiguration: true + }, + permissions: { + label: "Permissions", + key: "permissions", + description: "Allows customization, how the members are allowed to interact with the data stored in the group.", + childrenElements: { + allowMemberManagingPlaydates: { + label: "Manage Playdates", + key: "allowMemberManagingPlaydates", + description: "Defines if the members are allowed to manage playdates like adding or deleting them.", + isConfiguration: true + } + } } } @@ -220,6 +234,8 @@ export class GroupConfigurationRenderer { return displaynames.of((value).baseName) ?? "Unknown"; case TransformerType.Channel: return channelMention(value); + case TransformerType.PermissionBoolean: + return value ? "Allowed" : "Disallowed" default: return "None"; @@ -279,6 +295,21 @@ export class GroupConfigurationRenderer { .setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath) .setChannelTypes(ChannelType.GuildText) .setPlaceholder("New Value"); + case TransformerType.PermissionBoolean: + return new StringSelectMenuBuilder() + .setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath) + .setOptions( + [ + { + label: "Allow", + value: "1" + }, + { + label: "Disallow", + value: "0" + } + ] + ) default: return new StringSelectMenuBuilder() @@ -309,6 +340,7 @@ export class GroupConfigurationRenderer { switch (transformerType) { case TransformerType.Locale: case TransformerType.Channel: + case TransformerType.PermissionBoolean: return interaction.values.join('; '); default: throw new Error("Unhandled select menu"); diff --git a/source/Groups/GroupConfigurationTransformers.ts b/source/Groups/GroupConfigurationTransformers.ts index 04092ee..ccb7d2b 100644 --- a/source/Groups/GroupConfigurationTransformers.ts +++ b/source/Groups/GroupConfigurationTransformers.ts @@ -8,6 +8,7 @@ import {ArrayUtils} from "../Utilities/ArrayUtils"; export enum TransformerType { Locale, Channel, + PermissionBoolean, } type GroupConfigurationTransformer = { @@ -16,7 +17,7 @@ type GroupConfigurationTransformer = { } export type GroupConfigurationResult = - ChannelId | Intl.Locale + ChannelId | Intl.Locale | boolean export class GroupConfigurationTransformers { static TRANSFORMERS: GroupConfigurationTransformer[] = [ @@ -31,6 +32,10 @@ export class GroupConfigurationTransformers { { path: ['locale'], type: TransformerType.Locale, + }, + { + path: ['permissions', 'allowMemberManagingPlaydates'], + type: TransformerType.PermissionBoolean } ]; @@ -45,6 +50,8 @@ export class GroupConfigurationTransformers { return new Intl.Locale(configValue.value) case TransformerType.Channel: return configValue.value; + case TransformerType.PermissionBoolean: + return configValue.value === '1'; } } diff --git a/source/Groups/RuntimeGroupConfiguration.d.ts b/source/Groups/RuntimeGroupConfiguration.d.ts index 966c597..3aba014 100644 --- a/source/Groups/RuntimeGroupConfiguration.d.ts +++ b/source/Groups/RuntimeGroupConfiguration.d.ts @@ -1,9 +1,17 @@ +import {ChannelId} from "../types/DiscordTypes"; +import {Nullable} from "../types/Nullable"; + export type RuntimeGroupConfiguration = { channels: Nullable, locale: Intl.Locale, + permissions: PermissionRuntimeGroupConfiguration }; export type ChannelRuntimeGroupConfiguration = { newPlaydates: ChannelId, playdateReminders: ChannelId +} + +export type PermissionRuntimeGroupConfiguration = { + allowMemberManagingPlaydates: boolean } \ No newline at end of file diff --git a/source/Icons/IconCache.ts b/source/Icons/IconCache.ts index 4eaf15c..bc8ac02 100644 --- a/source/Icons/IconCache.ts +++ b/source/Icons/IconCache.ts @@ -2,7 +2,7 @@ import {Routes} from "discord.js"; import {DiscordClient} from "../Discord/DiscordClient"; export class IconCache { - private existingIcons: Map|null; + private existingIcons: Map | undefined; constructor( private readonly client: DiscordClient @@ -11,7 +11,7 @@ export class IconCache { } public get(iconName: string): string | null { - if (!this.existingIcons?.has(iconName) ?? false) { + if (!this.existingIcons?.has(iconName)) { return null; } diff --git a/source/Repositories/PlaydateRepository.ts b/source/Repositories/PlaydateRepository.ts index dd29a48..d69b17f 100644 --- a/source/Repositories/PlaydateRepository.ts +++ b/source/Repositories/PlaydateRepository.ts @@ -34,6 +34,30 @@ export class PlaydateRepository extends Repository { return finds.map((playdate) => this.convertToModelType(playdate, group)); } + findPlaydatesInRange(fromDate: Date|number, toDate: Date|number, group: GroupModel | undefined = undefined) { + if (fromDate instanceof Date) { + fromDate = fromDate.getTime(); + } + if (toDate instanceof Date) { + toDate = toDate.getTime(); + } + + let sql = `SELECT * FROM ${this.schema.name} WHERE time_from > ? AND time_from < ?`; + const params = [fromDate, toDate]; + + if (group) { + sql = `${sql} AND groupid = ?` + params.push(group.id) + } + + const finds = this.database.fetchAll( + sql, + ...params + ); + + return finds.map((playdate) => this.convertToModelType(playdate, group)); + } + protected convertToModelType(intermediateModel: DBPlaydate | undefined, fixedGroup: Nullable = null): PlaydateModel { if (!intermediateModel) { throw new Error("Unable to convert the playdate model"); diff --git a/source/Repositories/Repository.ts b/source/Repositories/Repository.ts index fbae56b..630cd28 100644 --- a/source/Repositories/Repository.ts +++ b/source/Repositories/Repository.ts @@ -3,8 +3,12 @@ import {Model} from "../Models/Model"; import { Nullable } from "../types/Nullable"; import {DatabaseDefinition} from "../Database/DatabaseDefinition"; import {debug} from "node:util"; +import {Container} from "../Container/Container"; +import {EventHandler} from "../Events/EventHandler"; +import {ElementCreatedEvent} from "../Events/ElementCreatedEvent"; export class Repository { + constructor( protected readonly database: DatabaseConnection, public readonly schema: DatabaseDefinition, @@ -30,7 +34,11 @@ export class Repository "?").join(',')})`; const result = this.database.execute(sql, ...Object.values(createObject)); - return result.lastInsertRowid; + const id = result.lastInsertRowid; + + Container.get(EventHandler.name).dispatch(new ElementCreatedEvent(this.schema.name, instance, id)); + + return id; } public update(instance: Partial&{id: number}): boolean { diff --git a/source/main.ts b/source/main.ts index 2b21ab3..4d77713 100644 --- a/source/main.ts +++ b/source/main.ts @@ -4,9 +4,12 @@ import {Container} from "./Container/Container"; import {DatabaseConnection} from "./Database/DatabaseConnection"; import {ServiceHint, Services} from "./Container/Services"; import {IconCache} from "./Icons/IconCache"; +import {DefaultEvents} from "./Events/DefaultEvents"; const container = Container.getInstance(); Services.setup(container, ServiceHint.App); +DefaultEvents.setupTimed(); +DefaultEvents.setupHandlers(); (async () => { const env = container.get(Environment.name); diff --git a/source/types/Class.ts b/source/types/Class.ts new file mode 100644 index 0000000..045a53a --- /dev/null +++ b/source/types/Class.ts @@ -0,0 +1,2 @@ +export type Class = {constructor: {name: string}} +export type ClassNamed = {name: string} \ No newline at end of file