mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 06:26:41 +02:00
Merge branch 'dev-1' into dev
This commit is contained in:
commit
d41d060bb3
1770 changed files with 168028 additions and 31031 deletions
38
apps-archived/mail/apps/backend/src/db/connection.ts
Normal file
38
apps-archived/mail/apps/backend/src/db/connection.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import * as schema from './schema';
|
||||
|
||||
// Use require for postgres to avoid ESM/CommonJS interop issues
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const postgres = require('postgres');
|
||||
|
||||
let connection: ReturnType<typeof postgres> | null = null;
|
||||
let db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
export function getConnection(databaseUrl: string) {
|
||||
if (!connection) {
|
||||
connection = postgres(databaseUrl, {
|
||||
max: 10,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
});
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function getDb(databaseUrl: string) {
|
||||
if (!db) {
|
||||
const conn = getConnection(databaseUrl);
|
||||
db = drizzle(conn, { schema });
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function closeConnection() {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
connection = null;
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
|
||||
export type Database = ReturnType<typeof getDb>;
|
||||
30
apps-archived/mail/apps/backend/src/db/database.module.ts
Normal file
30
apps-archived/mail/apps/backend/src/db/database.module.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Module, Global } from '@nestjs/common';
|
||||
import type { OnModuleDestroy } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { getDb, closeConnection } from './connection';
|
||||
import type { Database } from './connection';
|
||||
|
||||
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useFactory: (configService: ConfigService): Database => {
|
||||
const databaseUrl = configService.get<string>('DATABASE_URL');
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL environment variable is not set');
|
||||
}
|
||||
return getDb(databaseUrl);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [DATABASE_CONNECTION],
|
||||
})
|
||||
export class DatabaseModule implements OnModuleDestroy {
|
||||
async onModuleDestroy() {
|
||||
await closeConnection();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { pgTable, uuid, timestamp, varchar, integer, boolean } from 'drizzle-orm/pg-core';
|
||||
import { emails } from './emails.schema';
|
||||
|
||||
export const attachments = pgTable('attachments', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
emailId: uuid('email_id')
|
||||
.references(() => emails.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
|
||||
filename: varchar('filename', { length: 500 }).notNull(),
|
||||
mimeType: varchar('mime_type', { length: 255 }).notNull(),
|
||||
size: integer('size').notNull(),
|
||||
contentId: varchar('content_id', { length: 255 }), // For inline images
|
||||
|
||||
// Storage
|
||||
storageKey: varchar('storage_key', { length: 500 }),
|
||||
storageUrl: varchar('storage_url', { length: 1000 }),
|
||||
isDownloaded: boolean('is_downloaded').default(false),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type Attachment = typeof attachments.$inferSelect;
|
||||
export type NewAttachment = typeof attachments.$inferInsert;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { pgTable, uuid, timestamp, varchar, text, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { emailAccounts } from './email-accounts.schema';
|
||||
import { emails, type EmailAddress } from './emails.schema';
|
||||
|
||||
export const drafts = pgTable('drafts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
accountId: uuid('account_id')
|
||||
.references(() => emailAccounts.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
|
||||
// Reply context
|
||||
replyToEmailId: uuid('reply_to_email_id').references(() => emails.id, { onDelete: 'set null' }),
|
||||
replyType: varchar('reply_type', { length: 20 }), // reply, reply-all, forward
|
||||
|
||||
// Content
|
||||
subject: text('subject'),
|
||||
toAddresses: jsonb('to_addresses').$type<EmailAddress[]>(),
|
||||
ccAddresses: jsonb('cc_addresses').$type<EmailAddress[]>(),
|
||||
bccAddresses: jsonb('bcc_addresses').$type<EmailAddress[]>(),
|
||||
bodyHtml: text('body_html'),
|
||||
bodyPlain: text('body_plain'),
|
||||
attachmentIds: jsonb('attachment_ids').$type<string[]>(),
|
||||
|
||||
// Scheduling
|
||||
scheduledAt: timestamp('scheduled_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type Draft = typeof drafts.$inferSelect;
|
||||
export type NewDraft = typeof drafts.$inferInsert;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
export interface SyncState {
|
||||
// IMAP sync state
|
||||
uidValidity?: number;
|
||||
lastUid?: number;
|
||||
// Gmail sync state
|
||||
historyId?: string;
|
||||
// Outlook sync state
|
||||
deltaLink?: string;
|
||||
}
|
||||
|
||||
export const emailAccounts = pgTable('email_accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
|
||||
// Account info
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }).notNull(),
|
||||
provider: varchar('provider', { length: 50 }).notNull(), // gmail, outlook, imap
|
||||
isDefault: boolean('is_default').default(false),
|
||||
|
||||
// IMAP/SMTP credentials (encrypted)
|
||||
imapHost: varchar('imap_host', { length: 255 }),
|
||||
imapPort: integer('imap_port'),
|
||||
imapSecurity: varchar('imap_security', { length: 20 }), // ssl, tls, none
|
||||
smtpHost: varchar('smtp_host', { length: 255 }),
|
||||
smtpPort: integer('smtp_port'),
|
||||
smtpSecurity: varchar('smtp_security', { length: 20 }),
|
||||
encryptedPassword: text('encrypted_password'),
|
||||
|
||||
// OAuth tokens (Gmail/Outlook)
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
tokenExpiresAt: timestamp('token_expires_at', { withTimezone: true }),
|
||||
tokenScopes: jsonb('token_scopes').$type<string[]>(),
|
||||
|
||||
// Sync settings
|
||||
syncEnabled: boolean('sync_enabled').default(true),
|
||||
syncInterval: integer('sync_interval').default(5), // minutes
|
||||
lastSyncAt: timestamp('last_sync_at', { withTimezone: true }),
|
||||
lastSyncError: text('last_sync_error'),
|
||||
syncState: jsonb('sync_state').$type<SyncState>(),
|
||||
|
||||
// Display settings
|
||||
color: varchar('color', { length: 7 }).default('#3B82F6'),
|
||||
signature: text('signature'),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type EmailAccount = typeof emailAccounts.$inferSelect;
|
||||
export type NewEmailAccount = typeof emailAccounts.$inferInsert;
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
index,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { emailAccounts } from './email-accounts.schema';
|
||||
import { folders } from './folders.schema';
|
||||
|
||||
export interface EmailAddress {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const emails = pgTable(
|
||||
'emails',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
accountId: uuid('account_id')
|
||||
.references(() => emailAccounts.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
folderId: uuid('folder_id').references(() => folders.id, { onDelete: 'set null' }),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
threadId: uuid('thread_id'), // For conversation threading
|
||||
|
||||
// Message identifiers
|
||||
messageId: varchar('message_id', { length: 500 }).notNull(), // RFC 2822 Message-ID
|
||||
externalId: varchar('external_id', { length: 255 }), // Provider-specific ID
|
||||
|
||||
// Headers
|
||||
subject: text('subject'),
|
||||
fromAddress: varchar('from_address', { length: 255 }),
|
||||
fromName: varchar('from_name', { length: 255 }),
|
||||
toAddresses: jsonb('to_addresses').$type<EmailAddress[]>(),
|
||||
ccAddresses: jsonb('cc_addresses').$type<EmailAddress[]>(),
|
||||
bccAddresses: jsonb('bcc_addresses').$type<EmailAddress[]>(),
|
||||
replyTo: varchar('reply_to', { length: 255 }),
|
||||
inReplyTo: varchar('in_reply_to', { length: 500 }), // Parent message ID
|
||||
references: jsonb('references').$type<string[]>(), // Thread references
|
||||
|
||||
// Content
|
||||
snippet: text('snippet'), // Preview text (first ~200 chars)
|
||||
bodyPlain: text('body_plain'),
|
||||
bodyHtml: text('body_html'),
|
||||
|
||||
// Dates
|
||||
sentAt: timestamp('sent_at', { withTimezone: true }),
|
||||
receivedAt: timestamp('received_at', { withTimezone: true }),
|
||||
|
||||
// Flags
|
||||
isRead: boolean('is_read').default(false),
|
||||
isStarred: boolean('is_starred').default(false),
|
||||
isDraft: boolean('is_draft').default(false),
|
||||
isDeleted: boolean('is_deleted').default(false),
|
||||
isSpam: boolean('is_spam').default(false),
|
||||
hasAttachments: boolean('has_attachments').default(false),
|
||||
|
||||
// AI-generated metadata
|
||||
aiSummary: text('ai_summary'),
|
||||
aiCategory: varchar('ai_category', { length: 50 }), // work, personal, newsletter, etc.
|
||||
aiPriority: varchar('ai_priority', { length: 20 }), // high, medium, low
|
||||
aiSentiment: varchar('ai_sentiment', { length: 20 }), // positive, neutral, negative
|
||||
aiSuggestedReplies: jsonb('ai_suggested_replies').$type<string[]>(),
|
||||
|
||||
// Size and metadata
|
||||
size: integer('size'), // bytes
|
||||
headers: jsonb('headers').$type<Record<string, string>>(),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('emails_account_id_idx').on(table.accountId),
|
||||
index('emails_folder_id_idx').on(table.folderId),
|
||||
index('emails_thread_id_idx').on(table.threadId),
|
||||
index('emails_message_id_idx').on(table.messageId),
|
||||
index('emails_received_at_idx').on(table.receivedAt),
|
||||
index('emails_user_id_idx').on(table.userId),
|
||||
]
|
||||
);
|
||||
|
||||
export type Email = typeof emails.$inferSelect;
|
||||
export type NewEmail = typeof emails.$inferInsert;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { pgTable, uuid, timestamp, varchar, integer, boolean } from 'drizzle-orm/pg-core';
|
||||
import { emailAccounts } from './email-accounts.schema';
|
||||
|
||||
export const folders = pgTable('folders', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
accountId: uuid('account_id')
|
||||
.references(() => emailAccounts.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
type: varchar('type', { length: 50 }).notNull(), // inbox, sent, drafts, trash, spam, archive, custom
|
||||
path: varchar('path', { length: 500 }).notNull(), // IMAP folder path
|
||||
color: varchar('color', { length: 7 }),
|
||||
icon: varchar('icon', { length: 50 }),
|
||||
|
||||
// Provider-specific ID
|
||||
externalId: varchar('external_id', { length: 255 }),
|
||||
|
||||
// Counts (cached)
|
||||
totalCount: integer('total_count').default(0),
|
||||
unreadCount: integer('unread_count').default(0),
|
||||
|
||||
// Flags
|
||||
isSystem: boolean('is_system').default(false),
|
||||
isHidden: boolean('is_hidden').default(false),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type Folder = typeof folders.$inferSelect;
|
||||
export type NewFolder = typeof folders.$inferInsert;
|
||||
6
apps-archived/mail/apps/backend/src/db/schema/index.ts
Normal file
6
apps-archived/mail/apps/backend/src/db/schema/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from './email-accounts.schema';
|
||||
export * from './folders.schema';
|
||||
export * from './emails.schema';
|
||||
export * from './attachments.schema';
|
||||
export * from './labels.schema';
|
||||
export * from './drafts.schema';
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { pgTable, uuid, timestamp, varchar, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { emailAccounts } from './email-accounts.schema';
|
||||
import { emails } from './emails.schema';
|
||||
|
||||
export const labels = pgTable('labels', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: varchar('user_id', { length: 255 }).notNull(),
|
||||
accountId: uuid('account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }),
|
||||
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
color: varchar('color', { length: 7 }).notNull(),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const emailLabels = pgTable(
|
||||
'email_labels',
|
||||
{
|
||||
emailId: uuid('email_id')
|
||||
.references(() => emails.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
labelId: uuid('label_id')
|
||||
.references(() => labels.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.emailId, table.labelId] })]
|
||||
);
|
||||
|
||||
export type Label = typeof labels.$inferSelect;
|
||||
export type NewLabel = typeof labels.$inferInsert;
|
||||
Loading…
Add table
Add a link
Reference in a new issue