@crombolo/api-management 0.10.0 → 0.12.1
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/package.json +3 -3
- package/src/audit.ts +430 -0
- package/src/divergences.ts +22 -0
- package/src/handlers/audit-log.ts +131 -0
- package/src/handlers/data-type.ts +25 -0
- package/src/handlers/diagnostics.ts +89 -0
- package/src/handlers/document-blueprint.ts +1 -3
- package/src/handlers/document.ts +75 -49
- package/src/handlers/dynamic-root.ts +63 -0
- package/src/handlers/element.ts +1 -2
- package/src/handlers/media.ts +13 -9
- package/src/handlers/reference-data.ts +35 -0
- package/src/handlers/relations.ts +60 -0
- package/src/handlers/search-index.ts +96 -0
- package/src/handlers/settings-stubs.ts +1 -10
- package/src/handlers/start-node-trees.ts +26 -0
- package/src/handlers/static-files.ts +98 -0
- package/src/handlers/template.ts +2 -0
- package/src/handlers/webhook.ts +146 -0
- package/src/index.ts +5 -0
- package/src/ports-content.ts +29 -0
- package/src/ports-diagnostics.ts +17 -0
- package/src/ports-webhooks.ts +32 -0
- package/src/ports.ts +68 -0
- package/src/router.ts +130 -1
- package/src/routes.ts +47 -1
- package/src/webhook-events.ts +74 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static-file picker's tree: the stylesheets and scripts folders the site
|
|
3
|
+
* serves at `/css/` and `/scripts/`, so every path it offers is one a browser
|
|
4
|
+
* can load as it stands. Read-only; the files are edited in their own areas.
|
|
5
|
+
*/
|
|
6
|
+
import { invalidSkipTake, paged, parseSkipTake, problemResponse } from '@crombolo/core'
|
|
7
|
+
import type { FileEntry, FileSystemPort } from '../ports-files.ts'
|
|
8
|
+
import type { ManagementApiRouter, RequestContext } from '../router.ts'
|
|
9
|
+
|
|
10
|
+
type Area = Pick<FileSystemPort, 'children' | 'entry'>
|
|
11
|
+
|
|
12
|
+
/** The two served folders, by the URL segment each is served under. */
|
|
13
|
+
export function createStaticFiles(mounts: Record<string, Area>): Area {
|
|
14
|
+
const split = (path: string): [Area, string, string] | undefined => {
|
|
15
|
+
const [, mount = '', ...rest] = path.replace(/\/+$/, '').split('/')
|
|
16
|
+
const area = mounts[mount]
|
|
17
|
+
return area ? [area, mount, `/${rest.join('/')}`] : undefined
|
|
18
|
+
}
|
|
19
|
+
const mounted = (mount: string, entry: FileEntry): FileEntry => ({
|
|
20
|
+
...entry,
|
|
21
|
+
path: `/${mount}${entry.path}`,
|
|
22
|
+
parentPath: entry.parentPath ? `/${mount}${entry.parentPath}` : `/${mount}`,
|
|
23
|
+
})
|
|
24
|
+
const folder = async (mount: string, area: Area): Promise<FileEntry | undefined> => {
|
|
25
|
+
const children = await area.children(null)
|
|
26
|
+
if (!children || children.length === 0) return undefined
|
|
27
|
+
return { name: mount, path: `/${mount}`, parentPath: null, isFolder: true, hasChildren: true }
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async children(parentPath) {
|
|
31
|
+
if (!parentPath || parentPath === '/') {
|
|
32
|
+
const roots = await Promise.all(
|
|
33
|
+
Object.entries(mounts).map(([mount, area]) => folder(mount, area)),
|
|
34
|
+
)
|
|
35
|
+
return roots.filter((entry): entry is FileEntry => entry !== undefined)
|
|
36
|
+
}
|
|
37
|
+
const found = split(parentPath)
|
|
38
|
+
if (!found) return undefined
|
|
39
|
+
const [area, mount, inner] = found
|
|
40
|
+
const children = await area.children(inner === '/' ? null : inner)
|
|
41
|
+
return children?.map((entry) => mounted(mount, entry))
|
|
42
|
+
},
|
|
43
|
+
async entry(path) {
|
|
44
|
+
const found = split(path)
|
|
45
|
+
if (!found) return undefined
|
|
46
|
+
const [area, mount, inner] = found
|
|
47
|
+
if (inner === '/') return folder(mount, area)
|
|
48
|
+
const entry = await area.entry(inner)
|
|
49
|
+
return entry ? mounted(mount, entry) : undefined
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const treeItem = (entry: FileEntry) => ({
|
|
55
|
+
name: entry.name,
|
|
56
|
+
path: entry.path,
|
|
57
|
+
parent: entry.parentPath ? { path: entry.parentPath } : null,
|
|
58
|
+
isFolder: entry.isFolder,
|
|
59
|
+
hasChildren: entry.hasChildren,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export function registerStaticFileHandlers(router: ManagementApiRouter, files: Area): void {
|
|
63
|
+
const page = (ctx: RequestContext, entries: readonly FileEntry[] | undefined) => {
|
|
64
|
+
const parsed = parseSkipTake(ctx.url.searchParams)
|
|
65
|
+
if (!parsed.ok) return problemResponse(invalidSkipTake())
|
|
66
|
+
const all = (entries ?? []).map(treeItem)
|
|
67
|
+
const { skip, take } = parsed.value
|
|
68
|
+
return Response.json(paged(all.slice(skip, skip + take), all.length))
|
|
69
|
+
}
|
|
70
|
+
router.handle('GetTreeStaticFileRoot', async (ctx) => page(ctx, await files.children(null)))
|
|
71
|
+
router.handle('GetTreeStaticFileChildren', async (ctx) =>
|
|
72
|
+
page(ctx, await files.children(ctx.url.searchParams.get('parentPath') || null)),
|
|
73
|
+
)
|
|
74
|
+
router.handle('GetTreeStaticFileAncestors', async (ctx) => {
|
|
75
|
+
const path = ctx.url.searchParams.get('descendantPath')
|
|
76
|
+
const chain: FileEntry[] = []
|
|
77
|
+
let current = path ? await files.entry(path) : undefined
|
|
78
|
+
while (current) {
|
|
79
|
+
chain.unshift(current)
|
|
80
|
+
current = current.parentPath ? await files.entry(current.parentPath) : undefined
|
|
81
|
+
}
|
|
82
|
+
return Response.json(chain.map(treeItem))
|
|
83
|
+
})
|
|
84
|
+
router.handle('GetItemStaticFile', async (ctx) => {
|
|
85
|
+
const items = []
|
|
86
|
+
for (const path of ctx.url.searchParams.getAll('path')) {
|
|
87
|
+
const entry = await files.entry(path)
|
|
88
|
+
if (entry)
|
|
89
|
+
items.push({
|
|
90
|
+
isFolder: entry.isFolder,
|
|
91
|
+
name: entry.name,
|
|
92
|
+
parent: entry.parentPath ? { path: entry.parentPath } : null,
|
|
93
|
+
path: entry.path,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return Response.json(items)
|
|
97
|
+
})
|
|
98
|
+
}
|
package/src/handlers/template.ts
CHANGED
|
@@ -22,6 +22,8 @@ function toResponse(model: ComponentModel): ResponseOf<'GetTemplateById'> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function registerTemplateHandlers(router: ManagementApiRouter, port: TemplatePort): void {
|
|
25
|
+
// Templates are the site's components and are always on.
|
|
26
|
+
router.handle('GetTemplateConfiguration', () => Response.json({ disabled: false }))
|
|
25
27
|
router.handle('GetTemplateById', async ({ params }) => {
|
|
26
28
|
const model = await port.byKey(params.id as string)
|
|
27
29
|
if (!model) return problemResponse(notFound('The template could not be found'))
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook operations: definitions, the events they may name, and the
|
|
3
|
+
* delivery log. docs/webhooks.md.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
invalidSkipTake,
|
|
7
|
+
notFound,
|
|
8
|
+
paged,
|
|
9
|
+
parseSkipTake,
|
|
10
|
+
problemDetails,
|
|
11
|
+
problemResponse,
|
|
12
|
+
} from '@crombolo/core'
|
|
13
|
+
import type {
|
|
14
|
+
WebhookInput,
|
|
15
|
+
WebhookPort,
|
|
16
|
+
WebhookPortResult,
|
|
17
|
+
WebhookPortView,
|
|
18
|
+
} from '../ports-webhooks.ts'
|
|
19
|
+
import type { ManagementApiRouter, RequestContext } from '../router.ts'
|
|
20
|
+
import { WEBHOOK_EVENTS } from '../webhook-events.ts'
|
|
21
|
+
import { created } from './content-type.ts'
|
|
22
|
+
|
|
23
|
+
const V1 = '/umbraco/management/api/v1'
|
|
24
|
+
|
|
25
|
+
const EVENTS = new Map(WEBHOOK_EVENTS.map((event) => [event.alias, event]))
|
|
26
|
+
|
|
27
|
+
/** Umbraco names each event in the response, not just by alias. */
|
|
28
|
+
function toResponse(webhook: WebhookPortView) {
|
|
29
|
+
return {
|
|
30
|
+
...webhook,
|
|
31
|
+
events: webhook.events.map(
|
|
32
|
+
(alias) => EVENTS.get(alias) ?? { alias, eventName: alias, eventType: 'Other' },
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const strings = (value: unknown): string[] =>
|
|
38
|
+
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
|
39
|
+
|
|
40
|
+
/** The request body as a webhook; what is not there reads as empty, for the validator to refuse. */
|
|
41
|
+
async function inputOf(ctx: RequestContext): Promise<WebhookInput> {
|
|
42
|
+
const body = ((await ctx.request.json().catch(() => ({}))) ?? {}) as Record<string, unknown>
|
|
43
|
+
const text = (key: string) => (typeof body[key] === 'string' ? (body[key] as string) : null)
|
|
44
|
+
const headers: Record<string, string> = {}
|
|
45
|
+
if (body.headers && typeof body.headers === 'object' && !Array.isArray(body.headers))
|
|
46
|
+
for (const [name, value] of Object.entries(body.headers as Record<string, unknown>))
|
|
47
|
+
if (typeof value === 'string') headers[name] = value
|
|
48
|
+
return {
|
|
49
|
+
id: text('id') ?? undefined,
|
|
50
|
+
name: text('name'),
|
|
51
|
+
description: text('description'),
|
|
52
|
+
url: text('url') ?? '',
|
|
53
|
+
enabled: body.enabled !== false,
|
|
54
|
+
events: strings(body.events),
|
|
55
|
+
contentTypeKeys: strings(body.contentTypeKeys).map((key) => key.toLowerCase()),
|
|
56
|
+
headers,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function refused(result: Exclude<WebhookPortResult, { ok: true }>): Response {
|
|
61
|
+
if (result.status === 404) return problemResponse(notFound('The webhook could not be found'))
|
|
62
|
+
return problemResponse(
|
|
63
|
+
problemDetails({
|
|
64
|
+
title: result.reason,
|
|
65
|
+
status: result.status,
|
|
66
|
+
detail: result.problems?.join(' '),
|
|
67
|
+
...(result.problems ? { errors: { webhook: result.problems } } : {}),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerWebhookHandlers(router: ManagementApiRouter, port: WebhookPort): void {
|
|
73
|
+
router.handle('GetWebhook', async (ctx) => {
|
|
74
|
+
const page = parseSkipTake(ctx.url.searchParams)
|
|
75
|
+
if (!page.ok) return problemResponse(invalidSkipTake())
|
|
76
|
+
const all = await port.list()
|
|
77
|
+
const { skip, take } = page.value
|
|
78
|
+
return Response.json(paged(all.slice(skip, skip + take).map(toResponse), all.length))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
router.handle('GetWebhookById', async (ctx) => {
|
|
82
|
+
const webhook = await port.get(ctx.params.id as string)
|
|
83
|
+
return webhook
|
|
84
|
+
? Response.json(toResponse(webhook))
|
|
85
|
+
: problemResponse(notFound('The webhook could not be found'))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
router.handle('GetItemWebhook', async (ctx) => {
|
|
89
|
+
const ids = new Set(ctx.url.searchParams.getAll('id').map((id) => id.toLowerCase()))
|
|
90
|
+
const all = await port.list()
|
|
91
|
+
return Response.json(
|
|
92
|
+
all
|
|
93
|
+
.filter((webhook) => ids.has(webhook.id))
|
|
94
|
+
.map((webhook) => ({
|
|
95
|
+
id: webhook.id,
|
|
96
|
+
name: webhook.name ?? webhook.url,
|
|
97
|
+
enabled: webhook.enabled,
|
|
98
|
+
url: webhook.url,
|
|
99
|
+
events: webhook.events.map((alias) => EVENTS.get(alias)?.eventName ?? alias).join(', '),
|
|
100
|
+
types: webhook.contentTypeKeys.join(', '),
|
|
101
|
+
flags: [],
|
|
102
|
+
})),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
router.handle('PostWebhook', async (ctx) => {
|
|
107
|
+
const input = await inputOf(ctx)
|
|
108
|
+
const result = await port.create({ ...input, id: input.id ?? crypto.randomUUID() })
|
|
109
|
+
if (!result.ok) return refused(result)
|
|
110
|
+
return created(`${ctx.url.origin}${V1}/webhook/${result.id}`, result.id)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
router.handle('PutWebhookById', async (ctx) => {
|
|
114
|
+
const result = await port.update(ctx.params.id as string, await inputOf(ctx))
|
|
115
|
+
return result.ok ? new Response(null, { status: 200 }) : refused(result)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
router.handle('DeleteWebhookById', async (ctx) =>
|
|
119
|
+
(await port.remove(ctx.params.id as string))
|
|
120
|
+
? new Response(null, { status: 200 })
|
|
121
|
+
: problemResponse(notFound('The webhook could not be found')),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
router.handle('GetWebhookEvents', (ctx) => {
|
|
125
|
+
const page = parseSkipTake(ctx.url.searchParams)
|
|
126
|
+
if (!page.ok) return problemResponse(invalidSkipTake())
|
|
127
|
+
const { skip, take } = page.value
|
|
128
|
+
return Response.json(paged(WEBHOOK_EVENTS.slice(skip, skip + take), WEBHOOK_EVENTS.length))
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
router.handle('GetWebhookLogs', async (ctx) => {
|
|
132
|
+
const page = parseSkipTake(ctx.url.searchParams)
|
|
133
|
+
if (!page.ok) return problemResponse(invalidSkipTake())
|
|
134
|
+
const result = await port.logs(undefined, page.value)
|
|
135
|
+
return Response.json(paged(result.items, result.total))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
router.handle('GetWebhookByIdLogs', async (ctx) => {
|
|
139
|
+
const page = parseSkipTake(ctx.url.searchParams)
|
|
140
|
+
if (!page.ok) return problemResponse(invalidSkipTake())
|
|
141
|
+
if (!(await port.get(ctx.params.id as string)))
|
|
142
|
+
return problemResponse(notFound('The webhook could not be found'))
|
|
143
|
+
const result = await port.logs(ctx.params.id as string, page.value)
|
|
144
|
+
return Response.json(paged(result.items, result.total))
|
|
145
|
+
})
|
|
146
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export * from './audit.ts'
|
|
1
2
|
export * from './authorization.ts'
|
|
3
|
+
export * from './divergences.ts'
|
|
2
4
|
export * from './handlers/content-type.ts'
|
|
3
5
|
export * from './handlers/package.ts'
|
|
4
6
|
export * from './handlers/settings-stubs.ts'
|
|
@@ -6,12 +8,15 @@ export * from './handlers/template-query.ts'
|
|
|
6
8
|
export * from './handlers/user.ts'
|
|
7
9
|
export * from './ports.ts'
|
|
8
10
|
export * from './ports-content.ts'
|
|
11
|
+
export * from './ports-diagnostics.ts'
|
|
9
12
|
export * from './ports-files.ts'
|
|
10
13
|
export * from './ports-logs.ts'
|
|
11
14
|
export * from './ports-members.ts'
|
|
12
15
|
export * from './ports-models-builder.ts'
|
|
13
16
|
export * from './ports-packages.ts'
|
|
14
17
|
export * from './ports-users.ts'
|
|
18
|
+
export * from './ports-webhooks.ts'
|
|
15
19
|
export * from './router.ts'
|
|
16
20
|
export * from './routes.ts'
|
|
17
21
|
export * from './server-events.ts'
|
|
22
|
+
export * from './webhook-events.ts'
|
package/src/ports-content.ts
CHANGED
|
@@ -231,6 +231,10 @@ export interface DataTypePort extends TreePort {
|
|
|
231
231
|
paging: SkipTake,
|
|
232
232
|
): Promise<Page<DataTypeModel>>
|
|
233
233
|
search(query: string, paging: SkipTake): Promise<Page<DataTypeModel>>
|
|
234
|
+
/** Whether any current value is saved through it; undefined when it does not exist. */
|
|
235
|
+
isUsed?(key: string): Promise<boolean | undefined>
|
|
236
|
+
/** Whether any current value is saved for a property; undefined when there is no such property. */
|
|
237
|
+
propertyIsUsed?(contentTypeKey: string, alias: string): Promise<boolean | undefined>
|
|
234
238
|
referencedBy(key: string): Promise<
|
|
235
239
|
Array<{
|
|
236
240
|
propertyKey: string
|
|
@@ -262,6 +266,8 @@ export interface TemplatePort extends TreePort {
|
|
|
262
266
|
|
|
263
267
|
export interface DocumentPort extends TreePort {
|
|
264
268
|
byKey(key: string): Promise<DocumentAggregate | undefined>
|
|
269
|
+
/** Every property alias its type has, compositions included. */
|
|
270
|
+
propertyAliases?(key: string): Promise<string[] | undefined>
|
|
265
271
|
/** The published version's values; undefined when nothing is published. */
|
|
266
272
|
byKeyPublished(key: string): Promise<DocumentAggregate | undefined>
|
|
267
273
|
/** What the editor asks before a save: the rules the server would enforce. */
|
|
@@ -498,11 +504,34 @@ export interface MediaPort {
|
|
|
498
504
|
|
|
499
505
|
/** What refers to a document, media item or member — the Info tab's list. */
|
|
500
506
|
export interface ReferencePort {
|
|
507
|
+
/** What refers to anything of this kind in the recycle bin, as the empty-bin dialog asks. */
|
|
508
|
+
recycleBinReferencedBy?(
|
|
509
|
+
area: 'Document' | 'Media' | 'Element',
|
|
510
|
+
paging: SkipTake,
|
|
511
|
+
): Promise<Page<unknown>>
|
|
501
512
|
referencedBy(key: string, paging: SkipTake): Promise<Page<unknown>>
|
|
502
513
|
referencedDescendants(key: string, paging: SkipTake): Promise<Page<{ id: string }>>
|
|
503
514
|
areReferenced(keys: readonly string[], paging: SkipTake): Promise<Page<{ id: string }>>
|
|
504
515
|
}
|
|
505
516
|
|
|
517
|
+
/** A relation type as the wire carries it: `RelationTypeResponseModel`. */
|
|
518
|
+
export interface RelationTypeView {
|
|
519
|
+
id: string
|
|
520
|
+
alias: string
|
|
521
|
+
name: string
|
|
522
|
+
parentObject: { id: string; name: string } | null
|
|
523
|
+
childObject: { id: string; name: string } | null
|
|
524
|
+
isBidirectional: boolean
|
|
525
|
+
isDependency: boolean
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Umbraco's relation types, read-only, with relations derived for each (story 046). */
|
|
529
|
+
export interface RelationPort {
|
|
530
|
+
types(): readonly RelationTypeView[]
|
|
531
|
+
/** Undefined when there is no such type. */
|
|
532
|
+
relations(typeKey: string, paging: SkipTake): Promise<Page<unknown> | undefined>
|
|
533
|
+
}
|
|
534
|
+
|
|
506
535
|
/** One redirect as the dashboard and the Info tab read it. */
|
|
507
536
|
export interface RedirectListing {
|
|
508
537
|
key: string
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResponseOf, Schemas } from '@crombolo/contracts'
|
|
2
|
+
|
|
3
|
+
export type HealthCheckResult = Schemas['HealthCheckResultResponseModel']
|
|
4
|
+
|
|
5
|
+
export interface HealthCheckGroup {
|
|
6
|
+
name: string
|
|
7
|
+
checks: Schemas['HealthCheckModel'][]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The health checks, troubleshooting report and upgrade check. docs/diagnostics.md. */
|
|
11
|
+
export interface DiagnosticsPort {
|
|
12
|
+
groups(): HealthCheckGroup[]
|
|
13
|
+
/** Runs every check in the group; undefined when there is no such group. */
|
|
14
|
+
run(group: string): Promise<Schemas['HealthCheckWithResultPresentationModel'][] | undefined>
|
|
15
|
+
troubleshooting(): Promise<ResponseOf<'GetServerTroubleshooting'>>
|
|
16
|
+
upgradeCheck(): Promise<ResponseOf<'GetServerUpgradeCheck'>>
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Webhooks, as the Management API's eight operations and the item lookup need them. */
|
|
2
|
+
import type { Page, SkipTake } from '@crombolo/core'
|
|
3
|
+
|
|
4
|
+
/** One webhook as the backoffice edits it; a typed-in secret comes back masked. */
|
|
5
|
+
export interface WebhookInput {
|
|
6
|
+
id?: string
|
|
7
|
+
name: string | null
|
|
8
|
+
description: string | null
|
|
9
|
+
url: string
|
|
10
|
+
enabled: boolean
|
|
11
|
+
events: string[]
|
|
12
|
+
contentTypeKeys: string[]
|
|
13
|
+
headers: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WebhookPortView extends WebhookInput {
|
|
17
|
+
id: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type WebhookPortResult =
|
|
21
|
+
| { ok: true; id: string }
|
|
22
|
+
| { ok: false; status: 400 | 404 | 409; reason: string; problems?: string[] }
|
|
23
|
+
|
|
24
|
+
export interface WebhookPort {
|
|
25
|
+
list(): Promise<WebhookPortView[]>
|
|
26
|
+
get(id: string): Promise<WebhookPortView | undefined>
|
|
27
|
+
create(input: WebhookInput): Promise<WebhookPortResult>
|
|
28
|
+
update(id: string, input: WebhookInput): Promise<WebhookPortResult>
|
|
29
|
+
remove(id: string): Promise<boolean>
|
|
30
|
+
/** Already in the wire's `WebhookLogResponseModel` shape; one webhook's when `id` is given. */
|
|
31
|
+
logs(id: string | undefined, paging: SkipTake): Promise<Page<unknown>>
|
|
32
|
+
}
|
package/src/ports.ts
CHANGED
|
@@ -18,15 +18,18 @@ import type {
|
|
|
18
18
|
MediaPort,
|
|
19
19
|
RedirectPort,
|
|
20
20
|
ReferencePort,
|
|
21
|
+
RelationPort,
|
|
21
22
|
TemplatePort,
|
|
22
23
|
TemplateQueryPort,
|
|
23
24
|
} from './ports-content.ts'
|
|
25
|
+
import type { DiagnosticsPort } from './ports-diagnostics.ts'
|
|
24
26
|
import type { FileSystemPort, Snippet } from './ports-files.ts'
|
|
25
27
|
import type { LogViewerPort } from './ports-logs.ts'
|
|
26
28
|
import type { MemberGroupPort, MemberPort, PublicAccessPort } from './ports-members.ts'
|
|
27
29
|
import type { ModelsBuilderPort } from './ports-models-builder.ts'
|
|
28
30
|
import type { PackagePort } from './ports-packages.ts'
|
|
29
31
|
import type { UserDataPort, UserGroupPort, UserPort } from './ports-users.ts'
|
|
32
|
+
import type { WebhookPort } from './ports-webhooks.ts'
|
|
30
33
|
import type { Principal } from './router.ts'
|
|
31
34
|
|
|
32
35
|
export interface ServerPort {
|
|
@@ -111,6 +114,54 @@ export interface PublishedCachePort {
|
|
|
111
114
|
isRebuilding(): boolean
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/** An index as the Examine dashboard lists it. */
|
|
118
|
+
export interface SearchIndexInfo {
|
|
119
|
+
name: string
|
|
120
|
+
searcherName: string
|
|
121
|
+
documentCount: number
|
|
122
|
+
fieldCount: number
|
|
123
|
+
/** Items waiting to be indexed; while any are, the index reports itself rebuilding. */
|
|
124
|
+
pending: number
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** An item's entries in the audit log, for its Info tab. docs/audit-log.md. */
|
|
128
|
+
export interface AuditHistoryPort {
|
|
129
|
+
forItem(
|
|
130
|
+
key: string,
|
|
131
|
+
): Promise<
|
|
132
|
+
Array<{ userKey: string | null; occurredAt: Date; action: string; detail: string | null }>
|
|
133
|
+
>
|
|
134
|
+
/** When the log began; an item's versions from before then stand in for its entries. */
|
|
135
|
+
startedAt(): Promise<Date | undefined>
|
|
136
|
+
/** Entries older than this have been aged out, and versions standing in for them are too. */
|
|
137
|
+
retainedSince(): Date | undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The backoffice search index behind the Examine dashboard. docs/search.md. */
|
|
141
|
+
export interface SearchIndexPort {
|
|
142
|
+
indexes(): Promise<SearchIndexInfo[]>
|
|
143
|
+
index(name: string): Promise<SearchIndexInfo | undefined>
|
|
144
|
+
/** False when there is no index by that name. */
|
|
145
|
+
rebuild(name: string): Promise<boolean>
|
|
146
|
+
searchers(): string[]
|
|
147
|
+
/** Undefined when there is no searcher by that name. */
|
|
148
|
+
query(
|
|
149
|
+
searcherName: string,
|
|
150
|
+
term: string,
|
|
151
|
+
paging: { skip: number; take: number },
|
|
152
|
+
): Promise<
|
|
153
|
+
| {
|
|
154
|
+
total: number
|
|
155
|
+
items: Array<{
|
|
156
|
+
key: string
|
|
157
|
+
score: number
|
|
158
|
+
fields: Array<{ name: string; values: string[] }>
|
|
159
|
+
}>
|
|
160
|
+
}
|
|
161
|
+
| undefined
|
|
162
|
+
>
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
/** Embed markup for a URL, from its oEmbed provider. */
|
|
115
166
|
export interface OEmbedPort {
|
|
116
167
|
markup(
|
|
@@ -122,6 +173,16 @@ export interface OEmbedPort {
|
|
|
122
173
|
>
|
|
123
174
|
}
|
|
124
175
|
|
|
176
|
+
/** A content picker's start node, found from the document being edited. */
|
|
177
|
+
export interface DynamicRootPort {
|
|
178
|
+
resolve(query: {
|
|
179
|
+
currentKey: string | null
|
|
180
|
+
parentKey: string | null
|
|
181
|
+
origin: { alias: string; key: string | null }
|
|
182
|
+
steps: Array<{ alias: string; documentTypeKeys: string[] }>
|
|
183
|
+
}): Promise<string[]>
|
|
184
|
+
}
|
|
185
|
+
|
|
125
186
|
/** Preview mode is a cookie; the server owns its name and attributes. */
|
|
126
187
|
export interface PreviewPort {
|
|
127
188
|
/** `Set-Cookie` values that enter preview for the signed-in editor. */
|
|
@@ -185,11 +246,14 @@ export interface AuthPort {
|
|
|
185
246
|
|
|
186
247
|
export interface ManagementApiDeps {
|
|
187
248
|
server: ServerPort
|
|
249
|
+
/** Health checks, troubleshooting and the upgrade check; needs the database. */
|
|
250
|
+
diagnostics?: DiagnosticsPort
|
|
188
251
|
manifests: ManifestPort
|
|
189
252
|
localization: LocalizationPort
|
|
190
253
|
currentUser: CurrentUserPort
|
|
191
254
|
users?: UserItemPort
|
|
192
255
|
preview?: PreviewPort
|
|
256
|
+
dynamicRoots?: DynamicRootPort
|
|
193
257
|
temporaryFiles?: TemporaryFilePort
|
|
194
258
|
media?: MediaPort
|
|
195
259
|
tags?: TagPort
|
|
@@ -213,6 +277,8 @@ export interface ManagementApiDeps {
|
|
|
213
277
|
logViewer?: LogViewerPort
|
|
214
278
|
modelsBuilder?: ModelsBuilderPort
|
|
215
279
|
publishedCache?: PublishedCachePort
|
|
280
|
+
searchIndex?: SearchIndexPort
|
|
281
|
+
auditHistory?: AuditHistoryPort
|
|
216
282
|
/** The template editor's query builder. */
|
|
217
283
|
templateQuery?: TemplateQueryPort
|
|
218
284
|
memberGroups?: MemberGroupPort
|
|
@@ -220,6 +286,8 @@ export interface ManagementApiDeps {
|
|
|
220
286
|
publicAccess?: PublicAccessPort
|
|
221
287
|
elements?: ElementPort
|
|
222
288
|
references?: ReferencePort
|
|
289
|
+
relations?: RelationPort
|
|
290
|
+
webhooks?: WebhookPort
|
|
223
291
|
redirects?: RedirectPort
|
|
224
292
|
packages?: PackagePort
|
|
225
293
|
}
|