bazis 0.136.0 → 0.138.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 (132) hide show
  1. package/README.md +7 -4
  2. package/cli/generateProject.ts +11 -18
  3. package/cli/main.ts +9 -1
  4. package/cli/parseCli.ts +9 -0
  5. package/cli/templates/module.ts +16 -19
  6. package/cli/templates/passport.ts +2 -2
  7. package/cli/upgrade/markColumns.ts +151 -0
  8. package/cli/upgrade/runUpgrade.ts +57 -0
  9. package/core/access/Access.module.ts +1 -1
  10. package/core/access/catalog/moduleFields.ts +41 -4
  11. package/core/access/catalog/permissions.ts +10 -0
  12. package/core/access/check/Access.ts +5 -5
  13. package/core/access/check/can.ts +2 -1
  14. package/core/access/index.ts +11 -1
  15. package/core/access/roles/roles.ts +4 -2
  16. package/core/access/testing/index.ts +26 -4
  17. package/core/agent/ToolRunner.ts +7 -1
  18. package/core/agent/defineTool.ts +109 -0
  19. package/core/agent/index.ts +8 -3
  20. package/core/agent/moduleContributions-v1.ts +3 -10
  21. package/core/agent/session/entities.ts +45 -45
  22. package/core/app/index.ts +1 -1
  23. package/core/app/messages.ts +3 -3
  24. package/core/app/runApp.ts +90 -26
  25. package/core/cache/ICache.ts +1 -1
  26. package/core/cache/README.md +4 -2
  27. package/core/cache/SPEC.md +16 -9
  28. package/core/cache/http/missingCacheModuleCheck.ts +5 -4
  29. package/core/cache/internal/buildCacheProviders.ts +4 -5
  30. package/core/cache/providers/cachedProviders.ts +1 -1
  31. package/core/di/index.ts +0 -4
  32. package/core/di/lowLevel.ts +12 -0
  33. package/core/di/module/index.ts +0 -3
  34. package/core/di/module/shortcuts.ts +31 -86
  35. package/core/generatedRuntime.ts +11 -0
  36. package/core/grpc/Grpc.module.ts +2 -1
  37. package/core/grpc/grpcClients.ts +2 -1
  38. package/core/http/Authorization/metadata.ts +1 -1
  39. package/core/http/Decorators/metadata.ts +3 -0
  40. package/core/http/Decorators/routes.ts +12 -0
  41. package/core/http/Errors/HttpError.ts +16 -2
  42. package/core/http/Http.config.ts +22 -0
  43. package/core/http/HttpContext/HttpContext.ts +1 -1
  44. package/core/http/HttpServer.ts +29 -9
  45. package/core/http/Middleware/errorHandler.ts +16 -1
  46. package/core/http/OpenApi/openApiDocument.ts +44 -5
  47. package/core/http/Results/normalize.ts +38 -1
  48. package/core/http/Results/results.ts +5 -2
  49. package/core/http/Routing/RouterBuilder.ts +27 -2
  50. package/core/http/httpModule.ts +10 -8
  51. package/core/http/index.ts +4 -0
  52. package/core/http/middlewareTokens.ts +12 -0
  53. package/core/http/options.ts +5 -1
  54. package/core/http-client/httpClients.ts +2 -1
  55. package/core/infra/Infra.ts +3 -3
  56. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  57. package/core/infra/connectors/codex/contracts.ts +3 -3
  58. package/core/infra/connectors/llm.ts +2 -1
  59. package/core/infra/connectors/postgres.ts +4 -2
  60. package/core/infra/connectors/redis.ts +2 -1
  61. package/core/infra/index.ts +1 -1
  62. package/core/kernel/KernelBuilder.ts +2 -1
  63. package/core/kernel/config/ConfigRegistry.ts +2 -1
  64. package/core/kernel/config/addConfigOptions.ts +2 -1
  65. package/core/kernel/config/defineConfig.ts +56 -20
  66. package/core/kernel/events/EventBus.ts +1 -0
  67. package/core/kernel/events/eventSubscribers.ts +1 -1
  68. package/core/kernel/events/onEvent.ts +2 -1
  69. package/core/kernel/index.ts +3 -0
  70. package/core/orm/buildOrmProviders.ts +4 -2
  71. package/core/orm/contextEntities.ts +36 -0
  72. package/core/orm/databaseConnector.ts +9 -3
  73. package/core/orm/databaseHttpError.ts +21 -0
  74. package/core/orm/encryptedWith.ts +1 -1
  75. package/core/orm/index.ts +12 -1
  76. package/core/orm/ormModule.ts +19 -8
  77. package/core/orm/ownedStoreContributions.ts +2 -1
  78. package/core/orm/registerRepositories.ts +2 -1
  79. package/core/scripts/di-generate.ts +113 -12
  80. package/core/testing/index.ts +51 -11
  81. package/core/tools/StandardTools.module.ts +1 -1
  82. package/core/tools/contracts/Tools.config.ts +13 -7
  83. package/core/tools/generated/deps.ts +3 -3
  84. package/core/tools/http/HttpRequest.tool.ts +1 -2
  85. package/core/tools/http/publicAddress.ts +2 -2
  86. package/core/tools/index.ts +1 -1
  87. package/core/tools/orm/OrmEntity.references.ts +7 -8
  88. package/core/tools/orm/ormEntityTools.ts +54 -28
  89. package/core/websocket/DESIGN.md +5 -3
  90. package/core/websocket/middleware.ts +1 -1
  91. package/core/websocket/websocketModule.ts +3 -3
  92. package/core/workflow/contracts/Workflow.config.ts +5 -5
  93. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +3 -4
  94. package/core/workflow/definitions_module/code/defineProcess.ts +213 -11
  95. package/core/workflow/definitions_module/code/inspectProcesses.ts +12 -2
  96. package/core/workflow/definitions_module/http/Processes.controller.ts +2 -2
  97. package/core/workflow/definitions_module/internal/PublishedVersions.ts +39 -0
  98. package/core/workflow/definitions_module/model/WfProcess.model.ts +24 -11
  99. package/core/workflow/definitions_module/model/WfVersion.model.ts +7 -1
  100. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +4 -1
  101. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +17 -7
  102. package/core/workflow/generated/deps.ts +2 -1
  103. package/core/workflow/index.ts +23 -1
  104. package/core/workflow/internal/WorkflowLanguages.ts +1 -1
  105. package/core/workflow/internal/http.ts +2 -2
  106. package/core/workflow/runtime_module/background/WorkflowWorker.ts +6 -2
  107. package/core/workflow/runtime_module/http/Instances.controller.ts +1 -1
  108. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +43 -1
  109. package/core/workflow/runtime_module/model/WfHistory.model.ts +13 -6
  110. package/core/workflow/runtime_module/model/WfInstance.model.ts +30 -10
  111. package/core/workflow/runtime_module/model/WfJob.model.ts +22 -10
  112. package/core/workflow/runtime_module/model/WfSubscription.model.ts +8 -3
  113. package/core/workflow/runtime_module/model/WfToken.model.ts +16 -7
  114. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +10 -6
  115. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +5 -2
  116. package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
  117. package/core/workflow/testing/index.ts +46 -4
  118. package/index.ts +4 -3
  119. package/library/openapi/codegen.ts +2 -2
  120. package/library/orm/DbContext.ts +37 -1
  121. package/library/orm/Metadata/ModelBuilder.ts +23 -7
  122. package/library/orm/Metadata/decorators.ts +23 -10
  123. package/library/orm/Metadata/types.ts +2 -0
  124. package/library/orm/Transactions/TransactionScopeCoordinator.ts +3 -3
  125. package/library/orm/errors.ts +39 -1
  126. package/library/orm/index.ts +1 -0
  127. package/library/terminal/messages.ts +6 -6
  128. package/library/validation/README.md +1 -1
  129. package/library/validation/SPEC.md +1 -1
  130. package/library/workflow/index.ts +1 -0
  131. package/library/workflow/nodeConfigs.ts +130 -0
  132. package/package.json +2 -1
@@ -30,7 +30,8 @@ import { Module, singleton } from "@/core/di";
30
30
  @Module({ gateways: [ChatGateway], providers: [singleton(ChatHistory)] })
31
31
  export class ChatModule {}
32
32
 
33
- await runApp(AppModule, { http: { port: 3000 }, websocket: { requireAuth: true, authenticator } });
33
+ @App({ modules: [AppModule], http: {}, websocket: { requireAuth: true, authenticator } })
34
+ export class Application {}
34
35
  ```
35
36
 
36
37
  Without `runApp`, `websocketModule({ imports: [ChatModule], ... })` next to
@@ -101,7 +102,7 @@ default, `replayDelivery: "transport"`.
101
102
  ### Client-confirmed replay
102
103
 
103
104
  Opt in with `?replay=client-ack` on each connection, or require it for all clients
104
- with `runApp({ websocket: { replayDelivery: "client-ack" } })`. The adapter
105
+ with `@App({ websocket: { replayDelivery: "client-ack" } })`. The adapter
105
106
  must support CAS. A saved client-ack session cannot reconnect in transport mode;
106
107
  such an upgrade returns 409. Invalid/duplicate replay query values return 400.
107
108
 
@@ -150,7 +151,8 @@ Pass an existing native Bun RedisClient from the host infrastructure:
150
151
  const adapter = new RedisWebSocketAdapter(hostRedisClient, {
151
152
  keyPrefix: "my-app:ws", // same on participating nodes; distinct per application
152
153
  });
153
- await runApp(AppModule, { http: {}, websocket: { adapter, replayDelivery: "client-ack" } });
154
+ @App({ modules: [AppModule], http: {}, websocket: { adapter, replayDelivery: "client-ack" } })
155
+ export class Application {}
154
156
  ```
155
157
 
156
158
  The adapter owns only a duplicate connection for pub/sub. Closing the adapter
@@ -26,7 +26,7 @@ export interface WsAuthenticationContext {
26
26
  readonly request?: Request;
27
27
  /**
28
28
  * The application's services, for a token check that needs them
29
- * (`services.resolve(AuthTokens)`). Set by `runApp({ websocket })`.
29
+ * (`services.resolve(AuthTokens)`). Set by `@App({ websocket })`.
30
30
  */
31
31
  readonly services?: ServiceResolver;
32
32
  }
@@ -1,5 +1,4 @@
1
1
  import {
2
- DI,
3
2
  collectModuleGateways,
4
3
  markGlobal,
5
4
  Module,
@@ -8,6 +7,7 @@ import {
8
7
  type ProviderDefinition,
9
8
  type ServiceResolver,
10
9
  } from "../di";
10
+ import { DI } from "../di/lowLevel";
11
11
  import { WEBSOCKET_UPGRADE } from "../http";
12
12
  import { LOGGER } from "../kernel";
13
13
  import { WebSocketService } from "./WebSocketService";
@@ -54,13 +54,13 @@ export interface WebSocketModuleConfig {
54
54
  readonly exposeHandlerErrors?: boolean;
55
55
  }
56
56
 
57
- /** Server-wide WebSocket settings of `runApp({ websocket })`; gateways come from the modules. */
57
+ /** Server-wide WebSocket settings of `@App({ websocket })`; gateways come from the modules. */
58
58
  export type WebSocketOptions = Omit<WebSocketModuleConfig, "imports">;
59
59
 
60
60
  /**
61
61
  * Serves the gateways declared by the modules of `imports` on the shared HTTP
62
62
  * listener. `runApp` installs it next to the HTTP module whenever a module
63
- * declares `gateways`; `runApp({ websocket })` passes the settings.
63
+ * declares `gateways`; `@App({ websocket })` passes the settings.
64
64
  *
65
65
  * ```ts
66
66
  * @Module({ gateways: [ChatGateway], providers: [singleton(ChatHistory)] })
@@ -1,15 +1,15 @@
1
- import { defineConfig } from "../../kernel";
1
+ import { configList, defineConfig } from "../../kernel";
2
2
 
3
3
  /**
4
4
  * Settings of the workflow pack, from the application configuration:
5
5
  * `BAZIS_WORKFLOW__WORKER__ENABLED=false` keeps an instance from running jobs,
6
6
  * for example a web-only one (WORKFLOW-001 §4). `language` is the default
7
- * language of texts, `languages` the comma-separated list a request may ask for
7
+ * language of texts, `languages` the list a request may ask for (comma-separated in the environment)
8
8
  * with `Accept-Language` (WORKFLOW-002 §6).
9
9
  */
10
10
  export interface WorkflowSettings {
11
11
  readonly language: string;
12
- readonly languages: string;
12
+ readonly languages: readonly string[];
13
13
  readonly "worker.enabled": boolean;
14
14
  readonly "worker.intervalMs": number;
15
15
  readonly "worker.batchSize": number;
@@ -23,7 +23,7 @@ const LANGUAGE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
23
23
  export const workflowConfig = defineConfig<WorkflowSettings>("workflow", {
24
24
  default: {
25
25
  language: "en",
26
- languages: "en",
26
+ languages: configList(["en"]),
27
27
  "worker.enabled": true,
28
28
  "worker.intervalMs": 1000,
29
29
  "worker.batchSize": 10,
@@ -33,7 +33,7 @@ export const workflowConfig = defineConfig<WorkflowSettings>("workflow", {
33
33
  },
34
34
  validate: {
35
35
  language: (value) => (LANGUAGE.test(value) ? undefined : "must be a language: en, ru, pt-BR"),
36
- languages: (value) => (value.split(",").every((item) => LANGUAGE.test(item.trim())) ? undefined : "must be a comma-separated list of languages"),
36
+ languages: (value) => (value.every((item) => LANGUAGE.test(item)) ? undefined : "must be a list of languages: en, ru, pt-BR"),
37
37
  "worker.intervalMs": (value) => positive(value, 3_600_000),
38
38
  "worker.batchSize": (value) => positive(value, 1000),
39
39
  "retention.intervalMs": (value) => positive(value, 86_400_000),
@@ -12,11 +12,11 @@ import { definitionsPermissions } from "./access/Definitions.permissions";
12
12
  import { FolderReferences } from "./access/Folder.references";
13
13
  import { ProcessReferences } from "./access/Process.references";
14
14
  import { ProcessFolderLookup } from "./access/ProcessFolder.lookup";
15
- import { ProcessPartitionLookup } from "./access/ProcessPartition.lookup";
16
15
  import { CatalogImport } from "./code/CatalogImport";
17
16
  import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
18
17
  import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
19
18
  import { ProcessesController } from "./http/Processes.controller";
19
+ import { PublishedVersions } from "./internal/PublishedVersions";
20
20
  import { DefinitionsDbContext, definitionsStore } from "./model/Definitions.dbContext";
21
21
  import { WfProcess } from "./model/WfProcess.model";
22
22
  import { WfVersion } from "./model/WfVersion.model";
@@ -39,6 +39,7 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
39
39
  singleton(WorkflowClock, SystemWorkflowClock),
40
40
  singleton(WorkflowTools),
41
41
  singleton(CodeProcessCatalog),
42
+ singleton(PublishedVersions),
42
43
  singleton(WorkflowLanguages),
43
44
  singleton(WorkflowExtensions, DefaultWorkflowExtensions),
44
45
  singleton(WorkflowPartitions, DefaultWorkflowPartitions),
@@ -46,10 +47,8 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
46
47
  scoped(WorkflowReferences),
47
48
  scoped(CatalogImport),
48
49
  scoped(IWorkflowDefinitions, WorkflowDefinitions),
49
- scoped(ProcessReferences),
50
- scoped(FolderReferences),
50
+ // Shared with the Runtime and Tasks parts: owned and exported here.
51
51
  scoped(ProcessFolderLookup),
52
- scoped(ProcessPartitionLookup),
53
52
  singleton(HOSTED_SERVICE, CodeProcessPublisher),
54
53
  ],
55
54
  controllers: [ProcessesController],
@@ -1,9 +1,134 @@
1
- import type { LocalizedText, ProcessDocument } from "../../../../library/workflow";
1
+ import { toolNameOf, type DefinedTool } from "../../../agent";
2
+ import type {
3
+ Expr,
4
+ LocalizedText,
5
+ NodeConfigs,
6
+ NodeKind,
7
+ ProcessDiagram,
8
+ ProcessDocument,
9
+ ProcessFlow,
10
+ ProcessNode,
11
+ ProcessNodeOf,
12
+ ProcessSettings,
13
+ TypedProcessFlow,
14
+ TypedProcessNode,
15
+ VariableSpec,
16
+ } from "../../../../library/workflow";
2
17
 
3
18
  const CODE_PROCESS = Symbol.for("bazis/core/workflow/code-process");
19
+ declare const PROCESS_TYPES: unique symbol;
20
+
21
+ /** A class with `@Tool`: a process names it instead of writing its name. */
22
+ export type ToolClass = abstract new (...args: never[]) => { execute(...args: never[]): unknown };
23
+ /** A Tool of a step: by reference (a class with `@Tool`, a Tool made by `defineTool`), or by name. */
24
+ export type ToolRef = string | ToolClass | DefinedTool;
25
+ /** A process another one calls or hands its failures to: by reference, or by key. */
26
+ export type ProcessRef = string | CodeProcess;
27
+
28
+ /** The node configs a process in code may write: a Tool and a process by reference. */
29
+ export interface CodeNodeConfigs extends Omit<NodeConfigs, "serviceTask" | "callProcess"> {
30
+ readonly serviceTask: Omit<NodeConfigs["serviceTask"], "tool"> & { readonly tool: ToolRef };
31
+ readonly callProcess: Omit<NodeConfigs["callProcess"], "process"> & { readonly process: ProcessRef };
32
+ }
33
+
34
+ /** A node as code writes it: the `config` of its kind, the ids of the document's own nodes. */
35
+ export type CodeProcessNode<Id extends string = string> = TypedProcessNode<Id, CodeNodeConfigs>;
36
+
37
+ /** The settings of a process in code: the error handler by reference. */
38
+ export interface CodeProcessSettings extends Omit<ProcessSettings, "errorHandler"> {
39
+ /** The process to start when an instance fails or times out. */
40
+ readonly errorHandler?: ProcessRef;
41
+ }
42
+
43
+ type FieldValue<F> =
44
+ F extends { readonly type: "enum"; readonly values: readonly (infer Value)[] } ? Value
45
+ : F extends { readonly type: "string" | "text" | "date" | "datetime" | "ref" | "enum" } ? string
46
+ : F extends { readonly type: "number" | "integer" } ? number
47
+ : F extends { readonly type: "boolean" } ? boolean
48
+ : F extends { readonly type: "list" } ? readonly unknown[]
49
+ : F extends { readonly type: "object" } ? Readonly<Record<string, unknown>>
50
+ : unknown;
51
+
52
+ type Flat<T> = { readonly [K in keyof T]: T[K] };
53
+
54
+ /** What a start of the process takes: its `in` variables; the `required` ones must be there. */
55
+ export type ProcessInput<V extends readonly VariableSpec[]> = string extends V[number]["name"]
56
+ ? Readonly<Record<string, unknown>>
57
+ : Flat<
58
+ & { [F in V[number] as F extends { readonly direction: "in"; readonly required: true } ? F["name"] : never]: FieldValue<F> }
59
+ & { [F in V[number] as F extends { readonly direction: "in" } ? (F extends { readonly required: true } ? never : F["name"]) : never]?: FieldValue<F> }
60
+ >;
61
+
62
+ /** What a finished process gives back: its `out` variables. */
63
+ export type ProcessOutput<V extends readonly VariableSpec[]> = string extends V[number]["name"]
64
+ ? Readonly<Record<string, unknown>>
65
+ : Flat<{ [F in V[number] as F extends { readonly direction: "out" } ? F["name"] : never]: FieldValue<F> | null }>;
66
+
67
+ /** What a Tool takes: the parameter of its `execute`. `unknown` when it does not say. */
68
+ type ToolInputOf<T> =
69
+ T extends DefinedTool<infer Input, unknown> ? Input
70
+ : T extends abstract new (...args: never[]) => { execute(input: infer Input, ...rest: never[]): unknown } ? Input
71
+ : unknown;
72
+
73
+ /** `config.input` of a step against what its Tool or process takes: every required parameter, no unknown one. */
74
+ type InputOf<Target, Given> = unknown extends Target ? unknown
75
+ : Target extends object
76
+ ? string extends keyof Target ? unknown
77
+ : { readonly config: ({} extends Target ? { readonly input?: MappingOf<Target, Given> } : { readonly input: MappingOf<Target, Given> }) }
78
+ : unknown;
79
+
80
+ type MappingOf<Target, Given> = { readonly [P in keyof Target]: Expr } & { readonly [P in Exclude<keyof Given, keyof Target>]?: never };
81
+
82
+ type CheckedInput<Node> =
83
+ Node extends { readonly kind: "serviceTask"; readonly config: { readonly tool: infer Tool; readonly input?: infer Given } } ? InputOf<ToolInputOf<Tool>, Given>
84
+ : Node extends { readonly kind: "callProcess"; readonly config: { readonly process: infer Called; readonly input?: infer Given } }
85
+ ? (Called extends CodeProcess<infer Input, unknown> ? InputOf<Input, Given> : unknown)
86
+ : unknown;
87
+
88
+ /** The nodes of a document, as `defineProcess` reads their type. */
89
+ export type CodeProcessNodes = Readonly<Record<string, ProcessNode | CodeProcessNode>>;
90
+
91
+ type KeysOf<T> = T extends unknown ? keyof T : never;
92
+ type ValueOf<T, K extends PropertyKey> = T extends unknown ? (K extends keyof T ? T[K] : never) : never;
93
+ /** Whether a value of this type is not a closed object: a primitive, a list of anything, a map by any key. */
94
+ type IsOpen<T> = T extends unknown ? (T extends readonly unknown[] ? false : T extends object ? (string extends keyof T ? true : false) : true) : never;
95
+
96
+ /**
97
+ * `Given` with every key the allowed type does not know turned into `never`:
98
+ * a misspelled optional field is an error, not a field nobody reads.
99
+ */
100
+ type Exact<Given, Allowed> =
101
+ true extends IsOpen<NonNullable<Allowed>> ? unknown
102
+ : Given extends readonly (infer Item)[] ? (NonNullable<Allowed> extends readonly (infer AllowedItem)[] ? readonly Exact<Item, AllowedItem>[] : unknown)
103
+ : Given extends object ? { readonly [P in keyof Given]: P extends KeysOf<NonNullable<Allowed>> ? Exact<Given[P], ValueOf<NonNullable<Allowed>, P>> : never }
104
+ : unknown;
105
+
106
+ type ExactNode<Node, Id extends string> = Node extends { readonly kind: infer Kind extends NodeKind } ? Exact<Node, ProcessNodeOf<Kind, Id, CodeNodeConfigs>> : unknown;
107
+
108
+ /** What the nodes written as a literal must also be: of their kind, with known fields, with the input their Tool takes. */
109
+ type CheckedNodes<N> = string extends keyof N ? unknown : { readonly [Id in keyof N]: CodeProcessNode<keyof N & string> & CheckedInput<N[Id]> & ExactNode<N[Id], keyof N & string> };
110
+
111
+ /**
112
+ * The document of a process in code. Written as a literal, it is checked while
113
+ * you type: the `config` of each node kind, the nodes that flows, `attachedTo`
114
+ * and `parent` name, the input of a Tool or a called process given by
115
+ * reference. A document that already exists as data (exported from a designer,
116
+ * built by a function, upgraded from version 1) is taken as it is.
117
+ */
118
+ export interface CodeProcessDocument<V extends readonly VariableSpec[], N extends CodeProcessNodes> {
119
+ readonly schemaVersion: ProcessDocument["schemaVersion"];
120
+ /** `in` — the input of a start, `local` — inside the process, `out` — what it gives back. */
121
+ readonly variables?: V;
122
+ readonly settings?: CodeProcessSettings;
123
+ readonly nodes: N & CheckedNodes<N>;
124
+ readonly flows: Readonly<Record<string, string extends keyof N ? ProcessFlow : TypedProcessFlow<keyof N & string>>>;
125
+ readonly diagram?: ProcessDiagram;
126
+ /** The application's data by namespace; the engine does not read it. */
127
+ readonly extensions?: Readonly<Record<string, unknown>>;
128
+ }
4
129
 
5
130
  /** What `defineProcess` takes: the catalog fields of the process and its document. */
6
- export interface ProcessDefinition {
131
+ export interface ProcessDefinition<V extends readonly VariableSpec[] = readonly VariableSpec[], N extends CodeProcessNodes = CodeProcessNodes> {
7
132
  /** The key it is started by: `^[a-z][a-z0-9_.-]{0,63}$`. */
8
133
  readonly key: string;
9
134
  readonly title: LocalizedText;
@@ -14,12 +139,27 @@ export interface ProcessDefinition {
14
139
  readonly owner?: string | null;
15
140
  /** Days to keep completed and cancelled instances; omitted keeps them. */
16
141
  readonly retentionDays?: number | null;
17
- readonly document: ProcessDocument;
142
+ readonly document: CodeProcessDocument<V, N>;
18
143
  }
19
144
 
20
- /** A process described in code: listed in `@Module({ processes })` and published at start (WORKFLOW-001 §14). */
21
- export interface CodeProcess extends ProcessDefinition {
145
+ /**
146
+ * A process described in code: listed in `@Module({ processes })` and published
147
+ * at start (WORKFLOW-001 §14). `Input` and `Output` come from its variables:
148
+ * `runtime.start(process, { input })` and a `callProcess` step are checked by them.
149
+ * Plain `CodeProcess` is any process.
150
+ */
151
+ export interface CodeProcess<Input = never, Output = unknown> {
152
+ readonly key: string;
153
+ readonly title: LocalizedText;
154
+ readonly description?: LocalizedText | null;
155
+ readonly folder?: string | null;
156
+ readonly owner?: string | null;
157
+ readonly retentionDays?: number | null;
158
+ /** The document as it is stored: Tools by name, processes by key. */
159
+ readonly document: ProcessDocument;
22
160
  readonly [CODE_PROCESS]: true;
161
+ /** Only for the types. */
162
+ readonly [PROCESS_TYPES]?: { readonly input: (input: Input) => void; readonly output: Output };
23
163
  }
24
164
 
25
165
  /**
@@ -27,20 +167,82 @@ export interface CodeProcess extends ProcessDefinition {
27
167
  * description: checks happen when the container is built and at start.
28
168
  *
29
169
  * ```ts
30
- * // orders_module/processes/OrderApproval.process.ts
31
- * export const OrderApprovalProcess = defineProcess({
170
+ * // order/process/OrderApproval.process.ts
171
+ * export const orderApproval = defineProcess({
32
172
  * key: "order-approval",
33
173
  * title: "Order approval",
34
- * document: { schemaVersion: "bazis.workflow/v2", nodes: { … }, flows: { … } },
174
+ * document: {
175
+ * schemaVersion: "bazis.workflow/v2",
176
+ * variables: [{ name: "orderId", type: "string", direction: "in", required: true }],
177
+ * nodes: {
178
+ * Start: { kind: "manualStart", title: "Start" },
179
+ * Approve: { kind: "serviceTask", title: "Approve", config: { tool: SetOrderStatusTool, input: { orderId: "$input.orderId", status: "approved" } } },
180
+ * End: { kind: "end", title: "End" },
181
+ * },
182
+ * flows: { a: { source: "Start", target: "Approve" }, b: { source: "Approve", target: "End" } },
183
+ * },
35
184
  * });
185
+ *
186
+ * await runtime.start(orderApproval, { actor, input: { orderId: "o-1" } });
36
187
  * ```
37
188
  *
38
- * A document of version 1 goes through `upgradeDocument`.
189
+ * A Tool is named by its class (or by a Tool made by `defineTool`:
190
+ * `orderTools.find`), a called process and the error handler by their
191
+ * `defineProcess` result; a name or a key in a string is still taken, for what
192
+ * the module does not see. A document of version 1 goes through `upgradeDocument`.
39
193
  */
40
- export function defineProcess(definition: ProcessDefinition): CodeProcess {
41
- return Object.freeze({ ...definition, [CODE_PROCESS]: true as const });
194
+ export function defineProcess<const V extends readonly VariableSpec[] = readonly VariableSpec[], const N extends CodeProcessNodes = Readonly<Record<string, ProcessNode>>>(
195
+ definition: ProcessDefinition<V, N>,
196
+ ): CodeProcess<ProcessInput<V>, ProcessOutput<V>> {
197
+ const { document, ...catalog } = definition;
198
+ return Object.freeze({ ...catalog, document: storedDocument(String(definition.key), document as unknown as LooseDocument), [CODE_PROCESS]: true as const });
42
199
  }
43
200
 
44
201
  export function isCodeProcess(value: unknown): value is CodeProcess {
45
202
  return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[CODE_PROCESS] === true;
46
203
  }
204
+
205
+ interface LooseDocument {
206
+ readonly settings?: { readonly errorHandler?: unknown };
207
+ readonly nodes?: unknown;
208
+ }
209
+
210
+ /** The document with its references written as the engine stores them: a Tool by name, a process by key. */
211
+ function storedDocument(key: string, document: LooseDocument): ProcessDocument {
212
+ // A wrong shape is the business of the checks at start: they name every problem at once.
213
+ if (typeof document !== "object" || document === null) return document as unknown as ProcessDocument;
214
+ const out: Record<string, unknown> = { ...document };
215
+ if (typeof document.settings === "object" && document.settings !== null && document.settings.errorHandler !== undefined) {
216
+ out.settings = { ...document.settings, errorHandler: processKeyOf(key, "settings.errorHandler", document.settings.errorHandler) };
217
+ }
218
+ if (typeof document.nodes === "object" && document.nodes !== null && !Array.isArray(document.nodes)) {
219
+ const nodes: Record<string, unknown> = {};
220
+ for (const [id, node] of Object.entries(document.nodes as Record<string, unknown>)) nodes[id] = storedNode(key, id, node);
221
+ out.nodes = nodes;
222
+ }
223
+ return out as unknown as ProcessDocument;
224
+ }
225
+
226
+ function storedNode(key: string, id: string, node: unknown): unknown {
227
+ if (typeof node !== "object" || node === null) return node;
228
+ const { kind, config } = node as { readonly kind?: unknown; readonly config?: unknown };
229
+ if (typeof config !== "object" || config === null) return node;
230
+ const fields = config as Record<string, unknown>;
231
+ if (kind === "serviceTask" && typeof fields.tool === "function") {
232
+ const name = toolNameOf(fields.tool);
233
+ if (name === undefined) {
234
+ throw new TypeError(`defineProcess("${key}"): node "${id}" names ${fields.tool.name || "a class"} as its Tool, but it is neither a class with @Tool nor a Tool made by defineTool.`);
235
+ }
236
+ return { ...node, config: { ...fields, tool: name } };
237
+ }
238
+ if (kind === "callProcess" && typeof fields.process !== "string" && fields.process !== undefined) {
239
+ return { ...node, config: { ...fields, process: processKeyOf(key, `node "${id}"`, fields.process) } };
240
+ }
241
+ return node;
242
+ }
243
+
244
+ function processKeyOf(key: string, where: string, value: unknown): unknown {
245
+ if (typeof value === "string") return value;
246
+ if (isCodeProcess(value)) return value.key;
247
+ throw new TypeError(`defineProcess("${key}"): ${where} must name a process by its key or by its defineProcess(...) result.`);
248
+ }
@@ -1,5 +1,6 @@
1
1
  import { expandModuleMetadata, type BazisModuleRef } from "../../../di";
2
2
  import { describeTool, toolMetadataOf } from "../../../agent";
3
+ import { moduleToolsOf } from "../../../agent/defineTool";
3
4
  import { documentReferences, schemaReferenceKinds, validateProcess } from "../../../../library/workflow";
4
5
  import { WORKFLOW_CALLER } from "../../internal/WorkflowTools";
5
6
  import { WorkflowDefinitionsModule } from "../WorkflowDefinitions.module";
@@ -38,8 +39,8 @@ export function inspectProcesses(root: BazisModuleRef): ProcessInspection {
38
39
  const fields = moduleRef as { readonly processes?: unknown; readonly tools?: unknown; readonly references?: unknown };
39
40
  if (typeof fields.references === "object" && fields.references !== null) for (const kind of Object.keys(fields.references)) kinds.add(kind);
40
41
  if (Array.isArray(fields.tools)) {
41
- for (const target of fields.tools) {
42
- const metadata = typeof target === "function" ? toolMetadataOf(target as new () => object) : undefined;
42
+ for (const target of toolsOrNone(fields.tools)) {
43
+ const metadata = toolMetadataOf(target as new () => object);
43
44
  if (metadata?.callers.includes(WORKFLOW_CALLER)) {
44
45
  tools.add(metadata.name);
45
46
  const input = describeTool({ target: target as new () => object, metadata }).input;
@@ -83,3 +84,12 @@ export function inspectProcesses(root: BazisModuleRef): ProcessInspection {
83
84
  tools: [...tools].sort(),
84
85
  };
85
86
  }
87
+
88
+ /** The Tools of a module; a wrong list is reported by the start, not by the check. */
89
+ function toolsOrNone(tools: unknown): readonly unknown[] {
90
+ try {
91
+ return moduleToolsOf(tools);
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
@@ -45,7 +45,7 @@ export class ProcessesController {
45
45
  owner: optionalString(request.owner, "owner") ?? null,
46
46
  retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null,
47
47
  }, actor, { request: from });
48
- return Created(`${ctx.path}/${process.key}`, withTitleText(process, language, defaultLanguage(ctx)));
48
+ return Created(withTitleText(process, language, defaultLanguage(ctx)), `${ctx.path}/${process.key}`);
49
49
  }
50
50
 
51
51
  @Get("processes/:key")
@@ -105,7 +105,7 @@ export class ProcessesController {
105
105
  const key = param(ctx, "key");
106
106
  const { actor, request, language } = await callerOf(ctx, "workflow.processes.publish", await this.objectOf(key));
107
107
  const version = await this.definitions.publish(key, actor, { request, language });
108
- return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`, version);
108
+ return Created(version, `${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`);
109
109
  }
110
110
 
111
111
  @Get("processes/:key/versions")
@@ -0,0 +1,39 @@
1
+ import type { VersionDetails } from "../services/IWorkflowDefinitions.service";
2
+
3
+ /** A process with a published version, as a list query reads it: no documents. */
4
+ export interface PublishedHead {
5
+ readonly id: string;
6
+ readonly key: string;
7
+ readonly latestVersion: number;
8
+ }
9
+
10
+ /**
11
+ * The latest published versions, kept between calls. Versions are immutable, so
12
+ * what was read once is read no more: an event or a firing of a schedule costs
13
+ * one query of the heads, and documents are loaded only for versions published
14
+ * since. One per application instance; another instance sees a publication with
15
+ * its next query of the heads.
16
+ */
17
+ export class PublishedVersions {
18
+ private readonly byProcess = new Map<string, VersionDetails>();
19
+
20
+ /** The latest version of each head; `load` reads the documents this cache does not hold yet. */
21
+ async resolve(heads: readonly PublishedHead[], load: (missing: readonly PublishedHead[]) => Promise<readonly VersionDetails[]>): Promise<readonly VersionDetails[]> {
22
+ const current = new Set(heads.map((head) => head.id));
23
+ for (const id of this.byProcess.keys()) if (!current.has(id)) this.byProcess.delete(id);
24
+ const missing = heads.filter((head) => this.byProcess.get(head.id)?.version !== head.latestVersion);
25
+ if (missing.length > 0) {
26
+ const loaded = new Map((await load(missing)).map((version) => [version.key, version]));
27
+ for (const head of missing) {
28
+ const version = loaded.get(head.key);
29
+ // A version another instance has just replaced is read on the next call.
30
+ if (version?.version === head.latestVersion) this.byProcess.set(head.id, version);
31
+ else this.byProcess.delete(head.id);
32
+ }
33
+ }
34
+ return heads.flatMap((head) => {
35
+ const version = this.byProcess.get(head.id);
36
+ return version === undefined ? [] : [version];
37
+ });
38
+ }
39
+ }
@@ -1,4 +1,4 @@
1
- import { Column, Entity, Index, UUID } from "../../../orm";
1
+ import { Column, Entity, Index, UUID, Nullable } from "../../../orm";
2
2
  import type { LocalizedText } from "../../../../library/workflow";
3
3
 
4
4
  /**
@@ -12,50 +12,63 @@ export class WfProcess {
12
12
  id = "";
13
13
 
14
14
  @Index({ unique: true })
15
+ @Nullable()
15
16
  @Column({ type: "text" })
16
17
  key = "";
17
18
 
19
+ @Nullable()
18
20
  @Column({ type: "json" })
19
21
  title: LocalizedText = "";
20
22
 
21
23
  /** Every text of `title`, the default language first: what a list searches and sorts by. */
24
+ @Nullable()
22
25
  @Column({ type: "text" })
23
26
  titleText = "";
24
27
 
25
- @Column({ type: "json", nullable: true })
28
+ @Nullable()
29
+ @Column({ type: "json" })
26
30
  description: LocalizedText | null = null;
27
31
 
28
32
  /** A path, `Sales/Orders`: there is no folder entity. */
29
33
  @Index()
30
- @Column({ type: "text", nullable: true })
34
+ @Nullable()
35
+ @Column({ type: "text" })
31
36
  folder: string | null = null;
32
37
 
33
38
  /** Whose process it is: an opaque id of the application. */
34
- @Column({ type: "text", nullable: true })
39
+ @Nullable()
40
+ @Column({ type: "text" })
35
41
  owner: string | null = null;
36
42
 
37
43
  /** The draft document; kept with validation errors too. */
38
- @Column({ type: "json", nullable: true })
44
+ @Nullable()
45
+ @Column({ type: "json" })
39
46
  draft: unknown = null;
40
47
 
41
- @Column({ type: "datetime", nullable: true })
48
+ @Nullable()
49
+ @Column({ type: "datetime" })
42
50
  draftUpdatedAt: Date | null = null;
43
51
 
44
- @Column({ type: "text", nullable: true })
52
+ @Nullable()
53
+ @Column({ type: "text" })
45
54
  draftUpdatedByKind: string | null = null;
46
55
 
47
- @Column({ type: "text", nullable: true })
56
+ @Nullable()
57
+ @Column({ type: "text" })
48
58
  draftUpdatedById: string | null = null;
49
59
 
50
- @Column({ type: "integer", nullable: true })
60
+ @Nullable()
61
+ @Column({ type: "integer" })
51
62
  latestVersion: number | null = null;
52
63
 
53
64
  /** Days to keep completed and cancelled instances; `null` keeps them. */
54
- @Column({ type: "integer", nullable: true })
65
+ @Nullable()
66
+ @Column({ type: "integer" })
55
67
  retentionDays: number | null = null;
56
68
 
57
69
  /** An archived process is not started. */
58
- @Column({ type: "datetime", nullable: true })
70
+ @Nullable()
71
+ @Column({ type: "datetime" })
59
72
  archivedAt: Date | null = null;
60
73
 
61
74
  @Column({ type: "createdAt" })
@@ -1,4 +1,4 @@
1
- import { Column, Entity, ForeignKey, Index, UUID } from "../../../orm";
1
+ import { Column, Entity, ForeignKey, Index, UUID, Nullable } from "../../../orm";
2
2
  import { WfProcess } from "./WfProcess.model";
3
3
 
4
4
  /** A published version of a process: an immutable snapshot of its document. */
@@ -10,25 +10,31 @@ export class WfVersion {
10
10
  id = "";
11
11
 
12
12
  /** A reference: the foreign key makes the column a native uuid. A non-key `type: "uuid"` would generate a value. */
13
+ @Nullable()
13
14
  @Column({ type: "text" })
14
15
  processId = "";
15
16
 
17
+ @Nullable()
16
18
  @Column({ type: "integer" })
17
19
  version = 0;
18
20
 
21
+ @Nullable()
19
22
  @Column({ type: "json" })
20
23
  document: unknown = null;
21
24
 
22
25
  /** sha-256 of the document: publishing the same document again adds no version. */
26
+ @Nullable()
23
27
  @Column({ type: "text" })
24
28
  documentHash = "";
25
29
 
26
30
  @Column({ type: "createdAt" })
27
31
  publishedAt = new Date(0);
28
32
 
33
+ @Nullable()
29
34
  @Column({ type: "text" })
30
35
  publishedByKind = "";
31
36
 
37
+ @Nullable()
32
38
  @Column({ type: "text" })
33
39
  publishedById = "";
34
40
  }
@@ -146,7 +146,10 @@ export abstract class IWorkflowDefinitions {
146
146
  abstract version(key: string, version: number): Promise<VersionDetails>;
147
147
  /** The latest published version of a process that is not archived. */
148
148
  abstract latest(key: string): Promise<VersionDetails>;
149
- /** The latest version of every process that is not archived: where event and scheduled starts come from. */
149
+ /**
150
+ * The latest version of every process that is not archived: where event and scheduled
151
+ * starts come from. Documents are kept between calls and must not be changed.
152
+ */
150
153
  abstract published(): Promise<readonly VersionDetails[]>;
151
154
  /** A version by id, for the engine; `undefined` if there is none. */
152
155
  abstract versionById(id: string): Promise<VersionDetails | undefined>;