@crombolo/content 0.0.0-stage → 0.23.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 +33 -4
- package/src/actor.ts +97 -0
- package/src/events.ts +257 -0
- package/src/index.ts +2 -0
- package/src/operations.ts +879 -0
- package/src/policy.ts +130 -0
- package/src/rules.ts +8 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kineco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
crombolo
|
|
2
|
+
Copyright (c) 2026 Kineco
|
|
3
|
+
|
|
4
|
+
This product includes third-party software. crombolo is an independent project.
|
|
5
|
+
It is not affiliated with, endorsed by, or sponsored by Umbraco A/S, which owns
|
|
6
|
+
the Umbraco trademark.
|
|
7
|
+
|
|
8
|
+
There are three places third-party material is redistributed, rather than merely
|
|
9
|
+
depended on:
|
|
10
|
+
|
|
11
|
+
@crombolo/backoffice-dist the built Umbraco backoffice and the browser
|
|
12
|
+
packages bundled into it, produced by
|
|
13
|
+
`bun run vendor:backoffice`
|
|
14
|
+
@crombolo/backoffice-dist three CSS files copied unmodified from the Umbraco
|
|
15
|
+
client source tree
|
|
16
|
+
@crombolo/contracts the Umbraco Management API OpenAPI document,
|
|
17
|
+
copied unmodified
|
|
18
|
+
|
|
19
|
+
No Umbraco trademark is redistributed. The Umbraco wordmark, the "U" roundel
|
|
20
|
+
favicon and the installer illustration are deliberately not carried in this
|
|
21
|
+
repository: MIT grants copyright permission and says nothing about trademarks, so
|
|
22
|
+
crombolo's own marks are served at those paths instead. See `BRANDED_ASSETS` in
|
|
23
|
+
`@crombolo/backoffice-host`.
|
|
24
|
+
|
|
25
|
+
The Umbraco backoffice
|
|
26
|
+
----------------------
|
|
27
|
+
@umbraco-cms/backoffice 18.2.0 — MIT — https://github.com/umbraco/Umbraco-CMS
|
|
28
|
+
@umbraco-ui/uui 2.0.2 — MIT — https://github.com/umbraco/Umbraco.UI
|
|
29
|
+
|
|
30
|
+
Static files from the Umbraco client source tree
|
|
31
|
+
------------------------------------------------
|
|
32
|
+
css/umb-css.css, css/rte-content.css, css/umbraco-blockgridlayout.css
|
|
33
|
+
— copied unmodified from Umbraco-CMS release-18.2.0 — MIT
|
|
34
|
+
— https://github.com/umbraco/Umbraco-CMS
|
|
35
|
+
Shipped in @crombolo/backoffice-dist under `upstream-static/`.
|
|
36
|
+
|
|
37
|
+
`rte-content.css` itself carries Tiptap's default editor styles, copied from
|
|
38
|
+
tiptap v2.11.7 (MIT); the file records this in its own header.
|
|
39
|
+
|
|
40
|
+
The Umbraco Management API contract
|
|
41
|
+
-----------------------------------
|
|
42
|
+
OpenApi.json — copied unmodified from Umbraco-CMS release-18.2.0
|
|
43
|
+
(src/Umbraco.Cms.Api.Management/OpenApi.json) — MIT
|
|
44
|
+
— https://github.com/umbraco/Umbraco-CMS
|
|
45
|
+
Shipped in @crombolo/contracts, which also ships the TypeScript types generated
|
|
46
|
+
from it.
|
|
47
|
+
|
|
48
|
+
Browser runtime dependencies bundled into the backoffice build
|
|
49
|
+
--------------------------------------------------------------
|
|
50
|
+
@heximal/expressions 0.1.5 — BSD-3-Clause
|
|
51
|
+
@microsoft/signalr 10.0.11 — MIT
|
|
52
|
+
@tiptap/* 3.31.3 — MIT
|
|
53
|
+
diff 9.0.0 — BSD-3-Clause
|
|
54
|
+
dompurify 3.4.16 — Apache-2.0, elected from "MPL-2.0 OR Apache-2.0"
|
|
55
|
+
element-internals-polyfill 3.0.2 — MIT
|
|
56
|
+
lit 3.3.3 — BSD-3-Clause
|
|
57
|
+
luxon 3.7.2 — MIT
|
|
58
|
+
marked 18.0.14 — MIT
|
|
59
|
+
monaco-editor 0.57.0 — MIT
|
|
60
|
+
rxjs 7.8.2 — Apache-2.0
|
|
61
|
+
uuid 14.0.2 — MIT
|
|
62
|
+
|
|
63
|
+
dompurify is offered under either licence; Apache-2.0 is elected here, so no
|
|
64
|
+
MPL-2.0 obligation attaches to the copy bundled into the backoffice build. Of the
|
|
65
|
+
Apache-2.0 packages above, none ships a NOTICE file of its own, so Apache-2.0
|
|
66
|
+
section 4(d) adds nothing to this file.
|
|
67
|
+
|
|
68
|
+
Each package's own licence text ships inside its npm distribution and applies
|
|
69
|
+
unchanged to the copies bundled here.
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/content",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "The rules every document write follows, whoever starts it: who may, approvals, and what it announces. No transport.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Crombolo",
|
|
7
|
+
"homepage": "https://github.com/kineco-au/crombolo-cms#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kineco-au/crombolo-cms.git",
|
|
11
|
+
"directory": "packages/content"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kineco-au/crombolo-cms/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.3.0"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./rules": "./src/rules.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@crombolo/core": "0.23.0",
|
|
33
|
+
"@crombolo/data": "0.23.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/actor.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a document write is for (story 170). Every operation is handed one, so what
|
|
3
|
+
* it may do, whether approvals apply and whom the audit names never depend on how
|
|
4
|
+
* the write was started. Authentication stays with the transport that made it.
|
|
5
|
+
*/
|
|
6
|
+
import type { GroupGrant, StartNodes } from '@crombolo/core'
|
|
7
|
+
|
|
8
|
+
/** A backoffice user, as far as a document write needs to know one. */
|
|
9
|
+
export interface ContentPrincipal {
|
|
10
|
+
/** The user's key. */
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
isAdmin: boolean
|
|
14
|
+
/** Section manifest aliases, such as `Umb.Section.Content`. */
|
|
15
|
+
allowedSections: readonly string[]
|
|
16
|
+
hasAccessToAllLanguages: boolean
|
|
17
|
+
languages: readonly string[]
|
|
18
|
+
startNodes: { document: StartNodes }
|
|
19
|
+
groups: readonly GroupGrant[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Where a write came from, for the audit log. */
|
|
23
|
+
export interface ActorOrigin {
|
|
24
|
+
source: 'request' | 'cli' | 'job'
|
|
25
|
+
ipAddress?: string | null
|
|
26
|
+
forwardedFor?: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Actor =
|
|
30
|
+
/** A signed-in user: their permissions, start nodes, languages and approvals. */
|
|
31
|
+
| { kind: 'user'; principal: ContentPrincipal; origin: ActorOrigin }
|
|
32
|
+
/**
|
|
33
|
+
* Someone at the command line on the box: no permission checks, since they hold
|
|
34
|
+
* the database, but approvals apply unless they say `skipApproval`, which the
|
|
35
|
+
* audit records.
|
|
36
|
+
*/
|
|
37
|
+
| { kind: 'operator'; name: string; skipApproval?: boolean; origin: ActorOrigin }
|
|
38
|
+
/**
|
|
39
|
+
* The site acting on what was already decided — a schedule, an approved
|
|
40
|
+
* request, a form's workflow: no permission checks and no approval gate.
|
|
41
|
+
*/
|
|
42
|
+
| {
|
|
43
|
+
kind: 'system'
|
|
44
|
+
name: string
|
|
45
|
+
origin: ActorOrigin
|
|
46
|
+
/** The person it acts for, when there is one: the approver of a request. */
|
|
47
|
+
user?: { id: string; name: string }
|
|
48
|
+
/** Said with what it did, in notifications and the audit: `approved through "Legal"`. */
|
|
49
|
+
note?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const userActor = (
|
|
53
|
+
principal: ContentPrincipal,
|
|
54
|
+
origin: Omit<ActorOrigin, 'source'> = {},
|
|
55
|
+
): Actor => ({ kind: 'user', principal, origin: { source: 'request', ...origin } })
|
|
56
|
+
|
|
57
|
+
export const operatorActor = (name: string, options: { skipApproval?: boolean } = {}): Actor => ({
|
|
58
|
+
kind: 'operator',
|
|
59
|
+
name,
|
|
60
|
+
skipApproval: options.skipApproval,
|
|
61
|
+
origin: { source: 'cli' },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const systemActor = (
|
|
65
|
+
name: string,
|
|
66
|
+
options: {
|
|
67
|
+
source?: ActorOrigin['source']
|
|
68
|
+
user?: { id: string; name: string }
|
|
69
|
+
note?: string
|
|
70
|
+
} = {},
|
|
71
|
+
): Actor => ({
|
|
72
|
+
kind: 'system',
|
|
73
|
+
name,
|
|
74
|
+
origin: { source: options.source ?? 'job' },
|
|
75
|
+
user: options.user,
|
|
76
|
+
note: options.note,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** The name an audit entry or a notification gives the actor. */
|
|
80
|
+
export const actorName = (actor: Actor): string =>
|
|
81
|
+
actor.kind === 'user'
|
|
82
|
+
? actor.principal.name
|
|
83
|
+
: actor.kind === 'system'
|
|
84
|
+
? (actor.user?.name ?? actor.name)
|
|
85
|
+
: actor.name
|
|
86
|
+
|
|
87
|
+
/** The user's key, when the actor is one; versions record it as their author. */
|
|
88
|
+
export const actorUserKey = (actor: Actor): string | null =>
|
|
89
|
+
actor.kind === 'user'
|
|
90
|
+
? actor.principal.id
|
|
91
|
+
: actor.kind === 'system'
|
|
92
|
+
? (actor.user?.id ?? null)
|
|
93
|
+
: null
|
|
94
|
+
|
|
95
|
+
/** What the actor says alongside the change, if anything. */
|
|
96
|
+
export const actorNote = (actor: Actor): string | undefined =>
|
|
97
|
+
actor.kind === 'system' ? actor.note : undefined
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a document write did, as the operation reports it (story 170). An
|
|
3
|
+
* operation reports to an `EventSink`; the server and the command line each wire
|
|
4
|
+
* one to the same database-backed steps — the audit log, webhooks, change
|
|
5
|
+
* notifications — and the server adds live backoffice refresh. The mappings here
|
|
6
|
+
* are pure, so every transport turns one event into the same entries.
|
|
7
|
+
*/
|
|
8
|
+
import { type Actor, actorName, actorNote, actorUserKey } from './actor.ts'
|
|
9
|
+
|
|
10
|
+
export type DocumentEvent =
|
|
11
|
+
| { type: 'created'; key: string }
|
|
12
|
+
| { type: 'saved'; key: string }
|
|
13
|
+
| { type: 'createdAndPublished'; key: string; cultures: readonly string[] | null }
|
|
14
|
+
| { type: 'savedAndPublished'; key: string; cultures: readonly string[] | null }
|
|
15
|
+
| { type: 'published'; key: string; cultures: readonly string[] | null }
|
|
16
|
+
/** Only scheduled: nothing is published until the schedule fires. */
|
|
17
|
+
| { type: 'scheduled'; key: string }
|
|
18
|
+
/** A page and the pages below it: `published` names each one that went live. */
|
|
19
|
+
| { type: 'branchPublished'; key: string; published: readonly string[] }
|
|
20
|
+
| { type: 'unpublished'; key: string; cultures: readonly string[] | null }
|
|
21
|
+
| { type: 'trashed'; key: string }
|
|
22
|
+
/** `name` is read before it went, since it cannot be read after. */
|
|
23
|
+
| { type: 'deleted'; key: string; name: string | null }
|
|
24
|
+
| { type: 'restored'; key: string }
|
|
25
|
+
| { type: 'binEmptied' }
|
|
26
|
+
| { type: 'moved'; key: string }
|
|
27
|
+
| { type: 'copied'; key: string; copyKey: string }
|
|
28
|
+
/** `parentKey` null: the top level. */
|
|
29
|
+
| { type: 'sorted'; parentKey: string | null }
|
|
30
|
+
| { type: 'rolledBack'; key: string; versionId: string }
|
|
31
|
+
| { type: 'domainsAssigned'; key: string }
|
|
32
|
+
| { type: 'publicAccessChanged'; key: string; change: 'added' | 'set' | 'removed' }
|
|
33
|
+
|
|
34
|
+
/** Where an operation reports what it did, once the change has committed. */
|
|
35
|
+
export interface EventSink {
|
|
36
|
+
emit(event: DocumentEvent, actor: Actor): Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A sink that hears nothing: for tests and tools that announce nothing. */
|
|
40
|
+
export const SILENT: EventSink = { emit: async () => {} }
|
|
41
|
+
|
|
42
|
+
/** Several sinks heard in turn. */
|
|
43
|
+
export const both = (...sinks: EventSink[]): EventSink => ({
|
|
44
|
+
async emit(event, actor) {
|
|
45
|
+
for (const sink of sinks) await sink.emit(event, actor)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** The audit entry an event writes: the same whichever transport started it. Pure. */
|
|
50
|
+
export function auditFor(event: DocumentEvent): {
|
|
51
|
+
category: 'content' | 'permission'
|
|
52
|
+
action: string
|
|
53
|
+
entityType: string
|
|
54
|
+
entityKey: string | null
|
|
55
|
+
detail?: string
|
|
56
|
+
} {
|
|
57
|
+
const doc = (action: string, key: string | null) => ({
|
|
58
|
+
category: 'content' as const,
|
|
59
|
+
action,
|
|
60
|
+
entityType: 'Document',
|
|
61
|
+
entityKey: key,
|
|
62
|
+
})
|
|
63
|
+
switch (event.type) {
|
|
64
|
+
case 'created':
|
|
65
|
+
return doc('Create', event.key)
|
|
66
|
+
case 'saved':
|
|
67
|
+
return doc('Save', event.key)
|
|
68
|
+
case 'createdAndPublished':
|
|
69
|
+
return doc('CreateAndPublish', event.key)
|
|
70
|
+
case 'savedAndPublished':
|
|
71
|
+
return doc('SaveAndPublish', event.key)
|
|
72
|
+
case 'published':
|
|
73
|
+
case 'scheduled':
|
|
74
|
+
return doc('Publish', event.key)
|
|
75
|
+
case 'branchPublished':
|
|
76
|
+
return doc('PublishWithDescendants', event.key)
|
|
77
|
+
case 'unpublished':
|
|
78
|
+
return doc('Unpublish', event.key)
|
|
79
|
+
case 'trashed':
|
|
80
|
+
return doc('Trash', event.key)
|
|
81
|
+
case 'deleted':
|
|
82
|
+
return doc('Delete', event.key)
|
|
83
|
+
case 'restored':
|
|
84
|
+
return doc('Restore', event.key)
|
|
85
|
+
case 'binEmptied':
|
|
86
|
+
return doc('EmptyRecycleBin', null)
|
|
87
|
+
case 'moved':
|
|
88
|
+
return doc('Move', event.key)
|
|
89
|
+
case 'copied':
|
|
90
|
+
return doc('Copy', event.key)
|
|
91
|
+
case 'sorted':
|
|
92
|
+
return doc('Sort', event.parentKey)
|
|
93
|
+
case 'rolledBack':
|
|
94
|
+
return { ...doc('Rollback', event.versionId), entityType: 'DocumentVersion' }
|
|
95
|
+
case 'domainsAssigned':
|
|
96
|
+
return doc('AssignDomains', event.key)
|
|
97
|
+
case 'publicAccessChanged':
|
|
98
|
+
return {
|
|
99
|
+
...doc(
|
|
100
|
+
event.change === 'added'
|
|
101
|
+
? 'AddPublicAccess'
|
|
102
|
+
: event.change === 'set'
|
|
103
|
+
? 'SetPublicAccess'
|
|
104
|
+
: 'RemovePublicAccess',
|
|
105
|
+
event.key,
|
|
106
|
+
),
|
|
107
|
+
category: 'permission',
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The webhook an event raises, by Umbraco's alias, and the page it names. Pure. */
|
|
113
|
+
export function webhookFor(event: DocumentEvent): { alias: string; key: string } | undefined {
|
|
114
|
+
switch (event.type) {
|
|
115
|
+
case 'created':
|
|
116
|
+
case 'saved':
|
|
117
|
+
return { alias: 'Umbraco.ContentSaved', key: event.key }
|
|
118
|
+
case 'createdAndPublished':
|
|
119
|
+
case 'savedAndPublished':
|
|
120
|
+
case 'published':
|
|
121
|
+
case 'branchPublished':
|
|
122
|
+
return { alias: 'Umbraco.ContentPublish', key: event.key }
|
|
123
|
+
case 'unpublished':
|
|
124
|
+
return { alias: 'Umbraco.ContentUnpublish', key: event.key }
|
|
125
|
+
case 'deleted':
|
|
126
|
+
return { alias: 'Umbraco.ContentDelete', key: event.key }
|
|
127
|
+
case 'trashed':
|
|
128
|
+
return { alias: 'Umbraco.ContentMovedToRecycleBin', key: event.key }
|
|
129
|
+
// Out of the bin is a move, as Umbraco raises it.
|
|
130
|
+
case 'moved':
|
|
131
|
+
case 'restored':
|
|
132
|
+
return { alias: 'Umbraco.ContentMoved', key: event.key }
|
|
133
|
+
case 'copied':
|
|
134
|
+
return { alias: 'Umbraco.ContentCopied', key: event.copyKey }
|
|
135
|
+
case 'sorted':
|
|
136
|
+
return event.parentKey ? { alias: 'Umbraco.ContentSorted', key: event.parentKey } : undefined
|
|
137
|
+
case 'rolledBack':
|
|
138
|
+
return { alias: 'Umbraco.ContentRolledBack', key: event.key }
|
|
139
|
+
// A schedule publishes nothing yet: the job raises it when it fires.
|
|
140
|
+
case 'scheduled':
|
|
141
|
+
case 'binEmptied':
|
|
142
|
+
case 'domainsAssigned':
|
|
143
|
+
case 'publicAccessChanged':
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The change people subscribed to a page hear of (story 045), if any. Pure. */
|
|
149
|
+
export function notificationFor(event: DocumentEvent):
|
|
150
|
+
| {
|
|
151
|
+
actions: string[]
|
|
152
|
+
key: string
|
|
153
|
+
keyKind: 'document' | 'version'
|
|
154
|
+
withDescendants?: boolean
|
|
155
|
+
}
|
|
156
|
+
| undefined {
|
|
157
|
+
const of = (actions: string[], key: string) => ({ actions, key, keyKind: 'document' as const })
|
|
158
|
+
switch (event.type) {
|
|
159
|
+
case 'created':
|
|
160
|
+
return of(['Umb.Document.Create'], event.key)
|
|
161
|
+
case 'saved':
|
|
162
|
+
return of(['Umb.Document.Update'], event.key)
|
|
163
|
+
case 'createdAndPublished':
|
|
164
|
+
return of(['Umb.Document.Create', 'Umb.Document.Publish'], event.key)
|
|
165
|
+
case 'savedAndPublished':
|
|
166
|
+
return of(['Umb.Document.Update', 'Umb.Document.Publish'], event.key)
|
|
167
|
+
case 'published':
|
|
168
|
+
return of(['Umb.Document.Publish'], event.key)
|
|
169
|
+
case 'branchPublished':
|
|
170
|
+
return { ...of(['Umb.Document.Publish'], event.key), withDescendants: true }
|
|
171
|
+
case 'trashed':
|
|
172
|
+
return of(['Umb.Document.Delete'], event.key)
|
|
173
|
+
case 'moved':
|
|
174
|
+
return of(['Umb.Document.Move'], event.key)
|
|
175
|
+
// Notified on the original, as Umbraco does.
|
|
176
|
+
case 'copied':
|
|
177
|
+
return of(['Umb.Document.Duplicate'], event.key)
|
|
178
|
+
// On the parent whose children were sorted; sorting the top level notifies nobody.
|
|
179
|
+
case 'sorted':
|
|
180
|
+
return event.parentKey ? of(['Umb.Document.Sort'], event.parentKey) : undefined
|
|
181
|
+
case 'rolledBack':
|
|
182
|
+
return { actions: ['Umb.Document.Rollback'], key: event.versionId, keyKind: 'version' }
|
|
183
|
+
case 'restored':
|
|
184
|
+
return of(['Umb.DocumentRecycleBin.Restore'], event.key)
|
|
185
|
+
case 'publicAccessChanged':
|
|
186
|
+
return of(['Umb.Document.PublicAccess'], event.key)
|
|
187
|
+
case 'scheduled':
|
|
188
|
+
case 'unpublished':
|
|
189
|
+
case 'deleted':
|
|
190
|
+
case 'binEmptied':
|
|
191
|
+
case 'domainsAssigned':
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The live events an open backoffice drops its cached copy on, by Umbraco's
|
|
198
|
+
* names: one per page the change touched. Pure.
|
|
199
|
+
*/
|
|
200
|
+
export function liveEventsFor(event: DocumentEvent): Array<{
|
|
201
|
+
eventSource: string
|
|
202
|
+
eventType: 'Created' | 'Updated' | 'Deleted' | 'Trashed'
|
|
203
|
+
key: string
|
|
204
|
+
}> {
|
|
205
|
+
const source = 'Umbraco:CMS:Document'
|
|
206
|
+
const one = (eventType: 'Created' | 'Updated' | 'Deleted' | 'Trashed', key: string) => [
|
|
207
|
+
{ eventSource: source, eventType, key },
|
|
208
|
+
]
|
|
209
|
+
switch (event.type) {
|
|
210
|
+
case 'created':
|
|
211
|
+
case 'createdAndPublished':
|
|
212
|
+
return one('Created', event.key)
|
|
213
|
+
case 'copied':
|
|
214
|
+
return one('Created', event.copyKey)
|
|
215
|
+
case 'trashed':
|
|
216
|
+
return one('Trashed', event.key)
|
|
217
|
+
case 'deleted':
|
|
218
|
+
return one('Deleted', event.key)
|
|
219
|
+
case 'branchPublished':
|
|
220
|
+
return [...new Set([event.key, ...event.published])].map((key) => ({
|
|
221
|
+
eventSource: source,
|
|
222
|
+
eventType: 'Updated' as const,
|
|
223
|
+
key,
|
|
224
|
+
}))
|
|
225
|
+
case 'sorted':
|
|
226
|
+
return event.parentKey ? one('Updated', event.parentKey) : []
|
|
227
|
+
case 'binEmptied':
|
|
228
|
+
return []
|
|
229
|
+
default:
|
|
230
|
+
return one('Updated', event.key)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The whole audit entry an event writes, as every transport writes it: the
|
|
236
|
+
* entry, who acted and from where. An operator who skipped approval is said to
|
|
237
|
+
* have. The item's name is the transport's to look up. Pure.
|
|
238
|
+
*/
|
|
239
|
+
export function auditEntryFor(event: DocumentEvent, actor: Actor) {
|
|
240
|
+
const entry = auditFor(event)
|
|
241
|
+
const skipped = actor.kind === 'operator' && actor.skipApproval === true
|
|
242
|
+
return {
|
|
243
|
+
...entry,
|
|
244
|
+
source: actor.origin.source,
|
|
245
|
+
userKey: actorUserKey(actor),
|
|
246
|
+
userName: actorName(actor),
|
|
247
|
+
ipAddress: actor.origin.ipAddress ?? null,
|
|
248
|
+
forwardedFor: actor.origin.forwardedFor ?? null,
|
|
249
|
+
detail: skipped
|
|
250
|
+
? 'Approval skipped from the command line'
|
|
251
|
+
: (sentence(actorNote(actor)) ?? entry.detail ?? null),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** A note as the audit log writes it: a capital first, as its entries read. */
|
|
256
|
+
const sentence = (note: string | undefined) =>
|
|
257
|
+
note ? note.charAt(0).toUpperCase() + note.slice(1) : undefined
|
package/src/index.ts
ADDED