Skip to content

Commit

Permalink
Merge pull request #74 from DashHub-ai/feature/add-chats-database-str…
Browse files Browse the repository at this point in the history
…ucture

Add chats database structure
  • Loading branch information
Mati365 authored Nov 16, 2024
2 parents 9291081 + 8f4ed2e commit f0841cb
Show file tree
Hide file tree
Showing 64 changed files with 1,017 additions and 113 deletions.
4 changes: 2 additions & 2 deletions apps/admin/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Router() {
<Switch>
<Route path={sitemap.login} component={LoginRoute} />
<Route>
<Redirect to={sitemap.login} />
<Redirect to={sitemap.login} replace />
</Route>
</Switch>
);
Expand All @@ -38,7 +38,7 @@ export function Router() {
<Route path={sitemap.s3Buckets.index.raw} component={S3BucketsRoute} />
<Route path={sitemap.forceRedirect.raw} component={ForceRedirectRoute} />
<Route>
<Redirect to={sitemap.home} />
<Redirect to={sitemap.home} replace />
</Route>
</Switch>
);
Expand Down
61 changes: 61 additions & 0 deletions apps/backend/src/migrations/0008-add-chats-tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type Kysely, sql } from 'kysely';

import { addArchivedAtColumns, addIdColumn, addTimestampColumns } from './utils';
import { addUuidColumn } from './utils/add-uuid';

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('chats')
.$call(addUuidColumn)
.$call(addTimestampColumns)
.$call(addArchivedAtColumns)
.addColumn('creator_user_id', 'integer', col => col.notNull().references('users.id').onDelete('restrict'))
.addColumn('organization_id', 'integer', col => col.notNull().references('organizations.id').onDelete('restrict'))
.addColumn('public', 'boolean', col => col.notNull().defaultTo(false))
.addColumn('last_message_at', 'timestamp')
.execute();

await db.schema
.createTable('chat_summaries')
.$call(addIdColumn)
.$call(addTimestampColumns)
.addColumn('chat_id', 'uuid', col => col.notNull().references('chats.id').onDelete('restrict'))
.addColumn('content', 'text', col => col.notNull())
.execute();

await db.schema
.createType('message_role')
.asEnum(['user', 'assistant', 'system'])
.execute();

await db.schema
.createTable('messages')
.$call(addUuidColumn)
.$call(addTimestampColumns)
.addColumn('chat_id', 'uuid', col => col.notNull().references('chats.id').onDelete('restrict'))
.addColumn('content', 'text', col => col.notNull())
.addColumn('role', sql`message_role`, col => col.notNull())
.addColumn('metadata', 'jsonb', col => col.notNull().defaultTo('{}'))
.addColumn('original_message_id', 'uuid', col => col.references('messages.id').onDelete('restrict'))
.addColumn('repeat_count', 'integer', col => col.notNull().defaultTo(0))
.execute();

await db.schema
.createIndex('chats_organization_id_index')
.on('chats')
.column('organization_id')
.execute();

await db.schema
.createIndex('messages_chat_id_index')
.on('messages')
.column('chat_id')
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('messages').execute();
await db.schema.dropTable('chat_summaries').execute();
await db.schema.dropTable('chats').execute();
await db.schema.dropType('message_role').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 @@ -6,6 +6,7 @@ import * as addProjectsTable from './0004-add-projects-table';
import * as addAppsTable from './0005-add-apps-table';
import * as addDescriptionToProjects from './0006-add-description-to-projects-table';
import * as addDescriptionToApps from './0007-add-description-to-apps-table';
import * as addChatsTables from './0008-add-chats-tables';

export const DB_MIGRATIONS = {
'0000-add-users-tables': addUsersTables,
Expand All @@ -16,4 +17,5 @@ export const DB_MIGRATIONS = {
'0005-add-apps-table': addAppsTable,
'0006-add-description-to-projects-table': addDescriptionToProjects,
'0007-add-description-to-apps-table': addDescriptionToApps,
'0008-add-chats-tables': addChatsTables,
};
12 changes: 12 additions & 0 deletions apps/backend/src/migrations/utils/add-uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* DO NOT CHANGE! It will break migrations! */
import { type AlterTableBuilder, type CreateTableBuilder, sql } from 'kysely';

export function addUuidColumn<
C extends AlterTableBuilder | CreateTableBuilder<any>,
>(qb: C) {
return qb.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql`gen_random_uuid()`)) as C;
}

export function dropUuidColumn<C extends AlterTableBuilder>(qb: C) {
return qb.dropColumn('id');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { pipe } from 'fp-ts/lib/function';
import { inject, injectable } from 'tsyringe';

import {
type ChatsSdk,
SdkCreateChatInputV,
} from '@llm/sdk';
import { ChatsService } from '~/modules/chats';
import { ConfigService } from '~/modules/config';

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

@injectable()
export class ChatsController extends AuthorizedController {
constructor(
@inject(ConfigService) configService: ConfigService,
@inject(ChatsService) chatsService: ChatsService,
) {
super(configService);

this.router
.post(
'/',
sdkSchemaValidator('json', SdkCreateChatInputV),
async context => pipe(
context.req.valid('json'),
chatsService.asUser(context.var.jwt).create,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<ChatsSdk['create']>>(context),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inject, injectable } from 'tsyringe';

import { BaseController } from '../shared';
import { AppsController } from './apps.controller';
import { ChatsController } from './chats.controller';
import { OrganizationsController } from './organizations.controller';
import { ProjectsController } from './projects.controller';
import { S3BucketsController } from './s3-buckets.controller';
Expand All @@ -15,6 +16,7 @@ export class DashboardController extends BaseController {
@inject(ProjectsController) projects: ProjectsController,
@inject(AppsController) apps: AppsController,
@inject(S3BucketsController) s3Buckets: S3BucketsController,
@inject(ChatsController) chats: ChatsController,
) {
super();

Expand All @@ -23,6 +25,7 @@ export class DashboardController extends BaseController {
.route('/users', users.router)
.route('/projects', projects.router)
.route('/apps', apps.router)
.route('/s3-buckets', s3Buckets.router);
.route('/s3-buckets', s3Buckets.router)
.route('/chats', chats.router);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './apps.controller';
export * from './chats.controller';
export * from './dashboard.controller';
export * from './organizations.controller';
export * from './projects.controller';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export abstract class AuthFirewallService {
return +this.jwt.sub;
}

protected get userIdRow() {
return {
id: this.userId,
};
}

protected get check() {
return createAccessLevelGuard(this.jwt);
}
Expand Down
63 changes: 63 additions & 0 deletions apps/backend/src/modules/chats/chats.firewall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { flow } from 'fp-ts/lib/function';

import {
ofSdkUnauthorizedErrorTE,
type SdkCreateChatInputT,
type SdkJwtTokenT,
type SdkUnauthorizedError,
} from '@llm/sdk';
import { AuthFirewallService } from '~/modules/auth/firewall';

import type { DatabaseTE, TableRowWithId } from '../database';
import type { ChatsService } from './chats.service';

export class ChatsFirewall extends AuthFirewallService {
constructor(
jwt: SdkJwtTokenT,
private readonly chatsService: ChatsService,
) {
super(jwt);
}

// TODO: Add belongs checks
unarchive = flow(
this.chatsService.archive,
this.tryTEIfUser.is.root,
);

// TODO: Add belongs checks
archive = flow(
this.chatsService.archive,
this.tryTEIfUser.is.root,
);

create = ({ creator, organization, ...chat }: SdkCreateChatInputT): DatabaseTE<TableRowWithId, SdkUnauthorizedError> => {
const { jwt } = this;

switch (jwt.role) {
case 'root':
if (!organization) {
return ofSdkUnauthorizedErrorTE();
}

return this.chatsService.create({
...chat,
creator: creator ?? this.userIdRow,
organization,
});

case 'user':
return this.chatsService.create({
...chat,
creator: this.userIdRow,
organization: jwt.organization,
});

default: {
const _: never = jwt;

return ofSdkUnauthorizedErrorTE();
}
}
};
}
20 changes: 20 additions & 0 deletions apps/backend/src/modules/chats/chats.repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { injectable } from 'tsyringe';

import {
createArchiveRecordQuery,
createArchiveRecordsQuery,
createDatabaseRepo,
createUnarchiveRecordQuery,
createUnarchiveRecordsQuery,
} from '~/modules/database';

@injectable()
export class ChatsRepo extends createDatabaseRepo('chats') {
archive = createArchiveRecordQuery(this.queryFactoryAttrs);

archiveRecords = createArchiveRecordsQuery(this.queryFactoryAttrs);

unarchive = createUnarchiveRecordQuery(this.queryFactoryAttrs);

unarchiveRecords = createUnarchiveRecordsQuery(this.queryFactoryAttrs);
}
39 changes: 39 additions & 0 deletions apps/backend/src/modules/chats/chats.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { pipe } from 'fp-ts/lib/function';
import { inject, injectable } from 'tsyringe';

import type { RequiredBy } from '@llm/commons';

import { SdkCreateChatInputT, SdkJwtTokenT, SdkTableRowIdT } from '@llm/sdk';

import { WithAuthFirewall } from '../auth';
import { ChatsFirewall } from './chats.firewall';
import { ChatsRepo } from './chats.repo';

@injectable()
export class ChatsService implements WithAuthFirewall<ChatsFirewall> {
constructor(
@inject(ChatsRepo) private readonly repo: ChatsRepo,
) {}

asUser = (jwt: SdkJwtTokenT) => new ChatsFirewall(jwt, this);

unarchive = (id: SdkTableRowIdT) => this.repo.unarchive({ id });

archive = (id: SdkTableRowIdT) => this.repo.archive({ id });

create = (
{
organization,
creator,
...values
}: RequiredBy<SdkCreateChatInputT, 'organization' | 'creator'>,
) => pipe(
this.repo.create({
value: {
...values,
organizationId: organization.id,
creatorUserId: creator.id,
},
}),
);
}
38 changes: 38 additions & 0 deletions apps/backend/src/modules/chats/chats.tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ColumnType } from 'kysely';

import type { SdkOffsetPaginationOutputT } from '@llm/sdk';
import type {
NormalizeSelectTableRow,
TableId,
TableUuid,
TableWithAccessTimeColumns,
TableWithArchivedAtColumn,
TableWithDefaultColumns,
TableWithUuidColumn,
} from '~/modules/database';

import type { MessageTableRowWithRelations } from './messages';

export type ChatsTable =
& TableWithUuidColumn
& TableWithAccessTimeColumns
& TableWithArchivedAtColumn
& {
creator_user_id: ColumnType<TableId, TableId, never>;
organization_id: ColumnType<TableId, TableId, never>;
public: boolean;
last_message_at: Date | null;
};

export type ChatSummariesTable = TableWithDefaultColumns & {
chat_id: ColumnType<TableUuid, TableUuid, never>;
content: string;
};

export type ChatTableRow = NormalizeSelectTableRow<ChatsTable>;
export type ChatSummaryTableRow = NormalizeSelectTableRow<ChatSummariesTable>;

export type ChatTableRowWithRelations = ChatTableRow & {
summary: ChatSummaryTableRow | null;
recentMessages: SdkOffsetPaginationOutputT<MessageTableRowWithRelations>;
};
5 changes: 5 additions & 0 deletions apps/backend/src/modules/chats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './chats.firewall';
export * from './chats.repo';
export * from './chats.service';
export * from './chats.tables';
export * from './messages';
1 change: 1 addition & 0 deletions apps/backend/src/modules/chats/messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './messages.tables';
32 changes: 32 additions & 0 deletions apps/backend/src/modules/chats/messages/messages.tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ColumnType } from 'kysely';

import type { SdkMessageRoleT } from '@llm/sdk';
import type {
NormalizeSelectTableRow,
TableId,
TableUuid,
TableWithAccessTimeColumns,
TableWithUuidColumn,
} from '~/modules/database';

export type MessagesTable =
& TableWithUuidColumn
& TableWithAccessTimeColumns
& {
chat_id: ColumnType<TableUuid, TableUuid, never>;
content: string;
role: SdkMessageRoleT;
metadata: Record<string, unknown>;
original_message_id: ColumnType<TableId, TableId, null>;
repeat_count: ColumnType<number, number, never>;
};

export type MessageTableRow = NormalizeSelectTableRow<MessagesTable>;

export type MessageTableRowWithRelations =
& OmitRepeatFields<MessageTableRow>
& {
repeats: Array<OmitRepeatFields<MessageTableRow>>;
};

type OmitRepeatFields<T> = Omit<T, 'originalMessageId' | 'repeatCount'>;
Loading

0 comments on commit f0841cb

Please sign in to comment.