refactor: replace TypeORM with knex.js

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-03-14 23:33:29 +01:00
parent 6e151c8a1b
commit c0ce00b3f9
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
242 changed files with 4601 additions and 6871 deletions

View file

@ -6,8 +6,10 @@
import { measurePerformance } from '../../../utils/measure-performance'
import type { ParserOptions } from '@hedgedoc/html-to-react'
import { convertHtmlToReact } from '@hedgedoc/html-to-react'
import type DOMPurify from 'dompurify'
import { sanitize } from 'dompurify'
import DOMPurify from 'dompurify'
// see https://github.com/cure53/DOMPurify/issues/1034#issuecomment-2493211056
// eslint-disable-next-line @typescript-eslint/unbound-method
const { sanitize } = DOMPurify
import React, { Fragment, useMemo } from 'react'
export interface HtmlToReactProps {

View file

@ -12,7 +12,7 @@ import React, { useCallback } from 'react'
import { FileEarmarkPlus as IconPlus } from 'react-bootstrap-icons'
import { Trans } from 'react-i18next'
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
import { GuestAccess } from '@hedgedoc/commons'
import { PermissionLevel } from '@hedgedoc/commons'
import { useIsLoggedIn } from '../../../hooks/common/use-is-logged-in'
/**
@ -27,14 +27,14 @@ export const NewNoteButton: React.FC = () => {
const createNewNoteAndRedirect = useCallback((): void => {
createNote('')
.then((note) => {
router?.push(`/n/${note.metadata.primaryAddress}`)
router?.push(`/n/${note.metadata.primaryAlias}`)
})
.catch((error: Error) => {
showErrorNotification(error.message)
})
}, [router, showErrorNotification])
if (!isLoggedIn && guestAccessLevel !== GuestAccess.CREATE) {
if (!isLoggedIn && guestAccessLevel !== PermissionLevel.CREATE) {
return null
}

View file

@ -23,7 +23,7 @@ describe('create non existing note hint', () => {
.mockImplementation(async (markdown, primaryAlias): Promise<NoteDto> => {
expect(markdown).toBe('')
expect(primaryAlias).toBe(mockedNoteId)
const metadata: NoteMetadataDto = Mock.of<NoteMetadataDto>({ primaryAddress: 'mockedPrimaryAlias' })
const metadata: NoteMetadataDto = Mock.of<NoteMetadataDto>({ primaryAlias: 'mockedPrimaryAlias' })
await new Promise((resolve) => setTimeout(resolve, 100))
await waitForOtherPromisesToFinish()
return Mock.of<NoteDto>({ metadata })

View file

@ -11,7 +11,6 @@ import { NoteInfoLineUpdatedBy } from '../editor-page/sidebar/specific-sidebar-e
import styles from './document-infobar.module.scss'
import React from 'react'
import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { Trans } from 'react-i18next'
/**
* Renders an info bar with metadata about the current note.
@ -34,10 +33,9 @@ export const DocumentInfobar: React.FC = () => {
<hr />
</div>
<span className={'ms-auto'}>
{noteDetails.viewCount} <Trans i18nKey={'views.readOnly.viewCount'} />
<InternalLink
text={''}
href={`/n/${noteDetails.primaryAddress}`}
href={`/n/${noteDetails.primaryAlias}`}
icon={IconPencil}
className={'text-primary text-decoration-none mx-1'}
title={linkTitle}

View file

@ -53,14 +53,14 @@ export const useHandleUpload = (): handleUploadSignature => {
: t('editor.upload.uploadFile.withoutDescription', { fileName: file.name })
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
const noteId = getGlobalState().noteDetails?.id
if (noteId === undefined) {
const noteAlias = getGlobalState().noteDetails?.primaryAlias
if (noteAlias === undefined) {
return
}
changeContent(({ currentSelection }) => {
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
})
uploadFile(noteId, file)
uploadFile(noteAlias, file)
.then(({ uuid }) => {
const fullUrl = `${baseUrl}media/${uuid}`
const replacement = `![${description ?? file.name ?? ''}](${fullUrl}${additionalUrlText ?? ''})`

View file

@ -14,7 +14,7 @@ const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
* Provides the URL for the realtime endpoint.
*/
export const useWebsocketUrl = (): URL | null => {
const noteId = useApplicationState((state) => state.noteDetails?.id)
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
const baseUrl = useBaseUrl()
const websocketUrl = useMemo(() => {
@ -33,11 +33,11 @@ export const useWebsocketUrl = (): URL | null => {
}, [baseUrl])
return useMemo(() => {
if (noteId === '' || noteId === undefined) {
if (noteAlias === '' || noteAlias === undefined) {
return null
}
const url = new URL(websocketUrl)
url.search = `?noteId=${noteId}`
url.search = `?noteAlias=${noteAlias}`
return url
}, [noteId, websocketUrl])
}, [noteAlias, websocketUrl])
}

View file

@ -34,7 +34,7 @@ export const MediaBrowserSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
selectedMenuId
}) => {
useTranslation()
const noteId = useApplicationState((state) => state.noteDetails?.id ?? '')
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias ?? '')
const [mediaEntryForDeletion, setMediaEntryForDeletion] = useState<MediaUploadDto | null>(null)
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
@ -43,7 +43,7 @@ export const MediaBrowserSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
onClick(menuId)
}, [menuId, onClick])
const { value, loading, error } = useAsync(() => getMediaForNote(noteId), [expand, noteId])
const { value, loading, error } = useAsync(() => getMediaForNote(noteAlias), [expand, noteAlias])
const mediaEntries = useMemo(() => {
if (loading || error || !value) {

View file

@ -15,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
*/
export const NoteInfoLineUpdatedBy: React.FC = () => {
useTranslation()
const noteUpdateUser = useApplicationState((state) => state.noteDetails?.updateUsername)
const noteUpdateUser = useApplicationState((state) => state.noteDetails?.lastUpdatedBy)
const userBlock = useMemo(() => {
if (!noteUpdateUser) {

View file

@ -6,7 +6,7 @@
import { useTranslatedText } from '../../../../../../hooks/common/use-translated-text'
import { UiIcon } from '../../../../../common/icons/ui-icon'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import { GuestAccess } from '@hedgedoc/commons'
import { PermissionLevel } from '@hedgedoc/commons'
import React, { useMemo } from 'react'
import { Button, ToggleButtonGroup } from 'react-bootstrap'
import { Eye as IconEye, Pencil as IconPencil, X as IconX } from 'react-bootstrap-icons'
@ -24,7 +24,7 @@ export enum PermissionType {
export interface PermissionEntryButtonsProps {
type: PermissionType
currentSetting: GuestAccess
currentSetting: PermissionLevel
name: string
onSetReadOnly: () => void
onSetWriteable: () => void
@ -79,14 +79,14 @@ export const PermissionEntryButtons: React.FC<PermissionEntryButtonsProps & Perm
<Button
disabled={disabled}
title={setReadOnlyTitle}
variant={currentSetting === GuestAccess.READ ? 'secondary' : 'outline-secondary'}
variant={currentSetting === PermissionLevel.READ ? 'secondary' : 'outline-secondary'}
onClick={onSetReadOnly}>
<UiIcon icon={IconEye} />
</Button>
<Button
disabled={disabled}
title={setWritableTitle}
variant={currentSetting === GuestAccess.WRITE ? 'secondary' : 'outline-secondary'}
variant={currentSetting === PermissionLevel.WRITE ? 'secondary' : 'outline-secondary'}
onClick={onSetWriteable}>
<UiIcon icon={IconPencil} />
</Button>

View file

@ -10,7 +10,7 @@ import { setNotePermissionsFromServer } from '../../../../../../redux/note-detai
import { IconButton } from '../../../../../common/icon-button/icon-button'
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
import { PermissionLevel, SpecialGroup } from '@hedgedoc/commons'
import React, { useCallback, useMemo } from 'react'
import { ToggleButtonGroup } from 'react-bootstrap'
import { Eye as IconEye, Pencil as IconPencil, SlashCircle as IconSlashCircle } from 'react-bootstrap-icons'
@ -19,7 +19,7 @@ import { PermissionInconsistentAlert } from './permission-inconsistent-alert'
import { cypressId } from '../../../../../../utils/cypress-attribute'
export interface PermissionEntrySpecialGroupProps {
level: GuestAccess
level: PermissionLevel
type: SpecialGroup
inconsistent?: boolean
}
@ -38,7 +38,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
disabled,
inconsistent
}) => {
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
const noteId = useApplicationState((state) => state.noteDetails?.primaryAlias)
const { t } = useTranslation()
const { showErrorNotification } = useUiNotifications()
@ -98,7 +98,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
<IconButton
icon={IconSlashCircle}
title={denyGroupText}
variant={level === GuestAccess.DENY ? 'secondary' : 'outline-secondary'}
variant={level === PermissionLevel.DENY ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryDenied}
disabled={disabled}
className={'p-1'}
@ -107,7 +107,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
<IconButton
icon={IconEye}
title={viewOnlyGroupText}
variant={level === GuestAccess.READ ? 'secondary' : 'outline-secondary'}
variant={level === PermissionLevel.READ ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryReadOnly}
disabled={disabled}
className={'p-1'}
@ -116,7 +116,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
<IconButton
icon={IconPencil}
title={editGroupText}
variant={level === GuestAccess.WRITE ? 'secondary' : 'outline-secondary'}
variant={level === PermissionLevel.WRITE ? 'secondary' : 'outline-secondary'}
onClick={onSetEntryWriteable}
disabled={disabled}
className={'p-1'}

View file

@ -11,7 +11,7 @@ import { useUiNotifications } from '../../../../../notifications/ui-notification
import type { PermissionDisabledProps } from './permission-disabled.prop'
import { PermissionEntryButtons, PermissionType } from './permission-entry-buttons'
import type { NoteUserPermissionEntryDto } from '@hedgedoc/commons'
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
import { PermissionLevel, SpecialGroup } from '@hedgedoc/commons'
import React, { useCallback, useMemo } from 'react'
import { useAsync } from 'react-use'
import { PermissionInconsistentAlert } from './permission-inconsistent-alert'
@ -33,7 +33,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
entry,
disabled
}) => {
const noteId = useApplicationState((state) => state.noteDetails?.primaryAddress)
const noteId = useApplicationState((state) => state.noteDetails?.primaryAlias)
const { showErrorNotification } = useUiNotifications()
const { [SpecialGroup.EVERYONE]: everyonePermission, [SpecialGroup.LOGGED_IN]: loggedInPermission } =
useGetSpecialPermissions()
@ -94,7 +94,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps & Permission
<PermissionInconsistentAlert show={permissionInconsistent ?? false} />
<PermissionEntryButtons
type={PermissionType.USER}
currentSetting={entry.canEdit ? GuestAccess.WRITE : GuestAccess.READ}
currentSetting={entry.canEdit ? PermissionLevel.WRITE : PermissionLevel.READ}
name={value.displayName}
onSetReadOnly={onSetEntryReadOnly}
onSetWriteable={onSetEntryWriteable}

View file

@ -6,7 +6,7 @@
import { useIsOwner } from '../../../../../../hooks/common/use-is-owner'
import type { PermissionDisabledProps } from './permission-disabled.prop'
import { PermissionEntrySpecialGroup } from './permission-entry-special-group'
import { GuestAccess, SpecialGroup } from '@hedgedoc/commons'
import { PermissionLevel, SpecialGroup } from '@hedgedoc/commons'
import React, { Fragment, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useGetSpecialPermissions } from './hooks/use-get-special-permissions'
@ -23,8 +23,16 @@ export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> =
const specialGroupEntries = useMemo(() => {
return {
everyoneLevel: groupEveryone ? (groupEveryone.canEdit ? GuestAccess.WRITE : GuestAccess.READ) : GuestAccess.DENY,
loggedInLevel: groupLoggedIn ? (groupLoggedIn.canEdit ? GuestAccess.WRITE : GuestAccess.READ) : GuestAccess.DENY,
everyoneLevel: groupEveryone
? groupEveryone.canEdit
? PermissionLevel.WRITE
: PermissionLevel.READ
: PermissionLevel.DENY,
loggedInLevel: groupLoggedIn
? groupLoggedIn.canEdit
? PermissionLevel.WRITE
: PermissionLevel.READ
: PermissionLevel.DENY,
loggedInInconsistentAlert: groupEveryone && (!groupLoggedIn || (groupEveryone.canEdit && !groupLoggedIn.canEdit))
}
}, [groupEveryone, groupLoggedIn])

View file

@ -12,11 +12,11 @@ import React, { useMemo } from 'react'
import { ListGroup } from 'react-bootstrap'
interface RevisionListProps {
selectedRevisionId?: number
selectedRevisionId?: string
revisions?: RevisionMetadataDto[]
loadingRevisions: boolean
error?: Error | boolean
onRevisionSelect: (selectedRevisionId: number) => void
onRevisionSelect: (selectedRevisionId: string) => void
}
/**
@ -47,10 +47,10 @@ export const RevisionList: React.FC<RevisionListProps> = ({
})
.map((revisionListEntry) => (
<RevisionListEntry
active={selectedRevisionId === revisionListEntry.id}
onSelect={() => onRevisionSelect(revisionListEntry.id)}
active={selectedRevisionId === revisionListEntry.uuid}
onSelect={() => onRevisionSelect(revisionListEntry.uuid)}
revision={revisionListEntry}
key={revisionListEntry.id}
key={revisionListEntry.uuid}
/>
))
}, [loadingRevisions, onRevisionSelect, revisions, selectedRevisionId])

View file

@ -26,18 +26,18 @@ import { useAsync } from 'react-use'
export const RevisionModalBody = ({ onShowDeleteModal, onHide }: RevisionModal) => {
useTranslation()
const isOwner = useIsOwner()
const [selectedRevisionId, setSelectedRevisionId] = useState<number>()
const noteId = useApplicationState((state) => state.noteDetails?.id)
const [selectedRevisionId, setSelectedRevisionId] = useState<string>()
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
const {
value: revisions,
error,
loading
} = useAsync(async () => {
if (!noteId) {
if (!noteAlias) {
return []
}
return getAllRevisions(noteId)
}, [noteId])
return getAllRevisions(noteAlias)
}, [noteAlias])
const revisionLength = revisions?.length ?? 0
const enableDeleteRevisions = revisionLength > 1 && isOwner

View file

@ -15,7 +15,7 @@ import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
interface RevisionModalFooter {
selectedRevisionId?: number
selectedRevisionId?: string
disableDeleteRevisions: boolean
}
@ -37,7 +37,7 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
disableDeleteRevisions
}) => {
useTranslation()
const noteId = useApplicationState((state) => state.noteDetails?.id)
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
const { showErrorNotification } = useUiNotifications()
const onRevertToRevision = useCallback(() => {
@ -47,15 +47,15 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps> = ({
}, [])
const onDownloadRevision = useCallback(() => {
if (selectedRevisionId === undefined || noteId === undefined) {
if (selectedRevisionId === undefined || noteAlias === undefined) {
return
}
getRevision(noteId, selectedRevisionId)
getRevision(noteAlias, selectedRevisionId)
.then((revision) => {
downloadRevision(noteId, revision)
downloadRevision(noteAlias, revision)
})
.catch(showErrorNotification(''))
}, [noteId, selectedRevisionId, showErrorNotification])
}, [noteAlias, selectedRevisionId, showErrorNotification])
const openDeleteModal = useCallback(() => {
onHide?.()

View file

@ -14,7 +14,7 @@ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
import { useAsync } from 'react-use'
export interface RevisionViewerProps {
selectedRevisionId?: number
selectedRevisionId?: string
}
/**
@ -24,7 +24,7 @@ export interface RevisionViewerProps {
* @param allRevisions List of metadata for all available revisions.
*/
export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevisionId }) => {
const noteId = useApplicationState((state) => state.noteDetails?.id)
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
const darkModeEnabled = useDarkModeState()
const {
@ -32,10 +32,10 @@ export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevision
error,
loading
} = useAsync(async () => {
if (noteId && selectedRevisionId !== undefined) {
return await getRevision(noteId, selectedRevisionId)
if (noteAlias && selectedRevisionId !== undefined) {
return await getRevision(noteAlias, selectedRevisionId)
}
}, [selectedRevisionId, noteId])
}, [selectedRevisionId, noteAlias])
const previousRevisionContent = useMemo(() => {
if (revision === undefined) {
@ -49,7 +49,7 @@ export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevision
return applyPatch(revision.content, inversePatch) || ''
}, [revision])
if (!noteId || selectedRevisionId === undefined) {
if (!noteAlias || selectedRevisionId === undefined) {
return <Fragment />
}

View file

@ -24,16 +24,16 @@ export interface LinkFieldProps {
*/
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
const baseUrl = useBaseUrl()
const noteId = useApplicationState((state) => state.noteDetails?.id)
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
const url = useMemo(() => {
if (noteId === undefined) {
if (noteAlias === undefined) {
return null
}
const url = new URL(baseUrl)
url.pathname += `${type}/${noteId}`
url.pathname += `${type}/${noteAlias}`
return url.toString()
}, [baseUrl, noteId, type])
}, [baseUrl, noteAlias, type])
return !url ? null : <CopyableField content={url} shareOriginUrl={url} />
}

View file

@ -10,7 +10,7 @@ import { NewNoteButton } from '../../common/new-note-button/new-note-button'
import { HistoryButton } from '../../layout/app-bar/app-bar-elements/help-dropdown/history-button'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { Trans, useTranslation } from 'react-i18next'
import { GuestAccess } from '@hedgedoc/commons'
import { PermissionLevel } from '@hedgedoc/commons'
/**
* Renders the card with the options for not logged-in users.
@ -20,7 +20,7 @@ export const GuestCard: React.FC = () => {
useTranslation()
if (guestAccessLevel === GuestAccess.DENY) {
if (guestAccessLevel === PermissionLevel.DENY) {
return null
}
@ -34,7 +34,7 @@ export const GuestCard: React.FC = () => {
<NewNoteButton />
<HistoryButton />
</div>
{guestAccessLevel !== GuestAccess.CREATE && (
{guestAccessLevel !== PermissionLevel.CREATE && (
<div className={'text-muted mt-2 small'}>
<Trans i18nKey={'login.guest.noteCreationDisabled'} />
</div>

View file

@ -7,7 +7,7 @@
import React, { Fragment, useMemo } from 'react'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import type { AuthProviderWithCustomNameDto } from '@hedgedoc/commons'
import { ProviderType } from '@hedgedoc/commons'
import { AuthProviderType } from '@hedgedoc/commons'
import { LdapLoginCard } from './ldap-login-card'
/**
@ -18,7 +18,7 @@ export const LdapLoginCards: React.FC = () => {
const ldapProviders = useMemo(() => {
return authProviders
.filter((provider) => provider.type === ProviderType.LDAP)
.filter((provider) => provider.type === AuthProviderType.LDAP)
.map((provider) => {
const ldapProvider = provider as AuthProviderWithCustomNameDto
return (

View file

@ -7,7 +7,7 @@
import React, { useMemo } from 'react'
import { Card } from 'react-bootstrap'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { ProviderType } from '@hedgedoc/commons'
import { AuthProviderType } from '@hedgedoc/commons'
import { LocalLoginCardBody } from './local-login-card-body'
import { LocalRegisterCardBody } from './register/local-register-card-body'
@ -18,7 +18,7 @@ export const LocalLoginCard: React.FC = () => {
const frontendConfig = useFrontendConfig()
const localLoginEnabled = useMemo(() => {
return frontendConfig.authProviders.some((provider) => provider.type === ProviderType.LOCAL)
return frontendConfig.authProviders.some((provider) => provider.type === AuthProviderType.LOCAL)
}, [frontendConfig])
if (!localLoginEnabled) {

View file

@ -17,7 +17,7 @@ import {
} from 'react-bootstrap-icons'
import { Logger } from '../../../utils/logger'
import type { AuthProviderDto } from '@hedgedoc/commons'
import { ProviderType } from '@hedgedoc/commons'
import { AuthProviderType } from '@hedgedoc/commons'
import { IconGitlab } from '../../common/icons/additional/icon-gitlab'
import styles from './one-click-login-button.module.scss'
@ -37,7 +37,7 @@ const logger = new Logger('GetOneClickProviderMetadata')
* @return Name, icon, URL and CSS class of the given provider for rendering a login button.
*/
export const getOneClickProviderMetadata = (provider: AuthProviderDto): OneClickMetadata => {
if (provider.type !== ProviderType.OIDC) {
if (provider.type !== AuthProviderType.OIDC) {
logger.warn('Metadata for one-click-provider does not exist', provider)
return {
name: '',

View file

@ -5,7 +5,7 @@
*/
import type { AuthProviderDto } from '@hedgedoc/commons'
import { ProviderType } from '@hedgedoc/commons'
import { AuthProviderType } from '@hedgedoc/commons'
/**
* Filters the given auth providers to one-click providers only.
@ -13,5 +13,5 @@ import { ProviderType } from '@hedgedoc/commons'
* @return only one click auth providers
*/
export const filterOneClickProviders = (authProviders: AuthProviderDto[]) => {
return authProviders.filter((provider: AuthProviderDto): boolean => provider.type === ProviderType.OIDC)
return authProviders.filter((provider: AuthProviderDto): boolean => provider.type === AuthProviderType.OIDC)
}