Restructure and refactor services in desktop

This commit is contained in:
Hakan Shehu
2025-01-17 14:35:13 +01:00
parent ddbf5e55e3
commit 74f74ea37a
347 changed files with 5064 additions and 5822 deletions

View File

@@ -5,21 +5,20 @@ import { createDebugger } from '@colanode/core';
import { app, BrowserWindow, ipcMain, protocol, shell } from 'electron'; import { app, BrowserWindow, ipcMain, protocol, shell } from 'electron';
import path from 'path'; import path from 'path';
import { metadataService } from '@/main/services/metadata-service';
import { notificationService } from '@/main/services/notification-service';
import { WindowSize } from '@/shared/types/metadata'; import { WindowSize } from '@/shared/types/metadata';
import { scheduler } from '@/main/scheduler'; import { mediator } from '@/main/mediator';
import { assetService } from '@/main/services/asset-service';
import { avatarService } from '@/main/services/avatar-service';
import { commandService } from '@/main/services/command-service';
import { fileService } from '@/main/services/file-service';
import { mutationService } from '@/main/services/mutation-service';
import { queryService } from '@/main/services/query-service';
import { getAppIconPath } from '@/main/utils'; import { getAppIconPath } from '@/main/utils';
import { CommandInput, CommandMap } from '@/shared/commands'; import { CommandInput, CommandMap } from '@/shared/commands';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { MutationInput, MutationMap } from '@/shared/mutations'; import { MutationInput, MutationMap } from '@/shared/mutations';
import { QueryInput, QueryMap } from '@/shared/queries'; import { QueryInput, QueryMap } from '@/shared/queries';
import { appService } from '@/main/services/app-service';
import {
handleAssetRequest,
handleAvatarRequest,
handleFilePreviewRequest,
handleFileRequest,
} from '@/main/lib/protocols';
const debug = createDebugger('desktop:main'); const debug = createDebugger('desktop:main');
@@ -42,11 +41,10 @@ updateElectronApp({
}); });
const createWindow = async () => { const createWindow = async () => {
await scheduler.init(); await appService.migrate();
notificationService.checkBadge();
// Create the browser window. // Create the browser window.
let windowSize = await metadataService.get<WindowSize>('window_size'); let windowSize = await appService.metadata.get<WindowSize>('window_size');
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: windowSize?.width ?? 1200, width: windowSize?.width ?? 1200,
height: windowSize?.height ?? 800, height: windowSize?.height ?? 800,
@@ -70,7 +68,7 @@ const createWindow = async () => {
fullscreen: false, fullscreen: false,
}; };
metadataService.set('window_size', windowSize); appService.metadata.set('window_size', windowSize);
}); });
mainWindow.on('enter-full-screen', () => { mainWindow.on('enter-full-screen', () => {
@@ -80,7 +78,7 @@ const createWindow = async () => {
fullscreen: true, fullscreen: true,
}; };
metadataService.set('window_size', windowSize); appService.metadata.set('window_size', windowSize);
}); });
mainWindow.on('leave-full-screen', () => { mainWindow.on('leave-full-screen', () => {
@@ -90,7 +88,7 @@ const createWindow = async () => {
fullscreen: false, fullscreen: false,
}; };
metadataService.set('window_size', windowSize); appService.metadata.set('window_size', windowSize);
}); });
// and load the index.html of the app. // and load the index.html of the app.
@@ -116,25 +114,25 @@ const createWindow = async () => {
if (!protocol.isProtocolHandled('avatar')) { if (!protocol.isProtocolHandled('avatar')) {
protocol.handle('avatar', (request) => { protocol.handle('avatar', (request) => {
return avatarService.handleAvatarRequest(request); return handleAvatarRequest(request);
}); });
} }
if (!protocol.isProtocolHandled('local-file')) { if (!protocol.isProtocolHandled('local-file')) {
protocol.handle('local-file', (request) => { protocol.handle('local-file', (request) => {
return fileService.handleFileRequest(request); return handleFileRequest(request);
}); });
} }
if (!protocol.isProtocolHandled('local-file-preview')) { if (!protocol.isProtocolHandled('local-file-preview')) {
protocol.handle('local-file-preview', (request) => { protocol.handle('local-file-preview', (request) => {
return fileService.handleFilePreviewRequest(request); return handleFilePreviewRequest(request);
}); });
} }
if (!protocol.isProtocolHandled('asset')) { if (!protocol.isProtocolHandled('asset')) {
protocol.handle('asset', (request) => { protocol.handle('asset', (request) => {
return assetService.handleAssetRequest(request); return handleAssetRequest(request);
}); });
} }
@@ -159,7 +157,7 @@ app.on('window-all-closed', () => {
app.quit(); app.quit();
} }
queryService.clearSubscriptions(); mediator.clearSubscriptions();
}); });
app.on('activate', () => { app.on('activate', () => {
@@ -173,7 +171,7 @@ app.on('activate', () => {
// In this file you can include the rest of your app's specific main process // In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here. // code. You can also put them in separate files and import them here.
ipcMain.handle('init', async () => { ipcMain.handle('init', async () => {
await scheduler.init(); await appService.init();
}); });
ipcMain.handle( ipcMain.handle(
@@ -182,7 +180,7 @@ ipcMain.handle(
_: unknown, _: unknown,
input: T input: T
): Promise<MutationMap[T['type']]['output']> => { ): Promise<MutationMap[T['type']]['output']> => {
return mutationService.executeMutation(input); return mediator.executeMutation(input);
} }
); );
@@ -192,7 +190,7 @@ ipcMain.handle(
_: unknown, _: unknown,
input: T input: T
): Promise<QueryMap[T['type']]['output']> => { ): Promise<QueryMap[T['type']]['output']> => {
return queryService.executeQuery(input); return mediator.executeQuery(input);
} }
); );
@@ -203,12 +201,12 @@ ipcMain.handle(
id: string, id: string,
input: T input: T
): Promise<QueryMap[T['type']]['output']> => { ): Promise<QueryMap[T['type']]['output']> => {
return queryService.executeQueryAndSubscribe(id, input); return mediator.executeQueryAndSubscribe(id, input);
} }
); );
ipcMain.handle('unsubscribe-query', (_: unknown, id: string): void => { ipcMain.handle('unsubscribe-query', (_: unknown, id: string): void => {
queryService.unsubscribeQuery(id); mediator.unsubscribeQuery(id);
}); });
ipcMain.handle( ipcMain.handle(
@@ -217,6 +215,6 @@ ipcMain.handle(
_: unknown, _: unknown,
input: T input: T
): Promise<CommandMap[T['type']]['output']> => { ): Promise<CommandMap[T['type']]['output']> => {
return commandService.executeCommand(input); return mediator.executeCommand(input);
} }
); );

View File

@@ -1,4 +1,7 @@
import { fileService } from '@/main/services/file-service'; import path from 'path';
import { shell } from 'electron';
import { getWorkspaceFilesDirectoryPath } from '@/main/utils';
import { CommandHandler } from '@/main/types'; import { CommandHandler } from '@/main/types';
import { FileOpenCommandInput } from '@/shared/commands/file-open'; import { FileOpenCommandInput } from '@/shared/commands/file-open';
@@ -6,6 +9,16 @@ export class FileOpenCommandHandler
implements CommandHandler<FileOpenCommandInput> implements CommandHandler<FileOpenCommandInput>
{ {
public async handleCommand(input: FileOpenCommandInput): Promise<void> { public async handleCommand(input: FileOpenCommandInput): Promise<void> {
fileService.openFile(input.userId, input.fileId, input.extension); const workspaceFilesDir = getWorkspaceFilesDirectoryPath(
input.accountId,
input.workspaceId
);
const filePath = path.join(
workspaceFilesDir,
`${input.fileId}${input.extension}`
);
shell.openPath(filePath);
} }
} }

View File

@@ -1,117 +0,0 @@
import { Migration } from 'kysely';
const createServersTable: Migration = {
up: async (db) => {
await db.schema
.createTable('servers')
.addColumn('domain', 'text', (col) => col.notNull().primaryKey())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('avatar', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('version', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('last_synced_at', 'text')
.execute();
await db
.insertInto('servers')
.values([
{
domain: 'eu.colanode.com',
name: 'Colanode Cloud (EU)',
avatar: 'https://colanode.com/assets/flags/eu.svg',
attributes: '{}',
version: '0.1.0',
created_at: new Date().toISOString(),
},
{
domain: 'us.colanode.com',
name: 'Colanode Cloud (US)',
avatar: 'https://colanode.com/assets/flags/us.svg',
attributes: '{}',
version: '0.1.0',
created_at: new Date().toISOString(),
},
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('servers').execute();
},
};
const createAccountsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('accounts')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('device_id', 'text', (col) => col.notNull())
.addColumn('server', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', (col) => col.notNull())
.addColumn('avatar', 'text')
.addColumn('token', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('accounts').execute();
},
};
const createWorkspacesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('workspaces')
.addColumn('user_id', 'text', (col) => col.notNull().primaryKey())
.addColumn('workspace_id', 'text', (col) => col.notNull())
.addColumn('account_id', 'text', (col) =>
col.notNull().references('accounts.id').onDelete('cascade')
)
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('description', 'text')
.addColumn('avatar', 'text')
.addColumn('role', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('workspaces').execute();
},
};
const createDeletedTokensTable: Migration = {
up: async (db) => {
await db.schema
.createTable('deleted_tokens')
.addColumn('account_id', 'text', (col) => col.notNull())
.addColumn('token', 'text', (col) => col.notNull().primaryKey())
.addColumn('server', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('deleted_tokens').execute();
},
};
const createMetadataTable: Migration = {
up: async (db) => {
await db.schema
.createTable('metadata')
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
.addColumn('value', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('metadata').execute();
},
};
export const appDatabaseMigrations: Record<string, Migration> = {
'00001_create_servers_table': createServersTable,
'00002_create_accounts_table': createAccountsTable,
'00003_create_workspaces_table': createWorkspacesTable,
'00004_create_deleted_tokens_table': createDeletedTokensTable,
'00005_create_metadata_table': createMetadataTable,
};

View File

@@ -1,186 +0,0 @@
import SQLite from 'better-sqlite3';
import { Kysely, Migration, Migrator, SqliteDialect } from 'kysely';
import { createDebugger } from '@colanode/core';
import fs from 'fs';
import { appDatabaseMigrations } from '@/main/data/app/migrations';
import { AppDatabaseSchema } from '@/main/data/app/schema';
import { workspaceDatabaseMigrations } from '@/main/data/workspace/migrations';
import { WorkspaceDatabaseSchema } from '@/main/data/workspace/schema';
import { appDatabasePath, getWorkspaceDirectoryPath } from '@/main/utils';
import { eventBus } from '@/shared/lib/event-bus';
class DatabaseService {
private initPromise: Promise<void> | null = null;
private readonly workspaceDatabases: Map<
string,
Kysely<WorkspaceDatabaseSchema>
> = new Map();
public readonly appDatabase: Kysely<AppDatabaseSchema>;
private readonly debug = createDebugger('desktop:service:database');
constructor() {
this.debug('Constructing database service');
const dialect = new SqliteDialect({
database: this.buildSqlite(appDatabasePath),
});
this.appDatabase = new Kysely<AppDatabaseSchema>({ dialect });
eventBus.subscribe((event) => {
if (event.type === 'workspace_created') {
this.initWorkspaceDatabase(event.workspace.userId);
}
});
}
public async init(): Promise<void> {
this.debug('Initializing database service');
if (!this.initPromise) {
this.initPromise = this.executeInit();
}
await this.initPromise;
}
public async getWorkspaceDatabase(
userId: string
): Promise<Kysely<WorkspaceDatabaseSchema>> {
await this.waitForInit();
if (this.workspaceDatabases.has(userId)) {
return this.workspaceDatabases.get(userId)!;
}
//try and check if it's in database but hasn't been loaded yet
const workspace = await this.appDatabase
.selectFrom('workspaces')
.selectAll()
.where('user_id', '=', userId)
.executeTakeFirst();
if (!workspace) {
throw new Error('Workspace database not found');
}
const workspaceDatabase = await this.initWorkspaceDatabase(userId);
this.workspaceDatabases.set(userId, workspaceDatabase);
return workspaceDatabase;
}
public async getWorkspaceDatabases(): Promise<
Map<string, Kysely<WorkspaceDatabaseSchema>>
> {
await this.waitForInit();
return this.workspaceDatabases;
}
public async removeWorkspaceDatabase(userId: string): Promise<void> {
this.debug(`Deleting workspace database for user: ${userId}`);
await this.waitForInit();
const workspaceDatabase = this.workspaceDatabases.get(userId);
if (workspaceDatabase) {
try {
workspaceDatabase.destroy();
} catch (error) {
this.debug(
`Failed to destroy workspace database for user: ${userId}`,
error
);
}
}
this.workspaceDatabases.delete(userId);
}
private async waitForInit(): Promise<void> {
if (!this.initPromise) {
this.initPromise = this.executeInit();
}
await this.initPromise;
}
private async executeInit(): Promise<void> {
await this.migrateAppDatabase();
const workspaces = await this.appDatabase
.selectFrom('workspaces')
.select('user_id')
.execute();
for (const workspace of workspaces) {
const workspaceDatabase = await this.initWorkspaceDatabase(
workspace.user_id
);
this.workspaceDatabases.set(workspace.user_id, workspaceDatabase);
}
}
private async initWorkspaceDatabase(
userId: string
): Promise<Kysely<WorkspaceDatabaseSchema>> {
this.debug(`Initializing workspace database for user: ${userId}`);
const workspaceDir = getWorkspaceDirectoryPath(userId);
if (!fs.existsSync(workspaceDir)) {
fs.mkdirSync(workspaceDir, {
recursive: true,
});
}
const dialect = new SqliteDialect({
database: this.buildSqlite(`${workspaceDir}/workspace.db`),
});
const workspaceDatabase = new Kysely<WorkspaceDatabaseSchema>({
dialect,
});
await this.migrateWorkspaceDatabase(workspaceDatabase);
return workspaceDatabase;
}
private async migrateAppDatabase(): Promise<void> {
this.debug('Migrating app database');
const migrator = new Migrator({
db: this.appDatabase,
provider: {
getMigrations(): Promise<Record<string, Migration>> {
return Promise.resolve(appDatabaseMigrations);
},
},
});
await migrator.migrateToLatest();
}
private async migrateWorkspaceDatabase(
database: Kysely<WorkspaceDatabaseSchema>
): Promise<void> {
this.debug('Migrating workspace database');
const migrator = new Migrator({
db: database,
provider: {
getMigrations(): Promise<Record<string, Migration>> {
return Promise.resolve(workspaceDatabaseMigrations);
},
},
});
await migrator.migrateToLatest();
}
private buildSqlite = (filename: string): SQLite.Database => {
this.debug(`Building sqlite database: ${filename}`);
const database = new SQLite(filename);
database.pragma('journal_mode = WAL');
return database;
};
}
export const databaseService = new DatabaseService();

View File

@@ -1,401 +0,0 @@
import { Migration, sql } from 'kysely';
const createUsersTable: Migration = {
up: async (db) => {
await db.schema
.createTable('users')
.addColumn('id', 'text', (col) => col.primaryKey().notNull())
.addColumn('email', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('avatar', 'text')
.addColumn('custom_name', 'text')
.addColumn('custom_avatar', 'text')
.addColumn('role', 'text', (col) => col.notNull())
.addColumn('storage_limit', 'integer', (col) => col.notNull())
.addColumn('max_file_size', 'integer', (col) => col.notNull())
.addColumn('status', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('version', 'integer')
.execute();
},
down: async (db) => {
await db.schema.dropTable('users').execute();
},
};
const createEntriesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entries')
.addColumn('id', 'text', (col) => col.primaryKey().notNull())
.addColumn('type', 'text', (col) =>
col
.notNull()
.generatedAlwaysAs(sql`json_extract(attributes, '$.type')`)
.stored()
)
.addColumn('parent_id', 'text', (col) =>
col
.generatedAlwaysAs(sql`json_extract(attributes, '$.parentId')`)
.stored()
)
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_by', 'text')
.addColumn('transaction_id', 'text', (col) => col.notNull())
.execute();
await db.schema
.createIndex('entries_parent_id_type_index')
.on('entries')
.columns(['parent_id', 'type'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('entries').execute();
},
};
const createEntryTransactionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_transactions')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('operation', 'integer', (col) => col.notNull())
.addColumn('data', 'blob')
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('server_created_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('entry_transactions').execute();
},
};
const createEntryInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_interactions')
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('first_seen_at', 'text')
.addColumn('last_seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('entry_interactions_pkey', [
'entry_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('entry_interactions').execute();
},
};
const createCollaborationsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('collaborations')
.addColumn('entry_id', 'text', (col) => col.notNull().primaryKey())
.addColumn('role', 'text')
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer')
.execute();
},
down: async (db) => {
await db.schema.dropTable('collaborations').execute();
},
};
const createMessagesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('messages')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'integer', (col) =>
col
.notNull()
.generatedAlwaysAs(sql`json_extract(attributes, '$.type')`)
.stored()
)
.addColumn('parent_id', 'text', (col) => col.notNull())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('updated_by', 'text')
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer')
.execute();
await db.schema
.createIndex('messages_parent_id_index')
.on('messages')
.columns(['parent_id'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('messages').execute();
},
};
const createMessageReactionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('message_reactions')
.addColumn('message_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('reaction', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('message_reactions_pkey', [
'message_id',
'collaborator_id',
'reaction',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('message_reactions').execute();
},
};
const createMessageInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('message_interactions')
.addColumn('message_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('first_seen_at', 'text')
.addColumn('last_seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('message_interactions_pkey', [
'message_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('message_interactions').execute();
},
};
const createFilesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('files')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'text', (col) => col.notNull())
.addColumn('parent_id', 'text', (col) => col.notNull())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('original_name', 'text', (col) => col.notNull())
.addColumn('mime_type', 'text', (col) => col.notNull())
.addColumn('extension', 'text', (col) => col.notNull())
.addColumn('size', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('updated_by', 'text')
.addColumn('deleted_at', 'text')
.addColumn('status', 'integer', (col) => col.notNull())
.addColumn('version', 'integer')
.execute();
await db.schema
.createIndex('files_parent_id_index')
.on('files')
.columns(['parent_id'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('files').execute();
},
};
const createFileStatesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('file_states')
.addColumn('file_id', 'text', (col) => col.notNull().primaryKey())
.addColumn('download_status', 'integer', (col) => col.notNull())
.addColumn('download_progress', 'integer', (col) => col.notNull())
.addColumn('download_retries', 'integer', (col) => col.notNull())
.addColumn('upload_status', 'integer', (col) => col.notNull())
.addColumn('upload_progress', 'integer', (col) => col.notNull())
.addColumn('upload_retries', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('file_states').execute();
},
};
const createFileInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('file_interactions')
.addColumn('file_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('file_interactions_pkey', [
'file_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('file_interactions').execute();
},
};
const createMutationsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('mutations')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'text', (col) => col.notNull())
.addColumn('data', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('retries', 'integer', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('mutations').execute();
},
};
const createEntryPathsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_paths')
.addColumn('ancestor_id', 'varchar(30)', (col) =>
col.notNull().references('entries.id').onDelete('cascade')
)
.addColumn('descendant_id', 'varchar(30)', (col) =>
col.notNull().references('entries.id').onDelete('cascade')
)
.addColumn('level', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('entry_paths_pkey', [
'ancestor_id',
'descendant_id',
])
.execute();
await sql`
CREATE TRIGGER trg_insert_entry_path
AFTER INSERT ON entries
FOR EACH ROW
BEGIN
-- Insert direct path from the new entry to itself
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
VALUES (NEW.id, NEW.id, 0);
-- Insert paths from ancestors to the new entry
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
SELECT ancestor_id, NEW.id, level + 1
FROM entry_paths
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
END;
`.execute(db);
await sql`
CREATE TRIGGER trg_update_entry_path
AFTER UPDATE ON entries
FOR EACH ROW
WHEN OLD.parent_id <> NEW.parent_id
BEGIN
-- Delete old paths involving the updated entry
DELETE FROM entry_paths
WHERE descendant_id = NEW.id AND ancestor_id <> NEW.id;
-- Insert new paths from ancestors to the updated entry
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
SELECT ancestor_id, NEW.id, level + 1
FROM entry_paths
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
END;
`.execute(db);
},
down: async (db) => {
await sql`
DROP TRIGGER IF EXISTS trg_insert_entry_path;
DROP TRIGGER IF EXISTS trg_update_entry_path;
`.execute(db);
await db.schema.dropTable('entry_paths').execute();
},
};
const createTextsTable: Migration = {
up: async (db) => {
await sql`
CREATE VIRTUAL TABLE texts USING fts5(id UNINDEXED, name, text);
`.execute(db);
},
down: async (db) => {
await sql`
DROP TABLE IF EXISTS texts;
`.execute(db);
},
};
const createCursorsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('cursors')
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
.addColumn('value', 'integer', (col) => col.notNull().defaultTo(0))
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('cursors').execute();
},
};
export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00001_create_users_table': createUsersTable,
'00002_create_entries_table': createEntriesTable,
'00003_create_entry_interactions_table': createEntryInteractionsTable,
'00004_create_entry_transactions_table': createEntryTransactionsTable,
'00005_create_collaborations_table': createCollaborationsTable,
'00006_create_messages_table': createMessagesTable,
'00007_create_message_reactions_table': createMessageReactionsTable,
'00008_create_message_interactions_table': createMessageInteractionsTable,
'00009_create_files_table': createFilesTable,
'00010_create_file_states_table': createFileStatesTable,
'00011_create_file_interactions_table': createFileInteractionsTable,
'00012_create_mutations_table': createMutationsTable,
'00013_create_entry_paths_table': createEntryPathsTable,
'00014_create_texts_table': createTextsTable,
'00015_create_cursors_table': createCursorsTable,
};

View File

@@ -0,0 +1,2 @@
export * from './schema';
export * from './migrations';

View File

@@ -0,0 +1,19 @@
import { Migration } from 'kysely';
export const createWorkspacesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('workspaces')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('user_id', 'text', (col) => col.notNull())
.addColumn('account_id', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('description', 'text')
.addColumn('avatar', 'text')
.addColumn('role', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('workspaces').execute();
},
};

View File

@@ -0,0 +1,7 @@
import { Migration } from 'kysely';
import { createWorkspacesTable } from './00001-create-workspaces-table';
export const accountDatabaseMigrations: Record<string, Migration> = {
'00001-create-workspaces-table': createWorkspacesTable,
};

View File

@@ -0,0 +1,20 @@
import { WorkspaceRole } from '@colanode/core';
import { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
interface WorkspaceTable {
id: ColumnType<string, string, never>;
user_id: ColumnType<string, string, never>;
account_id: ColumnType<string, string, never>;
name: ColumnType<string, string, string>;
description: ColumnType<string | null, string | null, string | null>;
avatar: ColumnType<string | null, string | null, string | null>;
role: ColumnType<WorkspaceRole, WorkspaceRole, WorkspaceRole>;
}
export type SelectWorkspace = Selectable<WorkspaceTable>;
export type CreateWorkspace = Insertable<WorkspaceTable>;
export type UpdateWorkspace = Updateable<WorkspaceTable>;
export interface AccountDatabaseSchema {
workspaces: WorkspaceTable;
}

View File

@@ -0,0 +1,2 @@
export * from './schema';
export * from './migrations';

View File

@@ -0,0 +1,41 @@
import { Migration } from 'kysely';
export const createServersTable: Migration = {
up: async (db) => {
await db.schema
.createTable('servers')
.addColumn('domain', 'text', (col) => col.notNull().primaryKey())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('avatar', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('version', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('last_synced_at', 'text')
.execute();
await db
.insertInto('servers')
.values([
{
domain: 'eu.colanode.com',
name: 'Colanode Cloud (EU)',
avatar: 'https://colanode.com/assets/flags/eu.svg',
attributes: '{}',
version: '0.1.0',
created_at: new Date().toISOString(),
},
{
domain: 'us.colanode.com',
name: 'Colanode Cloud (US)',
avatar: 'https://colanode.com/assets/flags/us.svg',
attributes: '{}',
version: '0.1.0',
created_at: new Date().toISOString(),
},
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('servers').execute();
},
};

View File

@@ -0,0 +1,19 @@
import { Migration } from 'kysely';
export const createAccountsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('accounts')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('device_id', 'text', (col) => col.notNull())
.addColumn('server', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', (col) => col.notNull())
.addColumn('avatar', 'text')
.addColumn('token', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('accounts').execute();
},
};

View File

@@ -0,0 +1,16 @@
import { Migration } from 'kysely';
export const createDeletedTokensTable: Migration = {
up: async (db) => {
await db.schema
.createTable('deleted_tokens')
.addColumn('account_id', 'text', (col) => col.notNull())
.addColumn('token', 'text', (col) => col.notNull().primaryKey())
.addColumn('server', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('deleted_tokens').execute();
},
};

View File

@@ -0,0 +1,16 @@
import { Migration } from 'kysely';
export const createMetadataTable: Migration = {
up: async (db) => {
await db.schema
.createTable('metadata')
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
.addColumn('value', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('metadata').execute();
},
};

View File

@@ -0,0 +1,13 @@
import { Migration } from 'kysely';
import { createServersTable } from './00001-create-servers-table';
import { createAccountsTable } from './00002-create-accounts-table';
import { createDeletedTokensTable } from './00003-create-deleted-tokens-table';
import { createMetadataTable } from './00004-create-metadata-table';
export const appDatabaseMigrations: Record<string, Migration> = {
'00001-create-servers-table': createServersTable,
'00002-create-accounts-table': createAccountsTable,
'00003-create-deleted-tokens-table': createDeletedTokensTable,
'00004-create-metadata-table': createMetadataTable,
};

View File

@@ -1,4 +1,3 @@
import { WorkspaceRole } from '@colanode/core';
import { ColumnType, Insertable, Selectable, Updateable } from 'kysely'; import { ColumnType, Insertable, Selectable, Updateable } from 'kysely';
interface ServerTable { interface ServerTable {
@@ -29,20 +28,6 @@ export type SelectAccount = Selectable<AccountTable>;
export type CreateAccount = Insertable<AccountTable>; export type CreateAccount = Insertable<AccountTable>;
export type UpdateAccount = Updateable<AccountTable>; export type UpdateAccount = Updateable<AccountTable>;
interface WorkspaceTable {
user_id: ColumnType<string, string, never>;
workspace_id: ColumnType<string, string, never>;
account_id: ColumnType<string, string, never>;
name: ColumnType<string, string, string>;
description: ColumnType<string | null, string | null, string | null>;
avatar: ColumnType<string | null, string | null, string | null>;
role: ColumnType<WorkspaceRole, WorkspaceRole, WorkspaceRole>;
}
export type SelectWorkspace = Selectable<WorkspaceTable>;
export type CreateWorkspace = Insertable<WorkspaceTable>;
export type UpdateWorkspace = Updateable<WorkspaceTable>;
interface DeletedTokenTable { interface DeletedTokenTable {
token: ColumnType<string, string, never>; token: ColumnType<string, string, never>;
account_id: ColumnType<string, string, never>; account_id: ColumnType<string, string, never>;
@@ -64,7 +49,6 @@ export type UpdateMetadata = Updateable<MetadataTable>;
export interface AppDatabaseSchema { export interface AppDatabaseSchema {
servers: ServerTable; servers: ServerTable;
accounts: AccountTable; accounts: AccountTable;
workspaces: WorkspaceTable;
deleted_tokens: DeletedTokenTable; deleted_tokens: DeletedTokenTable;
metadata: MetadataTable; metadata: MetadataTable;
} }

View File

@@ -0,0 +1,2 @@
export * from './schema';
export * from './migrations';

View File

@@ -0,0 +1,25 @@
import { Migration } from 'kysely';
export const createUsersTable: Migration = {
up: async (db) => {
await db.schema
.createTable('users')
.addColumn('id', 'text', (col) => col.primaryKey().notNull())
.addColumn('email', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('avatar', 'text')
.addColumn('custom_name', 'text')
.addColumn('custom_avatar', 'text')
.addColumn('role', 'text', (col) => col.notNull())
.addColumn('storage_limit', 'integer', (col) => col.notNull())
.addColumn('max_file_size', 'integer', (col) => col.notNull())
.addColumn('status', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('version', 'integer')
.execute();
},
down: async (db) => {
await db.schema.dropTable('users').execute();
},
};

View File

@@ -0,0 +1,37 @@
import { Migration, sql } from 'kysely';
export const createEntriesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entries')
.addColumn('id', 'text', (col) => col.primaryKey().notNull())
.addColumn('type', 'text', (col) =>
col
.notNull()
.generatedAlwaysAs(sql`json_extract(attributes, '$.type')`)
.stored()
)
.addColumn('parent_id', 'text', (col) =>
col
.generatedAlwaysAs(sql`json_extract(attributes, '$.parentId')`)
.stored()
)
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_by', 'text')
.addColumn('transaction_id', 'text', (col) => col.notNull())
.execute();
await db.schema
.createIndex('entries_parent_id_type_index')
.on('entries')
.columns(['parent_id', 'type'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('entries').execute();
},
};

View File

@@ -0,0 +1,21 @@
import { Migration } from 'kysely';
export const createEntryTransactionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_transactions')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('operation', 'integer', (col) => col.notNull())
.addColumn('data', 'blob')
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('server_created_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('entry_transactions').execute();
},
};

View File

@@ -0,0 +1,24 @@
import { Migration } from 'kysely';
export const createEntryInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_interactions')
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('first_seen_at', 'text')
.addColumn('last_seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('entry_interactions_pkey', [
'entry_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('entry_interactions').execute();
},
};

View File

@@ -0,0 +1,18 @@
import { Migration } from 'kysely';
export const createCollaborationsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('collaborations')
.addColumn('entry_id', 'text', (col) => col.notNull().primaryKey())
.addColumn('role', 'text')
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer')
.execute();
},
down: async (db) => {
await db.schema.dropTable('collaborations').execute();
},
};

View File

@@ -0,0 +1,35 @@
import { Migration, sql } from 'kysely';
export const createMessagesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('messages')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'integer', (col) =>
col
.notNull()
.generatedAlwaysAs(sql`json_extract(attributes, '$.type')`)
.stored()
)
.addColumn('parent_id', 'text', (col) => col.notNull())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('attributes', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('updated_by', 'text')
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer')
.execute();
await db.schema
.createIndex('messages_parent_id_index')
.on('messages')
.columns(['parent_id'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('messages').execute();
},
};

View File

@@ -0,0 +1,24 @@
import { Migration } from 'kysely';
export const createMessageReactionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('message_reactions')
.addColumn('message_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('reaction', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('deleted_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('message_reactions_pkey', [
'message_id',
'collaborator_id',
'reaction',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('message_reactions').execute();
},
};

View File

@@ -0,0 +1,24 @@
import { Migration } from 'kysely';
export const createMessageInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('message_interactions')
.addColumn('message_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('first_seen_at', 'text')
.addColumn('last_seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('message_interactions_pkey', [
'message_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('message_interactions').execute();
},
};

View File

@@ -0,0 +1,35 @@
import { Migration } from 'kysely';
export const createFilesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('files')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'text', (col) => col.notNull())
.addColumn('parent_id', 'text', (col) => col.notNull())
.addColumn('entry_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('original_name', 'text', (col) => col.notNull())
.addColumn('mime_type', 'text', (col) => col.notNull())
.addColumn('extension', 'text', (col) => col.notNull())
.addColumn('size', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('created_by', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.addColumn('updated_by', 'text')
.addColumn('deleted_at', 'text')
.addColumn('status', 'integer', (col) => col.notNull())
.addColumn('version', 'integer')
.execute();
await db.schema
.createIndex('files_parent_id_index')
.on('files')
.columns(['parent_id'])
.execute();
},
down: async (db) => {
await db.schema.dropTable('files').execute();
},
};

View File

@@ -0,0 +1,21 @@
import { Migration } from 'kysely';
export const createFileStatesTable: Migration = {
up: async (db) => {
await db.schema
.createTable('file_states')
.addColumn('file_id', 'text', (col) => col.notNull().primaryKey())
.addColumn('download_status', 'integer', (col) => col.notNull())
.addColumn('download_progress', 'integer', (col) => col.notNull())
.addColumn('download_retries', 'integer', (col) => col.notNull())
.addColumn('upload_status', 'integer', (col) => col.notNull())
.addColumn('upload_progress', 'integer', (col) => col.notNull())
.addColumn('upload_retries', 'integer', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('file_states').execute();
},
};

View File

@@ -0,0 +1,23 @@
import { Migration } from 'kysely';
export const createFileInteractionsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('file_interactions')
.addColumn('file_id', 'text', (col) => col.notNull())
.addColumn('collaborator_id', 'text', (col) => col.notNull())
.addColumn('root_id', 'text', (col) => col.notNull())
.addColumn('seen_at', 'text')
.addColumn('first_opened_at', 'text')
.addColumn('last_opened_at', 'text')
.addColumn('version', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('file_interactions_pkey', [
'file_id',
'collaborator_id',
])
.execute();
},
down: async (db) => {
await db.schema.dropTable('file_interactions').execute();
},
};

View File

@@ -0,0 +1,17 @@
import { Migration } from 'kysely';
export const createMutationsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('mutations')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.addColumn('type', 'text', (col) => col.notNull())
.addColumn('data', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('retries', 'integer', (col) => col.notNull())
.execute();
},
down: async (db) => {
await db.schema.dropTable('mutations').execute();
},
};

View File

@@ -0,0 +1,63 @@
import { Migration, sql } from 'kysely';
export const createEntryPathsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('entry_paths')
.addColumn('ancestor_id', 'varchar(30)', (col) =>
col.notNull().references('entries.id').onDelete('cascade')
)
.addColumn('descendant_id', 'varchar(30)', (col) =>
col.notNull().references('entries.id').onDelete('cascade')
)
.addColumn('level', 'integer', (col) => col.notNull())
.addPrimaryKeyConstraint('entry_paths_pkey', [
'ancestor_id',
'descendant_id',
])
.execute();
await sql`
CREATE TRIGGER trg_insert_entry_path
AFTER INSERT ON entries
FOR EACH ROW
BEGIN
-- Insert direct path from the new entry to itself
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
VALUES (NEW.id, NEW.id, 0);
-- Insert paths from ancestors to the new entry
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
SELECT ancestor_id, NEW.id, level + 1
FROM entry_paths
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
END;
`.execute(db);
await sql`
CREATE TRIGGER trg_update_entry_path
AFTER UPDATE ON entries
FOR EACH ROW
WHEN OLD.parent_id <> NEW.parent_id
BEGIN
-- Delete old paths involving the updated entry
DELETE FROM entry_paths
WHERE descendant_id = NEW.id AND ancestor_id <> NEW.id;
-- Insert new paths from ancestors to the updated entry
INSERT INTO entry_paths (ancestor_id, descendant_id, level)
SELECT ancestor_id, NEW.id, level + 1
FROM entry_paths
WHERE descendant_id = NEW.parent_id AND ancestor_id <> NEW.id;
END;
`.execute(db);
},
down: async (db) => {
await sql`
DROP TRIGGER IF EXISTS trg_insert_entry_path;
DROP TRIGGER IF EXISTS trg_update_entry_path;
`.execute(db);
await db.schema.dropTable('entry_paths').execute();
},
};

View File

@@ -0,0 +1,14 @@
import { Migration, sql } from 'kysely';
export const createTextsTable: Migration = {
up: async (db) => {
await sql`
CREATE VIRTUAL TABLE texts USING fts5(id UNINDEXED, name, text);
`.execute(db);
},
down: async (db) => {
await sql`
DROP TABLE IF EXISTS texts;
`.execute(db);
},
};

View File

@@ -0,0 +1,16 @@
import { Migration } from 'kysely';
export const createCursorsTable: Migration = {
up: async (db) => {
await db.schema
.createTable('cursors')
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
.addColumn('value', 'integer', (col) => col.notNull().defaultTo(0))
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('cursors').execute();
},
};

View File

@@ -0,0 +1,35 @@
import { Migration } from 'kysely';
import { createUsersTable } from './00001-create-users-table';
import { createEntriesTable } from './00002-create-entries-table';
import { createEntryInteractionsTable } from './00004-create-entry-interactions-table';
import { createEntryTransactionsTable } from './00003-create-entry-transactions-table';
import { createCollaborationsTable } from './00005-create-collaborations-table';
import { createMessagesTable } from './00006-create-messages-table';
import { createMessageReactionsTable } from './00007-create-message-reactions-table';
import { createMessageInteractionsTable } from './00008-create-message-interactions-table';
import { createFilesTable } from './00009-create-files-table';
import { createFileStatesTable } from './00010-create-file-states-table';
import { createFileInteractionsTable } from './00011-create-file-interactions-table';
import { createMutationsTable } from './00012-create-mutations-table';
import { createEntryPathsTable } from './00013-create-entry-paths-table';
import { createTextsTable } from './00014-create-texts-table';
import { createCursorsTable } from './00015-create-cursors-table';
export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00001-create-users-table': createUsersTable,
'00002-create-entries-table': createEntriesTable,
'00003-create-entry-transactions-table': createEntryTransactionsTable,
'00004-create-entry-interactions-table': createEntryInteractionsTable,
'00005-create-collaborations-table': createCollaborationsTable,
'00006-create-messages-table': createMessagesTable,
'00007-create-message-reactions-table': createMessageReactionsTable,
'00008-create-message-interactions-table': createMessageInteractionsTable,
'00009-create-files-table': createFilesTable,
'00010-create-file-states-table': createFileStatesTable,
'00011-create-file-interactions-table': createFileInteractionsTable,
'00012-create-mutations-table': createMutationsTable,
'00013-create-entry-paths-table': createEntryPathsTable,
'00014-create-texts-table': createTextsTable,
'00015-create-cursors-table': createCursorsTable,
};

View File

@@ -1,26 +0,0 @@
import { fileService } from '@/main/services/file-service';
import { JobHandler } from '@/main/jobs';
export type CleanDeletedFilesInput = {
type: 'clean_deleted_files';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
clean_deleted_files: {
input: CleanDeletedFilesInput;
};
}
}
export class CleanDeletedFilesJobHandler
implements JobHandler<CleanDeletedFilesInput>
{
public triggerDebounce = 1000;
public interval = 1000 * 60 * 10;
public async handleJob(input: CleanDeletedFilesInput) {
await fileService.cleanDeletedFiles(input.userId);
}
}

View File

@@ -1,26 +0,0 @@
import { fileService } from '@/main/services/file-service';
import { JobHandler } from '@/main/jobs';
export type CleanTempFilesInput = {
type: 'clean_temp_files';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
clean_temp_files: {
input: CleanTempFilesInput;
};
}
}
export class CleanTempFilesJobHandler
implements JobHandler<CleanTempFilesInput>
{
public triggerDebounce = 1000;
public interval = 1000 * 60 * 30;
public async handleJob(input: CleanTempFilesInput) {
await fileService.cleanTempFiles(input.userId);
}
}

View File

@@ -1,41 +0,0 @@
import { createDebugger } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { socketService } from '@/main/services/socket-service';
import { JobHandler } from '@/main/jobs';
export type ConnectSocketInput = {
type: 'connect_socket';
accountId: string;
};
declare module '@/main/jobs' {
interface JobMap {
connect_socket: {
input: ConnectSocketInput;
};
}
}
export class ConnectSocketJobHandler implements JobHandler<ConnectSocketInput> {
public triggerDebounce = 0;
public interval = 1000 * 30;
private readonly debug = createDebugger('desktop:job:connect-socket');
public async handleJob(input: ConnectSocketInput) {
const account = await databaseService.appDatabase
.selectFrom('accounts')
.selectAll()
.where('id', '=', input.accountId)
.executeTakeFirst();
if (!account) {
this.debug(`Account ${input.accountId} not found`);
return;
}
this.debug(`Checking connection to socket for account ${account.email}`);
socketService.checkConnection(account);
}
}

View File

@@ -1,24 +0,0 @@
import { fileService } from '@/main/services/file-service';
import { JobHandler } from '@/main/jobs';
export type DownloadFilesInput = {
type: 'download_files';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
download_files: {
input: DownloadFilesInput;
};
}
}
export class DownloadFilesJobHandler implements JobHandler<DownloadFilesInput> {
public triggerDebounce = 0;
public interval = 1000 * 60;
public async handleJob(input: DownloadFilesInput) {
await fileService.downloadFiles(input.userId);
}
}

View File

@@ -1,10 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface JobMap {}
export type JobInput = JobMap[keyof JobMap]['input'];
export interface JobHandler<T extends JobInput> {
triggerDebounce: number;
interval: number;
handleJob: (input: T) => Promise<void>;
}

View File

@@ -1,26 +0,0 @@
import { syncService } from '@/main/services/sync-service';
import { JobHandler } from '@/main/jobs';
export type InitSynchronizersInput = {
type: 'init_synchronizers';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
init_synchronizers: {
input: InitSynchronizersInput;
};
}
}
export class InitSynchronizersJobHandler
implements JobHandler<InitSynchronizersInput>
{
public triggerDebounce = 100;
public interval = 1000 * 60;
public async handleJob(input: InitSynchronizersInput) {
syncService.initSynchronizers(input.userId);
}
}

View File

@@ -1,96 +0,0 @@
import { createDebugger } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { fileService } from '@/main/services/file-service';
import { messageService } from '@/main/services/message-service';
import { databaseService } from '@/main/data/database-service';
import { JobHandler } from '@/main/jobs';
import { mapMutation } from '@/main/utils';
export type RevertInvalidMutationsInput = {
type: 'revert_invalid_mutations';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
revert_invalid_mutations: {
input: RevertInvalidMutationsInput;
};
}
}
export class RevertInvalidMutationsJobHandler
implements JobHandler<RevertInvalidMutationsInput>
{
public triggerDebounce = 100;
public interval = 1000 * 60 * 5;
private readonly debug = createDebugger(
'desktop:job:revert-invalid-mutations'
);
public async handleJob(input: RevertInvalidMutationsInput) {
this.debug(`Reverting invalid mutations for user ${input.userId}`);
const workspaceDatabase = await databaseService.getWorkspaceDatabase(
input.userId
);
const invalidMutations = await workspaceDatabase
.selectFrom('mutations')
.selectAll()
.where('retries', '>=', 10)
.execute();
if (invalidMutations.length === 0) {
this.debug(
`No invalid mutations found for user ${input.userId}, skipping`
);
return;
}
for (const mutationRow of invalidMutations) {
const mutation = mapMutation(mutationRow);
if (mutation.type === 'create_file') {
await fileService.revertFileCreation(input.userId, mutation.id);
} else if (mutation.type === 'delete_file') {
await fileService.revertFileDeletion(input.userId, mutation.id);
} else if (mutation.type === 'apply_create_transaction') {
await entryService.revertCreateTransaction(input.userId, mutation.data);
} else if (mutation.type === 'apply_update_transaction') {
await entryService.revertUpdateTransaction(input.userId, mutation.data);
} else if (mutation.type === 'apply_delete_transaction') {
await entryService.revertDeleteTransaction(input.userId, mutation.data);
} else if (mutation.type === 'create_message') {
await messageService.revertMessageCreation(
input.userId,
mutation.data.id
);
} else if (mutation.type === 'delete_message') {
await messageService.revertMessageDeletion(
input.userId,
mutation.data.id
);
} else if (mutation.type === 'create_message_reaction') {
await messageService.revertMessageReactionCreation(
input.userId,
mutation.data
);
} else if (mutation.type === 'delete_message_reaction') {
await messageService.revertMessageReactionDeletion(
input.userId,
mutation.data
);
}
}
const mutationIds = invalidMutations.map((m) => m.id);
await workspaceDatabase
.deleteFrom('mutations')
.where('id', 'in', mutationIds)
.execute();
}
}

View File

@@ -1,24 +0,0 @@
import { accountService } from '@/main/services/account-service';
import { JobHandler } from '@/main/jobs';
export type SyncAccountInput = {
type: 'sync_account';
accountId: string;
};
declare module '@/main/jobs' {
interface JobMap {
sync_account: {
input: SyncAccountInput;
};
}
}
export class SyncAccountJobHandler implements JobHandler<SyncAccountInput> {
public triggerDebounce = 0;
public interval = 1000 * 60;
public async handleJob(input: SyncAccountInput) {
await accountService.syncAccount(input.accountId);
}
}

View File

@@ -1,104 +0,0 @@
import { ApiErrorCode, createDebugger } from '@colanode/core';
import { serverService } from '@/main/services/server-service';
import { databaseService } from '@/main/data/database-service';
import { JobHandler } from '@/main/jobs';
import { httpClient } from '@/shared/lib/http-client';
import { parseApiError } from '@/shared/lib/axios';
export type SyncDeletedTokensInput = {
type: 'sync_deleted_tokens';
};
declare module '@/main/jobs' {
interface JobMap {
sync_deleted_tokens: {
input: SyncDeletedTokensInput;
};
}
}
export class SyncDeletedTokensJobHandler
implements JobHandler<SyncDeletedTokensInput>
{
public triggerDebounce = 100;
public interval = 1000 * 60 * 5;
private readonly debug = createDebugger('desktop:job:sync-deleted-tokens');
public async handleJob(_: SyncDeletedTokensInput) {
this.debug('Syncing deleted tokens');
const deletedTokens = await databaseService.appDatabase
.selectFrom('deleted_tokens')
.innerJoin('servers', 'deleted_tokens.server', 'servers.domain')
.select([
'deleted_tokens.token',
'deleted_tokens.account_id',
'servers.domain',
'servers.attributes',
])
.execute();
if (deletedTokens.length === 0) {
this.debug('No deleted tokens found');
return;
}
for (const deletedToken of deletedTokens) {
if (!serverService.isAvailable(deletedToken.domain)) {
this.debug(
`Server ${deletedToken.domain} is not available for logging out account ${deletedToken.account_id}`
);
continue;
}
try {
const { status } = await httpClient.delete(`/v1/accounts/logout`, {
domain: deletedToken.domain,
token: deletedToken.token,
});
this.debug(`Deleted token logout response status code: ${status}`);
if (status !== 200) {
return;
}
await databaseService.appDatabase
.deleteFrom('deleted_tokens')
.where('token', '=', deletedToken.token)
.where('account_id', '=', deletedToken.account_id)
.execute();
this.debug(
`Logged out account ${deletedToken.account_id} from server ${deletedToken.domain}`
);
} catch (error) {
const parsedError = parseApiError(error);
if (
parsedError.code === ApiErrorCode.TokenInvalid ||
parsedError.code === ApiErrorCode.AccountNotFound ||
parsedError.code === ApiErrorCode.DeviceNotFound
) {
this.debug(
`Account ${deletedToken.account_id} is already logged out, skipping...`
);
await databaseService.appDatabase
.deleteFrom('deleted_tokens')
.where('token', '=', deletedToken.token)
.where('account_id', '=', deletedToken.account_id)
.execute();
continue;
}
this.debug(
`Failed to logout account ${deletedToken.account_id} from server ${deletedToken.domain}`,
error
);
}
}
}
}

View File

@@ -1,23 +0,0 @@
import { serverService } from '@/main/services/server-service';
import { JobHandler } from '@/main/jobs';
export type SyncServersInput = {
type: 'sync_servers';
};
declare module '@/main/jobs' {
interface JobMap {
sync_servers: {
input: SyncServersInput;
};
}
}
export class SyncServersJobHandler implements JobHandler<SyncServersInput> {
public triggerDebounce = 0;
public interval = 1000 * 60;
public async handleJob(_: SyncServersInput) {
await serverService.syncServers();
}
}

View File

@@ -1,24 +0,0 @@
import { fileService } from '@/main/services/file-service';
import { JobHandler } from '@/main/jobs';
export type UploadFilesInput = {
type: 'upload_files';
userId: string;
};
declare module '@/main/jobs' {
interface JobMap {
upload_files: {
input: UploadFilesInput;
};
}
}
export class UploadFilesJobHandler implements JobHandler<UploadFilesInput> {
public triggerDebounce = 0;
public interval = 1000 * 60;
public async handleJob(input: UploadFilesInput) {
await fileService.uploadFiles(input.userId);
}
}

View File

@@ -0,0 +1,24 @@
import fs from 'fs';
import path from 'path';
import { getAssetsSourcePath } from '@/main/utils';
import { EmojiData } from '@/shared/types/emojis';
import { IconData } from '@/shared/types/icons';
export const getEmojiData = (): EmojiData => {
const emojisMetadataPath = path.join(
getAssetsSourcePath(),
'emojis',
'emojis.json'
);
return JSON.parse(fs.readFileSync(emojisMetadataPath, 'utf8'));
};
export const getIconData = (): IconData => {
const iconsMetadataPath = path.join(
getAssetsSourcePath(),
'icons',
'icons.json'
);
return JSON.parse(fs.readFileSync(iconsMetadataPath, 'utf8'));
};

View File

@@ -0,0 +1,102 @@
import { net } from 'electron';
import fs from 'fs';
import path from 'path';
import {
getAccountAvatarsDirectoryPath,
getAssetsSourcePath,
getWorkspaceFilesDirectoryPath,
} from '@/main/utils';
import { appService } from '@/main/services/app-service';
export const handleAssetRequest = (request: Request): Promise<Response> => {
const url = request.url.replace('asset://', '');
const assetPath = path.join(getAssetsSourcePath(), url);
const localFileUrl = `file://${assetPath}`;
return net.fetch(localFileUrl);
};
export const handleAvatarRequest = async (
request: Request
): Promise<Response> => {
const url = request.url.replace('avatar://', '');
const [accountId, avatarId] = url.split('/');
if (!accountId || !avatarId) {
return new Response(null, { status: 400 });
}
const avatarsDir = getAccountAvatarsDirectoryPath(accountId);
const avatarPath = path.join(avatarsDir, `${avatarId}.jpeg`);
const avatarLocalUrl = `file://${avatarPath}`;
// Check if the avatar file already exists
if (fs.existsSync(avatarPath)) {
return net.fetch(avatarLocalUrl);
}
// Download the avatar file if it doesn't exist
const account = appService.getAccount(accountId);
if (!account) {
return new Response(null, { status: 404 });
}
const response = await account.client.get<NodeJS.ReadableStream>(
`/v1/avatars/${avatarId}`,
{
responseType: 'stream',
}
);
if (response.status !== 200 || !response.data) {
return new Response(null, { status: 404 });
}
if (!fs.existsSync(avatarsDir)) {
fs.mkdirSync(avatarsDir, { recursive: true });
}
const fileStream = fs.createWriteStream(avatarPath);
return new Promise((resolve, reject) => {
response.data.pipe(fileStream);
fileStream.on('finish', async () => {
resolve(net.fetch(avatarLocalUrl));
});
fileStream.on('error', (err) => {
reject(new Response(null, { status: 500, statusText: err.message }));
});
});
};
export const handleFilePreviewRequest = async (
request: Request
): Promise<Response> => {
const url = request.url.replace('local-file-preview://', 'file://');
return net.fetch(url);
};
export const handleFileRequest = async (
request: Request
): Promise<Response> => {
const url = request.url.replace('local-file://', '');
const [accountId, workspaceId, file] = url.split('/');
if (!accountId || !workspaceId || !file) {
return new Response(null, { status: 400 });
}
const workspaceFilesDir = getWorkspaceFilesDirectoryPath(
accountId,
workspaceId
);
const filePath = path.join(workspaceFilesDir, file);
if (fs.existsSync(filePath)) {
const fileUrl = `file://${filePath}`;
return net.fetch(fileUrl);
}
return new Response(null, { status: 404 });
};

View File

@@ -1,14 +1,29 @@
import { isEqual } from 'lodash-es';
import { createDebugger } from '@colanode/core'; import { createDebugger } from '@colanode/core';
import { isEqual } from 'lodash-es';
import { mutationHandlerMap } from '@/main/mutations';
import {
MutationHandler,
CommandHandler,
QueryHandler,
SubscribedQuery,
} from '@/main/types';
import {
MutationError,
MutationErrorCode,
MutationInput,
MutationResult,
} from '@/shared/mutations';
import { commandHandlerMap } from '@/main/commands';
import { CommandInput, CommandMap } from '@/shared/commands';
import { queryHandlerMap } from '@/main/queries'; import { queryHandlerMap } from '@/main/queries';
import { QueryHandler, SubscribedQuery } from '@/main/types';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { QueryInput, QueryMap } from '@/shared/queries'; import { QueryInput, QueryMap } from '@/shared/queries';
import { Event } from '@/shared/types/events'; import { Event } from '@/shared/types/events';
class QueryService { class Mediator {
private readonly debug = createDebugger('desktop:service:query'); private readonly debug = createDebugger('desktop:mediator');
private readonly subscribedQueries: Map<string, SubscribedQuery<QueryInput>> = private readonly subscribedQueries: Map<string, SubscribedQuery<QueryInput>> =
new Map(); new Map();
@@ -127,6 +142,60 @@ class QueryService {
this.processEventsQueue(); this.processEventsQueue();
} }
} }
public async executeMutation<T extends MutationInput>(
input: T
): Promise<MutationResult<T>> {
const handler = mutationHandlerMap[
input.type
] as unknown as MutationHandler<T>;
this.debug(`Executing mutation: ${input.type}`);
try {
if (!handler) {
throw new Error(`No handler found for mutation type: ${input.type}`);
}
const output = await handler.handleMutation(input);
return { success: true, output };
} catch (error) {
this.debug(`Error executing mutation: ${input.type}`, error);
if (error instanceof MutationError) {
return {
success: false,
error: {
code: error.code,
message: error.message,
},
};
}
return {
success: false,
error: {
code: MutationErrorCode.Unknown,
message: 'Something went wrong trying to execute the mutation.',
},
};
}
}
public async executeCommand<T extends CommandInput>(
input: T
): Promise<CommandMap[T['type']]['output']> {
this.debug(`Executing command: ${input.type}`);
const handler = commandHandlerMap[
input.type
] as unknown as CommandHandler<T>;
if (!handler) {
throw new Error(`No handler found for command type: ${input.type}`);
}
return handler.handleCommand(input);
}
} }
export const queryService = new QueryService(); export const mediator = new Mediator();

View File

@@ -1,5 +1,4 @@
import { databaseService } from '@/main/data/database-service'; import { appService } from '@/main/services/app-service';
import { accountService } from '@/main/services/account-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
@@ -13,11 +12,7 @@ export class AccountLogoutMutationHandler
async handleMutation( async handleMutation(
input: AccountLogoutMutationInput input: AccountLogoutMutationInput
): Promise<AccountLogoutMutationOutput> { ): Promise<AccountLogoutMutationOutput> {
const account = await databaseService.appDatabase const account = appService.getAccount(input.accountId);
.selectFrom('accounts')
.selectAll()
.where('id', '=', input.accountId)
.executeTakeFirst();
if (!account) { if (!account) {
throw new MutationError( throw new MutationError(
@@ -26,7 +21,7 @@ export class AccountLogoutMutationHandler
); );
} }
await accountService.logoutAccount(account); await account.logout();
return { return {
success: true, success: true,
}; };

View File

@@ -1,15 +1,15 @@
import { AccountUpdateOutput } from '@colanode/core'; import { AccountUpdateOutput } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { httpClient } from '@/shared/lib/http-client';
import { import {
AccountUpdateMutationInput, AccountUpdateMutationInput,
AccountUpdateMutationOutput, AccountUpdateMutationOutput,
} from '@/shared/mutations/accounts/account-update'; } from '@/shared/mutations/accounts/account-update';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios'; import { parseApiError } from '@/shared/lib/axios';
import { appService } from '@/main/services/app-service';
import { mapAccount } from '@/main/utils';
export class AccountUpdateMutationHandler export class AccountUpdateMutationHandler
implements MutationHandler<AccountUpdateMutationInput> implements MutationHandler<AccountUpdateMutationInput>
@@ -17,46 +17,25 @@ export class AccountUpdateMutationHandler
async handleMutation( async handleMutation(
input: AccountUpdateMutationInput input: AccountUpdateMutationInput
): Promise<AccountUpdateMutationOutput> { ): Promise<AccountUpdateMutationOutput> {
const account = await databaseService.appDatabase const accountService = appService.getAccount(input.id);
.selectFrom('accounts')
.selectAll()
.where('id', '=', input.id)
.executeTakeFirst();
if (!account) { if (!accountService) {
throw new MutationError( throw new MutationError(
MutationErrorCode.AccountNotFound, MutationErrorCode.AccountNotFound,
'Account not found or has been logged out already. Try closing the app and opening it again.' 'Account not found or has been logged out already. Try closing the app and opening it again.'
); );
} }
const server = await databaseService.appDatabase
.selectFrom('servers')
.selectAll()
.where('domain', '=', account.server)
.executeTakeFirst();
if (!server) {
throw new MutationError(
MutationErrorCode.ServerNotFound,
`The server ${account.server} associated with this account was not found. Try closing the app and opening it again.`
);
}
try { try {
const { data } = await httpClient.put<AccountUpdateOutput>( const { data } = await accountService.client.put<AccountUpdateOutput>(
`/v1/accounts/${input.id}`, `/v1/accounts/${input.id}`,
{ {
name: input.name, name: input.name,
avatar: input.avatar, avatar: input.avatar,
},
{
domain: server.domain,
token: account.token,
} }
); );
const updatedAccount = await databaseService.appDatabase const updatedAccount = await appService.database
.updateTable('accounts') .updateTable('accounts')
.set({ .set({
name: data.name, name: data.name,
@@ -73,17 +52,12 @@ export class AccountUpdateMutationHandler
); );
} }
const account = mapAccount(updatedAccount);
accountService.updateAccount(account);
eventBus.publish({ eventBus.publish({
type: 'account_updated', type: 'account_updated',
account: { account,
id: updatedAccount.id,
name: updatedAccount.name,
email: updatedAccount.email,
token: updatedAccount.token,
avatar: updatedAccount.avatar,
deviceId: updatedAccount.device_id,
server: updatedAccount.server,
},
}); });
return { return {

View File

@@ -0,0 +1,63 @@
import { LoginSuccessOutput } from '@colanode/core';
import { appService } from '@/main/services/app-service';
import { ServerService } from '@/main/services/server-service';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { mapAccount, mapWorkspace } from '@/main/utils';
export abstract class AccountMutationHandlerBase {
protected async handleLoginSuccess(
login: LoginSuccessOutput,
server: ServerService
): Promise<void> {
const createdAccount = await appService.database
.insertInto('accounts')
.returningAll()
.values({
id: login.account.id,
email: login.account.email,
name: login.account.name,
server: server.server.domain,
token: login.token,
device_id: login.deviceId,
avatar: login.account.avatar,
})
.executeTakeFirst();
if (!createdAccount) {
throw new MutationError(
MutationErrorCode.AccountLoginFailed,
'Account login failed, please try again.'
);
}
const account = mapAccount(createdAccount);
const accountService = await appService.initAccount(account);
if (login.workspaces.length === 0) {
return;
}
for (const workspace of login.workspaces) {
const createdWorkspace = await accountService.database
.insertInto('workspaces')
.returningAll()
.values({
id: workspace.id,
name: workspace.name,
user_id: workspace.user.id,
account_id: account.id,
role: workspace.user.role,
avatar: workspace.avatar,
description: workspace.description,
})
.executeTakeFirst();
if (!createdWorkspace) {
continue;
}
await accountService.initWorkspace(mapWorkspace(createdWorkspace));
}
}
}

View File

@@ -1,24 +1,19 @@
import { EmailLoginInput, LoginOutput } from '@colanode/core'; import { EmailLoginInput, LoginOutput } from '@colanode/core';
import axios from 'axios';
import { app } from 'electron';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import { EmailLoginMutationInput } from '@/shared/mutations/accounts/email-login'; import { EmailLoginMutationInput } from '@/shared/mutations/accounts/email-login';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios'; import { parseApiError } from '@/shared/lib/axios';
import { accountService } from '@/main/services/account-service'; import { appService } from '@/main/services/app-service';
import { AccountMutationHandlerBase } from '@/main/mutations/accounts/base';
export class EmailLoginMutationHandler export class EmailLoginMutationHandler
extends AccountMutationHandlerBase
implements MutationHandler<EmailLoginMutationInput> implements MutationHandler<EmailLoginMutationInput>
{ {
async handleMutation(input: EmailLoginMutationInput): Promise<LoginOutput> { async handleMutation(input: EmailLoginMutationInput): Promise<LoginOutput> {
const server = await databaseService.appDatabase const server = appService.getServer(input.server);
.selectFrom('servers')
.selectAll()
.where('domain', '=', input.server)
.executeTakeFirst();
if (!server) { if (!server) {
throw new MutationError( throw new MutationError(
@@ -32,24 +27,23 @@ export class EmailLoginMutationHandler
email: input.email, email: input.email,
password: input.password, password: input.password,
platform: process.platform, platform: process.platform,
version: app.getVersion(), version: appService.version,
}; };
const { data } = await httpClient.post<LoginOutput>( const { data } = await axios.post<LoginOutput>(
'/v1/accounts/emails/login', `${server.apiBaseUrl}/v1/accounts/emails/login`,
emailLoginInput, emailLoginInput
{
domain: server.domain,
}
); );
if (data.type === 'verify') { if (data.type === 'verify') {
return data; return data;
} }
await accountService.initAccount(data, server.domain); await this.handleLoginSuccess(data, server);
return data; return data;
} catch (error) { } catch (error) {
console.error(error);
const apiError = parseApiError(error); const apiError = parseApiError(error);
throw new MutationError(MutationErrorCode.ApiError, apiError.message); throw new MutationError(MutationErrorCode.ApiError, apiError.message);
} }

View File

@@ -1,26 +1,21 @@
import { EmailRegisterInput, LoginOutput } from '@colanode/core'; import { EmailRegisterInput, LoginOutput } from '@colanode/core';
import axios from 'axios';
import { app } from 'electron';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import { EmailRegisterMutationInput } from '@/shared/mutations/accounts/email-register'; import { EmailRegisterMutationInput } from '@/shared/mutations/accounts/email-register';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios'; import { parseApiError } from '@/shared/lib/axios';
import { accountService } from '@/main/services/account-service'; import { appService } from '@/main/services/app-service';
import { AccountMutationHandlerBase } from '@/main/mutations/accounts/base';
export class EmailRegisterMutationHandler export class EmailRegisterMutationHandler
extends AccountMutationHandlerBase
implements MutationHandler<EmailRegisterMutationInput> implements MutationHandler<EmailRegisterMutationInput>
{ {
async handleMutation( async handleMutation(
input: EmailRegisterMutationInput input: EmailRegisterMutationInput
): Promise<LoginOutput> { ): Promise<LoginOutput> {
const server = await databaseService.appDatabase const server = appService.getServer(input.server);
.selectFrom('servers')
.selectAll()
.where('domain', '=', input.server)
.executeTakeFirst();
if (!server) { if (!server) {
throw new MutationError( throw new MutationError(
@@ -35,22 +30,20 @@ export class EmailRegisterMutationHandler
email: input.email, email: input.email,
password: input.password, password: input.password,
platform: process.platform, platform: process.platform,
version: app.getVersion(), version: appService.version,
}; };
const { data } = await httpClient.post<LoginOutput>( const { data } = await axios.post<LoginOutput>(
'/v1/accounts/emails/register', `${server.apiBaseUrl}/v1/accounts/emails/register`,
emailRegisterInput, emailRegisterInput
{
domain: server.domain,
}
); );
if (data.type === 'verify') { if (data.type === 'verify') {
return data; return data;
} }
await accountService.initAccount(data, server.domain); await this.handleLoginSuccess(data, server);
return data; return data;
} catch (error) { } catch (error) {
const apiError = parseApiError(error); const apiError = parseApiError(error);

View File

@@ -1,24 +1,18 @@
import { EmailVerifyInput, LoginOutput } from '@colanode/core'; import { EmailVerifyInput, LoginOutput } from '@colanode/core';
import axios from 'axios';
import { app } from 'electron';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import { EmailVerifyMutationInput } from '@/shared/mutations/accounts/email-verify'; import { EmailVerifyMutationInput } from '@/shared/mutations/accounts/email-verify';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios'; import { parseApiError } from '@/shared/lib/axios';
import { accountService } from '@/main/services/account-service'; import { appService } from '@/main/services/app-service';
import { AccountMutationHandlerBase } from '@/main/mutations/accounts/base';
export class EmailVerifyMutationHandler export class EmailVerifyMutationHandler
extends AccountMutationHandlerBase
implements MutationHandler<EmailVerifyMutationInput> implements MutationHandler<EmailVerifyMutationInput>
{ {
async handleMutation(input: EmailVerifyMutationInput): Promise<LoginOutput> { async handleMutation(input: EmailVerifyMutationInput): Promise<LoginOutput> {
const server = await databaseService.appDatabase const server = appService.getServer(input.server);
.selectFrom('servers')
.selectAll()
.where('domain', '=', input.server)
.executeTakeFirst();
if (!server) { if (!server) {
throw new MutationError( throw new MutationError(
@@ -32,15 +26,12 @@ export class EmailVerifyMutationHandler
id: input.id, id: input.id,
otp: input.otp, otp: input.otp,
platform: process.platform, platform: process.platform,
version: app.getVersion(), version: appService.version,
}; };
const { data } = await httpClient.post<LoginOutput>( const { data } = await axios.post<LoginOutput>(
'/v1/accounts/emails/verify', `${server.apiBaseUrl}/v1/accounts/emails/verify`,
emailVerifyInput, emailVerifyInput
{
domain: server.domain,
}
); );
if (data.type === 'verify') { if (data.type === 'verify') {
@@ -50,7 +41,7 @@ export class EmailVerifyMutationHandler
); );
} }
await accountService.initAccount(data, server.domain); await this.handleLoginSuccess(data, server);
return data; return data;
} catch (error) { } catch (error) {

View File

@@ -2,15 +2,14 @@ import FormData from 'form-data';
import fs from 'fs'; import fs from 'fs';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import { import {
AvatarUploadMutationInput, AvatarUploadMutationInput,
AvatarUploadMutationOutput, AvatarUploadMutationOutput,
} from '@/shared/mutations/avatars/avatar-upload'; } from '@/shared/mutations/avatars/avatar-upload';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios'; import { parseApiError } from '@/shared/lib/axios';
import { appService } from '@/main/services/app-service';
interface AvatarUploadResponse { interface AvatarUploadResponse {
id: string; id: string;
@@ -22,14 +21,9 @@ export class AvatarUploadMutationHandler
async handleMutation( async handleMutation(
input: AvatarUploadMutationInput input: AvatarUploadMutationInput
): Promise<AvatarUploadMutationOutput> { ): Promise<AvatarUploadMutationOutput> {
const credentials = await databaseService.appDatabase const account = appService.getAccount(input.accountId);
.selectFrom('accounts')
.innerJoin('servers', 'accounts.server', 'servers.domain')
.select(['domain', 'attributes', 'token'])
.where('id', '=', input.accountId)
.executeTakeFirst();
if (!credentials) { if (!account) {
throw new MutationError( throw new MutationError(
MutationErrorCode.AccountNotFound, MutationErrorCode.AccountNotFound,
'Account not found or has been logged out already. Try closing the app and opening it again.' 'Account not found or has been logged out already. Try closing the app and opening it again.'
@@ -43,12 +37,10 @@ export class AvatarUploadMutationHandler
const formData = new FormData(); const formData = new FormData();
formData.append('avatar', fileStream); formData.append('avatar', fileStream);
const { data } = await httpClient.post<AvatarUploadResponse>( const { data } = await account.client.post<AvatarUploadResponse>(
'/v1/avatars', '/v1/avatars',
formData, formData,
{ {
domain: credentials.domain,
token: credentials.token,
headers: formData.getHeaders(), headers: formData.getHeaders(),
} }
); );

View File

@@ -1,25 +1,23 @@
import { ChannelAttributes, generateId, IdType } from '@colanode/core'; import { ChannelAttributes, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
ChannelCreateMutationInput, ChannelCreateMutationInput,
ChannelCreateMutationOutput, ChannelCreateMutationOutput,
} from '@/shared/mutations/channels/channel-create'; } from '@/shared/mutations/channels/channel-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ChannelCreateMutationHandler export class ChannelCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChannelCreateMutationInput> implements MutationHandler<ChannelCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: ChannelCreateMutationInput input: ChannelCreateMutationInput
): Promise<ChannelCreateMutationOutput> { ): Promise<ChannelCreateMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const space = await workspaceDatabase const space = await workspace.database
.selectFrom('entries') .selectFrom('entries')
.selectAll() .selectAll()
.where('id', '=', input.spaceId) .where('id', '=', input.spaceId)
@@ -40,7 +38,7 @@ export class ChannelCreateMutationHandler
parentId: input.spaceId, parentId: input.spaceId,
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id, id,
attributes, attributes,
parentId: input.spaceId, parentId: input.spaceId,

View File

@@ -1,17 +1,19 @@
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
ChannelDeleteMutationInput, ChannelDeleteMutationInput,
ChannelDeleteMutationOutput, ChannelDeleteMutationOutput,
} from '@/shared/mutations/channels/channel-delete'; } from '@/shared/mutations/channels/channel-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ChannelDeleteMutationHandler export class ChannelDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChannelDeleteMutationInput> implements MutationHandler<ChannelDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: ChannelDeleteMutationInput input: ChannelDeleteMutationInput
): Promise<ChannelDeleteMutationOutput> { ): Promise<ChannelDeleteMutationOutput> {
await entryService.deleteEntry(input.channelId, input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
await workspace.entries.deleteEntry(input.channelId);
return { return {
success: true, success: true,

View File

@@ -1,22 +1,24 @@
import { ChannelAttributes } from '@colanode/core'; import { ChannelAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
ChannelUpdateMutationInput, ChannelUpdateMutationInput,
ChannelUpdateMutationOutput, ChannelUpdateMutationOutput,
} from '@/shared/mutations/channels/channel-update'; } from '@/shared/mutations/channels/channel-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ChannelUpdateMutationHandler export class ChannelUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChannelUpdateMutationInput> implements MutationHandler<ChannelUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: ChannelUpdateMutationInput input: ChannelUpdateMutationInput
): Promise<ChannelUpdateMutationOutput> { ): Promise<ChannelUpdateMutationOutput> {
const result = await entryService.updateEntry<ChannelAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<ChannelAttributes>(
input.channelId, input.channelId,
input.userId,
(attributes) => { (attributes) => {
attributes.name = input.name; attributes.name = input.name;
attributes.avatar = input.avatar; attributes.avatar = input.avatar;

View File

@@ -1,37 +1,35 @@
import { ChatAttributes, generateId, IdType } from '@colanode/core'; import { ChatAttributes, generateId, IdType } from '@colanode/core';
import { sql } from 'kysely'; import { sql } from 'kysely';
import { databaseService } from '@/main/data/database-service';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
ChatCreateMutationInput, ChatCreateMutationInput,
ChatCreateMutationOutput, ChatCreateMutationOutput,
} from '@/shared/mutations/chats/chat-create'; } from '@/shared/mutations/chats/chat-create';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
interface ChatRow { interface ChatRow {
id: string; id: string;
} }
export class ChatCreateMutationHandler export class ChatCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChatCreateMutationInput> implements MutationHandler<ChatCreateMutationInput>
{ {
public async handleMutation( public async handleMutation(
input: ChatCreateMutationInput input: ChatCreateMutationInput
): Promise<ChatCreateMutationOutput> { ): Promise<ChatCreateMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const query = sql<ChatRow>` const query = sql<ChatRow>`
SELECT id SELECT id
FROM entries FROM entries
WHERE type = 'chat' WHERE type = 'chat'
AND json_extract(attributes, '$.collaborators.${sql.raw(input.userId)}') is not null AND json_extract(attributes, '$.collaborators.${sql.raw(input.userId)}') is not null
AND json_extract(attributes, '$.collaborators.${sql.raw(input.otherUserId)}') is not null AND json_extract(attributes, '$.collaborators.${sql.raw(workspace.userId)}') is not null
`.compile(workspaceDatabase); `.compile(workspace.database);
const existingChats = await workspaceDatabase.executeQuery(query); const existingChats = await workspace.database.executeQuery(query);
const chat = existingChats.rows?.[0]; const chat = existingChats.rows?.[0];
if (chat) { if (chat) {
return { return {
@@ -44,11 +42,11 @@ export class ChatCreateMutationHandler
type: 'chat', type: 'chat',
collaborators: { collaborators: {
[input.userId]: 'admin', [input.userId]: 'admin',
[input.otherUserId]: 'admin', [workspace.userId]: 'admin',
}, },
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id, id,
attributes, attributes,
parentId: null, parentId: null,

View File

@@ -5,19 +5,22 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
DatabaseCreateMutationInput, DatabaseCreateMutationInput,
DatabaseCreateMutationOutput, DatabaseCreateMutationOutput,
} from '@/shared/mutations/databases/database-create'; } from '@/shared/mutations/databases/database-create';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class DatabaseCreateMutationHandler export class DatabaseCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseCreateMutationInput> implements MutationHandler<DatabaseCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: DatabaseCreateMutationInput input: DatabaseCreateMutationInput
): Promise<DatabaseCreateMutationOutput> { ): Promise<DatabaseCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const databaseId = generateId(IdType.Database); const databaseId = generateId(IdType.Database);
const viewId = generateId(IdType.View); const viewId = generateId(IdType.View);
const fieldId = generateId(IdType.Field); const fieldId = generateId(IdType.Field);
@@ -45,7 +48,7 @@ export class DatabaseCreateMutationHandler
}, },
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id: databaseId, id: databaseId,
attributes, attributes,
parentId: input.spaceId, parentId: input.spaceId,

View File

@@ -1,17 +1,19 @@
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
DatabaseDeleteMutationInput, DatabaseDeleteMutationInput,
DatabaseDeleteMutationOutput, DatabaseDeleteMutationOutput,
} from '@/shared/mutations/databases/database-delete'; } from '@/shared/mutations/databases/database-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class DatabaseDeleteMutationHandler export class DatabaseDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseDeleteMutationInput> implements MutationHandler<DatabaseDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: DatabaseDeleteMutationInput input: DatabaseDeleteMutationInput
): Promise<DatabaseDeleteMutationOutput> { ): Promise<DatabaseDeleteMutationOutput> {
await entryService.deleteEntry(input.databaseId, input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
await workspace.entries.deleteEntry(input.databaseId);
return { return {
success: true, success: true,

View File

@@ -1,25 +1,25 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
DatabaseUpdateMutationInput, DatabaseUpdateMutationInput,
DatabaseUpdateMutationOutput, DatabaseUpdateMutationOutput,
} from '@/shared/mutations/databases/database-update'; } from '@/shared/mutations/databases/database-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class DatabaseUpdateMutationHandler export class DatabaseUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseUpdateMutationInput> implements MutationHandler<DatabaseUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: DatabaseUpdateMutationInput input: DatabaseUpdateMutationInput
): Promise<DatabaseUpdateMutationOutput> { ): Promise<DatabaseUpdateMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
attributes.name = input.name; attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes; return attributes;
} }

View File

@@ -7,22 +7,24 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FieldCreateMutationInput, FieldCreateMutationInput,
FieldCreateMutationOutput, FieldCreateMutationOutput,
} from '@/shared/mutations/databases/field-create'; } from '@/shared/mutations/databases/field-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { databaseService } from '@/main/data/database-service';
import { fetchEntry } from '@/main/utils'; import { fetchEntry } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FieldCreateMutationHandler export class FieldCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldCreateMutationInput> implements MutationHandler<FieldCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: FieldCreateMutationInput input: FieldCreateMutationInput
): Promise<FieldCreateMutationOutput> { ): Promise<FieldCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
if (input.fieldType === 'relation') { if (input.fieldType === 'relation') {
if (!input.relationDatabaseId) { if (!input.relationDatabaseId) {
throw new MutationError( throw new MutationError(
@@ -31,12 +33,8 @@ export class FieldCreateMutationHandler
); );
} }
const workspaceDatabase = await databaseService.getWorkspaceDatabase(
input.userId
);
const relationDatabase = await fetchEntry( const relationDatabase = await fetchEntry(
workspaceDatabase, workspace.database,
input.relationDatabaseId input.relationDatabaseId
); );
@@ -49,9 +47,8 @@ export class FieldCreateMutationHandler
} }
const fieldId = generateId(IdType.Field); const fieldId = generateId(IdType.Field);
const result = await entryService.updateEntry( const result = await workspace.entries.updateEntry(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
if (attributes.type !== 'database') { if (attributes.type !== 'database') {
throw new Error('Invalid node type'); throw new Error('Invalid node type');

View File

@@ -1,30 +1,25 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
FieldDeleteMutationInput, FieldDeleteMutationInput,
FieldDeleteMutationOutput, FieldDeleteMutationOutput,
} from '@/shared/mutations/databases/field-delete'; } from '@/shared/mutations/databases/field-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FieldDeleteMutationHandler export class FieldDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldDeleteMutationInput> implements MutationHandler<FieldDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: FieldDeleteMutationInput input: FieldDeleteMutationInput
): Promise<FieldDeleteMutationOutput> { ): Promise<FieldDeleteMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.databaseId,
input.userId,
(attributes) => {
if (!attributes.fields[input.fieldId]) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to delete does not exist.'
);
}
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId,
(attributes) => {
delete attributes.fields[input.fieldId]; delete attributes.fields[input.fieldId];
return attributes; return attributes;

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
FieldNameUpdateMutationInput, FieldNameUpdateMutationInput,
FieldNameUpdateMutationOutput, FieldNameUpdateMutationOutput,
} from '@/shared/mutations/databases/field-name-update'; } from '@/shared/mutations/databases/field-name-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FieldNameUpdateMutationHandler export class FieldNameUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldNameUpdateMutationInput> implements MutationHandler<FieldNameUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: FieldNameUpdateMutationInput input: FieldNameUpdateMutationInput
): Promise<FieldNameUpdateMutationOutput> { ): Promise<FieldNameUpdateMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const field = attributes.fields[input.fieldId]; const field = attributes.fields[input.fieldId];
if (!field) { if (!field) {

View File

@@ -6,24 +6,26 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
SelectOptionCreateMutationInput, SelectOptionCreateMutationInput,
SelectOptionCreateMutationOutput, SelectOptionCreateMutationOutput,
} from '@/shared/mutations/databases/select-option-create'; } from '@/shared/mutations/databases/select-option-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class SelectOptionCreateMutationHandler export class SelectOptionCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionCreateMutationInput> implements MutationHandler<SelectOptionCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: SelectOptionCreateMutationInput input: SelectOptionCreateMutationInput
): Promise<SelectOptionCreateMutationOutput> { ): Promise<SelectOptionCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const id = generateId(IdType.SelectOption); const id = generateId(IdType.SelectOption);
const result = await entryService.updateEntry<DatabaseAttributes>( const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const field = attributes.fields[input.fieldId]; const field = attributes.fields[input.fieldId];
if (!field) { if (!field) {

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
SelectOptionDeleteMutationInput, SelectOptionDeleteMutationInput,
SelectOptionDeleteMutationOutput, SelectOptionDeleteMutationOutput,
} from '@/shared/mutations/databases/select-option-delete'; } from '@/shared/mutations/databases/select-option-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class SelectOptionDeleteMutationHandler export class SelectOptionDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionDeleteMutationInput> implements MutationHandler<SelectOptionDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: SelectOptionDeleteMutationInput input: SelectOptionDeleteMutationInput
): Promise<SelectOptionDeleteMutationOutput> { ): Promise<SelectOptionDeleteMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const field = attributes.fields[input.fieldId]; const field = attributes.fields[input.fieldId];
if (!field) { if (!field) {

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
SelectOptionUpdateMutationInput, SelectOptionUpdateMutationInput,
SelectOptionUpdateMutationOutput, SelectOptionUpdateMutationOutput,
} from '@/shared/mutations/databases/select-option-update'; } from '@/shared/mutations/databases/select-option-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class SelectOptionUpdateMutationHandler export class SelectOptionUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionUpdateMutationInput> implements MutationHandler<SelectOptionUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: SelectOptionUpdateMutationInput input: SelectOptionUpdateMutationInput
): Promise<SelectOptionUpdateMutationOutput> { ): Promise<SelectOptionUpdateMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const field = attributes.fields[input.fieldId]; const field = attributes.fields[input.fieldId];
if (!field) { if (!field) {

View File

@@ -6,24 +6,26 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
ViewCreateMutationInput, ViewCreateMutationInput,
ViewCreateMutationOutput, ViewCreateMutationOutput,
} from '@/shared/mutations/databases/view-create'; } from '@/shared/mutations/databases/view-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewCreateMutationHandler export class ViewCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewCreateMutationInput> implements MutationHandler<ViewCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: ViewCreateMutationInput input: ViewCreateMutationInput
): Promise<ViewCreateMutationOutput> { ): Promise<ViewCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const id = generateId(IdType.View); const id = generateId(IdType.View);
const result = await entryService.updateEntry<DatabaseAttributes>( const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const maxIndex = Object.values(attributes.views) const maxIndex = Object.values(attributes.views)
.map((view) => view.index) .map((view) => view.index)

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
ViewDeleteMutationInput, ViewDeleteMutationInput,
ViewDeleteMutationOutput, ViewDeleteMutationOutput,
} from '@/shared/mutations/databases/view-delete'; } from '@/shared/mutations/databases/view-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewDeleteMutationHandler export class ViewDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewDeleteMutationInput> implements MutationHandler<ViewDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: ViewDeleteMutationInput input: ViewDeleteMutationInput
): Promise<ViewDeleteMutationOutput> { ): Promise<ViewDeleteMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
if (!attributes.views[input.viewId]) { if (!attributes.views[input.viewId]) {
throw new MutationError( throw new MutationError(

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
ViewNameUpdateMutationInput, ViewNameUpdateMutationInput,
ViewNameUpdateMutationOutput, ViewNameUpdateMutationOutput,
} from '@/shared/mutations/databases/view-name-update'; } from '@/shared/mutations/databases/view-name-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewNameUpdateMutationHandler export class ViewNameUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewNameUpdateMutationInput> implements MutationHandler<ViewNameUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: ViewNameUpdateMutationInput input: ViewNameUpdateMutationInput
): Promise<ViewNameUpdateMutationOutput> { ): Promise<ViewNameUpdateMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
const view = attributes.views[input.viewId]; const view = attributes.views[input.viewId];
if (!view) { if (!view) {

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core'; import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
ViewUpdateMutationInput, ViewUpdateMutationInput,
ViewUpdateMutationOutput, ViewUpdateMutationOutput,
} from '@/shared/mutations/databases/view-update'; } from '@/shared/mutations/databases/view-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewUpdateMutationHandler export class ViewUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewUpdateMutationInput> implements MutationHandler<ViewUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: ViewUpdateMutationInput input: ViewUpdateMutationInput
): Promise<ViewUpdateMutationOutput> { ): Promise<ViewUpdateMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId, input.databaseId,
input.userId,
(attributes) => { (attributes) => {
if (!attributes.views[input.view.id]) { if (!attributes.views[input.view.id]) {
throw new MutationError( throw new MutationError(

View File

@@ -1,22 +1,24 @@
import { set } from 'lodash-es'; import { set } from 'lodash-es';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
EntryCollaboratorCreateMutationInput, EntryCollaboratorCreateMutationInput,
EntryCollaboratorCreateMutationOutput, EntryCollaboratorCreateMutationOutput,
} from '@/shared/mutations/entries/entry-collaborator-create'; } from '@/shared/mutations/entries/entry-collaborator-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class EntryCollaboratorCreateMutationHandler export class EntryCollaboratorCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<EntryCollaboratorCreateMutationInput> implements MutationHandler<EntryCollaboratorCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: EntryCollaboratorCreateMutationInput input: EntryCollaboratorCreateMutationInput
): Promise<EntryCollaboratorCreateMutationOutput> { ): Promise<EntryCollaboratorCreateMutationOutput> {
const result = await entryService.updateEntry( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry(
input.entryId, input.entryId,
input.userId,
(attributes) => { (attributes) => {
for (const collaboratorId of input.collaboratorIds) { for (const collaboratorId of input.collaboratorIds) {
set(attributes, `collaborators.${collaboratorId}`, input.role); set(attributes, `collaborators.${collaboratorId}`, input.role);

View File

@@ -1,22 +1,24 @@
import { unset } from 'lodash-es'; import { unset } from 'lodash-es';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
EntryCollaboratorDeleteMutationInput, EntryCollaboratorDeleteMutationInput,
EntryCollaboratorDeleteMutationOutput, EntryCollaboratorDeleteMutationOutput,
} from '@/shared/mutations/entries/entry-collaborator-delete'; } from '@/shared/mutations/entries/entry-collaborator-delete';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class EntryCollaboratorDeleteMutationHandler export class EntryCollaboratorDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<EntryCollaboratorDeleteMutationInput> implements MutationHandler<EntryCollaboratorDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: EntryCollaboratorDeleteMutationInput input: EntryCollaboratorDeleteMutationInput
): Promise<EntryCollaboratorDeleteMutationOutput> { ): Promise<EntryCollaboratorDeleteMutationOutput> {
const result = await entryService.updateEntry( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry(
input.entryId, input.entryId,
input.userId,
(attributes) => { (attributes) => {
unset(attributes, `collaborators.${input.collaboratorId}`); unset(attributes, `collaborators.${input.collaboratorId}`);
return attributes; return attributes;

View File

@@ -1,22 +1,24 @@
import { set } from 'lodash-es'; import { set } from 'lodash-es';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
EntryCollaboratorUpdateMutationInput, EntryCollaboratorUpdateMutationInput,
EntryCollaboratorUpdateMutationOutput, EntryCollaboratorUpdateMutationOutput,
} from '@/shared/mutations/entries/entry-collaborator-update'; } from '@/shared/mutations/entries/entry-collaborator-update';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class EntryCollaboratorUpdateMutationHandler export class EntryCollaboratorUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<EntryCollaboratorUpdateMutationInput> implements MutationHandler<EntryCollaboratorUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: EntryCollaboratorUpdateMutationInput input: EntryCollaboratorUpdateMutationInput
): Promise<EntryCollaboratorUpdateMutationOutput> { ): Promise<EntryCollaboratorUpdateMutationOutput> {
const result = await entryService.updateEntry( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry(
input.entryId, input.entryId,
input.userId,
(attributes) => { (attributes) => {
set(attributes, `collaborators.${input.collaboratorId}`, input.role); set(attributes, `collaborators.${input.collaboratorId}`, input.role);
return attributes; return attributes;

View File

@@ -1,29 +1,24 @@
import { MarkEntryOpenedMutation, generateId, IdType } from '@colanode/core'; import { MarkEntryOpenedMutation, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
EntryMarkOpenedMutationInput, EntryMarkOpenedMutationInput,
EntryMarkOpenedMutationOutput, EntryMarkOpenedMutationOutput,
} from '@/shared/mutations/entries/entry-mark-opened'; } from '@/shared/mutations/entries/entry-mark-opened';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { mapEntryInteraction } from '@/main/utils'; import { fetchEntry, mapEntryInteraction } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class EntryMarkOpenedMutationHandler export class EntryMarkOpenedMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<EntryMarkOpenedMutationInput> implements MutationHandler<EntryMarkOpenedMutationInput>
{ {
async handleMutation( async handleMutation(
input: EntryMarkOpenedMutationInput input: EntryMarkOpenedMutationInput
): Promise<EntryMarkOpenedMutationOutput> { ): Promise<EntryMarkOpenedMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const entry = await workspaceDatabase const entry = await fetchEntry(workspace.database, input.entryId);
.selectFrom('entries')
.selectAll()
.where('id', '=', input.entryId)
.executeTakeFirst();
if (!entry) { if (!entry) {
return { return {
@@ -31,11 +26,11 @@ export class EntryMarkOpenedMutationHandler
}; };
} }
const existingInteraction = await workspaceDatabase const existingInteraction = await workspace.database
.selectFrom('entry_interactions') .selectFrom('entry_interactions')
.selectAll() .selectAll()
.where('entry_id', '=', input.entryId) .where('entry_id', '=', input.entryId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.executeTakeFirst(); .executeTakeFirst();
if (existingInteraction) { if (existingInteraction) {
@@ -55,7 +50,7 @@ export class EntryMarkOpenedMutationHandler
? existingInteraction.first_opened_at ? existingInteraction.first_opened_at
: lastOpenedAt; : lastOpenedAt;
const { createdInteraction, createdMutation } = await workspaceDatabase const { createdInteraction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdInteraction = await trx const createdInteraction = await trx
@@ -63,7 +58,7 @@ export class EntryMarkOpenedMutationHandler
.returningAll() .returningAll()
.values({ .values({
entry_id: input.entryId, entry_id: input.entryId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
last_opened_at: lastOpenedAt, last_opened_at: lastOpenedAt,
first_opened_at: firstOpenedAt, first_opened_at: firstOpenedAt,
version: 0n, version: 0n,
@@ -87,7 +82,7 @@ export class EntryMarkOpenedMutationHandler
type: 'mark_entry_opened', type: 'mark_entry_opened',
data: { data: {
entryId: input.entryId, entryId: input.entryId,
collaboratorId: input.userId, collaboratorId: workspace.userId,
openedAt: new Date().toISOString(), openedAt: new Date().toISOString(),
}, },
}; };
@@ -114,14 +109,12 @@ export class EntryMarkOpenedMutationHandler
throw new Error('Failed to create entry interaction'); throw new Error('Failed to create entry interaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'entry_interaction_updated', type: 'entry_interaction_updated',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
entryInteraction: mapEntryInteraction(createdInteraction), entryInteraction: mapEntryInteraction(createdInteraction),
}); });

View File

@@ -1,29 +1,24 @@
import { MarkEntrySeenMutation, generateId, IdType } from '@colanode/core'; import { MarkEntrySeenMutation, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
EntryMarkSeenMutationInput, EntryMarkSeenMutationInput,
EntryMarkSeenMutationOutput, EntryMarkSeenMutationOutput,
} from '@/shared/mutations/entries/entry-mark-seen'; } from '@/shared/mutations/entries/entry-mark-seen';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { mapEntryInteraction } from '@/main/utils'; import { fetchEntry, mapEntryInteraction } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class EntryMarkSeenMutationHandler export class EntryMarkSeenMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<EntryMarkSeenMutationInput> implements MutationHandler<EntryMarkSeenMutationInput>
{ {
async handleMutation( async handleMutation(
input: EntryMarkSeenMutationInput input: EntryMarkSeenMutationInput
): Promise<EntryMarkSeenMutationOutput> { ): Promise<EntryMarkSeenMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const entry = await workspaceDatabase const entry = await fetchEntry(workspace.database, input.entryId);
.selectFrom('entries')
.selectAll()
.where('id', '=', input.entryId)
.executeTakeFirst();
if (!entry) { if (!entry) {
return { return {
@@ -31,11 +26,11 @@ export class EntryMarkSeenMutationHandler
}; };
} }
const existingInteraction = await workspaceDatabase const existingInteraction = await workspace.database
.selectFrom('entry_interactions') .selectFrom('entry_interactions')
.selectAll() .selectAll()
.where('entry_id', '=', input.entryId) .where('entry_id', '=', input.entryId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.executeTakeFirst(); .executeTakeFirst();
if (existingInteraction) { if (existingInteraction) {
@@ -55,7 +50,7 @@ export class EntryMarkSeenMutationHandler
? existingInteraction.first_seen_at ? existingInteraction.first_seen_at
: lastSeenAt; : lastSeenAt;
const { createdInteraction, createdMutation } = await workspaceDatabase const { createdInteraction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdInteraction = await trx const createdInteraction = await trx
@@ -63,7 +58,7 @@ export class EntryMarkSeenMutationHandler
.returningAll() .returningAll()
.values({ .values({
entry_id: input.entryId, entry_id: input.entryId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
last_seen_at: lastSeenAt, last_seen_at: lastSeenAt,
first_seen_at: firstSeenAt, first_seen_at: firstSeenAt,
version: 0n, version: 0n,
@@ -87,7 +82,7 @@ export class EntryMarkSeenMutationHandler
type: 'mark_entry_seen', type: 'mark_entry_seen',
data: { data: {
entryId: input.entryId, entryId: input.entryId,
collaboratorId: input.userId, collaboratorId: workspace.userId,
seenAt: new Date().toISOString(), seenAt: new Date().toISOString(),
}, },
}; };
@@ -114,14 +109,12 @@ export class EntryMarkSeenMutationHandler
throw new Error('Failed to create entry interaction'); throw new Error('Failed to create entry interaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'entry_interaction_updated', type: 'entry_interaction_updated',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
entryInteraction: mapEntryInteraction(createdInteraction), entryInteraction: mapEntryInteraction(createdInteraction),
}); });

View File

@@ -6,32 +6,33 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { fileService } from '@/main/services/file-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FileCreateMutationInput, FileCreateMutationInput,
FileCreateMutationOutput, FileCreateMutationOutput,
} from '@/shared/mutations/files/file-create'; } from '@/shared/mutations/files/file-create';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { databaseService } from '@/main/data/database-service';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { import {
fetchEntry, fetchEntry,
fetchUser, fetchUser,
fetchUserStorageUsed, fetchUserStorageUsed,
getFileMetadata,
mapEntry, mapEntry,
mapFile, mapFile,
} from '@/main/utils'; } from '@/main/utils';
import { formatBytes } from '@/shared/lib/files'; import { formatBytes } from '@/shared/lib/files';
import { DownloadStatus, UploadStatus } from '@/shared/types/files'; import { DownloadStatus, UploadStatus } from '@/shared/types/files';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileCreateMutationHandler export class FileCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileCreateMutationInput> implements MutationHandler<FileCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileCreateMutationInput input: FileCreateMutationInput
): Promise<FileCreateMutationOutput> { ): Promise<FileCreateMutationOutput> {
const metadata = fileService.getFileMetadata(input.filePath); const metadata = getFileMetadata(input.filePath);
if (!metadata) { if (!metadata) {
throw new MutationError( throw new MutationError(
MutationErrorCode.FileInvalid, MutationErrorCode.FileInvalid,
@@ -39,11 +40,10 @@ export class FileCreateMutationHandler
); );
} }
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
); const user = await fetchUser(workspace.database, workspace.userId);
const user = await fetchUser(workspaceDatabase, input.userId);
if (!user) { if (!user) {
throw new MutationError( throw new MutationError(
MutationErrorCode.UserNotFound, MutationErrorCode.UserNotFound,
@@ -60,8 +60,8 @@ export class FileCreateMutationHandler
} }
const storageUsed = await fetchUserStorageUsed( const storageUsed = await fetchUserStorageUsed(
workspaceDatabase, workspace.database,
input.userId workspace.userId
); );
if (storageUsed + BigInt(metadata.size) > user.storage_limit) { if (storageUsed + BigInt(metadata.size) > user.storage_limit) {
@@ -76,7 +76,7 @@ export class FileCreateMutationHandler
); );
} }
const entry = await fetchEntry(workspaceDatabase, input.entryId); const entry = await fetchEntry(workspace.database, input.entryId);
if (!entry) { if (!entry) {
throw new MutationError( throw new MutationError(
MutationErrorCode.EntryNotFound, MutationErrorCode.EntryNotFound,
@@ -84,7 +84,7 @@ export class FileCreateMutationHandler
); );
} }
const root = await fetchEntry(workspaceDatabase, input.rootId); const root = await fetchEntry(workspace.database, input.rootId);
if (!root) { if (!root) {
throw new MutationError( throw new MutationError(
MutationErrorCode.RootNotFound, MutationErrorCode.RootNotFound,
@@ -96,8 +96,8 @@ export class FileCreateMutationHandler
if ( if (
!canCreateFile({ !canCreateFile({
user: { user: {
userId: input.userId, userId: workspace.userId,
role: user.role, role: workspace.role,
}, },
root: mapEntry(root), root: mapEntry(root),
entry: mapEntry(entry), entry: mapEntry(entry),
@@ -113,11 +113,10 @@ export class FileCreateMutationHandler
); );
} }
fileService.copyFileToWorkspace( workspace.files.copyFileToWorkspace(
input.filePath, input.filePath,
fileId, fileId,
metadata.extension, metadata.extension
input.userId
); );
const mutationData: CreateFileMutationData = { const mutationData: CreateFileMutationData = {
@@ -134,7 +133,7 @@ export class FileCreateMutationHandler
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}; };
const createdFile = await workspaceDatabase const createdFile = await workspace.database
.transaction() .transaction()
.execute(async (tx) => { .execute(async (tx) => {
const createdFile = await tx const createdFile = await tx
@@ -152,7 +151,7 @@ export class FileCreateMutationHandler
size: metadata.size, size: metadata.size,
extension: metadata.extension, extension: metadata.extension,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
created_by: input.userId, created_by: workspace.userId,
status: FileStatus.Pending, status: FileStatus.Pending,
version: 0n, version: 0n,
}) })
@@ -196,18 +195,17 @@ export class FileCreateMutationHandler
eventBus.publish({ eventBus.publish({
type: 'file_created', type: 'file_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
file: mapFile(createdFile), file: mapFile(createdFile),
}); });
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'file_state_created', type: 'file_state_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
fileState: { fileState: {
fileId: fileId, fileId: fileId,
downloadProgress: 100, downloadProgress: 100,

View File

@@ -5,27 +5,26 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FileDeleteMutationInput, FileDeleteMutationInput,
FileDeleteMutationOutput, FileDeleteMutationOutput,
} from '@/shared/mutations/files/file-delete'; } from '@/shared/mutations/files/file-delete';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { fetchEntry, fetchUser, mapEntry, mapFile } from '@/main/utils'; import { fetchEntry, mapEntry, mapFile } from '@/main/utils';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileDeleteMutationHandler export class FileDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileDeleteMutationInput> implements MutationHandler<FileDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileDeleteMutationInput input: FileDeleteMutationInput
): Promise<FileDeleteMutationOutput> { ): Promise<FileDeleteMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const file = await workspaceDatabase const file = await workspace.database
.selectFrom('files') .selectFrom('files')
.selectAll() .selectAll()
.where('id', '=', input.fileId) .where('id', '=', input.fileId)
@@ -38,15 +37,7 @@ export class FileDeleteMutationHandler
); );
} }
const user = await fetchUser(workspaceDatabase, input.userId); const entry = await fetchEntry(workspace.database, file.root_id);
if (!user) {
throw new MutationError(
MutationErrorCode.UserNotFound,
'There was an error while fetching the user. Please make sure you are logged in.'
);
}
const entry = await fetchEntry(workspaceDatabase, file.root_id);
if (!entry) { if (!entry) {
throw new MutationError( throw new MutationError(
MutationErrorCode.EntryNotFound, MutationErrorCode.EntryNotFound,
@@ -54,7 +45,7 @@ export class FileDeleteMutationHandler
); );
} }
const root = await fetchEntry(workspaceDatabase, entry.root_id); const root = await fetchEntry(workspace.database, entry.root_id);
if (!root) { if (!root) {
throw new MutationError( throw new MutationError(
MutationErrorCode.RootNotFound, MutationErrorCode.RootNotFound,
@@ -65,8 +56,8 @@ export class FileDeleteMutationHandler
if ( if (
!canDeleteFile({ !canDeleteFile({
user: { user: {
userId: input.userId, userId: workspace.userId,
role: user.role, role: workspace.role,
}, },
root: mapEntry(root), root: mapEntry(root),
entry: mapEntry(entry), entry: mapEntry(entry),
@@ -90,7 +81,7 @@ export class FileDeleteMutationHandler
deletedAt, deletedAt,
}; };
await workspaceDatabase.transaction().execute(async (tx) => { await workspace.database.transaction().execute(async (tx) => {
await tx await tx
.updateTable('files') .updateTable('files')
.set({ .set({
@@ -113,14 +104,12 @@ export class FileDeleteMutationHandler
eventBus.publish({ eventBus.publish({
type: 'file_deleted', type: 'file_deleted',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
file: mapFile(file), file: mapFile(file),
}); });
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
return { return {
success: true, success: true,

View File

@@ -1,6 +1,5 @@
import { FileStatus } from '@colanode/core'; import { FileStatus } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { mapFileState } from '@/main/utils'; import { mapFileState } from '@/main/utils';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
@@ -10,18 +9,18 @@ import {
FileDownloadMutationOutput, FileDownloadMutationOutput,
} from '@/shared/mutations/files/file-download'; } from '@/shared/mutations/files/file-download';
import { DownloadStatus, UploadStatus } from '@/shared/types/files'; import { DownloadStatus, UploadStatus } from '@/shared/types/files';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileDownloadMutationHandler export class FileDownloadMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileDownloadMutationInput> implements MutationHandler<FileDownloadMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileDownloadMutationInput input: FileDownloadMutationInput
): Promise<FileDownloadMutationOutput> { ): Promise<FileDownloadMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const file = await workspaceDatabase const file = await workspace.database
.selectFrom('files') .selectFrom('files')
.selectAll() .selectAll()
.where('id', '=', input.fileId) .where('id', '=', input.fileId)
@@ -41,7 +40,7 @@ export class FileDownloadMutationHandler
); );
} }
const existingFileState = await workspaceDatabase const existingFileState = await workspace.database
.selectFrom('file_states') .selectFrom('file_states')
.selectAll() .selectAll()
.where('file_id', '=', input.fileId) .where('file_id', '=', input.fileId)
@@ -56,7 +55,7 @@ export class FileDownloadMutationHandler
}; };
} }
const fileState = await workspaceDatabase const fileState = await workspace.database
.insertInto('file_states') .insertInto('file_states')
.returningAll() .returningAll()
.values({ .values({
@@ -85,7 +84,8 @@ export class FileDownloadMutationHandler
eventBus.publish({ eventBus.publish({
type: 'file_state_created', type: 'file_state_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
fileState: mapFileState(fileState), fileState: mapFileState(fileState),
}); });

View File

@@ -1,6 +1,5 @@
import { MarkFileOpenedMutation, generateId, IdType } from '@colanode/core'; import { MarkFileOpenedMutation, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FileMarkOpenedMutationInput, FileMarkOpenedMutationInput,
@@ -8,18 +7,18 @@ import {
} from '@/shared/mutations/files/file-mark-opened'; } from '@/shared/mutations/files/file-mark-opened';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { mapFileInteraction } from '@/main/utils'; import { mapFileInteraction } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileMarkOpenedMutationHandler export class FileMarkOpenedMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileMarkOpenedMutationInput> implements MutationHandler<FileMarkOpenedMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileMarkOpenedMutationInput input: FileMarkOpenedMutationInput
): Promise<FileMarkOpenedMutationOutput> { ): Promise<FileMarkOpenedMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const file = await workspaceDatabase const file = await workspace.database
.selectFrom('files') .selectFrom('files')
.selectAll() .selectAll()
.where('id', '=', input.fileId) .where('id', '=', input.fileId)
@@ -31,11 +30,11 @@ export class FileMarkOpenedMutationHandler
}; };
} }
const existingInteraction = await workspaceDatabase const existingInteraction = await workspace.database
.selectFrom('file_interactions') .selectFrom('file_interactions')
.selectAll() .selectAll()
.where('file_id', '=', input.fileId) .where('file_id', '=', input.fileId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.executeTakeFirst(); .executeTakeFirst();
if (existingInteraction) { if (existingInteraction) {
@@ -55,7 +54,7 @@ export class FileMarkOpenedMutationHandler
? existingInteraction.first_opened_at ? existingInteraction.first_opened_at
: lastOpenedAt; : lastOpenedAt;
const { createdInteraction, createdMutation } = await workspaceDatabase const { createdInteraction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdInteraction = await trx const createdInteraction = await trx
@@ -63,7 +62,7 @@ export class FileMarkOpenedMutationHandler
.returningAll() .returningAll()
.values({ .values({
file_id: input.fileId, file_id: input.fileId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
last_opened_at: lastOpenedAt, last_opened_at: lastOpenedAt,
first_opened_at: firstOpenedAt, first_opened_at: firstOpenedAt,
version: 0n, version: 0n,
@@ -87,7 +86,7 @@ export class FileMarkOpenedMutationHandler
type: 'mark_file_opened', type: 'mark_file_opened',
data: { data: {
fileId: input.fileId, fileId: input.fileId,
collaboratorId: input.userId, collaboratorId: workspace.userId,
openedAt: new Date().toISOString(), openedAt: new Date().toISOString(),
}, },
}; };
@@ -114,14 +113,12 @@ export class FileMarkOpenedMutationHandler
throw new Error('Failed to create file interaction'); throw new Error('Failed to create file interaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'file_interaction_updated', type: 'file_interaction_updated',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
fileInteraction: mapFileInteraction(createdInteraction), fileInteraction: mapFileInteraction(createdInteraction),
}); });

View File

@@ -1,6 +1,5 @@
import { MarkFileSeenMutation, generateId, IdType } from '@colanode/core'; import { MarkFileSeenMutation, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FileMarkSeenMutationInput, FileMarkSeenMutationInput,
@@ -8,18 +7,18 @@ import {
} from '@/shared/mutations/files/file-mark-seen'; } from '@/shared/mutations/files/file-mark-seen';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { mapFileInteraction } from '@/main/utils'; import { mapFileInteraction } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileMarkSeenMutationHandler export class FileMarkSeenMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileMarkSeenMutationInput> implements MutationHandler<FileMarkSeenMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileMarkSeenMutationInput input: FileMarkSeenMutationInput
): Promise<FileMarkSeenMutationOutput> { ): Promise<FileMarkSeenMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const file = await workspaceDatabase const file = await workspace.database
.selectFrom('files') .selectFrom('files')
.selectAll() .selectAll()
.where('id', '=', input.fileId) .where('id', '=', input.fileId)
@@ -31,11 +30,11 @@ export class FileMarkSeenMutationHandler
}; };
} }
const existingInteraction = await workspaceDatabase const existingInteraction = await workspace.database
.selectFrom('file_interactions') .selectFrom('file_interactions')
.selectAll() .selectAll()
.where('file_id', '=', input.fileId) .where('file_id', '=', input.fileId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.executeTakeFirst(); .executeTakeFirst();
if (existingInteraction) { if (existingInteraction) {
@@ -55,7 +54,7 @@ export class FileMarkSeenMutationHandler
? existingInteraction.first_seen_at ? existingInteraction.first_seen_at
: lastSeenAt; : lastSeenAt;
const { createdInteraction, createdMutation } = await workspaceDatabase const { createdInteraction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdInteraction = await trx const createdInteraction = await trx
@@ -63,7 +62,7 @@ export class FileMarkSeenMutationHandler
.returningAll() .returningAll()
.values({ .values({
file_id: input.fileId, file_id: input.fileId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
last_seen_at: lastSeenAt, last_seen_at: lastSeenAt,
first_seen_at: firstSeenAt, first_seen_at: firstSeenAt,
version: 0n, version: 0n,
@@ -87,7 +86,7 @@ export class FileMarkSeenMutationHandler
type: 'mark_file_seen', type: 'mark_file_seen',
data: { data: {
fileId: input.fileId, fileId: input.fileId,
collaboratorId: input.userId, collaboratorId: workspace.userId,
seenAt: new Date().toISOString(), seenAt: new Date().toISOString(),
}, },
}; };
@@ -114,14 +113,12 @@ export class FileMarkSeenMutationHandler
throw new Error('Failed to create file interaction'); throw new Error('Failed to create file interaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'file_interaction_updated', type: 'file_interaction_updated',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
fileInteraction: mapFileInteraction(createdInteraction), fileInteraction: mapFileInteraction(createdInteraction),
}); });

View File

@@ -7,14 +7,19 @@ import {
FileSaveTempMutationOutput, FileSaveTempMutationOutput,
} from '@/shared/mutations/files/file-save-temp'; } from '@/shared/mutations/files/file-save-temp';
import { getWorkspaceTempFilesDirectoryPath } from '@/main/utils'; import { getWorkspaceTempFilesDirectoryPath } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileSaveTempMutationHandler export class FileSaveTempMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileSaveTempMutationInput> implements MutationHandler<FileSaveTempMutationInput>
{ {
async handleMutation( async handleMutation(
input: FileSaveTempMutationInput input: FileSaveTempMutationInput
): Promise<FileSaveTempMutationOutput> { ): Promise<FileSaveTempMutationOutput> {
const directoryPath = getWorkspaceTempFilesDirectoryPath(input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const directoryPath = getWorkspaceTempFilesDirectoryPath(
workspace.accountId,
workspace.id
);
const fileName = this.generateUniqueName(directoryPath, input.name); const fileName = this.generateUniqueName(directoryPath, input.name);
const filePath = path.join(directoryPath, fileName); const filePath = path.join(directoryPath, fileName);

View File

@@ -1,18 +1,21 @@
import { FolderAttributes, generateId, IdType } from '@colanode/core'; import { FolderAttributes, generateId, IdType } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FolderCreateMutationInput, FolderCreateMutationInput,
FolderCreateMutationOutput, FolderCreateMutationOutput,
} from '@/shared/mutations/folders/folder-create'; } from '@/shared/mutations/folders/folder-create';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FolderCreateMutationHandler export class FolderCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FolderCreateMutationInput> implements MutationHandler<FolderCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: FolderCreateMutationInput input: FolderCreateMutationInput
): Promise<FolderCreateMutationOutput> { ): Promise<FolderCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const id = generateId(IdType.Folder); const id = generateId(IdType.Folder);
const attributes: FolderAttributes = { const attributes: FolderAttributes = {
type: 'folder', type: 'folder',
@@ -21,7 +24,7 @@ export class FolderCreateMutationHandler
avatar: input.avatar, avatar: input.avatar,
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id, id,
attributes, attributes,
parentId: input.parentId, parentId: input.parentId,

View File

@@ -1,17 +1,20 @@
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
FolderDeleteMutationInput, FolderDeleteMutationInput,
FolderDeleteMutationOutput, FolderDeleteMutationOutput,
} from '@/shared/mutations/folders/folder-delete'; } from '@/shared/mutations/folders/folder-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FolderDeleteMutationHandler export class FolderDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FolderDeleteMutationInput> implements MutationHandler<FolderDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: FolderDeleteMutationInput input: FolderDeleteMutationInput
): Promise<FolderDeleteMutationOutput> { ): Promise<FolderDeleteMutationOutput> {
await entryService.deleteEntry(input.folderId, input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
await workspace.entries.deleteEntry(input.folderId);
return { return {
success: true, success: true,

View File

@@ -1,22 +1,24 @@
import { FolderAttributes } from '@colanode/core'; import { FolderAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
FolderUpdateMutationInput, FolderUpdateMutationInput,
FolderUpdateMutationOutput, FolderUpdateMutationOutput,
} from '@/shared/mutations/folders/folder-update'; } from '@/shared/mutations/folders/folder-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FolderUpdateMutationHandler export class FolderUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FolderUpdateMutationInput> implements MutationHandler<FolderUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: FolderUpdateMutationInput input: FolderUpdateMutationInput
): Promise<FolderUpdateMutationOutput> { ): Promise<FolderUpdateMutationOutput> {
const result = await entryService.updateEntry<FolderAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<FolderAttributes>(
input.folderId, input.folderId,
input.userId,
(attributes) => { (attributes) => {
attributes.name = input.name; attributes.name = input.name;
attributes.avatar = input.avatar; attributes.avatar = input.avatar;

View File

@@ -11,7 +11,6 @@ import {
MessageType, MessageType,
} from '@colanode/core'; } from '@colanode/core';
import { fileService } from '@/main/services/file-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { mapContentsToBlocks } from '@/shared/lib/editor'; import { mapContentsToBlocks } from '@/shared/lib/editor';
import { import {
@@ -23,13 +22,13 @@ import {
CreateFile, CreateFile,
CreateFileState, CreateFileState,
CreateMutation, CreateMutation,
} from '@/main/data/workspace/schema'; } from '@/main/databases/workspace';
import { databaseService } from '@/main/data/database-service';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { import {
fetchEntry, fetchEntry,
fetchUser, fetchUser,
fetchUserStorageUsed, fetchUserStorageUsed,
getFileMetadata,
mapEntry, mapEntry,
mapFile, mapFile,
mapFileState, mapFileState,
@@ -37,18 +36,18 @@ import {
} from '@/main/utils'; } from '@/main/utils';
import { formatBytes } from '@/shared/lib/files'; import { formatBytes } from '@/shared/lib/files';
import { DownloadStatus, UploadStatus } from '@/shared/types/files'; import { DownloadStatus, UploadStatus } from '@/shared/types/files';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class MessageCreateMutationHandler export class MessageCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<MessageCreateMutationInput> implements MutationHandler<MessageCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: MessageCreateMutationInput input: MessageCreateMutationInput
): Promise<MessageCreateMutationOutput> { ): Promise<MessageCreateMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const user = await fetchUser(workspaceDatabase, input.userId); const user = await fetchUser(workspace.database, workspace.userId);
if (!user) { if (!user) {
throw new MutationError( throw new MutationError(
MutationErrorCode.UserNotFound, MutationErrorCode.UserNotFound,
@@ -56,7 +55,7 @@ export class MessageCreateMutationHandler
); );
} }
const entry = await fetchEntry(workspaceDatabase, input.conversationId); const entry = await fetchEntry(workspace.database, input.conversationId);
if (!entry) { if (!entry) {
throw new MutationError( throw new MutationError(
MutationErrorCode.EntryNotFound, MutationErrorCode.EntryNotFound,
@@ -64,7 +63,7 @@ export class MessageCreateMutationHandler
); );
} }
const root = await fetchEntry(workspaceDatabase, input.rootId); const root = await fetchEntry(workspace.database, input.rootId);
if (!root) { if (!root) {
throw new MutationError( throw new MutationError(
MutationErrorCode.RootNotFound, MutationErrorCode.RootNotFound,
@@ -75,7 +74,7 @@ export class MessageCreateMutationHandler
if ( if (
!canCreateMessage({ !canCreateMessage({
user: { user: {
userId: input.userId, userId: workspace.userId,
role: user.role, role: user.role,
}, },
root: mapEntry(root), root: mapEntry(root),
@@ -101,7 +100,7 @@ export class MessageCreateMutationHandler
for (const block of blocks) { for (const block of blocks) {
if (block.type === EditorNodeTypes.FilePlaceholder) { if (block.type === EditorNodeTypes.FilePlaceholder) {
const path = block.attrs?.path; const path = block.attrs?.path;
const metadata = fileService.getFileMetadata(path); const metadata = getFileMetadata(path);
if (!metadata) { if (!metadata) {
throw new MutationError( throw new MutationError(
MutationErrorCode.FileInvalid, MutationErrorCode.FileInvalid,
@@ -122,12 +121,7 @@ export class MessageCreateMutationHandler
block.type = 'file'; block.type = 'file';
block.attrs = null; block.attrs = null;
fileService.copyFileToWorkspace( workspace.files.copyFileToWorkspace(path, fileId, metadata.extension);
path,
fileId,
metadata.extension,
input.userId
);
files.push({ files.push({
id: fileId, id: fileId,
@@ -142,7 +136,7 @@ export class MessageCreateMutationHandler
extension: metadata.extension, extension: metadata.extension,
size: metadata.size, size: metadata.size,
created_at: createdAt, created_at: createdAt,
created_by: input.userId, created_by: workspace.userId,
version: 0n, version: 0n,
}); });
@@ -183,8 +177,8 @@ export class MessageCreateMutationHandler
if (files.length > 0) { if (files.length > 0) {
const storageUsed = await fetchUserStorageUsed( const storageUsed = await fetchUserStorageUsed(
workspaceDatabase, workspace.database,
input.userId workspace.userId
); );
const fileSizeSum = BigInt( const fileSizeSum = BigInt(
@@ -193,7 +187,7 @@ export class MessageCreateMutationHandler
if (storageUsed + fileSizeSum > user.storage_limit) { if (storageUsed + fileSizeSum > user.storage_limit) {
for (const file of files) { for (const file of files) {
fileService.deleteFile(input.userId, file.id, file.extension); workspace.files.deleteFile(file.id, file.extension);
} }
throw new MutationError( throw new MutationError(
@@ -223,7 +217,7 @@ export class MessageCreateMutationHandler
}; };
const { createdMessage, createdFiles, createdFileStates } = const { createdMessage, createdFiles, createdFileStates } =
await workspaceDatabase.transaction().execute(async (tx) => { await workspace.database.transaction().execute(async (tx) => {
const createdMessage = await tx const createdMessage = await tx
.insertInto('messages') .insertInto('messages')
.returningAll() .returningAll()
@@ -234,7 +228,7 @@ export class MessageCreateMutationHandler
root_id: input.rootId, root_id: input.rootId,
attributes: JSON.stringify(messageAttributes), attributes: JSON.stringify(messageAttributes),
created_at: createdAt, created_at: createdAt,
created_by: input.userId, created_by: workspace.userId,
version: 0n, version: 0n,
}) })
.executeTakeFirst(); .executeTakeFirst();
@@ -301,7 +295,8 @@ export class MessageCreateMutationHandler
if (createdMessage) { if (createdMessage) {
eventBus.publish({ eventBus.publish({
type: 'message_created', type: 'message_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
message: mapMessage(createdMessage), message: mapMessage(createdMessage),
}); });
} }
@@ -310,7 +305,8 @@ export class MessageCreateMutationHandler
for (const file of createdFiles) { for (const file of createdFiles) {
eventBus.publish({ eventBus.publish({
type: 'file_created', type: 'file_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
file: mapFile(file), file: mapFile(file),
}); });
} }
@@ -320,16 +316,14 @@ export class MessageCreateMutationHandler
for (const fileState of createdFileStates) { for (const fileState of createdFileStates) {
eventBus.publish({ eventBus.publish({
type: 'file_state_created', type: 'file_state_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
fileState: mapFileState(fileState), fileState: mapFileState(fileState),
}); });
} }
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
return { return {
id: messageId, id: messageId,

View File

@@ -5,27 +5,25 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
MessageDeleteMutationInput, MessageDeleteMutationInput,
MessageDeleteMutationOutput, MessageDeleteMutationOutput,
} from '@/shared/mutations/messages/message-delete'; } from '@/shared/mutations/messages/message-delete';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { fetchEntry, fetchUser, mapEntry, mapMessage } from '@/main/utils'; import { fetchEntry, mapEntry, mapMessage } from '@/main/utils';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class MessageDeleteMutationHandler export class MessageDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<MessageDeleteMutationInput> implements MutationHandler<MessageDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: MessageDeleteMutationInput input: MessageDeleteMutationInput
): Promise<MessageDeleteMutationOutput> { ): Promise<MessageDeleteMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const message = await workspaceDatabase const message = await workspace.database
.selectFrom('messages') .selectFrom('messages')
.selectAll() .selectAll()
.where('id', '=', input.messageId) .where('id', '=', input.messageId)
@@ -37,15 +35,7 @@ export class MessageDeleteMutationHandler
}; };
} }
const user = await fetchUser(workspaceDatabase, input.userId); const entry = await fetchEntry(workspace.database, message.entry_id);
if (!user) {
throw new MutationError(
MutationErrorCode.UserNotFound,
'There was an error while fetching the user. Please make sure you are logged in.'
);
}
const entry = await fetchEntry(workspaceDatabase, message.entry_id);
if (!entry) { if (!entry) {
throw new MutationError( throw new MutationError(
MutationErrorCode.EntryNotFound, MutationErrorCode.EntryNotFound,
@@ -53,7 +43,7 @@ export class MessageDeleteMutationHandler
); );
} }
const root = await fetchEntry(workspaceDatabase, message.root_id); const root = await fetchEntry(workspace.database, message.root_id);
if (!root) { if (!root) {
throw new MutationError( throw new MutationError(
MutationErrorCode.RootNotFound, MutationErrorCode.RootNotFound,
@@ -64,8 +54,8 @@ export class MessageDeleteMutationHandler
if ( if (
!canDeleteMessage({ !canDeleteMessage({
user: { user: {
userId: input.userId, userId: workspace.userId,
role: user.role, role: workspace.role,
}, },
root: mapEntry(root), root: mapEntry(root),
entry: mapEntry(entry), entry: mapEntry(entry),
@@ -88,7 +78,7 @@ export class MessageDeleteMutationHandler
deletedAt, deletedAt,
}; };
await workspaceDatabase.transaction().execute(async (tx) => { await workspace.database.transaction().execute(async (tx) => {
await tx await tx
.updateTable('messages') .updateTable('messages')
.set({ .set({
@@ -111,14 +101,12 @@ export class MessageDeleteMutationHandler
eventBus.publish({ eventBus.publish({
type: 'message_deleted', type: 'message_deleted',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
message: mapMessage(message), message: mapMessage(message),
}); });
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
return { return {
success: true, success: true,

View File

@@ -1,6 +1,5 @@
import { MarkMessageSeenMutation, generateId, IdType } from '@colanode/core'; import { MarkMessageSeenMutation, generateId, IdType } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
MessageMarkSeenMutationInput, MessageMarkSeenMutationInput,
@@ -8,18 +7,18 @@ import {
} from '@/shared/mutations/messages/message-mark-seen'; } from '@/shared/mutations/messages/message-mark-seen';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { mapMessageInteraction } from '@/main/utils'; import { mapMessageInteraction } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class MessageMarkSeenMutationHandler export class MessageMarkSeenMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<MessageMarkSeenMutationInput> implements MutationHandler<MessageMarkSeenMutationInput>
{ {
async handleMutation( async handleMutation(
input: MessageMarkSeenMutationInput input: MessageMarkSeenMutationInput
): Promise<MessageMarkSeenMutationOutput> { ): Promise<MessageMarkSeenMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const message = await workspaceDatabase const message = await workspace.database
.selectFrom('messages') .selectFrom('messages')
.selectAll() .selectAll()
.where('id', '=', input.messageId) .where('id', '=', input.messageId)
@@ -31,11 +30,11 @@ export class MessageMarkSeenMutationHandler
}; };
} }
const existingInteraction = await workspaceDatabase const existingInteraction = await workspace.database
.selectFrom('message_interactions') .selectFrom('message_interactions')
.selectAll() .selectAll()
.where('message_id', '=', input.messageId) .where('message_id', '=', input.messageId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.executeTakeFirst(); .executeTakeFirst();
if (existingInteraction) { if (existingInteraction) {
@@ -55,7 +54,7 @@ export class MessageMarkSeenMutationHandler
? existingInteraction.first_seen_at ? existingInteraction.first_seen_at
: lastSeenAt; : lastSeenAt;
const { createdInteraction, createdMutation } = await workspaceDatabase const { createdInteraction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdInteraction = await trx const createdInteraction = await trx
@@ -63,7 +62,7 @@ export class MessageMarkSeenMutationHandler
.returningAll() .returningAll()
.values({ .values({
message_id: input.messageId, message_id: input.messageId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
first_seen_at: firstSeenAt, first_seen_at: firstSeenAt,
last_seen_at: lastSeenAt, last_seen_at: lastSeenAt,
version: 0n, version: 0n,
@@ -87,7 +86,7 @@ export class MessageMarkSeenMutationHandler
type: 'mark_message_seen', type: 'mark_message_seen',
data: { data: {
messageId: input.messageId, messageId: input.messageId,
collaboratorId: input.userId, collaboratorId: workspace.userId,
seenAt: new Date().toISOString(), seenAt: new Date().toISOString(),
}, },
}; };
@@ -114,14 +113,12 @@ export class MessageMarkSeenMutationHandler
throw new Error('Failed to create message interaction'); throw new Error('Failed to create message interaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'message_interaction_updated', type: 'message_interaction_updated',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
messageInteraction: mapMessageInteraction(createdInteraction), messageInteraction: mapMessageInteraction(createdInteraction),
}); });

View File

@@ -5,32 +5,26 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
MessageReactionCreateMutationInput, MessageReactionCreateMutationInput,
MessageReactionCreateMutationOutput, MessageReactionCreateMutationOutput,
} from '@/shared/mutations/messages/message-reaction-create'; } from '@/shared/mutations/messages/message-reaction-create';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { import { fetchEntry, mapEntry, mapMessageReaction } from '@/main/utils';
fetchEntry,
fetchUser,
mapEntry,
mapMessageReaction,
} from '@/main/utils';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class MessageReactionCreateMutationHandler export class MessageReactionCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<MessageReactionCreateMutationInput> implements MutationHandler<MessageReactionCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: MessageReactionCreateMutationInput input: MessageReactionCreateMutationInput
): Promise<MessageReactionCreateMutationOutput> { ): Promise<MessageReactionCreateMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const message = await workspaceDatabase const message = await workspace.database
.selectFrom('messages') .selectFrom('messages')
.selectAll() .selectAll()
.where('id', '=', input.messageId) .where('id', '=', input.messageId)
@@ -43,11 +37,11 @@ export class MessageReactionCreateMutationHandler
); );
} }
const existingMessageReaction = await workspaceDatabase const existingMessageReaction = await workspace.database
.selectFrom('message_reactions') .selectFrom('message_reactions')
.selectAll() .selectAll()
.where('message_id', '=', input.messageId) .where('message_id', '=', input.messageId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.where('reaction', '=', input.reaction) .where('reaction', '=', input.reaction)
.executeTakeFirst(); .executeTakeFirst();
@@ -57,15 +51,7 @@ export class MessageReactionCreateMutationHandler
}; };
} }
const user = await fetchUser(workspaceDatabase, input.userId); const root = await fetchEntry(workspace.database, input.rootId);
if (!user) {
throw new MutationError(
MutationErrorCode.UserNotFound,
'There was an error while fetching the user. Please make sure you are logged in.'
);
}
const root = await fetchEntry(workspaceDatabase, input.rootId);
if (!root) { if (!root) {
throw new MutationError( throw new MutationError(
MutationErrorCode.RootNotFound, MutationErrorCode.RootNotFound,
@@ -76,8 +62,8 @@ export class MessageReactionCreateMutationHandler
if ( if (
!canCreateMessageReaction({ !canCreateMessageReaction({
user: { user: {
userId: input.userId, userId: workspace.userId,
role: user.role, role: workspace.role,
}, },
root: mapEntry(root), root: mapEntry(root),
message: { message: {
@@ -92,7 +78,7 @@ export class MessageReactionCreateMutationHandler
); );
} }
const { createdMessageReaction, createdMutation } = await workspaceDatabase const { createdMessageReaction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const createdMessageReaction = await trx const createdMessageReaction = await trx
@@ -100,7 +86,7 @@ export class MessageReactionCreateMutationHandler
.returningAll() .returningAll()
.values({ .values({
message_id: input.messageId, message_id: input.messageId,
collaborator_id: input.userId, collaborator_id: workspace.userId,
reaction: input.reaction, reaction: input.reaction,
root_id: input.rootId, root_id: input.rootId,
version: 0n, version: 0n,
@@ -153,14 +139,12 @@ export class MessageReactionCreateMutationHandler
throw new Error('Failed to create message reaction'); throw new Error('Failed to create message reaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'message_reaction_created', type: 'message_reaction_created',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
messageReaction: mapMessageReaction(createdMessageReaction), messageReaction: mapMessageReaction(createdMessageReaction),
}); });

View File

@@ -4,7 +4,6 @@ import {
IdType, IdType,
} from '@colanode/core'; } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
MessageReactionDeleteMutationInput, MessageReactionDeleteMutationInput,
@@ -12,22 +11,22 @@ import {
} from '@/shared/mutations/messages/message-reaction-delete'; } from '@/shared/mutations/messages/message-reaction-delete';
import { mapMessageReaction } from '@/main/utils'; import { mapMessageReaction } from '@/main/utils';
import { eventBus } from '@/shared/lib/event-bus'; import { eventBus } from '@/shared/lib/event-bus';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class MessageReactionDeleteMutationHandler export class MessageReactionDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<MessageReactionDeleteMutationInput> implements MutationHandler<MessageReactionDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: MessageReactionDeleteMutationInput input: MessageReactionDeleteMutationInput
): Promise<MessageReactionDeleteMutationOutput> { ): Promise<MessageReactionDeleteMutationOutput> {
const workspaceDatabase = await databaseService.getWorkspaceDatabase( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
input.userId
);
const existingMessageReaction = await workspaceDatabase const existingMessageReaction = await workspace.database
.selectFrom('message_reactions') .selectFrom('message_reactions')
.selectAll() .selectAll()
.where('message_id', '=', input.messageId) .where('message_id', '=', input.messageId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.where('reaction', '=', input.reaction) .where('reaction', '=', input.reaction)
.executeTakeFirst(); .executeTakeFirst();
@@ -37,7 +36,7 @@ export class MessageReactionDeleteMutationHandler
}; };
} }
const { deletedMessageReaction, createdMutation } = await workspaceDatabase const { deletedMessageReaction, createdMutation } = await workspace.database
.transaction() .transaction()
.execute(async (trx) => { .execute(async (trx) => {
const deletedMessageReaction = await trx const deletedMessageReaction = await trx
@@ -47,7 +46,7 @@ export class MessageReactionDeleteMutationHandler
deleted_at: new Date().toISOString(), deleted_at: new Date().toISOString(),
}) })
.where('message_id', '=', input.messageId) .where('message_id', '=', input.messageId)
.where('collaborator_id', '=', input.userId) .where('collaborator_id', '=', workspace.userId)
.where('reaction', '=', input.reaction) .where('reaction', '=', input.reaction)
.executeTakeFirst(); .executeTakeFirst();
@@ -89,14 +88,12 @@ export class MessageReactionDeleteMutationHandler
throw new Error('Failed to delete message reaction'); throw new Error('Failed to delete message reaction');
} }
eventBus.publish({ workspace.mutations.triggerSync();
type: 'mutation_created',
userId: input.userId,
});
eventBus.publish({ eventBus.publish({
type: 'message_reaction_deleted', type: 'message_reaction_deleted',
userId: input.userId, accountId: workspace.accountId,
workspaceId: workspace.id,
messageReaction: mapMessageReaction(deletedMessageReaction), messageReaction: mapMessageReaction(deletedMessageReaction),
}); });

View File

@@ -1,7 +1,6 @@
import { PageAttributes } from '@colanode/core'; import { PageAttributes } from '@colanode/core';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { mapContentsToBlocks } from '@/shared/lib/editor'; import { mapContentsToBlocks } from '@/shared/lib/editor';
import { import {
@@ -9,16 +8,19 @@ import {
PageContentUpdateMutationOutput, PageContentUpdateMutationOutput,
} from '@/shared/mutations/pages/page-content-update'; } from '@/shared/mutations/pages/page-content-update';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class PageContentUpdateMutationHandler export class PageContentUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageContentUpdateMutationInput> implements MutationHandler<PageContentUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: PageContentUpdateMutationInput input: PageContentUpdateMutationInput
): Promise<PageContentUpdateMutationOutput> { ): Promise<PageContentUpdateMutationOutput> {
const result = await entryService.updateEntry<PageAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<PageAttributes>(
input.pageId, input.pageId,
input.userId,
(attributes) => { (attributes) => {
const indexMap = new Map<string, string>(); const indexMap = new Map<string, string>();
if (attributes.content) { if (attributes.content) {

View File

@@ -1,18 +1,21 @@
import { generateId, IdType, PageAttributes } from '@colanode/core'; import { generateId, IdType, PageAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
PageCreateMutationInput, PageCreateMutationInput,
PageCreateMutationOutput, PageCreateMutationOutput,
} from '@/shared/mutations/pages/page-create'; } from '@/shared/mutations/pages/page-create';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class PageCreateMutationHandler export class PageCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageCreateMutationInput> implements MutationHandler<PageCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: PageCreateMutationInput input: PageCreateMutationInput
): Promise<PageCreateMutationOutput> { ): Promise<PageCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const id = generateId(IdType.Page); const id = generateId(IdType.Page);
const attributes: PageAttributes = { const attributes: PageAttributes = {
type: 'page', type: 'page',
@@ -21,7 +24,7 @@ export class PageCreateMutationHandler
name: input.name, name: input.name,
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id, id,
attributes, attributes,
parentId: input.parentId, parentId: input.parentId,

View File

@@ -1,17 +1,20 @@
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { import {
PageDeleteMutationInput, PageDeleteMutationInput,
PageDeleteMutationOutput, PageDeleteMutationOutput,
} from '@/shared/mutations/pages/page-delete'; } from '@/shared/mutations/pages/page-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@/main/types';
export class PageDeleteMutationHandler export class PageDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageDeleteMutationInput> implements MutationHandler<PageDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: PageDeleteMutationInput input: PageDeleteMutationInput
): Promise<PageDeleteMutationOutput> { ): Promise<PageDeleteMutationOutput> {
await entryService.deleteEntry(input.pageId, input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
await workspace.entries.deleteEntry(input.pageId);
return { return {
success: true, success: true,

View File

@@ -1,22 +1,24 @@
import { PageAttributes } from '@colanode/core'; import { PageAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
PageUpdateMutationInput, PageUpdateMutationInput,
PageUpdateMutationOutput, PageUpdateMutationOutput,
} from '@/shared/mutations/pages/page-update'; } from '@/shared/mutations/pages/page-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class PageUpdateMutationHandler export class PageUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageUpdateMutationInput> implements MutationHandler<PageUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: PageUpdateMutationInput input: PageUpdateMutationInput
): Promise<PageUpdateMutationOutput> { ): Promise<PageUpdateMutationOutput> {
const result = await entryService.updateEntry<PageAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<PageAttributes>(
input.pageId, input.pageId,
input.userId,
(attributes) => { (attributes) => {
attributes.name = input.name; attributes.name = input.name;
attributes.avatar = input.avatar; attributes.avatar = input.avatar;

View File

@@ -1,22 +1,24 @@
import { RecordAttributes } from '@colanode/core'; import { RecordAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
RecordAvatarUpdateMutationInput, RecordAvatarUpdateMutationInput,
RecordAvatarUpdateMutationOutput, RecordAvatarUpdateMutationOutput,
} from '@/shared/mutations/records/record-avatar-update'; } from '@/shared/mutations/records/record-avatar-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordAvatarUpdateMutationHandler export class RecordAvatarUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordAvatarUpdateMutationInput> implements MutationHandler<RecordAvatarUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: RecordAvatarUpdateMutationInput input: RecordAvatarUpdateMutationInput
): Promise<RecordAvatarUpdateMutationOutput> { ): Promise<RecordAvatarUpdateMutationOutput> {
const result = await entryService.updateEntry<RecordAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<RecordAttributes>(
input.recordId, input.recordId,
input.userId,
(attributes) => { (attributes) => {
attributes.avatar = input.avatar; attributes.avatar = input.avatar;
return attributes; return attributes;

View File

@@ -1,7 +1,6 @@
import { RecordAttributes } from '@colanode/core'; import { RecordAttributes } from '@colanode/core';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { mapContentsToBlocks } from '@/shared/lib/editor'; import { mapContentsToBlocks } from '@/shared/lib/editor';
import { import {
@@ -9,16 +8,19 @@ import {
RecordContentUpdateMutationOutput, RecordContentUpdateMutationOutput,
} from '@/shared/mutations/records/record-content-update'; } from '@/shared/mutations/records/record-content-update';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordContentUpdateMutationHandler export class RecordContentUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordContentUpdateMutationInput> implements MutationHandler<RecordContentUpdateMutationInput>
{ {
async handleMutation( async handleMutation(
input: RecordContentUpdateMutationInput input: RecordContentUpdateMutationInput
): Promise<RecordContentUpdateMutationOutput> { ): Promise<RecordContentUpdateMutationOutput> {
const result = await entryService.updateEntry<RecordAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<RecordAttributes>(
input.recordId, input.recordId,
input.userId,
(attributes) => { (attributes) => {
const indexMap = new Map<string, string>(); const indexMap = new Map<string, string>();
if (attributes.content) { if (attributes.content) {

View File

@@ -1,18 +1,21 @@
import { generateId, IdType, RecordAttributes } from '@colanode/core'; import { generateId, IdType, RecordAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
RecordCreateMutationInput, RecordCreateMutationInput,
RecordCreateMutationOutput, RecordCreateMutationOutput,
} from '@/shared/mutations/records/record-create'; } from '@/shared/mutations/records/record-create';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordCreateMutationHandler export class RecordCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordCreateMutationInput> implements MutationHandler<RecordCreateMutationInput>
{ {
async handleMutation( async handleMutation(
input: RecordCreateMutationInput input: RecordCreateMutationInput
): Promise<RecordCreateMutationOutput> { ): Promise<RecordCreateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const id = generateId(IdType.Record); const id = generateId(IdType.Record);
const attributes: RecordAttributes = { const attributes: RecordAttributes = {
type: 'record', type: 'record',
@@ -22,7 +25,7 @@ export class RecordCreateMutationHandler
fields: input.fields ?? {}, fields: input.fields ?? {},
}; };
await entryService.createEntry(input.userId, { await workspace.entries.createEntry({
id, id,
attributes, attributes,
parentId: input.databaseId, parentId: input.databaseId,

View File

@@ -1,17 +1,20 @@
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { import {
RecordDeleteMutationInput, RecordDeleteMutationInput,
RecordDeleteMutationOutput, RecordDeleteMutationOutput,
} from '@/shared/mutations/records/record-delete'; } from '@/shared/mutations/records/record-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordDeleteMutationHandler export class RecordDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordDeleteMutationInput> implements MutationHandler<RecordDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: RecordDeleteMutationInput input: RecordDeleteMutationInput
): Promise<RecordDeleteMutationOutput> { ): Promise<RecordDeleteMutationOutput> {
await entryService.deleteEntry(input.recordId, input.userId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
await workspace.entries.deleteEntry(input.recordId);
return { return {
success: true, success: true,

View File

@@ -1,22 +1,24 @@
import { RecordAttributes } from '@colanode/core'; import { RecordAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types'; import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations'; import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { import {
RecordFieldValueDeleteMutationInput, RecordFieldValueDeleteMutationInput,
RecordFieldValueDeleteMutationOutput, RecordFieldValueDeleteMutationOutput,
} from '@/shared/mutations/records/record-field-value-delete'; } from '@/shared/mutations/records/record-field-value-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordFieldValueDeleteMutationHandler export class RecordFieldValueDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordFieldValueDeleteMutationInput> implements MutationHandler<RecordFieldValueDeleteMutationInput>
{ {
async handleMutation( async handleMutation(
input: RecordFieldValueDeleteMutationInput input: RecordFieldValueDeleteMutationInput
): Promise<RecordFieldValueDeleteMutationOutput> { ): Promise<RecordFieldValueDeleteMutationOutput> {
const result = await entryService.updateEntry<RecordAttributes>( const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<RecordAttributes>(
input.recordId, input.recordId,
input.userId,
(attributes) => { (attributes) => {
delete attributes.fields[input.fieldId]; delete attributes.fields[input.fieldId];
return attributes; return attributes;

Some files were not shown because too many files have changed in this diff Show More