-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(backend): add categories database tables and es indices
- Loading branch information
Showing
30 changed files
with
631 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import type { Kysely } from 'kysely'; | ||
|
||
import { | ||
addArchivedAtColumns, | ||
addIdColumn, | ||
addTimestampColumns, | ||
} from './utils'; | ||
|
||
export async function up(db: Kysely<any>): Promise<void> { | ||
await db.schema | ||
.createTable('apps_categories') | ||
.$call(addIdColumn) | ||
.$call(addTimestampColumns) | ||
.$call(addArchivedAtColumns) | ||
.addColumn('name', 'text', col => col.notNull()) | ||
.addColumn('icon', 'text', col => col.notNull()) | ||
.addColumn('description', 'text') | ||
.addColumn('parent_category_id', 'integer', col => col.references('apps_categories.id')) | ||
.execute(); | ||
|
||
await db.schema | ||
.alterTable('apps') | ||
.addColumn('category_id', 'integer', col => col.references('apps_categories.id')) | ||
.execute(); | ||
|
||
const { id } = await db | ||
.insertInto('apps_categories') | ||
.values([ | ||
{ name: 'Other', icon: 'ellipsis', description: 'Other apps that do not fit into any other category' }, | ||
]) | ||
.returning('id') | ||
.executeTakeFirstOrThrow(); | ||
|
||
await db | ||
.updateTable('apps') | ||
.set({ category_id: id }) | ||
.execute(); | ||
|
||
await db.schema | ||
.alterTable('apps') | ||
.alterColumn('category_id', col => col.setNotNull()) | ||
.execute(); | ||
} | ||
|
||
export async function down(db: Kysely<any>): Promise<void> { | ||
await db.schema.alterTable('apps').dropColumn('category_id').execute(); | ||
await db.schema.dropTable('apps_categories').execute(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
apps/backend/src/modules/apps-categories/apps-categories.firewall.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { flow } from 'fp-ts/lib/function'; | ||
|
||
import type { SdkJwtTokenT } from '@llm/sdk'; | ||
|
||
import { AuthFirewallService } from '~/modules/auth/firewall'; | ||
|
||
import type { AppsCategoriesService } from './apps-categories.service'; | ||
|
||
export class AppsCategoriesFirewall extends AuthFirewallService { | ||
constructor( | ||
jwt: SdkJwtTokenT, | ||
private readonly appsService: AppsCategoriesService, | ||
) { | ||
super(jwt); | ||
} | ||
|
||
unarchive = flow( | ||
this.appsService.unarchive, | ||
this.tryTEIfUser.is.root, | ||
); | ||
|
||
archive = flow( | ||
this.appsService.archive, | ||
this.tryTEIfUser.is.root, | ||
); | ||
|
||
update = flow( | ||
this.appsService.update, | ||
this.tryTEIfUser.is.root, | ||
); | ||
|
||
create = flow( | ||
this.appsService.create, | ||
this.tryTEIfUser.is.root, | ||
); | ||
|
||
search = flow( | ||
this.appsService.search, | ||
this.tryTEIfUser.is.root, | ||
); | ||
|
||
get = flow( | ||
this.appsService.get, | ||
this.tryTEIfUser.is.root, | ||
); | ||
} |
66 changes: 66 additions & 0 deletions
66
apps/backend/src/modules/apps-categories/apps-categories.repo.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import camelcaseKeys from 'camelcase-keys'; | ||
import { array as A, taskEither as TE } from 'fp-ts'; | ||
import { pipe } from 'fp-ts/lib/function'; | ||
import { injectable } from 'tsyringe'; | ||
|
||
import { | ||
createArchiveRecordQuery, | ||
createArchiveRecordsQuery, | ||
createDatabaseRepo, | ||
createUnarchiveRecordQuery, | ||
createUnarchiveRecordsQuery, | ||
DatabaseError, | ||
TableId, | ||
TransactionalAttrs, | ||
tryReuseTransactionOrSkip, | ||
} from '~/modules/database'; | ||
|
||
import { AppTableRowWithRelations } from './apps-categories.tables'; | ||
|
||
@injectable() | ||
export class AppsCategoriesRepo extends createDatabaseRepo('apps_categories') { | ||
archive = createArchiveRecordQuery(this.queryFactoryAttrs); | ||
|
||
archiveRecords = createArchiveRecordsQuery(this.queryFactoryAttrs); | ||
|
||
unarchive = createUnarchiveRecordQuery(this.queryFactoryAttrs); | ||
|
||
unarchiveRecords = createUnarchiveRecordsQuery(this.queryFactoryAttrs); | ||
|
||
findWithRelationsByIds = ({ forwardTransaction, ids }: TransactionalAttrs<{ ids: TableId[]; }>) => { | ||
const transaction = tryReuseTransactionOrSkip({ db: this.db, forwardTransaction }); | ||
|
||
return pipe( | ||
transaction( | ||
async qb => | ||
qb | ||
.selectFrom(`${this.table} as category`) | ||
.where('category.id', 'in', ids) | ||
.leftJoin('apps_categories as parent_categories', 'parent_categories.id', 'category.parent_category_id') | ||
.selectAll('category') | ||
.select([ | ||
'parent_categories.id as parent_category_id', | ||
'parent_categories.name as parent_category_name', | ||
]) | ||
.limit(ids.length) | ||
.execute(), | ||
), | ||
DatabaseError.tryTask, | ||
TE.map( | ||
A.map(({ | ||
parent_category_id: parentId, | ||
parent_category_name: parentName, | ||
...item | ||
}): AppTableRowWithRelations => ({ | ||
...camelcaseKeys(item), | ||
parentCategory: parentId | ||
? { | ||
id: parentId, | ||
name: parentName || '', | ||
} | ||
: null, | ||
})), | ||
), | ||
); | ||
}; | ||
} |
83 changes: 83 additions & 0 deletions
83
apps/backend/src/modules/apps-categories/apps-categories.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { taskEither as TE } from 'fp-ts'; | ||
import { pipe } from 'fp-ts/lib/function'; | ||
import { inject, injectable } from 'tsyringe'; | ||
|
||
import type { | ||
SdkCreateAppCategoryInputT, | ||
SdkJwtTokenT, | ||
SdkTableRowIdT, | ||
SdkUpdateAppInputT, | ||
} from '@llm/sdk'; | ||
|
||
import { | ||
asyncIteratorToVoidPromise, | ||
runTaskAsVoid, | ||
tapAsyncIterator, | ||
tryOrThrowTE, | ||
} from '@llm/commons'; | ||
|
||
import type { WithAuthFirewall } from '../auth'; | ||
import type { TableId, TableRowWithId } from '../database'; | ||
|
||
import { AppsCategoriesFirewall } from './apps-categories.firewall'; | ||
import { AppsCategoriesRepo } from './apps-categories.repo'; | ||
import { AppsCategoriesEsIndexRepo, AppsCategoriesEsSearchRepo } from './elasticsearch'; | ||
|
||
@injectable() | ||
export class AppsCategoriesService implements WithAuthFirewall<AppsCategoriesFirewall> { | ||
constructor( | ||
@inject(AppsCategoriesRepo) private readonly repo: AppsCategoriesRepo, | ||
@inject(AppsCategoriesEsSearchRepo) private readonly esSearchRepo: AppsCategoriesEsSearchRepo, | ||
@inject(AppsCategoriesEsIndexRepo) private readonly esIndexRepo: AppsCategoriesEsIndexRepo, | ||
) {} | ||
|
||
asUser = (jwt: SdkJwtTokenT) => new AppsCategoriesFirewall(jwt, this); | ||
|
||
get = this.esSearchRepo.get; | ||
|
||
archiveSeqStream = (stream: AsyncIterableIterator<TableId[]>) => async () => | ||
pipe( | ||
stream, | ||
tapAsyncIterator<TableId[], void>(async ids => | ||
pipe( | ||
this.repo.archiveRecords({ | ||
where: [ | ||
['id', 'in', ids], | ||
['archived', '=', false], | ||
], | ||
}), | ||
TE.tap(() => this.esIndexRepo.findAndIndexDocumentsByIds(ids)), | ||
tryOrThrowTE, | ||
runTaskAsVoid, | ||
), | ||
), | ||
asyncIteratorToVoidPromise, | ||
); | ||
|
||
unarchive = (id: SdkTableRowIdT) => pipe( | ||
this.repo.unarchive({ id }), | ||
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)), | ||
); | ||
|
||
archive = (id: SdkTableRowIdT) => pipe( | ||
this.repo.archive({ id }), | ||
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)), | ||
); | ||
|
||
search = this.esSearchRepo.search; | ||
|
||
create = ({ parentCategory, ...values }: SdkCreateAppCategoryInputT) => pipe( | ||
this.repo.create({ | ||
value: { | ||
...values, | ||
parentCategoryId: parentCategory?.id || null, | ||
}, | ||
}), | ||
TE.tap(({ id }) => this.esIndexRepo.findAndIndexDocumentById(id)), | ||
); | ||
|
||
update = ({ id, ...value }: SdkUpdateAppInputT & TableRowWithId) => pipe( | ||
this.repo.update({ id, value }), | ||
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)), | ||
); | ||
} |
21 changes: 21 additions & 0 deletions
21
apps/backend/src/modules/apps-categories/apps-categories.tables.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { | ||
NormalizeSelectTableRow, | ||
TableId, | ||
TableRowWithIdName, | ||
TableWithArchivedAtColumn, | ||
TableWithDefaultColumns, | ||
} from '../database'; | ||
|
||
export type AppsCategoriesTable = TableWithDefaultColumns & | ||
TableWithArchivedAtColumn & { | ||
name: string; | ||
description: string | null; | ||
icon: string; | ||
parent_category_id: TableId | null; | ||
}; | ||
|
||
export type AppCategoryTableRow = NormalizeSelectTableRow<AppsCategoriesTable>; | ||
|
||
export type AppTableRowWithRelations = Omit<AppCategoryTableRow, 'parentCategoryId'> & { | ||
parentCategory: TableRowWithIdName | null; | ||
}; |
73 changes: 73 additions & 0 deletions
73
apps/backend/src/modules/apps-categories/elasticsearch/apps-categories-es-index.repo.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
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 { | ||
createArchivedRecordMappings, | ||
createAutocompleteFieldAnalyzeSettings, | ||
createBaseAutocompleteFieldMappings, | ||
createBaseDatedRecordMappings, | ||
createElasticsearchIndexRepo, | ||
createIdNameObjectMapping, | ||
ElasticsearchRepo, | ||
type EsDocument, | ||
} from '~/modules/elasticsearch'; | ||
|
||
import type { AppTableRowWithRelations } from '../apps-categories.tables'; | ||
|
||
import { AppsCategoriesRepo } from '../apps-categories.repo'; | ||
|
||
const AppsAbstractEsIndexRepo = createElasticsearchIndexRepo({ | ||
indexName: 'dashboard-apps-categories', | ||
schema: { | ||
mappings: { | ||
dynamic: false, | ||
properties: { | ||
...createBaseDatedRecordMappings(), | ||
...createBaseAutocompleteFieldMappings(), | ||
...createArchivedRecordMappings(), | ||
parent_category: createIdNameObjectMapping(), | ||
description: { | ||
type: 'text', | ||
analyzer: 'folded_lowercase_analyzer', | ||
}, | ||
}, | ||
}, | ||
settings: { | ||
'index.number_of_replicas': 1, | ||
'analysis': createAutocompleteFieldAnalyzeSettings(), | ||
}, | ||
}, | ||
}); | ||
|
||
export type AppsEsDocument = EsDocument<AppTableRowWithRelations>; | ||
|
||
@injectable() | ||
export class AppsCategoriesEsIndexRepo extends AppsAbstractEsIndexRepo<AppsEsDocument> { | ||
constructor( | ||
@inject(ElasticsearchRepo) elasticsearchRepo: ElasticsearchRepo, | ||
@inject(AppsCategoriesRepo) private readonly appsRepo: AppsCategoriesRepo, | ||
) { | ||
super(elasticsearchRepo); | ||
} | ||
|
||
protected async findEntities(ids: number[]): Promise<AppsEsDocument[]> { | ||
return pipe( | ||
this.appsRepo.findWithRelationsByIds({ ids }), | ||
TE.map( | ||
A.map(entity => ({ | ||
...snakecaseKeys(entity, { deep: true }), | ||
_id: String(entity.id), | ||
})), | ||
), | ||
tryOrThrowTE, | ||
)(); | ||
} | ||
|
||
protected createAllEntitiesIdsIterator = () => | ||
this.appsRepo.createIdsIterator({ | ||
chunkSize: 100, | ||
}); | ||
} |
Oops, something went wrong.