feat: rename enum instead of string

This commit is contained in:
Mohamed Marrouchi 2025-01-16 10:14:50 +01:00
parent 3f9dd692bf
commit c27f37a6e6
18 changed files with 116 additions and 70 deletions

View File

@ -39,6 +39,7 @@ import { attachment, attachmentFile } from '../mocks/attachment.mock';
import { AttachmentRepository } from '../repositories/attachment.repository';
import { Attachment, AttachmentModel } from '../schemas/attachment.schema';
import { AttachmentService } from '../services/attachment.service';
import { AttachmentResourceRef } from '../types';
import { AttachmentController } from './attachment.controller';
@ -110,7 +111,7 @@ describe('AttachmentController', () => {
file: [],
},
{} as Request,
{ resourceRef: 'block_attachment' },
{ resourceRef: AttachmentResourceRef.BlockAttachment },
);
await expect(promiseResult).rejects.toThrow(
new BadRequestException('No file was selected'),
@ -128,7 +129,7 @@ describe('AttachmentController', () => {
{
session: { passport: { user: { id: '9'.repeat(24) } } },
} as unknown as Request,
{ resourceRef: 'block_attachment' },
{ resourceRef: AttachmentResourceRef.BlockAttachment },
);
const [name] = attachmentFile.filename.split('.');
expect(attachmentService.create).toHaveBeenCalledWith({
@ -136,7 +137,7 @@ describe('AttachmentController', () => {
type: attachmentFile.mimetype,
name: attachmentFile.originalname,
location: expect.stringMatching(new RegExp(`^/${name}`)),
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
access: 'public',
createdByRef: 'User',
createdBy: '9'.repeat(24),
@ -145,7 +146,7 @@ describe('AttachmentController', () => {
[
{
...attachment,
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
createdByRef: 'User',
createdBy: '9'.repeat(24),
},

View File

@ -18,6 +18,7 @@ import { Action } from '@/user/types/action.type';
import { attachment } from '../mocks/attachment.mock';
import { Attachment } from '../schemas/attachment.schema';
import { AttachmentService } from '../services/attachment.service';
import { AttachmentResourceRef } from '../types';
import { AttachmentGuard } from './attachment-ability.guard';
@ -55,7 +56,7 @@ describe('AttachmentGuard', () => {
describe('canActivate', () => {
it('should allow GET requests with valid ref', async () => {
const mockUser = { roles: ['admin-id'] } as any;
const mockRef = ['user_avatar'];
const mockRef = [AttachmentResourceRef.UserAvatar];
jest.spyOn(modelService, 'findOne').mockImplementation((criteria) => {
return typeof criteria === 'string' ||
@ -120,7 +121,7 @@ describe('AttachmentGuard', () => {
? Promise.reject('Invalid ID')
: Promise.resolve({
id: '9'.repeat(24),
resourceRef: `user_avatar`,
resourceRef: AttachmentResourceRef.UserAvatar,
} as Attachment);
});
@ -191,7 +192,7 @@ describe('AttachmentGuard', () => {
const mockExecutionContext = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
query: { resourceRef: 'block_attachment' },
query: { resourceRef: AttachmentResourceRef.BlockAttachment },
method: 'POST',
user: mockUser,
}),

View File

@ -26,7 +26,7 @@ import { Action } from '@/user/types/action.type';
import { TModel } from '@/user/types/model.type';
import { AttachmentService } from '../services/attachment.service';
import { TAttachmentResourceRef } from '../types';
import { AttachmentResourceRef, TAttachmentResourceRef } from '../types';
import {
isAttachmentResourceRef,
isAttachmentResourceRefArray,
@ -46,46 +46,46 @@ export class AttachmentGuard implements CanActivate {
> = {
// Read attachments by ref
[Action.READ]: {
setting_attachment: [
[AttachmentResourceRef.SettingAttachment]: [
['setting', Action.READ],
['attachment', Action.READ],
],
user_avatar: [['user', Action.READ]],
block_attachment: [
[AttachmentResourceRef.UserAvatar]: [['user', Action.READ]],
[AttachmentResourceRef.BlockAttachment]: [
['block', Action.READ],
['attachment', Action.READ],
],
content_attachment: [
[AttachmentResourceRef.ContentAttachment]: [
['content', Action.READ],
['attachment', Action.READ],
],
subscriber_avatar: [['subscriber', Action.READ]],
message_attachment: [
[AttachmentResourceRef.SubscriberAvatar]: [['subscriber', Action.READ]],
[AttachmentResourceRef.MessageAttachment]: [
['message', Action.READ],
['attachment', Action.READ],
],
},
// Create attachments by ref
[Action.CREATE]: {
setting_attachment: [
[AttachmentResourceRef.SettingAttachment]: [
['setting', Action.UPDATE],
['attachment', Action.CREATE],
],
user_avatar: [
[AttachmentResourceRef.UserAvatar]: [
// Not authorized, done via /user/:id/edit endpoint
],
block_attachment: [
[AttachmentResourceRef.BlockAttachment]: [
['block', Action.UPDATE],
['attachment', Action.CREATE],
],
content_attachment: [
[AttachmentResourceRef.ContentAttachment]: [
['content', Action.UPDATE],
['attachment', Action.CREATE],
],
subscriber_avatar: [
[AttachmentResourceRef.SubscriberAvatar]: [
// Not authorized, done programmatically by the channel
],
message_attachment: [
[AttachmentResourceRef.MessageAttachment]: [
// Unless we're in case of a handover, done programmatically by the channel
['message', Action.CREATE],
['attachment', Action.CREATE],
@ -93,36 +93,36 @@ export class AttachmentGuard implements CanActivate {
},
// Delete attachments by ref
[Action.DELETE]: {
setting_attachment: [
[AttachmentResourceRef.SettingAttachment]: [
['setting', Action.UPDATE],
['attachment', Action.DELETE],
],
user_avatar: [
[AttachmentResourceRef.UserAvatar]: [
// Not authorized
],
block_attachment: [
[AttachmentResourceRef.BlockAttachment]: [
['block', Action.UPDATE],
['attachment', Action.DELETE],
],
content_attachment: [
[AttachmentResourceRef.ContentAttachment]: [
['content', Action.UPDATE],
['attachment', Action.DELETE],
],
subscriber_avatar: [
[AttachmentResourceRef.SubscriberAvatar]: [
// Not authorized, done programmatically by the channel
],
message_attachment: [
[AttachmentResourceRef.MessageAttachment]: [
// Not authorized
],
},
// Update attachments is not possible
[Action.UPDATE]: {
setting_attachment: [],
user_avatar: [],
block_attachment: [],
content_attachment: [],
subscriber_avatar: [],
message_attachment: [],
[AttachmentResourceRef.SettingAttachment]: [],
[AttachmentResourceRef.UserAvatar]: [],
[AttachmentResourceRef.BlockAttachment]: [],
[AttachmentResourceRef.ContentAttachment]: [],
[AttachmentResourceRef.SubscriberAvatar]: [],
[AttachmentResourceRef.MessageAttachment]: [],
},
};
@ -163,7 +163,7 @@ export class AttachmentGuard implements CanActivate {
*
* @param action - The action on the attachment.
* @param user - The current user.
* @param resourceRef - The resource ref of the attachment (e.g., user_avatar, setting_attachment).
* @param resourceRef - The resource ref of the attachment (e.g., [AttachmentResourceRef.UserAvatar], [AttachmentResourceRef.SettingAttachment]).
* @returns A promise that resolves to `true` if the user has the required upload permission, otherwise `false`.
*/
private async isAuthorized(

View File

@ -9,6 +9,7 @@
import { Stream } from 'node:stream';
import { Attachment } from '../schemas/attachment.schema';
import { AttachmentResourceRef } from '../types';
export const attachment: Attachment = {
name: 'Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
@ -16,7 +17,7 @@ export const attachment: Attachment = {
size: 343370,
location:
'/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
access: 'public',
id: '65940d115178607da65c82b6',
createdAt: new Date(),
@ -47,7 +48,7 @@ export const attachments: Attachment[] = [
location:
'/app/src/attachment/uploads/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
channel: { ['some-channel']: {} },
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
access: 'public',
id: '65940d115178607da65c82b7',
createdAt: new Date(),
@ -62,7 +63,7 @@ export const attachments: Attachment[] = [
location:
'/app/src/attachment/uploads/Screenshot from 2022-03-18 08-58-15-af61e7f71281f9fd3f1ad7ad10107741c.png',
channel: { ['some-channel']: {} },
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
access: 'public',
id: '65940d115178607da65c82b8',
createdAt: new Date(),

View File

@ -30,7 +30,7 @@ import { BaseService } from '@/utils/generics/base-service';
import { AttachmentMetadataDto } from '../dto/attachment.dto';
import { AttachmentRepository } from '../repositories/attachment.repository';
import { Attachment } from '../schemas/attachment.schema';
import { TAttachmentResourceRef } from '../types';
import { AttachmentResourceRef, TAttachmentResourceRef } from '../types';
import {
fileExists,
generateUniqueFilename,
@ -164,7 +164,8 @@ export class AttachmentService extends BaseService<Attachment> {
* @returns The root directory path
*/
getRootDirByResourceRef(ref: TAttachmentResourceRef) {
return ref === 'subscriber_avatar' || ref === 'user_avatar'
return ref === AttachmentResourceRef.SubscriberAvatar ||
ref === AttachmentResourceRef.UserAvatar
? config.parameters.avatarDir
: config.parameters.uploadDir;
}

View File

@ -24,12 +24,12 @@ export type TAttachmentCreatedByRef = `${AttachmentCreatedByRef}`;
* These resource references influence how the attachment is uploaded, stored, and accessed:
*/
export enum AttachmentResourceRef {
SettingAttachment = 'setting_attachment', // Attachments related to app settings, restricted to users with specific permissions.
UserAvatar = 'user_avatar', // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
SubscriberAvatar = 'subscriber_avatar', // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
BlockAttachment = 'block_attachment', // Files sent by the bot, public or private based on the channel and user authentication.
ContentAttachment = 'content_attachment', // Files in the knowledge base, usually public but could vary based on specific needs.
MessageAttachment = 'message_attachment', // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
SettingAttachment = 'Setting', // Attachments related to app settings, restricted to users with specific permissions.
UserAvatar = 'User', // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
SubscriberAvatar = 'Subscriber', // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
BlockAttachment = 'Block', // Files sent by the bot, public or private based on the channel and user authentication.
ContentAttachment = 'Content', // Files in the knowledge base, usually public but could vary based on specific needs.
MessageAttachment = 'Message', // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
}
export type TAttachmentResourceRef = `${AttachmentResourceRef}`;

View File

@ -22,7 +22,7 @@ import { v4 as uuidv4 } from 'uuid';
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { AttachmentFile } from '@/attachment/types';
import { AttachmentFile, AttachmentResourceRef } from '@/attachment/types';
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
import { AttachmentRef } from '@/chat/schemas/types/attachment';
import {
@ -258,7 +258,7 @@ export default abstract class ChannelHandler<
name: `${name ? `${name}-` : ''}${uuidv4()}.${mime.extension(type)}`,
type,
size,
resourceRef: 'message_attachment',
resourceRef: AttachmentResourceRef.MessageAttachment,
access: 'private',
createdByRef: 'Subscriber',
createdBy: subscriber.id,

View File

@ -7,6 +7,7 @@
*/
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentResourceRef } from '@/attachment/types';
import { ButtonType } from '@/chat/schemas/types/button';
import {
FileType,
@ -88,7 +89,7 @@ const attachment: Attachment = {
id: 'any-channel-attachment-id',
},
},
resourceRef: 'block_attachment',
resourceRef: AttachmentResourceRef.BlockAttachment,
access: 'public',
createdByRef: 'User',
createdBy: null,

View File

@ -12,6 +12,7 @@ import mime from 'mime';
import { v4 as uuidv4 } from 'uuid';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { AttachmentResourceRef } from '@/attachment/types';
import EventWrapper from '@/channel/lib/EventWrapper';
import { config } from '@/config';
import { HelperService } from '@/helper/helper.service';
@ -280,7 +281,7 @@ export class ChatService {
name: `avatar-${uuidv4()}.${extension}`,
size,
type,
resourceRef: 'subscriber_avatar',
resourceRef: AttachmentResourceRef.SubscriberAvatar,
access: 'private',
createdByRef: 'Subscriber',
createdBy: subscriber.id,

View File

@ -15,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid';
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { AttachmentResourceRef } from '@/attachment/types';
import { ChannelService } from '@/channel/channel.service';
import ChannelHandler from '@/channel/lib/Handler';
import { ChannelName } from '@/channel/types';
@ -625,7 +626,7 @@ export default abstract class BaseWebChannelHandler<
name: data.name,
size: Buffer.byteLength(data.file),
type: data.type,
resourceRef: 'message_attachment',
resourceRef: AttachmentResourceRef.MessageAttachment,
access: 'private',
createdByRef: 'Subscriber',
createdBy: req.session?.web?.profile?.id,
@ -692,7 +693,7 @@ export default abstract class BaseWebChannelHandler<
name: file.originalname,
size: file.size,
type: file.mimetype,
resourceRef: 'message_attachment',
resourceRef: AttachmentResourceRef.MessageAttachment,
access: 'private',
createdByRef: 'Subscriber',
createdBy: req.session.web.profile?.id,

View File

@ -362,6 +362,8 @@ const undoPopulateAttachments = async ({ logger }: MigrationServices) => {
AttachmentResourceRef.SettingAttachment,
AttachmentResourceRef.UserAvatar,
AttachmentResourceRef.SubscriberAvatar,
AttachmentResourceRef.ContentAttachment,
AttachmentResourceRef.MessageAttachment,
],
},
},
@ -376,11 +378,11 @@ const undoPopulateAttachments = async ({ logger }: MigrationServices) => {
);
logger.log(
`Successfully reverted attributes for ${result.modifiedCount} attachments with ref 'setting_attachment'`,
`Successfully reverted attributes for ${result.modifiedCount} attachments with ref AttachmentResourceRef.SettingAttachment`,
);
} catch (error) {
logger.error(
`Failed to revert attributes for attachments with ref 'setting_attachment': ${error.message}`,
`Failed to revert attributes for attachments with ref AttachmentResourceRef.SettingAttachment: ${error.message}`,
);
}
};
@ -552,7 +554,7 @@ const buildRenameAttributeCallback =
};
/**
* Traverses an content document to search for any attachment object
* Traverses a content document to search for any attachment object
* @param obj
* @param callback
* @returns
@ -584,7 +586,14 @@ const migrateAttachmentContents = async (
const updateField = action === MigrationAction.UP ? 'id' : 'attachment_id';
const unsetField = action === MigrationAction.UP ? 'attachment_id' : 'id';
const ContentModel = mongoose.model<Content>(Content.name, contentSchema);
// Find blocks where "message.attachment" exists
const AttachmentModel = mongoose.model<Attachment>(
Attachment.name,
attachmentSchema,
);
const adminUser = await getAdminUser();
// Process all contents
const cursor = ContentModel.find({}).cursor();
for await (const content of cursor) {
@ -594,6 +603,30 @@ const migrateAttachmentContents = async (
buildRenameAttributeCallback(unsetField, updateField),
);
for (const key in content.dynamicFields) {
if (
content.dynamicFields[key] &&
typeof content.dynamicFields[key] === 'object' &&
'payload' in content.dynamicFields[key] &&
'id' in content.dynamicFields[key].payload &&
content.dynamicFields[key].payload.id
) {
await AttachmentModel.updateOne(
{
_id: content.dynamicFields[key].payload.id,
},
{
$set: {
resourceRef: AttachmentResourceRef.ContentAttachment,
createdBy: adminUser.id,
createdByRef: 'User',
access: 'public',
},
},
);
}
}
await ContentModel.replaceOne({ _id: content._id }, content);
} catch (error) {
logger.error(`Failed to update content ${content._id}: ${error.message}`);
@ -648,7 +681,7 @@ const migrateAndPopulateAttachmentMessages = async ({
await attachmentService.updateOne(
msg.message.attachment.payload.attachment_id as string,
{
resourceRef: 'message_attachment',
resourceRef: AttachmentResourceRef.MessageAttachment,
access: 'private',
createdByRef: msg.sender ? 'Subscriber' : 'User',
createdBy: msg.sender ? msg.sender : adminUser.id,
@ -681,7 +714,7 @@ const migrateAndPopulateAttachmentMessages = async ({
size: fileBuffer.length,
type: response.headers['content-type'],
channel: {},
resourceRef: 'message_attachment',
resourceRef: AttachmentResourceRef.MessageAttachment,
access: msg.sender ? 'private' : 'public',
createdBy: msg.sender ? msg.sender : adminUser.id,
createdByRef: msg.sender ? 'Subscriber' : 'User',

View File

@ -31,6 +31,7 @@ import { Session as ExpressSession } from 'express-session';
import { diskStorage, memoryStorage } from 'multer';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { AttachmentResourceRef } from '@/attachment/types';
import { config } from '@/config';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
@ -294,7 +295,7 @@ export class ReadWriteUserController extends ReadOnlyUserController {
name: avatarFile.originalname,
size: avatarFile.size,
type: avatarFile.mimetype,
resourceRef: 'user_avatar',
resourceRef: AttachmentResourceRef.UserAvatar,
access: 'private',
createdByRef: 'User',
createdBy: req.user.id,

View File

@ -10,6 +10,7 @@ import mongoose from 'mongoose';
import { AttachmentCreateDto } from '@/attachment/dto/attachment.dto';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentResourceRef } from '@/attachment/types';
export const attachmentFixtures: AttachmentCreateDto[] = [
{
@ -22,7 +23,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
id: '1',
},
},
resourceRef: 'content_attachment',
resourceRef: AttachmentResourceRef.ContentAttachment,
access: 'public',
createdByRef: 'User',
createdBy: '9'.repeat(24),
@ -37,7 +38,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
id: '2',
},
},
resourceRef: 'content_attachment',
resourceRef: AttachmentResourceRef.ContentAttachment,
access: 'public',
createdByRef: 'User',
createdBy: '9'.repeat(24),

View File

@ -38,6 +38,7 @@ import { DialogControlProps } from "@/hooks/useDialog";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType } from "@/services/types";
import { AttachmentResourceRef } from "@/types/attachment.types";
import {
ContentField,
ContentFieldType,
@ -116,7 +117,7 @@ const ContentFieldInput: React.FC<ContentFieldInput> = ({
value={field.value?.payload?.id}
accept={MIME_TYPES["images"].join(",")}
format="full"
resourceRef="content_attachment"
resourceRef={AttachmentResourceRef.ContentAttachment}
/>
);
default:

View File

@ -20,6 +20,7 @@ import { useApiClient } from "@/hooks/useApiClient";
import { DialogControlProps } from "@/hooks/useDialog";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { AttachmentResourceRef } from "@/types/attachment.types";
import { IContentType } from "@/types/content-type.types";
export type ContentImportDialogProps = DialogControlProps<{
@ -81,7 +82,7 @@ export const ContentImportDialog: FC<ContentImportDialogProps> = ({
}}
label=""
value={attachmentId}
resourceRef="content_attachment"
resourceRef={AttachmentResourceRef.ContentAttachment}
/>
</ContentItem>
</ContentContainer>

View File

@ -20,6 +20,7 @@ import MultipleInput from "@/app-components/inputs/MultipleInput";
import { PasswordInput } from "@/app-components/inputs/PasswordInput";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format } from "@/services/types";
import { AttachmentResourceRef } from "@/types/attachment.types";
import { IBlock } from "@/types/block.types";
import { IHelper } from "@/types/helper.types";
import { ISetting, SettingType } from "@/types/setting.types";
@ -186,7 +187,7 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
accept={MIME_TYPES["images"].join(",")}
format="full"
size={128}
resourceRef="setting_attachment"
resourceRef={AttachmentResourceRef.SettingAttachment}
/>
);
@ -199,7 +200,7 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
accept={MIME_TYPES["images"].join(",")}
format="full"
size={128}
resourceRef="setting_attachment"
resourceRef={AttachmentResourceRef.SettingAttachment}
/>
);
default:

View File

@ -13,6 +13,7 @@ import AttachmentInput from "@/app-components/attachment/AttachmentInput";
import { ContentItem } from "@/app-components/dialogs";
import AttachmentIcon from "@/app-components/svg/toolbar/AttachmentIcon";
import { useTranslate } from "@/hooks/useTranslate";
import { AttachmentResourceRef } from "@/types/attachment.types";
import { IBlockAttributes } from "@/types/block.types";
import { FileType } from "@/types/message.types";
import { MIME_TYPES, getFileType } from "@/utils/attachment";
@ -69,7 +70,7 @@ const AttachmentMessageForm = () => {
},
});
}}
resourceRef="block_attachment"
resourceRef={AttachmentResourceRef.BlockAttachment}
/>
);
}}

View File

@ -29,12 +29,12 @@ export type TAttachmentCreatedByRef = `${AttachmentCreatedByRef}`;
* These references influence how the attachment is uploaded, stored, and accessed:
*/
export enum AttachmentResourceRef {
SettingAttachment = "setting_attachment", // Attachments related to app settings, restricted to users with specific permissions.
UserAvatar = "user_avatar", // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
SubscriberAvatar = "subscriber_avatar", // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
BlockAttachment = "block_attachment", // Files sent by the bot, public or private based on the channel and user authentication.
ContentAttachment = "content_attachment", // Files in the knowledge base, usually public but could vary based on specific needs.
MessageAttachment = "message_attachment", // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
SettingAttachment = "Setting", // Attachments related to app settings, restricted to users with specific permissions.
UserAvatar = "User", // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
SubscriberAvatar = "Subscriber", // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
BlockAttachment = "Block", // Files sent by the bot, public or private based on the channel and user authentication.
ContentAttachment = "Content", // Files in the knowledge base, usually public but could vary based on specific needs.
MessageAttachment = "Message", // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
}
export type TAttachmentResourceRef = `${AttachmentResourceRef}`;