bazis 0.130.0 → 0.132.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.
Files changed (76) hide show
  1. package/cli/templates/process.ts +1 -1
  2. package/core/agent/AgentToolExecutor.ts +3 -0
  3. package/core/agent/Tool.contract.ts +1 -1
  4. package/core/agent/index.ts +1 -0
  5. package/core/agent/metadata.ts +9 -0
  6. package/core/agent/semantic.ts +8 -1
  7. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  8. package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
  9. package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
  10. package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
  11. package/core/workflow/contracts/types.ts +37 -1
  12. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +18 -3
  13. package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
  14. package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
  15. package/core/workflow/definitions_module/access/Process.references.ts +1 -1
  16. package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
  17. package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
  18. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +26 -16
  19. package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
  20. package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
  21. package/core/workflow/definitions_module/events/Definitions.events.ts +16 -0
  22. package/core/workflow/definitions_module/http/Processes.controller.ts +72 -25
  23. package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
  24. package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
  25. package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
  26. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +49 -14
  27. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +128 -45
  28. package/core/workflow/generated/deps.ts +15 -5
  29. package/core/workflow/index.ts +17 -3
  30. package/core/workflow/internal/WorkflowEvents.ts +23 -0
  31. package/core/workflow/internal/WorkflowLanguages.ts +35 -0
  32. package/core/workflow/internal/canonicalJson.ts +8 -0
  33. package/core/workflow/internal/http.ts +53 -5
  34. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +0 -2
  35. package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
  36. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  37. package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
  38. package/core/workflow/runtime_module/events/Runtime.events.ts +30 -3
  39. package/core/workflow/runtime_module/http/Events.controller.ts +2 -1
  40. package/core/workflow/runtime_module/http/Instances.controller.ts +32 -21
  41. package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
  42. package/core/workflow/runtime_module/internal/ToolStep.ts +42 -10
  43. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +83 -31
  44. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +2 -4
  45. package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
  46. package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
  47. package/core/workflow/runtime_module/model/WfInstance.model.ts +28 -3
  48. package/core/workflow/runtime_module/model/WfJob.model.ts +17 -1
  49. package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
  50. package/core/workflow/runtime_module/model/WfToken.model.ts +11 -1
  51. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +19 -6
  52. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +20 -17
  53. package/core/workflow/tasks_module/events/Tasks.events.ts +15 -0
  54. package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -18
  55. package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
  56. package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
  57. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +4 -4
  58. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +11 -8
  59. package/library/openapi/codegen.ts +12 -0
  60. package/library/validation/types/ValidatorOptions.ts +12 -0
  61. package/library/workflow/ProcessDocument.ts +59 -23
  62. package/library/workflow/expressionSyntax.ts +243 -0
  63. package/library/workflow/expressions.ts +343 -0
  64. package/library/workflow/formFields.ts +120 -34
  65. package/library/workflow/index.ts +48 -9
  66. package/library/workflow/localized.ts +35 -0
  67. package/library/workflow/masking.ts +58 -0
  68. package/library/workflow/mermaid.ts +18 -8
  69. package/library/workflow/nodeKinds.ts +88 -17
  70. package/library/workflow/palette.ts +108 -0
  71. package/library/workflow/paths.ts +31 -6
  72. package/library/workflow/structure.ts +282 -0
  73. package/library/workflow/upgradeDocument.ts +84 -0
  74. package/library/workflow/validateProcess.ts +130 -48
  75. package/package.json +1 -1
  76. package/library/workflow/conditions.ts +0 -44
@@ -1,4 +1,4 @@
1
- import type { ProcessDocument } from "../../../../library/workflow";
1
+ import type { LocalizedText, ProcessDocument } from "../../../../library/workflow";
2
2
 
3
3
  const CODE_PROCESS = Symbol.for("bazis/core/workflow/code-process");
4
4
 
@@ -6,8 +6,12 @@ const CODE_PROCESS = Symbol.for("bazis/core/workflow/code-process");
6
6
  export interface ProcessDefinition {
7
7
  /** The key it is started by: `^[a-z][a-z0-9_.-]{0,63}$`. */
8
8
  readonly key: string;
9
- readonly title: string;
10
- readonly description?: string | null;
9
+ readonly title: LocalizedText;
10
+ readonly description?: LocalizedText | null;
11
+ /** A path in the catalog: `Sales/Orders`. */
12
+ readonly folder?: string | null;
13
+ /** Whose process it is: an opaque id. */
14
+ readonly owner?: string | null;
11
15
  /** Days to keep completed and cancelled instances; omitted keeps them. */
12
16
  readonly retentionDays?: number | null;
13
17
  readonly document: ProcessDocument;
@@ -27,9 +31,11 @@ export interface CodeProcess extends ProcessDefinition {
27
31
  * export const OrderApprovalProcess = defineProcess({
28
32
  * key: "order-approval",
29
33
  * title: "Order approval",
30
- * document: { schemaVersion: "bazis.workflow/v1", nodes: { … }, flows: { … } },
34
+ * document: { schemaVersion: "bazis.workflow/v2", nodes: { … }, flows: { … } },
31
35
  * });
32
36
  * ```
37
+ *
38
+ * A document of version 1 goes through `upgradeDocument`.
33
39
  */
34
40
  export function defineProcess(definition: ProcessDefinition): CodeProcess {
35
41
  return Object.freeze({ ...definition, [CODE_PROCESS]: true as const });
@@ -0,0 +1,16 @@
1
+ import { createEventToken } from "../../../kernel";
2
+ import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
3
+
4
+ export type ProcessChange = "created" | "updated" | "draftSaved" | "published" | "archived" | "restored";
5
+
6
+ /** A process of the catalog changed, for the application's audit. Published after the commit. */
7
+ export interface WorkflowProcessChanged {
8
+ readonly change: ProcessChange;
9
+ readonly key: string;
10
+ /** The version a `published` change made; otherwise `null`. */
11
+ readonly version: number | null;
12
+ readonly actor: WorkflowActor;
13
+ readonly request: WorkflowRequest | null;
14
+ }
15
+
16
+ export const WORKFLOW_PROCESS_CHANGED = createEventToken<WorkflowProcessChanged>("workflow.process-changed");
@@ -1,9 +1,11 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
- import { can } from "../../../access";
2
+ import { can, type AccessObject } from "../../../access";
3
+ import type { LocalizedText } from "../../../../library/workflow";
3
4
  import { Authorize, Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
4
- import { body, callerOf, optionalInteger, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
5
+ import { buildListDocument } from "../../../../library/jsonapi";
6
+ import { body, callerOf, defaultLanguage, filterOf, listQuery, optionalInteger, optionalString, optionalText, pagingOfList, param, requiredString, withTitleText } from "../../internal/http";
5
7
  import { WorkflowError } from "../../contracts/WorkflowError";
6
- import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
8
+ import { IWorkflowDefinitions, type ProcessSummary } from "../services/IWorkflowDefinitions.service";
7
9
 
8
10
  /** The process catalog over HTTP (WORKFLOW-001 §3); rights are `workflow.processes.*` (WORKFLOW-002 §6.2). */
9
11
  @Controller("workflow")
@@ -13,11 +15,19 @@ export class ProcessesController {
13
15
  @Get("processes")
14
16
  @Authorize(can("workflow.processes.read"))
15
17
  async list(ctx: HttpContext) {
16
- const { access } = await callerOf(ctx);
17
- const archived = ctx.query("archived");
18
+ const { access, language } = await callerOf(ctx);
19
+ const list = listQuery(ctx, { filter: { archived: ["eq"], search: ["eq"], folder: ["eq"], owner: ["eq"] }, sort: ["key", "title", "folder", "createdAt", "updatedAt"] });
20
+ const archived = filterOf(list, "archived");
18
21
  if (archived !== undefined && archived !== "true" && archived !== "false") throw new WorkflowError("invalid_input", "archived must be true or false");
19
- const page = await this.definitions.list({ ...pagingQuery(ctx), access: await access.filter("workflow.processes.read"), archived: archived === undefined ? undefined : archived === "true", search: optionalString(ctx.query("search"), "search") });
20
- return { data: page.items, meta: { total: page.total } };
22
+ const page = await this.definitions.list({
23
+ ...pagingOfList(list),
24
+ access: await access.filter("workflow.processes.read"),
25
+ archived: archived === undefined ? undefined : archived === "true",
26
+ search: filterOf(list, "search"),
27
+ folder: filterOf(list, "folder"),
28
+ owner: filterOf(list, "owner"),
29
+ });
30
+ return buildListDocument(this.localized(page.items, language, ctx), list, page.total, { basePath: ctx.path });
21
31
  }
22
32
 
23
33
  @Post("processes")
@@ -25,22 +35,25 @@ export class ProcessesController {
25
35
  async create(ctx: HttpContext) {
26
36
  const request = await body(ctx);
27
37
  const key = requiredString(request.key, "key", 64);
28
- const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
38
+ const folder = optionalString(request.folder, "folder", 500) ?? null;
39
+ const { actor, request: from, language } = await callerOf(ctx, "workflow.processes.manage", { process: key, folder });
29
40
  const process = await this.definitions.create({
30
41
  key,
31
- title: requiredString(request.title, "title"),
32
- description: optionalString(request.description, "description", 2000) ?? null,
42
+ title: optionalText(request.title) as LocalizedText,
43
+ description: optionalText(request.description) ?? null,
44
+ folder,
45
+ owner: optionalString(request.owner, "owner") ?? null,
33
46
  retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null,
34
- }, actor);
35
- return Created(`${ctx.path}/${process.key}`, process);
47
+ }, actor, { request: from });
48
+ return Created(`${ctx.path}/${process.key}`, withTitleText(process, language, defaultLanguage(ctx)));
36
49
  }
37
50
 
38
51
  @Get("processes/:key")
39
52
  @Authorize(can("workflow.processes.read"))
40
53
  async get(ctx: HttpContext) {
41
54
  const key = param(ctx, "key");
42
- await callerOf(ctx, "workflow.processes.read", { process: key });
43
- return await this.definitions.get(key);
55
+ const { language } = await callerOf(ctx, "workflow.processes.read", await this.objectOf(key));
56
+ return withTitleText(await this.definitions.get(key), language, defaultLanguage(ctx));
44
57
  }
45
58
 
46
59
  @Patch("processes/:key")
@@ -48,14 +61,20 @@ export class ProcessesController {
48
61
  async update(ctx: HttpContext) {
49
62
  const key = param(ctx, "key");
50
63
  const request = await body(ctx);
51
- const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
64
+ const { actor, request: from, language } = await callerOf(ctx, "workflow.processes.manage", await this.objectOf(key));
52
65
  if (request.archived !== undefined && typeof request.archived !== "boolean") throw new WorkflowError("invalid_input", "archived must be a boolean");
53
- return await this.definitions.update(key, {
54
- ...(request.title === undefined ? {} : { title: requiredString(request.title, "title") }),
55
- ...(request.description === undefined ? {} : { description: optionalString(request.description, "description", 2000) ?? null }),
66
+ const folder = optionalString(request.folder, "folder", 500) ?? null;
67
+ // Moving a process needs the right in the folder it goes to as well.
68
+ if (request.folder !== undefined) await callerOf(ctx, "workflow.processes.manage", { process: key, folder });
69
+ const process = await this.definitions.update(key, {
70
+ ...(request.title === undefined ? {} : { title: optionalText(request.title) as LocalizedText }),
71
+ ...(request.description === undefined ? {} : { description: optionalText(request.description) ?? null }),
72
+ ...(request.folder === undefined ? {} : { folder }),
73
+ ...(request.owner === undefined ? {} : { owner: optionalString(request.owner, "owner") ?? null }),
56
74
  ...(request.retentionDays === undefined ? {} : { retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null }),
57
75
  ...(request.archived === undefined ? {} : { archived: request.archived as boolean }),
58
- }, actor);
76
+ }, actor, { request: from });
77
+ return withTitleText(process, language, defaultLanguage(ctx));
59
78
  }
60
79
 
61
80
  @Put("processes/:key/draft")
@@ -63,16 +82,19 @@ export class ProcessesController {
63
82
  async saveDraft(ctx: HttpContext) {
64
83
  const key = param(ctx, "key");
65
84
  const request = await body(ctx);
66
- const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
67
- return await this.definitions.saveDraft(key, request.document, actor);
85
+ const { actor, request: from, language } = await callerOf(ctx, "workflow.processes.manage", await this.objectOf(key));
86
+ const expected = optionalString(request.expected, "expected");
87
+ const expectedAt = expected === undefined ? undefined : new Date(expected);
88
+ if (expectedAt !== undefined && Number.isNaN(expectedAt.getTime())) throw new WorkflowError("invalid_input", "expected must be the draftUpdatedAt that was read");
89
+ return await this.definitions.saveDraft(key, request.document, actor, { request: from, language, ...(expectedAt === undefined ? {} : { expected: expectedAt }) });
68
90
  }
69
91
 
70
92
  @Post("processes/:key/publish")
71
93
  @Authorize(can("workflow.processes.publish"))
72
94
  async publish(ctx: HttpContext) {
73
95
  const key = param(ctx, "key");
74
- const { actor } = await callerOf(ctx, "workflow.processes.publish", { process: key });
75
- const version = await this.definitions.publish(key, actor);
96
+ const { actor, request, language } = await callerOf(ctx, "workflow.processes.publish", await this.objectOf(key));
97
+ const version = await this.definitions.publish(key, actor, { request, language });
76
98
  return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`, version);
77
99
  }
78
100
 
@@ -80,7 +102,7 @@ export class ProcessesController {
80
102
  @Authorize(can("workflow.processes.read"))
81
103
  async versions(ctx: HttpContext) {
82
104
  const key = param(ctx, "key");
83
- await callerOf(ctx, "workflow.processes.read", { process: key });
105
+ await callerOf(ctx, "workflow.processes.read", await this.objectOf(key));
84
106
  return { data: await this.definitions.versions(key) };
85
107
  }
86
108
 
@@ -88,16 +110,41 @@ export class ProcessesController {
88
110
  @Authorize(can("workflow.processes.read"))
89
111
  async version(ctx: HttpContext) {
90
112
  const key = param(ctx, "key");
91
- await callerOf(ctx, "workflow.processes.read", { process: key });
113
+ await callerOf(ctx, "workflow.processes.read", await this.objectOf(key));
92
114
  const version = optionalInteger(param(ctx, "version"), "version");
93
115
  if (version === undefined) throw new WorkflowError("invalid_input", "version must be an integer");
94
116
  return await this.definitions.version(key, version);
95
117
  }
96
118
 
119
+ /** Checks a document without storing it: every finding, as a save and a publication would give them. */
120
+ @Post("validate")
121
+ @Authorize(can("workflow.processes.read"))
122
+ async validate(ctx: HttpContext) {
123
+ const request = await body(ctx);
124
+ const { actor, language } = await callerOf(ctx);
125
+ return { issues: await this.definitions.validate(request.document, { actor, language }) };
126
+ }
127
+
128
+ /** What a designer builds its forms from (WORKFLOW-002 §4.2). */
129
+ @Get("palette")
130
+ @Authorize(can("workflow.processes.read"))
131
+ async palette() {
132
+ return this.definitions.palette();
133
+ }
134
+
97
135
  @Get("tools")
98
136
  @Authorize(can("workflow.processes.read"))
99
137
  async tools() {
100
138
  return { data: this.definitions.tools() };
101
139
  }
140
+
141
+ /** A process as the object of a permission. */
142
+ private async objectOf(key: string): Promise<AccessObject> {
143
+ return { process: key, folder: await this.definitions.folderOf(key) };
144
+ }
145
+
146
+ private localized(items: readonly ProcessSummary[], language: string, ctx: HttpContext) {
147
+ return items.map((item) => withTitleText(item, language, defaultLanguage(ctx)));
148
+ }
102
149
  }
103
150
  contextOnlyController(ProcessesController);
@@ -10,7 +10,7 @@ export class DefinitionsDbContext extends DbContext {
10
10
  /** The tables of the Definitions part: bazis creates and checks them in the application database. */
11
11
  export const definitionsStore = defineOrmOwnedStoreV1({
12
12
  contract: "bazis.orm-owned-store/v1",
13
- storeKey: "bazis.workflow.definitions",
13
+ storeKey: "bazis.workflow.definitions.v2",
14
14
  formatVersion: 1,
15
- ownedScope: { schema: "public", tablePrefix: "bazis_wf_def_" },
15
+ ownedScope: { schema: "public", tablePrefix: "bazis_wf2_def_" },
16
16
  });
@@ -1,11 +1,12 @@
1
1
  import { Column, Entity, Index, UUID } from "../../../orm";
2
+ import type { LocalizedText } from "../../../../library/workflow";
2
3
 
3
4
  /**
4
- * A process in the catalog: the key it is started by, its names, the draft a
5
- * designer edits and the retention of finished instances. Changes; the
5
+ * A process in the catalog: the key it is started by, its card (names, folder,
6
+ * owner), the draft a designer edits and the retention of finished instances. Changes; the
6
7
  * published versions ({@link WfVersion}) do not.
7
8
  */
8
- @Entity({ table: "bazis_wf_def_processes" })
9
+ @Entity({ table: "bazis_wf2_def_processes" })
9
10
  export class WfProcess {
10
11
  @UUID({ version: "v7" })
11
12
  id = "";
@@ -14,11 +15,24 @@ export class WfProcess {
14
15
  @Column({ type: "text" })
15
16
  key = "";
16
17
 
18
+ @Column({ type: "json" })
19
+ title: LocalizedText = "";
20
+
21
+ /** Every text of `title`, the default language first: what a list searches and sorts by. */
17
22
  @Column({ type: "text" })
18
- title = "";
23
+ titleText = "";
24
+
25
+ @Column({ type: "json", nullable: true })
26
+ description: LocalizedText | null = null;
27
+
28
+ /** A path, `Sales/Orders`: there is no folder entity. */
29
+ @Index()
30
+ @Column({ type: "text", nullable: true })
31
+ folder: string | null = null;
19
32
 
33
+ /** Whose process it is: an opaque id of the application. */
20
34
  @Column({ type: "text", nullable: true })
21
- description: string | null = null;
35
+ owner: string | null = null;
22
36
 
23
37
  /** The draft document; kept with validation errors too. */
24
38
  @Column({ type: "json", nullable: true })
@@ -4,7 +4,7 @@ import { WfProcess } from "./WfProcess.model";
4
4
  /** A published version of a process: an immutable snapshot of its document. */
5
5
  @ForeignKey(() => WfProcess, { properties: ["processId"], onDelete: "cascade" })
6
6
  @Index(["processId", "version"], { unique: true })
7
- @Entity({ table: "bazis_wf_def_versions" })
7
+ @Entity({ table: "bazis_wf2_def_versions" })
8
8
  export class WfVersion {
9
9
  @UUID({ version: "v7" })
10
10
  id = "";
@@ -1,13 +1,17 @@
1
1
  import type { AccessFilter } from "../../../access";
2
2
  import type { AgentToolContract } from "../../../agent";
3
- import type { ProcessDocument, ProcessIssue } from "../../../../library/workflow";
4
- import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
3
+ import type { LocalizedText, ProcessDocument, ProcessIssue, ProcessPalette } from "../../../../library/workflow";
4
+ import type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging } from "../../contracts/types";
5
5
  import type { OwnedCodeProcess } from "../code/codeProcesses";
6
6
 
7
7
  export interface ProcessSummary {
8
8
  readonly key: string;
9
- readonly title: string;
10
- readonly description: string | null;
9
+ readonly title: LocalizedText;
10
+ readonly description: LocalizedText | null;
11
+ /** A path, `Sales/Orders`. */
12
+ readonly folder: string | null;
13
+ /** Whose process it is: an opaque id. */
14
+ readonly owner: string | null;
11
15
  readonly latestVersion: number | null;
12
16
  readonly retentionDays: number | null;
13
17
  readonly archived: boolean;
@@ -41,14 +45,18 @@ export interface VersionDetails extends VersionSummary {
41
45
 
42
46
  export interface CreateProcess {
43
47
  readonly key: string;
44
- readonly title: string;
45
- readonly description?: string | null;
48
+ readonly title: LocalizedText;
49
+ readonly description?: LocalizedText | null;
50
+ readonly folder?: string | null;
51
+ readonly owner?: string | null;
46
52
  readonly retentionDays?: number | null;
47
53
  }
48
54
 
49
55
  export interface UpdateProcess {
50
- readonly title?: string;
51
- readonly description?: string | null;
56
+ readonly title?: LocalizedText;
57
+ readonly description?: LocalizedText | null;
58
+ readonly folder?: string | null;
59
+ readonly owner?: string | null;
52
60
  readonly retentionDays?: number | null;
53
61
  readonly archived?: boolean;
54
62
  }
@@ -57,8 +65,29 @@ export interface ProcessListQuery extends WorkflowPaging {
57
65
  /** Which processes the caller may see, from `IAccess`; without it the calling code has decided. */
58
66
  readonly access?: AccessFilter;
59
67
  readonly archived?: boolean;
60
- /** Part of the key or title, case-insensitive. */
68
+ /** Part of the key or a title in any language, case-insensitive. */
61
69
  readonly search?: string;
70
+ /** This folder and those below it. */
71
+ readonly folder?: string;
72
+ readonly owner?: string;
73
+ }
74
+
75
+ /** Who asks for a validation: it goes to the application's `WorkflowExtensions`. */
76
+ export interface ValidateOptions {
77
+ readonly language?: string;
78
+ readonly actor?: WorkflowActor;
79
+ }
80
+
81
+ export interface SaveDraftOptions extends WorkflowAuditOptions, Pick<ValidateOptions, "language"> {
82
+ /** The `draftUpdatedAt` the caller read: another save since then is `conflict`. Without it the last save wins. */
83
+ readonly expected?: Date;
84
+ }
85
+
86
+ export interface SavedDraft {
87
+ /** Every validation finding: a draft is stored with errors too. */
88
+ readonly issues: readonly ProcessIssue[];
89
+ /** Goes to `expected` of the next save. */
90
+ readonly draftUpdatedAt: Date;
62
91
  }
63
92
 
64
93
  /**
@@ -69,16 +98,22 @@ export interface ProcessListQuery extends WorkflowPaging {
69
98
  export abstract class IWorkflowDefinitions {
70
99
  abstract list(query?: ProcessListQuery): Promise<WorkflowPage<ProcessSummary>>;
71
100
  abstract get(key: string): Promise<ProcessDetails>;
72
- abstract create(process: CreateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
101
+ /** The folder of a process, for a check of rights; `null` also for a process that does not exist. */
102
+ abstract folderOf(key: string): Promise<string | null>;
103
+ /** Every finding of a document, with the application's `extensions` checks; nothing is stored. */
104
+ abstract validate(document: unknown, options?: ValidateOptions): Promise<readonly ProcessIssue[]>;
105
+ /** What a designer builds its forms from: node kinds, functions, paths and Tools. */
106
+ abstract palette(): ProcessPalette;
107
+ abstract create(process: CreateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
73
108
  /** A process declared in code takes only `archived`; other fields give `conflict`. */
74
- abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
75
- /** Stores the draft even with errors and returns every validation finding; `conflict` for a process declared in code. */
76
- abstract saveDraft(key: string, document: unknown, actor: WorkflowActor): Promise<{ readonly issues: readonly ProcessIssue[] }>;
109
+ abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
110
+ /** Stores the draft even with errors; `conflict` for a process declared in code and when `expected` is out of date. */
111
+ abstract saveDraft(key: string, document: unknown, actor: WorkflowActor, options?: SaveDraftOptions): Promise<SavedDraft>;
77
112
  /**
78
113
  * Validates the draft and stores it as the next version; the same document
79
114
  * again returns the latest version. `conflict` for a process declared in code.
80
115
  */
81
- abstract publish(key: string, actor: WorkflowActor): Promise<VersionDetails>;
116
+ abstract publish(key: string, actor: WorkflowActor, options?: WorkflowAuditOptions & Pick<ValidateOptions, "language">): Promise<VersionDetails>;
82
117
  abstract versions(key: string): Promise<readonly VersionSummary[]>;
83
118
  abstract version(key: string, version: number): Promise<VersionDetails>;
84
119
  /** The latest published version of a process that is not archived. */