bazis 0.128.0 → 0.130.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 (75) hide show
  1. package/core/access/Access.module.ts +36 -0
  2. package/core/access/catalog/AccessCatalog.ts +73 -0
  3. package/core/access/catalog/IAccessCatalog.service.ts +17 -0
  4. package/core/access/catalog/moduleFields.ts +153 -0
  5. package/core/access/catalog/permissions.ts +127 -0
  6. package/core/access/check/Access.ts +191 -0
  7. package/core/access/check/IAccess.service.ts +20 -0
  8. package/core/access/check/accessConfig.ts +18 -0
  9. package/core/access/check/can.ts +21 -0
  10. package/core/access/check/conditions.ts +132 -0
  11. package/core/access/check/filter.ts +132 -0
  12. package/core/access/contracts/AccessPolicy.ts +29 -0
  13. package/core/access/contracts/AttributeLookup.ts +17 -0
  14. package/core/access/contracts/References.ts +33 -0
  15. package/core/access/contracts/errors.ts +13 -0
  16. package/core/access/contracts/types.ts +66 -0
  17. package/core/access/generated/deps.ts +14 -0
  18. package/core/access/http/AccessCatalog.controller.ts +51 -0
  19. package/core/access/index.ts +35 -0
  20. package/core/access/roles/RolesAccessPolicy.ts +64 -0
  21. package/core/access/roles/roles.ts +62 -0
  22. package/core/access/testing/index.ts +54 -0
  23. package/core/background/BackgroundService.ts +1 -1
  24. package/core/di/README.md +5 -5
  25. package/core/di/errors/ClassDependenciesMismatchError.ts +1 -1
  26. package/core/di/internal/GraphValidator.ts +1 -1
  27. package/core/di/module/autoDeps.ts +11 -1
  28. package/core/di/module/createContainer.ts +1 -1
  29. package/core/di/module/providerSelection.ts +1 -1
  30. package/core/grpc/{GrpcClient.ts → GrpcClientBase.ts} +1 -1
  31. package/core/grpc/grpcClients.ts +79 -0
  32. package/core/grpc/index.ts +2 -2
  33. package/core/http/options.ts +1 -1
  34. package/core/http-client/httpClients.ts +82 -0
  35. package/core/http-client/index.ts +11 -91
  36. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  37. package/core/scripts/di-generate-target.ts +9 -0
  38. package/core/scripts/di-generate.ts +177 -9
  39. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +15 -11
  40. package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
  41. package/core/workflow/definitions_module/access/Process.references.ts +27 -0
  42. package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +4 -3
  43. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +2 -2
  44. package/core/workflow/definitions_module/http/Processes.controller.ts +23 -14
  45. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +9 -0
  46. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +4 -0
  47. package/core/workflow/generated/deps.ts +56 -0
  48. package/core/workflow/index.ts +1 -1
  49. package/core/workflow/internal/WorkflowTools.ts +2 -2
  50. package/core/workflow/internal/http.ts +18 -17
  51. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +17 -34
  52. package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
  53. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  54. package/core/workflow/runtime_module/background/WorkflowWorker.ts +1 -10
  55. package/core/workflow/runtime_module/config/Runtime.config.ts +11 -1
  56. package/core/workflow/runtime_module/http/Events.controller.ts +5 -3
  57. package/core/workflow/runtime_module/http/Instances.controller.ts +25 -15
  58. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +4 -3
  59. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +3 -0
  60. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -5
  61. package/core/workflow/tasks_module/WorkflowTasks.module.ts +8 -10
  62. package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
  63. package/core/workflow/tasks_module/events/TaskEvents.ts +29 -8
  64. package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -12
  65. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +8 -3
  66. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +41 -42
  67. package/core/workflow/testing/index.ts +1 -0
  68. package/index.ts +1 -1
  69. package/library/http-client/{HttpClient.ts → HttpClientBase.ts} +5 -5
  70. package/library/http-client/HttpClientFactory.ts +5 -5
  71. package/library/http-client/index.ts +3 -3
  72. package/library/terminal/messages.ts +2 -2
  73. package/package.json +3 -1
  74. package/core/grpc/grpcClientProvider.ts +0 -9
  75. package/core/workflow/access/WorkflowAccess.ts +0 -52
@@ -31,6 +31,13 @@ const FRAMEWORK_SOURCE = ((): string | null => {
31
31
  })();
32
32
  const FRAMEWORK_ROOT = FRAMEWORK_SOURCE ?? "src/bazis";
33
33
  const isFrameworkSourcePath = (file: string): boolean => FRAMEWORK_SOURCE !== null && file.startsWith(`${FRAMEWORK_SOURCE}/`);
34
+ /**
35
+ * Packs of the framework's own checkout whose DI wiring ships inside the
36
+ * package (`framework.packs` of bazis.config.json): an application's codegen
37
+ * skips node_modules, so the pack carries `<pack>/generated/deps.ts` itself.
38
+ */
39
+ let frameworkPacks: readonly string[] = [];
40
+ const isFrameworkPackPath = (file: string): boolean => frameworkPacks.some((pack) => file.startsWith(`${pack}/`));
34
41
  const APP_GENERATED_DIR = "src/generated/bazis";
35
42
  let activeGeneratedDir = APP_GENERATED_DIR;
36
43
  const plannedWrites = new Map<string, string>();
@@ -163,8 +170,9 @@ function readRequestedTarget(): string | undefined {
163
170
  throw new Error(`BAZIS_CODEGEN_TARGET_UNKNOWN: ${target ?? ""}`);
164
171
  }
165
172
 
166
- // Framework internals: their classes are excluded from the auto-deps map (they
167
- // wire their own services explicitly). Auto-deps magic targets user code only.
173
+ // Framework internals: their classes are excluded from the auto-deps map of a
174
+ // target. Packs listed in `framework.packs` get their own generated wiring
175
+ // (generateFrameworkPack); the rest still wire their services explicitly.
168
176
  const FRAMEWORK_INTERNAL_PREFIXES = FRAMEWORK_SOURCE === null ? [] : [
169
177
  "core/di/", "core/kernel/", "core/http/", "core/grpc/", "core/http-client/", "core/background/",
170
178
  "core/agent/", "core/websocket/", "core/cache/", "library/validation/", "library/boundary/",
@@ -251,7 +259,8 @@ let program: ts.Program;
251
259
  let programFiles = new Map<string, ts.SourceFile>();
252
260
  // Framework sources outside the scanned project (node_modules/bazis, vendor/bazis).
253
261
  // Only their class and token names are read, so app constructor dependencies on
254
- // framework classes are not discarded as unknown; their own wiring ships with them.
262
+ // framework classes are not discarded as unknown; their own wiring ships with them
263
+ // (`<pack>/generated/deps.ts` of the framework packs).
255
264
  let installedFrameworkSources: ts.SourceFile[] = [];
256
265
  let sourceFilePathSet = new Set<string>();
257
266
  let checker: ts.TypeChecker;
@@ -348,6 +357,7 @@ async function runConfiguredTargets(): Promise<void> {
348
357
  const totalStartedAt = performance.now();
349
358
  const configStartedAt = performance.now();
350
359
  const config = await readCodegenConfig();
360
+ frameworkPacks = FRAMEWORK_SOURCE === null ? [] : (config.framework?.packs ?? []);
351
361
  await planStaleGeneratedTargetRemoval(Object.keys(config.targets));
352
362
  // Test sources remain excluded from the broad candidate scan. A test-only
353
363
  // target can still opt a single fixture in explicitly through its declared
@@ -434,6 +444,7 @@ async function runConfiguredTargets(): Promise<void> {
434
444
  eligible += metrics.eligible;
435
445
  candidates += metrics.candidates;
436
446
  }
447
+ for (const pack of frameworkPacks) generateFrameworkPack(pack);
437
448
  const targetMs = performance.now() - targetStartedAt;
438
449
  const writeStartedAt = performance.now();
439
450
  await commitPlannedWrites();
@@ -444,7 +455,7 @@ async function runConfiguredTargets(): Promise<void> {
444
455
 
445
456
  function createSourceCandidateIndex(files: ReadonlyMap<string, ts.SourceFile>): ReadonlyMap<string, boolean> {
446
457
  for (const [file, source] of files) {
447
- if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => file.startsWith(prefix))) collectDiClassRegistrations(source);
458
+ if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => file.startsWith(prefix)) && !isFrameworkPackPath(file)) collectDiClassRegistrations(source);
448
459
  }
449
460
  return new Map([...files].map(([file, source]) => [file, sourceHasCodegenCandidate(source)]));
450
461
  }
@@ -511,15 +522,16 @@ async function generateTarget(name: string, reachable: Set<string>, production:
511
522
  resetTargetAnalysis(reachable, generatedDir);
512
523
  for (const { source } of sourceEntries) collectListRequestClasses(source);
513
524
  for (const { filePath, source } of sourceEntries) {
514
- if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => filePath.startsWith(prefix))) collectDiClassRegistrations(source);
525
+ if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => filePath.startsWith(prefix)) && !isFrameworkPackPath(filePath)) collectDiClassRegistrations(source);
515
526
  }
516
527
  const prepassMs = performance.now() - startedAt;
517
528
  for (const source of installedFrameworkSources) collectTokenDescriptions(source);
518
529
  for (const { filePath, source } of sourceEntries) {
519
530
  fatalErrors.push(...analyzeOrmPredicates(checker, source));
520
531
  collectTokenDescriptions(source);
521
- if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => filePath.startsWith(prefix))) {
532
+ if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => filePath.startsWith(prefix)) && !isFrameworkPackPath(filePath)) {
522
533
  collectClassDeps(source);
534
+ collectProviderFormErrors(source);
523
535
  collectGrpcBindings(source);
524
536
  toolCodegenSources.push({ filePath, sourceFile: source });
525
537
  }
@@ -575,6 +587,72 @@ function classifyToolWarnings(toolWarnings: readonly string[]): void {
575
587
  }
576
588
  }
577
589
 
590
+ /**
591
+ * The wiring of one framework pack: constructor dependencies of the classes
592
+ * its modules register, written to `<pack>/generated/deps.ts`. The pack's
593
+ * `index.ts` imports that file, so the package carries its own wiring and
594
+ * framework modules register `singleton(IContract, Implementation)` like an
595
+ * application does.
596
+ */
597
+ function generateFrameworkPack(pack: string): void {
598
+ const files = new Set([...programFiles.keys()].filter((file) => file.startsWith(`${pack}/`)));
599
+ if (!files.has(`${pack}/index.ts`)) throw new Error(`BAZIS_CODEGEN_CONFIG_INVALID: framework pack ${pack} has no index.ts`);
600
+ resetTargetAnalysis(files, `${pack}/generated`);
601
+ // Tokens, configs and classes the pack depends on may live anywhere in the framework.
602
+ for (const source of programFiles.values()) collectTokenDescriptions(source);
603
+ for (const { source } of sourceEntries) collectDiClassRegistrations(source);
604
+ for (const { source } of sourceEntries) collectClassDeps(source);
605
+ for (const { source } of sourceEntries) collectProviderFormErrors(source);
606
+ resolveConfigViewDeps();
607
+ validateDepsAgainstKnownTokens();
608
+ failOnFatalErrors();
609
+ stageWrite(`${pack}/generated/deps.ts`, renderFrameworkPackDeps(pack));
610
+ for (const warning of warnings) reportWarning(warning);
611
+ reportInfo(`[di:generate] framework pack=${pack} deps=${classDeps.size}`);
612
+ }
613
+
614
+ function renderFrameworkPackDeps(pack: string): string {
615
+ const generatedDir = `${pack}/generated`;
616
+ const entries = [...classDeps.entries()].sort(([left], [right]) =>
617
+ left.name!.text.localeCompare(right.name!.text)
618
+ || left.getSourceFile().fileName.localeCompare(right.getSourceFile().fileName)
619
+ || left.pos - right.pos);
620
+ const references = new Map<ts.ClassDeclaration, string>();
621
+ for (const [declaration] of entries) references.set(declaration, declaration.name!.text);
622
+ for (const [, dependencies] of entries) {
623
+ for (const dependency of dependencies) {
624
+ if (dependency.target && !references.has(dependency.target)) references.set(dependency.target, dependency.target.name!.text);
625
+ }
626
+ }
627
+ const di = (file: string) => JSON.stringify(toModuleSpecifierFrom(generatedDir, `${FRAMEWORK_SOURCE}/core/di/${file}`));
628
+ const lines = ["// This file is auto-generated by `bun run di:generate` (framework.packs of bazis.config.json).", "// Do not edit manually."];
629
+ const helpers = ["lazyDependency", "optionalDependency"].filter((helper) => entries.some(([, deps]) =>
630
+ deps.some((dep) => dep.target && (helper === "lazyDependency" ? dep.lazy : dep.optional))));
631
+ if (helpers.length > 0) lines.push(`import { ${helpers.join(", ")} } from ${di("provider/index.ts")};`);
632
+ lines.push(`import { registerFrameworkClassDeps } from ${di("module/autoDeps.ts")};`);
633
+ const byFile = new Map<string, string[]>();
634
+ for (const [declaration, reference] of references) {
635
+ const name = dependencyClassExportName(declaration);
636
+ if (!name) throw new Error(`BAZIS_DI_CLASS_UNIMPORTABLE: ${sourceLocation(declaration)}`);
637
+ const specifier = toModuleSpecifierFrom(generatedDir, normalizeProjectPath(declaration.getSourceFile().fileName));
638
+ byFile.set(specifier, [...(byFile.get(specifier) ?? []), name === reference ? name : `${name} as ${reference}`]);
639
+ }
640
+ for (const [specifier, names] of [...byFile].sort(([left], [right]) => left.localeCompare(right))) {
641
+ lines.push(`import { ${names.sort().join(", ")} } from ${JSON.stringify(specifier)};`);
642
+ }
643
+ lines.push("", "registerFrameworkClassDeps([");
644
+ for (const [declaration, dependencies] of entries) {
645
+ const values = dependencies.map((dependency) => {
646
+ if (!dependency.target) return JSON.stringify(dependency.optional ? `optional:${dependency.name}` : dependency.name);
647
+ const reference = references.get(dependency.target)!;
648
+ return dependency.lazy ? `lazyDependency(${reference})` : dependency.optional ? `optionalDependency(${reference})` : reference;
649
+ });
650
+ lines.push(` [${references.get(declaration)}, [${values.join(", ")}]],`);
651
+ }
652
+ lines.push("]);");
653
+ return `${lines.join("\n")}\n`;
654
+ }
655
+
578
656
  function failOnFatalErrors(): void {
579
657
  if (fatalErrors.length === 0) {
580
658
  return;
@@ -797,7 +875,7 @@ function resolveConfigViewDeps(): void {
797
875
  if (needsInferredDiDeps(declaration)) {
798
876
  fatalErrors.push(prefixes.length === 0
799
877
  ? `BAZIS_DI_CONFIG_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", but no defineConfig<${typeName}>(...) declaration was found. Declare the configuration with an explicit type argument, for example defineConfig<${typeName}>("name", { default: {...} }).`
800
- : `BAZIS_DI_CONFIG_AMBIGUOUS: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", which matches several declarations: ${prefixes.map((prefix) => `"${prefix}"`).join(", ")}. Give each declaration its own type, or pass the token explicitly: scoped(Service, Service, [config.token]).`);
878
+ : `BAZIS_DI_CONFIG_AMBIGUOUS: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", which matches several declarations: ${prefixes.map((prefix) => `"${prefix}"`).join(", ")}. Give each declaration its own type.`);
801
879
  }
802
880
  changed = false;
803
881
  break;
@@ -833,7 +911,7 @@ function validateDepsAgainstKnownTokens(): void {
833
911
  // would fail later, far from the cause. Other classes need no entry.
834
912
  if (needsInferredDiDeps(declaration)) {
835
913
  fatalErrors.push(declaration.members.some(ts.isConstructorDeclaration)
836
- ? `BAZIS_DI_DEPENDENCY_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "${depName}", which is neither a DI token (createToken) nor a class known to codegen. Register it, import it from the framework package, or bind the class with an explicit factory.`
914
+ ? `BAZIS_DI_DEPENDENCY_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "${depName}", which is neither a DI token (createToken) nor a class known to codegen. Register it in a module or import it from the framework package.`
837
915
  : `BAZIS_DI_CONSTRUCTOR_UNRESOLVED: ${sourceLocation(declaration)}: inherited constructor dependency "${depName}" has no known DI token.`);
838
916
  }
839
917
  break;
@@ -847,7 +925,7 @@ function collectClassDeps(source: ts.SourceFile): void {
847
925
  const result = readConstructorTypeNames(node);
848
926
  if (result.unsupportedParameter !== undefined && needsInferredDiDeps(node)) {
849
927
  const { index, name, type } = result.unsupportedParameter;
850
- fatalErrors.push(`BAZIS_DI_DEPENDENCY_UNKNOWN: ${sourceLocation(node)}: constructor parameter ${index + 1} "${name}" of "${node.name.text}" has type "${type}", which cannot be injected: DI resolves dependencies by class, contract or token, and a plain type does not say which value to inject. Declare a token with the same name as a type alias (export type Prefix = string; export const Prefix = createToken<Prefix>("Prefix")) and register a value, or pass the deps explicitly: scoped(${node.name.text}, ${node.name.text}, [TOKEN] as const).`);
928
+ fatalErrors.push(`BAZIS_DI_DEPENDENCY_UNKNOWN: ${sourceLocation(node)}: constructor parameter ${index + 1} "${name}" of "${node.name.text}" has type "${type}", which cannot be injected: DI resolves dependencies by class, contract or token, and a plain type does not say which value to inject. Declare a token with the same name as a type alias (export type Prefix = string; export const Prefix = createToken<Prefix>("Prefix")), register a value with singletonValue(Prefix, "…") and type the parameter with it: prefix: Prefix.`);
851
929
  }
852
930
  if (result.unsupportedInheritedParameter !== undefined && needsInferredDiDeps(node)) {
853
931
  fatalErrors.push(`BAZIS_DI_CONSTRUCTOR_UNRESOLVED: ${sourceLocation(node)}: cannot infer inherited constructor parameter "${result.unsupportedInheritedParameter}" for "${node.name.text}". The parameter needs a runtime DI token or an explicit value/factory binding.`);
@@ -951,6 +1029,96 @@ function collectDiClassRegistrations(source: ts.SourceFile): void {
951
1029
  visit(source);
952
1030
  }
953
1031
 
1032
+ /**
1033
+ * A module registers its services only in the agreed forms (docs/guide
1034
+ * fundamentals/dependency-injection.md): a class under its contract, a ready
1035
+ * value, an asynchronously created resource, event subscribers. Constructor dependencies come from codegen, so a
1036
+ * dependency list, a factory or a low-level `DI.*` provider is a fatal error
1037
+ * with the agreed form in the message.
1038
+ */
1039
+ function collectProviderFormErrors(source: ts.SourceFile): void {
1040
+ // Inside the function: the script runs before later top-level constants are initialized.
1041
+ const AGREED_PROVIDERS = "scoped(IService, Service), singleton(IService, Service), transient(IService, Service) (or the class alone), singletonValue(TOKEN, value), singletonAsyncFactory(TOKEN, [deps], async factory) or ...withEventSubscribers(Class)";
1042
+ const CLASS_LISTS = ["controllers", "grpcControllers", "gateways", "background", "tools", "grpcClients", "httpClients"];
1043
+ const callee = (call: ts.CallExpression): { file?: string; name?: string } => {
1044
+ let symbol = checker.getSymbolAtLocation(call.expression);
1045
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
1046
+ const declaration = symbol?.declarations?.find((entry) => ts.isFunctionDeclaration(entry) || ts.isMethodDeclaration(entry));
1047
+ const name = declaration && "name" in declaration && declaration.name && ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
1048
+ return { file: declaration?.getSourceFile().fileName.replace(/\\/g, "/"), name };
1049
+ };
1050
+ const resolved = <T extends ts.Node>(expression: ts.Expression, is: (node: ts.Node) => node is T): T | undefined => {
1051
+ let current: ts.Expression = expression;
1052
+ for (let depth = 0; depth < 5; depth++) {
1053
+ if (is(current)) return current;
1054
+ if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current)) { current = current.expression; continue; }
1055
+ let symbol = checker.getSymbolAtLocation(current);
1056
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
1057
+ const declaration = symbol?.valueDeclaration;
1058
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return undefined;
1059
+ current = declaration.initializer;
1060
+ }
1061
+ return undefined;
1062
+ };
1063
+ const text = (node: ts.Node): string => {
1064
+ const value = node.getText().replace(/\s+/g, " ");
1065
+ return value.length > 90 ? `${value.slice(0, 87)}...` : value;
1066
+ };
1067
+ const checkProvider = (module: string, element: ts.Expression): void => {
1068
+ if (ts.isSpreadElement(element)) {
1069
+ const call = resolved(element.expression, ts.isCallExpression);
1070
+ const target = call && callee(call);
1071
+ if (target?.name === "withEventSubscribers" && target.file?.endsWith("/core/kernel/events/eventSubscribers.ts")) return;
1072
+ fatalErrors.push(`BAZIS_DI_PROVIDER_FORM: ${sourceLocation(element)}: providers of ${module}: "${text(element)}" is not an agreed registration. Agreed forms: ${AGREED_PROVIDERS}.`);
1073
+ return;
1074
+ }
1075
+ const call = ts.isCallExpression(element) ? element : undefined;
1076
+ const target = call && callee(call);
1077
+ if (call && target?.file?.endsWith("/core/di/module/shortcuts.ts")) {
1078
+ if (["scoped", "singleton", "transient"].includes(target.name ?? "")) {
1079
+ if (call.arguments.length <= 2) return;
1080
+ fatalErrors.push(`BAZIS_DI_PROVIDER_FORM: ${sourceLocation(element)}: providers of ${module}: "${text(element)}" passes a dependency list. Remove it: codegen reads the constructor. Agreed forms: ${AGREED_PROVIDERS}.`);
1081
+ return;
1082
+ }
1083
+ if (target.name === "singletonValue" && call.arguments.length === 2) return;
1084
+ // A resource that is created asynchronously (a connection): created at startup.
1085
+ if (target.name === "singletonAsyncFactory" && call.arguments.length === 3) return;
1086
+ }
1087
+ fatalErrors.push(`BAZIS_DI_PROVIDER_FORM: ${sourceLocation(element)}: providers of ${module}: "${text(element)}" is not an agreed registration. A computed service is a class with a constructor: scoped(IService, Service). Agreed forms: ${AGREED_PROVIDERS}.`);
1088
+ };
1089
+ const checkModule = (module: string, argument: ts.Expression | undefined): void => {
1090
+ const object = argument && resolved(argument, ts.isObjectLiteralExpression);
1091
+ if (!object) return;
1092
+ for (const property of object.properties) {
1093
+ if (!ts.isPropertyAssignment(property)) continue;
1094
+ const field = property.name.getText().replace(/["']/g, "");
1095
+ if (field !== "providers" && !CLASS_LISTS.includes(field)) continue;
1096
+ const list = resolved(property.initializer, ts.isArrayLiteralExpression);
1097
+ if (!list) {
1098
+ fatalErrors.push(`BAZIS_DI_PROVIDER_FORM: ${sourceLocation(property)}: ${field} of ${module} must be an array written in the module.`);
1099
+ continue;
1100
+ }
1101
+ for (const element of list.elements) {
1102
+ if (field === "providers") checkProvider(module, element);
1103
+ else if (!ts.isIdentifier(element) && !ts.isPropertyAccessExpression(element)) {
1104
+ fatalErrors.push(`BAZIS_DI_PROVIDER_FORM: ${sourceLocation(element)}: ${field} of ${module}: "${text(element)}" — list the class itself; its constructor dependencies come from codegen.`);
1105
+ }
1106
+ }
1107
+ }
1108
+ };
1109
+ const visit = (node: ts.Node): void => {
1110
+ if (ts.isClassDeclaration(node)) {
1111
+ for (const decorator of ts.getDecorators(node) ?? []) {
1112
+ if (!ts.isCallExpression(decorator.expression)) continue;
1113
+ const target = callee(decorator.expression);
1114
+ if (target.name === "Module" && target.file?.endsWith("/core/di/module/Module.ts")) checkModule(node.name?.text ?? "<anonymous module>", decorator.expression.arguments[0]);
1115
+ }
1116
+ }
1117
+ ts.forEachChild(node, visit);
1118
+ };
1119
+ visit(source);
1120
+ }
1121
+
954
1122
  function hasStaticClassDeps(node: ts.ClassDeclaration): boolean {
955
1123
  if (!node.name) return false;
956
1124
  const symbol = checker.getSymbolAtLocation(node.name);
@@ -1,9 +1,11 @@
1
- import { DI, HOSTED_SERVICE, Module, SERVICE_PROVIDER, scoped } from "../../di";
1
+ import { AccessModule } from "../../access";
2
+ import { HOSTED_SERVICE, Module, scoped, singleton } from "../../di";
2
3
  import { SystemWorkflowClock, WorkflowClock } from "../contracts/WorkflowClock";
3
4
  import { WorkflowTools } from "../internal/WorkflowTools";
5
+ import { definitionsPermissions } from "./access/Definitions.permissions";
6
+ import { ProcessReferences } from "./access/Process.references";
4
7
  import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
5
8
  import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
6
- import { codeProcessesOf } from "./code/codeProcesses";
7
9
  import { ProcessesController } from "./http/Processes.controller";
8
10
  import { DefinitionsDbContext, definitionsStore } from "./model/Definitions.dbContext";
9
11
  import { WfProcess } from "./model/WfProcess.model";
@@ -13,20 +15,22 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
13
15
 
14
16
  /**
15
17
  * The process catalog: drafts and published versions, and the processes
16
- * declared in code ([passport](MODULE.md)). Dependencies are explicit: the
17
- * framework's classes are not covered by the application's codegen.
18
+ * declared in code ([passport](MODULE.md)).
18
19
  */
19
20
  @Module({
21
+ imports: [AccessModule],
22
+ permissions: definitionsPermissions,
23
+ references: { "workflow.process": ProcessReferences },
20
24
  ormBazis: { context: DefinitionsDbContext, entities: [WfProcess, WfVersion], ownedStore: definitionsStore, registerRepositories: false },
21
25
  providers: [
22
- DI.singleton(DI.factoryProvider(WorkflowClock, [], () => new SystemWorkflowClock())),
23
- DI.singleton(DI.factoryProviderWithResolver(WorkflowTools, [], (resolver) => new WorkflowTools(resolver.resolve(SERVICE_PROVIDER)))),
24
- DI.singleton(DI.factoryProviderWithResolver(CodeProcessCatalog, [], (resolver) => new CodeProcessCatalog(codeProcessesOf(resolver.resolve(SERVICE_PROVIDER))))),
25
- scoped(WorkflowDefinitions, WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock] as const),
26
- DI.scoped(DI.factoryProviderWithResolver(IWorkflowDefinitions, [], (resolver) => resolver.resolve(WorkflowDefinitions))),
27
- DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => new CodeProcessPublisher(resolver.resolve(SERVICE_PROVIDER)))),
26
+ singleton(WorkflowClock, SystemWorkflowClock),
27
+ singleton(WorkflowTools),
28
+ singleton(CodeProcessCatalog),
29
+ scoped(IWorkflowDefinitions, WorkflowDefinitions),
30
+ scoped(ProcessReferences),
31
+ singleton(HOSTED_SERVICE, CodeProcessPublisher),
28
32
  ],
29
- controllers: [DI.bindDeps(ProcessesController, IWorkflowDefinitions)],
33
+ controllers: [ProcessesController],
30
34
  exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock],
31
35
  })
32
36
  export class WorkflowDefinitionsModule {}
@@ -0,0 +1,23 @@
1
+ import { definePermissions } from "../../../access";
2
+
3
+ const process = { ref: "workflow.process", column: "key" };
4
+
5
+ /**
6
+ * The rights of the process catalog (WORKFLOW-002 §6.2). A process is one per
7
+ * application, so these have no partition until `WorkflowPartitions` connects
8
+ * processes to partitions (stage 4).
9
+ */
10
+ export const definitionsPermissions = definePermissions({
11
+ "workflow.processes.read": {
12
+ title: { en: "See processes", ru: "Видеть процессы" },
13
+ on: { process },
14
+ },
15
+ "workflow.processes.manage": {
16
+ title: { en: "Edit processes", ru: "Редактировать процессы" },
17
+ on: { process },
18
+ },
19
+ "workflow.processes.publish": {
20
+ title: { en: "Publish processes", ru: "Публиковать процессы" },
21
+ on: { process },
22
+ },
23
+ });
@@ -0,0 +1,27 @@
1
+ import { References, type ReferenceItem, type ReferencePage } from "../../../access";
2
+ import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
3
+
4
+ /** Reference kind `workflow.process`: what an administrator picks in a condition. */
5
+ export class ProcessReferences extends References {
6
+ constructor(private readonly db: DefinitionsDbContext) {
7
+ super();
8
+ }
9
+
10
+ async exists(ids: readonly string[]): Promise<ReadonlySet<string>> {
11
+ return new Set((await this.describe(ids)).map((item) => item.id));
12
+ }
13
+
14
+ async describe(ids: readonly string[]): Promise<readonly ReferenceItem[]> {
15
+ if (ids.length === 0) return [];
16
+ const rows = await this.db.processes.asNoTracking().where((p) => p.key.in([...ids])).toList();
17
+ return rows.map((row) => ({ id: row.key, title: row.title }));
18
+ }
19
+
20
+ async search(text: string, _context: unknown, page: ReferencePage): Promise<readonly ReferenceItem[]> {
21
+ const found = text.trim();
22
+ let source = this.db.processes.asNoTracking();
23
+ if (found !== "") source = source.where((p) => p.key.contains(found, { ignoreCase: true }).or(p.title.contains(found, { ignoreCase: true })));
24
+ const rows = await source.orderBy((p) => p.key).skip(page.offset).take(page.limit).toList();
25
+ return rows.map((row) => ({ id: row.key, title: row.title }));
26
+ }
27
+ }
@@ -1,11 +1,12 @@
1
- import type { OwnedCodeProcess } from "./codeProcesses";
1
+ import type { ServiceProvider } from "../../../di";
2
+ import { codeProcessesOf, type OwnedCodeProcess } from "./codeProcesses";
2
3
 
3
4
  /** The processes declared in the application's modules, by key: which module owns a key. */
4
5
  export class CodeProcessCatalog {
5
6
  private readonly byKey: ReadonlyMap<string, OwnedCodeProcess>;
6
7
 
7
- constructor(entries: readonly OwnedCodeProcess[]) {
8
- this.byKey = new Map([...entries].sort((a, b) => a.process.key.localeCompare(b.process.key)).map((entry) => [entry.process.key, entry]));
8
+ constructor(services: ServiceProvider) {
9
+ this.byKey = new Map([...codeProcessesOf(services)].sort((a, b) => a.process.key.localeCompare(b.process.key)).map((entry) => [entry.process.key, entry]));
9
10
  }
10
11
 
11
12
  /** The module whose `processes` declare `key`; `undefined` for a process of the catalog. */
@@ -2,7 +2,7 @@ import { markConfigurationError, type ServiceProvider } from "../../../di";
2
2
  import type { HostedService } from "../../../di/extensions/hosted-service";
3
3
  import { LOGGER } from "../../../kernel";
4
4
  import { WorkflowTools } from "../../internal/WorkflowTools";
5
- import { WorkflowDefinitions } from "../services/WorkflowDefinitions.service";
5
+ import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
6
6
  import { CodeProcessCatalog } from "./CodeProcessCatalog";
7
7
  import { definitionProblems, WorkflowSetupError } from "./codeProcesses";
8
8
 
@@ -30,7 +30,7 @@ export class CodeProcessPublisher implements HostedService {
30
30
  }
31
31
  const scope = this.services.createScope();
32
32
  try {
33
- const results = await scope.resolve(WorkflowDefinitions).publishCode(entries);
33
+ const results = await scope.resolve(IWorkflowDefinitions).publishCode(entries);
34
34
  this.services.tryResolve(LOGGER)?.info("workflow processes from code", {
35
35
  published: results.filter((result) => result.published).map((result) => `${result.key} v${result.version}`),
36
36
  unchanged: results.filter((result) => !result.published).map((result) => result.key),
@@ -1,28 +1,31 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
- import { Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
3
- import { authorize, body, optionalInteger, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
2
+ import { can } from "../../../access";
3
+ import { Authorize, Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
4
+ import { body, callerOf, optionalInteger, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
4
5
  import { WorkflowError } from "../../contracts/WorkflowError";
5
6
  import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
6
7
 
7
- /** The process catalog over HTTP (WORKFLOW-001 §3); rights come from `WorkflowAccess`. */
8
+ /** The process catalog over HTTP (WORKFLOW-001 §3); rights are `workflow.processes.*` (WORKFLOW-002 §6.2). */
8
9
  @Controller("workflow")
9
10
  export class ProcessesController {
10
11
  constructor(private readonly definitions: IWorkflowDefinitions) {}
11
12
 
12
13
  @Get("processes")
14
+ @Authorize(can("workflow.processes.read"))
13
15
  async list(ctx: HttpContext) {
14
- await authorize(ctx, "processes.read");
16
+ const { access } = await callerOf(ctx);
15
17
  const archived = ctx.query("archived");
16
18
  if (archived !== undefined && archived !== "true" && archived !== "false") throw new WorkflowError("invalid_input", "archived must be true or false");
17
- const page = await this.definitions.list({ ...pagingQuery(ctx), archived: archived === undefined ? undefined : archived === "true", search: optionalString(ctx.query("search"), "search") });
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") });
18
20
  return { data: page.items, meta: { total: page.total } };
19
21
  }
20
22
 
21
23
  @Post("processes")
24
+ @Authorize(can("workflow.processes.manage"))
22
25
  async create(ctx: HttpContext) {
23
26
  const request = await body(ctx);
24
27
  const key = requiredString(request.key, "key", 64);
25
- const actor = await authorize(ctx, "processes.manage", { processKey: key });
28
+ const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
26
29
  const process = await this.definitions.create({
27
30
  key,
28
31
  title: requiredString(request.title, "title"),
@@ -33,17 +36,19 @@ export class ProcessesController {
33
36
  }
34
37
 
35
38
  @Get("processes/:key")
39
+ @Authorize(can("workflow.processes.read"))
36
40
  async get(ctx: HttpContext) {
37
41
  const key = param(ctx, "key");
38
- await authorize(ctx, "processes.read", { processKey: key });
42
+ await callerOf(ctx, "workflow.processes.read", { process: key });
39
43
  return await this.definitions.get(key);
40
44
  }
41
45
 
42
46
  @Patch("processes/:key")
47
+ @Authorize(can("workflow.processes.manage"))
43
48
  async update(ctx: HttpContext) {
44
49
  const key = param(ctx, "key");
45
50
  const request = await body(ctx);
46
- const actor = await authorize(ctx, "processes.manage", { processKey: key });
51
+ const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
47
52
  if (request.archived !== undefined && typeof request.archived !== "boolean") throw new WorkflowError("invalid_input", "archived must be a boolean");
48
53
  return await this.definitions.update(key, {
49
54
  ...(request.title === undefined ? {} : { title: requiredString(request.title, "title") }),
@@ -54,40 +59,44 @@ export class ProcessesController {
54
59
  }
55
60
 
56
61
  @Put("processes/:key/draft")
62
+ @Authorize(can("workflow.processes.manage"))
57
63
  async saveDraft(ctx: HttpContext) {
58
64
  const key = param(ctx, "key");
59
65
  const request = await body(ctx);
60
- const actor = await authorize(ctx, "processes.manage", { processKey: key });
66
+ const { actor } = await callerOf(ctx, "workflow.processes.manage", { process: key });
61
67
  return await this.definitions.saveDraft(key, request.document, actor);
62
68
  }
63
69
 
64
70
  @Post("processes/:key/publish")
71
+ @Authorize(can("workflow.processes.publish"))
65
72
  async publish(ctx: HttpContext) {
66
73
  const key = param(ctx, "key");
67
- const actor = await authorize(ctx, "processes.publish", { processKey: key });
74
+ const { actor } = await callerOf(ctx, "workflow.processes.publish", { process: key });
68
75
  const version = await this.definitions.publish(key, actor);
69
76
  return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`, version);
70
77
  }
71
78
 
72
79
  @Get("processes/:key/versions")
80
+ @Authorize(can("workflow.processes.read"))
73
81
  async versions(ctx: HttpContext) {
74
82
  const key = param(ctx, "key");
75
- await authorize(ctx, "processes.read", { processKey: key });
83
+ await callerOf(ctx, "workflow.processes.read", { process: key });
76
84
  return { data: await this.definitions.versions(key) };
77
85
  }
78
86
 
79
87
  @Get("processes/:key/versions/:version")
88
+ @Authorize(can("workflow.processes.read"))
80
89
  async version(ctx: HttpContext) {
81
90
  const key = param(ctx, "key");
82
- await authorize(ctx, "processes.read", { processKey: key });
91
+ await callerOf(ctx, "workflow.processes.read", { process: key });
83
92
  const version = optionalInteger(param(ctx, "version"), "version");
84
93
  if (version === undefined) throw new WorkflowError("invalid_input", "version must be an integer");
85
94
  return await this.definitions.version(key, version);
86
95
  }
87
96
 
88
97
  @Get("tools")
89
- async tools(ctx: HttpContext) {
90
- await authorize(ctx, "processes.read");
98
+ @Authorize(can("workflow.processes.read"))
99
+ async tools() {
91
100
  return { data: this.definitions.tools() };
92
101
  }
93
102
  }
@@ -1,6 +1,8 @@
1
+ import type { AccessFilter } from "../../../access";
1
2
  import type { AgentToolContract } from "../../../agent";
2
3
  import type { ProcessDocument, ProcessIssue } from "../../../../library/workflow";
3
4
  import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
5
+ import type { OwnedCodeProcess } from "../code/codeProcesses";
4
6
 
5
7
  export interface ProcessSummary {
6
8
  readonly key: string;
@@ -52,6 +54,8 @@ export interface UpdateProcess {
52
54
  }
53
55
 
54
56
  export interface ProcessListQuery extends WorkflowPaging {
57
+ /** Which processes the caller may see, from `IAccess`; without it the calling code has decided. */
58
+ readonly access?: AccessFilter;
55
59
  readonly archived?: boolean;
56
60
  /** Part of the key or title, case-insensitive. */
57
61
  readonly search?: string;
@@ -87,4 +91,9 @@ export abstract class IWorkflowDefinitions {
87
91
  abstract retentionPolicies(): Promise<readonly { readonly key: string; readonly retentionDays: number }[]>;
88
92
  /** The Tools a service task may call, with their input and output schemas. */
89
93
  abstract tools(): readonly AgentToolContract[];
94
+ /**
95
+ * Publishes the processes of `@Module({ processes })` as `{ kind: "system", id: "code" }`
96
+ * (WORKFLOW-001 §14): called once at start by the framework, not by the application.
97
+ */
98
+ abstract publishCode(entries: readonly OwnedCodeProcess[]): Promise<readonly { readonly key: string; readonly version: number | null; readonly published: boolean }[]>;
90
99
  }
@@ -1,3 +1,4 @@
1
+ import { grantsToWhere } from "../../../access";
1
2
  import { createHash } from "node:crypto";
2
3
  import { UniqueViolationError } from "../../../orm";
3
4
  import type { AgentToolContract } from "../../../agent";
@@ -38,7 +39,10 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
38
39
 
39
40
  async list(query: ProcessListQuery = {}): Promise<WorkflowPage<ProcessSummary>> {
40
41
  const { limit, offset } = pagingOf(query);
42
+ const where = query.access === undefined ? "all" : grantsToWhere(query.access);
43
+ if (where === "none") return { items: [], total: 0 };
41
44
  let source = this.db.processes.asNoTracking();
45
+ if (where !== "all") source = source.where(() => where);
42
46
  if (query.archived === true) source = source.where((p) => p.archivedAt.isNotNull());
43
47
  if (query.archived === false) source = source.where((p) => p.archivedAt.isNull());
44
48
  if (query.search !== undefined && query.search.trim() !== "") {
@@ -0,0 +1,56 @@
1
+ // This file is auto-generated by `bun run di:generate` (framework.packs of bazis.config.json).
2
+ // Do not edit manually.
3
+ import { registerFrameworkClassDeps } from "../../di/module/autoDeps";
4
+ import { DbContextOptions } from "../../../library/orm/DbContextOptions";
5
+ import { ServiceProvider } from "../../di/ServiceProvider";
6
+ import { EventBus } from "../../kernel/events/EventBus";
7
+ import { WorkflowClock } from "../contracts/WorkflowClock";
8
+ import { ProcessReferences } from "../definitions_module/access/Process.references";
9
+ import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
10
+ import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
11
+ import { ProcessesController } from "../definitions_module/http/Processes.controller";
12
+ import { DefinitionsDbContext } from "../definitions_module/model/Definitions.dbContext";
13
+ import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDefinitions.service";
14
+ import { WorkflowDefinitions } from "../definitions_module/services/WorkflowDefinitions.service";
15
+ import { WorkflowTools } from "../internal/WorkflowTools";
16
+ import { WorkflowRetention } from "../runtime_module/background/WorkflowRetention";
17
+ import { WorkflowWorker } from "../runtime_module/background/WorkflowWorker";
18
+ import { EventsController } from "../runtime_module/http/Events.controller";
19
+ import { InstancesController } from "../runtime_module/http/Instances.controller";
20
+ import { GraphCache } from "../runtime_module/internal/GraphCache";
21
+ import { Schedules } from "../runtime_module/internal/Schedules";
22
+ import { TimerStep } from "../runtime_module/internal/TimerStep";
23
+ import { ToolStep } from "../runtime_module/internal/ToolStep";
24
+ import { WorkflowEngine } from "../runtime_module/internal/WorkflowEngine";
25
+ import { RuntimeDbContext } from "../runtime_module/model/Runtime.dbContext";
26
+ import { IWorkflowRuntime } from "../runtime_module/services/IWorkflowRuntime.service";
27
+ import { WorkflowRuntime } from "../runtime_module/services/WorkflowRuntime.service";
28
+ import { TaskEvents } from "../tasks_module/events/TaskEvents";
29
+ import { TasksController } from "../tasks_module/http/Tasks.controller";
30
+ import { TasksDbContext } from "../tasks_module/model/Tasks.dbContext";
31
+ import { IWorkflowTasks } from "../tasks_module/services/IWorkflowTasks.service";
32
+ import { WorkflowTasks } from "../tasks_module/services/WorkflowTasks.service";
33
+
34
+ registerFrameworkClassDeps([
35
+ [CodeProcessCatalog, [ServiceProvider]],
36
+ [CodeProcessPublisher, [ServiceProvider]],
37
+ [DefinitionsDbContext, [DbContextOptions]],
38
+ [EventsController, [IWorkflowRuntime]],
39
+ [InstancesController, [IWorkflowRuntime]],
40
+ [ProcessesController, [IWorkflowDefinitions]],
41
+ [ProcessReferences, [DefinitionsDbContext]],
42
+ [RuntimeDbContext, [DbContextOptions]],
43
+ [Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
44
+ [TaskEvents, [TasksDbContext]],
45
+ [TasksController, [IWorkflowTasks]],
46
+ [TasksDbContext, [DbContextOptions]],
47
+ [TimerStep, [RuntimeDbContext, WorkflowEngine]],
48
+ [ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools]],
49
+ [WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock]],
50
+ [WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache, EventBus, ServiceProvider, WorkflowClock, "optional:Logger"]],
51
+ [WorkflowRetention, [ServiceProvider, "Config:workflow"]],
52
+ [WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider]],
53
+ [WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock]],
54
+ [WorkflowTools, [ServiceProvider]],
55
+ [WorkflowWorker, [ServiceProvider, "Config:workflow"]],
56
+ ]);