Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-02-09 12:02:35 +07:00
12 changed files with 413 additions and 69 deletions
+1
View File
@@ -519,6 +519,7 @@ services:
- REGION=cockroach
- QUEUE_CONFIG=${QUEUE_CONFIG}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- SECURE=true
restart: unless-stopped
hulylake:
image: hardcoreeng/hulylake
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadMultipart, uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class DatalakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class DatalakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/blob/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`)
try {
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class FrontStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class FrontStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class HulylakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class HulylakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/api/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -29,6 +29,7 @@ export interface FileStorageUploadOptions {
/** @public */
export interface FileStorage {
getFileUrl: (workspace: string, file: string, filename?: string) => string
getCookiePath: (workspace: string) => string
getFileMeta: (token: string, workspace: string, file: string) => Promise<Record<string, any>>
uploadFile: (
token: string,
+12 -8
View File
@@ -891,15 +891,19 @@ export function isSpaceClass (_class: Ref<Class<Doc>>): boolean {
}
export function setPresentationCookie (token: string, workspaceUuid: WorkspaceUuid): void {
function setToken (path: string): void {
const res =
encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-')) +
'=' +
encodeURIComponent(token) +
`; path=${path}`
document.cookie = res
const cookieName = encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-'))
const cookieValue = encodeURIComponent(token)
const storage = getMetadata(plugin.metadata.FileStorage)
if (storage !== undefined) {
let path = `/files/${workspaceUuid}`
try {
path = storage.getCookiePath(workspaceUuid)
} catch {}
const normalized = path.startsWith('/') ? path : `/${path}`
document.cookie = `${cookieName}=${cookieValue}; path=${normalized}`
}
setToken('/files/' + workspaceUuid)
}
export const upgradeDownloadProgress = writable(-1)
@@ -0,0 +1,180 @@
//
// Copyright © 2026 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 { AttributeModel } from '@hcengineering/view'
import type { IntlString } from '@hcengineering/platform'
import type { Class, Client, Doc, Hierarchy, Ref } from '@hcengineering/core'
import { rebuildRelationshipTableViewModel } from '../data/relationshipBuilder'
jest.mock('@hcengineering/view-resources', () => ({
buildConfigAssociation: jest.fn(() => []),
buildConfigLookup: jest.fn(() => ({}))
}))
type DocOverrides = Partial<Doc> & {
$associations?: Record<string, Doc[] | Doc>
title?: string
}
function doc (id: string, overrides: DocOverrides = {}): Doc {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- test helper with spread overrides
const result = {
_id: id as Ref<Doc>,
_class: 'test:class:Doc' as Ref<Class<Doc>>,
space: 'test:space' as any,
modifiedOn: 0,
modifiedBy: '' as any,
createdOn: 0,
createdBy: '' as any,
...overrides
} as Doc
return result
}
function attr (key: string, label: string = key): AttributeModel {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- minimal AttributeModel for tests
const result = {
key,
label: label as IntlString,
_class: '' as Ref<Class<Doc>>,
sortingKey: '',
collectionAttr: false,
isLookup: false
} as AttributeModel
return result
}
describe('relationshipBuilder', () => {
const hierarchy: Hierarchy = {} as any
const client: Client = {} as any
const cardClass = 'test:class:Card' as Ref<Class<Doc>>
describe('rebuildRelationshipTableViewModel', () => {
it('returns one row when doc has no associations (single expanded row with undefined children)', async () => {
const root = doc('root-1')
const model: AttributeModel[] = [
attr(''),
attr('$associations.assoc1_b', 'Level1'),
attr('$associations.assoc1_b.$associations.assoc2_b', 'Level2')
]
const viewModel = await rebuildRelationshipTableViewModel([root], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(1)
expect(viewModel[0].cells).toHaveLength(3)
expect(viewModel[0].cells[0].object).toBe(root)
expect(viewModel[0].cells[0].rowSpan).toBe(1)
expect(viewModel[0].cells[1].object).toBeUndefined()
expect(viewModel[0].cells[1].parentObject).toBe(root)
expect(viewModel[0].cells[2].object).toBeUndefined()
expect(viewModel[0].cells[2].parentObject).toBeUndefined()
})
it('builds one row per first-level child for single-level association', async () => {
const b1 = doc('b1', { title: 'B1' })
const b2 = doc('b2', { title: 'B2' })
const root = doc('root-1', {
$associations: { assoc1_b: [b1, b2] }
})
const model: AttributeModel[] = [attr(''), attr('$associations.assoc1_b', 'Level1')]
const viewModel = await rebuildRelationshipTableViewModel([root], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(2)
expect(viewModel[0].cells[0].object).toBe(root)
expect(viewModel[0].cells[0].rowSpan).toBe(2)
expect(viewModel[0].cells[1].object).toBe(b1)
expect(viewModel[0].cells[1].parentObject).toBe(root)
expect(viewModel[0].cells[1].rowSpan).toBe(1)
expect(viewModel[1].cells[0].rowSpan).toBe(0)
expect(viewModel[1].cells[0].object).toBeUndefined()
expect(viewModel[1].cells[1].object).toBe(b2)
expect(viewModel[1].cells[1].parentObject).toBe(root)
})
it('builds rows with correct nested objects for two-level association (A -> B -> C)', async () => {
const c1 = doc('c1', { title: 'C1' })
const c2 = doc('c2', { title: 'C2' })
const c3 = doc('c3', { title: 'C3' })
const b1 = doc('b1', { title: 'B1', $associations: { assoc2_b: [c1, c2] } })
const b2 = doc('b2', { title: 'B2', $associations: { assoc2_b: [c3] } })
const root = doc('root-1', {
$associations: { assoc1_b: [b1, b2] }
})
const model: AttributeModel[] = [
attr(''),
attr('$associations.assoc1_b', 'Level1'),
attr('$associations.assoc1_b.$associations.assoc2_b', 'Level2')
]
const viewModel = await rebuildRelationshipTableViewModel([root], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(3)
expect(viewModel[0].cells[0].object).toBe(root)
expect(viewModel[0].cells[0].rowSpan).toBe(3)
expect(viewModel[0].cells[1].object).toBe(b1)
expect(viewModel[0].cells[1].parentObject).toBe(root)
expect(viewModel[0].cells[1].rowSpan).toBe(2)
expect(viewModel[0].cells[2].object).toBe(c1)
expect(viewModel[0].cells[2].parentObject).toBe(b1)
expect(viewModel[0].cells[2].rowSpan).toBe(1)
expect(viewModel[1].cells[2].object).toBe(c2)
expect(viewModel[1].cells[2].parentObject).toBe(b1)
expect(viewModel[2].cells[1].object).toBe(b2)
expect(viewModel[2].cells[1].parentObject).toBe(root)
expect(viewModel[2].cells[1].rowSpan).toBe(1)
expect(viewModel[2].cells[2].object).toBe(c3)
expect(viewModel[2].cells[2].parentObject).toBe(b2)
})
it('emits one row when first-level child has no nested children', async () => {
const b1 = doc('b1', { title: 'B1' })
const root = doc('root-1', { $associations: { assoc1_b: [b1] } })
const model: AttributeModel[] = [
attr(''),
attr('$associations.assoc1_b', 'Level1'),
attr('$associations.assoc1_b.$associations.assoc2_b', 'Level2')
]
const viewModel = await rebuildRelationshipTableViewModel([root], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(1)
expect(viewModel[0].cells[0].object).toBe(root)
expect(viewModel[0].cells[1].object).toBe(b1)
expect(viewModel[0].cells[1].parentObject).toBe(root)
expect(viewModel[0].cells[2].object).toBeUndefined()
expect(viewModel[0].cells[2].parentObject).toBe(b1)
})
it('handles multiple root docs each with their own expanded rows', async () => {
const b1 = doc('b1')
const b2 = doc('b2')
const root1 = doc('root-1', { $associations: { assoc1_b: [b1] } })
const root2 = doc('root-2', { $associations: { assoc1_b: [b2] } })
const model: AttributeModel[] = [attr(''), attr('$associations.assoc1_b', 'Level1')]
const viewModel = await rebuildRelationshipTableViewModel([root1, root2], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(2)
expect(viewModel[0].cells[0].object).toBe(root1)
expect(viewModel[0].cells[0].rowSpan).toBe(1)
expect(viewModel[0].cells[1].object).toBe(b1)
expect(viewModel[1].cells[0].object).toBe(root2)
expect(viewModel[1].cells[1].object).toBe(b2)
})
it('preserves model order for non-association attributes', async () => {
const b1 = doc('b1')
const root = doc('root-1', { $associations: { assoc1_b: [b1] } })
const model: AttributeModel[] = [attr(''), attr('$associations.assoc1_b', 'Assoc'), attr('title', 'Title')]
const viewModel = await rebuildRelationshipTableViewModel([root], model, cardClass, hierarchy, client)
expect(viewModel).toHaveLength(1)
expect(viewModel[0].cells).toHaveLength(3)
expect(viewModel[0].cells[2].attribute.key).toBe('title')
expect(viewModel[0].cells[2].object).toBe(root)
expect(viewModel[0].cells[2].rowSpan).toBe(1)
})
})
})
@@ -18,9 +18,86 @@ import type { AttributeModel } from '@hcengineering/view'
import { buildConfigAssociation, buildConfigLookup } from '@hcengineering/view-resources'
import type { RelationshipCellModel, RelationshipRowModel } from '../types'
/**
* Parse association attribute key into path of association keys.
* e.g. "$associations.X_b" -> ["X_b"], "$associations.X_b.$associations.Y_b" -> ["X_b", "Y_b"]
*/
function parseAssociationKeyPath (key: string): string[] {
if (!key.startsWith('$associations')) return []
const parts = key.split('$associations.').filter((p) => p.length > 0)
return parts.map((p) => (p.endsWith('.') ? p.slice(0, -1) : p).trim()).filter(Boolean)
}
/**
* Get doc at given depth from parent's $associations using path[depth].
* depth 0 = parent itself, depth 1 = parent.$associations[path[0]][index], etc.
*/
function getAssocChildren (parent: any, pathKey: string): Doc[] | undefined {
const arr = parent?.$associations?.[pathKey]
if (Array.isArray(arr)) return arr
if (arr !== undefined && arr !== null) return [arr as Doc]
return undefined
}
/**
* Expand rows for one root: one row per (level0, level1, ..., levelN) with correct nesting.
* Each row is [root, child1, child2, ...] where child1 from root.$associations[path[0]], child2 from child1.$associations[path[1]], etc.
*/
function expandRowsForRoot (
root: Doc,
associationAttrs: AttributeModel[],
pathPerAttr: string[][]
): Array<{ docsByLevel: (Doc | undefined)[] }> {
const rows: Array<{ docsByLevel: (Doc | undefined)[] }> = []
function expand (parent: Doc | undefined, level: number, docsSoFar: (Doc | undefined)[]): void {
if (level >= associationAttrs.length) {
rows.push({ docsByLevel: docsSoFar })
return
}
const path = pathPerAttr[level]
const key = path[level]
const children = parent !== undefined ? getAssocChildren(parent as any, key) : undefined
const list = children !== undefined && children !== null && children.length > 0 ? children : [undefined]
for (const doc of list) {
expand(doc ?? undefined, level + 1, [...docsSoFar, doc])
}
}
const firstPath = pathPerAttr[0]
const firstKey = firstPath[0]
const firstChildren = getAssocChildren(root as any, firstKey)
const firstList =
firstChildren !== undefined && firstChildren !== null && firstChildren.length > 0 ? firstChildren : [undefined]
for (const doc of firstList) {
expand(doc ?? undefined, 1, [root, doc])
}
return rows
}
/**
* Compute rowSpan for a given level at each row index: how many consecutive rows share the same doc at this level.
*/
function computeRowSpans (rows: Array<{ docsByLevel: (Doc | undefined)[] }>, level: number): number[] {
const spans: number[] = []
for (let i = 0; i < rows.length; i++) {
const doc = rows[i].docsByLevel[level]
const docId = doc?._id
let span = 1
for (let j = i + 1; j < rows.length; j++) {
if (rows[j].docsByLevel[level]?._id === docId) span++
else break
}
spans.push(span)
}
return spans
}
/**
* Rebuild relationship table viewModel from documents and metadata
* Recreates the hierarchical structure with row spans and separate rows for each associated child
* Recreates the hierarchical structure with row spans and separate rows for each associated child.
* Supports multi-level associations (A -> B -> C): nested keys like $associations.X_b.$associations.Y_b
* are resolved from the correct parent doc per level.
*/
export async function rebuildRelationshipTableViewModel (
docs: Doc[],
@@ -36,33 +113,21 @@ export async function rebuildRelationshipTableViewModel (
const lookup = buildConfigLookup(hierarchy, cardClass, config)
const associationAttrs = model.filter((attr) => attr.key.startsWith('$associations'))
const pathPerAttr = associationAttrs.map((attr) => parseAssociationKeyPath(attr.key))
let docsWithAssociations: Doc[] = docs
if (associations !== undefined && associations.length > 0) {
const docIds = docs.map((d) => d._id)
const query = { _id: { $in: docIds } }
docsWithAssociations = await client.findAll(cardClass, query, { lookup, associations })
// Preserve order of input docs (findAll does not guarantee order)
const idToIndex = new Map(docs.map((d, i) => [d._id, i]))
docsWithAssociations.sort((a, b) => (idToIndex.get(a._id) ?? Infinity) - (idToIndex.get(b._id) ?? Infinity))
}
for (const parentDoc of docsWithAssociations) {
const docWithAssoc = parentDoc as any
const parentAssociations = docWithAssoc.$associations ?? {}
const expandedRows = expandRowsForRoot(parentDoc, associationAttrs, pathPerAttr)
let maxChildren = 0
for (const assocAttr of associationAttrs) {
const assocKey = assocAttr.key.replace('$associations.', '')
const children = parentAssociations[assocKey]
if (Array.isArray(children)) {
maxChildren = Math.max(maxChildren, children.length)
} else if (children !== undefined && children !== null) {
maxChildren = Math.max(maxChildren, 1)
}
}
if (maxChildren === 0) {
if (expandedRows.length === 0) {
const cells: RelationshipCellModel[] = []
for (const attr of model) {
const isAssociationKey = attr.key.startsWith('$associations')
@@ -77,45 +142,59 @@ export async function rebuildRelationshipTableViewModel (
continue
}
for (let childIndex = 0; childIndex < maxChildren; childIndex++) {
const rowSpanByLevel: number[][] = []
for (let level = 0; level <= associationAttrs.length; level++) {
rowSpanByLevel.push(computeRowSpans(expandedRows, level))
}
for (let rowIdx = 0; rowIdx < expandedRows.length; rowIdx++) {
const rowData = expandedRows[rowIdx]
const cells: RelationshipCellModel[] = []
for (const attr of model) {
const isAssociationKey = attr.key.startsWith('$associations')
if (attr.key === '') {
const span = rowSpanByLevel[0][rowIdx]
const isFirstInSpan =
rowIdx === 0 || expandedRows[rowIdx - 1].docsByLevel[0]?._id !== rowData.docsByLevel[0]?._id
cells.push({
attribute: attr,
rowSpan: maxChildren,
object: parentDoc,
parentObject: undefined
})
} else if (isAssociationKey) {
const assocKey = attr.key.replace('$associations.', '')
const children = parentAssociations[assocKey]
let childDoc: Doc | undefined
if (Array.isArray(children) && children.length > childIndex) {
childDoc = children[childIndex] as Doc
} else if (!Array.isArray(children) && children !== undefined && children !== null && childIndex === 0) {
childDoc = children as Doc
}
cells.push({
attribute: attr,
rowSpan: 1,
object: childDoc,
parentObject: parentDoc
})
} else {
cells.push({
attribute: attr,
rowSpan: 1,
object: childIndex === 0 ? parentDoc : undefined,
rowSpan: isFirstInSpan ? span : 0,
object: isFirstInSpan ? parentDoc : undefined,
parentObject: undefined
})
continue
}
}
if (isAssociationKey) {
const assocIdx = associationAttrs.indexOf(attr)
if (assocIdx < 0) {
cells.push({ attribute: attr, rowSpan: 1, object: undefined, parentObject: undefined })
continue
}
const level = assocIdx + 1
const docAtLevel = rowData.docsByLevel[level]
const parentAtLevel = rowData.docsByLevel[level - 1]
const span = rowSpanByLevel[level][rowIdx]
const isFirstInSpan =
rowIdx === 0 || expandedRows[rowIdx - 1].docsByLevel[level]?._id !== rowData.docsByLevel[level]?._id
cells.push({
attribute: attr,
rowSpan: isFirstInSpan ? span : 0,
object: isFirstInSpan ? docAtLevel : undefined,
parentObject: isFirstInSpan ? parentAtLevel : undefined
})
continue
}
cells.push({
attribute: attr,
rowSpan: 1,
object: rowIdx === 0 ? parentDoc : undefined,
parentObject: undefined
})
}
viewModel.push({ cells })
}
}
@@ -25,33 +25,45 @@ import { rebuildRelationshipTableViewModel, isRelationshipTable } from '../data'
import { escapeMarkdownLinkText } from './escape'
import { createMarkdownLink } from './link'
async function buildRelationshipTableFromMetadata (
async function buildRelationshipTablePropsFromMetadata (
docs: Doc[],
metadata: BuildMarkdownTableMetadata,
client: Client
): Promise<string> {
): Promise<CopyRelationshipTableAsMarkdownProps> {
const hierarchy = client.getHierarchy()
const cardClass = metadata.cardClass as Ref<Class<Doc>>
const config = metadata.config ?? []
const lookup = buildConfigLookup(hierarchy, cardClass, config)
const model = await buildModel({
client,
_class: cardClass,
keys: config,
lookup
})
let model: AttributeModel[]
if (metadata.config !== undefined && metadata.config.length > 0) {
const config = metadata.config
const lookup = buildConfigLookup(hierarchy, cardClass, config)
model = await buildModel({
client,
_class: cardClass,
keys: config,
lookup
})
} else {
model = await buildTableModel(client, hierarchy, cardClass, undefined)
}
const viewModel = await rebuildRelationshipTableViewModel(docs, model, cardClass, hierarchy, client)
const props: CopyRelationshipTableAsMarkdownProps = {
return {
viewModel,
model,
objects: docs,
cardClass,
query: metadata.query
}
}
async function buildRelationshipTableFromMetadata (
docs: Doc[],
metadata: BuildMarkdownTableMetadata,
client: Client
): Promise<string> {
const hierarchy = client.getHierarchy()
const props = await buildRelationshipTablePropsFromMetadata(docs, metadata, client)
const language = getCurrentLanguage()
return await buildRelationshipTableMarkdown(props, hierarchy, language)
}
@@ -234,6 +246,7 @@ export async function buildRelationshipTableMarkdown (
}
if (doc === undefined) {
if (cell.rowSpan === 0) continue
row[attrIndex] = ''
continue
}
@@ -35,6 +35,7 @@ export interface Config {
DbUrl: string
Buckets: BucketConfig[]
CleanupInterval: number
Secure: boolean
Readonly: boolean
Cache: CacheConfig
}
@@ -86,6 +87,7 @@ const config: Config = (() => {
AccountsUrl: process.env.ACCOUNTS_URL,
DbUrl: process.env.DB_URL,
Buckets: parseBucketsConfig(process.env.BUCKETS),
Secure: process.env.SECURE === 'true',
Readonly: process.env.READONLY === 'true',
Cache: {
enabled: process.env.CACHE_ENABLED !== 'false',
@@ -38,6 +38,14 @@ export const keepAlive = (options: KeepAliveOptions): RequestHandler => {
}
}
export const withOptionalAuth = (secure: boolean): RequestHandler => {
return secure
? withAuthorization
: (req: Request, res: Response, next: NextFunction) => {
next()
}
}
export const withAdminAuthorization = (req: RequestWithAuth, res: Response, next: NextFunction): void => {
try {
const token = extractToken(req.headers)
+32 -6
View File
@@ -36,7 +36,8 @@ import {
withAuthorization,
withBlob,
withWorkspace,
withReadonly
withReadonly,
withOptionalAuth
} from './middleware'
import {
handleBlobDelete,
@@ -176,13 +177,33 @@ export async function createServer (
app.get('/blob/:workspace', withAdminAuthorization, withWorkspace, wrapRequest(ctx, 'listBlobs', handleBlobList))
app.head('/blob/:workspace/:name', withBlob, wrapRequest(ctx, 'headBlob', handleBlobHead))
app.head(
'/blob/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'headBlob', handleBlobHead)
)
app.head('/blob/:workspace/:name/:filename', withBlob, wrapRequest(ctx, 'headBlob', handleBlobHead))
app.head(
'/blob/:workspace/:name/:filename',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'headBlob', handleBlobHead)
)
app.get('/blob/:workspace/:name', withBlob, wrapRequest(ctx, 'getBlob', handleBlobGet))
app.get(
'/blob/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getBlob', handleBlobGet)
)
app.get('/blob/:workspace/:name/:filename', withBlob, wrapRequest(ctx, 'getBlob', handleBlobGet))
app.get(
'/blob/:workspace/:name/:filename',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getBlob', handleBlobGet)
)
app.delete('/blob/:workspace/:name', withAuthorization, withBlob, wrapRequest(ctx, 'deleteBlob', handleBlobDelete))
@@ -206,7 +227,12 @@ export async function createServer (
// Blob meta
app.get('/meta/:workspace/:name', withBlob, wrapRequest(ctx, 'getMeta', handleMetaGet))
app.get(
'/meta/:workspace/:name',
withOptionalAuth(config.Secure),
withBlob,
wrapRequest(ctx, 'getMeta', handleMetaGet)
)
app.put('/meta/:workspace/:name', withAuthorization, withBlob, wrapRequest(ctx, 'putMeta', handleMetaPut))