diff --git a/.eslintrc.yml b/.eslintrc.yml index 504b65d..9e5c366 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -16,6 +16,8 @@ ignorePatterns: - 'coverage' - '**/*.js' - '**/*.d.ts' + - '__tests__' + - '__test__' parser: '@typescript-eslint/parser' diff --git a/packages/cache/README.md b/packages/cache/README.md index c66165b..94c7157 100644 --- a/packages/cache/README.md +++ b/packages/cache/README.md @@ -1,13 +1,39 @@ # @biscuitland/cache -In progress. +## Most importantly, biscuit's cache is: + +A resource control cache layer, based on carriers and resource-intensive policies [](https://github.com/oasisjs/biscuit) [](https://discord.gg/XNw2RZFzaP) +biscuit + +## Install (for [node18](https://nodejs.org/en/download/)) + +```sh-session +npm install @biscuitland/cache +``` + +## Example (Basic) + +```ts +import { Cache, MemoryCacheAdapter } from '@biscuitland/cache'; + +const bootstrap = async () => { + const cache = new Cache({ + adapter: new MemoryCacheAdapter(), + }); + + // You can listen to the raw biscuit event + + cache.start(); +}; + +bootstrap(); +``` + ## Links -- [Website](https://biscuitjs.com/) - [Documentation](https://docs.biscuitjs.com/) -- [Discord](https://discord.gg/XNw2RZFzaP) -- [core](https://www.npmjs.com/package/@biscuitland/core) | [api-types](https://www.npmjs.com/package/@biscuitland/api-types) | [rest](https://www.npmjs.com/package/@biscuitland/rest) | [ws](https://www.npmjs.com/package/@biscuitland/ws) | [helpers](https://www.npmjs.com/package/@biscuitland/helpers) +- [Website](https://biscuitjs.com/) diff --git a/packages/cache/src/adapters/cache-adapter.ts b/packages/cache/src/adapters/cache-adapter.ts deleted file mode 100644 index b944165..0000000 --- a/packages/cache/src/adapters/cache-adapter.ts +++ /dev/null @@ -1,50 +0,0 @@ -export interface CacheAdapter { - /** - * @inheritDoc - */ - - get(id: string): any | Promise; - get(id: string, guild?: string): string | Promise; - - /** - * @inheritDoc - */ - - set(id: string, data: any, expire?: number): void | Promise; - - /** - * @inheritDoc - */ - - count(to: string): number | Promise; - - /** - * @inheritDoc - */ - - remove(id: string): void | Promise; - - /** - * @inheritDoc - */ - - contains(to: string, id: string): boolean | Promise; - - /** - * @inheritDoc - */ - - getToRelationship(to: string): string[] | Promise; - - /** - * @inheritDoc - */ - - addToRelationship(to: string, id: string): void | Promise; - - /** - * @inheritDoc - */ - - removeToRelationship(to: string, id: string): void | Promise; -} diff --git a/packages/cache/src/adapters/redis-cache-adapter.ts b/packages/cache/src/adapters/redis-cache-adapter.ts deleted file mode 100644 index 9b0bbcf..0000000 --- a/packages/cache/src/adapters/redis-cache-adapter.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { CacheAdapter } from './cache-adapter'; - -import type { RedisOptions } from 'ioredis'; -import type Redis from 'ioredis'; -import IORedis from 'ioredis'; - -interface BaseOptions { - namespace: string; - expire?: number; -} - -interface BuildOptions extends BaseOptions, RedisOptions { } - -interface ClientOptions extends BaseOptions { - client: Redis; -} - -type Options = BuildOptions | ClientOptions; - -export class RedisCacheAdapter implements CacheAdapter { - private static readonly DEFAULTS = { - namespace: 'biscuitland' - }; - - private readonly client: Redis; - - readonly options: Options; - - constructor(options?: Options) { - this.options = Object.assign(RedisCacheAdapter.DEFAULTS, options); - - if ((this.options as ClientOptions).client) { - this.client = (this.options as ClientOptions).client; - } else { - const { ...redisOpt } = this.options as BuildOptions; - this.client = new IORedis(redisOpt); - } - } - - /** - * @inheritDoc - */ - - async get(id: string): Promise { - const data = await this.client.get(this.composite(id)); - - if (!data) { - return null; - } - - return JSON.parse(data); - } - - /** - * @inheritDoc - */ - - async set(id: string, data: unknown, expire = this.options.expire): Promise { - if (expire) { - await this.client.set(this.composite(id), JSON.stringify(data), 'EX', expire); - } else { - await this.client.set(this.composite(id), JSON.stringify(data)); - } - } - - /** - * @inheritDoc - */ - - async count(_to: string): Promise { - throw new Error('Method not implemented.'); - } - - /** - * @inheritDoc - */ - - async remove(id: string): Promise { - await this.client.del(this.composite(id)); - } - - /** - * @inheritDoc - */ - - async contains(_to: string, _id: string): Promise { - throw new Error('Method not implemented.'); - } - - /** - * @inheritDoc - */ - - async getToRelationship(_to: string): Promise { - throw new Error('Method not implemented.'); - } - - /** - * @inheritDoc - */ - - async addToRelationship(_to: string, _id: string): Promise { - throw new Error('Method not implemented.'); - } - - /** - * @inheritDoc - */ - - async removeToRelationship(_to: string, _id: string): Promise { - throw new Error('Method not implemented.'); - } - - /** - * @inheritDoc - */ - - composite(id: string): string { - return `${this.options.namespace}:${id}`; - } -} diff --git a/packages/cache/src/cache.ts b/packages/cache/src/cache.ts index 5ac79f5..f073969 100644 --- a/packages/cache/src/cache.ts +++ b/packages/cache/src/cache.ts @@ -1,6 +1,8 @@ /* eslint-disable no-case-declarations */ -import { MemoryCacheAdapter } from './adapters/memory-cache-adapter'; -// import { RedisCacheAdapter } from './adapters/redis-cache-adapter'; +import type { CacheOptions, CO } from './types'; + +import type { CacheAdapter } from './scheme/adapters/cache-adapter'; +import { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; import { ChannelResource, @@ -9,26 +11,23 @@ import { GuildResource, GuildRoleResource, GuildStickerResource, + GuildVoiceResource, + PresenceResource, UserResource, - VoiceResource } from './resources'; -/** - * add options and adaptor passable by options - * @default MemoryCacheAdapter - */ +import { Options } from './utils/options'; -/** - * Add more adapters and options - * Allow passing customizable resources and deleting resources - */ - -/** - * Add presence system (disabled by default) - * Add TTL option (default 7 days) - * Add permissions resource (accessible as a subResource) - */ export class Cache { + static readonly DEFAULTS = { + adapter: new MemoryCacheAdapter(), + }; + + readonly options: CO; + #adapter: CacheAdapter; + + // move to resources assigned + readonly channels: ChannelResource; readonly emojis: GuildEmojiResource; @@ -36,61 +35,64 @@ export class Cache { readonly guilds: GuildResource; readonly roles: GuildRoleResource; readonly stickers: GuildStickerResource; + readonly voices: GuildVoiceResource; + readonly presences: PresenceResource; readonly users: UserResource; - readonly voices: VoiceResource; - ready: boolean; + constructor(options: CacheOptions) { + this.options = Options({}, Cache.DEFAULTS, options); + this.#adapter = this.options.adapter; - constructor() { - this.ready = false; + this.channels = new ChannelResource(this.#adapter); - /** this change to memory */ - const adapter = new MemoryCacheAdapter(); + this.emojis = new GuildEmojiResource(this.#adapter); + this.members = new GuildMemberResource(this.#adapter); - this.channels = new ChannelResource(adapter); + this.guilds = new GuildResource(this.#adapter); + this.roles = new GuildRoleResource(this.#adapter); - this.emojis = new GuildEmojiResource(adapter); - this.members = new GuildMemberResource(adapter); + this.stickers = new GuildStickerResource(this.#adapter); + this.voices = new GuildVoiceResource(this.#adapter); - this.guilds = new GuildResource(adapter); - this.roles = new GuildRoleResource(adapter); - this.stickers = new GuildStickerResource(adapter); - - this.users = new UserResource(adapter); - this.voices = new VoiceResource(adapter); + this.presences = new PresenceResource(this.#adapter); + this.users = new UserResource(this.#adapter); } /** * @inheritDoc */ - async start(event: { t: string; d: any }) { + async start(event: any) { + let resources: any[] = []; + + let contents: any[] = []; + switch (event.t) { case 'READY': + resources = []; + await this.users.set(event.d.user.id, event.d.user); - const guilds: (Promise | undefined)[] = []; - for (const guild of event.d.guilds) { - guilds.push(this.guilds.set(guild.id, guild)); + resources.push(this.guilds.set(guild.id, guild)); } - await Promise.all(guilds); + await Promise.all(resources); - this.ready = true; break; case 'USER_UPDATE': await this.users.set(event.d.id, event.d); break; + case 'PRESENCE_UPDATE': + await this.presences.set(event.d.user?.id, event.d); - case 'GUILD_CREATE': - await this.guilds.set(event.d.id, event.d); break; + case 'GUILD_CREATE': case 'GUILD_UPDATE': - this.guilds.set(event.d.id, event.d); + await this.guilds.set(event.d.id, event.d); break; case 'GUILD_DELETE': @@ -102,10 +104,6 @@ export class Cache { break; case 'CHANNEL_CREATE': - // modify [Add elimination system] - await this.channels.set(event.d.id, event.d); - break; - case 'CHANNEL_UPDATE': // modify [Add elimination system] await this.channels.set(event.d.id, event.d); @@ -116,14 +114,18 @@ export class Cache { await this.channels.remove(event.d.id); break; - case 'GUILD_ROLE_CREATE': - await this.roles.set( - event.d.role.id, - event.d.guild_id, - event.d.role - ); + case 'MESSAGE_CREATE': + if (event.d.webhook_id) { + return; + } + + if (event.d.author) { + await this.users.set(event.d.author.id, event.d.author); + } + break; + case 'GUILD_ROLE_CREATE': case 'GUILD_ROLE_UPDATE': await this.roles.set( event.d.role.id, @@ -137,27 +139,62 @@ export class Cache { break; case 'GUILD_EMOJIS_UPDATE': - // modify [Add elimination system] - for (const v of event.d.emojis) { - await this.emojis?.set(v.id, event.d.guild_id, v); + contents = []; + contents = await this.emojis.items(event.d.guild_id); + + for (const emoji of event.d.emojis) { + const emote = contents.find(o => o?.id === emoji.id); + + if (!emote || emote !== emoji) { + await this.emojis.set( + emoji.id, + event.d.guild_id, + emoji + ); + } } + + for (const emoji of contents) { + const emote = event.d.emojis.find( + (o: any) => o.id === emoji?.id + ); + + if (!emote) { + await this.emojis.remove(emote.id, event.d.guild_id); + } + } + break; case 'GUILD_STICKERS_UPDATE': - // modify [Add elimination system] - for (const v of event.d.stickers) { - await this.stickers?.set(v.id, event.d.guild_id, v); + contents = []; + contents = await this.stickers.items(event.d.guild_id); + + for (const sticker of event.d.stickers) { + const stick = contents.find(o => o?.id === sticker.id); + + if (!stick || stick !== sticker) { + await this.stickers.set( + sticker.id, + event.d.guild_id, + sticker + ); + } } + + for (const sticker of contents) { + const stick = event.d.stickers.find( + (o: any) => o.id === sticker?.id + ); + + if (!stick) { + await this.stickers.remove(stick.id, event.d.guild_id); + } + } + break; case 'GUILD_MEMBER_ADD': - await this.members.set( - event.d.user.id, - event.d.guild_id, - event.d - ); - break; - case 'GUILD_MEMBER_UPDATE': await this.members.set( event.d.user.id, @@ -171,10 +208,10 @@ export class Cache { break; case 'GUILD_MEMBERS_CHUNK': - const members: (Promise | undefined)[] = []; + resources = []; for (const member of event.d.members) { - members.push( + resources.push( this.members.set( member.user.id, event.d.guild_id, @@ -183,7 +220,7 @@ export class Cache { ); } - await Promise.all(members); + await Promise.all(resources); break; @@ -192,16 +229,15 @@ export class Cache { return; } - if (event.d.user_id && event.d.member) { - await this.members.set( - event.d.user_id, - event.d.guild_id, - event.d.member - ); + if (event.d.guild_id && event.d.member && event.d.user_id) { + await this.members.set(event.d.user_id, event.d.guild_id, { + guild_id: event.d.guild_id, + ...event.d.member, + }); } if (event.d.channel_id != null) { - await this.voices.set( + await this.members.set( event.d.user_id, event.d.guild_id, event.d diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts index ada9f80..1b88152 100644 --- a/packages/cache/src/index.ts +++ b/packages/cache/src/index.ts @@ -1,6 +1,7 @@ -export { CacheAdapter } from './adapters/cache-adapter'; +export { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; +export { RedisCacheAdapter } from './scheme/adapters/redis-cache-adapter'; -export { MemoryCacheAdapter } from './adapters/memory-cache-adapter'; -export { RedisCacheAdapter } from './adapters/redis-cache-adapter'; +export { CacheAdapter } from './scheme/adapters/cache-adapter'; +export { RedisOptions, MemoryOptions, CacheOptions } from './types'; export { Cache } from './cache'; diff --git a/packages/cache/src/resources/base-resource.ts b/packages/cache/src/resources/base-resource.ts index b453e35..1457344 100644 --- a/packages/cache/src/resources/base-resource.ts +++ b/packages/cache/src/resources/base-resource.ts @@ -1,63 +1,117 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +/* eslint-disable @typescript-eslint/naming-convention */ +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; -export class BaseResource { - namespace = 'base'; +/** + * Base class for all resources + * All Methods from BaseResource are also available on every class extends + */ - adapter!: CacheAdapter; // replace +class Base { + /** + * Resource name + */ + + #namespace = 'base'; /** - * @inheritDoc + * Adapter for storage processes and operations + */ + + #adapter: CacheAdapter; + + /** + * Guild linked and assigned to the current entity (resource) + */ + + parent?: string; + + /** + * Constructor + */ + + constructor(namespace: string, adapter: CacheAdapter) { + this.#namespace = namespace; + this.#adapter = adapter; + } + + /** + * Entity linked + */ + + setEntity(entity: T): void { + Object.assign(this, entity); + } + + /** + * Parent linked + */ + + setParent(parent: string): void { + // rename + this.parent = parent; + } + + /** + * Count how many resources there are in the relationships */ async count(to: string): Promise { - return await this.adapter.count(this.hashId(to)); + return await this.#adapter.count(this.hashId(to)); } /** - * @inheritDoc + * Check if the resource is in the relationships */ - async contains(to: string, id: string): Promise { - return await this.adapter.contains(this.hashId(to), id); + async contains( + id: string, + guild: string = this.parent as string + ): Promise { + return await this.#adapter.contains(this.hashId(guild), id); } /** - * @inheritDoc + * Gets the resource relationships */ - async getToRelationship(to: string): Promise { - return await this.adapter.getToRelationship(this.hashId(to)); + async getToRelationship( + id: string = this.parent as string + ): Promise { + return await this.#adapter.getToRelationship(this.hashId(id)); } /** - * @inheritDoc + * Adds the resource to relationships */ - async addToRelationship(to: string, id: string): Promise { - await this.adapter.addToRelationship(this.hashId(to), id); + async addToRelationship( + id: string, + guild: string = this.parent as string + ): Promise { + await this.#adapter.addToRelationship(this.hashId(guild), id); } /** - * @inheritDoc // to-do replace + * Removes the relationship resource */ - async removeToRelationship(to: string, id: string): Promise { - await this.adapter.removeToRelationship(this.hashId(to), id); + async removeToRelationship( + id: string, + guild: string = this.parent as string + ): Promise { + await this.#adapter.removeToRelationship(this.hashId(guild), id); } /** - * @inheritDoc + * Construct an id consisting of namespace.id */ - hashId(id: string): string { - return `${this.namespace}.${id}`; - } - - /** - * @inheritDoc - */ - - hashGuildId(id: string, guild: string): string { - return `${this.namespace}.${guild}.${id}`; + protected hashId(id: string): string { + return `${this.#namespace}.${id}`; } } + +export const BaseResource = Base as new ( + data: string, + adapter: CacheAdapter +) => Base & T; diff --git a/packages/cache/src/resources/channel-resource.ts b/packages/cache/src/resources/channel-resource.ts index bcc65c1..93abe8b 100644 --- a/packages/cache/src/resources/channel-resource.ts +++ b/packages/cache/src/resources/channel-resource.ts @@ -1,42 +1,92 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +/** + * refactor + */ + +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordChannel } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; +import { UserResource } from './user-resource'; -export class ChannelResource extends BaseResource { - namespace = 'channel' as const; +/** + * Resource represented by an channel of discord + */ - adapter: CacheAdapter; +export class ChannelResource extends BaseResource { + #namespace = 'channel'; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; + #users: UserResource; + + constructor(adapter: CacheAdapter, entity?: DiscordChannel | null) { + super('channel', adapter); + + this.#adapter = adapter; + this.#users = new UserResource(adapter); + + if (entity) { + this.setEntity(entity); + } } /** * @inheritDoc */ - async get(id: string): Promise { - const kv = await this.adapter.get(this.hashId(id)); + async get(id: string): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashId(id)); if (kv) { - return kv; + return new ChannelResource(this.#adapter, kv); } return null; } /** - * @inheritDoc // to-do rework + * @inheritDoc */ - async set(id: string, data: any, expire?: number): Promise { + async set(id: string, data: any): Promise { + if (data.recipients) { + const recipients = []; + + for (const recipient of data.recipients) { + recipients.push(this.#users.set(recipient.id, recipient)); + } + + await Promise.all(recipients); + } + delete data.recipients; + delete data.permission_overwrites; await this.addToRelationship(id); - await this.adapter.set(this.hashId(id), data, expire); + await this.#adapter.set(this.hashId(id), data); + } + + /** + * @inheritDoc + */ + + async items(): Promise { + const data = await this.#adapter.items(this.#namespace); + + if (data) { + return data.map(dt => { + const resource = new ChannelResource(this.#adapter, dt); + resource.setParent(resource.id); + + return resource; + }); + } + + return []; } /** @@ -44,7 +94,7 @@ export class ChannelResource extends BaseResource { */ async count(): Promise { - return await this.adapter.count(this.namespace); + return await this.#adapter.count(this.#namespace); } /** @@ -52,7 +102,7 @@ export class ChannelResource extends BaseResource { */ async remove(id: string): Promise { - await this.adapter.remove(this.hashId(id)); + await this.#adapter.remove(this.hashId(id)); } /** @@ -60,7 +110,7 @@ export class ChannelResource extends BaseResource { */ async contains(id: string): Promise { - return await this.adapter.contains(this.namespace, id); + return await this.#adapter.contains(this.#namespace, id); } /** @@ -68,7 +118,7 @@ export class ChannelResource extends BaseResource { */ async getToRelationship(): Promise { - return await this.adapter.getToRelationship(this.namespace); + return await this.#adapter.getToRelationship(this.#namespace); } /** @@ -76,6 +126,14 @@ export class ChannelResource extends BaseResource { */ async addToRelationship(id: string): Promise { - await this.adapter.addToRelationship(this.namespace, id); + await this.#adapter.addToRelationship(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async removeToRelationship(id: string): Promise { + await this.#adapter.removeToRelationship(this.#namespace, id); } } diff --git a/packages/cache/src/resources/guild-emoji-resource.ts b/packages/cache/src/resources/guild-emoji-resource.ts index accd145..85e6eba 100644 --- a/packages/cache/src/resources/guild-emoji-resource.ts +++ b/packages/cache/src/resources/guild-emoji-resource.ts @@ -1,28 +1,55 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordEmoji } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; +import { UserResource } from './user-resource'; -export class GuildEmojiResource extends BaseResource { - namespace = 'emoji' as const; +/** + * Resource represented by an emoji of discord + */ - adapter: CacheAdapter; +export class GuildEmojiResource extends BaseResource { + #namespace = 'emoji' as const; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; + #users: UserResource; + + constructor( + adapter: CacheAdapter, + entity?: DiscordEmoji | null, + parent?: string + ) { + super('emoji', adapter); + + this.#adapter = adapter; + this.#users = new UserResource(adapter); + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } } /** * @inheritDoc */ - async get(id: string, guild: string): Promise { - const kv = await this.adapter.get(this.hashGuildId(id, guild)); + async get( + id: string, + guild: string | undefined = this.parent + ): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashGuildId(id, guild)); if (kv) { - return kv; + return new GuildEmojiResource(this.#adapter, kv, guild); } return null; @@ -34,18 +61,68 @@ export class GuildEmojiResource extends BaseResource { async set( id: string, - guild: string, - data: any, - expire?: number + guild: string | undefined = this.parent, + data: any ): Promise { - await this.adapter.set(this.hashGuildId(id, guild), data, expire); + if (data.user) { + await this.#users.set(data.user.id, data.user); + } + + delete data.user; + delete data.roles; + + if (this.parent) { + this.setEntity(data); + } + + await this.addToRelationship(id, guild); + await this.#adapter.set(this.hashGuildId(id, guild), data); } /** * @inheritDoc */ - async remove(id: string, guild: string): Promise { - await this.adapter.remove(this.hashGuildId(id, guild)); + async items(to: string): Promise { + if (!to && this.parent) { + to = this.parent; + } + + const data = await this.#adapter.items(this.hashId(to)); + + if (data) { + return data.map(dt => { + const resource = new GuildEmojiResource(this.#adapter, dt); + resource.setParent(to); + + return resource; + }); + } + + return []; + } + + /** + * @inheritDoc + */ + + async remove( + id: string, + guild: string | undefined = this.parent + ): Promise { + await this.removeToRelationship(id, guild); + await this.#adapter.remove(this.hashGuildId(id, guild)); + } + + /** + * @inheritDoc + */ + + protected hashGuildId(id: string, guild?: string): string { + if (!guild) { + return this.hashId(id); + } + + return `${this.#namespace}.${guild}.${id}`; } } diff --git a/packages/cache/src/resources/guild-member-resource.ts b/packages/cache/src/resources/guild-member-resource.ts index 87ac4b1..603d8ca 100644 --- a/packages/cache/src/resources/guild-member-resource.ts +++ b/packages/cache/src/resources/guild-member-resource.ts @@ -1,20 +1,37 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordMember } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; import { UserResource } from './user-resource'; -export class GuildMemberResource extends BaseResource { - namespace = 'member' as const; +/** + * Resource represented by an member of discord + */ - adapter: CacheAdapter; - users: UserResource; +export class GuildMemberResource extends BaseResource { + #namespace = 'member' as const; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; - this.users = new UserResource(adapter); + #users: UserResource; + + constructor( + adapter: CacheAdapter, + entity?: DiscordMember | null, + parent?: string + ) { + super('member', adapter); + + this.#adapter = adapter; + this.#users = new UserResource(adapter); + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } } /** @@ -23,12 +40,16 @@ export class GuildMemberResource extends BaseResource { async get( id: string, - guild: string - ): Promise<(DiscordMember & { id: string }) | null> { - const kv = await this.adapter.get(this.hashGuildId(id, guild)); + guild: string | undefined = this.parent + ): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashGuildId(id, guild)); if (kv) { - return kv; + return new GuildMemberResource(this.#adapter, kv, guild); } return null; @@ -40,33 +61,68 @@ export class GuildMemberResource extends BaseResource { async set( id: string, - guild: string, - data: any, - expire?: number + guild: string | undefined = this.parent, + data: any ): Promise { - if (!data.id) { - data.id = id; - } - if (data.user) { - await this.users.set(data.user.id, data.user); - } - - if (!data.guild_id) { - data.guild_id = guild; + await this.#users.set(data.user.id, data.user); } delete data.user; + delete data.roles; + + if (this.parent) { + this.setEntity(data); + } await this.addToRelationship(id, guild); - await this.adapter.set(this.hashGuildId(id, guild), data, expire); + await this.#adapter.set(this.hashGuildId(id, guild), data); } /** * @inheritDoc */ - async remove(id: string, guild: string): Promise { - await this.adapter.remove(this.hashGuildId(id, guild)); + async items(to: string): Promise { + if (!to && this.parent) { + to = this.parent; + } + + const data = await this.#adapter.items(this.hashId(to)); + + if (data) { + return data.map(dt => { + const resource = new GuildMemberResource(this.#adapter, dt); + resource.setParent(to); + + return resource; + }); + } + + return []; + } + + /** + * @inheritDoc + */ + + async remove( + id: string, + guild: string | undefined = this.parent + ): Promise { + await this.removeToRelationship(id, guild); + await this.#adapter.remove(this.hashGuildId(id, guild)); + } + + /** + * @inheritDoc + */ + + protected hashGuildId(id: string, guild?: string): string { + if (!guild) { + return this.hashId(id); + } + + return `${this.#namespace}.${guild}.${id}`; } } diff --git a/packages/cache/src/resources/guild-resource.ts b/packages/cache/src/resources/guild-resource.ts index f7d80a1..3633ec0 100644 --- a/packages/cache/src/resources/guild-resource.ts +++ b/packages/cache/src/resources/guild-resource.ts @@ -1,4 +1,8 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +/** + * refactor + */ + +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordGuild } from '@biscuitland/api-types'; import { ChannelResource } from './channel-resource'; @@ -6,44 +10,90 @@ import { GuildEmojiResource } from './guild-emoji-resource'; import { GuildMemberResource } from './guild-member-resource'; import { GuildRoleResource } from './guild-role-resource'; import { GuildStickerResource } from './guild-sticker-resource'; -import { VoiceResource } from './voice-resource'; +import { GuildVoiceResource } from './guild-voice-resource'; +import { PresenceResource } from './presence-resource'; import { BaseResource } from './base-resource'; -export class GuildResource extends BaseResource { - namespace = 'guild' as const; +/** + * Resource represented by an guild of discord + */ - adapter: CacheAdapter; +export class GuildResource extends BaseResource { + #namespace = 'guild' as const; - private channels: ChannelResource; - private emojis: GuildEmojiResource; - private members: GuildMemberResource; - private roles: GuildRoleResource; - private stickers: GuildStickerResource; - private voices: VoiceResource; + #adapter: CacheAdapter; - constructor(adapter: CacheAdapter) { - super(); + #channels: ChannelResource; + #emojis: GuildEmojiResource; + #members: GuildMemberResource; + #roles: GuildRoleResource; + #stickers: GuildStickerResource; + #voices: GuildVoiceResource; - this.adapter = adapter; + #presences: PresenceResource; - this.channels = new ChannelResource(adapter); - this.emojis = new GuildEmojiResource(adapter); - this.members = new GuildMemberResource(adapter); - this.roles = new GuildRoleResource(adapter); - this.stickers = new GuildStickerResource(adapter); - this.voices = new VoiceResource(adapter); + constructor( + adapter: CacheAdapter, + entity?: DiscordGuild | null, + parent?: string, + channels?: ChannelResource, + emojis?: GuildEmojiResource, + members?: GuildMemberResource, + roles?: GuildRoleResource, + stickers?: GuildStickerResource, + voices?: GuildVoiceResource, + presences?: PresenceResource + ) { + super('guild', adapter); + + this.#adapter = adapter; + + this.#channels = channels ?? new ChannelResource(adapter); + + this.#emojis = emojis ?? new GuildEmojiResource(adapter); + this.#members = members ?? new GuildMemberResource(adapter); + + this.#roles = roles ?? new GuildRoleResource(adapter); + + this.#stickers = stickers ?? new GuildStickerResource(adapter); + + this.#voices = voices ?? new GuildVoiceResource(adapter); + this.#presences = presences ?? new PresenceResource(adapter); + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } } /** * @inheritDoc */ - async get(id: string): Promise { - const kv = await this.adapter.get(this.hashId(id)); + async get(id: string): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashId(id)); if (kv) { - return kv; + return new GuildResource( + this.#adapter, + kv, + id, + new ChannelResource(this.#adapter), + new GuildEmojiResource(this.#adapter, null, id), + new GuildMemberResource(this.#adapter, null, id), + new GuildRoleResource(this.#adapter, null, id), + new GuildStickerResource(this.#adapter, null, id), + new GuildVoiceResource(this.#adapter, null, id), + new PresenceResource(this.#adapter) + ); } return null; @@ -53,83 +103,125 @@ export class GuildResource extends BaseResource { * @inheritDoc */ - async set(id: string, data: any, expire?: number): Promise { + async set(id: string, data: any): Promise { if (data.channels) { - const channels: (Promise | undefined)[] = []; + const channels: unknown[] = []; for (const channel of data.channels) { - await this.channels.set(channel.id, channel); + channel.guild_id = id; + + await this.#channels.set(channel.id, channel); } await Promise.all(channels); } + if (data.emojis) { + const emojis: unknown[] = []; + + for (const emoji of data.emojis) { + emoji.guild_id = id; + + await this.#emojis.set(emoji.id, id, emoji); + } + + await Promise.all(emojis); + } + if (data.members) { - const members: (Promise | undefined)[] = []; + const members: unknown[] = []; for (const member of data.members) { - await this.members.set(member.user.id, id, member); + member.guild_id = id; + + await this.#members.set(member.user.id, id, member); } await Promise.all(members); } if (data.roles) { - const roles: (Promise | undefined)[] = []; + const roles: unknown[] = []; for (const role of data.roles) { - await this.roles.set(role.id, id, role); + role.guild_id = id; + + await this.#roles.set(role.id, id, role); } await Promise.all(roles); } if (data.stickers) { - const stickers: (Promise | undefined)[] = []; + const stickers: unknown[] = []; for (const sticker of data.stickers) { - await this.stickers.set(sticker.id, id, sticker); + sticker.guild_id = id; + + await this.#stickers.set(sticker.id, id, sticker); } await Promise.all(stickers); } - if (data.emojis) { - const emojis: (Promise | undefined)[] = []; - - for (const emoji of data.emojis) { - await this.emojis.set(emoji.id, id, emoji); - } - - await Promise.all(emojis); - } - if (data.voice_states) { - const voices: Promise[] = []; + const voices: unknown[] = []; for (const voice of data.voice_states) { - if (!voice.guild_id) { - voice.guild_id = id; - } + voice.guild_id = id; - voices.push(this.voices.set(voice.user_id, id, voice)); + voices.push(this.#voices.set(voice.user_id, id, voice)); } await Promise.all(voices); } + if (data.presences) { + const presences: unknown[] = []; + + for (const presence of data.presences) { + await this.#presences.set(presence.user.id, presence); + } + + await Promise.all(presences); + } + delete data.channels; + delete data.emojis; delete data.members; delete data.roles; delete data.stickers; - delete data.emojis; + + delete data.voice_states; + delete data.guild_hashes; delete data.presences; - delete data.voice_states; + if (this.parent) { + this.setEntity(data); + } await this.addToRelationship(id); - await this.adapter.set(this.hashId(id), data, expire); + await this.#adapter.set(this.hashId(id), data); + } + + /** + * @inheritDoc + */ + + async items(): Promise { + const data = await this.#adapter.items(this.#namespace); + + if (data) { + return data.map(dt => { + const resource = new GuildResource(this.#adapter, dt); + resource.setParent(resource.id); + + return resource; + }); + } + + return []; } /** @@ -137,7 +229,7 @@ export class GuildResource extends BaseResource { */ async count(): Promise { - return await this.adapter.count(this.namespace); + return await this.#adapter.count(this.#namespace); } /** @@ -145,7 +237,32 @@ export class GuildResource extends BaseResource { */ async remove(id: string): Promise { - await this.adapter.remove(this.hashId(id)); + const members = await this.#members.getToRelationship(id); + + for (const member of members) { + await this.#members.remove(member, id); + } + + const roles = await this.#roles.getToRelationship(id); + + for (const role of roles) { + await this.#roles.remove(role, id); + } + + const emojis = await this.#emojis.getToRelationship(id); + + for (const emoji of emojis) { + await this.#emojis.remove(emoji, id); + } + + const stickers = await this.#stickers.getToRelationship(id); + + for (const sticker of stickers) { + await this.#stickers.remove(sticker, id); + } + + await this.removeToRelationship(id); + await this.#adapter.remove(this.hashId(id)); } /** @@ -153,7 +270,7 @@ export class GuildResource extends BaseResource { */ async contains(id: string): Promise { - return await this.adapter.contains(this.namespace, id); + return await this.#adapter.contains(this.#namespace, id); } /** @@ -161,7 +278,7 @@ export class GuildResource extends BaseResource { */ async getToRelationship(): Promise { - return await this.adapter.getToRelationship(this.namespace); + return await this.#adapter.getToRelationship(this.#namespace); } /** @@ -169,6 +286,54 @@ export class GuildResource extends BaseResource { */ async addToRelationship(id: string): Promise { - await this.adapter.addToRelationship(this.namespace, id); + await this.#adapter.addToRelationship(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async removeToRelationship(id: string): Promise { + await this.#adapter.removeToRelationship(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async getEmojis(): Promise { + return await this.#emojis.items(this.parent as string); + } + + /** + * @inheritDoc + */ + + async getMembers(): Promise { + return await this.#members.items(this.parent as string); + } + + /** + * @inheritDoc + */ + + async getRoles(): Promise { + return await this.#roles.items(this.parent as string); + } + + /** + * @inheritDoc + */ + + async getStickers(): Promise { + return await this.#stickers.items(this.parent as string); + } + + /** + * @inheritDoc + */ + + async getVoiceStates(): Promise { + return await this.#voices.items(this.parent as string); } } diff --git a/packages/cache/src/resources/guild-role-resource.ts b/packages/cache/src/resources/guild-role-resource.ts index 7c69b99..e9170f5 100644 --- a/packages/cache/src/resources/guild-role-resource.ts +++ b/packages/cache/src/resources/guild-role-resource.ts @@ -1,28 +1,51 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordRole } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; -export class GuildRoleResource extends BaseResource { - namespace = 'role' as const; +/** + * Resource represented by an role of discord + */ - adapter: CacheAdapter; +export class GuildRoleResource extends BaseResource { + #namespace = 'role' as const; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; + constructor( + adapter: CacheAdapter, + entity?: DiscordRole | null, + parent?: string + ) { + super('role', adapter); + + this.#adapter = adapter; + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } } /** * @inheritDoc */ - async get(id: string, guild: string): Promise { - const kv = await this.adapter.get(this.hashGuildId(id, guild)); + async get( + id: string, + guild: string | undefined = this.parent + ): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashGuildId(id, guild)); if (kv) { - return kv; + return new GuildRoleResource(this.#adapter, kv, guild); } return null; @@ -34,9 +57,8 @@ export class GuildRoleResource extends BaseResource { async set( id: string, - guild: string, - data: any, - expire?: number + guild: string | undefined = this.parent, + data: any ): Promise { if (!data.id) { data.id = id; @@ -46,14 +68,66 @@ export class GuildRoleResource extends BaseResource { data.guild_id = guild; } - await this.adapter.set(this.hashGuildId(id, guild), data, expire); + if (this.parent) { + this.setEntity(data); + } + + await this.addToRelationship(id, guild); + await this.#adapter.set(this.hashGuildId(id, guild), data); } /** * @inheritDoc */ - async remove(id: string, guild: string): Promise { - await this.adapter.remove(this.hashGuildId(id, guild)); + async count(): Promise { + return await this.#adapter.count(this.#namespace); + } + + /** + * @inheritDoc + */ + + async items(to: string): Promise { + if (!to && this.parent) { + to = this.parent; + } + + const data = await this.#adapter.items(this.hashId(to)); + + if (data) { + return data.map(dt => { + const resource = new GuildRoleResource(this.#adapter, dt); + resource.setParent(to); + + return resource; + }); + } + + return []; + } + + /** + * @inheritDoc + */ + + async remove( + id: string, + guild: string | undefined = this.parent + ): Promise { + await this.removeToRelationship(id, guild); + await this.#adapter.remove(this.hashGuildId(id, guild)); + } + + /** + * @inheritDoc + */ + + protected hashGuildId(id: string, guild?: string): string { + if (!guild) { + return this.hashId(id); + } + + return `${this.#namespace}.${guild}.${id}`; } } diff --git a/packages/cache/src/resources/guild-sticker-resource.ts b/packages/cache/src/resources/guild-sticker-resource.ts index e7938b0..de8d610 100644 --- a/packages/cache/src/resources/guild-sticker-resource.ts +++ b/packages/cache/src/resources/guild-sticker-resource.ts @@ -1,28 +1,52 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordSticker } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; +import { UserResource } from './user-resource'; -export class GuildStickerResource extends BaseResource { - namespace = 'sticker' as const; +/** + * Resource represented by an sticker of discord + */ - adapter: CacheAdapter; +export class GuildStickerResource extends BaseResource { + #namespace = 'sticker' as const; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; + #users: UserResource; + + constructor( + adapter: CacheAdapter, + entity?: DiscordSticker | null, + parent?: string + ) { + super('sticker', adapter); + + this.#adapter = adapter; + this.#users = new UserResource(adapter); + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } } /** * @inheritDoc */ - async get(id: string, guild: string): Promise { - const kv = await this.adapter.get(this.hashGuildId(id, guild)); + async get(id: string, guild: string): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashGuildId(id, guild)); if (kv) { - return kv; + return new GuildStickerResource(this.#adapter, kv, guild); } return null; @@ -34,26 +58,67 @@ export class GuildStickerResource extends BaseResource { async set( id: string, - guild: string, - data: any, - expire?: number + guild: string | undefined = this.parent, + data: any ): Promise { - if (!data.id) { - data.id = id; + if (data.user) { + await this.#users.set(data.user.id, data.user); } - if (!data.guild_id) { - data.guild_id = guild; + delete data.user; + + if (this.parent) { + this.setEntity(data); } - await this.adapter.set(this.hashGuildId(id, guild), data, expire); + await this.addToRelationship(id, guild); + await this.#adapter.set(this.hashGuildId(id, guild), data); } /** * @inheritDoc */ - async remove(id: string, guild: string): Promise { - await this.adapter.remove(this.hashGuildId(id, guild)); + async items(to: string): Promise { + if (!to && this.parent) { + to = this.parent; + } + + const data = await this.#adapter.items(this.hashId(to)); + + if (data) { + return data.map(dt => { + const resource = new GuildStickerResource(this.#adapter, dt); + resource.setParent(to); + + return resource; + }); + } + + return []; + } + + /** + * @inheritDoc + */ + + async remove( + id: string, + guild: string | undefined = this.parent + ): Promise { + await this.removeToRelationship(id, guild); + await this.#adapter.remove(this.hashGuildId(id, guild)); + } + + /** + * @inheritDoc + */ + + protected hashGuildId(id: string, guild?: string): string { + if (!guild) { + return this.hashId(id); + } + + return `${this.#namespace}.${guild}.${id}`; } } diff --git a/packages/cache/src/resources/guild-voice-resource.ts b/packages/cache/src/resources/guild-voice-resource.ts new file mode 100644 index 0000000..0c370eb --- /dev/null +++ b/packages/cache/src/resources/guild-voice-resource.ts @@ -0,0 +1,127 @@ +/** + * refactor + */ + +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; +import type { DiscordVoiceState } from '@biscuitland/api-types'; + +import { BaseResource } from './base-resource'; + +/** + * Resource represented by an voice state of discord + */ + +export class GuildVoiceResource extends BaseResource { + #namespace = 'voice' as const; + + #adapter: CacheAdapter; + + constructor( + adapter: CacheAdapter, + entity?: DiscordVoiceState | null, + parent?: string + ) { + super('voice', adapter); + + this.#adapter = adapter; + + if (entity) { + this.setEntity(entity); + } + + if (parent) { + this.setParent(parent); + } + } + + /** + * @inheritDoc + */ + + async get( + id: string, + guild: string | undefined = this.parent + ): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashGuildId(id, guild)); + + if (kv) { + return new GuildVoiceResource(this.#adapter, kv, guild); + } + + return null; + } + + /** + * @inheritDoc + */ + + async set( + id: string, + guild: string | undefined = this.parent, + data: any + ): Promise { + if (!data.guild_id) { + data.guild_id = guild; + } + + delete data.member; + + if (this.parent) { + this.setEntity(data); + } + + await this.addToRelationship(id, guild); + await this.#adapter.set(this.hashGuildId(id, guild), data); + } + + /** + * @inheritDoc + */ + + async items(to: string): Promise { + if (!to && this.parent) { + to = this.parent; + } + + const data = await this.#adapter.items(this.hashId(to)); + + if (data) { + return data.map(dt => { + const resource = new GuildVoiceResource(this.#adapter, dt); + resource.setParent(to); + + return resource; + }); + } + + return []; + } + + /** + * @inheritDoc + */ + + async remove( + id: string, + guild: string | undefined = this.parent + ): Promise { + await this.removeToRelationship(id, guild); + await this.#adapter.remove(this.hashGuildId(id, guild)); + } + + /** + * @inheritDoc + */ + + protected hashGuildId(id: string, guild?: string): string { + if (!guild) { + return this.hashId(id); + } + + return `${this.#namespace}.${guild}.${id}`; + } +} diff --git a/packages/cache/src/resources/index.ts b/packages/cache/src/resources/index.ts index c9bdb3b..3207fa7 100644 --- a/packages/cache/src/resources/index.ts +++ b/packages/cache/src/resources/index.ts @@ -7,7 +7,9 @@ export { GuildMemberResource } from './guild-member-resource'; export { GuildResource } from './guild-resource'; export { GuildRoleResource } from './guild-role-resource'; -export { GuildStickerResource } from './guild-sticker-resource'; +export { GuildStickerResource } from './guild-sticker-resource'; +export { GuildVoiceResource } from './guild-voice-resource'; + +export { PresenceResource } from './presence-resource'; export { UserResource } from './user-resource'; -export { VoiceResource } from './voice-resource'; diff --git a/packages/cache/src/resources/presence-resource.ts b/packages/cache/src/resources/presence-resource.ts new file mode 100644 index 0000000..50ce1c1 --- /dev/null +++ b/packages/cache/src/resources/presence-resource.ts @@ -0,0 +1,135 @@ +/** + * refactor + */ + +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; +import type { DiscordPresenceUpdate } from '@biscuitland/api-types'; + +import { BaseResource } from './base-resource'; +import { UserResource } from './user-resource'; + +/** + * Resource represented by an presence of discord + */ + +export class PresenceResource extends BaseResource { + #namespace = 'presence' as const; + + #adapter: CacheAdapter; + + #users: UserResource; + + constructor(adapter: CacheAdapter, entity?: DiscordPresenceUpdate | null) { + super('presence', adapter); + + this.#adapter = adapter; + this.#users = new UserResource(this.#adapter); + + if (entity) { + this.setEntity(entity); + } + } + + /** + * @inheritDoc + */ + + async get(id: string): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashId(id)); + + if (kv) { + return new PresenceResource(this.#adapter, kv); + } + + return null; + } + + /** + * @inheritDoc + */ + + async set(id: string, data: any): Promise { + if (data.user) { + await this.#users.set(data.user.id, data.user); + } + + delete data.user; + delete data.roles; + + delete data.guild_id; + + if (this.parent) { + this.setEntity(data); + } + + await this.addToRelationship(id); + await this.#adapter.set(this.hashId(id), data); + } + + /** + * @inheritDoc + */ + + async items(): Promise { + const data = await this.#adapter.items(this.#namespace); + + if (data) { + return data.map(dt => new PresenceResource(this.#adapter, dt)); + } + + return []; + } + + /** + * @inheritDoc + */ + + async count(): Promise { + return await this.#adapter.count(this.#namespace); + } + + /** + * @inheritDoc + */ + + async remove(id: string): Promise { + await this.removeToRelationship(id); + await this.#adapter.remove(this.hashId(id)); + } + + /** + * @inheritDoc + */ + + async contains(id: string): Promise { + return await this.#adapter.contains(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async getToRelationship(): Promise { + return await this.#adapter.getToRelationship(this.#namespace); + } + + /** + * @inheritDoc + */ + + async addToRelationship(id: string): Promise { + await this.#adapter.addToRelationship(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async removeToRelationship(id: string): Promise { + await this.#adapter.removeToRelationship(this.#namespace, id); + } +} diff --git a/packages/cache/src/resources/user-resource.ts b/packages/cache/src/resources/user-resource.ts index 310d3ef..8d100a4 100644 --- a/packages/cache/src/resources/user-resource.ts +++ b/packages/cache/src/resources/user-resource.ts @@ -1,28 +1,44 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; +/** + * refactor + */ + +import type { CacheAdapter } from '../scheme/adapters/cache-adapter'; import type { DiscordUser } from '@biscuitland/api-types'; import { BaseResource } from './base-resource'; -export class UserResource extends BaseResource { - namespace = 'user' as const; +/** + * Resource represented by an user of discord + */ - adapter: CacheAdapter; +export class UserResource extends BaseResource { + #namespace = 'user' as const; - constructor(adapter: CacheAdapter) { - super(); + #adapter: CacheAdapter; - this.adapter = adapter; + constructor(adapter: CacheAdapter, entity?: DiscordUser | null) { + super('user', adapter); + + this.#adapter = adapter; + + if (entity) { + this.setEntity(entity); + } } /** * @inheritDoc */ - async get(id: string): Promise { - const kv = await this.adapter.get(this.hashId(id)); + async get(id: string): Promise { + if (this.parent) { + return this; + } + + const kv = await this.#adapter.get(this.hashId(id)); if (kv) { - return kv; + return new UserResource(this.#adapter, kv); } return null; @@ -32,8 +48,35 @@ export class UserResource extends BaseResource { * @inheritDoc */ - async set(id: string, data: any, expire?: number): Promise { - await this.adapter.set(this.hashId(id), data, expire); + async set(id: string, data: any): Promise { + if (this.parent) { + this.setEntity(data); + } + + await this.addToRelationship(id); + await this.#adapter.set(this.hashId(id), data); + } + + /** + * @inheritDoc + */ + + async items(): Promise { + const data = await this.#adapter.items(this.#namespace); + + if (data) { + return data.map(dt => new UserResource(this.#adapter, dt)); + } + + return []; + } + + /** + * @inheritDoc + */ + + async count(): Promise { + return await this.#adapter.count(this.#namespace); } /** @@ -41,6 +84,39 @@ export class UserResource extends BaseResource { */ async remove(id: string): Promise { - await this.adapter.remove(this.hashId(id)); + await this.removeToRelationship(id); + await this.#adapter.remove(this.hashId(id)); + } + + /** + * @inheritDoc + */ + + async contains(id: string): Promise { + return await this.#adapter.contains(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async getToRelationship(): Promise { + return await this.#adapter.getToRelationship(this.#namespace); + } + + /** + * @inheritDoc + */ + + async addToRelationship(id: string): Promise { + await this.#adapter.addToRelationship(this.#namespace, id); + } + + /** + * @inheritDoc + */ + + async removeToRelationship(id: string): Promise { + await this.#adapter.removeToRelationship(this.#namespace, id); } } diff --git a/packages/cache/src/resources/voice-resource.ts b/packages/cache/src/resources/voice-resource.ts deleted file mode 100644 index 20b25d5..0000000 --- a/packages/cache/src/resources/voice-resource.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { CacheAdapter } from '../adapters/cache-adapter'; -import type { DiscordVoiceState } from '@biscuitland/api-types'; - -import { BaseResource } from './base-resource'; - -export class VoiceResource extends BaseResource { - namespace = 'voice' as const; - - adapter: CacheAdapter; - - constructor(adapter: CacheAdapter) { - super(); - - this.adapter = adapter; - } - - /** - * @inheritDoc - */ - - async get(id: string, guild: string): Promise { - const kv = await this.adapter.get(this.hashGuildId(id, guild)); - - if (kv) { - return kv; - } - - return null; - } - - /** - * @inheritDoc - */ - - async set( - id: string, - guild: string, - data: any, - expire?: number - ): Promise { - if (!data.guild_id) { - data.guild_id = guild; - } - - delete data.member; - - await this.adapter.set(this.hashGuildId(id, guild), data, expire); - } - - /** - * @inheritDoc - */ - - async remove(id: string, guild: string): Promise { - await this.adapter.remove(this.hashGuildId(id, guild)); - } -} diff --git a/packages/cache/src/scheme/adapters/cache-adapter.ts b/packages/cache/src/scheme/adapters/cache-adapter.ts new file mode 100644 index 0000000..c5323a2 --- /dev/null +++ b/packages/cache/src/scheme/adapters/cache-adapter.ts @@ -0,0 +1,63 @@ +/** + * Base class for all adapters + * All Methods from CacheAdapter are also available on every class extends + */ + +export interface CacheAdapter { + /** + * Gets the resource to adapter + */ + + get(id: string): any | Promise; + get(id: string, guild: string): string | Promise; + + /** + * Sets the resource to adapter + */ + + set(id: string, data: any): void | Promise; + set(id: string, guild: string, data: any): void | Promise; + + /** + * Get the items of a relationship + */ + + items(to?: string): any[] | Promise; + + /** + * Count how many resources there are in the relationships + */ + + count(to: string): number | Promise; + + /** + * Removes the adapter resource + */ + + remove(id: string): void | Promise; + remove(id: string, guild: string): void | Promise; + + /** + * Check if the resource is in the relationships + */ + + contains(to: string, id: string): boolean | Promise; + + /** + * Gets the resource relationships + */ + + getToRelationship(to: string): string[] | Promise; + + /** + * Adds the resource to relationships + */ + + addToRelationship(to: string, id: string): void | Promise; + + /** + * Removes the relationship resource + */ + + removeToRelationship(to: string, id: string): void | Promise; +} diff --git a/packages/cache/src/adapters/memory-cache-adapter.ts b/packages/cache/src/scheme/adapters/memory-cache-adapter.ts similarity index 62% rename from packages/cache/src/adapters/memory-cache-adapter.ts rename to packages/cache/src/scheme/adapters/memory-cache-adapter.ts index 24dd640..fabb5ce 100644 --- a/packages/cache/src/adapters/memory-cache-adapter.ts +++ b/packages/cache/src/scheme/adapters/memory-cache-adapter.ts @@ -1,20 +1,24 @@ -import type { CacheAdapter } from './cache-adapter'; +/** + * refactor + */ -interface Options { - expire?: number; -} +import type { CacheAdapter } from './cache-adapter'; +import type { MemoryOptions, MO } from '../../types'; + +import { Options } from '../../utils/options'; export class MemoryCacheAdapter implements CacheAdapter { - private static readonly DEFAULTS = { + static readonly DEFAULTS = { + expire: 3600000, }; - private readonly relationships = new Map(); - private readonly storage = new Map(); + readonly relationships = new Map(); + readonly storage = new Map(); - readonly options: Options; + readonly options: MO; - constructor(options?: Options) { - this.options = Object.assign(MemoryCacheAdapter.DEFAULTS, options); + constructor(options?: MemoryOptions) { + this.options = Options({}, MemoryCacheAdapter.DEFAULTS, options); } /** @@ -39,14 +43,40 @@ export class MemoryCacheAdapter implements CacheAdapter { * @inheritDoc */ - set(id: string, data: any, expire = this.options.expire): void { + set(id: string, data: any): void { + const expire = this.options.expire; + if (expire) { - this.storage.set(id, { data: JSON.stringify(data), expire: Date.now() + expire }); + this.storage.set(id, { + data: JSON.stringify(data), + expire: Date.now() + expire, + }); } else { this.storage.set(id, { data: JSON.stringify(data) }); } } + /** + * @inheritDoc + */ + + items(to: string): any[] { + const array: unknown[] = []; + let data = this.getToRelationship(to); + + data = data.map(id => `${to}.${id}`); + + for (const key of data) { + const content = this.get(key); + + if (content) { + array.push(content); + } + } + + return array; + } + /** * @inheritDoc */ diff --git a/packages/cache/src/scheme/adapters/redis-cache-adapter.ts b/packages/cache/src/scheme/adapters/redis-cache-adapter.ts new file mode 100644 index 0000000..8a87193 --- /dev/null +++ b/packages/cache/src/scheme/adapters/redis-cache-adapter.ts @@ -0,0 +1,195 @@ +/** + * refactor + */ + +import type { CacheAdapter } from './cache-adapter'; + +import type { RedisOptions } from 'ioredis'; +import type Redis from 'ioredis'; +import IORedis from 'ioredis'; + +interface BaseOptions { + namespace: string; + expire?: number; +} + +interface BuildOptions extends BaseOptions, RedisOptions {} + +interface ClientOptions extends BaseOptions { + client: Redis; +} + +type Options = BuildOptions | ClientOptions; + +export class RedisCacheAdapter implements CacheAdapter { + static readonly DEFAULTS = { + namespace: 'biscuitland', + }; + + readonly options: Options; + + readonly client: Redis; + + constructor(options?: Options) { + this.options = Object.assign(RedisCacheAdapter.DEFAULTS, options); + + if ((this.options as ClientOptions).client) { + this.client = (this.options as ClientOptions).client; + } else { + const { ...redisOpt } = this.options as BuildOptions; + this.client = new IORedis(redisOpt); + } + } + + /** + * @inheritDoc + */ + + async get(id: string): Promise { + const data = await this.client.get(this.build(id)); + + if (!data) { + return null; + } + + return JSON.parse(data); + } + + /** + * @inheritDoc + */ + + async set(id: string, data: unknown): Promise { + const expire = this.options.expire; + + if (expire) { + await this.client.set( + this.build(id), + JSON.stringify(data), + 'EX', + expire + ); + } else { + await this.client.set(this.build(id), JSON.stringify(data)); + } + } + + /** + * @inheritDoc + */ + + async items(to: string): Promise { + const array: unknown[] = []; + + let data = await this.getToRelationship(to); + data = data.map(id => this.build(`${to}.${id}`)); + + if (data && data.length > 0) { + const items = await this.client.mget(data); + + for (const item of items) { + if (item) { + array.push(JSON.parse(item)); + } + } + } + + return array; + } + + /** + * @inheritDoc + */ + + async count(to: string): Promise { + return new Promise((resolve, reject) => { + this.client.scard(this.build(to), (err, result) => { + if (err) { + return reject(err); + } + + return resolve(result || 0); + }); + }); + } + + /** + * @inheritDoc + */ + + async remove(id: string): Promise { + await this.client.del(this.build(id)); + } + + /** + * @inheritDoc + */ + + async contains(to: string, id: string): Promise { + return new Promise((resolve, reject) => { + this.client.sismember(this.build(to), id, (err, result) => { + if (err) { + return reject(err); + } + + return resolve(result === 1); + }); + }); + } + + /** + * @inheritDoc + */ + + async getToRelationship(to: string): Promise { + return new Promise((resolve, reject) => { + this.client.smembers(this.build(to), (err, result) => { + if (err) { + reject(err); + } + + resolve(result || []); + }); + }); + } + + /** + * @inheritDoc + */ + + async addToRelationship(to: string, id: string): Promise { + return new Promise((resolve, reject) => { + this.client.sadd(this.build(to), id, err => { + if (err) { + reject(err); + } + + resolve(); + }); + }); + } + + /** + * @inheritDoc + */ + + async removeToRelationship(to: string, id: string): Promise { + return new Promise((resolve, reject) => { + this.client.srem(this.build(to), id, err => { + if (err) { + reject(err); + } + + resolve(); + }); + }); + } + + /** + * @inheritDoc + */ + + protected build(id: string): string { + return `${this.options.namespace}:${id}`; + } +} diff --git a/packages/cache/src/scheme/transporters/base-transporter.ts b/packages/cache/src/scheme/transporters/base-transporter.ts new file mode 100644 index 0000000..674185f --- /dev/null +++ b/packages/cache/src/scheme/transporters/base-transporter.ts @@ -0,0 +1,7 @@ +/** + * future update + */ + +export interface BaseTransporter { + // +} diff --git a/packages/cache/src/scheme/transporters/redis-transporter.ts b/packages/cache/src/scheme/transporters/redis-transporter.ts new file mode 100644 index 0000000..1b41bcf --- /dev/null +++ b/packages/cache/src/scheme/transporters/redis-transporter.ts @@ -0,0 +1,3 @@ +import type { BaseTransporter } from './base-transporter'; + +export class RedisTransporter implements BaseTransporter {} diff --git a/packages/cache/src/scheme/transporters/tcp-transporter.ts b/packages/cache/src/scheme/transporters/tcp-transporter.ts new file mode 100644 index 0000000..f8851c3 --- /dev/null +++ b/packages/cache/src/scheme/transporters/tcp-transporter.ts @@ -0,0 +1,3 @@ +import type { BaseTransporter } from './base-transporter'; + +export class TcpTransporter implements BaseTransporter {} diff --git a/packages/cache/src/types.ts b/packages/cache/src/types.ts new file mode 100644 index 0000000..179491b --- /dev/null +++ b/packages/cache/src/types.ts @@ -0,0 +1,55 @@ +import type { Cache } from './cache'; +import type { CacheAdapter } from './scheme/adapters/cache-adapter'; + +import type { MemoryCacheAdapter } from './scheme/adapters/memory-cache-adapter'; + +// + +export type CacheOptions = Pick< + CO, + Exclude +> & + Partial; + +export interface CO { + /** + * Adapter to be used for storing resources + * @default MemoryCacheAdapter + */ + + adapter: CacheAdapter; +} + +// + +export type MemoryOptions = Pick< + MO, + Exclude +> & + Partial; + +export interface MO { + /** + * Time the resource will be stored + * @default 3600000 + */ + + expire: number; +} + +// + +export type RedisOptions = Pick< + RO, + Exclude +> & + Partial; + +export interface RO { + /** + * Time the resource will be stored + * @default 300 + */ + + expire: number; +} diff --git a/packages/cache/src/utils/options.ts b/packages/cache/src/utils/options.ts new file mode 100644 index 0000000..17ca5da --- /dev/null +++ b/packages/cache/src/utils/options.ts @@ -0,0 +1,48 @@ +/** + * Needs to be moved to a common location + * refactor + */ + +const isPlainObject = (value: any) => { + return ( + (value !== null && + typeof value === 'object' && + typeof value.constructor === 'function' && + // eslint-disable-next-line no-prototype-builtins + (value.constructor.prototype.hasOwnProperty('isPrototypeOf') || + Object.getPrototypeOf(value.constructor.prototype) === null)) || + (value && Object.getPrototypeOf(value) === null) + ); +}; + +const isObject = (o: any) => { + return !!o && typeof o === 'object' && !Array.isArray(o); +}; + +export const Options = (defaults: any, ...options: any[]): any => { + if (!options.length) { + return defaults; + } + + const source = options.shift(); + + if (isObject(defaults) && isPlainObject(source)) { + Object.entries(source).forEach(([key, value]) => { + if (typeof value === 'undefined') { + return; + } + + if (isPlainObject(value)) { + if (!(key in defaults)) { + Object.assign(defaults, { [key]: {} }); + } + + Options(defaults[key], value); + } else { + Object.assign(defaults, { [key]: value }); + } + }); + } + + return Options(defaults, ...options); +};