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 path from 'path';
import { metadataService } from '@/main/services/metadata-service';
import { notificationService } from '@/main/services/notification-service';
import { WindowSize } from '@/shared/types/metadata';
import { scheduler } from '@/main/scheduler';
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 { mediator } from '@/main/mediator';
import { getAppIconPath } from '@/main/utils';
import { CommandInput, CommandMap } from '@/shared/commands';
import { eventBus } from '@/shared/lib/event-bus';
import { MutationInput, MutationMap } from '@/shared/mutations';
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');
@@ -42,11 +41,10 @@ updateElectronApp({
});
const createWindow = async () => {
await scheduler.init();
notificationService.checkBadge();
await appService.migrate();
// 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({
width: windowSize?.width ?? 1200,
height: windowSize?.height ?? 800,
@@ -70,7 +68,7 @@ const createWindow = async () => {
fullscreen: false,
};
metadataService.set('window_size', windowSize);
appService.metadata.set('window_size', windowSize);
});
mainWindow.on('enter-full-screen', () => {
@@ -80,7 +78,7 @@ const createWindow = async () => {
fullscreen: true,
};
metadataService.set('window_size', windowSize);
appService.metadata.set('window_size', windowSize);
});
mainWindow.on('leave-full-screen', () => {
@@ -90,7 +88,7 @@ const createWindow = async () => {
fullscreen: false,
};
metadataService.set('window_size', windowSize);
appService.metadata.set('window_size', windowSize);
});
// and load the index.html of the app.
@@ -116,25 +114,25 @@ const createWindow = async () => {
if (!protocol.isProtocolHandled('avatar')) {
protocol.handle('avatar', (request) => {
return avatarService.handleAvatarRequest(request);
return handleAvatarRequest(request);
});
}
if (!protocol.isProtocolHandled('local-file')) {
protocol.handle('local-file', (request) => {
return fileService.handleFileRequest(request);
return handleFileRequest(request);
});
}
if (!protocol.isProtocolHandled('local-file-preview')) {
protocol.handle('local-file-preview', (request) => {
return fileService.handleFilePreviewRequest(request);
return handleFilePreviewRequest(request);
});
}
if (!protocol.isProtocolHandled('asset')) {
protocol.handle('asset', (request) => {
return assetService.handleAssetRequest(request);
return handleAssetRequest(request);
});
}
@@ -159,7 +157,7 @@ app.on('window-all-closed', () => {
app.quit();
}
queryService.clearSubscriptions();
mediator.clearSubscriptions();
});
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
// code. You can also put them in separate files and import them here.
ipcMain.handle('init', async () => {
await scheduler.init();
await appService.init();
});
ipcMain.handle(
@@ -182,7 +180,7 @@ ipcMain.handle(
_: unknown,
input: T
): Promise<MutationMap[T['type']]['output']> => {
return mutationService.executeMutation(input);
return mediator.executeMutation(input);
}
);
@@ -192,7 +190,7 @@ ipcMain.handle(
_: unknown,
input: T
): Promise<QueryMap[T['type']]['output']> => {
return queryService.executeQuery(input);
return mediator.executeQuery(input);
}
);
@@ -203,12 +201,12 @@ ipcMain.handle(
id: string,
input: T
): Promise<QueryMap[T['type']]['output']> => {
return queryService.executeQueryAndSubscribe(id, input);
return mediator.executeQueryAndSubscribe(id, input);
}
);
ipcMain.handle('unsubscribe-query', (_: unknown, id: string): void => {
queryService.unsubscribeQuery(id);
mediator.unsubscribeQuery(id);
});
ipcMain.handle(
@@ -217,6 +215,6 @@ ipcMain.handle(
_: unknown,
input: T
): 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 { FileOpenCommandInput } from '@/shared/commands/file-open';
@@ -6,6 +9,16 @@ export class FileOpenCommandHandler
implements CommandHandler<FileOpenCommandInput>
{
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';
interface ServerTable {
@@ -29,20 +28,6 @@ export type SelectAccount = Selectable<AccountTable>;
export type CreateAccount = Insertable<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 {
token: ColumnType<string, string, never>;
account_id: ColumnType<string, string, never>;
@@ -64,7 +49,6 @@ export type UpdateMetadata = Updateable<MetadataTable>;
export interface AppDatabaseSchema {
servers: ServerTable;
accounts: AccountTable;
workspaces: WorkspaceTable;
deleted_tokens: DeletedTokenTable;
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 { 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 { QueryHandler, SubscribedQuery } from '@/main/types';
import { eventBus } from '@/shared/lib/event-bus';
import { QueryInput, QueryMap } from '@/shared/queries';
import { Event } from '@/shared/types/events';
class QueryService {
private readonly debug = createDebugger('desktop:service:query');
class Mediator {
private readonly debug = createDebugger('desktop:mediator');
private readonly subscribedQueries: Map<string, SubscribedQuery<QueryInput>> =
new Map();
@@ -127,6 +142,60 @@ class QueryService {
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 { accountService } from '@/main/services/account-service';
import { appService } from '@/main/services/app-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
@@ -13,11 +12,7 @@ export class AccountLogoutMutationHandler
async handleMutation(
input: AccountLogoutMutationInput
): Promise<AccountLogoutMutationOutput> {
const account = await databaseService.appDatabase
.selectFrom('accounts')
.selectAll()
.where('id', '=', input.accountId)
.executeTakeFirst();
const account = appService.getAccount(input.accountId);
if (!account) {
throw new MutationError(
@@ -26,7 +21,7 @@ export class AccountLogoutMutationHandler
);
}
await accountService.logoutAccount(account);
await account.logout();
return {
success: true,
};

View File

@@ -1,15 +1,15 @@
import { AccountUpdateOutput } from '@colanode/core';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types';
import { eventBus } from '@/shared/lib/event-bus';
import { httpClient } from '@/shared/lib/http-client';
import {
AccountUpdateMutationInput,
AccountUpdateMutationOutput,
} from '@/shared/mutations/accounts/account-update';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios';
import { appService } from '@/main/services/app-service';
import { mapAccount } from '@/main/utils';
export class AccountUpdateMutationHandler
implements MutationHandler<AccountUpdateMutationInput>
@@ -17,46 +17,25 @@ export class AccountUpdateMutationHandler
async handleMutation(
input: AccountUpdateMutationInput
): Promise<AccountUpdateMutationOutput> {
const account = await databaseService.appDatabase
.selectFrom('accounts')
.selectAll()
.where('id', '=', input.id)
.executeTakeFirst();
const accountService = appService.getAccount(input.id);
if (!account) {
if (!accountService) {
throw new MutationError(
MutationErrorCode.AccountNotFound,
'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 {
const { data } = await httpClient.put<AccountUpdateOutput>(
const { data } = await accountService.client.put<AccountUpdateOutput>(
`/v1/accounts/${input.id}`,
{
name: input.name,
avatar: input.avatar,
},
{
domain: server.domain,
token: account.token,
}
);
const updatedAccount = await databaseService.appDatabase
const updatedAccount = await appService.database
.updateTable('accounts')
.set({
name: data.name,
@@ -73,17 +52,12 @@ export class AccountUpdateMutationHandler
);
}
const account = mapAccount(updatedAccount);
accountService.updateAccount(account);
eventBus.publish({
type: 'account_updated',
account: {
id: updatedAccount.id,
name: updatedAccount.name,
email: updatedAccount.email,
token: updatedAccount.token,
avatar: updatedAccount.avatar,
deviceId: updatedAccount.device_id,
server: updatedAccount.server,
},
account,
});
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 axios from 'axios';
import { app } from 'electron';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import { EmailLoginMutationInput } from '@/shared/mutations/accounts/email-login';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
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
extends AccountMutationHandlerBase
implements MutationHandler<EmailLoginMutationInput>
{
async handleMutation(input: EmailLoginMutationInput): Promise<LoginOutput> {
const server = await databaseService.appDatabase
.selectFrom('servers')
.selectAll()
.where('domain', '=', input.server)
.executeTakeFirst();
const server = appService.getServer(input.server);
if (!server) {
throw new MutationError(
@@ -32,24 +27,23 @@ export class EmailLoginMutationHandler
email: input.email,
password: input.password,
platform: process.platform,
version: app.getVersion(),
version: appService.version,
};
const { data } = await httpClient.post<LoginOutput>(
'/v1/accounts/emails/login',
emailLoginInput,
{
domain: server.domain,
}
const { data } = await axios.post<LoginOutput>(
`${server.apiBaseUrl}/v1/accounts/emails/login`,
emailLoginInput
);
if (data.type === 'verify') {
return data;
}
await accountService.initAccount(data, server.domain);
await this.handleLoginSuccess(data, server);
return data;
} catch (error) {
console.error(error);
const apiError = parseApiError(error);
throw new MutationError(MutationErrorCode.ApiError, apiError.message);
}

View File

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

View File

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

View File

@@ -2,15 +2,14 @@ import FormData from 'form-data';
import fs from 'fs';
import { databaseService } from '@/main/data/database-service';
import { MutationHandler } from '@/main/types';
import { httpClient } from '@/shared/lib/http-client';
import {
AvatarUploadMutationInput,
AvatarUploadMutationOutput,
} from '@/shared/mutations/avatars/avatar-upload';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import { parseApiError } from '@/shared/lib/axios';
import { appService } from '@/main/services/app-service';
interface AvatarUploadResponse {
id: string;
@@ -22,14 +21,9 @@ export class AvatarUploadMutationHandler
async handleMutation(
input: AvatarUploadMutationInput
): Promise<AvatarUploadMutationOutput> {
const credentials = await databaseService.appDatabase
.selectFrom('accounts')
.innerJoin('servers', 'accounts.server', 'servers.domain')
.select(['domain', 'attributes', 'token'])
.where('id', '=', input.accountId)
.executeTakeFirst();
const account = appService.getAccount(input.accountId);
if (!credentials) {
if (!account) {
throw new MutationError(
MutationErrorCode.AccountNotFound,
'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();
formData.append('avatar', fileStream);
const { data } = await httpClient.post<AvatarUploadResponse>(
const { data } = await account.client.post<AvatarUploadResponse>(
'/v1/avatars',
formData,
{
domain: credentials.domain,
token: credentials.token,
headers: formData.getHeaders(),
}
);

View File

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

View File

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

View File

@@ -1,22 +1,24 @@
import { ChannelAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
ChannelUpdateMutationInput,
ChannelUpdateMutationOutput,
} from '@/shared/mutations/channels/channel-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ChannelUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChannelUpdateMutationInput>
{
async handleMutation(
input: ChannelUpdateMutationInput
): 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.userId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,25 @@
import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
DatabaseUpdateMutationInput,
DatabaseUpdateMutationOutput,
} from '@/shared/mutations/databases/database-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class DatabaseUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseUpdateMutationInput>
{
async handleMutation(
input: DatabaseUpdateMutationInput
): 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.userId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
}

View File

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

View File

@@ -1,30 +1,25 @@
import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
FieldDeleteMutationInput,
FieldDeleteMutationOutput,
} from '@/shared/mutations/databases/field-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FieldDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldDeleteMutationInput>
{
async handleMutation(
input: FieldDeleteMutationInput
): Promise<FieldDeleteMutationOutput> {
const result = await entryService.updateEntry<DatabaseAttributes>(
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 workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await workspace.entries.updateEntry<DatabaseAttributes>(
input.databaseId,
(attributes) => {
delete attributes.fields[input.fieldId];
return attributes;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
ViewDeleteMutationInput,
ViewDeleteMutationOutput,
} from '@/shared/mutations/databases/view-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewDeleteMutationInput>
{
async handleMutation(
input: ViewDeleteMutationInput
): 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.userId,
(attributes) => {
if (!attributes.views[input.viewId]) {
throw new MutationError(

View File

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

View File

@@ -1,22 +1,24 @@
import { DatabaseAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
ViewUpdateMutationInput,
ViewUpdateMutationOutput,
} from '@/shared/mutations/databases/view-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class ViewUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewUpdateMutationInput>
{
async handleMutation(
input: ViewUpdateMutationInput
): 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.userId,
(attributes) => {
if (!attributes.views[input.view.id]) {
throw new MutationError(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,19 @@ import {
FileSaveTempMutationOutput,
} from '@/shared/mutations/files/file-save-temp';
import { getWorkspaceTempFilesDirectoryPath } from '@/main/utils';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FileSaveTempMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FileSaveTempMutationInput>
{
async handleMutation(
input: FileSaveTempMutationInput
): 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 filePath = path.join(directoryPath, fileName);

View File

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

View File

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

View File

@@ -1,22 +1,24 @@
import { FolderAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
FolderUpdateMutationInput,
FolderUpdateMutationOutput,
} from '@/shared/mutations/folders/folder-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class FolderUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FolderUpdateMutationInput>
{
async handleMutation(
input: FolderUpdateMutationInput
): 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.userId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,24 @@
import { PageAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
PageUpdateMutationInput,
PageUpdateMutationOutput,
} from '@/shared/mutations/pages/page-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class PageUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageUpdateMutationInput>
{
async handleMutation(
input: PageUpdateMutationInput
): 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.userId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;

View File

@@ -1,22 +1,24 @@
import { RecordAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
RecordAvatarUpdateMutationInput,
RecordAvatarUpdateMutationOutput,
} from '@/shared/mutations/records/record-avatar-update';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordAvatarUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordAvatarUpdateMutationInput>
{
async handleMutation(
input: RecordAvatarUpdateMutationInput
): 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.userId,
(attributes) => {
attributes.avatar = input.avatar;
return attributes;

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,24 @@
import { RecordAttributes } from '@colanode/core';
import { entryService } from '@/main/services/entry-service';
import { MutationHandler } from '@/main/types';
import { MutationError, MutationErrorCode } from '@/shared/mutations';
import {
RecordFieldValueDeleteMutationInput,
RecordFieldValueDeleteMutationOutput,
} from '@/shared/mutations/records/record-field-value-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
export class RecordFieldValueDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordFieldValueDeleteMutationInput>
{
async handleMutation(
input: RecordFieldValueDeleteMutationInput
): 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.userId,
(attributes) => {
delete attributes.fields[input.fieldId];
return attributes;

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