Skip to content

Commit

Permalink
Merge pull request #172 from DashHub-ai/feature/pinning-messages
Browse files Browse the repository at this point in the history
Add messages pinning
  • Loading branch information
Mati365 authored Feb 22, 2025
2 parents d04458e + e22e5ae commit 59e507f
Show file tree
Hide file tree
Showing 60 changed files with 1,399 additions and 37 deletions.
24 changes: 24 additions & 0 deletions apps/backend/src/migrations/0044-add-pinned-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Kysely } from 'kysely';

import { addIdColumn, addTimestampColumns } from './utils';

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('pinned_messages')
.$call(addIdColumn)
.$call(addTimestampColumns)
.addColumn('creator_user_id', 'integer', col => col.notNull().references('users.id').onDelete('restrict'))
.addColumn('message_id', 'uuid', col => col.references('messages.id').onDelete('restrict'))
.execute();

await db.schema
.createIndex('pinned_messages_uniq_index')
.on('pinned_messages')
.unique()
.columns(['creator_user_id', 'message_id'])
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('pinned_messages').execute();
}
2 changes: 2 additions & 0 deletions apps/backend/src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import * as addUsersAvatars from './0040-add-users-avatars';
import * as addUsersAISettings from './0041-add-users-ai-settings-table';
import * as addSearchEnginesTable from './0042-add-search-engines-table';
import * as addWebSearchToMessages from './0043-add-websearch-flag-to-messages';
import * as addPinnedMessages from './0044-add-pinned-messages';

export const DB_MIGRATIONS = {
'0000-add-users-tables': addUsersTables,
Expand Down Expand Up @@ -88,4 +89,5 @@ export const DB_MIGRATIONS = {
'0041-add-users-ai-settings-table': addUsersAISettings,
'0042-add-search-engines-table': addSearchEnginesTable,
'0043-add-websearch-flag-to-messages': addWebSearchToMessages,
'0044-add-pinned-messages': addPinnedMessages,
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppsCategoriesController } from './apps-categories.controller';
import { AppsController } from './apps.controller';
import { ChatsController } from './chats.controller';
import { OrganizationsController } from './organizations.controller';
import { PinnedMessagesController } from './pinned-messages.controller';
import { ProjectsController } from './projects.controller';
import { S3BucketsController } from './s3-buckets.controller';
import { SearchEnginesController } from './search-engines.controller';
Expand All @@ -29,6 +30,7 @@ export class DashboardController extends BaseController {
@inject(ShareResourceController) shareResource: ShareResourceController,
@inject(UsersMeController) usersMe: UsersMeController,
@inject(SearchEnginesController) searchEngines: SearchEnginesController,
@inject(PinnedMessagesController) pinnedMessages: PinnedMessagesController,
) {
super();

Expand All @@ -44,6 +46,7 @@ export class DashboardController extends BaseController {
.route('/chats', chats.router)
.route('/ai-models', aiModels.router)
.route('/search-engines', searchEngines.router)
.route('/share-resource', shareResource.router);
.route('/share-resource', shareResource.router)
.route('/pinned-messages', pinnedMessages.router);
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/modules/api/controllers/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export * from './apps.controller';
export * from './chats.controller';
export * from './dashboard.controller';
export * from './organizations.controller';
export * from './pinned-messages.controller';
export * from './projects.controller';
export * from './s3-buckets.controller';
export * from './search-engines.controller';
export * from './share-resource.controller';
export * from './users-groups.controller';
export * from './users-me.controller';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { inject, injectable } from 'tsyringe';

import {
ofSdkSuccess,
type PinnedMessagesSdk,
SdkPinMessageInputV,
SdkSearchPinnedMessagesInputV,
} from '@llm/sdk';
import { ConfigService } from '~/modules/config';
import { PinnedMessagesService } from '~/modules/pinned-messages';

import {
mapDbRecordAlreadyExistsToSdkError,
rejectUnsafeSdkErrors,
sdkSchemaValidator,
serializeSdkResponseTE,
} from '../../helpers';
import { AuthorizedController } from '../shared/authorized.controller';

@injectable()
export class PinnedMessagesController extends AuthorizedController {
constructor(
@inject(ConfigService) configService: ConfigService,
@inject(PinnedMessagesService) pinnedMessagesService: PinnedMessagesService,
) {
super(configService);

this.router
.get(
'/search',
sdkSchemaValidator('query', SdkSearchPinnedMessagesInputV),
async context => pipe(
context.req.valid('query'),
pinnedMessagesService.asUser(context.var.jwt).search,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<PinnedMessagesSdk['search']>>(context),
),
)
.get(
'/all',
async context => pipe(
pinnedMessagesService.asUser(context.var.jwt).findAll(),
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<PinnedMessagesSdk['all']>>(context),
),
)
.post(
'/',
sdkSchemaValidator('json', SdkPinMessageInputV),
async context => pipe(
context.req.valid('json'),
pinnedMessagesService.asUser(context.var.jwt).create,
mapDbRecordAlreadyExistsToSdkError,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<PinnedMessagesSdk['create']>>(context),
),
)
.delete(
'/:id',
async context => pipe(
Number(context.req.param('id')),
pinnedMessagesService.asUser(context.var.jwt).delete,
TE.map(ofSdkSuccess),
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<PinnedMessagesSdk['delete']>>(context),
),
);
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/modules/database/database.tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
OrganizationsUsersTable,
} from '../organizations';
import type { PermissionsTable } from '../permissions';
import type { PinnedMessagesTable } from '../pinned-messages';
import type {
ProjectsTable,
} from '../projects';
Expand Down Expand Up @@ -74,6 +75,7 @@ export type DatabaseTables = {
chats: ChatsTable;
chat_summaries: ChatSummariesTable;
messages: MessagesTable;
pinned_messages: PinnedMessagesTable;

// LLM
ai_models: AIModelsTable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LoggerService } from '~/modules/logger';
import { MessagesEsIndexRepo } from '~/modules/messages/elasticsearch/messages-es-index.repo';
import { OrganizationsEsIndexRepo } from '~/modules/organizations/elasticsearch';
import { OrganizationsS3BucketsEsIndexRepo } from '~/modules/organizations/s3-buckets/elasticsearch/organizations-s3-buckets-es-index.repo';
import { PinnedMessagesEsIndexRepo } from '~/modules/pinned-messages/elasticsearch/pinned-messages-es-index.repo';
import { ProjectsEmbeddingsEsIndexRepo } from '~/modules/projects-embeddings/elasticsearch/projects-embeddings-es-index.repo';
import { ProjectsFilesEsIndexRepo } from '~/modules/projects-files/elasticsearch/projects-files-es-index.repo';
import { ProjectsEsIndexRepo } from '~/modules/projects/elasticsearch/projects-es-index.repo';
Expand Down Expand Up @@ -39,6 +40,7 @@ export class ElasticsearchRegistryBootService {
@inject(ProjectsFilesEsIndexRepo) private readonly projectsFilesEsIndexRepo: ProjectsFilesEsIndexRepo,
@inject(ProjectsEmbeddingsEsIndexRepo) private readonly projectsEmbeddingsEsIndexRepo: ProjectsEmbeddingsEsIndexRepo,
@inject(SearchEnginesEsIndexRepo) private readonly searchEnginesEsIndexRepo: SearchEnginesEsIndexRepo,
@inject(PinnedMessagesEsIndexRepo) private readonly pinnedMessagesEsIndexRepo: PinnedMessagesEsIndexRepo,
) {}

register = TE.fromIO(() => {
Expand All @@ -56,6 +58,7 @@ export class ElasticsearchRegistryBootService {
this.projectsFilesEsIndexRepo,
this.projectsEmbeddingsEsIndexRepo,
this.searchEnginesEsIndexRepo,
this.pinnedMessagesEsIndexRepo,
]);

this.logger.info('Registered elasticsearch repos!');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class MessagesEsSearchRepo {
},
chat: {
id: source.chat.id,
creator: source.chat.creator,
},
aiModel: source.ai_model && {
id: source.ai_model.id,
Expand Down
71 changes: 71 additions & 0 deletions apps/backend/src/modules/permissions/permissions.firewall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,41 @@ export class PermissionsFirewall extends AuthFirewallService {
}
};

/**
* Enforces creator scoped access by enhancing the DTO with proper creator field.
*
* @example
* search = (filters: SearchInputT) => pipe(
* this.permissionsFirewall.enforceCreatorScopeFilters(filters),
* TE.fromEither,
* TE.chainW(this.someService.search),
* );
*/
enforceCreatorScopeFilters = <F extends { creatorsIds?: SdkIdsArrayT; }>(
filters: F,
): E.Either<SdkUnauthorizedError, F> => {
const { jwt } = this;

switch (jwt.role) {
case 'root':
return E.right({
...filters,
creatorsIds: filters.creatorsIds ?? [this.userId],
});

case 'user':
return E.right({
...filters,
creatorsIds: [this.userId],
});

default: {
const _: never = jwt;
return ofSdkUnauthorizedErrorE();
}
}
};

/**
* Enforces organization scoped access by enhancing the DTO with proper organization field.
* For root users, requires organization to be provided in DTO.
Expand Down Expand Up @@ -395,6 +430,42 @@ export class PermissionsFirewall extends AuthFirewallService {
}
};

/**
* Enforces creator scoped access by enhancing the DTO with proper creator field.
*
* @example
* create = (dto: SdkCreateInputT) => pipe(
* this.permissionsFirewall.enforceCreatorScope(dto),
* TE.fromEither,
* TE.chainW(this.someService.create),
* );
*/
enforceCreatorScope = <DTO extends Partial<WithSdkCreator>>(
dto: DTO,
): E.Either<SdkUnauthorizedError, DTO & WithSdkCreator> => {
const { jwt } = this;

switch (jwt.role) {
case 'root':
return E.right({
...dto,
creator: dto.creator ?? this.userIdRow,
});

case 'user':
return E.right({
...dto,
creator: this.userIdRow,
});

default: {
const _: never = jwt;

return ofSdkUnauthorizedErrorE();
}
}
};

/**
* Drops permission keys from a record if the current user is not the creator.
* Also handles tech/owner roles and group permissions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './pinned-messages-es-index.repo';
export * from './pinned-messages-es-search.repo';
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { array as A, taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import snakecaseKeys from 'snakecase-keys';
import { inject, injectable } from 'tsyringe';

import { tryOrThrowTE } from '@llm/commons';
import {
createAutocompleteFieldAnalyzeSettings,
createBaseAutocompleteFieldMappings,
createBaseDatedRecordMappings,
createElasticsearchIndexRepo,
createIdNameObjectMapping,
createIdObjectMapping,
ElasticsearchRepo,
type EsDocument,
} from '~/modules/elasticsearch';

import type { PinnedMessageTableRowWithRelations } from '../pinned-messages.tables';

import { PinnedMessagesRepo } from '../pinned-messages.repo';

const PinnedMessagesAbstractEsIndexRepo = createElasticsearchIndexRepo({
indexName: 'dashboard-pinned-messages',
schema: {
mappings: {
dynamic: false,
properties: {
...createBaseDatedRecordMappings(),
creator: createIdNameObjectMapping(),
message: createIdObjectMapping(
createBaseAutocompleteFieldMappings('content'),
'keyword',
),
},
},
settings: {
'index.number_of_replicas': 1,
'analysis': createAutocompleteFieldAnalyzeSettings(),
},
},
});

export type PinnedMessagesEsDocument = EsDocument<PinnedMessageTableRowWithRelations>;

@injectable()
export class PinnedMessagesEsIndexRepo extends PinnedMessagesAbstractEsIndexRepo<PinnedMessagesEsDocument> {
constructor(
@inject(ElasticsearchRepo) elasticsearchRepo: ElasticsearchRepo,
@inject(PinnedMessagesRepo) private readonly pinnedMessagesRepo: PinnedMessagesRepo,
) {
super(elasticsearchRepo);
}

protected async findEntities(ids: number[]): Promise<PinnedMessagesEsDocument[]> {
return pipe(
this.pinnedMessagesRepo.findWithRelationsByIds({ ids }),
TE.map(
A.map(entity => ({
...snakecaseKeys(entity, { deep: true }),
_id: String(entity.id),
})),
),
tryOrThrowTE,
)();
}

protected createAllEntitiesIdsIterator = () =>
this.pinnedMessagesRepo.createIdsIterator({
chunkSize: 100,
});
}
Loading

0 comments on commit 59e507f

Please sign in to comment.