@crombolo/api-management 0.0.0-stage → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +32 -4
- package/src/authorization.ts +453 -0
- package/src/handlers/content-type.ts +784 -0
- package/src/handlers/data-type.ts +267 -0
- package/src/handlers/dictionary.ts +245 -0
- package/src/handlers/document-blueprint.ts +193 -0
- package/src/handlers/document.ts +961 -0
- package/src/handlers/editor-services.ts +59 -0
- package/src/handlers/element.ts +492 -0
- package/src/handlers/file-systems.ts +207 -0
- package/src/handlers/import.ts +46 -0
- package/src/handlers/localization.ts +116 -0
- package/src/handlers/log-viewer.ts +125 -0
- package/src/handlers/manifest.ts +15 -0
- package/src/handlers/media.ts +453 -0
- package/src/handlers/member-group.ts +87 -0
- package/src/handlers/member.ts +189 -0
- package/src/handlers/models-builder.ts +29 -0
- package/src/handlers/package.ts +193 -0
- package/src/handlers/public-access.ts +107 -0
- package/src/handlers/published-cache.ts +27 -0
- package/src/handlers/redirects.ts +78 -0
- package/src/handlers/references.ts +44 -0
- package/src/handlers/server.ts +25 -0
- package/src/handlers/settings-stubs.ts +76 -0
- package/src/handlers/start-node-trees.ts +84 -0
- package/src/handlers/template-query.ts +191 -0
- package/src/handlers/template.ts +150 -0
- package/src/handlers/temporary-file.ts +62 -0
- package/src/handlers/user.ts +604 -0
- package/src/index.ts +17 -0
- package/src/ports-content.ts +564 -0
- package/src/ports-files.ts +51 -0
- package/src/ports-logs.ts +38 -0
- package/src/ports-members.ts +111 -0
- package/src/ports-models-builder.ts +26 -0
- package/src/ports-packages.ts +59 -0
- package/src/ports-users.ts +230 -0
- package/src/ports.ts +225 -0
- package/src/router.ts +216 -0
- package/src/routes.ts +99 -0
- package/src/server-events.ts +71 -0
- package/README.md +0 -3
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/** Data types: the property editor behind each property, plus its configuration. */
|
|
2
|
+
import type { ResponseOf } from '@crombolo/contracts'
|
|
3
|
+
import type { DataTypeModel, TreeItem } from '@crombolo/core'
|
|
4
|
+
import { invalidSkipTake, notFound, problemDetails, problemResponse } from '@crombolo/core'
|
|
5
|
+
import type { DataTypePort } from '../ports-content.ts'
|
|
6
|
+
import type { ManagementApiRouter, RequestContext } from '../router.ts'
|
|
7
|
+
import {
|
|
8
|
+
created,
|
|
9
|
+
paging,
|
|
10
|
+
refOf,
|
|
11
|
+
registerFolderHandlers,
|
|
12
|
+
toTreeItemResponse,
|
|
13
|
+
} from './content-type.ts'
|
|
14
|
+
|
|
15
|
+
function toResponse(model: DataTypeModel): ResponseOf<'GetDataTypeById'> {
|
|
16
|
+
return {
|
|
17
|
+
id: model.key,
|
|
18
|
+
name: model.name,
|
|
19
|
+
editorAlias: model.editorAlias,
|
|
20
|
+
editorUiAlias: model.editorUiAlias,
|
|
21
|
+
values: model.values.map((value) => ({ alias: value.alias, value: value.value })),
|
|
22
|
+
isDeletable: true,
|
|
23
|
+
canIgnoreStartNodes: false,
|
|
24
|
+
} as ResponseOf<'GetDataTypeById'>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fromRequest(key: string, body: Record<string, unknown>): DataTypeModel {
|
|
28
|
+
const values = Array.isArray(body.values)
|
|
29
|
+
? (body.values as Array<{ alias?: unknown; value?: unknown }>).map((value) => ({
|
|
30
|
+
alias: String(value.alias ?? ''),
|
|
31
|
+
value: value.value,
|
|
32
|
+
}))
|
|
33
|
+
: []
|
|
34
|
+
return {
|
|
35
|
+
key,
|
|
36
|
+
alias: null,
|
|
37
|
+
name: String(body.name ?? ''),
|
|
38
|
+
editorAlias: String(body.editorAlias ?? ''),
|
|
39
|
+
editorUiAlias: (body.editorUiAlias as string | null) ?? null,
|
|
40
|
+
// The storage type is a property of the editor, not of the request.
|
|
41
|
+
dbType: 'Ntext',
|
|
42
|
+
values,
|
|
43
|
+
parentKey:
|
|
44
|
+
body.parent && typeof body.parent === 'object' && 'id' in body.parent
|
|
45
|
+
? String((body.parent as { id: unknown }).id)
|
|
46
|
+
: null,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerDataTypeHandlers(router: ManagementApiRouter, port: DataTypePort): void {
|
|
51
|
+
router.handle('GetDataTypeByIdSchema', async (ctx) => {
|
|
52
|
+
const schema = await port.valueSchema(ctx.params.id as string)
|
|
53
|
+
if (!schema) return problemResponse(notFound('The data type could not be found'))
|
|
54
|
+
return Response.json(schema)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
router.handle('GetDataTypeSchemasBatch', async (ctx) => {
|
|
58
|
+
const ids = ctx.url.searchParams.getAll('id')
|
|
59
|
+
const items = []
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
const schema = await port.valueSchema(id)
|
|
62
|
+
// Umbraco reports a missing one in the row rather than failing the batch.
|
|
63
|
+
items.push(
|
|
64
|
+
schema
|
|
65
|
+
? { id, ...schema, error: null }
|
|
66
|
+
: { id, valueTypeName: null, jsonSchema: null, error: 'Not found' },
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
return Response.json({ total: items.length, items })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
router.handle('GetDataTypeById', async ({ params }) => {
|
|
73
|
+
const model = await port.byKey(params.id as string)
|
|
74
|
+
if (!model) return problemResponse(notFound('The data type could not be found'))
|
|
75
|
+
return Response.json(toResponse(model))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
router.handle('GetDataTypeBatch', async (ctx) => {
|
|
79
|
+
const keys = ctx.url.searchParams.getAll('id')
|
|
80
|
+
const models = await port.byKeys(keys)
|
|
81
|
+
return Response.json({ total: models.length, items: models.map(toResponse) })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
router.handle('PostDataType', async (ctx) => {
|
|
85
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
86
|
+
const key = typeof body.id === 'string' && body.id ? body.id : crypto.randomUUID()
|
|
87
|
+
await port.save(fromRequest(key, body), ctx.principal as NonNullable<typeof ctx.principal>)
|
|
88
|
+
ctx.notifications.push({ message: 'Data type created', category: 'Data Type', type: 'Success' })
|
|
89
|
+
return created(`${ctx.url.origin}/umbraco/management/api/v1/data-type/${key}`, key)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
router.handle('PutDataTypeById', async (ctx) => {
|
|
93
|
+
const key = ctx.params.id as string
|
|
94
|
+
const existing = await port.byKey(key)
|
|
95
|
+
if (!existing) return problemResponse(notFound('The data type could not be found'))
|
|
96
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
97
|
+
// Keep the storage type the editor was created with.
|
|
98
|
+
await port.save(
|
|
99
|
+
{ ...fromRequest(key, body), dbType: existing.dbType },
|
|
100
|
+
ctx.principal as NonNullable<typeof ctx.principal>,
|
|
101
|
+
)
|
|
102
|
+
ctx.notifications.push({ message: 'Data type saved', category: 'Data Type', type: 'Success' })
|
|
103
|
+
return new Response(null, { status: 200 })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
router.handle('DeleteDataTypeById', async (ctx) => {
|
|
107
|
+
const removed = await port.remove(ctx.params.id as string)
|
|
108
|
+
if (removed === 'in-use')
|
|
109
|
+
return problemResponse(
|
|
110
|
+
problemDetails({
|
|
111
|
+
title: 'The data type is in use',
|
|
112
|
+
status: 400,
|
|
113
|
+
detail: 'Remove it from every content type that uses it first.',
|
|
114
|
+
operationStatus: 'InUse',
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
if (!removed) return problemResponse(notFound('The data type could not be found'))
|
|
118
|
+
return new Response(null, { status: 200 })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const foldersOnly = (ctx: RequestContext) => ctx.url.searchParams.get('foldersOnly') === 'true'
|
|
122
|
+
const toDataTypeTreeItem = (item: TreeItem) => ({
|
|
123
|
+
...toTreeItemResponse(item),
|
|
124
|
+
editorUiAlias: item.editorUiAlias ?? null,
|
|
125
|
+
isDeletable: true,
|
|
126
|
+
noAccess: false,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
router.handle('GetTreeDataTypeRoot', async (ctx) => {
|
|
130
|
+
const page = paging(ctx)
|
|
131
|
+
if (!page) return problemResponse(invalidSkipTake())
|
|
132
|
+
const result = await port.treeRoot({ ...page, foldersOnly: foldersOnly(ctx) })
|
|
133
|
+
return Response.json({ total: result.total, items: result.items.map(toDataTypeTreeItem) })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
router.handle('GetTreeDataTypeChildren', async (ctx) => {
|
|
137
|
+
const page = paging(ctx)
|
|
138
|
+
if (!page) return problemResponse(invalidSkipTake())
|
|
139
|
+
const parentId = ctx.url.searchParams.get('parentId')
|
|
140
|
+
if (!parentId)
|
|
141
|
+
return problemResponse(problemDetails({ title: 'parentId is required', status: 400 }))
|
|
142
|
+
const result = await port.treeChildren(parentId, { ...page, foldersOnly: foldersOnly(ctx) })
|
|
143
|
+
return Response.json({ total: result.total, items: result.items.map(toDataTypeTreeItem) })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
router.handle('GetTreeDataTypeAncestors', async (ctx) => {
|
|
147
|
+
const descendantId = ctx.url.searchParams.get('descendantId')
|
|
148
|
+
// Umbraco binds a missing id to an empty GUID and answers with no ancestors.
|
|
149
|
+
if (!descendantId) return Response.json([])
|
|
150
|
+
return Response.json((await port.ancestors(descendantId)).map(toDataTypeTreeItem))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
router.handle('GetItemDataTypeAncestors', async (ctx) => {
|
|
154
|
+
const result = []
|
|
155
|
+
for (const id of ctx.url.searchParams.getAll('id'))
|
|
156
|
+
result.push({
|
|
157
|
+
id,
|
|
158
|
+
ancestors: (await port.ancestors(id)).map((a) => ({ id: a.key, name: a.name, flags: [] })),
|
|
159
|
+
})
|
|
160
|
+
return Response.json(result)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
router.handle('GetTreeDataTypeSiblings', async (ctx) => {
|
|
164
|
+
const target = ctx.url.searchParams.get('target')
|
|
165
|
+
if (!target)
|
|
166
|
+
return problemResponse(problemDetails({ title: 'target is required', status: 400 }))
|
|
167
|
+
const window = await port.folders.siblings(
|
|
168
|
+
target,
|
|
169
|
+
Number(ctx.url.searchParams.get('before') ?? 0),
|
|
170
|
+
Number(ctx.url.searchParams.get('after') ?? 0),
|
|
171
|
+
foldersOnly(ctx),
|
|
172
|
+
)
|
|
173
|
+
if (!window) return problemResponse(notFound('The data type could not be found'))
|
|
174
|
+
return Response.json({ ...window, items: window.items.map(toDataTypeTreeItem) })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const toItem = (model: DataTypeModel) => ({
|
|
178
|
+
id: model.key,
|
|
179
|
+
name: model.name,
|
|
180
|
+
editorAlias: model.editorAlias,
|
|
181
|
+
editorUiAlias: model.editorUiAlias,
|
|
182
|
+
isDeletable: true,
|
|
183
|
+
flags: [],
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
router.handle('GetFilterDataType', async (ctx) => {
|
|
187
|
+
const page = paging(ctx)
|
|
188
|
+
if (!page) return problemResponse(invalidSkipTake())
|
|
189
|
+
const q = ctx.url.searchParams
|
|
190
|
+
const result = await port.filter(
|
|
191
|
+
{
|
|
192
|
+
name: q.get('name') ?? undefined,
|
|
193
|
+
editorAlias: q.get('editorAlias') ?? undefined,
|
|
194
|
+
editorUiAlias: q.get('editorUiAlias') ?? undefined,
|
|
195
|
+
},
|
|
196
|
+
page,
|
|
197
|
+
)
|
|
198
|
+
return Response.json({ total: result.total, items: result.items.map(toItem) })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
router.handle('GetItemDataTypeSearch', async (ctx) => {
|
|
202
|
+
const page = paging(ctx)
|
|
203
|
+
if (!page) return problemResponse(invalidSkipTake())
|
|
204
|
+
const result = await port.search(ctx.url.searchParams.get('query') ?? '', page)
|
|
205
|
+
return Response.json({ total: result.total, items: result.items.map(toItem) })
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
router.handle('GetDataTypeByIdReferencedBy', async (ctx) => {
|
|
209
|
+
const page = paging(ctx)
|
|
210
|
+
if (!page) return problemResponse(invalidSkipTake())
|
|
211
|
+
const refs = await port.referencedBy(ctx.params.id as string)
|
|
212
|
+
return Response.json({
|
|
213
|
+
total: refs.length,
|
|
214
|
+
items: refs.slice(page.skip, page.skip + page.take).map((r) => ({
|
|
215
|
+
$type: 'DocumentTypePropertyTypeReferenceResponseModel',
|
|
216
|
+
id: r.propertyKey,
|
|
217
|
+
alias: r.alias,
|
|
218
|
+
name: r.name,
|
|
219
|
+
documentType: {
|
|
220
|
+
id: r.contentType.key,
|
|
221
|
+
alias: r.contentType.alias,
|
|
222
|
+
name: r.contentType.name,
|
|
223
|
+
icon: r.contentType.icon,
|
|
224
|
+
},
|
|
225
|
+
})),
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
router.handle('GetDataTypeConfiguration', () =>
|
|
230
|
+
Response.json({
|
|
231
|
+
canBeChanged: 'True',
|
|
232
|
+
documentListViewId: 'c0808dd3-8133-4e4b-8ce8-e2bea84a96a4',
|
|
233
|
+
mediaListViewId: '3a0156c4-3b8c-4803-bdc1-6871faa83fff',
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
router.handle('PutDataTypeByIdMove', async (ctx) => {
|
|
238
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
239
|
+
const moved = await port.move(ctx.params.id as string, refOf(body.target))
|
|
240
|
+
if (!moved) return problemResponse(notFound('The data type could not be found'))
|
|
241
|
+
return new Response(null, { status: 200 })
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
router.handle('PostDataTypeByIdCopy', async (ctx) => {
|
|
245
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
246
|
+
const copy = await port.copy(ctx.params.id as string, refOf(body.target))
|
|
247
|
+
if (!copy) return problemResponse(notFound('The data type could not be found'))
|
|
248
|
+
return created(`${ctx.url.origin}/umbraco/management/api/v1/data-type/${copy.key}`, copy.key)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
registerFolderHandlers(router, 'DataType', port.folders, toDataTypeTreeItem)
|
|
252
|
+
|
|
253
|
+
router.handle('GetItemDataType', async (ctx) => {
|
|
254
|
+
const keys = ctx.url.searchParams.getAll('id')
|
|
255
|
+
const models = await port.byKeys(keys)
|
|
256
|
+
return Response.json(
|
|
257
|
+
models.map((model) => ({
|
|
258
|
+
id: model.key,
|
|
259
|
+
name: model.name,
|
|
260
|
+
editorAlias: model.editorAlias,
|
|
261
|
+
editorUiAlias: model.editorUiAlias,
|
|
262
|
+
isDeletable: true,
|
|
263
|
+
flags: [],
|
|
264
|
+
})),
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** Dictionary items: the list, tree, item lookups, CRUD, move, and `.udt` import and export. */
|
|
2
|
+
import {
|
|
3
|
+
invalidSkipTake,
|
|
4
|
+
notFound,
|
|
5
|
+
paged,
|
|
6
|
+
parseSkipTake,
|
|
7
|
+
problemDetails,
|
|
8
|
+
problemResponse,
|
|
9
|
+
} from '@crombolo/core'
|
|
10
|
+
import type { DictionaryItemModel, DictionaryPort, DictionaryWriteStatus } from '../ports.ts'
|
|
11
|
+
import type { ManagementApiRouter, RequestContext } from '../router.ts'
|
|
12
|
+
import { created } from './content-type.ts'
|
|
13
|
+
|
|
14
|
+
const itemNotFound = () => problemResponse(notFound('The dictionary item could not be found'))
|
|
15
|
+
|
|
16
|
+
function statusResult(status: DictionaryWriteStatus): Response | undefined {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'Success':
|
|
19
|
+
return undefined
|
|
20
|
+
case 'NotFound':
|
|
21
|
+
return itemNotFound()
|
|
22
|
+
case 'ParentNotFound':
|
|
23
|
+
return problemResponse(notFound('The dictionary item parent could not be found', undefined))
|
|
24
|
+
case 'DuplicateItemKey':
|
|
25
|
+
return problemResponse(
|
|
26
|
+
problemDetails({
|
|
27
|
+
title: 'Duplicate dictionary item name detected',
|
|
28
|
+
detail:
|
|
29
|
+
'Another dictionary item exists with the same name. Dictionary item names must be unique.',
|
|
30
|
+
status: 409,
|
|
31
|
+
operationStatus: 'DuplicateItemKey',
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
case 'InvalidParent':
|
|
35
|
+
return problemResponse(
|
|
36
|
+
problemDetails({
|
|
37
|
+
title: 'Invalid parent',
|
|
38
|
+
detail:
|
|
39
|
+
'The targeted parent dictionary item is not valid for this dictionary item operation.',
|
|
40
|
+
status: 400,
|
|
41
|
+
operationStatus: 'InvalidParent',
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
case 'DuplicateKey':
|
|
45
|
+
return problemResponse(
|
|
46
|
+
problemDetails({
|
|
47
|
+
title: 'Duplicate key',
|
|
48
|
+
detail: 'A dictionary item with this id already exists.',
|
|
49
|
+
status: 400,
|
|
50
|
+
operationStatus: 'DuplicateKey',
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
case 'InvalidLanguage':
|
|
54
|
+
return problemResponse(
|
|
55
|
+
problemDetails({
|
|
56
|
+
title: 'Invalid language',
|
|
57
|
+
detail: 'A translation names a language the site does not have.',
|
|
58
|
+
status: 400,
|
|
59
|
+
operationStatus: 'InvalidLanguageIsoCode',
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readTranslations(value: unknown): DictionaryItemModel['translations'] {
|
|
66
|
+
if (!Array.isArray(value)) return []
|
|
67
|
+
return value.flatMap((raw) => {
|
|
68
|
+
const t = raw as Record<string, unknown>
|
|
69
|
+
return typeof t.isoCode === 'string'
|
|
70
|
+
? [
|
|
71
|
+
{
|
|
72
|
+
isoCode: t.isoCode,
|
|
73
|
+
translation: typeof t.translation === 'string' ? t.translation : '',
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
: []
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const refId = (value: unknown): string | null => {
|
|
81
|
+
const id = (value as { id?: unknown } | null | undefined)?.id
|
|
82
|
+
return typeof id === 'string' ? id : null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const treeItem = (item: {
|
|
86
|
+
key: string
|
|
87
|
+
name: string
|
|
88
|
+
parentKey: string | null
|
|
89
|
+
hasChildren: boolean
|
|
90
|
+
}) => ({
|
|
91
|
+
id: item.key,
|
|
92
|
+
name: item.name,
|
|
93
|
+
parent: item.parentKey ? { id: item.parentKey } : null,
|
|
94
|
+
hasChildren: item.hasChildren,
|
|
95
|
+
flags: [],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function page<T>(ctx: RequestContext, items: readonly T[]): Response {
|
|
99
|
+
const parsed = parseSkipTake(ctx.url.searchParams)
|
|
100
|
+
if (!parsed.ok) return problemResponse(invalidSkipTake())
|
|
101
|
+
const { skip, take } = parsed.value
|
|
102
|
+
return Response.json(paged(items.slice(skip, skip + take), items.length))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function registerDictionaryHandlers(
|
|
106
|
+
router: ManagementApiRouter,
|
|
107
|
+
dictionary: DictionaryPort,
|
|
108
|
+
): void {
|
|
109
|
+
router.handle('GetDictionary', async (ctx) => {
|
|
110
|
+
const filter = ctx.url.searchParams.get('filter') ?? undefined
|
|
111
|
+
const all = await dictionary.all(filter || undefined)
|
|
112
|
+
return page(
|
|
113
|
+
ctx,
|
|
114
|
+
all.map((item) => ({
|
|
115
|
+
id: item.key,
|
|
116
|
+
name: item.name,
|
|
117
|
+
parent: item.parentKey ? { id: item.parentKey } : null,
|
|
118
|
+
translatedIsoCodes: item.translations
|
|
119
|
+
.filter((t) => t.translation.trim() !== '')
|
|
120
|
+
.map((t) => t.isoCode),
|
|
121
|
+
})),
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
router.handle('GetDictionaryById', async ({ params }) => {
|
|
126
|
+
const item = await dictionary.get(params.id as string)
|
|
127
|
+
if (!item) return itemNotFound()
|
|
128
|
+
return Response.json({ id: item.key, name: item.name, translations: item.translations })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
router.handle('PostDictionary', async (ctx) => {
|
|
132
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
133
|
+
const key = typeof body.id === 'string' ? body.id : crypto.randomUUID()
|
|
134
|
+
const failed = statusResult(
|
|
135
|
+
await dictionary.create({
|
|
136
|
+
key,
|
|
137
|
+
name: String(body.name ?? '').trim(),
|
|
138
|
+
parentKey: refId(body.parent),
|
|
139
|
+
translations: readTranslations(body.translations),
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
if (failed) return failed
|
|
143
|
+
return created(`${ctx.url.origin}/umbraco/management/api/v1/dictionary/${key}`, key)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
router.handle('PutDictionaryById', async (ctx) => {
|
|
147
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
148
|
+
const failed = statusResult(
|
|
149
|
+
await dictionary.update(ctx.params.id as string, {
|
|
150
|
+
name: String(body.name ?? '').trim(),
|
|
151
|
+
translations: readTranslations(body.translations),
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
return failed ?? new Response(null, { status: 200 })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
router.handle('DeleteDictionaryById', async ({ params }) => {
|
|
158
|
+
return statusResult(await dictionary.delete(params.id as string)) ?? new Response(null)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
router.handle('PutDictionaryByIdMove', async (ctx) => {
|
|
162
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
163
|
+
return (
|
|
164
|
+
statusResult(await dictionary.move(ctx.params.id as string, refId(body.target))) ??
|
|
165
|
+
new Response(null)
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
router.handle('GetDictionaryByIdExport', async (ctx) => {
|
|
170
|
+
const file = await dictionary.export(
|
|
171
|
+
ctx.params.id as string,
|
|
172
|
+
ctx.url.searchParams.get('includeChildren') === 'true',
|
|
173
|
+
)
|
|
174
|
+
if (!file) return itemNotFound()
|
|
175
|
+
return new Response(file.content, {
|
|
176
|
+
headers: {
|
|
177
|
+
'content-type': 'application/octet-stream',
|
|
178
|
+
'content-disposition': `attachment; filename="${file.fileName.replaceAll('"', '')}"`,
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
router.handle('PostDictionaryImport', async (ctx) => {
|
|
184
|
+
const body = (await ctx.request.json()) as Record<string, unknown>
|
|
185
|
+
const fileId = refId(body.temporaryFile)
|
|
186
|
+
if (!fileId)
|
|
187
|
+
return problemResponse(problemDetails({ title: 'A temporary file is required', status: 400 }))
|
|
188
|
+
const result = await dictionary.import(fileId, refId(body.parent))
|
|
189
|
+
if (!result.ok)
|
|
190
|
+
return problemResponse(
|
|
191
|
+
result.status === 'Invalid'
|
|
192
|
+
? problemDetails({
|
|
193
|
+
title: 'Invalid file',
|
|
194
|
+
detail: result.reason,
|
|
195
|
+
status: 400,
|
|
196
|
+
operationStatus: 'InvalidFileContent',
|
|
197
|
+
})
|
|
198
|
+
: notFound(
|
|
199
|
+
result.status === 'ParentNotFound'
|
|
200
|
+
? 'The parent dictionary item could not be found'
|
|
201
|
+
: 'The temporary file could not be found',
|
|
202
|
+
result.reason,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
return created(
|
|
206
|
+
`${ctx.url.origin}/umbraco/management/api/v1/dictionary/${result.key}`,
|
|
207
|
+
result.key,
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
router.handle('GetItemDictionary', async (ctx) => {
|
|
212
|
+
const items = await dictionary.items(ctx.url.searchParams.getAll('id'))
|
|
213
|
+
return Response.json(items.map((item) => ({ id: item.key, name: item.name, flags: [] })))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
router.handle('GetTreeDictionaryRoot', async (ctx) =>
|
|
217
|
+
page(ctx, (await dictionary.children(null)).map(treeItem)),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
router.handle('GetTreeDictionaryChildren', async (ctx) => {
|
|
221
|
+
const parentId = ctx.url.searchParams.get('parentId')
|
|
222
|
+
return page(ctx, parentId ? (await dictionary.children(parentId)).map(treeItem) : [])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
router.handle('GetTreeDictionaryAncestors', async (ctx) => {
|
|
226
|
+
const id = ctx.url.searchParams.get('descendantId')
|
|
227
|
+
if (!id) return Response.json([])
|
|
228
|
+
const chain = await dictionary.ancestry(id)
|
|
229
|
+
const withChildren = new Set(
|
|
230
|
+
(await Promise.all(chain.map((item) => dictionary.children(item.key))))
|
|
231
|
+
.map((children, i) => (children.length > 0 ? chain[i]?.key : undefined))
|
|
232
|
+
.filter((key): key is string => key !== undefined),
|
|
233
|
+
)
|
|
234
|
+
return Response.json(
|
|
235
|
+
chain.map((item) =>
|
|
236
|
+
treeItem({
|
|
237
|
+
key: item.key,
|
|
238
|
+
name: item.name,
|
|
239
|
+
parentKey: item.parentKey,
|
|
240
|
+
hasChildren: withChildren.has(item.key),
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
}
|