@crombolo/api-management 0.12.1 → 0.13.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crombolo/api-management",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "Umbraco-compatible Management API handlers for crombolo.",
5
5
  "license": "MIT",
6
6
  "author": "Crombolo",
@@ -28,7 +28,7 @@
28
28
  "access": "public"
29
29
  },
30
30
  "dependencies": {
31
- "@crombolo/core": "0.12.1",
32
- "@crombolo/contracts": "0.12.1"
31
+ "@crombolo/core": "0.13.1",
32
+ "@crombolo/contracts": "0.13.1"
33
33
  }
34
34
  }
@@ -20,7 +20,7 @@ import {
20
20
  ROOT_ACCESS,
21
21
  } from '@crombolo/core'
22
22
  import type { PreviewPort } from '../ports.ts'
23
- import type { DocumentPort, PortFailure, SaveDocument } from '../ports-content.ts'
23
+ import type { ApprovalGatePort, DocumentPort, PortFailure, SaveDocument } from '../ports-content.ts'
24
24
  import type { ManagementApiRouter, RequestContext } from '../router.ts'
25
25
  import { created, paging } from './content-type.ts'
26
26
  import {
@@ -277,7 +277,14 @@ export function registerDocumentHandlers(
277
277
  router: ManagementApiRouter,
278
278
  port: DocumentPort,
279
279
  preview?: PreviewPort,
280
+ approvals?: ApprovalGatePort,
280
281
  ): void {
282
+ /** A page a workflow covers goes through it: the gate's refusal, as a problem. */
283
+ const needsApproval = (title: string, detail: string) =>
284
+ problemResponse(
285
+ problemDetails({ title, status: 400, detail, operationStatus: 'NeedsApproval' }),
286
+ )
287
+ const principalOf = (ctx: RequestContext) => ctx.principal as NonNullable<typeof ctx.principal>
281
288
  const trees: TreeSource = {
282
289
  treeRoot: (paging) => port.treeRoot(paging),
283
290
  treeChildren: (key, paging) => port.treeChildren(key, paging),
@@ -330,6 +337,12 @@ export function registerDocumentHandlers(
330
337
  const body = await readBody(ctx)
331
338
  const key = typeof body.id === 'string' && body.id ? body.id : crypto.randomUUID()
332
339
  const input = toSaveDocument(key, body)
340
+ const refusal = await approvals?.newPageRefusal(
341
+ principalOf(ctx),
342
+ input.parentKey,
343
+ input.contentTypeKey,
344
+ )
345
+ if (refusal) return needsApproval('The document needs approval before it is published', refusal)
333
346
  const result = await port.createAndPublish(
334
347
  input,
335
348
  readCultures(body),
@@ -346,6 +359,10 @@ export function registerDocumentHandlers(
346
359
  const body = await readBody(ctx)
347
360
  const key = ctx.params.id as string
348
361
  const input = toSaveDocument(key, body)
362
+ const refusal =
363
+ (await approvals?.beforeSave(principalOf(ctx), key)) ??
364
+ (await approvals?.publishRefusal(principalOf(ctx), key, 'publish'))
365
+ if (refusal) return needsApproval('The document needs approval before it is published', refusal)
349
366
  const result = await port.updateAndPublish(
350
367
  key,
351
368
  input,
@@ -479,6 +496,8 @@ export function registerDocumentHandlers(
479
496
  existing: DocumentAggregate,
480
497
  body: Record<string, unknown>,
481
498
  ) => {
499
+ const refusal = await approvals?.beforeSave(principalOf(ctx), key)
500
+ if (refusal) return needsApproval('The document is waiting for approval', refusal)
482
501
  const result = await port.update(
483
502
  key,
484
503
  {
@@ -497,6 +516,7 @@ export function registerDocumentHandlers(
497
516
  }),
498
517
  )
499
518
  }
519
+ await approvals?.afterSave(principalOf(ctx), key)
500
520
  ctx.notifications.push({ message: 'Document saved', category: 'Content', type: 'Success' })
501
521
  return new Response(null, { status: 200 })
502
522
  }
@@ -548,6 +568,8 @@ export function registerDocumentHandlers(
548
568
  if (!(await port.byKey(key)))
549
569
  return problemResponse(notFound('The document could not be found'))
550
570
  const body = await readBody(ctx)
571
+ const refusal = await approvals?.publishRefusal(principalOf(ctx), key, 'publish')
572
+ if (refusal) return needsApproval('The document needs approval before it is published', refusal)
551
573
  const schedules = readSchedules(body)
552
574
  if (schedules === 'invalid')
553
575
  return problemResponse(
@@ -592,6 +614,8 @@ export function registerDocumentHandlers(
592
614
  const key = ctx.params.id as string
593
615
  if (!(await port.byKey(key)))
594
616
  return problemResponse(notFound('The document could not be found'))
617
+ const refusal = await approvals?.publishRefusal(principalOf(ctx), key, 'unpublish')
618
+ if (refusal) return needsApproval('Taking the document down needs approval', refusal)
595
619
  const result = await port.unpublish(key, readCultures(await readBody(ctx)))
596
620
  if (!result.ok) {
597
621
  return problemResponse(
@@ -710,6 +734,9 @@ export function registerDocumentHandlers(
710
734
  const branchTasks = new Map<string, { documentKey: string }>()
711
735
  router.handle('PutDocumentByIdPublishWithDescendants', async (ctx) => {
712
736
  const key = ctx.params.id as string
737
+ const refusal = await approvals?.branchRefusal(principalOf(ctx), key)
738
+ if (refusal)
739
+ return needsApproval('Part of this branch needs approval before it is published', refusal)
713
740
  const body = await readBody(ctx)
714
741
  const cultures = Array.isArray(body.cultures) ? (body.cultures as unknown[]).map(String) : []
715
742
  const result = await port.publishBranch(
@@ -591,3 +591,26 @@ export interface TemplateQueryPort {
591
591
  documentTypeAliases(): Promise<string[]>
592
592
  execute(request: TemplateQueryRequest): Promise<{ items: TemplateQueryItem[]; total: number }>
593
593
  }
594
+
595
+ /**
596
+ * Approval before publishing (story 028): asked before a publish, an unpublish
597
+ * or a save, so a page a workflow covers goes through it. Each answer is
598
+ * undefined to go ahead, or what to tell the person.
599
+ */
600
+ export interface ApprovalGatePort {
601
+ publishRefusal(
602
+ principal: Principal,
603
+ key: string,
604
+ action: 'publish' | 'unpublish',
605
+ ): Promise<string | undefined>
606
+ /** A branch publish: the root and every page below it. */
607
+ branchRefusal(principal: Principal, key: string): Promise<string | undefined>
608
+ /** A page created and published in one go, by where it will sit and its type. */
609
+ newPageRefusal(
610
+ principal: Principal,
611
+ parentKey: string | null,
612
+ contentTypeKey: string,
613
+ ): Promise<string | undefined>
614
+ beforeSave(principal: Principal, key: string): Promise<string | undefined>
615
+ afterSave(principal: Principal, key: string): Promise<void>
616
+ }
package/src/ports.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import type { ResponseOf } from '@crombolo/contracts'
11
11
  import type { UploadSettings } from '@crombolo/core'
12
12
  import type {
13
+ ApprovalGatePort,
13
14
  BlueprintPort,
14
15
  ContentTypePort,
15
16
  DataTypePort,
@@ -265,6 +266,8 @@ export interface ManagementApiDeps {
265
266
  dataTypes?: DataTypePort
266
267
  templates?: TemplatePort
267
268
  documents?: DocumentPort
269
+ /** Approval workflows: the gate on publishing and saving a page a workflow covers. */
270
+ approvals?: ApprovalGatePort
268
271
  dictionary?: DictionaryPort
269
272
  userManagement?: UserPort
270
273
  userGroups?: UserGroupPort
package/src/routes.ts CHANGED
@@ -96,7 +96,7 @@ export function createManagementApiRouter(options: ManagementApiOptions = {}): M
96
96
  if (deps.dataTypes) registerDataTypeHandlers(router, deps.dataTypes)
97
97
  if (deps.templates) registerTemplateHandlers(router, deps.templates)
98
98
  if (deps.templateQuery) registerTemplateQueryHandlers(router, deps.templateQuery)
99
- if (deps.documents) registerDocumentHandlers(router, deps.documents, deps.preview)
99
+ if (deps.documents) registerDocumentHandlers(router, deps.documents, deps.preview, deps.approvals)
100
100
  if (deps.blueprints) registerDocumentBlueprintHandlers(router, deps.blueprints)
101
101
  if (deps.temporaryFiles) registerTemporaryFileHandlers(router, deps.temporaryFiles)
102
102
  if (deps.media) registerMediaHandlers(router, deps.media)