@crombolo/api-management 0.0.0-stage → 0.9.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/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 +189 -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 +58 -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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Members and the groups they belong to. The response types come from the
|
|
3
|
+
* generated contract, so an implementation that drifts from the wire shape is a
|
|
4
|
+
* compile error rather than a broken editor.
|
|
5
|
+
*/
|
|
6
|
+
import type { ResponseOf } from '@crombolo/contracts'
|
|
7
|
+
import type { DocumentValidationError, DocumentValue, Page, SkipTake } from '@crombolo/core'
|
|
8
|
+
import type { PortFailure } from './ports-content.ts'
|
|
9
|
+
import type { Principal } from './router.ts'
|
|
10
|
+
|
|
11
|
+
export type MemberGroupResponse = ResponseOf<'GetMemberGroupById'>
|
|
12
|
+
export type MemberGroupItem = ResponseOf<'GetItemMemberGroup'>[number]
|
|
13
|
+
export type MemberGroupTreeItem = ResponseOf<'GetTreeMemberGroupRoot'>['items'][number]
|
|
14
|
+
|
|
15
|
+
/** A name already taken is Umbraco's only failure here besides a missing group. */
|
|
16
|
+
export type MemberGroupStatus = 'ok' | 'not-found' | 'duplicate-name'
|
|
17
|
+
|
|
18
|
+
export type MemberResponse = ResponseOf<'GetMemberById'>
|
|
19
|
+
export type MemberItem = ResponseOf<'GetItemMember'>[number]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What a create or a save carries. `password` is plaintext here and hashed by
|
|
23
|
+
* the adapter: hashing belongs to `@crombolo/auth`, and a handler should not be
|
|
24
|
+
* able to store one by accident.
|
|
25
|
+
*/
|
|
26
|
+
export interface SaveMember {
|
|
27
|
+
key: string
|
|
28
|
+
memberTypeKey: string
|
|
29
|
+
email: string
|
|
30
|
+
username: string
|
|
31
|
+
password?: string | null
|
|
32
|
+
oldPassword?: string | null
|
|
33
|
+
isApproved: boolean
|
|
34
|
+
isLockedOut: boolean
|
|
35
|
+
isTwoFactorEnabled: boolean
|
|
36
|
+
groupKeys: string[] | undefined
|
|
37
|
+
values: DocumentValue[]
|
|
38
|
+
variants: Array<{ culture: string | null; segment: string | null; name: string }>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MemberFilterCriteria extends SkipTake {
|
|
42
|
+
memberTypeKey?: string | null
|
|
43
|
+
memberGroupName?: string | null
|
|
44
|
+
isApproved?: boolean
|
|
45
|
+
isLockedOut?: boolean
|
|
46
|
+
filter?: string
|
|
47
|
+
orderBy?: string
|
|
48
|
+
orderDirection?: 'Ascending' | 'Descending'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MemberPort {
|
|
52
|
+
byKey(key: string, viewer: Principal): Promise<MemberResponse | undefined>
|
|
53
|
+
/** The rules the server would enforce on a save, for the editor to show first. */
|
|
54
|
+
validate(input: SaveMember): Promise<DocumentValidationError[]>
|
|
55
|
+
create(input: SaveMember, principal: Principal): Promise<{ ok: true; key: string } | PortFailure>
|
|
56
|
+
update(key: string, input: SaveMember, principal: Principal): Promise<{ ok: true } | PortFailure>
|
|
57
|
+
remove(key: string): Promise<boolean>
|
|
58
|
+
/** The Members collection. */
|
|
59
|
+
filter(criteria: MemberFilterCriteria, viewer: Principal): Promise<Page<MemberResponse>>
|
|
60
|
+
items(keys: readonly string[]): Promise<MemberItem[]>
|
|
61
|
+
search(
|
|
62
|
+
query: string,
|
|
63
|
+
options: { allowedMemberTypeKeys: readonly string[] } & SkipTake,
|
|
64
|
+
): Promise<Page<MemberItem>>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PublicAccessResponse = ResponseOf<'GetDocumentByIdPublicAccess'>
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Why a public-access write was refused, in Umbraco's own vocabulary — the
|
|
71
|
+
* client keys its messages off these.
|
|
72
|
+
*/
|
|
73
|
+
export type PublicAccessStatus =
|
|
74
|
+
| 'ok'
|
|
75
|
+
| 'content-not-found'
|
|
76
|
+
| 'login-node-not-found'
|
|
77
|
+
| 'error-node-not-found'
|
|
78
|
+
| 'entry-not-found'
|
|
79
|
+
|
|
80
|
+
export interface PublicAccessInput {
|
|
81
|
+
loginDocumentKey: string
|
|
82
|
+
errorDocumentKey: string
|
|
83
|
+
memberGroupNames: string[]
|
|
84
|
+
memberUserNames: string[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PublicAccessPort {
|
|
88
|
+
/**
|
|
89
|
+
* The entry protecting a document. `includeAncestors` follows the branch up,
|
|
90
|
+
* which is how the editor tells "protected here" from "protected above".
|
|
91
|
+
*/
|
|
92
|
+
entry(
|
|
93
|
+
documentKey: string,
|
|
94
|
+
includeAncestors: boolean,
|
|
95
|
+
): Promise<{ status: PublicAccessStatus; entry?: PublicAccessResponse }>
|
|
96
|
+
save(documentKey: string, input: PublicAccessInput): Promise<PublicAccessStatus>
|
|
97
|
+
remove(documentKey: string): Promise<PublicAccessStatus>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MemberGroupPort {
|
|
101
|
+
list(paging: SkipTake): Promise<Page<MemberGroupResponse>>
|
|
102
|
+
byKey(key: string): Promise<MemberGroupResponse | undefined>
|
|
103
|
+
items(keys: readonly string[]): Promise<MemberGroupItem[]>
|
|
104
|
+
tree(paging: SkipTake): Promise<Page<MemberGroupTreeItem>>
|
|
105
|
+
create(input: {
|
|
106
|
+
key?: string
|
|
107
|
+
name: string
|
|
108
|
+
}): Promise<{ status: MemberGroupStatus; key?: string }>
|
|
109
|
+
rename(key: string, name: string): Promise<MemberGroupStatus>
|
|
110
|
+
remove(key: string): Promise<MemberGroupStatus>
|
|
111
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Umbraco's ModelsBuilder, which generates strongly-typed models from the
|
|
3
|
+
* content types. Crombolo's models are the TypeScript `crombolo generate`
|
|
4
|
+
* writes from the TOML schema, so the same three operations describe it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Umbraco's `OutOfDateType`: `Unknown` when staleness cannot be determined. */
|
|
8
|
+
export type OutOfDateStatus = 'OutOfDate' | 'Current' | 'Unknown'
|
|
9
|
+
|
|
10
|
+
export interface ModelsBuilderInfo {
|
|
11
|
+
/** One of Umbraco's `ModelsMode` strings; the client explains only those. */
|
|
12
|
+
mode: string
|
|
13
|
+
canGenerate: boolean
|
|
14
|
+
outOfDateModels: boolean
|
|
15
|
+
trackingOutOfDateModels: boolean
|
|
16
|
+
lastError: string | null
|
|
17
|
+
version: string | null
|
|
18
|
+
modelsNamespace: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelsBuilderPort {
|
|
22
|
+
info(): Promise<ModelsBuilderInfo>
|
|
23
|
+
status(): Promise<OutOfDateStatus>
|
|
24
|
+
/** Writes the models; the message of a failure is kept for `info().lastError`. */
|
|
25
|
+
build(): Promise<{ ok: true } | { ok: false; error: string }>
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Created packages: the definitions the Packages section builds, and the zip a
|
|
3
|
+
* download turns one into. `docs/17-bundles.md`.
|
|
4
|
+
*
|
|
5
|
+
* The port hands back built bytes rather than a path, because the artifact is
|
|
6
|
+
* never stored — it is serialized when it is asked for, so it cannot be stale.
|
|
7
|
+
*/
|
|
8
|
+
import type { Page, SkipTake } from '@crombolo/core'
|
|
9
|
+
|
|
10
|
+
/** What a package would contain, in the aliases and keys the contract uses. */
|
|
11
|
+
export interface PackageDefinitionInput {
|
|
12
|
+
name: string
|
|
13
|
+
/** The content subtree's root, by key, or null when none is picked. */
|
|
14
|
+
contentNodeId: string | null
|
|
15
|
+
contentLoadChildNodes: boolean
|
|
16
|
+
mediaIds: string[]
|
|
17
|
+
mediaLoadChildNodes: boolean
|
|
18
|
+
/** Null and empty mean the same thing; the contract allows both. */
|
|
19
|
+
elementIds: string[] | null
|
|
20
|
+
contentTypes: string[]
|
|
21
|
+
mediaTypes: string[]
|
|
22
|
+
dataTypes: string[]
|
|
23
|
+
templates: string[]
|
|
24
|
+
partialViews: string[]
|
|
25
|
+
stylesheets: string[]
|
|
26
|
+
scripts: string[]
|
|
27
|
+
languages: string[]
|
|
28
|
+
dictionaryItems: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PackageDefinition extends PackageDefinitionInput {
|
|
32
|
+
id: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PackageWriteResult =
|
|
36
|
+
| { ok: true; id: string }
|
|
37
|
+
| { ok: false; status: PackageWriteStatus }
|
|
38
|
+
|
|
39
|
+
export type PackageWriteStatus = 'NotFound' | 'DuplicateName' | 'InvalidName'
|
|
40
|
+
|
|
41
|
+
/** A built package, ready to send. */
|
|
42
|
+
export interface BuiltPackage {
|
|
43
|
+
fileName: string
|
|
44
|
+
/** Owns its buffer, so it can be a response body without a copy. */
|
|
45
|
+
bytes: Uint8Array<ArrayBuffer>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PackagePort {
|
|
49
|
+
/** Where the Marketplace view points; from the site's configuration. */
|
|
50
|
+
marketplaceUrl(): string
|
|
51
|
+
list(paging: SkipTake): Promise<Page<PackageDefinition>>
|
|
52
|
+
byId(id: string): Promise<PackageDefinition | undefined>
|
|
53
|
+
create(input: PackageDefinitionInput, id: string | undefined): Promise<PackageWriteResult>
|
|
54
|
+
update(id: string, input: PackageDefinitionInput): Promise<PackageWriteResult>
|
|
55
|
+
remove(id: string): Promise<'deleted' | 'notFound'>
|
|
56
|
+
/** Serializes and zips the definition now; undefined when it is gone. */
|
|
57
|
+
build(id: string): Promise<BuiltPackage | undefined>
|
|
58
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Ports for backoffice users, user groups and user data. */
|
|
2
|
+
import type { ResponseOf } from '@crombolo/contracts'
|
|
3
|
+
import type { Page } from '@crombolo/core'
|
|
4
|
+
import type { Principal } from './router.ts'
|
|
5
|
+
|
|
6
|
+
export type UserResponse = ResponseOf<'GetUserById'>
|
|
7
|
+
export type UserGroupResponse = ResponseOf<'GetUserGroupById'>
|
|
8
|
+
|
|
9
|
+
/** Umbraco's `UserOperationStatus` names, as its problem details report them. */
|
|
10
|
+
export type UserStatus =
|
|
11
|
+
| 'Success'
|
|
12
|
+
| 'UserNotFound'
|
|
13
|
+
| 'MissingUserGroup'
|
|
14
|
+
| 'NoUserGroup'
|
|
15
|
+
| 'DuplicateUserName'
|
|
16
|
+
| 'DuplicateEmail'
|
|
17
|
+
| 'InvalidEmail'
|
|
18
|
+
| 'InvalidUserName'
|
|
19
|
+
| 'UserNameIsNotEmail'
|
|
20
|
+
| 'Unauthorized'
|
|
21
|
+
| 'Forbidden'
|
|
22
|
+
| 'CannotDeleteSelf'
|
|
23
|
+
| 'CannotDisableSelf'
|
|
24
|
+
| 'CannotDeleteUserWithLoginHistory'
|
|
25
|
+
| 'CannotDisableInvitedUser'
|
|
26
|
+
| 'CannotInvite'
|
|
27
|
+
| 'InvalidPassword'
|
|
28
|
+
| 'SelfOldPasswordRequired'
|
|
29
|
+
| 'InvalidIsoCode'
|
|
30
|
+
| 'ContentStartNodeNotFound'
|
|
31
|
+
| 'MediaStartNodeNotFound'
|
|
32
|
+
| 'ElementStartNodeNotFound'
|
|
33
|
+
| 'InvalidInviteToken'
|
|
34
|
+
| 'NotInInviteState'
|
|
35
|
+
| 'AvatarFileNotFound'
|
|
36
|
+
| 'InvalidAvatar'
|
|
37
|
+
| 'InvalidUserType'
|
|
38
|
+
| 'DuplicateClientId'
|
|
39
|
+
| 'InvalidClientId'
|
|
40
|
+
| 'InvalidResetCode'
|
|
41
|
+
|
|
42
|
+
export type UserOrder =
|
|
43
|
+
| 'UserName'
|
|
44
|
+
| 'Language'
|
|
45
|
+
| 'Name'
|
|
46
|
+
| 'Email'
|
|
47
|
+
| 'Id'
|
|
48
|
+
| 'CreateDate'
|
|
49
|
+
| 'UpdateDate'
|
|
50
|
+
| 'IsApproved'
|
|
51
|
+
| 'IsLockedOut'
|
|
52
|
+
| 'LastLoginDate'
|
|
53
|
+
|
|
54
|
+
export interface UserFilter {
|
|
55
|
+
filter?: string
|
|
56
|
+
groupKeys: string[]
|
|
57
|
+
states: string[]
|
|
58
|
+
orderBy: UserOrder
|
|
59
|
+
direction: 'Ascending' | 'Descending'
|
|
60
|
+
skip: number
|
|
61
|
+
take: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UserInput {
|
|
65
|
+
key?: string
|
|
66
|
+
kind: 'Default' | 'Api'
|
|
67
|
+
email: string
|
|
68
|
+
userName: string
|
|
69
|
+
name: string
|
|
70
|
+
groupKeys: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface UserUpdateInput {
|
|
74
|
+
email: string
|
|
75
|
+
userName: string
|
|
76
|
+
name: string
|
|
77
|
+
languageIsoCode: string
|
|
78
|
+
groupKeys: string[]
|
|
79
|
+
documentStartNodeKeys: string[]
|
|
80
|
+
hasDocumentRootAccess: boolean
|
|
81
|
+
mediaStartNodeKeys: string[]
|
|
82
|
+
hasMediaRootAccess: boolean
|
|
83
|
+
elementStartNodeKeys: string[]
|
|
84
|
+
hasElementRootAccess: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type Result<T> = { status: 'Success'; value: T } | { status: Exclude<UserStatus, 'Success'> }
|
|
88
|
+
|
|
89
|
+
export interface UserPort {
|
|
90
|
+
list(principal: Principal, skip: number, take: number): Promise<Page<UserResponse>>
|
|
91
|
+
filter(principal: Principal, query: UserFilter): Promise<Page<UserResponse>>
|
|
92
|
+
get(principal: Principal, key: string): Promise<Result<UserResponse>>
|
|
93
|
+
batch(principal: Principal, keys: readonly string[]): Promise<UserResponse[]>
|
|
94
|
+
create(principal: Principal, input: UserInput): Promise<Result<string>>
|
|
95
|
+
invite(principal: Principal, input: UserInput, message: string | null): Promise<Result<string>>
|
|
96
|
+
resendInvite(principal: Principal, key: string, message: string | null): Promise<UserStatus>
|
|
97
|
+
/** Anonymous: whether an invitation token is still good. */
|
|
98
|
+
verifyInvite(key: string, token: string): Promise<UserStatus>
|
|
99
|
+
/** Anonymous: accepts an invitation by setting the first password. */
|
|
100
|
+
createInitialPassword(key: string, token: string, password: string): Promise<UserStatus>
|
|
101
|
+
update(principal: Principal, key: string, input: UserUpdateInput): Promise<UserStatus>
|
|
102
|
+
delete(principal: Principal, keys: readonly string[]): Promise<UserStatus>
|
|
103
|
+
setApproved(principal: Principal, keys: readonly string[], approved: boolean): Promise<UserStatus>
|
|
104
|
+
unlock(principal: Principal, keys: readonly string[]): Promise<UserStatus>
|
|
105
|
+
setGroups(
|
|
106
|
+
principal: Principal,
|
|
107
|
+
userKeys: readonly string[],
|
|
108
|
+
groupKeys: readonly string[],
|
|
109
|
+
): Promise<UserStatus>
|
|
110
|
+
changePassword(
|
|
111
|
+
principal: Principal,
|
|
112
|
+
key: string,
|
|
113
|
+
newPassword: string,
|
|
114
|
+
oldPassword: string | null,
|
|
115
|
+
): Promise<UserStatus>
|
|
116
|
+
/** A generated password, which the backoffice shows once. */
|
|
117
|
+
resetPassword(principal: Principal, key: string): Promise<Result<string>>
|
|
118
|
+
setAvatar(principal: Principal, key: string, temporaryFileId: string): Promise<UserStatus>
|
|
119
|
+
removeAvatar(principal: Principal, key: string): Promise<UserStatus>
|
|
120
|
+
calculateStartNodes(
|
|
121
|
+
principal: Principal,
|
|
122
|
+
key: string,
|
|
123
|
+
): Promise<Result<ResponseOf<'GetUserByIdCalculateStartNodes'>>>
|
|
124
|
+
clientCredentials(principal: Principal, key: string): Promise<Result<string[]>>
|
|
125
|
+
addClientCredential(
|
|
126
|
+
principal: Principal,
|
|
127
|
+
key: string,
|
|
128
|
+
clientId: string,
|
|
129
|
+
secret: string,
|
|
130
|
+
): Promise<UserStatus>
|
|
131
|
+
removeClientCredential(principal: Principal, key: string, clientId: string): Promise<UserStatus>
|
|
132
|
+
setLanguage(principal: Principal, isoCode: string): Promise<UserStatus>
|
|
133
|
+
/** The caller's verbs on each node, as the permission rules calculate them. */
|
|
134
|
+
permissions(
|
|
135
|
+
principal: Principal,
|
|
136
|
+
tree: 'document' | 'media' | 'element',
|
|
137
|
+
keys: readonly string[],
|
|
138
|
+
): Promise<Array<{ nodeKey: string; permissions: string[] }>>
|
|
139
|
+
configuration(): ResponseOf<'GetUserConfiguration'>
|
|
140
|
+
currentConfiguration(): ResponseOf<'GetUserCurrentConfiguration'>
|
|
141
|
+
passwordConfiguration(): ResponseOf<'GetSecurityConfiguration'>['passwordConfiguration']
|
|
142
|
+
/** Anonymous: sends a reset link when the address belongs to a user; says nothing either way. */
|
|
143
|
+
forgotPassword(email: string): Promise<void>
|
|
144
|
+
verifyResetCode(key: string, code: string): Promise<UserStatus>
|
|
145
|
+
resetWithCode(key: string, code: string, password: string): Promise<UserStatus>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface UserGroupInput {
|
|
149
|
+
key?: string
|
|
150
|
+
name: string
|
|
151
|
+
alias: string
|
|
152
|
+
description: string | null
|
|
153
|
+
icon: string | null
|
|
154
|
+
sections: string[]
|
|
155
|
+
languages: string[]
|
|
156
|
+
hasAccessToAllLanguages: boolean
|
|
157
|
+
documentStartNode: string | null
|
|
158
|
+
documentRootAccess: boolean
|
|
159
|
+
mediaStartNode: string | null
|
|
160
|
+
mediaRootAccess: boolean
|
|
161
|
+
elementStartNode: string | null
|
|
162
|
+
elementRootAccess: boolean
|
|
163
|
+
fallbackPermissions: string[]
|
|
164
|
+
permissions: UserGroupResponse['permissions']
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Umbraco's `UserGroupOperationStatus` names. */
|
|
168
|
+
export type UserGroupStatus =
|
|
169
|
+
| 'Success'
|
|
170
|
+
| 'NotFound'
|
|
171
|
+
| 'Unauthorized'
|
|
172
|
+
| 'Forbidden'
|
|
173
|
+
| 'DuplicateAlias'
|
|
174
|
+
| 'DuplicateName'
|
|
175
|
+
| 'MissingName'
|
|
176
|
+
| 'NameTooLong'
|
|
177
|
+
| 'AliasTooLong'
|
|
178
|
+
| 'IsSystemUserGroup'
|
|
179
|
+
| 'CanNotUpdateAliasIsSystemUserGroup'
|
|
180
|
+
| 'DocumentStartNodeKeyNotFound'
|
|
181
|
+
| 'MediaStartNodeKeyNotFound'
|
|
182
|
+
| 'ElementStartNodeKeyNotFound'
|
|
183
|
+
| 'LanguageNotFound'
|
|
184
|
+
| 'AdminGroupCannotBeEmpty'
|
|
185
|
+
| 'UserNotFound'
|
|
186
|
+
|
|
187
|
+
export interface UserGroupPort {
|
|
188
|
+
list(skip: number, take: number): Promise<Page<UserGroupResponse>>
|
|
189
|
+
filter(
|
|
190
|
+
principal: Principal,
|
|
191
|
+
filter: string,
|
|
192
|
+
skip: number,
|
|
193
|
+
take: number,
|
|
194
|
+
): Promise<Page<UserGroupResponse>>
|
|
195
|
+
get(principal: Principal, key: string): Promise<UserGroupResponse | 'NotFound' | 'Forbidden'>
|
|
196
|
+
items(keys: readonly string[]): Promise<ResponseOf<'GetItemUserGroup'>>
|
|
197
|
+
create(
|
|
198
|
+
principal: Principal,
|
|
199
|
+
input: UserGroupInput,
|
|
200
|
+
): Promise<{ status: UserGroupStatus; key?: string }>
|
|
201
|
+
update(principal: Principal, key: string, input: UserGroupInput): Promise<UserGroupStatus>
|
|
202
|
+
delete(principal: Principal, keys: readonly string[]): Promise<UserGroupStatus>
|
|
203
|
+
addUsers(principal: Principal, key: string, userKeys: readonly string[]): Promise<UserGroupStatus>
|
|
204
|
+
removeUsers(
|
|
205
|
+
principal: Principal,
|
|
206
|
+
key: string,
|
|
207
|
+
userKeys: readonly string[],
|
|
208
|
+
): Promise<UserGroupStatus>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface UserDataEntryModel {
|
|
212
|
+
key: string
|
|
213
|
+
group: string
|
|
214
|
+
identifier: string
|
|
215
|
+
value: string
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The current user's key/value data. */
|
|
219
|
+
export interface UserDataPort {
|
|
220
|
+
list(
|
|
221
|
+
principal: Principal,
|
|
222
|
+
filter: { groups: string[]; identifiers: string[] },
|
|
223
|
+
skip: number,
|
|
224
|
+
take: number,
|
|
225
|
+
): Promise<Page<UserDataEntryModel>>
|
|
226
|
+
get(principal: Principal, key: string): Promise<UserDataEntryModel | undefined>
|
|
227
|
+
create(principal: Principal, entry: UserDataEntryModel): Promise<'Success' | 'AlreadyExists'>
|
|
228
|
+
update(principal: Principal, entry: UserDataEntryModel): Promise<'Success' | 'NotFound'>
|
|
229
|
+
delete(principal: Principal, key: string): Promise<'Success' | 'NotFound'>
|
|
230
|
+
}
|
package/src/ports.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports the Management API needs from the rest of the application.
|
|
3
|
+
*
|
|
4
|
+
* Declared here as interfaces so this package depends only on @crombolo/core and
|
|
5
|
+
* @crombolo/contracts; the composition root supplies the implementations. The
|
|
6
|
+
* return types come from the generated contract, so an implementation that
|
|
7
|
+
* drifts from the wire shape fails to compile.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ResponseOf } from '@crombolo/contracts'
|
|
11
|
+
import type { UploadSettings } from '@crombolo/core'
|
|
12
|
+
import type {
|
|
13
|
+
BlueprintPort,
|
|
14
|
+
ContentTypePort,
|
|
15
|
+
DataTypePort,
|
|
16
|
+
DocumentPort,
|
|
17
|
+
ElementPort,
|
|
18
|
+
MediaPort,
|
|
19
|
+
RedirectPort,
|
|
20
|
+
ReferencePort,
|
|
21
|
+
TemplatePort,
|
|
22
|
+
TemplateQueryPort,
|
|
23
|
+
} from './ports-content.ts'
|
|
24
|
+
import type { FileSystemPort, Snippet } from './ports-files.ts'
|
|
25
|
+
import type { LogViewerPort } from './ports-logs.ts'
|
|
26
|
+
import type { MemberGroupPort, MemberPort, PublicAccessPort } from './ports-members.ts'
|
|
27
|
+
import type { ModelsBuilderPort } from './ports-models-builder.ts'
|
|
28
|
+
import type { PackagePort } from './ports-packages.ts'
|
|
29
|
+
import type { UserDataPort, UserGroupPort, UserPort } from './ports-users.ts'
|
|
30
|
+
import type { Principal } from './router.ts'
|
|
31
|
+
|
|
32
|
+
export interface ServerPort {
|
|
33
|
+
status(): ResponseOf<'GetServerStatus'>
|
|
34
|
+
configuration(): ResponseOf<'GetServerConfiguration'>
|
|
35
|
+
information(): ResponseOf<'GetServerInformation'>
|
|
36
|
+
/** What may be uploaded; Umbraco's defaults when omitted. */
|
|
37
|
+
uploadSettings?(): UploadSettings
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ManifestVisibility = 'all' | 'public' | 'private'
|
|
41
|
+
|
|
42
|
+
export interface ManifestPort {
|
|
43
|
+
list(visibility: ManifestVisibility): ResponseOf<'GetManifestManifest'>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LanguageInput {
|
|
47
|
+
isoCode: string
|
|
48
|
+
name: string
|
|
49
|
+
isDefault: boolean
|
|
50
|
+
isMandatory: boolean
|
|
51
|
+
fallbackIsoCode: string | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LocalizationPort {
|
|
55
|
+
/** Languages live in the database (and `schema/languages.toml`); cultures are the fixed list. */
|
|
56
|
+
allLanguages(): Promise<LanguageInput[]>
|
|
57
|
+
language(isoCode: string): Promise<LanguageInput | undefined>
|
|
58
|
+
saveLanguage(language: LanguageInput): Promise<void>
|
|
59
|
+
deleteLanguage(isoCode: string): Promise<'deleted' | 'default' | 'in-use' | 'not-found'>
|
|
60
|
+
cultures(): ResponseOf<'GetCulture'>['items']
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CurrentUserPort {
|
|
64
|
+
get(principal: Principal): ResponseOf<'GetUserCurrent'> | Promise<ResponseOf<'GetUserCurrent'>>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Backoffice users by key, for anything that names who did something. */
|
|
68
|
+
export interface UserItemPort {
|
|
69
|
+
items(
|
|
70
|
+
keys: readonly string[],
|
|
71
|
+
): Promise<Array<{ key: string; name: string; avatarUrls: string[]; kind?: 'Default' | 'Api' }>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Uploads waiting to be saved into a value (Umbraco's temporary files). */
|
|
75
|
+
export interface TemporaryFilePort {
|
|
76
|
+
save(
|
|
77
|
+
id: string,
|
|
78
|
+
file: File,
|
|
79
|
+
): Promise<
|
|
80
|
+
| { ok: true; file: { id: string; fileName: string; availableUntil: Date } }
|
|
81
|
+
| { ok: false; reason: string }
|
|
82
|
+
>
|
|
83
|
+
get(id: string): Promise<{ id: string; fileName: string; availableUntil: Date } | undefined>
|
|
84
|
+
/** The upload's name and bytes, for files that are read rather than placed. */
|
|
85
|
+
read(id: string): Promise<{ fileName: string; bytes: Uint8Array } | undefined>
|
|
86
|
+
remove(id: string): Promise<boolean>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Tags in use, for the tags editor's suggestions. */
|
|
90
|
+
export interface TagPort {
|
|
91
|
+
list(filter: {
|
|
92
|
+
query?: string
|
|
93
|
+
group?: string
|
|
94
|
+
culture?: string | null
|
|
95
|
+
}): Promise<Array<{ id: string; text: string; group: string; nodeCount: number }>>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The published content cache behind the Published Status dashboard.
|
|
100
|
+
*
|
|
101
|
+
* Umbraco distinguishes reloading the in-memory cache from rebuilding the
|
|
102
|
+
* database cache it reads (`cmsContentNu`). There is no such table here — the
|
|
103
|
+
* content tables are the source — so the difference is when the cost is paid:
|
|
104
|
+
* a reload drops the snapshot and lets the next request rebuild it, a rebuild
|
|
105
|
+
* builds it before answering.
|
|
106
|
+
*/
|
|
107
|
+
export interface PublishedCachePort {
|
|
108
|
+
reload(): void
|
|
109
|
+
rebuild(): Promise<void>
|
|
110
|
+
/** Ours completes within the request, so this is only ever false afterwards. */
|
|
111
|
+
isRebuilding(): boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Embed markup for a URL, from its oEmbed provider. */
|
|
115
|
+
export interface OEmbedPort {
|
|
116
|
+
markup(
|
|
117
|
+
url: string,
|
|
118
|
+
maxWidth?: number,
|
|
119
|
+
maxHeight?: number,
|
|
120
|
+
): Promise<
|
|
121
|
+
{ ok: true; markup: string } | { ok: false; status: 'unsupported' | 'failed'; reason: string }
|
|
122
|
+
>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Preview mode is a cookie; the server owns its name and attributes. */
|
|
126
|
+
export interface PreviewPort {
|
|
127
|
+
/** `Set-Cookie` values that enter preview for the signed-in editor. */
|
|
128
|
+
enter(): string[]
|
|
129
|
+
/** `Set-Cookie` values that leave it. */
|
|
130
|
+
exit(): string[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface DictionaryItemModel {
|
|
134
|
+
key: string
|
|
135
|
+
name: string
|
|
136
|
+
parentKey: string | null
|
|
137
|
+
translations: Array<{ isoCode: string; translation: string }>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type DictionaryWriteStatus =
|
|
141
|
+
| 'Success'
|
|
142
|
+
| 'DuplicateItemKey'
|
|
143
|
+
| 'DuplicateKey'
|
|
144
|
+
| 'ParentNotFound'
|
|
145
|
+
| 'NotFound'
|
|
146
|
+
| 'InvalidParent'
|
|
147
|
+
| 'InvalidLanguage'
|
|
148
|
+
|
|
149
|
+
/** Dictionary items and their translations. */
|
|
150
|
+
export interface DictionaryPort {
|
|
151
|
+
all(filter?: string): Promise<DictionaryItemModel[]>
|
|
152
|
+
get(key: string): Promise<DictionaryItemModel | undefined>
|
|
153
|
+
items(keys: readonly string[]): Promise<DictionaryItemModel[]>
|
|
154
|
+
children(
|
|
155
|
+
parentKey: string | null,
|
|
156
|
+
): Promise<Array<{ key: string; name: string; parentKey: string | null; hasChildren: boolean }>>
|
|
157
|
+
/** The item and its ancestors, root first. */
|
|
158
|
+
ancestry(key: string): Promise<DictionaryItemModel[]>
|
|
159
|
+
create(item: DictionaryItemModel): Promise<DictionaryWriteStatus>
|
|
160
|
+
update(
|
|
161
|
+
key: string,
|
|
162
|
+
change: { name: string; translations: DictionaryItemModel['translations'] },
|
|
163
|
+
): Promise<DictionaryWriteStatus>
|
|
164
|
+
move(key: string, parentKey: string | null): Promise<DictionaryWriteStatus>
|
|
165
|
+
delete(key: string): Promise<DictionaryWriteStatus>
|
|
166
|
+
/** The item, with its descendants when asked, as a `.udt` file. */
|
|
167
|
+
export(
|
|
168
|
+
key: string,
|
|
169
|
+
includeChildren: boolean,
|
|
170
|
+
): Promise<{ fileName: string; content: string } | undefined>
|
|
171
|
+
/** Creates or updates the items in an uploaded `.udt` file, under `parentKey`. */
|
|
172
|
+
import(
|
|
173
|
+
temporaryFileId: string,
|
|
174
|
+
parentKey: string | null,
|
|
175
|
+
): Promise<
|
|
176
|
+
| { ok: true; key: string }
|
|
177
|
+
| { ok: false; status: 'NotFound' | 'ParentNotFound' | 'Invalid'; reason: string }
|
|
178
|
+
>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AuthPort {
|
|
182
|
+
/** Ends the caller's session. */
|
|
183
|
+
signOut(principal: Principal): Promise<void>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ManagementApiDeps {
|
|
187
|
+
server: ServerPort
|
|
188
|
+
manifests: ManifestPort
|
|
189
|
+
localization: LocalizationPort
|
|
190
|
+
currentUser: CurrentUserPort
|
|
191
|
+
users?: UserItemPort
|
|
192
|
+
preview?: PreviewPort
|
|
193
|
+
temporaryFiles?: TemporaryFilePort
|
|
194
|
+
media?: MediaPort
|
|
195
|
+
tags?: TagPort
|
|
196
|
+
oembed?: OEmbedPort
|
|
197
|
+
blueprints?: BlueprintPort
|
|
198
|
+
contentTypes?: ContentTypePort
|
|
199
|
+
mediaTypes?: ContentTypePort
|
|
200
|
+
memberTypes?: ContentTypePort
|
|
201
|
+
dataTypes?: DataTypePort
|
|
202
|
+
templates?: TemplatePort
|
|
203
|
+
documents?: DocumentPort
|
|
204
|
+
dictionary?: DictionaryPort
|
|
205
|
+
userManagement?: UserPort
|
|
206
|
+
userGroups?: UserGroupPort
|
|
207
|
+
userData?: UserDataPort
|
|
208
|
+
/** The site's `components/` tree: one root, folders and all. */
|
|
209
|
+
components?: FileSystemPort
|
|
210
|
+
componentSnippets?: readonly Snippet[]
|
|
211
|
+
stylesheets?: FileSystemPort
|
|
212
|
+
scripts?: FileSystemPort
|
|
213
|
+
logViewer?: LogViewerPort
|
|
214
|
+
modelsBuilder?: ModelsBuilderPort
|
|
215
|
+
publishedCache?: PublishedCachePort
|
|
216
|
+
/** The template editor's query builder. */
|
|
217
|
+
templateQuery?: TemplateQueryPort
|
|
218
|
+
memberGroups?: MemberGroupPort
|
|
219
|
+
members?: MemberPort
|
|
220
|
+
publicAccess?: PublicAccessPort
|
|
221
|
+
elements?: ElementPort
|
|
222
|
+
references?: ReferencePort
|
|
223
|
+
redirects?: RedirectPort
|
|
224
|
+
packages?: PackagePort
|
|
225
|
+
}
|