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
@@ -19,6 +19,7 @@ import type { CodeProcessCatalog } from "../code/CodeProcessCatalog";
19
19
  import type { OwnedCodeProcess } from "../code/codeProcesses";
20
20
  import { WORKFLOW_PROCESS_CHANGED, type ProcessChange } from "../events/Definitions.events";
21
21
  import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
22
+ import type { PublishedVersions } from "../internal/PublishedVersions";
22
23
  import { WfProcess } from "../model/WfProcess.model";
23
24
  import { WfVersion } from "../model/WfVersion.model";
24
25
  import {
@@ -52,6 +53,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
52
53
  private readonly events: WorkflowEvents,
53
54
  private readonly languages: WorkflowLanguages,
54
55
  private readonly references: WorkflowReferences,
56
+ private readonly publishedVersions: PublishedVersions,
55
57
  /** The scope: the port is asked here, so that the application's own registration replaces the default. */
56
58
  private readonly services: ServiceProvider,
57
59
  ) {
@@ -351,13 +353,21 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
351
353
  }
352
354
 
353
355
  async published(): Promise<readonly VersionDetails[]> {
354
- const processes = await this.db.processes.asNoTracking().where((p) => p.archivedAt.isNull().and(p.latestVersion.isNotNull())).toList();
355
- if (processes.length === 0) return [];
356
- const byId = new Map(processes.map((process) => [process.id, process]));
357
- const versions = await this.db.versions.asNoTracking().where((v) => v.processId.in([...byId.keys()])).toList();
358
- return versions
359
- .filter((version) => byId.get(version.processId)?.latestVersion === version.version)
360
- .map((version) => versionOf(byId.get(version.processId)!.key, version));
356
+ // Without the drafts and documents: this runs on every event and every firing of a schedule.
357
+ const rows = await this.db.processes.asNoTracking()
358
+ .where((p) => p.archivedAt.isNull().and(p.latestVersion.isNotNull()))
359
+ .select((p) => ({ id: p.id, key: p.key, latestVersion: p.latestVersion }))
360
+ .toList();
361
+ const heads = rows.map((row) => ({ id: row.id, key: row.key, latestVersion: row.latestVersion! }));
362
+ return await this.publishedVersions.resolve(heads, async (missing) => {
363
+ const versions = await this.db.versions.asNoTracking()
364
+ .where((v) => v.processId.in(missing.map((head) => head.id)).and(v.version.in([...new Set(missing.map((head) => head.latestVersion))])))
365
+ .toList();
366
+ const byId = new Map(missing.map((head) => [head.id, head]));
367
+ return versions
368
+ .filter((version) => byId.get(version.processId)?.latestVersion === version.version)
369
+ .map((version) => versionOf(byId.get(version.processId)!.key, version));
370
+ });
361
371
  }
362
372
 
363
373
  async versionById(id: string): Promise<VersionDetails | undefined> {
@@ -14,6 +14,7 @@ import { CatalogImport } from "../definitions_module/code/CatalogImport";
14
14
  import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
15
15
  import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
16
16
  import { ProcessesController } from "../definitions_module/http/Processes.controller";
17
+ import { PublishedVersions } from "../definitions_module/internal/PublishedVersions";
17
18
  import { DefinitionsDbContext } from "../definitions_module/model/Definitions.dbContext";
18
19
  import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDefinitions.service";
19
20
  import { WorkflowDefinitions } from "../definitions_module/services/WorkflowDefinitions.service";
@@ -66,7 +67,7 @@ registerFrameworkClassDeps([
66
67
  [TasksDbContext, [DbContextOptions]],
67
68
  [TimerStep, [RuntimeDbContext, WorkflowEngine]],
68
69
  [ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools, WorkflowReferences, WorkflowLanguages, ServiceProvider]],
69
- [WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowReferences, ServiceProvider]],
70
+ [WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowReferences, PublishedVersions, ServiceProvider]],
70
71
  [WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache, WorkflowEvents, WorkflowClock]],
71
72
  [WorkflowEvents, [EventBus, ServiceProvider, "optional:Logger"]],
72
73
  [WorkflowLanguages, ["Config:workflow"]],
@@ -3,11 +3,28 @@
3
3
  * with the Definitions and Runtime parts, their services, HTTP, events and
4
4
  * extension points. The document format is `bazis/library/workflow`.
5
5
  */
6
+ import type { PermissionsOf } from "../access";
7
+ import type { definitionsPermissions } from "./definitions_module/access/Definitions.permissions";
6
8
  import type { CodeProcess } from "./definitions_module/code/defineProcess";
9
+ import type { runtimePermissions } from "./runtime_module/access/Runtime.permissions";
10
+ import type { tasksPermissions } from "./tasks_module/access/Tasks.permissions";
7
11
  import "./generated/deps";
8
12
 
9
13
  export { WorkflowModule } from "./Workflow.module";
10
- export { defineProcess, type CodeProcess, type ProcessDefinition } from "./definitions_module/code/defineProcess";
14
+ export {
15
+ defineProcess,
16
+ type CodeNodeConfigs,
17
+ type CodeProcess,
18
+ type CodeProcessDocument,
19
+ type CodeProcessNode,
20
+ type CodeProcessSettings,
21
+ type ProcessDefinition,
22
+ type ProcessInput,
23
+ type ProcessOutput,
24
+ type ProcessRef,
25
+ type ToolClass,
26
+ type ToolRef,
27
+ } from "./definitions_module/code/defineProcess";
11
28
  export { WorkflowSetupError } from "./definitions_module/code/codeProcesses";
12
29
  export { WorkflowClock, SystemWorkflowClock } from "./contracts/WorkflowClock";
13
30
  export { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefinitions.module";
@@ -73,6 +90,11 @@ export { WorkflowPartitions, DefaultWorkflowPartitions, type ProcessPartition }
73
90
  export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
74
91
  export type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging, WorkflowRequest, WorkflowToolMetadata } from "./contracts/types";
75
92
 
93
+ // The pack's own permissions: an application that imports the pack may name them in `can` and `defineRoles`.
94
+ declare module "../access/index" {
95
+ interface BazisPermissions extends PermissionsOf<typeof definitionsPermissions>, PermissionsOf<typeof runtimePermissions>, PermissionsOf<typeof tasksPermissions> {}
96
+ }
97
+
76
98
  // Augment the declaring file, not the "../di" re-export: re-export targets
77
99
  // depend on program file order and silently stop merging in some projects.
78
100
  declare module "../di/module/types/BazisModule" {
@@ -16,7 +16,7 @@ export class WorkflowLanguages {
16
16
  /** The best language of an `Accept-Language` header: by weight, then by order; `ru-RU` also finds `ru`. */
17
17
  of(header: string | undefined): string {
18
18
  if (header === undefined) return this.default;
19
- const known = new Set([this.default, ...this.config.get("languages").split(",").map((item) => item.trim())]);
19
+ const known = new Set([this.default, ...this.config.get("languages")]);
20
20
  const wanted = header.split(",")
21
21
  .map((part, index) => {
22
22
  const [tag = "", ...parameters] = part.trim().split(";");
@@ -1,4 +1,4 @@
1
- import { IAccess, type AccessObject, type SubjectAccess } from "../../access";
1
+ import { IAccess, type AccessObject, type SubjectAccess, type PermissionKey } from "../../access";
2
2
  import { ForbiddenError, type HttpContext } from "../../http";
3
3
  import { frameworkBadRequest } from "../../http/Errors/HttpError";
4
4
  import { ListQueryError, parseListQuery, type ListQuery, type ListQueryOptions, type ListQueryProblem } from "../../../library/jsonapi";
@@ -32,7 +32,7 @@ export function defaultLanguage(ctx: HttpContext): string {
32
32
  * right. The actor's id is the first owner id, so that `{ own: true }` means
33
33
  * the instances the subject started.
34
34
  */
35
- export async function callerOf(ctx: HttpContext, permission?: string, object?: AccessObject): Promise<WorkflowCaller> {
35
+ export async function callerOf(ctx: HttpContext, permission?: PermissionKey, object?: AccessObject): Promise<WorkflowCaller> {
36
36
  const access = await ctx.services.resolve(IAccess).for(ctx);
37
37
  if (permission !== undefined && !await access.can(permission, object)) throw new ForbiddenError();
38
38
  const sub = access.subject.sub;
@@ -38,8 +38,12 @@ export class WorkflowWorker extends BackgroundService {
38
38
  try {
39
39
  const jobs = await this.jobs.claim(batchSize);
40
40
  claimed = jobs.length;
41
- for (const job of jobs) {
42
- if (signal.aborted) return;
41
+ for (const [index, job] of jobs.entries()) {
42
+ if (signal.aborted) {
43
+ // What was claimed and will not run is free for another worker at once, not in a lease.
44
+ await this.jobs.release(jobs.slice(index));
45
+ return;
46
+ }
43
47
  await this.jobs.run(job, signal);
44
48
  }
45
49
  } catch (error) {
@@ -32,7 +32,7 @@ export class InstancesController {
32
32
  language,
33
33
  ...(request.test === true ? { test: true } : {}),
34
34
  });
35
- return Created(`${ctx.path}/${instance.id}`, instance);
35
+ return Created(instance, `${ctx.path}/${instance.id}`);
36
36
  }
37
37
 
38
38
  /** A start per element, for an action over many records (WORKFLOW-002 §5.1). */
@@ -26,7 +26,10 @@ export class WorkflowJobs {
26
26
  private readonly report: JobFailureReport,
27
27
  ) {}
28
28
 
29
- /** Takes up to `batchSize` due jobs with `FOR UPDATE SKIP LOCKED` and a lease, so instances never take one job twice. */
29
+ /**
30
+ * Takes up to `batchSize` due jobs with `FOR UPDATE SKIP LOCKED` and a lease, so
31
+ * instances never take one job twice. {@link run} renews the lease of each as it starts.
32
+ */
30
33
  async claim(batchSize: number): Promise<WfJob[]> {
31
34
  const scope = this.services.createScope();
32
35
  try {
@@ -51,7 +54,46 @@ export class WorkflowJobs {
51
54
  }
52
55
  }
53
56
 
57
+ /**
58
+ * The lease of a claimed job starts when the job does: a batch runs one job after
59
+ * another, and a job that waited for the ones before it longer than a lease may
60
+ * have been taken by another worker. `false` then: the job is not this worker's.
61
+ */
62
+ private async begin(job: WfJob): Promise<boolean> {
63
+ const scope = this.services.createScope();
64
+ try {
65
+ const db = scope.resolve(RuntimeDbContext);
66
+ const lockedUntil = new Date(scope.resolve(WorkflowClock).now().getTime() + LEASE_MS);
67
+ const held = job.lockedUntil;
68
+ const { affectedRows } = await db.jobs.asNoTracking()
69
+ .where((j) => j.id.eq(job.id).and(held === null ? j.lockedUntil.isNull() : j.lockedUntil.eq(held)))
70
+ .executeUpdate({ lockedUntil });
71
+ if (affectedRows === 0) return false;
72
+ job.lockedUntil = lockedUntil;
73
+ return true;
74
+ } finally {
75
+ await scope.dispose();
76
+ }
77
+ }
78
+
79
+ /** Gives back claimed jobs that will not run, a stopping worker's: they are free at once, and the claim does not count as an attempt. */
80
+ async release(jobs: readonly WfJob[]): Promise<void> {
81
+ if (jobs.length === 0) return;
82
+ const scope = this.services.createScope();
83
+ try {
84
+ const db = scope.resolve(RuntimeDbContext);
85
+ for (const job of jobs) {
86
+ const held = job.lockedUntil;
87
+ if (held === null) continue;
88
+ await db.jobs.asNoTracking().where((j) => j.id.eq(job.id).and(j.lockedUntil.eq(held))).executeUpdate({ lockedUntil: null, attempts: Math.max(job.attempts - 1, 0) });
89
+ }
90
+ } finally {
91
+ await scope.dispose();
92
+ }
93
+ }
94
+
54
95
  async run(job: WfJob, signal: AbortSignal): Promise<void> {
96
+ if (!await this.begin(job).catch((error: unknown) => { this.report("job could not begin", error, { job: job.id }); return false; })) return;
55
97
  const scope = this.services.createScope();
56
98
  try {
57
99
  switch (job.kind) {
@@ -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 { WfInstance } from "./WfInstance.model";
3
3
 
4
4
  export type HistoryType = "started" | "queued" | "released" | "entered" | "left" | "tool" | "assigned" | "iteration" | "called" | "returned" | "skipped" | "timer" | "event" | "error" | "retried" | "cancelled" | "timedOut" | "handler" | "completed";
@@ -11,26 +11,33 @@ export class WfHistory {
11
11
  @UUID({ version: "v7" })
12
12
  id = "";
13
13
 
14
+ @Nullable()
14
15
  @Column({ type: "text" })
15
16
  instanceId = "";
16
17
 
17
- @Column({ type: "text", nullable: true })
18
+ @Nullable()
19
+ @Column({ type: "text" })
18
20
  tokenId: string | null = null;
19
21
 
20
- @Column({ type: "text", nullable: true })
22
+ @Nullable()
23
+ @Column({ type: "text" })
21
24
  nodeId: string | null = null;
22
25
 
26
+ @Nullable()
23
27
  @Column({ type: "text" })
24
28
  type: HistoryType = "entered";
25
29
 
26
- @Column({ type: "json", nullable: true })
30
+ @Nullable()
31
+ @Column({ type: "json" })
27
32
  data: Record<string, unknown> | null = null;
28
33
 
29
34
  /** Who did it: cancel, retry, task completion; `null` for engine steps. */
30
- @Column({ type: "text", nullable: true })
35
+ @Nullable()
36
+ @Column({ type: "text" })
31
37
  actorKind: string | null = null;
32
38
 
33
- @Column({ type: "text", nullable: true })
39
+ @Nullable()
40
+ @Column({ type: "text" })
34
41
  actorId: string | null = null;
35
42
 
36
43
  @Column({ type: "createdAt" })
@@ -1,4 +1,4 @@
1
- import { Column, Entity, Index, UUID } from "../../../orm";
1
+ import { Column, Entity, Index, UUID, Nullable } from "../../../orm";
2
2
 
3
3
  /**
4
4
  * `completed`, `cancelled` and `timedOut` are finished: retention deletes them.
@@ -23,76 +23,96 @@ export class WfInstance {
23
23
  id = "";
24
24
 
25
25
  /** The version (a Definitions row: a reference without a foreign key). */
26
+ @Nullable()
26
27
  @Column({ type: "text" })
27
28
  versionId = "";
28
29
 
30
+ @Nullable()
29
31
  @Column({ type: "text" })
30
32
  processKey = "";
31
33
 
34
+ @Nullable()
32
35
  @Column({ type: "integer" })
33
36
  version = 0;
34
37
 
35
38
  /** The application's scope: a space, an organization. Never interpreted. */
36
- @Column({ type: "text", nullable: true })
39
+ @Nullable()
40
+ @Column({ type: "text" })
37
41
  partition: string | null = null;
38
42
 
43
+ @Nullable()
39
44
  @Column({ type: "text" })
40
45
  status: InstanceStatus = "running";
41
46
 
47
+ @Nullable()
42
48
  @Column({ type: "json" })
43
49
  input: Record<string, unknown> = {};
44
50
 
51
+ @Nullable()
45
52
  @Column({ type: "json" })
46
53
  vars: Record<string, unknown> = {};
47
54
 
48
55
  /** The result of every passed step by node id: `$nodes.<node>.<field>`. */
56
+ @Nullable()
49
57
  @Column({ type: "json" })
50
58
  nodes: Record<string, unknown> = {};
51
59
 
52
60
  /** What the process gave back: its `out` variables when it completed. */
53
- @Column({ type: "json", nullable: true })
61
+ @Nullable()
62
+ @Column({ type: "json" })
54
63
  output: Record<string, unknown> | null = null;
55
64
 
56
65
  /** `$event`: the data of the event that started or last continued it. */
57
- @Column({ type: "json", nullable: true })
66
+ @Nullable()
67
+ @Column({ type: "json" })
58
68
  event: unknown = null;
59
69
 
60
70
  /** `$trigger`: how it was started. */
61
- @Column({ type: "json", nullable: true })
71
+ @Nullable()
72
+ @Column({ type: "json" })
62
73
  trigger: Record<string, unknown> | null = null;
63
74
 
64
75
  /** A trial run (WORKFLOW-002 §5.4, stage 5). */
76
+ @Nullable()
65
77
  @Column({ type: "boolean" })
66
78
  test = false;
67
79
 
68
80
  /** The instance whose `callProcess` started this one, and the waiting token there. */
69
81
  @Index()
70
- @Column({ type: "text", nullable: true })
82
+ @Nullable()
83
+ @Column({ type: "text" })
71
84
  parentInstanceId: string | null = null;
72
85
 
73
- @Column({ type: "text", nullable: true })
86
+ @Nullable()
87
+ @Column({ type: "text" })
74
88
  parentTokenId: string | null = null;
75
89
 
76
90
  @Index()
77
- @Column({ type: "text", nullable: true })
91
+ @Nullable()
92
+ @Column({ type: "text" })
78
93
  businessKey: string | null = null;
79
94
 
95
+ @Nullable()
80
96
  @Column({ type: "text" })
81
97
  startedVia: StartedVia = "manual";
82
98
 
99
+ @Nullable()
83
100
  @Column({ type: "text" })
84
101
  startedByKind: "user" | "system" = "user";
85
102
 
103
+ @Nullable()
86
104
  @Column({ type: "text" })
87
105
  startedById = "";
88
106
 
89
107
  /** Why the instance failed: node, code and message. */
90
- @Column({ type: "json", nullable: true })
108
+ @Nullable()
109
+ @Column({ type: "json" })
91
110
  error: { readonly node: string; readonly code: string; readonly message: string } | null = null;
92
111
 
93
112
  @Column({ type: "createdAt" })
94
113
  createdAt = new Date(0);
95
114
 
96
- @Column({ type: "datetime", nullable: true })
115
+ @Nullable()
116
+ @Column({ type: "datetime" })
97
117
  endedAt: Date | null = null;
98
118
  }
@@ -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 { WfInstance } from "./WfInstance.model";
3
3
 
4
4
  /**
@@ -24,50 +24,62 @@ export class WfJob {
24
24
  @UUID({ version: "v7" })
25
25
  id = "";
26
26
 
27
+ @Nullable()
27
28
  @Column({ type: "text" })
28
29
  kind: JobKind = "tool";
29
30
 
30
31
  /** `null` for `schedule`, `event` and `eventStart` jobs: the instance does not exist yet. */
31
32
  @Index()
32
- @Column({ type: "text", nullable: true })
33
+ @Nullable()
34
+ @Column({ type: "text" })
33
35
  instanceId: string | null = null;
34
36
 
35
37
  @Index()
36
- @Column({ type: "text", nullable: true })
38
+ @Nullable()
39
+ @Column({ type: "text" })
37
40
  tokenId: string | null = null;
38
41
 
39
42
  /** The version a `schedule` or `eventStart` job starts. */
40
- @Column({ type: "text", nullable: true })
43
+ @Nullable()
44
+ @Column({ type: "text" })
41
45
  versionId: string | null = null;
42
46
 
43
47
  @Index()
48
+ @Nullable()
44
49
  @Column({ type: "datetime" })
45
50
  dueAt = new Date(0);
46
51
 
47
52
  /** What a job carries besides its token: the caller to tell, the process to release. */
48
- @Column({ type: "json", nullable: true })
53
+ @Nullable()
54
+ @Column({ type: "json" })
49
55
  payload: unknown = null;
50
56
 
51
57
  /** Jobs with one key merge into the last: `debounce` of an event start. */
52
58
  @Index()
53
- @Column({ type: "text", nullable: true })
59
+ @Nullable()
60
+ @Column({ type: "text" })
54
61
  dedupeKey: string | null = null;
55
62
 
56
63
  /** The partition and the start node of a `schedule` or `eventStart` job. */
57
- @Column({ type: "text", nullable: true })
64
+ @Nullable()
65
+ @Column({ type: "text" })
58
66
  partition: string | null = null;
59
67
 
60
- @Column({ type: "text", nullable: true })
68
+ @Nullable()
69
+ @Column({ type: "text" })
61
70
  nodeId: string | null = null;
62
71
 
72
+ @Nullable()
63
73
  @Column({ type: "integer" })
64
74
  attempts = 0;
65
75
 
66
76
  /** The worker holds the job until then; if it dies, the job becomes free again. */
67
- @Column({ type: "datetime", nullable: true })
77
+ @Nullable()
78
+ @Column({ type: "datetime" })
68
79
  lockedUntil: Date | null = null;
69
80
 
70
- @Column({ type: "text", nullable: true })
81
+ @Nullable()
82
+ @Column({ type: "text" })
71
83
  lastError: string | null = null;
72
84
 
73
85
  @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 { WfInstance } from "./WfInstance.model";
3
3
 
4
4
  /** A token waiting for an event: `eventWait` or `boundaryEvent`. Matched by key and correlation. */
@@ -9,20 +9,25 @@ export class WfSubscription {
9
9
  @UUID({ version: "v7" })
10
10
  id = "";
11
11
 
12
+ @Nullable()
12
13
  @Column({ type: "text" })
13
14
  instanceId = "";
14
15
 
15
16
  @Index()
17
+ @Nullable()
16
18
  @Column({ type: "text" })
17
19
  tokenId = "";
18
20
 
21
+ @Nullable()
19
22
  @Column({ type: "text" })
20
23
  eventKey = "";
21
24
 
22
- @Column({ type: "text", nullable: true })
25
+ @Nullable()
26
+ @Column({ type: "text" })
23
27
  correlation: string | null = null;
24
28
 
25
- @Column({ type: "text", nullable: true })
29
+ @Nullable()
30
+ @Column({ type: "text" })
26
31
  partition: string | null = null;
27
32
 
28
33
  @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 { WfInstance } from "./WfInstance.model";
3
3
 
4
4
  /**
@@ -16,36 +16,45 @@ export class WfToken {
16
16
  @UUID({ version: "v7" })
17
17
  id = "";
18
18
 
19
+ @Nullable()
19
20
  @Column({ type: "text" })
20
21
  instanceId = "";
21
22
 
23
+ @Nullable()
22
24
  @Column({ type: "text" })
23
25
  nodeId = "";
24
26
 
25
27
  /** The flow the token came by; `null` on a start node. */
26
- @Column({ type: "text", nullable: true })
28
+ @Nullable()
29
+ @Column({ type: "text" })
27
30
  viaFlowId: string | null = null;
28
31
 
32
+ @Nullable()
29
33
  @Column({ type: "text" })
30
34
  status: TokenStatus = "active";
31
35
 
32
36
  /** A step's result; a loop keeps its progress here while it runs. */
33
- @Column({ type: "json", nullable: true })
37
+ @Nullable()
38
+ @Column({ type: "json" })
34
39
  output: unknown = null;
35
40
 
36
41
  /** The loop token this one runs inside, with its iteration and what only the body sees: `Locals` of the engine. */
37
- @Column({ type: "text", nullable: true })
42
+ @Nullable()
43
+ @Column({ type: "text" })
38
44
  parentTokenId: string | null = null;
39
45
 
40
- @Column({ type: "integer", nullable: true })
46
+ @Nullable()
47
+ @Column({ type: "integer" })
41
48
  iteration: number | null = null;
42
49
 
43
- @Column({ type: "json", nullable: true })
50
+ @Nullable()
51
+ @Column({ type: "json" })
44
52
  locals: Record<string, unknown> | null = null;
45
53
 
46
54
  @Column({ type: "createdAt" })
47
55
  createdAt = new Date(0);
48
56
 
49
- @Column({ type: "datetime", nullable: true })
57
+ @Nullable()
58
+ @Column({ type: "datetime" })
50
59
  endedAt: Date | null = null;
51
60
  }
@@ -1,13 +1,15 @@
1
1
  import type { AccessFilter } from "../../../access";
2
2
  import type { FormIssue } from "../../../../library/workflow";
3
3
  import type { OrmTransaction } from "../../../orm";
4
+ import type { CodeProcess } from "../../definitions_module/code/defineProcess";
4
5
  import type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging } from "../../contracts/types";
5
6
  import type { HistoryType } from "../model/WfHistory.model";
6
7
  import type { InstanceStatus, StartedVia } from "../model/WfInstance.model";
7
8
  import type { TokenStatus } from "../model/WfToken.model";
8
9
 
9
- export interface StartInstance extends WorkflowAuditOptions {
10
- readonly input?: unknown;
10
+ export interface StartInstance<Input = unknown> extends WorkflowAuditOptions {
11
+ /** The `in` variables of the process. */
12
+ readonly input?: Input;
11
13
  /** The application's scope: a space, an organization. */
12
14
  readonly partition?: string | null;
13
15
  /** The application's own key of what the instance is about: `order:42`. */
@@ -24,14 +26,14 @@ export interface StartInstance extends WorkflowAuditOptions {
24
26
  }
25
27
 
26
28
  /** One element of a mass start. */
27
- export interface StartManyItem {
28
- readonly input?: unknown;
29
+ export interface StartManyItem<Input = unknown> {
30
+ readonly input?: Input;
29
31
  /** What the instance is about: required, it is what `sameKey` compares. */
30
32
  readonly businessKey: string;
31
33
  }
32
34
 
33
- export interface StartMany extends WorkflowAuditOptions {
34
- readonly items: readonly StartManyItem[];
35
+ export interface StartMany<Input = unknown> extends WorkflowAuditOptions {
36
+ readonly items: readonly StartManyItem<Input>[];
35
37
  readonly partition?: string | null;
36
38
  readonly actor: WorkflowActor;
37
39
  readonly language?: string;
@@ -163,8 +165,10 @@ export interface InstanceListQuery extends WorkflowPaging {
163
165
  */
164
166
  export abstract class IWorkflowRuntime {
165
167
  /** Starts the latest version of a process from its manual start. It may wait as `pending` (`maxConcurrent`, `sameKey: "queue"`). */
168
+ abstract start<Input>(process: CodeProcess<Input, unknown>, options: StartInstance<Input>): Promise<InstanceDetails>;
166
169
  abstract start(processKey: string, options: StartInstance): Promise<InstanceDetails>;
167
170
  /** Starts an instance per element, each in its own transaction, and says what became of each. */
171
+ abstract startMany<Input>(process: CodeProcess<Input, unknown>, options: StartMany<Input>): Promise<readonly StartManyResult[]>;
168
172
  abstract startMany(processKey: string, options: StartMany): Promise<readonly StartManyResult[]>;
169
173
  abstract cancel(instanceId: string, options: CancelInstance): Promise<InstanceDetails>;
170
174
  /** Retries the failed steps of a `failed` instance. An instance called by another one is retried through its caller. */
@@ -1,3 +1,4 @@
1
+ import type { CodeProcess } from "../../definitions_module/code/defineProcess";
1
2
  import { grantsToWhere } from "../../../access";
2
3
  import { paginate, type OrmTransaction } from "../../../orm";
3
4
  import { inputSpecOf, type FormSpec } from "../../../../library/workflow";
@@ -46,7 +47,8 @@ export class WorkflowRuntime extends IWorkflowRuntime {
46
47
  super();
47
48
  }
48
49
 
49
- async start(processKey: string, options: StartInstance): Promise<InstanceDetails> {
50
+ async start(process: string | CodeProcess<unknown, unknown>, options: StartInstance): Promise<InstanceDetails> {
51
+ const processKey = typeof process === "string" ? process : process.key;
50
52
  actorOf(options.actor);
51
53
  const partition = optionalText(options.partition, "partition");
52
54
  const businessKey = optionalText(options.businessKey, "businessKey");
@@ -60,7 +62,8 @@ export class WorkflowRuntime extends IWorkflowRuntime {
60
62
  return await this.instance(id);
61
63
  }
62
64
 
63
- async startMany(processKey: string, options: StartMany): Promise<readonly StartManyResult[]> {
65
+ async startMany(process: string | CodeProcess<unknown, unknown>, options: StartMany): Promise<readonly StartManyResult[]> {
66
+ const processKey = typeof process === "string" ? process : process.key;
64
67
  actorOf(options.actor);
65
68
  const partition = optionalText(options.partition, "partition");
66
69
  if (!Array.isArray(options.items) || options.items.length === 0 || options.items.length > MAX_START_MANY) throw new WorkflowError("invalid_input", `items must be a list of 1–${MAX_START_MANY}`);