mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-09 09:41:03 +00:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
227ec717e6
@ -58,7 +58,8 @@ import {
|
|||||||
TypeDate,
|
TypeDate,
|
||||||
TypeRef,
|
TypeRef,
|
||||||
TypeString,
|
TypeString,
|
||||||
UX
|
UX,
|
||||||
|
TypeBoolean
|
||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import calendar, { TEvent } from '@hcengineering/model-calendar'
|
import calendar, { TEvent } from '@hcengineering/model-calendar'
|
||||||
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
|
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
|
||||||
@ -106,9 +107,14 @@ export class TRoom extends TDoc implements Room {
|
|||||||
x!: number
|
x!: number
|
||||||
y!: number
|
y!: number
|
||||||
|
|
||||||
language!: RoomLanguage
|
@Prop(TypeString(), love.string.Language, { editor: love.component.RoomLanguageEditor })
|
||||||
startWithTranscription!: boolean
|
language!: RoomLanguage
|
||||||
startWithRecording!: boolean
|
|
||||||
|
@Prop(TypeBoolean(), love.string.StartWithTranscription)
|
||||||
|
startWithTranscription!: boolean
|
||||||
|
|
||||||
|
@Prop(TypeBoolean(), love.string.StartWithRecording)
|
||||||
|
startWithRecording!: boolean
|
||||||
|
|
||||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||||
attachments?: number
|
attachments?: number
|
||||||
|
@ -28,7 +28,8 @@ export default mergeIds(loveId, love, {
|
|||||||
Settings: '' as AnyComponent,
|
Settings: '' as AnyComponent,
|
||||||
LoveWidget: '' as AnyComponent,
|
LoveWidget: '' as AnyComponent,
|
||||||
MeetingWidget: '' as AnyComponent,
|
MeetingWidget: '' as AnyComponent,
|
||||||
WidgetSwitcher: '' as AnyComponent
|
WidgetSwitcher: '' as AnyComponent,
|
||||||
|
RoomLanguageEditor: '' as AnyComponent
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
Love: '' as Ref<Doc>
|
Love: '' as Ref<Doc>
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
{#each createMessages as valueMessage, index}
|
{#each createMessages as valueMessage, index}
|
||||||
<DocUpdateMessageObjectValue
|
<DocUpdateMessageObjectValue
|
||||||
|
attachedTo={valueMessage.attachedTo}
|
||||||
objectClass={valueMessage.objectClass}
|
objectClass={valueMessage.objectClass}
|
||||||
objectId={valueMessage.objectId}
|
objectId={valueMessage.objectId}
|
||||||
action={valueMessage.action}
|
action={valueMessage.action}
|
||||||
@ -74,6 +75,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{#each removeMessages as valueMessage, index}
|
{#each removeMessages as valueMessage, index}
|
||||||
<DocUpdateMessageObjectValue
|
<DocUpdateMessageObjectValue
|
||||||
|
attachedTo={valueMessage.attachedTo}
|
||||||
objectClass={valueMessage.objectClass}
|
objectClass={valueMessage.objectClass}
|
||||||
objectId={valueMessage.objectId}
|
objectId={valueMessage.objectId}
|
||||||
action={valueMessage.action}
|
action={valueMessage.action}
|
||||||
@ -87,6 +89,7 @@
|
|||||||
{@const len = valueMessages.length}
|
{@const len = valueMessages.length}
|
||||||
{#each valueMessages as valueMessage, index}
|
{#each valueMessages as valueMessage, index}
|
||||||
<DocUpdateMessageObjectValue
|
<DocUpdateMessageObjectValue
|
||||||
|
attachedTo={valueMessage.attachedTo}
|
||||||
objectClass={valueMessage.objectClass}
|
objectClass={valueMessage.objectClass}
|
||||||
objectId={valueMessage.objectId}
|
objectId={valueMessage.objectId}
|
||||||
action={valueMessage.action}
|
action={valueMessage.action}
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
isAttachedDoc
|
isAttachedDoc
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
|
|
||||||
|
export let attachedTo: DisplayDocUpdateMessage['attachedTo']
|
||||||
export let objectClass: DisplayDocUpdateMessage['objectClass']
|
export let objectClass: DisplayDocUpdateMessage['objectClass']
|
||||||
export let objectId: DisplayDocUpdateMessage['objectId']
|
export let objectId: DisplayDocUpdateMessage['objectId']
|
||||||
export let action: DisplayDocUpdateMessage['action']
|
export let action: DisplayDocUpdateMessage['action']
|
||||||
@ -51,8 +52,8 @@
|
|||||||
return await getDocLinkTitle(client, object._id, object._class, object)
|
return await getDocLinkTitle(client, object._id, object._class, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadObject (_id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<void> {
|
async function loadObject (_id: Ref<Doc>, _class: Ref<Class<Doc>>, attachedTo: Ref<Doc>): Promise<void> {
|
||||||
const isRemoved = await checkIsObjectRemoved(client, _id, _class)
|
const isRemoved = attachedTo === _id ? false : await checkIsObjectRemoved(client, _id, _class)
|
||||||
|
|
||||||
if (isRemoved) {
|
if (isRemoved) {
|
||||||
object = await buildRemovedDoc(client, _id, _class)
|
object = await buildRemovedDoc(client, _id, _class)
|
||||||
@ -64,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: void loadObject(objectId, objectClass)
|
$: void loadObject(objectId, objectClass, attachedTo)
|
||||||
|
|
||||||
function getPanelComponent (object: Doc, objectPanel?: ObjectPanel): AnyComponent {
|
function getPanelComponent (object: Doc, objectPanel?: ObjectPanel): AnyComponent {
|
||||||
if (objectPanel !== undefined) {
|
if (objectPanel !== undefined) {
|
||||||
|
@ -148,7 +148,7 @@
|
|||||||
object = detail.object
|
object = detail.object
|
||||||
}
|
}
|
||||||
|
|
||||||
openChannel(selectedData.id, selectedData._class)
|
openChannel(selectedData.id, selectedData._class, undefined, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineSeparators('chat', [
|
defineSeparators('chat', [
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
group: 'edit',
|
group: 'edit',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const id = await getObjectLinkId(linkProviders, object._id, object._class, object)
|
const id = await getObjectLinkId(linkProviders, object._id, object._class, object)
|
||||||
openChannel(id, object._class)
|
openChannel(id, object._class, undefined, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -77,5 +77,6 @@
|
|||||||
onExtensions={handleExtensions}
|
onExtensions={handleExtensions}
|
||||||
on:update
|
on:update
|
||||||
on:open-document
|
on:open-document
|
||||||
|
on:loaded
|
||||||
bind:this={collabEditor}
|
bind:this={collabEditor}
|
||||||
/>
|
/>
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
//
|
//
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import activity from '@hcengineering/activity'
|
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import core, { Doc, Ref, WithLookup, generateId, type Blob } from '@hcengineering/core'
|
import core, { Doc, Ref, WithLookup, generateId, type Blob } from '@hcengineering/core'
|
||||||
import { Document, DocumentEvents } from '@hcengineering/document'
|
import { Document, DocumentEvents } from '@hcengineering/document'
|
||||||
@ -59,7 +58,6 @@
|
|||||||
import DocumentEditor from './DocumentEditor.svelte'
|
import DocumentEditor from './DocumentEditor.svelte'
|
||||||
import DocumentPresenter from './DocumentPresenter.svelte'
|
import DocumentPresenter from './DocumentPresenter.svelte'
|
||||||
import DocumentTitle from './DocumentTitle.svelte'
|
import DocumentTitle from './DocumentTitle.svelte'
|
||||||
import Activity from './sidebar/Activity.svelte'
|
|
||||||
import History from './sidebar/History.svelte'
|
import History from './sidebar/History.svelte'
|
||||||
import References from './sidebar/References.svelte'
|
import References from './sidebar/References.svelte'
|
||||||
|
|
||||||
@ -88,11 +86,14 @@
|
|||||||
|
|
||||||
let headings: Heading[] = []
|
let headings: Heading[] = []
|
||||||
|
|
||||||
|
let loadedDocumentContent = false
|
||||||
|
|
||||||
const notificationClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
|
const notificationClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
|
||||||
|
|
||||||
$: read(_id)
|
$: read(_id)
|
||||||
function read (_id: Ref<Doc>): void {
|
function read (_id: Ref<Doc>): void {
|
||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
|
loadedDocumentContent = false
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void notificationClient.then((client) => client.readDoc(prev))
|
void notificationClient.then((client) => client.readDoc(prev))
|
||||||
@ -200,11 +201,6 @@
|
|||||||
id: 'references',
|
id: 'references',
|
||||||
icon: document.icon.References,
|
icon: document.icon.References,
|
||||||
showTooltip: { label: document.string.Backlinks, direction: 'bottom' }
|
showTooltip: { label: document.string.Backlinks, direction: 'bottom' }
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'activity',
|
|
||||||
icon: activity.icon.Activity,
|
|
||||||
showTooltip: { label: activity.string.Activity, direction: 'bottom' }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
let selectedAside: string | boolean = false
|
let selectedAside: string | boolean = false
|
||||||
@ -248,8 +244,8 @@
|
|||||||
|
|
||||||
{#if doc !== undefined}
|
{#if doc !== undefined}
|
||||||
<Panel
|
<Panel
|
||||||
|
withoutActivity={!loadedDocumentContent}
|
||||||
object={doc}
|
object={doc}
|
||||||
withoutActivity
|
|
||||||
allowClose={!embedded}
|
allowClose={!embedded}
|
||||||
isAside={true}
|
isAside={true}
|
||||||
customAside={aside}
|
customAside={aside}
|
||||||
@ -367,7 +363,7 @@
|
|||||||
{readonly}
|
{readonly}
|
||||||
boundary={content}
|
boundary={content}
|
||||||
overflow={'none'}
|
overflow={'none'}
|
||||||
editorAttributes={{ style: 'padding: 0 2em 30vh; margin: 0 -2em;' }}
|
editorAttributes={{ style: 'padding: 0 2em 2em; margin: 0 -2em; min-height: 30vh' }}
|
||||||
attachFile={async (file) => {
|
attachFile={async (file) => {
|
||||||
return await createEmbedding(file)
|
return await createEmbedding(file)
|
||||||
}}
|
}}
|
||||||
@ -381,6 +377,9 @@
|
|||||||
navigate(location)
|
navigate(location)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
on:loaded={() => {
|
||||||
|
loadedDocumentContent = true
|
||||||
|
}}
|
||||||
bind:this={editor}
|
bind:this={editor}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
@ -390,8 +389,6 @@
|
|||||||
<svelte:fragment slot="aside">
|
<svelte:fragment slot="aside">
|
||||||
{#if selectedAside === 'references'}
|
{#if selectedAside === 'references'}
|
||||||
<References doc={doc._id} />
|
<References doc={doc._id} />
|
||||||
{:else if selectedAside === 'activity'}
|
|
||||||
<Activity value={doc} />
|
|
||||||
{:else if selectedAside === 'history'}
|
{:else if selectedAside === 'history'}
|
||||||
<History value={doc} {readonly} />
|
<History value={doc} {readonly} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -115,7 +115,6 @@
|
|||||||
let currentSpace: Ref<Space> | undefined
|
let currentSpace: Ref<Space> | undefined
|
||||||
let currentSpecial: string | undefined
|
let currentSpecial: string | undefined
|
||||||
let specialComponent: SpecialNavModel | undefined
|
let specialComponent: SpecialNavModel | undefined
|
||||||
let asideId: string | undefined
|
|
||||||
let currentFragment: string | undefined = ''
|
let currentFragment: string | undefined = ''
|
||||||
|
|
||||||
let currentApplication: Application | undefined
|
let currentApplication: Application | undefined
|
||||||
@ -124,13 +123,10 @@
|
|||||||
|
|
||||||
function setSpaceSpecial (spaceSpecial: string | undefined): void {
|
function setSpaceSpecial (spaceSpecial: string | undefined): void {
|
||||||
if (currentSpecial !== undefined && spaceSpecial === currentSpecial) return
|
if (currentSpecial !== undefined && spaceSpecial === currentSpecial) return
|
||||||
if (asideId !== undefined && spaceSpecial === asideId) return
|
|
||||||
if (spaceSpecial === undefined) return
|
if (spaceSpecial === undefined) return
|
||||||
specialComponent = getSpecialComponent(spaceSpecial)
|
specialComponent = getSpecialComponent(spaceSpecial)
|
||||||
if (specialComponent !== undefined) {
|
if (specialComponent !== undefined) {
|
||||||
currentSpecial = spaceSpecial
|
currentSpecial = spaceSpecial
|
||||||
} else if (navigatorModel?.aside !== undefined || currentApplication?.aside !== undefined) {
|
|
||||||
asideId = spaceSpecial
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,10 +171,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (special !== currentSpecial && (navigatorModel?.aside || currentApplication?.aside)) {
|
|
||||||
asideId = special
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fragment !== currentFragment) {
|
if (fragment !== currentFragment) {
|
||||||
currentFragment = fragment
|
currentFragment = fragment
|
||||||
if (fragment != null && fragment.trim().length > 0) {
|
if (fragment != null && fragment.trim().length > 0) {
|
||||||
@ -250,13 +242,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAside (): void {
|
|
||||||
const loc = getLocation()
|
|
||||||
loc.path.length = 4
|
|
||||||
asideId = undefined
|
|
||||||
navigate(loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getWindowTitle (loc: Location): Promise<string | undefined> {
|
async function getWindowTitle (loc: Location): Promise<string | undefined> {
|
||||||
if (loc.fragment == null) return
|
if (loc.fragment == null) return
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -277,7 +262,6 @@
|
|||||||
|
|
||||||
defineSeparators('workbenchGuest', workbenchGuestSeparators)
|
defineSeparators('workbenchGuest', workbenchGuestSeparators)
|
||||||
|
|
||||||
let aside: HTMLElement
|
|
||||||
let cover: HTMLElement
|
let cover: HTMLElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -303,15 +287,6 @@
|
|||||||
<SpaceView {currentSpace} {currentView} />
|
<SpaceView {currentSpace} {currentView} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if asideId}
|
|
||||||
{@const asideComponent = navigatorModel?.aside ?? currentApplication?.aside}
|
|
||||||
{#if asideComponent !== undefined}
|
|
||||||
<Separator name={'workbenchGuest'} index={0} />
|
|
||||||
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
|
|
||||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div bind:this={cover} class="cover" />
|
<div bind:this={cover} class="cover" />
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Stav",
|
"Status": "Stav",
|
||||||
"Active": "Aktivní",
|
"Active": "Aktivní",
|
||||||
"Finished": "Dokončeno",
|
"Finished": "Dokončeno",
|
||||||
"StartWithRecording": "Začít s nahráváním"
|
"StartWithRecording": "Začít s nahráváním",
|
||||||
|
"Language": "Jazyk"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"Finished": "Finished",
|
"Finished": "Finished",
|
||||||
"StartWithRecording": "Start with recording"
|
"StartWithRecording": "Start with recording",
|
||||||
|
"Language": "Language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Estado",
|
"Status": "Estado",
|
||||||
"Active": "Activo",
|
"Active": "Activo",
|
||||||
"Finished": "Terminado",
|
"Finished": "Terminado",
|
||||||
"StartWithRecording": "Iniciar con grabación"
|
"StartWithRecording": "Iniciar con grabación",
|
||||||
|
"Language": "Idioma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Statut",
|
"Status": "Statut",
|
||||||
"Active": "Actif",
|
"Active": "Actif",
|
||||||
"Finished": "Terminé",
|
"Finished": "Terminé",
|
||||||
"StartWithRecording": "Démarrer avec l'enregistrement"
|
"StartWithRecording": "Démarrer avec l'enregistrement",
|
||||||
|
"Language": "Langue"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Stato",
|
"Status": "Stato",
|
||||||
"Active": "Attivo",
|
"Active": "Attivo",
|
||||||
"Finished": "Finito",
|
"Finished": "Finito",
|
||||||
"StartWithRecording": "Inizia con la registrazione"
|
"StartWithRecording": "Inizia con la registrazione",
|
||||||
|
"Language": "Lingua"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Estado",
|
"Status": "Estado",
|
||||||
"Active": "Ativo",
|
"Active": "Ativo",
|
||||||
"Finished": "Finalizado",
|
"Finished": "Finalizado",
|
||||||
"StartWithRecording": "Começar com gravação"
|
"StartWithRecording": "Começar com gravação",
|
||||||
|
"Language": "Idioma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "Статус",
|
"Status": "Статус",
|
||||||
"Active": "Активно",
|
"Active": "Активно",
|
||||||
"Finished": "Завершено",
|
"Finished": "Завершено",
|
||||||
"StartWithRecording": "Начинать с записью"
|
"StartWithRecording": "Начинать с записью",
|
||||||
|
"Language": "Язык"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"Status": "状态",
|
"Status": "状态",
|
||||||
"Active": "活动",
|
"Active": "活动",
|
||||||
"Finished": "已完成",
|
"Finished": "已完成",
|
||||||
"StartWithRecording": "开始录制"
|
"StartWithRecording": "开始录制",
|
||||||
|
"Language": "语言"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
{#if inline}
|
{#if inline}
|
||||||
<ObjectMention object={floor} />
|
<ObjectMention object={floor} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex-presenter overflow-label sm-tool-icon">
|
<div class="flex-presenter overflow-label sm-tool-icon ml-3">
|
||||||
{floor.name}
|
{floor.name}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -18,10 +18,14 @@
|
|||||||
import { languagesDisplayData } from '../types'
|
import { languagesDisplayData } from '../types'
|
||||||
|
|
||||||
export let room: Room
|
export let room: Room
|
||||||
|
export let withLabel = false
|
||||||
|
|
||||||
$: lang = room.language
|
$: lang = room.language
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span title={languagesDisplayData[lang].label ?? languagesDisplayData.en.label}>
|
<span title={languagesDisplayData[lang].label ?? languagesDisplayData.en.label}>
|
||||||
{languagesDisplayData[lang].emoji ?? languagesDisplayData.en.emoji}
|
{languagesDisplayData[lang].emoji ?? languagesDisplayData.en.emoji}
|
||||||
|
{#if withLabel}
|
||||||
|
<span class="ml-1">{languagesDisplayData[lang].label ?? languagesDisplayData.en.label}</span>
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Room, RoomLanguage } from '@hcengineering/love'
|
||||||
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
|
|
||||||
|
import RoomLanguageComponent from './RoomLanguage.svelte'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
type ButtonKind,
|
||||||
|
type ButtonSize,
|
||||||
|
DropdownIntlItem,
|
||||||
|
DropdownLabelsPopupIntl,
|
||||||
|
eventToHTMLElement,
|
||||||
|
showPopup
|
||||||
|
} from '@hcengineering/ui'
|
||||||
|
import { languagesDisplayData } from '../types'
|
||||||
|
import LanguageIcon from './LanguageIcon.svelte'
|
||||||
|
|
||||||
|
export let value: RoomLanguage = 'en'
|
||||||
|
export let object: Room | undefined = undefined
|
||||||
|
export let onChange: (value: any) => void
|
||||||
|
export let disabled: boolean = false
|
||||||
|
export let kind: ButtonKind = 'no-border'
|
||||||
|
export let size: ButtonSize = 'small'
|
||||||
|
export let justify: 'left' | 'center' = 'center'
|
||||||
|
export let width: string | undefined = 'fit-content'
|
||||||
|
|
||||||
|
let shown: boolean = false
|
||||||
|
|
||||||
|
let items: DropdownIntlItem[] = []
|
||||||
|
$: items = Object.entries(languagesDisplayData).map(([lang, data]) => ({
|
||||||
|
id: lang,
|
||||||
|
label: getEmbeddedLabel(data.label),
|
||||||
|
icon: LanguageIcon,
|
||||||
|
iconProps: { lang }
|
||||||
|
}))
|
||||||
|
|
||||||
|
function showLanguagesPopup (ev: MouseEvent): void {
|
||||||
|
shown = true
|
||||||
|
showPopup(DropdownLabelsPopupIntl, { items, selected: value }, eventToHTMLElement(ev), async (result) => {
|
||||||
|
if (result != null && result !== '') {
|
||||||
|
value = result
|
||||||
|
onChange(value)
|
||||||
|
|
||||||
|
shown = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if object}
|
||||||
|
<Button
|
||||||
|
{kind}
|
||||||
|
{size}
|
||||||
|
{justify}
|
||||||
|
{width}
|
||||||
|
{disabled}
|
||||||
|
on:click={(ev) => {
|
||||||
|
if (!shown && !disabled) {
|
||||||
|
showLanguagesPopup(ev)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
<div class="pointer-events-none"><RoomLanguageComponent room={object} withLabel /></div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
@ -23,6 +23,7 @@ import PanelControlBar from './components/PanelControlBar.svelte'
|
|||||||
import RoomPresenter from './components/RoomPresenter.svelte'
|
import RoomPresenter from './components/RoomPresenter.svelte'
|
||||||
import MeetingMinutesDocEditor from './components/MeetingMinutesDocEditor.svelte'
|
import MeetingMinutesDocEditor from './components/MeetingMinutesDocEditor.svelte'
|
||||||
import MeetingMinutesStatusPresenter from './components/MeetingMinutesStatusPresenter.svelte'
|
import MeetingMinutesStatusPresenter from './components/MeetingMinutesStatusPresenter.svelte'
|
||||||
|
import RoomLanguageEditor from './components/RoomLanguageEditor.svelte'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyGuestLink,
|
copyGuestLink,
|
||||||
@ -59,7 +60,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
PanelControlBar,
|
PanelControlBar,
|
||||||
RoomPresenter,
|
RoomPresenter,
|
||||||
MeetingMinutesDocEditor,
|
MeetingMinutesDocEditor,
|
||||||
MeetingMinutesStatusPresenter
|
MeetingMinutesStatusPresenter,
|
||||||
|
RoomLanguageEditor
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
CreateMeeting: createMeeting,
|
CreateMeeting: createMeeting,
|
||||||
|
@ -94,6 +94,7 @@ export default mergeIds(loveId, love, {
|
|||||||
KnockAction: '' as IntlString,
|
KnockAction: '' as IntlString,
|
||||||
Select: '' as IntlString,
|
Select: '' as IntlString,
|
||||||
ChooseShare: '' as IntlString,
|
ChooseShare: '' as IntlString,
|
||||||
MoreOptions: '' as IntlString
|
MoreOptions: '' as IntlString,
|
||||||
|
Language: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -140,6 +140,10 @@
|
|||||||
void localProvider.loaded.then(() => (localSynced = true))
|
void localProvider.loaded.then(() => (localSynced = true))
|
||||||
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
||||||
|
|
||||||
|
void Promise.all([localProvider.loaded, remoteProvider.loaded]).then(() => {
|
||||||
|
dispatch('loaded')
|
||||||
|
})
|
||||||
|
|
||||||
let editor: Editor
|
let editor: Editor
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
let textToolbarElement: HTMLElement
|
let textToolbarElement: HTMLElement
|
||||||
|
@ -103,6 +103,7 @@
|
|||||||
on:update
|
on:update
|
||||||
on:open-document
|
on:open-document
|
||||||
on:blur
|
on:blur
|
||||||
|
on:loaded
|
||||||
on:focus={handleFocus}
|
on:focus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||||
import { type FileAttachFunction } from './extension/types'
|
import { type FileAttachFunction } from './extension/types'
|
||||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
import { completionConfig, InlineCommandId, inlineCommandsConfig } from './extensions'
|
||||||
|
import { MermaidExtension, mermaidOptions } from './extension/mermaid'
|
||||||
|
|
||||||
export let label: IntlString | undefined = undefined
|
export let label: IntlString | undefined = undefined
|
||||||
export let content: Markup
|
export let content: Markup
|
||||||
@ -192,6 +193,7 @@
|
|||||||
}
|
}
|
||||||
extensions.push(
|
extensions.push(
|
||||||
imageUploadPlugin,
|
imageUploadPlugin,
|
||||||
|
MermaidExtension.configure(mermaidOptions),
|
||||||
FocusExtension.configure({ onCanBlur: (value: boolean) => (canBlur = value), onFocus: handleFocus })
|
FocusExtension.configure({ onCanBlur: (value: boolean) => (canBlur = value), onFocus: handleFocus })
|
||||||
)
|
)
|
||||||
if (enableEmojiReplace) {
|
if (enableEmojiReplace) {
|
||||||
@ -199,10 +201,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enableInlineCommands) {
|
if (enableInlineCommands) {
|
||||||
|
const excludedInlineCommands: InlineCommandId[] = ['drawing-board', 'todo-list']
|
||||||
|
|
||||||
|
if (attachFile === undefined) excludedInlineCommands.push('image')
|
||||||
|
|
||||||
extensions.push(
|
extensions.push(
|
||||||
InlineCommandsExtension.configure(
|
InlineCommandsExtension.configure(inlineCommandsConfig(handleCommandSelected, excludedInlineCommands))
|
||||||
inlineCommandsConfig(handleCommandSelected, attachFile === undefined ? ['image'] : [])
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,11 +230,13 @@
|
|||||||
}
|
}
|
||||||
case 'code-block':
|
case 'code-block':
|
||||||
editor.editorHandler.insertCodeBlock(pos)
|
editor.editorHandler.insertCodeBlock(pos)
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'separator-line':
|
case 'separator-line':
|
||||||
editor.editorHandler.insertSeparatorLine()
|
editor.editorHandler.insertSeparatorLine()
|
||||||
break
|
break
|
||||||
|
case 'mermaid':
|
||||||
|
editor.getEditor()?.commands.insertContentAt(pos, { type: 'mermaid' })
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,10 +17,9 @@
|
|||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { EmptyMarkup } from '@hcengineering/text'
|
import { EmptyMarkup } from '@hcengineering/text'
|
||||||
import { Button, type ButtonSize, Scroller } from '@hcengineering/ui'
|
import { Button, type ButtonSize, Scroller } from '@hcengineering/ui'
|
||||||
import { AnyExtension, mergeAttributes } from '@tiptap/core'
|
import { AnyExtension, mergeAttributes, type Editor } from '@tiptap/core'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import textEditor, { RefAction, TextEditorHandler, TextFormatCategory } from '@hcengineering/text-editor'
|
import textEditor, { RefAction, TextEditorHandler, TextFormatCategory } from '@hcengineering/text-editor'
|
||||||
|
|
||||||
import { defaultRefActions, getModelRefActions } from './editor/actions'
|
import { defaultRefActions, getModelRefActions } from './editor/actions'
|
||||||
import TextEditor from './TextEditor.svelte'
|
import TextEditor from './TextEditor.svelte'
|
||||||
|
|
||||||
@ -63,6 +62,9 @@
|
|||||||
export function insertText (text: string): void {
|
export function insertText (text: string): void {
|
||||||
editor?.insertText(text)
|
editor?.insertText(text)
|
||||||
}
|
}
|
||||||
|
export function getEditor (): Editor | undefined {
|
||||||
|
return editor?.getEditor()
|
||||||
|
}
|
||||||
|
|
||||||
$: varsStyle =
|
$: varsStyle =
|
||||||
maxHeight === 'card'
|
maxHeight === 'card'
|
||||||
|
@ -90,6 +90,9 @@
|
|||||||
editor.commands.clearContent(true)
|
editor.commands.clearContent(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function getEditor (): Editor {
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
|
||||||
export function insertText (text: string): void {
|
export function insertText (text: string): void {
|
||||||
editor?.commands.insertContent(text)
|
editor?.commands.insertContent(text)
|
||||||
|
@ -130,7 +130,15 @@ export const completionConfig: Partial<CompletionOptions> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inlineCommandsIds = ['image', 'table', 'code-block', 'separator-line', 'todo-list'] as const
|
const inlineCommandsIds = [
|
||||||
|
'image',
|
||||||
|
'table',
|
||||||
|
'code-block',
|
||||||
|
'separator-line',
|
||||||
|
'todo-list',
|
||||||
|
'drawing-board',
|
||||||
|
'mermaid'
|
||||||
|
] as const
|
||||||
export type InlineCommandId = (typeof inlineCommandsIds)[number]
|
export type InlineCommandId = (typeof inlineCommandsIds)[number]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,7 +229,7 @@ async function move (issues: Issue | Issue[]): Promise<void> {
|
|||||||
async function editWorkflowStatuses (project: Project): Promise<void> {
|
async function editWorkflowStatuses (project: Project): Promise<void> {
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
loc.path[2] = settingId
|
loc.path[2] = settingId
|
||||||
loc.path[3] = 'statuses'
|
loc.path[3] = 'spaceTypes'
|
||||||
loc.path[4] = project.type
|
loc.path[4] = project.type
|
||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttachedDoc, Doc } from '@hcengineering/core'
|
import { AttachedDoc, Doc, Ref, Class } from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { isAttachedDoc } from '../utils'
|
import { isAttachedDoc } from '../utils'
|
||||||
import DocsNavigator from './DocsNavigator.svelte'
|
import DocsNavigator from './DocsNavigator.svelte'
|
||||||
@ -26,19 +26,31 @@
|
|||||||
return 'parent' in doc && doc.parent != null
|
return 'parent' in doc && doc.parent != null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getParents (doc: Doc | AttachedDoc): Promise<readonly Doc[]> {
|
function getParentId (doc: Doc | AttachedDoc): Ref<Doc> {
|
||||||
if (!isAttachedDoc(doc) && !hasParent(doc)) {
|
return isAttachedDoc(doc) ? doc.attachedTo : (doc as any).parent
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentClass (doc: Doc | AttachedDoc): Ref<Class<Doc>> {
|
||||||
|
return isAttachedDoc(doc) ? doc.attachedToClass : doc._class
|
||||||
|
}
|
||||||
|
|
||||||
|
function withParent (doc: Doc | AttachedDoc): boolean {
|
||||||
|
return isAttachedDoc(doc) || hasParent(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getParents (_id: Ref<Doc>, _class: Ref<Class<Doc>>, showParents: boolean): Promise<readonly Doc[]> {
|
||||||
|
if (!showParents) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const parents: Doc[] = []
|
const parents: Doc[] = []
|
||||||
|
|
||||||
let currentDoc: Doc | undefined = doc
|
let currentDoc: Doc | undefined = element
|
||||||
|
|
||||||
while (currentDoc && (isAttachedDoc(currentDoc) || hasParent(currentDoc))) {
|
while (currentDoc && withParent(currentDoc)) {
|
||||||
const parent: Doc | undefined = isAttachedDoc(currentDoc)
|
const _id = getParentId(currentDoc)
|
||||||
? await client.findOne(currentDoc.attachedToClass, { _id: currentDoc.attachedTo })
|
const _class = getParentClass(currentDoc)
|
||||||
: await client.findOne(currentDoc._class, { _id: (currentDoc as any).parent })
|
const parent: Doc | undefined = await client.findOne(_class, { _id })
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
currentDoc = parent
|
currentDoc = parent
|
||||||
@ -50,8 +62,15 @@
|
|||||||
|
|
||||||
return parents.reverse()
|
return parents.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parents: readonly Doc[] = []
|
||||||
|
|
||||||
|
$: parentId = getParentId(element)
|
||||||
|
$: parentClass = getParentClass(element)
|
||||||
|
$: showParents = withParent(element)
|
||||||
|
$: getParents(parentId, parentClass, showParents).then((res) => {
|
||||||
|
parents = res
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await getParents(element) then parents}
|
<DocsNavigator elements={parents} />
|
||||||
<DocsNavigator elements={parents} />
|
|
||||||
{/await}
|
|
||||||
|
@ -752,9 +752,18 @@ export function categorizeFields (
|
|||||||
|
|
||||||
export function makeViewletKey (loc?: Location): string {
|
export function makeViewletKey (loc?: Location): string {
|
||||||
loc = loc != null ? { path: loc.path } : getCurrentResolvedLocation()
|
loc = loc != null ? { path: loc.path } : getCurrentResolvedLocation()
|
||||||
loc.fragment = undefined
|
|
||||||
loc.query = undefined
|
loc.query = undefined
|
||||||
|
|
||||||
|
if (loc.fragment != null && loc.fragment !== '') {
|
||||||
|
const props = decodeURIComponent(loc.fragment).split('|')
|
||||||
|
if (props.length >= 3) {
|
||||||
|
const [panel, , _class] = props
|
||||||
|
|
||||||
|
return 'viewlet' + '#' + encodeURIComponent([panel, _class].join('|'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loc.fragment = undefined
|
||||||
return 'viewlet' + locationToUrl(loc)
|
return 'viewlet' + locationToUrl(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +136,6 @@
|
|||||||
let currentSpecial: string | undefined
|
let currentSpecial: string | undefined
|
||||||
let currentQuery: Record<string, string | null> | undefined
|
let currentQuery: Record<string, string | null> | undefined
|
||||||
let specialComponent: SpecialNavModel | undefined
|
let specialComponent: SpecialNavModel | undefined
|
||||||
let asideId: string | undefined
|
|
||||||
let currentFragment: string | undefined = ''
|
let currentFragment: string | undefined = ''
|
||||||
|
|
||||||
let currentApplication: Application | undefined
|
let currentApplication: Application | undefined
|
||||||
@ -374,8 +373,6 @@
|
|||||||
loc.path[4] = currentSpecial
|
loc.path[4] = currentSpecial
|
||||||
} else if (loc.path[3] === resolved.defaultLocation.path[3]) {
|
} else if (loc.path[3] === resolved.defaultLocation.path[3]) {
|
||||||
loc.path[4] = resolved.defaultLocation.path[4]
|
loc.path[4] = resolved.defaultLocation.path[4]
|
||||||
} else {
|
|
||||||
loc.path[4] = asideId as string
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loc.path.length = 4
|
loc.path.length = 4
|
||||||
@ -512,10 +509,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (special !== currentSpecial && (navigatorModel?.aside || currentApplication?.aside)) {
|
|
||||||
asideId = special
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app !== undefined) {
|
if (app !== undefined) {
|
||||||
localStorage.setItem(`${locationStorageKeyId}_${app}`, originalLoc)
|
localStorage.setItem(`${locationStorageKeyId}_${app}`, originalLoc)
|
||||||
}
|
}
|
||||||
@ -589,21 +582,12 @@
|
|||||||
specialComponent = undefined
|
specialComponent = undefined
|
||||||
// eslint-disable-next-line no-fallthrough
|
// eslint-disable-next-line no-fallthrough
|
||||||
case 3:
|
case 3:
|
||||||
asideId = undefined
|
|
||||||
if (currentSpace !== undefined) {
|
if (currentSpace !== undefined) {
|
||||||
specialComponent = undefined
|
specialComponent = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAside (): void {
|
|
||||||
const loc = getLocation()
|
|
||||||
loc.path.length = 4
|
|
||||||
asideId = undefined
|
|
||||||
checkOnHide()
|
|
||||||
navigate(loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateSpace (spaceId?: Ref<Space>): Promise<void> {
|
async function updateSpace (spaceId?: Ref<Space>): Promise<void> {
|
||||||
if (spaceId === currentSpace) return
|
if (spaceId === currentSpace) return
|
||||||
clear(2)
|
clear(2)
|
||||||
@ -620,14 +604,11 @@
|
|||||||
|
|
||||||
function setSpaceSpecial (spaceSpecial: string | undefined): void {
|
function setSpaceSpecial (spaceSpecial: string | undefined): void {
|
||||||
if (currentSpecial !== undefined && spaceSpecial === currentSpecial) return
|
if (currentSpecial !== undefined && spaceSpecial === currentSpecial) return
|
||||||
if (asideId !== undefined && spaceSpecial === asideId) return
|
|
||||||
clear(3)
|
clear(3)
|
||||||
if (spaceSpecial === undefined) return
|
if (spaceSpecial === undefined) return
|
||||||
specialComponent = getSpecialComponent(spaceSpecial)
|
specialComponent = getSpecialComponent(spaceSpecial)
|
||||||
if (specialComponent !== undefined) {
|
if (specialComponent !== undefined) {
|
||||||
currentSpecial = spaceSpecial
|
currentSpecial = spaceSpecial
|
||||||
} else if (navigatorModel?.aside !== undefined || currentApplication?.aside !== undefined) {
|
|
||||||
asideId = spaceSpecial
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +628,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let aside: HTMLElement
|
|
||||||
let cover: HTMLElement
|
let cover: HTMLElement
|
||||||
let workbenchWidth: number = $deviceInfo.docWidth
|
let workbenchWidth: number = $deviceInfo.docWidth
|
||||||
|
|
||||||
@ -762,14 +742,6 @@
|
|||||||
person && client.getHierarchy().hasMixin(person, contact.mixin.Employee)
|
person && client.getHierarchy().hasMixin(person, contact.mixin.Employee)
|
||||||
? !client.getHierarchy().as(person, contact.mixin.Employee).active
|
? !client.getHierarchy().as(person, contact.mixin.Employee).active
|
||||||
: false
|
: false
|
||||||
|
|
||||||
let asideComponent: AnyComponent | undefined
|
|
||||||
|
|
||||||
$: if (asideId !== undefined && navigatorModel !== undefined) {
|
|
||||||
asideComponent = navigatorModel?.aside ?? currentApplication?.aside
|
|
||||||
} else {
|
|
||||||
asideComponent = undefined
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if person && deactivated && !isAdminUser()}
|
{#if person && deactivated && !isAdminUser()}
|
||||||
@ -1001,11 +973,8 @@
|
|||||||
<Component
|
<Component
|
||||||
is={currentApplication.component}
|
is={currentApplication.component}
|
||||||
props={{
|
props={{
|
||||||
currentSpace,
|
currentSpace
|
||||||
asideId,
|
|
||||||
asideComponent: currentApplication?.aside
|
|
||||||
}}
|
}}
|
||||||
on:close={closeAside}
|
|
||||||
/>
|
/>
|
||||||
{:else if specialComponent}
|
{:else if specialComponent}
|
||||||
<Component
|
<Component
|
||||||
@ -1035,12 +1004,6 @@
|
|||||||
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
|
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if asideComponent !== undefined}
|
|
||||||
<Separator name={'workbench'} index={1} color={'transparent'} separatorSize={0} short />
|
|
||||||
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
|
|
||||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if !$deviceInfo.aside.float}
|
{#if !$deviceInfo.aside.float}
|
||||||
{#if $sidebarStore.variant === SidebarVariant.EXPANDED}
|
{#if $sidebarStore.variant === SidebarVariant.EXPANDED}
|
||||||
|
@ -192,8 +192,7 @@ export async function buildNavModel (
|
|||||||
const newSpaces = (nm.spaces ?? []).filter((it) => !spaces.some((sp) => sp.id === it.id))
|
const newSpaces = (nm.spaces ?? []).filter((it) => !spaces.some((sp) => sp.id === it.id))
|
||||||
newNavModel = {
|
newNavModel = {
|
||||||
spaces: [...spaces, ...newSpaces],
|
spaces: [...spaces, ...newSpaces],
|
||||||
specials: [...(newNavModel?.specials ?? []), ...(nm.specials ?? [])],
|
specials: [...(newNavModel?.specials ?? []), ...(nm.specials ?? [])]
|
||||||
aside: newNavModel?.aside ?? nm?.aside
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,6 @@ export interface Application extends Doc {
|
|||||||
|
|
||||||
// Also attached ApplicationNavModel will be joined after this one main.
|
// Also attached ApplicationNavModel will be joined after this one main.
|
||||||
navigatorModel?: NavigatorModel
|
navigatorModel?: NavigatorModel
|
||||||
aside?: AnyComponent
|
|
||||||
|
|
||||||
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||||
locationDataResolver?: Resource<(loc: Location) => Promise<LocationData>>
|
locationDataResolver?: Resource<(loc: Location) => Promise<LocationData>>
|
||||||
@ -130,7 +129,6 @@ export interface ApplicationNavModel extends Doc {
|
|||||||
|
|
||||||
spaces?: SpacesNavModel[]
|
spaces?: SpacesNavModel[]
|
||||||
specials?: SpecialNavModel[]
|
specials?: SpecialNavModel[]
|
||||||
aside?: AnyComponent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,7 +161,6 @@ export interface SpacesNavModel {
|
|||||||
export interface NavigatorModel {
|
export interface NavigatorModel {
|
||||||
spaces: SpacesNavModel[]
|
spaces: SpacesNavModel[]
|
||||||
specials?: SpecialNavModel[]
|
specials?: SpecialNavModel[]
|
||||||
aside?: AnyComponent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,6 +62,9 @@ export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
|
|||||||
domain: Domain
|
domain: Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// using for join query security
|
||||||
|
allowedSpaces?: Ref<Space>[]
|
||||||
|
|
||||||
// Optional measure context, for server side operations
|
// Optional measure context, for server side operations
|
||||||
ctx?: MeasureContext
|
ctx?: MeasureContext
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
type MeasureContext,
|
type MeasureContext,
|
||||||
Ref
|
Ref
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { BaseMiddleware, Middleware, type PipelineContext } from '@hcengineering/server-core'
|
import { BaseMiddleware, Middleware, ServerFindOptions, type PipelineContext } from '@hcengineering/server-core'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
|
|
||||||
interface Query {
|
interface Query {
|
||||||
@ -45,7 +45,7 @@ export class QueryJoiner {
|
|||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: ServerFindOptions<T>
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
// Will find a query or add + 1 to callbacks
|
// Will find a query or add + 1 to callbacks
|
||||||
const q = this.findQuery(_class, query, options) ?? this.createQuery(_class, query, options)
|
const q = this.findQuery(_class, query, options) ?? this.createQuery(_class, query, options)
|
||||||
@ -131,7 +131,7 @@ export class QueryJoinMiddleware extends BaseMiddleware implements Middleware {
|
|||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: ServerFindOptions<T>
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
// Will find a query or add + 1 to callbacks
|
// Will find a query or add + 1 to callbacks
|
||||||
return this.joiner.findAll(ctx, _class, query, options)
|
return this.joiner.findAll(ctx, _class, query, options)
|
||||||
|
@ -21,7 +21,6 @@ import core, {
|
|||||||
Doc,
|
Doc,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
Domain,
|
Domain,
|
||||||
FindOptions,
|
|
||||||
FindResult,
|
FindResult,
|
||||||
LookupData,
|
LookupData,
|
||||||
MeasureContext,
|
MeasureContext,
|
||||||
@ -47,7 +46,13 @@ import core, {
|
|||||||
type SessionData
|
type SessionData
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||||
import { BaseMiddleware, Middleware, TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
|
import {
|
||||||
|
BaseMiddleware,
|
||||||
|
Middleware,
|
||||||
|
ServerFindOptions,
|
||||||
|
TxMiddlewareResult,
|
||||||
|
type PipelineContext
|
||||||
|
} from '@hcengineering/server-core'
|
||||||
import { isOwner, isSystem } from './utils'
|
import { isOwner, isSystem } from './utils'
|
||||||
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
|
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
|
||||||
|
|
||||||
@ -416,6 +421,14 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.contextData.broadcast.targets.spaceSec = (tx) => {
|
||||||
|
const space = this.spacesMap.get(tx.objectSpace)
|
||||||
|
if (space === undefined) return undefined
|
||||||
|
if (this.systemSpaces.has(space._id) || this.mainSpaces.includes(space._id)) return undefined
|
||||||
|
|
||||||
|
return space.members.length === 0 ? undefined : this.getTargets(space?.members)
|
||||||
|
}
|
||||||
|
|
||||||
await this.next?.handleBroadcast(ctx)
|
await this.next?.handleBroadcast(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,7 +510,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
ctx: MeasureContext<SessionData>,
|
ctx: MeasureContext<SessionData>,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: ServerFindOptions<T>
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
await this.init(ctx)
|
await this.init(ctx)
|
||||||
|
|
||||||
@ -509,12 +522,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
|
|
||||||
let clientFilterSpaces: Set<Ref<Space>> | undefined
|
let clientFilterSpaces: Set<Ref<Space>> | undefined
|
||||||
|
|
||||||
if (
|
if (!isSystem(account, ctx) && account.role !== AccountRole.DocGuest && domain !== DOMAIN_MODEL) {
|
||||||
!this.skipFindCheck &&
|
|
||||||
!isSystem(account, ctx) &&
|
|
||||||
account.role !== AccountRole.DocGuest &&
|
|
||||||
domain !== DOMAIN_MODEL
|
|
||||||
) {
|
|
||||||
if (!isOwner(account, ctx) || !isSpace) {
|
if (!isOwner(account, ctx) || !isSpace) {
|
||||||
if (query[field] !== undefined) {
|
if (query[field] !== undefined) {
|
||||||
const res = await this.mergeQuery(ctx, account, query[field], domain, isSpace)
|
const res = await this.mergeQuery(ctx, account, query[field], domain, isSpace)
|
||||||
@ -536,6 +544,11 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
delete (newQuery as any)[field]
|
delete (newQuery as any)[field]
|
||||||
} else if (spaces.result.length === 1) {
|
} else if (spaces.result.length === 1) {
|
||||||
;(newQuery as any)[field] = spaces.result[0]
|
;(newQuery as any)[field] = spaces.result[0]
|
||||||
|
if (options !== undefined) {
|
||||||
|
options.allowedSpaces = spaces.result
|
||||||
|
} else {
|
||||||
|
options = { allowedSpaces: spaces.result }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if spaces > 85% of all domain spaces, in this case return all and filter on client.
|
// Check if spaces > 85% of all domain spaces, in this case return all and filter on client.
|
||||||
if (spaces.result.length / spaces.domainSpaces.size > 0.85 && options?.limit === undefined) {
|
if (spaces.result.length / spaces.domainSpaces.size > 0.85 && options?.limit === undefined) {
|
||||||
@ -543,17 +556,22 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
delete newQuery.space
|
delete newQuery.space
|
||||||
} else {
|
} else {
|
||||||
;(newQuery as any)[field] = { $in: spaces.result }
|
;(newQuery as any)[field] = { $in: spaces.result }
|
||||||
|
if (options !== undefined) {
|
||||||
|
options.allowedSpaces = spaces.result
|
||||||
|
} else {
|
||||||
|
options = { allowedSpaces: spaces.result }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let findResult = await this.provideFindAll(ctx, _class, newQuery, options)
|
let findResult = await this.provideFindAll(ctx, _class, !this.skipFindCheck ? newQuery : query, options)
|
||||||
if (clientFilterSpaces !== undefined) {
|
if (clientFilterSpaces !== undefined) {
|
||||||
const cfs = clientFilterSpaces
|
const cfs = clientFilterSpaces
|
||||||
findResult = toFindResult(
|
findResult = toFindResult(
|
||||||
findResult.filter((it) => cfs.has(it.space)),
|
findResult.filter((it) => cfs.has((it as any)[field])),
|
||||||
findResult.total,
|
findResult.total,
|
||||||
findResult.lookupMap
|
findResult.lookupMap
|
||||||
)
|
)
|
||||||
|
@ -14,14 +14,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { error, json } from 'itty-router'
|
import { error, json } from 'itty-router'
|
||||||
import postgres from 'postgres'
|
import { type Sql } from 'postgres'
|
||||||
import * as db from './db'
|
import db, { withPostgres } from './db'
|
||||||
import { cacheControl, hashLimit } from './const'
|
import { cacheControl, hashLimit } from './const'
|
||||||
import { toUUID } from './encodings'
|
import { toUUID } from './encodings'
|
||||||
import { getSha256 } from './hash'
|
import { getSha256 } from './hash'
|
||||||
import { selectStorage } from './storage'
|
import { selectStorage } from './storage'
|
||||||
import { type BlobRequest, type WorkspaceRequest, type UUID } from './types'
|
import { type BlobRequest, type WorkspaceRequest, type UUID } from './types'
|
||||||
import { copyVideo, deleteVideo } from './video'
|
import { copyVideo, deleteVideo } from './video'
|
||||||
|
import { measure, LoggedCache } from './measure'
|
||||||
|
|
||||||
interface BlobMetadata {
|
interface BlobMetadata {
|
||||||
lastModified: number
|
lastModified: number
|
||||||
@ -38,20 +39,22 @@ export function getBlobURL (request: Request, workspace: string, name: string):
|
|||||||
export async function handleBlobGet (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
export async function handleBlobGet (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
const { workspace, name } = request
|
const { workspace, name } = request
|
||||||
|
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
const cache = new LoggedCache(caches.default)
|
||||||
const { bucket } = selectStorage(env, workspace)
|
|
||||||
|
|
||||||
const blob = await db.getBlob(sql, { workspace, name })
|
|
||||||
if (blob === null || blob.deleted) {
|
|
||||||
return error(404)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = caches.default
|
|
||||||
const cached = await cache.match(request)
|
const cached = await cache.match(request)
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
|
console.log({ message: 'cache hit' })
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { bucket } = selectStorage(env, workspace)
|
||||||
|
|
||||||
|
const blob = await withPostgres(env, ctx, (sql) => {
|
||||||
|
return db.getBlob(sql, { workspace, name })
|
||||||
|
})
|
||||||
|
if (blob === null || blob.deleted) {
|
||||||
|
return error(404)
|
||||||
|
}
|
||||||
|
|
||||||
const range = request.headers.has('Range') ? request.headers : undefined
|
const range = request.headers.has('Range') ? request.headers : undefined
|
||||||
const object = await bucket.get(blob.filename, { range })
|
const object = await bucket.get(blob.filename, { range })
|
||||||
if (object === null) {
|
if (object === null) {
|
||||||
@ -67,6 +70,7 @@ export async function handleBlobGet (request: BlobRequest, env: Env, ctx: Execut
|
|||||||
const status = length !== undefined && length < object.size ? 206 : 200
|
const status = length !== undefined && length < object.size ? 206 : 200
|
||||||
|
|
||||||
const response = new Response(object?.body, { headers, status })
|
const response = new Response(object?.body, { headers, status })
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
ctx.waitUntil(cache.put(request, response.clone()))
|
ctx.waitUntil(cache.put(request, response.clone()))
|
||||||
}
|
}
|
||||||
@ -77,10 +81,11 @@ export async function handleBlobGet (request: BlobRequest, env: Env, ctx: Execut
|
|||||||
export async function handleBlobHead (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
export async function handleBlobHead (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
const { workspace, name } = request
|
const { workspace, name } = request
|
||||||
|
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
|
||||||
const { bucket } = selectStorage(env, workspace)
|
const { bucket } = selectStorage(env, workspace)
|
||||||
|
|
||||||
const blob = await db.getBlob(sql, { workspace, name })
|
const blob = await withPostgres(env, ctx, (sql) => {
|
||||||
|
return db.getBlob(sql, { workspace, name })
|
||||||
|
})
|
||||||
if (blob === null || blob.deleted) {
|
if (blob === null || blob.deleted) {
|
||||||
return error(404)
|
return error(404)
|
||||||
}
|
}
|
||||||
@ -97,10 +102,10 @@ export async function handleBlobHead (request: BlobRequest, env: Env, ctx: Execu
|
|||||||
export async function handleBlobDelete (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
export async function handleBlobDelete (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
const { workspace, name } = request
|
const { workspace, name } = request
|
||||||
|
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([db.deleteBlob(sql, { workspace, name }), deleteVideo(env, workspace, name)])
|
await withPostgres(env, ctx, (sql) => {
|
||||||
|
return Promise.all([db.deleteBlob(sql, { workspace, name }), deleteVideo(env, workspace, name)])
|
||||||
|
})
|
||||||
|
|
||||||
return new Response(null, { status: 204 })
|
return new Response(null, { status: 204 })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -110,7 +115,11 @@ export async function handleBlobDelete (request: BlobRequest, env: Env, ctx: Exe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleUploadFormData (request: WorkspaceRequest, env: Env): Promise<Response> {
|
export async function handleUploadFormData (
|
||||||
|
request: WorkspaceRequest,
|
||||||
|
env: Env,
|
||||||
|
ctx: ExecutionContext
|
||||||
|
): Promise<Response> {
|
||||||
const contentType = request.headers.get('Content-Type')
|
const contentType = request.headers.get('Content-Type')
|
||||||
if (contentType === null || !contentType.includes('multipart/form-data')) {
|
if (contentType === null || !contentType.includes('multipart/form-data')) {
|
||||||
console.error({ error: 'expected multipart/form-data' })
|
console.error({ error: 'expected multipart/form-data' })
|
||||||
@ -119,11 +128,9 @@ export async function handleUploadFormData (request: WorkspaceRequest, env: Env)
|
|||||||
|
|
||||||
const { workspace } = request
|
const { workspace } = request
|
||||||
|
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
|
||||||
|
|
||||||
let formData: FormData
|
let formData: FormData
|
||||||
try {
|
try {
|
||||||
formData = await request.formData()
|
formData = await measure('fetch formdata', () => request.formData())
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
console.error({ error: 'failed to parse form data', message })
|
console.error({ error: 'failed to parse form data', message })
|
||||||
@ -139,7 +146,9 @@ export async function handleUploadFormData (request: WorkspaceRequest, env: Env)
|
|||||||
files.map(async ([file, key]) => {
|
files.map(async ([file, key]) => {
|
||||||
const { name, type, lastModified } = file
|
const { name, type, lastModified } = file
|
||||||
try {
|
try {
|
||||||
const metadata = await saveBlob(env, sql, file.stream(), file.size, type, workspace, name, lastModified)
|
const metadata = await withPostgres(env, ctx, (sql) => {
|
||||||
|
return saveBlob(env, sql, file.stream(), file.size, type, workspace, name, lastModified)
|
||||||
|
})
|
||||||
|
|
||||||
// TODO this probably should happen via queue, let it be here for now
|
// TODO this probably should happen via queue, let it be here for now
|
||||||
if (type.startsWith('video/')) {
|
if (type.startsWith('video/')) {
|
||||||
@ -161,7 +170,7 @@ export async function handleUploadFormData (request: WorkspaceRequest, env: Env)
|
|||||||
|
|
||||||
export async function saveBlob (
|
export async function saveBlob (
|
||||||
env: Env,
|
env: Env,
|
||||||
sql: postgres.Sql,
|
sql: Sql,
|
||||||
stream: ReadableStream,
|
stream: ReadableStream,
|
||||||
size: number,
|
size: number,
|
||||||
type: string,
|
type: string,
|
||||||
@ -179,6 +188,7 @@ export async function saveBlob (
|
|||||||
|
|
||||||
const hash = await getSha256(hashStream)
|
const hash = await getSha256(hashStream)
|
||||||
const data = await db.getData(sql, { hash, location })
|
const data = await db.getData(sql, { hash, location })
|
||||||
|
|
||||||
if (data !== null) {
|
if (data !== null) {
|
||||||
// Lucky boy, nothing to upload, use existing blob
|
// Lucky boy, nothing to upload, use existing blob
|
||||||
await db.createBlob(sql, { workspace, name, hash, location })
|
await db.createBlob(sql, { workspace, name, hash, location })
|
||||||
@ -212,8 +222,13 @@ export async function saveBlob (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleBlobUploaded (env: Env, workspace: string, name: string, filename: UUID): Promise<void> {
|
export async function handleBlobUploaded (
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
env: Env,
|
||||||
|
ctx: ExecutionContext,
|
||||||
|
workspace: string,
|
||||||
|
name: string,
|
||||||
|
filename: UUID
|
||||||
|
): Promise<void> {
|
||||||
const { location, bucket } = selectStorage(env, workspace)
|
const { location, bucket } = selectStorage(env, workspace)
|
||||||
|
|
||||||
const object = await bucket.head(filename)
|
const object = await bucket.head(filename)
|
||||||
@ -223,16 +238,20 @@ export async function handleBlobUploaded (env: Env, workspace: string, name: str
|
|||||||
|
|
||||||
const hash = object.checksums.md5 !== undefined ? digestToUUID(object.checksums.md5) : (crypto.randomUUID() as UUID)
|
const hash = object.checksums.md5 !== undefined ? digestToUUID(object.checksums.md5) : (crypto.randomUUID() as UUID)
|
||||||
|
|
||||||
const data = await db.getData(sql, { hash, location })
|
await withPostgres(env, ctx, async (sql) => {
|
||||||
if (data !== null) {
|
const data = await db.getData(sql, { hash, location })
|
||||||
await Promise.all([bucket.delete(filename), db.createBlob(sql, { workspace, name, hash, location })])
|
if (data !== null) {
|
||||||
} else {
|
await Promise.all([bucket.delete(filename), db.createBlob(sql, { workspace, name, hash, location })])
|
||||||
const size = object.size
|
} else {
|
||||||
const type = object.httpMetadata.contentType ?? 'application/octet-stream'
|
const size = object.size
|
||||||
|
const type = object.httpMetadata?.contentType ?? 'application/octet-stream'
|
||||||
|
|
||||||
await db.createData(sql, { hash, location, filename, type, size })
|
await sql.begin((sql) => [
|
||||||
await db.createBlob(sql, { workspace, name, hash, location })
|
db.createData(sql, { hash, location, filename, type, size }),
|
||||||
}
|
db.createBlob(sql, { workspace, name, hash, location })
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadLargeFile (
|
async function uploadLargeFile (
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import type postgres from 'postgres'
|
import postgres from 'postgres'
|
||||||
|
import { measure, measureSync } from './measure'
|
||||||
import { type Location, type UUID } from './types'
|
import { type Location, type UUID } from './types'
|
||||||
|
|
||||||
export interface BlobDataId {
|
export interface BlobDataId {
|
||||||
@ -42,63 +43,94 @@ export interface BlobRecordWithFilename extends BlobRecord {
|
|||||||
filename: string
|
filename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getData (sql: postgres.Sql, dataId: BlobDataId): Promise<BlobDataRecord | null> {
|
export async function withPostgres<T> (
|
||||||
const { hash, location } = dataId
|
env: Env,
|
||||||
|
ctx: ExecutionContext,
|
||||||
const rows = await sql<BlobDataRecord[]>`
|
fn: (sql: postgres.Sql) => Promise<T>
|
||||||
SELECT hash, location, filename, size, type
|
): Promise<T> {
|
||||||
FROM blob.data
|
const sql = measureSync('db.connect', () => {
|
||||||
WHERE hash = ${hash} AND location = ${location}
|
return postgres(env.HYPERDRIVE.connectionString)
|
||||||
`
|
})
|
||||||
|
try {
|
||||||
if (rows.length > 0) {
|
return await fn(sql)
|
||||||
return rows[0]
|
} finally {
|
||||||
|
measureSync('db.close', () => {
|
||||||
|
ctx.waitUntil(sql.end({ timeout: 0 }))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createData (sql: postgres.Sql, data: BlobDataRecord): Promise<void> {
|
export interface BlobDB {
|
||||||
const { hash, location, filename, size, type } = data
|
getData: (sql: postgres.Sql, dataId: BlobDataId) => Promise<BlobDataRecord | null>
|
||||||
|
createData: (sql: postgres.Sql, data: BlobDataRecord) => Promise<void>
|
||||||
await sql`
|
getBlob: (sql: postgres.Sql, blobId: BlobId) => Promise<BlobRecordWithFilename | null>
|
||||||
UPSERT INTO blob.data (hash, location, filename, size, type)
|
createBlob: (sql: postgres.Sql, blob: Omit<BlobRecord, 'filename' | 'deleted'>) => Promise<void>
|
||||||
VALUES (${hash}, ${location}, ${filename}, ${size}, ${type})
|
deleteBlob: (sql: postgres.Sql, blob: BlobId) => Promise<void>
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBlob (sql: postgres.Sql, blobId: BlobId): Promise<BlobRecordWithFilename | null> {
|
const db: BlobDB = {
|
||||||
const { workspace, name } = blobId
|
async getData (sql: postgres.Sql, dataId: BlobDataId): Promise<BlobDataRecord | null> {
|
||||||
|
const { hash, location } = dataId
|
||||||
|
const rows = await sql<BlobDataRecord[]>`
|
||||||
|
SELECT hash, location, filename, size, type
|
||||||
|
FROM blob.data
|
||||||
|
WHERE hash = ${hash} AND location = ${location}
|
||||||
|
`
|
||||||
|
return rows.length > 0 ? rows[0] : null
|
||||||
|
},
|
||||||
|
|
||||||
const rows = await sql<BlobRecordWithFilename[]>`
|
async createData (sql: postgres.Sql, data: BlobDataRecord): Promise<void> {
|
||||||
SELECT b.workspace, b.name, b.hash, b.location, b.deleted, d.filename
|
const { hash, location, filename, size, type } = data
|
||||||
FROM blob.blob AS b
|
|
||||||
JOIN blob.data AS d ON b.hash = d.hash AND b.location = d.location
|
|
||||||
WHERE b.workspace = ${workspace} AND b.name = ${name}
|
|
||||||
`
|
|
||||||
|
|
||||||
if (rows.length > 0) {
|
await sql`
|
||||||
return rows[0]
|
UPSERT INTO blob.data (hash, location, filename, size, type)
|
||||||
|
VALUES (${hash}, ${location}, ${filename}, ${size}, ${type})
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
async getBlob (sql: postgres.Sql, blobId: BlobId): Promise<BlobRecordWithFilename | null> {
|
||||||
|
const { workspace, name } = blobId
|
||||||
|
|
||||||
|
const rows = await sql<BlobRecordWithFilename[]>`
|
||||||
|
SELECT b.workspace, b.name, b.hash, b.location, b.deleted, d.filename
|
||||||
|
FROM blob.blob AS b
|
||||||
|
JOIN blob.data AS d ON b.hash = d.hash AND b.location = d.location
|
||||||
|
WHERE b.workspace = ${workspace} AND b.name = ${name}
|
||||||
|
`
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return rows[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
async createBlob (sql: postgres.Sql, blob: Omit<BlobRecord, 'filename' | 'deleted'>): Promise<void> {
|
||||||
|
const { workspace, name, hash, location } = blob
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
UPSERT INTO blob.blob (workspace, name, hash, location, deleted)
|
||||||
|
VALUES (${workspace}, ${name}, ${hash}, ${location}, false)
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteBlob (sql: postgres.Sql, blob: BlobId): Promise<void> {
|
||||||
|
const { workspace, name } = blob
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
UPDATE blob.blob
|
||||||
|
SET deleted = true
|
||||||
|
WHERE workspace = ${workspace} AND name = ${name}
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBlob (sql: postgres.Sql, blob: Omit<BlobRecord, 'filename' | 'deleted'>): Promise<void> {
|
export const measuredDb: BlobDB = {
|
||||||
const { workspace, name, hash, location } = blob
|
getData: (sql, dataId) => measure('db.getData', () => db.getData(sql, dataId)),
|
||||||
|
createData: (sql, data) => measure('db.createData', () => db.createData(sql, data)),
|
||||||
await sql`
|
getBlob: (sql, blobId) => measure('db.getBlob', () => db.getBlob(sql, blobId)),
|
||||||
UPSERT INTO blob.blob (workspace, name, hash, location, deleted)
|
createBlob: (sql, blob) => measure('db.createBlob', () => db.createBlob(sql, blob)),
|
||||||
VALUES (${workspace}, ${name}, ${hash}, ${location}, false)
|
deleteBlob: (sql, blob) => measure('db.deleteBlob', () => db.deleteBlob(sql, blob))
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBlob (sql: postgres.Sql, blob: BlobId): Promise<void> {
|
export default measuredDb
|
||||||
const { workspace, name } = blob
|
|
||||||
|
|
||||||
await sql`
|
|
||||||
UPDATE blob.blob
|
|
||||||
SET deleted = true
|
|
||||||
WHERE workspace = ${workspace} AND name = ${name}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
@ -18,6 +18,7 @@ import { type IRequestStrict, type RequestHandler, Router, error, html } from 'i
|
|||||||
|
|
||||||
import { handleBlobDelete, handleBlobGet, handleBlobHead, handleUploadFormData } from './blob'
|
import { handleBlobDelete, handleBlobGet, handleBlobHead, handleUploadFormData } from './blob'
|
||||||
import { cors } from './cors'
|
import { cors } from './cors'
|
||||||
|
import { LoggedKVNamespace, LoggedR2Bucket, requestTimeAfter, requestTimeBefore } from './measure'
|
||||||
import { handleImageGet } from './image'
|
import { handleImageGet } from './image'
|
||||||
import { handleS3Blob } from './s3'
|
import { handleS3Blob } from './s3'
|
||||||
import { handleVideoMetaGet } from './video'
|
import { handleVideoMetaGet } from './video'
|
||||||
@ -35,8 +36,8 @@ const { preflight, corsify } = cors({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const router = Router<IRequestStrict, [Env, ExecutionContext], Response>({
|
const router = Router<IRequestStrict, [Env, ExecutionContext], Response>({
|
||||||
before: [preflight],
|
before: [preflight, requestTimeBefore],
|
||||||
finally: [corsify]
|
finally: [corsify, requestTimeAfter]
|
||||||
})
|
})
|
||||||
|
|
||||||
const withWorkspace: RequestHandler<WorkspaceRequest> = (request: WorkspaceRequest) => {
|
const withWorkspace: RequestHandler<WorkspaceRequest> = (request: WorkspaceRequest) => {
|
||||||
@ -87,6 +88,19 @@ router
|
|||||||
.all('*', () => error(404))
|
.all('*', () => error(404))
|
||||||
|
|
||||||
export default class DatalakeWorker extends WorkerEntrypoint<Env> {
|
export default class DatalakeWorker extends WorkerEntrypoint<Env> {
|
||||||
|
constructor (ctx: ExecutionContext, env: Env) {
|
||||||
|
env = {
|
||||||
|
...env,
|
||||||
|
datalake_blobs: new LoggedKVNamespace(env.datalake_blobs),
|
||||||
|
DATALAKE_APAC: new LoggedR2Bucket(env.DATALAKE_APAC),
|
||||||
|
DATALAKE_EEUR: new LoggedR2Bucket(env.DATALAKE_EEUR),
|
||||||
|
DATALAKE_WEUR: new LoggedR2Bucket(env.DATALAKE_WEUR),
|
||||||
|
DATALAKE_ENAM: new LoggedR2Bucket(env.DATALAKE_ENAM),
|
||||||
|
DATALAKE_WNAM: new LoggedR2Bucket(env.DATALAKE_WNAM)
|
||||||
|
}
|
||||||
|
super(ctx, env)
|
||||||
|
}
|
||||||
|
|
||||||
async fetch (request: Request): Promise<Response> {
|
async fetch (request: Request): Promise<Response> {
|
||||||
return await router.fetch(request, this.env, this.ctx).catch(error)
|
return await router.fetch(request, this.env, this.ctx).catch(error)
|
||||||
}
|
}
|
||||||
|
177
workers/datalake/src/measure.ts
Normal file
177
workers/datalake/src/measure.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the 'License');
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an 'AS IS' BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { type IRequest, type ResponseHandler, type RequestHandler } from 'itty-router'
|
||||||
|
|
||||||
|
export async function measure<T> (label: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
const start = performance.now()
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
|
const duration = performance.now() - start
|
||||||
|
console.log({ stage: label, duration })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measureSync<T> (label: string, fn: () => T): T {
|
||||||
|
const start = performance.now()
|
||||||
|
try {
|
||||||
|
return fn()
|
||||||
|
} finally {
|
||||||
|
const duration = performance.now() - start
|
||||||
|
console.log({ stage: label, duration })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestTimeBefore: RequestHandler<IRequest> = async (request: IRequest) => {
|
||||||
|
request.startTime = performance.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestTimeAfter: ResponseHandler<Response> = async (response: Response, request: IRequest) => {
|
||||||
|
const duration = performance.now() - request.startTime
|
||||||
|
console.log({ stage: 'total', duration })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoggedR2Bucket implements R2Bucket {
|
||||||
|
constructor (private readonly bucket: R2Bucket) {}
|
||||||
|
|
||||||
|
async head (key: string): Promise<R2Object | null> {
|
||||||
|
return await measure('r2.head', () => this.bucket.head(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
async get (
|
||||||
|
key: string,
|
||||||
|
options?: R2GetOptions & {
|
||||||
|
onlyIf?: R2Conditional | Headers
|
||||||
|
}
|
||||||
|
): Promise<R2ObjectBody | null> {
|
||||||
|
return await measure('r2.get', () => this.bucket.get(key, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async put (
|
||||||
|
key: string,
|
||||||
|
value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob,
|
||||||
|
options?: R2PutOptions & {
|
||||||
|
onlyIf?: R2Conditional | Headers
|
||||||
|
}
|
||||||
|
): Promise<R2Object> {
|
||||||
|
return await measure('r2.put', () => this.bucket.put(key, value, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMultipartUpload (key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload> {
|
||||||
|
return await measure('r2.createMultipartUpload', () => this.bucket.createMultipartUpload(key, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeMultipartUpload (key: string, uploadId: string): R2MultipartUpload {
|
||||||
|
return measureSync('r2.resumeMultipartUpload', () => this.bucket.resumeMultipartUpload(key, uploadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete (keys: string | string[]): Promise<void> {
|
||||||
|
await measure('r2.delete', () => this.bucket.delete(keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
async list (options?: R2ListOptions): Promise<R2Objects> {
|
||||||
|
return await measure('r2.list', () => this.bucket.list(options))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoggedKVNamespace implements KVNamespace {
|
||||||
|
constructor (private readonly kv: KVNamespace) {}
|
||||||
|
|
||||||
|
get (key: string, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<string | null>
|
||||||
|
get (key: string, type: 'text'): Promise<string | null>
|
||||||
|
get<ExpectedValue = unknown>(key: string, type: 'json'): Promise<ExpectedValue | null>
|
||||||
|
get (key: string, type: 'arrayBuffer'): Promise<ArrayBuffer | null>
|
||||||
|
get (key: string, type: 'stream'): Promise<ReadableStream | null>
|
||||||
|
get (key: string, options?: KVNamespaceGetOptions<'text'>): Promise<string | null>
|
||||||
|
get<ExpectedValue = unknown>(key: string, options?: KVNamespaceGetOptions<'json'>): Promise<ExpectedValue | null>
|
||||||
|
get (key: string, options?: KVNamespaceGetOptions<'arrayBuffer'>): Promise<ArrayBuffer | null>
|
||||||
|
get (key: string, options?: KVNamespaceGetOptions<'stream'>): Promise<ReadableStream | null>
|
||||||
|
async get (key: string, options?: any): Promise<any> {
|
||||||
|
return await measure('kv.get', () => this.kv.get(key, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
options?: Partial<KVNamespaceGetOptions<undefined>>
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
type: 'text'
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>
|
||||||
|
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
type: 'json'
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
type: 'arrayBuffer'
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
type: 'stream'
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
options?: KVNamespaceGetOptions<'text'>
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>
|
||||||
|
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
options?: KVNamespaceGetOptions<'json'>
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
options?: KVNamespaceGetOptions<'arrayBuffer'>
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>
|
||||||
|
getWithMetadata<Metadata = unknown>(
|
||||||
|
key: string,
|
||||||
|
options?: KVNamespaceGetOptions<'stream'>
|
||||||
|
): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>
|
||||||
|
async getWithMetadata (key: string, options?: any): Promise<any> {
|
||||||
|
return await measure('kv.getWithMetadata', () => this.kv.getWithMetadata(key, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async list<Metadata = unknown>(options?: KVNamespaceListOptions): Promise<KVNamespaceListResult<Metadata, string>> {
|
||||||
|
return await measure('kv.list', () => this.kv.list(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async put (
|
||||||
|
key: string,
|
||||||
|
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
|
||||||
|
options?: KVNamespacePutOptions
|
||||||
|
): Promise<void> {
|
||||||
|
await measure('kv.put', () => this.kv.put(key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete (key: string): Promise<void> {
|
||||||
|
await measure('kv.delete', () => this.kv.delete(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoggedCache implements Cache {
|
||||||
|
constructor (private readonly cache: Cache) {}
|
||||||
|
|
||||||
|
async match (request: RequestInfo, options?: CacheQueryOptions): Promise<Response | undefined> {
|
||||||
|
return await measure('cache.match', () => this.cache.match(request, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete (request: RequestInfo, options?: CacheQueryOptions): Promise<boolean> {
|
||||||
|
return await measure('cache.delete', () => this.cache.delete(request, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
async put (request: RequestInfo, response: Response): Promise<void> {
|
||||||
|
await measure('cache.put', () => this.cache.put(request, response))
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { error, json } from 'itty-router'
|
import { error, json } from 'itty-router'
|
||||||
import postgres from 'postgres'
|
import db, { withPostgres } from './db'
|
||||||
import * as db from './db'
|
|
||||||
import { cacheControl } from './const'
|
import { cacheControl } from './const'
|
||||||
import { toUUID } from './encodings'
|
import { toUUID } from './encodings'
|
||||||
import { selectStorage } from './storage'
|
import { selectStorage } from './storage'
|
||||||
@ -85,8 +84,6 @@ export async function handleMultipartUploadComplete (
|
|||||||
env: Env,
|
env: Env,
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
|
||||||
|
|
||||||
const { workspace, name } = request
|
const { workspace, name } = request
|
||||||
|
|
||||||
const multipartKey = request.query?.key
|
const multipartKey = request.query?.key
|
||||||
@ -108,17 +105,19 @@ export async function handleMultipartUploadComplete (
|
|||||||
const size = object.size ?? 0
|
const size = object.size ?? 0
|
||||||
const filename = multipartKey as UUID
|
const filename = multipartKey as UUID
|
||||||
|
|
||||||
const data = await db.getData(sql, { hash, location })
|
await withPostgres(env, ctx, async (sql) => {
|
||||||
if (data !== null) {
|
const data = await db.getData(sql, { hash, location })
|
||||||
// blob already exists
|
if (data !== null) {
|
||||||
await Promise.all([bucket.delete(filename), db.createBlob(sql, { workspace, name, hash, location })])
|
// blob already exists
|
||||||
} else {
|
await Promise.all([bucket.delete(filename), db.createBlob(sql, { workspace, name, hash, location })])
|
||||||
// Otherwise register a new hash and blob
|
} else {
|
||||||
await sql.begin((sql) => [
|
// Otherwise register a new hash and blob
|
||||||
db.createData(sql, { hash, location, filename, type, size }),
|
await sql.begin((sql) => [
|
||||||
db.createBlob(sql, { workspace, name, hash, location })
|
db.createData(sql, { hash, location, filename, type, size }),
|
||||||
])
|
db.createBlob(sql, { workspace, name, hash, location })
|
||||||
}
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return new Response(null, { status: 204 })
|
return new Response(null, { status: 204 })
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,7 @@
|
|||||||
|
|
||||||
import { AwsClient } from 'aws4fetch'
|
import { AwsClient } from 'aws4fetch'
|
||||||
import { error, json } from 'itty-router'
|
import { error, json } from 'itty-router'
|
||||||
import postgres from 'postgres'
|
import db, { withPostgres } from './db'
|
||||||
import * as db from './db'
|
|
||||||
import { saveBlob } from './blob'
|
import { saveBlob } from './blob'
|
||||||
import { type BlobRequest } from './types'
|
import { type BlobRequest } from './types'
|
||||||
|
|
||||||
@ -38,34 +37,35 @@ function getS3Client (payload: S3UploadPayload): AwsClient {
|
|||||||
|
|
||||||
export async function handleS3Blob (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
export async function handleS3Blob (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
const { workspace, name } = request
|
const { workspace, name } = request
|
||||||
const sql = postgres(env.HYPERDRIVE.connectionString)
|
|
||||||
|
|
||||||
const payload = await request.json<S3UploadPayload>()
|
const payload = await request.json<S3UploadPayload>()
|
||||||
|
|
||||||
const client = getS3Client(payload)
|
const client = getS3Client(payload)
|
||||||
|
|
||||||
// Ensure the blob does not exist
|
return await withPostgres(env, ctx, async (sql) => {
|
||||||
const blob = await db.getBlob(sql, { workspace, name })
|
// Ensure the blob does not exist
|
||||||
if (blob !== null) {
|
const blob = await db.getBlob(sql, { workspace, name })
|
||||||
return new Response(null, { status: 200 })
|
if (blob !== null) {
|
||||||
}
|
return new Response(null, { status: 200 })
|
||||||
|
}
|
||||||
|
|
||||||
const object = await client.fetch(payload.url)
|
const object = await client.fetch(payload.url)
|
||||||
if (!object.ok || object.status !== 200) {
|
if (!object.ok || object.status !== 200) {
|
||||||
return error(object.status)
|
return error(object.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.body === null) {
|
if (object.body === null) {
|
||||||
return error(400)
|
return error(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = object.headers.get('content-type') ?? 'application/octet-stream'
|
const contentType = object.headers.get('content-type') ?? 'application/octet-stream'
|
||||||
const contentLengthHeader = object.headers.get('content-length') ?? '0'
|
const contentLengthHeader = object.headers.get('content-length') ?? '0'
|
||||||
const lastModifiedHeader = object.headers.get('last-modified')
|
const lastModifiedHeader = object.headers.get('last-modified')
|
||||||
|
|
||||||
const contentLength = Number.parseInt(contentLengthHeader)
|
const contentLength = Number.parseInt(contentLengthHeader)
|
||||||
const lastModified = lastModifiedHeader !== null ? new Date(lastModifiedHeader).getTime() : Date.now()
|
const lastModified = lastModifiedHeader !== null ? new Date(lastModifiedHeader).getTime() : Date.now()
|
||||||
|
|
||||||
const result = await saveBlob(env, sql, object.body, contentLength, contentType, workspace, name, lastModified)
|
const result = await saveBlob(env, sql, object.body, contentLength, contentType, workspace, name, lastModified)
|
||||||
return json(result)
|
return json(result)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ export async function handleSignComplete (request: BlobRequest, env: Env, ctx: E
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleBlobUploaded(env, workspace, name, uuid)
|
await handleBlobUploaded(env, ctx, workspace, name, uuid)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
console.error({ error: message, workspace, name, uuid })
|
console.error({ error: message, workspace, name, uuid })
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#:schema node_modules/wrangler/config-schema.json
|
#:schema node_modules/wrangler/config-schema.json
|
||||||
name = "datalake-worker"
|
name = "datalake-worker"
|
||||||
main = "src/index.ts"
|
main = "src/index.ts"
|
||||||
compatibility_date = "2024-07-01"
|
compatibility_date = "2024-09-23"
|
||||||
compatibility_flags = ["nodejs_compat"]
|
compatibility_flags = ["nodejs_compat"]
|
||||||
keep_vars = true
|
keep_vars = true
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user