@tailor-platform/sdk 2.0.0-next.10 → 2.0.0-next.11

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 (129) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/dist/{application-miwiXtB4.mjs → application-CM6hKnSK.mjs} +312 -89
  3. package/dist/application-CM6hKnSK.mjs.map +1 -0
  4. package/dist/application-CXNaUNhv.mjs +3 -0
  5. package/dist/cli/commands/deploy/aigateway.d.mts +1 -1
  6. package/dist/cli/commands/deploy/application.d.mts +1 -1
  7. package/dist/cli/commands/deploy/auth.d.mts +1 -1
  8. package/dist/cli/commands/deploy/deploy.d.mts +1 -0
  9. package/dist/cli/commands/deploy/executor.d.mts +1 -1
  10. package/dist/cli/commands/deploy/function-registry.d.mts +1 -2
  11. package/dist/cli/commands/deploy/idp.d.mts +1 -1
  12. package/dist/cli/commands/deploy/label.d.mts +2 -0
  13. package/dist/cli/commands/deploy/resolver.d.mts +1 -1
  14. package/dist/cli/commands/deploy/staticwebsite.d.mts +1 -1
  15. package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
  16. package/dist/cli/commands/deploy/types.d.mts +2 -1
  17. package/dist/cli/commands/deploy/workflow-execution-policy.d.mts +1 -2
  18. package/dist/cli/commands/deploy/workflow.d.mts +1 -1
  19. package/dist/cli/commands/executor/get.d.mts +1 -10
  20. package/dist/cli/commands/executor/jobs.d.mts +1 -38
  21. package/dist/cli/commands/executor/trigger.d.mts +1 -11
  22. package/dist/cli/commands/generate/seed/bundler.d.mts +2 -1
  23. package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +5 -0
  24. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +1 -1
  25. package/dist/cli/commands/workflow/executions.d.mts +1 -13
  26. package/dist/cli/commands/workflow/get.d.mts +1 -10
  27. package/dist/cli/commands/workflow/start.d.mts +2 -14
  28. package/dist/cli/lib.d.mts +7 -11
  29. package/dist/cli/lib.mjs +99 -42
  30. package/dist/cli/lib.mjs.map +1 -1
  31. package/dist/cli/main.mjs +318 -124
  32. package/dist/cli/main.mjs.map +1 -1
  33. package/dist/cli/services/http-adapter/bundler.d.mts +1 -0
  34. package/dist/cli/services/workflow/bundler.d.mts +2 -1
  35. package/dist/cli/shared/client.d.mts +28 -3
  36. package/dist/cli/shared/config-loader.d.mts +11 -4
  37. package/dist/cli/shared/tsconfig-paths-plugin.d.mts +2 -0
  38. package/dist/cli/shared/type-generator.d.mts +4 -3
  39. package/dist/completion/zsh-worker.zsh +82 -22
  40. package/dist/configure/config/types.d.mts +26 -3
  41. package/dist/configure/index.d.mts +2 -2
  42. package/dist/configure/index.mjs +171 -3
  43. package/dist/configure/index.mjs.map +1 -1
  44. package/dist/configure/services/executor/index.d.mts +2 -2
  45. package/dist/configure/services/executor/trigger/event.d.mts +245 -2
  46. package/dist/configure/services/executor/trigger/index.d.mts +3 -3
  47. package/dist/configure/services/idp/index.d.mts +1 -1
  48. package/dist/configure/services/index.d.mts +2 -2
  49. package/dist/configure/services/tailordb/index.d.mts +0 -1
  50. package/dist/configure/services/workflow/execution-policy.d.mts +1 -1
  51. package/dist/configure/services/workflow/execution-policy.types.d.mts +1 -1
  52. package/dist/configure/services/workflow/job.d.mts +11 -2
  53. package/dist/configure/services/workflow/workflow.d.mts +10 -0
  54. package/dist/{globals-DQotNuwK.mjs → globals-TfAVItuK.mjs} +1 -2
  55. package/dist/globals-TfAVItuK.mjs.map +1 -0
  56. package/dist/node-builtins-CmaL2Cbq.mjs +36 -0
  57. package/dist/node-builtins-CmaL2Cbq.mjs.map +1 -0
  58. package/dist/plugin/builtin/enum-constants/index.mjs +115 -2
  59. package/dist/plugin/builtin/enum-constants/index.mjs.map +1 -0
  60. package/dist/plugin/builtin/file-utils/index.mjs +149 -2
  61. package/dist/plugin/builtin/file-utils/index.mjs.map +1 -0
  62. package/dist/plugin/builtin/kysely-type/index.mjs +274 -2
  63. package/dist/plugin/builtin/kysely-type/index.mjs.map +1 -0
  64. package/dist/plugin/builtin/seed/index.mjs +1 -1
  65. package/dist/plugin/builtin/seed/seed-type-processor.d.mts +2 -0
  66. package/dist/{register-ts-hook-BAHMwxfG.mjs → register-ts-hook-LYV7zH-e.mjs} +2252 -797
  67. package/dist/register-ts-hook-LYV7zH-e.mjs.map +1 -0
  68. package/dist/registry-BIGVUrMB.mjs.map +1 -1
  69. package/dist/runtime/index.mjs +1 -1
  70. package/dist/runtime/workflow.d.mts +2 -20
  71. package/dist/runtime/workflow.mjs +1 -1
  72. package/dist/{seed-LUVr7jHL.mjs → seed-B-yckgQY.mjs} +42 -3
  73. package/dist/seed-B-yckgQY.mjs.map +1 -0
  74. package/dist/service-M7OYziQP.mjs +3 -0
  75. package/dist/{service-BNtPsK9Q.mjs → service-k1xy_A5G.mjs} +29 -2
  76. package/dist/{service-BNtPsK9Q.mjs.map → service-k1xy_A5G.mjs.map} +1 -1
  77. package/dist/{service_pb-BwZRiNug.mjs → service_pb-D5--My_M.mjs} +5 -5
  78. package/dist/service_pb-D5--My_M.mjs.map +1 -0
  79. package/dist/{service_pb-wRkxUta6.mjs → service_pb-i8CmnQP4.mjs} +1 -1
  80. package/dist/tailor-proto/src/tailor/v1/application_pb.d.mts +106 -1
  81. package/dist/tailor-proto/src/tailor/v1/executor_resource_pb.d.mts +24 -1
  82. package/dist/tailor-proto/src/tailor/v1/function_pb.d.mts +49 -1
  83. package/dist/tailor-proto/src/tailor/v1/service_pb.d.mts +89 -6
  84. package/dist/tailor-proto/src/tailor/v1/telemetryrouter_resource_pb.d.mts +26 -1
  85. package/dist/tailor-proto/src/tailor/v1/workflow_pb.d.mts +101 -1
  86. package/dist/tailor-proto/src/tailor/v1/workflow_resource_pb.d.mts +22 -1
  87. package/dist/types/executor.generated.d.mts +23 -3
  88. package/dist/types/idp.generated.d.mts +1 -1
  89. package/dist/utils/test/index.d.mts +1 -2
  90. package/dist/utils/test/index.mjs +1 -173
  91. package/dist/utils/test/index.mjs.map +1 -1
  92. package/dist/vitest/environment.mjs +1 -1
  93. package/dist/vitest/index.d.mts +2 -2
  94. package/dist/vitest/index.mjs +10 -52
  95. package/dist/vitest/index.mjs.map +1 -1
  96. package/dist/vitest/mocks/workflow.d.mts +0 -9
  97. package/dist/vitest/setup.mjs +1 -1
  98. package/dist/{workflow-CsaQ9qK-.mjs → workflow-Bamae_Yc.mjs} +1 -9
  99. package/dist/workflow-Bamae_Yc.mjs.map +1 -0
  100. package/dist/{workspace_resource_pb-UGK1SSn_.mjs → workspace_resource_pb-DXnO2AT-.mjs} +7 -7
  101. package/dist/workspace_resource_pb-DXnO2AT-.mjs.map +1 -0
  102. package/docs/cli/tailordb.md +39 -7
  103. package/docs/cli-reference.md +11 -10
  104. package/docs/configuration.md +50 -0
  105. package/docs/migration/v2.md +306 -9
  106. package/docs/runtime.md +1 -1
  107. package/docs/services/executor.md +83 -3
  108. package/docs/services/idp.md +8 -6
  109. package/docs/services/resolver.md +6 -4
  110. package/docs/services/tailordb-migration.md +17 -6
  111. package/docs/services/tailordb.md +6 -4
  112. package/docs/services/workflow.md +33 -3
  113. package/package.json +19 -17
  114. package/dist/application-DArFTTJ9.mjs +0 -3
  115. package/dist/application-miwiXtB4.mjs.map +0 -1
  116. package/dist/enum-constants-j9QBF0cB.mjs +0 -116
  117. package/dist/enum-constants-j9QBF0cB.mjs.map +0 -1
  118. package/dist/file-utils-yEiIS4nJ.mjs +0 -150
  119. package/dist/file-utils-yEiIS4nJ.mjs.map +0 -1
  120. package/dist/globals-DQotNuwK.mjs.map +0 -1
  121. package/dist/kysely-type-DqIRgDYR.mjs +0 -275
  122. package/dist/kysely-type-DqIRgDYR.mjs.map +0 -1
  123. package/dist/register-ts-hook-BAHMwxfG.mjs.map +0 -1
  124. package/dist/seed-LUVr7jHL.mjs.map +0 -1
  125. package/dist/service-D5jn05ZI.mjs +0 -3
  126. package/dist/service_pb-BwZRiNug.mjs.map +0 -1
  127. package/dist/utils/test/mock.d.mts +0 -86
  128. package/dist/workflow-CsaQ9qK-.mjs.map +0 -1
  129. package/dist/workspace_resource_pb-UGK1SSn_.mjs.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { t as db } from "./schema--xYWRGfe.mjs";
2
- import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as Subgraph_ServiceType, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, Ft as CreateApplicationRequestSchema, H as IdPPermissionPermit, Ht as UpdateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, Jt as FilterSchema, K as CreateExecutorExecutorRequestSchema, Kt as ConditionSchema, L as CreateIdPServiceRequestSchema, Lt as UpdateApplicationRequestSchema, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, V as IdPPermissionOperator, Vt as CreateAIGatewayRequestSchema, X as ExecutorTargetType, Y as ExecutorJobStatus, Yt as PageDirection, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as Condition_Operator, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status, zt as ApplicationSchemaUpdateAttemptStatus } from "./workspace_resource_pb-UGK1SSn_.mjs";
2
+ import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as Subgraph_ServiceType, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, Ft as CreateApplicationRequestSchema, H as IdPPermissionPermit, Ht as UpdateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, Jt as FilterSchema, K as CreateExecutorExecutorRequestSchema, Kt as ConditionSchema, L as CreateIdPServiceRequestSchema, Lt as UpdateApplicationRequestSchema, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, V as IdPPermissionOperator, Vt as CreateAIGatewayRequestSchema, X as ExecutorTargetType, Y as ExecutorJobStatus, Yt as PageDirection, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as Condition_Operator, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status, zt as ApplicationSchemaUpdateAttemptStatus } from "./workspace_resource_pb-DXnO2AT-.mjs";
3
3
  import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
4
4
  import { a as parseBoolean, i as symbols, n as logger, o as renderTable, r as styles, t as CIPromptError } from "./logger-CXQq9YIp.mjs";
5
- import { A as defaultPlatformBaseUrl, B as getPlatformBaseUrl, C as getPluginGenerationDependencies, D as hashContent, E as getDistDir, G as normalizeBaseUrl, H as initOperatorClient, I as fetchUserInfo, J as byName, K as rememberPlatformConfigForToken, L as getConsoleBaseUrl, M as fetchAllTolerant, N as fetchMachineUserToken, O as hashFile, P as fetchPaged, R as getOAuth2ClientId, T as createBundleCache, U as isDefaultPlatform, V as initOAuth2Client, W as isNotFoundError, X as LOG_LEVELS, Y as createApplyLimiter, _ as stringifyFunction, f as buildExecutorArgsExpr, g as assertUniqueTailorDBTypeNamesWithExternal, h as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as fetchAll, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildResolverOperationHookExpr, q as resolveStaticWebsiteUrls, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, u as HTTP_METHODS, v as resolveTSConfigWithFallback, w as hasGenerationHooks, y as platformBundleDefinePlugin, z as getOrNull } from "./application-miwiXtB4.mjs";
6
- import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-BNtPsK9Q.mjs";
5
+ import { $ as LOG_LEVELS, A as hashContent, B as getConsoleBaseUrl, D as hasGenerationHooks, E as getPluginGenerationDependencies, F as fetchAllTolerant, G as initOperatorClient, H as getOrNull, I as fetchMachineUserToken, J as normalizeBaseUrl, K as isDefaultPlatform, L as fetchPaged, N as defaultPlatformBaseUrl, O as createBundleCache, P as fetchAll, Q as createApplyLimiter, T as createBundleLog, U as getPlatformBaseUrl, V as getOAuth2ClientId, W as initOAuth2Client, X as resolveStaticWebsiteUrls, Y as rememberPlatformConfigForToken, Z as byName, _ as stringifyFunction, b as resolveTSConfigWithFallback, f as buildExecutorArgsExpr, g as assertUniqueTailorDBTypeNamesWithExternal, h as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as hashFile, k as getDistDir, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildResolverOperationHookExpr, q as isNotFoundError, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, u as HTTP_METHODS, v as createGeneratedEntryResolverPlugin, x as platformBundleDefinePlugin, y as createTsconfigPathsPlugin, z as fetchUserInfo } from "./application-CM6hKnSK.mjs";
6
+ import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-k1xy_A5G.mjs";
7
7
  import { t as multiline } from "./multiline-sfHpTZZK.mjs";
8
8
  import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
9
9
  import { t as userAgent } from "./user-agent-Bgsszb5I.mjs";
@@ -315,6 +315,11 @@ const envValueSchema = z.union([
315
315
  z.number(),
316
316
  z.boolean()
317
317
  ]);
318
+ const allowedSecretValueSchema = z.union([z.string(), z.number()]);
319
+ const envEntrySchema = z.union([envValueSchema, z.strictObject({
320
+ value: allowedSecretValueSchema,
321
+ allowSecretReason: z.string().min(1, { message: "'allowSecretReason' must state why the value is safe to keep in 'env'." })
322
+ })]);
318
323
  const LogLevelSchema = z.enum(LOG_LEVELS);
319
324
  const logLevelSchema = z.string().refine((value) => LogLevelSchema.safeParse(value.trim().toUpperCase()).success, { message: `'logLevel' must be one of: ${LOG_LEVELS.join(", ")}.` });
320
325
  /**
@@ -331,7 +336,7 @@ const logLevelSchema = z.string().refine((value) => LogLevelSchema.safeParse(val
331
336
  const AppConfigSchema = z.strictObject({
332
337
  id: z.uuid({ message: "'id' must be a UUID." }).optional(),
333
338
  name: z.string().min(1, { message: "'name' must be a non-empty string." }),
334
- env: z.record(z.string(), envValueSchema).optional(),
339
+ env: z.record(z.string(), envEntrySchema).optional(),
335
340
  cors: z.array(z.string()).optional(),
336
341
  allowedIpAddresses: z.array(z.string()).optional(),
337
342
  disableIntrospection: z.boolean().optional(),
@@ -1105,6 +1110,213 @@ function loadConfigPath(configPath) {
1105
1110
  return findUpSync(DEFAULT_CONFIG_FILENAME);
1106
1111
  }
1107
1112
 
1113
+ //#endregion
1114
+ //#region src/cli/shared/env-secret-scan.ts
1115
+ const AWS_RULE_ID = "@secretlint/secretlint-rule-aws";
1116
+ const RULE_ID_PREFIX = "@secretlint/secretlint-rule-";
1117
+ /**
1118
+ * Honors `secretlint-disable` comments found in the scanned text. `env` values
1119
+ * are author-controlled text, so leaving this rule in let a value act as a
1120
+ * directive and suppress the findings of the entries after it — defeating the
1121
+ * check without the explicit allowance.
1122
+ *
1123
+ * Matched by id on purpose: the rule declares itself a `scanner`, so it cannot
1124
+ * be told apart by `meta.type`, and the preset's `disabled` option has no
1125
+ * effect on it.
1126
+ */
1127
+ const COMMENT_DIRECTIVE_RULE_ID = "@secretlint/secretlint-rule-filter-comments";
1128
+ const HIGH_ENTROPY_DETECTOR = "high-entropy";
1129
+ /**
1130
+ * The scanned document is assembled in memory, so the path only labels the
1131
+ * source for the scanner and never reaches the filesystem.
1132
+ */
1133
+ const VIRTUAL_SOURCE_PATH = "/tailor-config-env";
1134
+ const ENTROPY_MIN_LENGTH = 20;
1135
+ /**
1136
+ * Bits per character. Above 4.0 no hex-only string can reach the threshold,
1137
+ * which keeps commit hashes, UUIDs and slugs out of the heuristic while still
1138
+ * catching base64/base62 credentials.
1139
+ */
1140
+ const ENTROPY_MIN_BITS_PER_CHAR = 4.2;
1141
+ const TOKEN_LIKE_VALUE = /^[A-Za-z0-9+/=_.-]+$/;
1142
+ /**
1143
+ * A single command can load the same config repeatedly — `setup generate`
1144
+ * derives five separate answers, each through its own `loadConfig` — so results
1145
+ * are memoized on the entries scanned. Without this the scanner would run, and
1146
+ * warnings would print, once per load.
1147
+ *
1148
+ * Keys are digests rather than the serialized entries, so the cache does not
1149
+ * hold the values it was asked to judge for the life of the process.
1150
+ */
1151
+ const scans = /* @__PURE__ */ new Map();
1152
+ /**
1153
+ * Resolve an `env` entry to the value that gets deployed.
1154
+ * @param entry - Entry as written in the config
1155
+ * @returns The entry's value, unwrapped when it carries an `allowSecretReason`
1156
+ */
1157
+ function resolveEnvValue(entry) {
1158
+ return isAllowedSecret(entry) ? entry.value : entry;
1159
+ }
1160
+ /**
1161
+ * Scan `env` values for credentials.
1162
+ *
1163
+ * Entries wrapped as `{ value, allowSecretReason }` are skipped.
1164
+ * Provider-specific matches are reported as errors; values that merely look
1165
+ * randomly generated are reported as warnings.
1166
+ * @param input - `env` entries as written in the config
1167
+ * @returns Provider matches first, then entropy warnings; empty when nothing matched
1168
+ */
1169
+ async function scanEnvForSecrets(input) {
1170
+ const entries = Object.entries(input.env ?? {}).filter(([, entry]) => !isAllowedSecret(entry)).map(([key, entry]) => [key, resolveEnvValue(entry)]);
1171
+ if (entries.length === 0) return [];
1172
+ const findings = await scanWithProviderRules(entries);
1173
+ const alreadyFound = new Set(findings.map((finding) => finding.key));
1174
+ for (const [key, value] of entries) {
1175
+ if (alreadyFound.has(key) || typeof value !== "string") continue;
1176
+ if (looksRandomlyGenerated(value)) findings.push({
1177
+ key,
1178
+ detector: HIGH_ENTROPY_DETECTOR,
1179
+ severity: "warning"
1180
+ });
1181
+ }
1182
+ return findings;
1183
+ }
1184
+ /**
1185
+ * Report secret-looking `env` values, failing when a credential is identified.
1186
+ *
1187
+ * Warnings are logged and do not fail the command. Repeated calls for the same
1188
+ * entries reuse the first result.
1189
+ * @param input - `env` entries as written in the config
1190
+ * @returns Promise that resolves when the entries carry no credential
1191
+ * @throws When a value is identified as a credential
1192
+ */
1193
+ async function assertEnvHasNoSecrets(input) {
1194
+ const key = createHash("sha256").update(JSON.stringify([input.configPath ?? "", input.env ?? {}])).digest("hex");
1195
+ const pending = scans.get(key);
1196
+ if (pending) return pending;
1197
+ const scan = reportEnvSecrets(input);
1198
+ scans.set(key, scan);
1199
+ return scan;
1200
+ }
1201
+ /**
1202
+ * Log warnings and throw on credentials found in `env`.
1203
+ * @param input - `env` entries as written in the config
1204
+ * @throws When a value is identified as a credential
1205
+ */
1206
+ async function reportEnvSecrets(input) {
1207
+ const findings = await scanEnvForSecrets(input);
1208
+ for (const finding of findings) {
1209
+ if (finding.severity !== "warning") continue;
1210
+ logger.warn(`env.${finding.key} looks like a randomly generated credential. If it is one, move it to Secret Manager; otherwise allow it with ${styles.bold("allowSecretReason")}.`);
1211
+ }
1212
+ const errors = findings.filter((finding) => finding.severity === "error");
1213
+ if (errors.length === 0) return;
1214
+ const location = input.configPath ? ` in ${input.configPath}` : "";
1215
+ const list = errors.map((error) => {
1216
+ const matched = error.rule ? `${error.detector}: ${error.rule}` : error.detector;
1217
+ const reference = error.docsUrl ? `\n ${error.docsUrl}` : "";
1218
+ return ` - env.${error.key} (matched ${matched})${reference}`;
1219
+ }).join("\n");
1220
+ const example = errors[0]?.key ?? "KEY";
1221
+ throw new Error(`Secret detected in 'env'${location}:\n${list}\n'env' values are deployed as plaintext and are readable by anyone who can read the application's configuration. Define these with defineSecretManager() instead, and read them through Secret Manager at runtime.
1222
+ If a value is genuinely safe to keep in 'env', allow it where it is defined: ${example}: { value: ..., allowSecretReason: "<why this is safe>" }`);
1223
+ }
1224
+ /**
1225
+ * Run the provider rule set over all entries at once.
1226
+ *
1227
+ * Entries are scanned as one `KEY=value` document because some rules only match
1228
+ * when the key name accompanies the value (`AWS_SECRET_ACCESS_KEY`, database
1229
+ * connection strings). Each finding is mapped back to its key through the
1230
+ * character range the entry occupies, which stays correct for multi-line values.
1231
+ * @param entries - `env` entries to scan
1232
+ * @returns Error-severity findings, deduplicated per key and rule
1233
+ */
1234
+ async function scanWithProviderRules(entries) {
1235
+ const [{ lintSource }, { rules }] = await Promise.all([import("@secretlint/core"), import("@secretlint/secretlint-rule-preset-recommend")]);
1236
+ const ranges = [];
1237
+ let content = "";
1238
+ for (const [key, value] of entries) {
1239
+ const line = `${key}=${String(value)}`;
1240
+ ranges.push({
1241
+ key,
1242
+ start: content.length,
1243
+ end: content.length + line.length
1244
+ });
1245
+ content += `${line}\n`;
1246
+ }
1247
+ const result = await lintSource({
1248
+ source: {
1249
+ filePath: VIRTUAL_SOURCE_PATH,
1250
+ content,
1251
+ contentType: "text"
1252
+ },
1253
+ options: {
1254
+ maskSecrets: true,
1255
+ noPhysicFilePath: true,
1256
+ config: { rules: rules.filter((rule) => rule.meta.id !== COMMENT_DIRECTIVE_RULE_ID).map((rule) => ({
1257
+ id: rule.meta.id,
1258
+ rule,
1259
+ ...rule.meta.id === AWS_RULE_ID ? { options: { enableIDScanRule: true } } : {}
1260
+ })) }
1261
+ }
1262
+ });
1263
+ const findings = [];
1264
+ const seen = /* @__PURE__ */ new Set();
1265
+ for (const message of result.messages) {
1266
+ const entry = ranges.find((range) => message.range[0] >= range.start && message.range[0] < range.end);
1267
+ if (!entry) continue;
1268
+ const detector = message.ruleId.startsWith(RULE_ID_PREFIX) ? message.ruleId.slice(28) : message.ruleId;
1269
+ const dedupeKey = JSON.stringify([
1270
+ entry.key,
1271
+ detector,
1272
+ message.messageId
1273
+ ]);
1274
+ if (seen.has(dedupeKey)) continue;
1275
+ seen.add(dedupeKey);
1276
+ findings.push({
1277
+ key: entry.key,
1278
+ detector,
1279
+ rule: message.messageId,
1280
+ ...message.docsUrl ? { docsUrl: message.docsUrl } : {},
1281
+ severity: "error"
1282
+ });
1283
+ }
1284
+ return findings;
1285
+ }
1286
+ /**
1287
+ * Decide whether an entry carries its own allowance.
1288
+ * @param entry - Entry as written in the config
1289
+ * @returns True when the entry is the `{ value, allowSecretReason }` form
1290
+ */
1291
+ function isAllowedSecret(entry) {
1292
+ return typeof entry === "object";
1293
+ }
1294
+ /**
1295
+ * Decide whether a value carries enough randomness to look generated rather
1296
+ * than authored.
1297
+ * @param value - `env` value to inspect
1298
+ * @returns True when the value is long, token-shaped and high-entropy
1299
+ */
1300
+ function looksRandomlyGenerated(value) {
1301
+ if (value.length < ENTROPY_MIN_LENGTH || !TOKEN_LIKE_VALUE.test(value)) return false;
1302
+ return shannonEntropyPerChar(value) >= ENTROPY_MIN_BITS_PER_CHAR;
1303
+ }
1304
+ /**
1305
+ * Compute Shannon entropy of a string in bits per character.
1306
+ * @param value - String to measure
1307
+ * @returns Bits per character
1308
+ */
1309
+ function shannonEntropyPerChar(value) {
1310
+ const occurrences = /* @__PURE__ */ new Map();
1311
+ for (const char of value) occurrences.set(char, (occurrences.get(char) ?? 0) + 1);
1312
+ let entropy = 0;
1313
+ for (const count of occurrences.values()) {
1314
+ const probability = count / value.length;
1315
+ entropy -= probability * Math.log2(probability);
1316
+ }
1317
+ return entropy;
1318
+ }
1319
+
1108
1320
  //#endregion
1109
1321
  //#region src/cli/shared/mock.ts
1110
1322
  /**
@@ -1152,6 +1364,12 @@ async function loadConfig(configPath, options = {}) {
1152
1364
  const issues = validated.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1153
1365
  throw new Error(`Invalid Tailor config in ${resolvedPath}:\n${issues}`);
1154
1366
  }
1367
+ const appConfig = configModule.default;
1368
+ await assertEnvHasNoSecrets({
1369
+ env: appConfig.env,
1370
+ configPath: resolvedPath
1371
+ });
1372
+ const env = appConfig.env ? Object.fromEntries(Object.entries(appConfig.env).map(([key, entry]) => [key, resolveEnvValue(entry)])) : void 0;
1155
1373
  const allPlugins = [];
1156
1374
  for (const value of Object.values(configModule)) if (Array.isArray(value)) {
1157
1375
  const pluginParsed = value.reduce((acc, item) => {
@@ -1169,6 +1387,7 @@ async function loadConfig(configPath, options = {}) {
1169
1387
  return {
1170
1388
  config: {
1171
1389
  ...configModule.default,
1390
+ ...env ? { env } : {},
1172
1391
  path: resolvedPath
1173
1392
  },
1174
1393
  plugins: allPlugins
@@ -1456,6 +1675,83 @@ function createCacheManager(options) {
1456
1675
  };
1457
1676
  }
1458
1677
 
1678
+ //#endregion
1679
+ //#region src/cli/shared/publish-events.ts
1680
+ /**
1681
+ * How each event-publishing resource is named in user-facing messages.
1682
+ *
1683
+ * Every message naming one of these reads from here, so a conflict error and the
1684
+ * confirmation that lists the same resource cannot drift apart.
1685
+ */
1686
+ const eventSourceLabel = {
1687
+ tailorDBType: (name) => `TailorDB type "${name}"`,
1688
+ resolver: (name) => `Resolver "${name}"`,
1689
+ idpService: (name) => `IdP service "${name}"`,
1690
+ workflow: (name) => `Workflow "${name}"`,
1691
+ workflowJob: (name) => `Job "${name}"`,
1692
+ workflowJobs: (workflowName) => `Jobs of workflow "${workflowName}"`
1693
+ };
1694
+ /** Opt-out conflict details per event-publishing resource. */
1695
+ const publishEventsConflict = {
1696
+ tailorDBType: (name) => ({
1697
+ resource: eventSourceLabel.tailorDBType(name),
1698
+ trigger: "record"
1699
+ }),
1700
+ resolver: (name) => ({
1701
+ resource: eventSourceLabel.resolver(name),
1702
+ trigger: "resolverExecuted"
1703
+ }),
1704
+ idpService: (name) => ({
1705
+ resource: eventSourceLabel.idpService(name),
1706
+ trigger: "idpUser"
1707
+ }),
1708
+ workflow: (name) => ({
1709
+ resource: eventSourceLabel.workflow(name),
1710
+ trigger: "workflowExecution"
1711
+ }),
1712
+ workflowJob: (name) => ({
1713
+ resource: eventSourceLabel.workflowJob(name),
1714
+ trigger: "workflowJobExecution",
1715
+ subscribesTo: "a workflow that runs it"
1716
+ })
1717
+ };
1718
+ /**
1719
+ * Build the error raised when a resource opts out of publishing that a
1720
+ * subscribing executor needs.
1721
+ * @param conflict - Resource, trigger, and subscription target named in the error
1722
+ * @returns Error message
1723
+ */
1724
+ function publishEventsConflictError(conflict) {
1725
+ const { resource, trigger, subscribesTo = "it" } = conflict;
1726
+ return `${resource} has "publishEvents: false", but executors with ${trigger} triggers subscribe to ${subscribesTo}. Either remove "publishEvents: false" or remove the matching executor triggers.`;
1727
+ }
1728
+ /**
1729
+ * Reject an opt-out that a subscribing executor contradicts.
1730
+ *
1731
+ * Separate from {@link resolvePublishEvents} so a planner can reject the whole
1732
+ * config before issuing any request, rather than partway through.
1733
+ * @param params - Declared value, subscriber presence, and conflict error details
1734
+ */
1735
+ function assertNoPublishEventsConflict(params) {
1736
+ const { explicit, subscribed, conflict } = params;
1737
+ if (explicit === false && subscribed) throw new Error(publishEventsConflictError(conflict));
1738
+ }
1739
+ /**
1740
+ * Resolve whether a resource publishes events.
1741
+ *
1742
+ * An unset value is recomputed from the executors taking part in the run, so
1743
+ * removing the last subscribing trigger turns publishing back off. A `deploy`
1744
+ * covering several configs counts a subscriber in any of them, so a resource
1745
+ * shared across configs needs `publishEvents: true` on the resource itself only
1746
+ * to keep publishing when the subscribing config is deployed on its own.
1747
+ * @param params - Declared value, subscriber presence, and conflict error details
1748
+ * @returns Whether the resource publishes events
1749
+ */
1750
+ function resolvePublishEvents(params) {
1751
+ assertNoPublishEventsConflict(params);
1752
+ return params.explicit ?? params.subscribed;
1753
+ }
1754
+
1459
1755
  //#endregion
1460
1756
  //#region src/cli/shared/type-generator.ts
1461
1757
  /**
@@ -1467,6 +1763,7 @@ function createCacheManager(options) {
1467
1763
  function extractAttributesFromConfig(config) {
1468
1764
  return collectAttributesFromConfig(config);
1469
1765
  }
1766
+ const isValidIdentifier = (s) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(s);
1470
1767
  /**
1471
1768
  * Generate the contents of the user-defined type definition file.
1472
1769
  * @param attributes - Attribute configuration
@@ -1486,13 +1783,10 @@ ${attributeFields}
1486
1783
  const listBody = `{
1487
1784
  __tuple?: ${attributeList ? `[${attributeList.map(() => "string").join(", ")}]` : "[]"};
1488
1785
  }`;
1489
- const envFields = env ? Object.entries(env).map(([key, value]) => {
1490
- return ` ${key}: ${typeof value === "string" ? `"${value}"` : String(value)};`;
1491
- }).join("\n") : "";
1786
+ const envFields = env ? Object.entries(env).map(([key, value]) => ` ${isValidIdentifier(key) ? key : JSON.stringify(key)}: ${typeof value};`).join("\n") : "";
1492
1787
  const envBody = !env || Object.keys(env).length === 0 ? "{}" : `{
1493
1788
  ${envFields}
1494
1789
  }`;
1495
- const isValidIdentifier = (s) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(s);
1496
1790
  const machineUserFields = machineUserNames?.length ? machineUserNames.map((name) => ` ${isValidIdentifier(name) ? name : JSON.stringify(name)}: true;`).join("\n") : "";
1497
1791
  const machineUserBody = !machineUserNames || machineUserNames.length === 0 ? "{}" : `{
1498
1792
  ${machineUserFields}
@@ -2132,6 +2426,42 @@ function trnPrefix(workspaceId) {
2132
2426
  function resourceTrn(workspaceId, kind, name) {
2133
2427
  return `${trnPrefix(workspaceId)}:${kind}:${name}`;
2134
2428
  }
2429
+ /**
2430
+ * Build the TRN for a resource nested inside another.
2431
+ *
2432
+ * The platform reads everything after the workspace id as alternating key/value
2433
+ * pairs and matches the pair list against one resource type, so a nested
2434
+ * resource is the parent's pair followed by its own. A TailorDB type is
2435
+ * `tailordb:<namespace>:type:<name>`, distinct from the namespace's own
2436
+ * `tailordb:<namespace>`.
2437
+ * @param workspaceId - Workspace ID
2438
+ * @param parent - Parent kind and name, e.g. the namespace holding the resource
2439
+ * @param child - Nested key and name, e.g. `["type", "Order"]`
2440
+ * @returns Fully-qualified TRN string
2441
+ */
2442
+ function nestedResourceTrn(workspaceId, parent, child) {
2443
+ return `${trnPrefix(workspaceId)}:${parent[0]}:${parent[1]}:${child[0]}:${child[1]}`;
2444
+ }
2445
+ /**
2446
+ * Build the TRN for one TailorDB type.
2447
+ * @param workspaceId - Workspace ID
2448
+ * @param namespace - TailorDB namespace holding the type
2449
+ * @param typeName - Type name
2450
+ * @returns Fully-qualified TRN string
2451
+ */
2452
+ function tailorDBTypeTrn(workspaceId, namespace, typeName) {
2453
+ return nestedResourceTrn(workspaceId, ["tailordb", namespace], ["type", typeName]);
2454
+ }
2455
+ /**
2456
+ * Build the TRN for one resolver.
2457
+ * @param workspaceId - Workspace ID
2458
+ * @param namespace - Resolver namespace holding the resolver
2459
+ * @param resolverName - Resolver name
2460
+ * @returns Fully-qualified TRN string
2461
+ */
2462
+ function resolverTrn(workspaceId, namespace, resolverName) {
2463
+ return nestedResourceTrn(workspaceId, ["pipeline", namespace], ["resolver", resolverName]);
2464
+ }
2135
2465
  const sdkNameLabelKey = "sdk-name";
2136
2466
  const sdkVersionLabelKey = "sdk-version";
2137
2467
  const sdkAppIdLabelKey = "sdk-app-id";
@@ -2165,28 +2495,190 @@ function isOwnedByApp(labels, appName, appId) {
2165
2495
  if (labelAppId) return appId !== void 0 && labelAppId === toAppIdLabelValue(appId);
2166
2496
  return labels[sdkNameLabelKey] === appName;
2167
2497
  }
2498
+ const dependedByAppLabelPrefix = "sdk-depended-by-app-";
2499
+ const jobDependedByAppLabelPrefix = "sdk-job-depended-by-app-";
2500
+ function prefixFor(scope) {
2501
+ return scope === "jobs" ? jobDependedByAppLabelPrefix : dependedByAppLabelPrefix;
2502
+ }
2503
+ const RECORDABLE_APP_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
2504
+ /**
2505
+ * Build the label key recording that an application depends on this deploy.
2506
+ * @param appId - Stable id of the dependent application
2507
+ * @param scope - Which of the resource's values the record concerns
2508
+ * @returns Label key, or undefined when the id cannot form a valid key
2509
+ */
2510
+ function dependedByAppLabelKey(appId, scope = "resource") {
2511
+ return RECORDABLE_APP_ID.test(appId) ? `${prefixFor(scope)}${appId}` : void 0;
2512
+ }
2513
+ /**
2514
+ * Read the dependent applications recorded on a resource.
2515
+ *
2516
+ * Only keys {@link dependedByAppLabelKey} could have written are read back, so a
2517
+ * label the SDK could not have produced cannot raise a confirmation prompt that
2518
+ * naming no config in the run makes unanswerable.
2519
+ * @param labels - Labels currently stored on the remote resource
2520
+ * @param scope - Which of the resource's values to read records for
2521
+ * @returns Recorded dependencies, in label-key order
2522
+ */
2523
+ function recordedDependencies(labels, scope = "resource") {
2524
+ if (!labels) return [];
2525
+ const prefix = prefixFor(scope);
2526
+ return Object.entries(labels).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).flatMap(([key, reason]) => {
2527
+ if (!key.startsWith(prefix)) return [];
2528
+ const appId = key.slice(prefix.length);
2529
+ return RECORDABLE_APP_ID.test(appId) ? [{
2530
+ appId,
2531
+ reason
2532
+ }] : [];
2533
+ });
2534
+ }
2535
+ /**
2536
+ * Key one event-publishing resource for the dependency records.
2537
+ *
2538
+ * Planners rebuild the same string from the resource they are applying, so the
2539
+ * shape is the TRN's tail rather than anything new to keep in step.
2540
+ */
2541
+ const eventSourceKey = {
2542
+ tailorDBType: (namespace, typeName) => `tailordb:${namespace}:type:${typeName}`,
2543
+ resolver: (namespace, resolverName) => `pipeline:${namespace}:resolver:${resolverName}`,
2544
+ idp: (name) => `idp:${name}`,
2545
+ workflow: (name) => `workflow:${name}`,
2546
+ workflowJobs: (name) => `workflow:${name}:jobs`
2547
+ };
2548
+ /**
2549
+ * Split the dependency records into the ones to write and the ones to drop.
2550
+ *
2551
+ * A record for an application outside the run appears in neither list, so
2552
+ * {@link writeMetadataLabels} keeps it: it was written when both took part, and
2553
+ * dropping it would lose the only signal that this partial deploy is about to
2554
+ * change how the resource is applied. A record for an application that does take
2555
+ * part is rewritten or dropped, so a dependency that no longer exists disappears
2556
+ * on the next deploy including both.
2557
+ *
2558
+ * A resource that declares `publishEvents` drops every record instead. Nothing
2559
+ * about it depends on which configs the run covers, so a record could only
2560
+ * produce a prompt about a change that cannot happen — and one the owner could
2561
+ * never clear on its own, since clearing needs the dependent to take part.
2562
+ * @param params - Existing labels and the run's dependency inputs
2563
+ * @returns Labels to set and label keys to delete
2564
+ */
2565
+ function dependencyLabelWrite(params) {
2566
+ const { existingLabels, dependentApps, runAppIds, pinned, scope = "resource" } = params;
2567
+ if (pinned) return {
2568
+ labels: {},
2569
+ remove: recordedDependencies(existingLabels, scope).flatMap(({ appId }) => dependedByAppLabelKey(appId, scope) ?? [])
2570
+ };
2571
+ const dependents = dependentApps ?? /* @__PURE__ */ new Map();
2572
+ const inRun = runAppIds ?? /* @__PURE__ */ new Set();
2573
+ const labels = {};
2574
+ for (const [appId, reason] of dependents) {
2575
+ const key = dependedByAppLabelKey(appId, scope);
2576
+ if (!key) throw new Error(`Application id "${appId}" cannot be recorded as a dependency of this deploy. Ids are written by deploy as lowercase UUIDs; restore the generated value in the config's "id".`);
2577
+ labels[key] = reason;
2578
+ }
2579
+ return {
2580
+ labels,
2581
+ remove: recordedDependencies(existingLabels, scope).flatMap(({ appId }) => {
2582
+ if (!inRun.has(appId) || dependents.has(appId)) return [];
2583
+ const key = dependedByAppLabelKey(appId, scope);
2584
+ return key ? [key] : [];
2585
+ })
2586
+ };
2587
+ }
2168
2588
  /**
2169
2589
  * Build metadata request with SDK labels.
2590
+ *
2591
+ * Sets only the SDK's own labels; {@link writeMetadataLabels} keeps the rest
2592
+ * from the labels it reads at write time.
2593
+ *
2594
+ * Without an app id the id label is removed rather than merely left unset,
2595
+ * because {@link isOwnedByApp} decides ownership by that label alone: one left
2596
+ * over from an earlier deploy would keep reading as another app's, and every
2597
+ * later deploy would ask to re-tag the same resources again.
2170
2598
  * @param params - Parameters for building the metadata request
2171
2599
  * @param params.trn - Target TRN
2172
2600
  * @param params.appName - Application name label
2173
2601
  * @param params.appId - Stable application id label (when managed by SDK)
2174
- * @param params.existingLabels - Existing labels to preserve (optional)
2175
2602
  * @returns Metadata request
2176
2603
  */
2177
2604
  async function buildMetaRequest(params) {
2178
- const { trn, appName, appId, existingLabels } = params;
2605
+ const { trn, appName, appId } = params;
2179
2606
  const packageJson = await readPackageJson();
2180
2607
  const sdkVersion = packageJson.version ? `v${packageJson.version.replace(/\./g, "-")}` : "unknown";
2181
2608
  return {
2182
2609
  trn,
2183
2610
  labels: {
2184
- ...existingLabels,
2185
2611
  [sdkNameLabelKey]: appName,
2186
2612
  [sdkVersionLabelKey]: sdkVersion,
2187
2613
  ...appId ? { [sdkAppIdLabelKey]: toAppIdLabelValue(appId) } : {}
2188
- }
2614
+ },
2615
+ remove: appId ? void 0 : [sdkAppIdLabelKey]
2616
+ };
2617
+ }
2618
+ /**
2619
+ * Fold a resource's dependency records into the write already planned for it.
2620
+ *
2621
+ * The reconciliation is attached rather than computed, so it runs against the
2622
+ * labels read at write time. Whatever {@link buildMetaRequest} asked for —
2623
+ * dropping a stale `sdk-app-id`, for instance — still happens alongside it.
2624
+ * @param write - The resource's planned metadata write, mutated in place
2625
+ * @param params - The resource's key and the run's inputs
2626
+ * @returns The same write, for use as an expression
2627
+ */
2628
+ function addDependencyRecords(write, params) {
2629
+ const { key, dependentApps, runAppIds, pinned, scope } = params;
2630
+ write.dependencies = [...write.dependencies ?? [], {
2631
+ dependentApps: dependentApps?.get(key),
2632
+ runAppIds,
2633
+ pinned,
2634
+ scope
2635
+ }];
2636
+ return write;
2637
+ }
2638
+ /**
2639
+ * Write metadata labels as a change against the resource's current labels.
2640
+ *
2641
+ * `SetMetadata` replaces the whole label map, so a request built from labels
2642
+ * read earlier deletes anything written in between. This re-reads immediately
2643
+ * before writing and applies `labels` and `remove` to what it finds, which is
2644
+ * why every label write in the SDK goes through here rather than calling
2645
+ * `client.setMetadata` directly.
2646
+ *
2647
+ * Concurrent writers are still not safe in the strict sense — that needs
2648
+ * server-side conditional writes — but a write can no longer be built from
2649
+ * state this process read at an unrelated point in time.
2650
+ *
2651
+ * A write that changes nothing does nothing — whether the caller requested no
2652
+ * change or the change turns out to already hold. Writing back what was just
2653
+ * read would still overwrite whatever landed in between, for no gain, and the
2654
+ * labels the SDK sets are unchanged on most deploys.
2655
+ * @param client - Operator client instance
2656
+ * @param write - TRN, labels to set, and label keys to delete
2657
+ * @returns Promise that resolves when the labels are written
2658
+ */
2659
+ async function writeMetadataLabels(client, write) {
2660
+ const { trn, labels, remove, dependencies } = write;
2661
+ if (!Object.keys(labels ?? {}).length && !(remove ?? []).length && !dependencies?.length) return;
2662
+ const currentLabels = (await getOrNull(() => client.getMetadata({ trn })))?.metadata?.labels ?? {};
2663
+ const resolved = (dependencies ?? []).map((pending) => dependencyLabelWrite({
2664
+ ...pending,
2665
+ existingLabels: currentLabels
2666
+ }));
2667
+ const merged = {
2668
+ ...currentLabels,
2669
+ ...labels,
2670
+ ...Object.assign({}, ...resolved.map((records) => records.labels))
2189
2671
  };
2672
+ for (const key of [...remove ?? [], ...resolved.flatMap((records) => records.remove)]) delete merged[key];
2673
+ if (areSameLabels(currentLabels, merged)) return;
2674
+ await client.setMetadata({
2675
+ trn,
2676
+ labels: merged
2677
+ });
2678
+ }
2679
+ function areSameLabels(a, b) {
2680
+ const keys = Object.keys(a);
2681
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
2190
2682
  }
2191
2683
 
2192
2684
  //#endregion
@@ -2277,14 +2769,14 @@ async function applyStaticWebsite(client, result, phase = "create-update") {
2277
2769
  if (phase === "create-update") {
2278
2770
  await Promise.all([...changeSet.creates.map(async (create) => {
2279
2771
  await client.createStaticWebsite(create.request);
2280
- await client.setMetadata(create.metaRequest);
2772
+ await writeMetadataLabels(client, create.metaRequest);
2281
2773
  }), ...changeSet.updates.map(async (update) => {
2282
2774
  await client.updateStaticWebsite(update.request);
2283
- await client.setMetadata(update.metaRequest);
2775
+ await writeMetadataLabels(client, update.metaRequest);
2284
2776
  })]);
2285
2777
  await Promise.all([...customDomainChangeSet.creates.map(async (add) => {
2286
2778
  await client.addCustomDomain(add.request);
2287
- await client.setMetadata(add.metaRequest);
2779
+ await writeMetadataLabels(client, add.metaRequest);
2288
2780
  }), ...customDomainChangeSet.deletes.map((del) => client.removeCustomDomain(del.request))]);
2289
2781
  } else await Promise.all(changeSet.deletes.map((del) => client.deleteStaticWebsite(del.request)));
2290
2782
  }
@@ -2478,11 +2970,11 @@ async function applyAIGateway(client, result, phase = "create-update") {
2478
2970
  if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
2479
2971
  create.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(create.request.workspaceId, "request missing workspaceId"), create.request.cors, "AIGateway CORS");
2480
2972
  await client.createAIGateway(create.request);
2481
- await client.setMetadata(create.metaRequest);
2973
+ await writeMetadataLabels(client, create.metaRequest);
2482
2974
  }), ...changeSet.updates.map(async (update) => {
2483
2975
  update.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(update.request.workspaceId, "request missing workspaceId"), update.request.cors, "AIGateway CORS");
2484
2976
  await client.updateAIGateway(update.request);
2485
- await client.setMetadata(update.metaRequest);
2977
+ await writeMetadataLabels(client, update.metaRequest);
2486
2978
  })]);
2487
2979
  else await Promise.all(changeSet.deletes.map((del) => client.deleteAIGateway(del.request)));
2488
2980
  }
@@ -2609,11 +3101,11 @@ async function applyApplication(client, changeSet, phase = "create-update") {
2609
3101
  await Promise.all([...changeSet.creates.map(async (create) => {
2610
3102
  create.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(create.request.workspaceId, "request missing workspaceId"), create.request.cors, "CORS");
2611
3103
  await client.createApplication(create.request);
2612
- await client.setMetadata(create.metaRequest);
3104
+ await writeMetadataLabels(client, create.metaRequest);
2613
3105
  }), ...updates.map(async (update) => {
2614
3106
  update.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(update.request.workspaceId, "request missing workspaceId"), update.request.cors, "CORS");
2615
3107
  await client.updateApplication(update.request);
2616
- await client.setMetadata(update.metaRequest);
3108
+ await writeMetadataLabels(client, update.metaRequest);
2617
3109
  })]);
2618
3110
  } else await Promise.all(changeSet.deletes.map(async (del) => {
2619
3111
  await client.deleteApplication(del.request);
@@ -2689,6 +3181,9 @@ function areApplicationsEqual(existing, desired) {
2689
3181
  */
2690
3182
  async function planApplication(context, httpAdapterBuildResult) {
2691
3183
  const { client, workspaceId, application, forRemoval } = context;
3184
+ const conflicts = [];
3185
+ const unmanaged = [];
3186
+ const resourceOwners = /* @__PURE__ */ new Set();
2692
3187
  const changeSet = createChangeSet("Applications");
2693
3188
  const existingApplications = await fetchAllTolerant(async (pageToken, maxPageSize) => {
2694
3189
  const { applications, nextPageToken } = await client.listApplications({
@@ -2701,7 +3196,14 @@ async function planApplication(context, httpAdapterBuildResult) {
2701
3196
  if (forRemoval) {
2702
3197
  const candidates = application.id ? existingApplications : existingApplications.filter((app) => app.name === application.name);
2703
3198
  const owned = await Promise.all(candidates.map(async (app) => {
2704
- return isOwnedByApp(await fetchAppLabels(client, workspaceId, app.name), application.name, application.id) ? app.name : null;
3199
+ const labels = await fetchAppLabels(client, workspaceId, app.name);
3200
+ return trackRemainingResourceOwner({
3201
+ labels,
3202
+ ownerLabel: labels?.["sdk-name"],
3203
+ appName: application.name,
3204
+ appId: application.id,
3205
+ resourceOwners
3206
+ }) ? app.name : null;
2705
3207
  }));
2706
3208
  for (const name of owned) if (name) changeSet.deletes.push({
2707
3209
  name,
@@ -2710,9 +3212,9 @@ async function planApplication(context, httpAdapterBuildResult) {
2710
3212
  applicationName: name
2711
3213
  }
2712
3214
  });
2713
- return changeSet;
3215
+ return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
2714
3216
  }
2715
- if (application.subgraphs.length === 0) return changeSet;
3217
+ if (application.subgraphs.length === 0) return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
2716
3218
  let authNamespace;
2717
3219
  let authIdpConfigName;
2718
3220
  if (application.authService) {
@@ -2743,6 +3245,7 @@ async function planApplication(context, httpAdapterBuildResult) {
2743
3245
  appName: application.name,
2744
3246
  appId: application.id
2745
3247
  });
3248
+ const existingLabels = await fetchAppLabels(client, workspaceId, application.name);
2746
3249
  const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
2747
3250
  const resolvedCors = await resolveStaticWebsiteUrls(client, workspaceId, application.config.cors, "CORS", { expectedLocalNames: expectedLocalWebsites });
2748
3251
  const httpAdapters = buildHttpAdapters(application, httpAdapterBuildResult);
@@ -2773,13 +3276,22 @@ async function planApplication(context, httpAdapterBuildResult) {
2773
3276
  });
2774
3277
  }
2775
3278
  if (existing) {
2776
- const labels = await fetchAppLabels(client, workspaceId, application.name);
3279
+ const owned = trackDesiredResourceOwnership({
3280
+ labels: existingLabels,
3281
+ ownerLabel: existingLabels?.[sdkNameLabelKey],
3282
+ appName: application.name,
3283
+ appId: application.id,
3284
+ resourceType: "Application",
3285
+ resourceName: application.name,
3286
+ conflicts,
3287
+ unmanaged
3288
+ });
2777
3289
  const update = {
2778
3290
  name: application.name,
2779
3291
  request,
2780
3292
  metaRequest
2781
3293
  };
2782
- if (isOwnedByApp(labels, application.name, application.id) && hasMatchingSdkVersion(labels, metaRequest.labels) && areApplicationsEqual(existing, desired)) changeSet.unchanged.push(update);
3294
+ if (owned && hasMatchingSdkVersion(existingLabels, metaRequest.labels) && areApplicationsEqual(existing, desired)) changeSet.unchanged.push(update);
2783
3295
  else {
2784
3296
  const details = diffHttpAdapterDisplay(existing.httpAdapters, httpAdapters);
2785
3297
  if (details.length > 0) update.details = details;
@@ -2794,7 +3306,28 @@ async function planApplication(context, httpAdapterBuildResult) {
2794
3306
  details: details.length > 0 ? details : void 0
2795
3307
  });
2796
3308
  }
2797
- return changeSet;
3309
+ return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
3310
+ }
3311
+ /**
3312
+ * Attach the ownership-tracking fields to the plan's change set.
3313
+ *
3314
+ * The other resource plans return `{ changeSet, conflicts, ... }`; this one
3315
+ * returns the change set itself, so the fields are attached to it. Without
3316
+ * them the application is the one managed resource that never reaches
3317
+ * `confirmOwnerConflict`, which would let a deploy re-tag — or a `remove`
3318
+ * silently skip — an application this config does not own.
3319
+ * @param changeSet - The application change set
3320
+ * @param conflicts - Resources owned by another application
3321
+ * @param unmanaged - Resources carrying no SDK label
3322
+ * @param resourceOwners - Owners of the resources that were skipped
3323
+ * @returns The change set, with the ownership-tracking fields attached
3324
+ */
3325
+ function withOwnership(changeSet, conflicts, unmanaged, resourceOwners) {
3326
+ return Object.assign(changeSet, {
3327
+ conflicts,
3328
+ unmanaged,
3329
+ resourceOwners
3330
+ });
2798
3331
  }
2799
3332
  async function fetchAppLabels(client, workspaceId, appName) {
2800
3333
  return (await getOrNull(async () => {
@@ -3172,7 +3705,7 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
3172
3705
  appId
3173
3706
  });
3174
3707
  if (existing) {
3175
- trackDesiredResourceOwnership({
3708
+ const owned = trackDesiredResourceOwnership({
3176
3709
  labels: existing.allLabels,
3177
3710
  ownerLabel: existing.label,
3178
3711
  appName,
@@ -3192,7 +3725,7 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
3192
3725
  },
3193
3726
  metaRequest
3194
3727
  });
3195
- else if (!existing.label) changeSet.updates.push({
3728
+ else if (!owned) changeSet.updates.push({
3196
3729
  name,
3197
3730
  metaRequest
3198
3731
  });
@@ -3240,12 +3773,12 @@ async function applyAuthConnections(client, result, phase) {
3240
3773
  if (changeSet.creates.length > 0 || changeSet.replaces.length > 0) await withSecretsStateLock(stateScope, async () => {
3241
3774
  await Promise.all(changeSet.creates.map(async (create) => {
3242
3775
  await client.createAuthConnection(create.request);
3243
- await client.setMetadata(create.metaRequest);
3776
+ await writeMetadataLabels(client, create.metaRequest);
3244
3777
  logger.info(`Connection "${create.name}" was created. Authorize it with:\n tailor authconnection authorize --name ${create.name}\nOr via the Console: tailor authconnection open`);
3245
3778
  }));
3246
3779
  for (const replace of changeSet.replaces) {
3247
3780
  if ((await client.updateAuthConnection(replace.updateRequest)).connection?.status === AuthConnection_Status.UNAUTHORIZED) logger.warn(`Connection "${replace.name}" requires re-authorization. Authorize with:\n tailor authconnection authorize --name ${replace.name}\nOr via the Console: tailor authconnection open`);
3248
- await client.setMetadata(replace.metaRequest);
3781
+ await writeMetadataLabels(client, replace.metaRequest);
3249
3782
  }
3250
3783
  const secretReplaces = changeSet.replaces.filter((replace) => replace.updateRequest.updateMask?.paths?.includes("oauth2.client_secret"));
3251
3784
  if (changeSet.creates.length > 0 || secretReplaces.length > 0) {
@@ -3263,7 +3796,7 @@ async function applyAuthConnections(client, result, phase) {
3263
3796
  }
3264
3797
  });
3265
3798
  await Promise.all(changeSet.updates.map(async (update) => {
3266
- await client.setMetadata(update.metaRequest);
3799
+ await writeMetadataLabels(client, update.metaRequest);
3267
3800
  }));
3268
3801
  } else if (changeSet.deletes.length > 0) await withSecretsStateLock(stateScope, async () => {
3269
3802
  await Promise.all(changeSet.deletes.map(async (del) => {
@@ -3590,10 +4123,10 @@ async function applyFunctionRegistry(client, workspaceId, result, phase = "creat
3590
4123
  const limitFunction = createApplyLimiter();
3591
4124
  await Promise.all([...changeSet.creates.map((create) => limitFunction(async () => {
3592
4125
  await uploadFunctionScript(client, workspaceId, create.entry, true);
3593
- await client.setMetadata(create.metaRequest);
4126
+ await writeMetadataLabels(client, create.metaRequest);
3594
4127
  })), ...changeSet.updates.map((update) => limitFunction(async () => {
3595
4128
  await uploadFunctionScript(client, workspaceId, update.entry, false);
3596
- await client.setMetadata(update.metaRequest);
4129
+ await writeMetadataLabels(client, update.metaRequest);
3597
4130
  }))]);
3598
4131
  } else await Promise.all(changeSet.deletes.map((del) => client.deleteFunctionRegistry({
3599
4132
  workspaceId: del.workspaceId,
@@ -3976,15 +4509,19 @@ function idpClientSecretName(namespaceName, clientName) {
3976
4509
  async function applyIdP(client, result, phase = "create-update") {
3977
4510
  const { changeSet } = result;
3978
4511
  if (phase === "create-update") {
3979
- await Promise.all([...changeSet.service.creates.map(async (create) => {
3980
- await resolveServiceReturnOrigins(client, create.request);
3981
- await client.createIdPService(create.request);
3982
- await client.setMetadata(create.metaRequest);
3983
- }), ...changeSet.service.updates.map(async (update) => {
3984
- await resolveServiceReturnOrigins(client, update.request);
3985
- await client.updateIdPService(update.request);
3986
- await client.setMetadata(update.metaRequest);
3987
- })]);
4512
+ await Promise.all([
4513
+ ...changeSet.service.creates.map(async (create) => {
4514
+ await resolveServiceReturnOrigins(client, create.request);
4515
+ await client.createIdPService(create.request);
4516
+ await writeMetadataLabels(client, create.metaRequest);
4517
+ }),
4518
+ ...changeSet.service.updates.map(async (update) => {
4519
+ await resolveServiceReturnOrigins(client, update.request);
4520
+ await client.updateIdPService(update.request);
4521
+ await writeMetadataLabels(client, update.metaRequest);
4522
+ }),
4523
+ ...changeSet.service.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
4524
+ ]);
3988
4525
  await Promise.all([...changeSet.client.creates.map(async (create) => {
3989
4526
  const resp = await client.createIdPClient(create.request);
3990
4527
  const vaultName = idpClientVaultName(assertDefined(create.request.namespaceName, "request missing namespaceName"), create.request.client?.name || "");
@@ -4035,10 +4572,13 @@ async function applyIdP(client, result, phase = "create-update") {
4035
4572
  * @returns Planned changes and metadata
4036
4573
  */
4037
4574
  async function planIdP(context) {
4038
- const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets } = context;
4575
+ const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets, dependentApps, runAppIds } = context;
4039
4576
  const idps = forRemoval ? [] : application.idpServices;
4040
4577
  const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
4041
- const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set(), expectedLocalWebsites);
4578
+ const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set(), expectedLocalWebsites, {
4579
+ dependentApps,
4580
+ runAppIds
4581
+ });
4042
4582
  return {
4043
4583
  changeSet: {
4044
4584
  service: serviceChangeSet,
@@ -4091,7 +4631,7 @@ function normalizeComparableIdPService(input) {
4091
4631
  authorization: input.authorization || void 0,
4092
4632
  lang: input.lang === IdPLang.UNSPECIFIED ? IdPLang.EN : input.lang,
4093
4633
  userAuthPolicy: input.userAuthPolicy,
4094
- publishUserEvents: input.publishUserEvents,
4634
+ publishEvents: input.publishEvents,
4095
4635
  disableGqlOperations: input.disableGqlOperations,
4096
4636
  emailConfig: input.emailConfig,
4097
4637
  permission: input.permission
@@ -4123,13 +4663,13 @@ function areIdPServicesEqual(existing, desired) {
4123
4663
  authorization: existing.authorization,
4124
4664
  lang: existing.lang,
4125
4665
  userAuthPolicy: normalizeComparableUserAuthPolicy(existing.userAuthPolicy),
4126
- publishUserEvents: existing.publishUserEvents,
4666
+ publishEvents: existing.publishUserEvents,
4127
4667
  disableGqlOperations: normalizeComparableDisableGqlOperations(existing.disableGqlOperations),
4128
4668
  emailConfig: normalizeComparableEmailConfig(existing.emailConfig),
4129
4669
  permission: normalizeComparablePermission(existing.permission)
4130
4670
  }), desired);
4131
4671
  }
4132
- async function planServices$3(client, workspaceId, appName, appId, idps, idpUserTriggerTargets, expectedLocalWebsites) {
4672
+ async function planServices$3(client, workspaceId, appName, appId, idps, idpUserTriggerTargets, expectedLocalWebsites, records) {
4133
4673
  const changeSet = createChangeSet("IdP services");
4134
4674
  const conflicts = [];
4135
4675
  const unmanaged = [];
@@ -4150,10 +4690,15 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
4150
4690
  for (const idp of idps) {
4151
4691
  const namespaceName = idp.name;
4152
4692
  const existing = existingServices[namespaceName];
4153
- const metaRequest = await buildMetaRequest({
4693
+ const metaRequest = addDependencyRecords(await buildMetaRequest({
4154
4694
  trn: resourceTrn(workspaceId, "idp", namespaceName),
4155
4695
  appName,
4156
4696
  appId
4697
+ }), {
4698
+ key: eventSourceKey.idp(namespaceName),
4699
+ dependentApps: records.dependentApps,
4700
+ runAppIds: records.runAppIds,
4701
+ pinned: idp.publishEvents !== void 0
4157
4702
  });
4158
4703
  let authorization;
4159
4704
  switch (idp.authorization) {
@@ -4172,9 +4717,11 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
4172
4717
  }
4173
4718
  const lang = convertLang(idp.lang);
4174
4719
  const userAuthPolicy = idp.userAuthPolicy;
4175
- const isIdpUserTriggerTarget = idpUserTriggerTargets.has(namespaceName);
4176
- if (isIdpUserTriggerTarget && idp.publishUserEvents === false) throw new Error(`IdP service "${namespaceName}" has "publishUserEvents: false", but executors with idpUser triggers subscribe to it. Either remove "publishUserEvents: false" or remove the matching executor triggers.`);
4177
- const publishUserEvents = idp.publishUserEvents ?? isIdpUserTriggerTarget;
4720
+ const publishEvents = resolvePublishEvents({
4721
+ explicit: idp.publishEvents,
4722
+ subscribed: idpUserTriggerTargets.has(namespaceName),
4723
+ conflict: publishEventsConflict.idpService(namespaceName)
4724
+ });
4178
4725
  const emailConfig = idp.emailConfig;
4179
4726
  if (!idp.permission) logger.warn(`IdP service "${namespaceName}" has no permission configured.`);
4180
4727
  const omittedPermitLocations = findOmittedPermitRules(idp.permission);
@@ -4190,7 +4737,7 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
4190
4737
  authorization,
4191
4738
  lang,
4192
4739
  userAuthPolicy: normalizeComparableUserAuthPolicy(userAuthPolicyForCompare),
4193
- publishUserEvents,
4740
+ publishEvents,
4194
4741
  disableGqlOperations: normalizeComparableDisableGqlOperations(convertGqlOperationsToDisable(idp.gqlOperations)),
4195
4742
  emailConfig: normalizeComparableEmailConfig(emailConfig),
4196
4743
  permission: protoPermission
@@ -4201,7 +4748,7 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
4201
4748
  authorization,
4202
4749
  lang,
4203
4750
  userAuthPolicy,
4204
- publishUserEvents,
4751
+ publishUserEvents: publishEvents,
4205
4752
  disableGqlOperations: convertGqlOperationsToDisable(idp.gqlOperations),
4206
4753
  emailConfig,
4207
4754
  permission: protoPermission
@@ -4216,7 +4763,10 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
4216
4763
  resourceName: idp.name,
4217
4764
  conflicts,
4218
4765
  unmanaged
4219
- }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({ name: namespaceName });
4766
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({
4767
+ name: namespaceName,
4768
+ metaRequest
4769
+ });
4220
4770
  else changeSet.updates.push({
4221
4771
  name: namespaceName,
4222
4772
  request,
@@ -4424,10 +4974,10 @@ async function applyAuth(client, result, phase = "create-update") {
4424
4974
  const applyServices = async () => {
4425
4975
  await Promise.all([...changeSet.service.creates.map(async (create) => {
4426
4976
  await client.createAuthService(create.request);
4427
- await client.setMetadata(create.metaRequest);
4977
+ await writeMetadataLabels(client, create.metaRequest);
4428
4978
  }), ...changeSet.service.updates.map(async (update) => {
4429
4979
  await client.updateAuthService(update.request);
4430
- await client.setMetadata(update.metaRequest);
4980
+ await writeMetadataLabels(client, update.metaRequest);
4431
4981
  })]);
4432
4982
  };
4433
4983
  const applyMachineUsers = async () => {
@@ -5548,10 +6098,10 @@ async function applyExecutor(client, result, phase = "create-update") {
5548
6098
  const { changeSet } = result;
5549
6099
  if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
5550
6100
  await client.createExecutorExecutor(create.request);
5551
- await client.setMetadata(create.metaRequest);
6101
+ await writeMetadataLabels(client, create.metaRequest);
5552
6102
  }), ...changeSet.updates.map(async (update) => {
5553
6103
  await client.updateExecutorExecutor(update.request);
5554
- await client.setMetadata(update.metaRequest);
6104
+ await writeMetadataLabels(client, update.metaRequest);
5555
6105
  })]);
5556
6106
  else await Promise.all(changeSet.deletes.map((del) => client.deleteExecutorExecutor(del.request)));
5557
6107
  }
@@ -5891,6 +6441,18 @@ function protoExecutor(context, executor) {
5891
6441
  }
5892
6442
  });
5893
6443
  break;
6444
+ case "workflowExecution":
6445
+ case "workflowJobExecution":
6446
+ triggerType = ExecutorTriggerType.EVENT;
6447
+ triggerConfig = typedEventTrigger({
6448
+ case: "workflow",
6449
+ value: {
6450
+ eventTypes: trigger.events,
6451
+ workflowName: trigger.workflowName,
6452
+ ...trigger.condition ? { condition: { expr: `(${stringifyFunction(trigger.condition)})(${argsExpr})` } } : {}
6453
+ }
6454
+ });
6455
+ break;
5894
6456
  default: throw new Error(`Unknown trigger: ${trigger}`);
5895
6457
  }
5896
6458
  const target = executor.operation;
@@ -6029,12 +6591,22 @@ async function applyPipeline(client, result, phase = "create-update") {
6029
6591
  if (phase === "create-update") {
6030
6592
  await Promise.all([...changeSet.service.creates.map(async (create) => {
6031
6593
  await client.createPipelineService(create.request);
6032
- await client.setMetadata(create.metaRequest);
6594
+ await writeMetadataLabels(client, create.metaRequest);
6033
6595
  }), ...changeSet.service.updates.map(async (update) => {
6034
6596
  await client.updatePipelineService(update.request);
6035
- await client.setMetadata(update.metaRequest);
6597
+ await writeMetadataLabels(client, update.metaRequest);
6036
6598
  })]);
6037
- await Promise.all([...changeSet.resolver.creates.map((create) => client.createPipelineResolver(create.request)), ...changeSet.resolver.updates.map((update) => client.updatePipelineResolver(update.request))]);
6599
+ await Promise.all([
6600
+ ...changeSet.resolver.creates.map(async (create) => {
6601
+ await client.createPipelineResolver(create.request);
6602
+ await writeMetadataLabels(client, create.metaRequest);
6603
+ }),
6604
+ ...changeSet.resolver.updates.map(async (update) => {
6605
+ await client.updatePipelineResolver(update.request);
6606
+ await writeMetadataLabels(client, update.metaRequest);
6607
+ }),
6608
+ ...changeSet.resolver.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
6609
+ ]);
6038
6610
  } else if (phase === "delete-resources") await Promise.all(changeSet.resolver.deletes.map((del) => client.deletePipelineResolver(del.request)));
6039
6611
  else await Promise.all(changeSet.service.deletes.map((del) => client.deletePipelineService(del.request)));
6040
6612
  }
@@ -6053,7 +6625,12 @@ async function planPipeline(context) {
6053
6625
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
6054
6626
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$1(client, workspaceId, application.name, application.id, pipelines);
6055
6627
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
6056
- const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, context.executorUsedResolvers ?? /* @__PURE__ */ new Set(), deletedServices, application.env, getApplicationAuthNamespace(application), forceApplyAll);
6628
+ const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, context.executorUsedResolvers ?? /* @__PURE__ */ new Set(), deletedServices, application.env, getApplicationAuthNamespace(application), forceApplyAll, {
6629
+ appName: application.name,
6630
+ appId: application.id,
6631
+ dependentApps: context.dependentApps,
6632
+ runAppIds: context.runAppIds
6633
+ });
6057
6634
  return {
6058
6635
  changeSet: {
6059
6636
  service: serviceChangeSet,
@@ -6141,8 +6718,28 @@ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
6141
6718
  resourceOwners
6142
6719
  };
6143
6720
  }
6144
- async function planResolvers(client, workspaceId, pipelines, executors, initialExecutorUsedResolvers, deletedServices, env, authNamespace, forceApplyAll = false) {
6721
+ async function planResolvers(client, workspaceId, pipelines, executors, initialExecutorUsedResolvers, deletedServices, env, authNamespace, forceApplyAll = false, records = {}) {
6145
6722
  const changeSet = createChangeSet("Pipeline resolvers");
6723
+ const { appName, appId, dependentApps, runAppIds } = records;
6724
+ /**
6725
+ * Build one resolver's metadata write, carrying the dependency records that
6726
+ * belong to it. The resolver is what publishes, so the record lives there.
6727
+ * @param namespace - Namespace holding the resolver
6728
+ * @param resolver - Resolver being planned
6729
+ * @returns The resolver's metadata write
6730
+ */
6731
+ const resolverMetaRequest = async (namespace, resolver) => {
6732
+ return addDependencyRecords(await buildMetaRequest({
6733
+ trn: resolverTrn(workspaceId, namespace, resolver.name),
6734
+ appName: appName ?? "",
6735
+ appId
6736
+ }), {
6737
+ key: eventSourceKey.resolver(namespace, resolver.name),
6738
+ dependentApps,
6739
+ runAppIds,
6740
+ pinned: resolver.publishEvents !== void 0
6741
+ });
6742
+ };
6146
6743
  const fetchResolvers = (namespaceName) => {
6147
6744
  return fetchAllTolerant(async (pageToken, maxPageSize) => {
6148
6745
  const { pipelineResolvers, nextPageToken } = await client.listPipelineResolvers({
@@ -6156,26 +6753,36 @@ async function planResolvers(client, workspaceId, pipelines, executors, initialE
6156
6753
  };
6157
6754
  const executorUsedResolvers = new Set(initialExecutorUsedResolvers);
6158
6755
  for (const executor of executors) if (executor.trigger.kind === "resolverExecuted") executorUsedResolvers.add(executor.trigger.resolverName);
6159
- for (const pipeline of pipelines) for (const resolver of Object.values(pipeline.resolvers)) if (executorUsedResolvers.has(resolver.name) && resolver.publishEvents === false) throw new Error(`Resolver "${resolver.name}" has publishEvents set to false, but it is used by an executor with a resolverExecuted trigger. Either remove the publishEvents: false setting or remove the executor trigger for this resolver.`);
6756
+ for (const pipeline of pipelines) for (const resolver of Object.values(pipeline.resolvers)) assertNoPublishEventsConflict({
6757
+ explicit: resolver.publishEvents,
6758
+ subscribed: executorUsedResolvers.has(resolver.name),
6759
+ conflict: publishEventsConflict.resolver(resolver.name)
6760
+ });
6160
6761
  for (const pipeline of pipelines) {
6161
6762
  const existingResolvers = await fetchResolvers(pipeline.namespace);
6162
6763
  const existingResolversMap = new Map(existingResolvers.map((resolver) => [resolver.name, resolver]));
6163
6764
  for (const resolver of Object.values(pipeline.resolvers)) {
6164
6765
  const desiredResolver = processResolver(pipeline.namespace, resolver, executorUsedResolvers, env, authNamespace);
6165
- if (existingResolversMap.get(resolver.name)) {
6766
+ const existingResolver = existingResolversMap.get(resolver.name);
6767
+ const metaRequest = await resolverMetaRequest(pipeline.namespace, resolver);
6768
+ if (existingResolver) {
6166
6769
  const { pipelineResolver: existingResolverDetail } = await client.getPipelineResolver({
6167
6770
  workspaceId,
6168
6771
  namespaceName: pipeline.namespace,
6169
6772
  resolverName: resolver.name
6170
6773
  });
6171
- if (!forceApplyAll && existingResolverDetail && areResolversEqual(existingResolverDetail, desiredResolver)) changeSet.unchanged.push({ name: resolver.name });
6774
+ if (!forceApplyAll && existingResolverDetail && areResolversEqual(existingResolverDetail, desiredResolver)) changeSet.unchanged.push({
6775
+ name: resolver.name,
6776
+ metaRequest
6777
+ });
6172
6778
  else changeSet.updates.push({
6173
6779
  name: resolver.name,
6174
6780
  request: {
6175
6781
  workspaceId,
6176
6782
  namespaceName: pipeline.namespace,
6177
6783
  pipelineResolver: desiredResolver
6178
- }
6784
+ },
6785
+ metaRequest
6179
6786
  });
6180
6787
  existingResolversMap.delete(resolver.name);
6181
6788
  } else changeSet.creates.push({
@@ -6184,7 +6791,8 @@ async function planResolvers(client, workspaceId, pipelines, executors, initialE
6184
6791
  workspaceId,
6185
6792
  namespaceName: pipeline.namespace,
6186
6793
  pipelineResolver: desiredResolver
6187
- }
6794
+ },
6795
+ metaRequest
6188
6796
  });
6189
6797
  }
6190
6798
  existingResolversMap.forEach((_resolver, name) => {
@@ -6292,9 +6900,11 @@ function processResolver(namespace, resolver, executorUsedResolvers, env, authNa
6292
6900
  const resolverDescription = resolver.description || `${resolver.name} resolver`;
6293
6901
  const outputDescription = resolver.output.metadata.description;
6294
6902
  const combinedDescription = outputDescription ? `${resolverDescription}\n\nReturns:\n${outputDescription}` : resolverDescription;
6295
- let publishExecutionEvents = false;
6296
- if (resolver.publishEvents !== void 0) publishExecutionEvents = resolver.publishEvents;
6297
- else if (executorUsedResolvers.has(resolver.name)) publishExecutionEvents = true;
6903
+ const publishExecutionEvents = resolvePublishEvents({
6904
+ explicit: resolver.publishEvents,
6905
+ subscribed: executorUsedResolvers.has(resolver.name),
6906
+ conflict: publishEventsConflict.resolver(resolver.name)
6907
+ });
6298
6908
  return {
6299
6909
  authorization: "true==true",
6300
6910
  description: combinedDescription,
@@ -6539,22 +7149,18 @@ async function applySecretManager(client, result, phase = "create-update", appli
6539
7149
  if (phase === "create-update") {
6540
7150
  await Promise.all(vaultChangeSet.creates.map(async (create) => {
6541
7151
  await client.createSecretManagerVault(vaultCreateRequest(create));
6542
- if (application) {
6543
- const metaRequest = await buildMetaRequest({
6544
- trn: resourceTrn(create.workspaceId, "vault", create.name),
6545
- appName: application.name,
6546
- appId: application.id
6547
- });
6548
- await client.setMetadata(metaRequest);
6549
- }
7152
+ if (application) await writeMetadataLabels(client, await buildMetaRequest({
7153
+ trn: resourceTrn(create.workspaceId, "vault", create.name),
7154
+ appName: application.name,
7155
+ appId: application.id
7156
+ }));
6550
7157
  }));
6551
7158
  if (application) await Promise.all(vaultChangeSet.updates.map(async (update) => {
6552
- const metaRequest = await buildMetaRequest({
7159
+ await writeMetadataLabels(client, await buildMetaRequest({
6553
7160
  trn: resourceTrn(update.workspaceId, "vault", update.name),
6554
7161
  appName: application.name,
6555
7162
  appId: application.id
6556
- });
6557
- await client.setMetadata(metaRequest);
7163
+ }));
6558
7164
  }));
6559
7165
  const secretHashUpdates = [...secretChangeSet.creates, ...secretChangeSet.updates];
6560
7166
  if (secretHashUpdates.length > 0) await withSecretsStateLock(stateScope, async () => {
@@ -6632,197 +7238,46 @@ function getNamespacesWithMigrations(config, configDir) {
6632
7238
  }
6633
7239
  return result;
6634
7240
  }
6635
-
6636
- //#endregion
6637
- //#region src/cli/commands/tailordb/migrate/diff-calculator.ts
6638
- /**
6639
- * Check if a migration diff has any changes
6640
- * @param {MigrationDiff} diff - Migration diff to check
6641
- * @returns {boolean} True if diff has changes
6642
- */
6643
- function hasChanges(diff) {
6644
- return diff.changes.length > 0;
6645
- }
6646
7241
  /**
6647
- * Format a migration diff for display
6648
- * @param {MigrationDiff} diff - Migration diff to format
6649
- * @returns {string} Formatted diff string
7242
+ * Select the single target namespace for a migration command
7243
+ * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations configured
7244
+ * @param {string | undefined} requested - Namespace requested via --namespace, if any
7245
+ * @returns {NamespaceWithMigrations} The selected namespace
6650
7246
  */
6651
- function formatMigrationDiff(diff) {
6652
- if (diff.changes.length === 0) return "No schema differences detected.";
6653
- const lines = [];
6654
- const changesByType = /* @__PURE__ */ new Map();
6655
- for (const change of diff.changes) {
6656
- const existing = changesByType.get(change.typeName) ?? [];
6657
- existing.push(change);
6658
- changesByType.set(change.typeName, existing);
6659
- }
6660
- for (const [typeName, changes] of changesByType) {
6661
- lines.push(`${diff.namespace}.${typeName}:`);
6662
- for (const change of changes) lines.push(formatDiffChange(change));
7247
+ function selectTargetNamespace(namespacesWithMigrations, requested) {
7248
+ if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
7249
+ if (requested) {
7250
+ const found = namespacesWithMigrations.find((ns) => ns.namespace === requested);
7251
+ if (!found) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
7252
+ return found;
6663
7253
  }
6664
- return lines.join("\n");
7254
+ if (namespacesWithMigrations.length > 1) throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
7255
+ return assertDefined(namespacesWithMigrations[0], "namespace with migrations missing");
6665
7256
  }
6666
- /**
6667
- * Format a single diff change for display
6668
- * @param {DiffChange} change - Diff change to format
6669
- * @returns {string} Formatted change string
6670
- */
6671
- function formatDiffChange(change) {
6672
- switch (change.kind) {
6673
- case "type_added": return ` + [Type] ${change.typeName} (new type)`;
6674
- case "type_removed": return ` - [Type] ${change.typeName} (removed)`;
6675
- case "type_modified": return ` ~ [Type] ${change.typeName}: ${change.reason}`;
6676
- case "type_settings_modified": return ` ~ [Type Settings] ${change.typeName}: ${change.reason ?? "settings changed"}`;
6677
- case "field_added": {
6678
- const typeStr = formatFieldType(change.after);
6679
- return ` + ${change.fieldName}: ${typeStr}`;
6680
- }
6681
- case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
6682
- case "field_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
6683
- case "index_added": return ` + [Index] ${change.indexName}`;
6684
- case "index_removed": return ` - [Index] ${change.indexName}`;
6685
- case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
6686
- case "file_added": return ` + [File] ${change.fieldName}`;
6687
- case "file_removed": return ` - [File] ${change.fieldName}`;
6688
- case "file_modified": return ` ~ [File] ${change.fieldName}: ${change.reason ?? "modified"}`;
6689
- case "relationship_added": return ` + [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
6690
- case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
6691
- case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
6692
- case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
6693
- case "type_scripts_modified": return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`;
6694
- default: {
6695
- const unknown = change;
6696
- return ` ? ${unknown.typeName}.${unknown.fieldName ?? ""}`;
6697
- }
6698
- }
6699
- }
6700
- /**
6701
- * Format field type with attributes
6702
- * @param {SnapshotFieldConfig} field - Field configuration
6703
- * @returns {string} Formatted field type string
6704
- */
6705
- function formatFieldType(field) {
6706
- let type = field.type;
6707
- if (field.array) type += "[]";
6708
- if (field.required) type += " (required)";
6709
- else type += " (optional)";
6710
- return type;
6711
- }
6712
- /**
6713
- * Format field modification details
6714
- * @param {SnapshotFieldConfig} before - Before field configuration
6715
- * @param {SnapshotFieldConfig} after - After field configuration
6716
- * @returns {string} Formatted modification details
6717
- */
6718
- function formatFieldModification(before, after) {
6719
- const changes = [];
6720
- if (before.type !== after.type) changes.push(`type: ${before.type} → ${after.type}`);
6721
- if (before.required !== after.required) changes.push(`required: ${before.required} → ${after.required}`);
6722
- if (Boolean(before.array) !== Boolean(after.array)) changes.push(`array: ${before.array ?? false} → ${after.array ?? false}`);
6723
- if (Boolean(before.index) !== Boolean(after.index)) changes.push(`index: ${before.index ?? false} → ${after.index ?? false}`);
6724
- if (Boolean(before.unique) !== Boolean(after.unique)) changes.push(`unique: ${before.unique ?? false} → ${after.unique ?? false}`);
6725
- if (Boolean(before.vector) !== Boolean(after.vector)) changes.push(`vector: ${before.vector ?? false} → ${after.vector ?? false}`);
6726
- const beforeAllowed = before.allowedValues ?? [];
6727
- const afterAllowed = after.allowedValues ?? [];
6728
- const afterSet = new Set(afterAllowed.map((v) => v.value));
6729
- if (beforeAllowed.length !== afterAllowed.length || beforeAllowed.some((v) => !afterSet.has(v.value))) {
6730
- const beforeValues = beforeAllowed.map((v) => v.value).join(", ");
6731
- const afterValues = afterAllowed.map((v) => v.value).join(", ");
6732
- changes.push(`allowedValues: [${beforeValues}] → [${afterValues}]`);
6733
- }
6734
- const beforeHooks = before.hooks;
6735
- const afterHooks = after.hooks;
6736
- if ((beforeHooks?.create?.expr ?? "") !== (afterHooks?.create?.expr ?? "") || (beforeHooks?.update?.expr ?? "") !== (afterHooks?.update?.expr ?? "")) changes.push("hooks modified");
6737
- const beforeValidate = before.validate ?? [];
6738
- const afterValidate = after.validate ?? [];
6739
- if (beforeValidate.length !== afterValidate.length) changes.push(`validations: ${beforeValidate.length} → ${afterValidate.length}`);
6740
- if (Boolean(before.serial) !== Boolean(after.serial)) changes.push(`serial: ${before.serial ? "enabled" : "disabled"} → ${after.serial ? "enabled" : "disabled"}`);
6741
- return changes.join(", ");
6742
- }
6743
- /**
6744
- * Format breaking changes for display
6745
- * @param {BreakingChangeInfo[]} breakingChanges - Breaking changes to format
6746
- * @returns {string} Formatted breaking changes string
6747
- */
6748
- function formatBreakingChanges(breakingChanges) {
6749
- if (breakingChanges.length === 0) return "";
6750
- const lines = ["Breaking changes detected:", ""];
6751
- for (const bc of breakingChanges) {
6752
- const location = bc.fieldName ? `${bc.typeName}.${bc.fieldName}` : bc.typeName;
6753
- lines.push(` - ${location}: ${bc.reason}`);
6754
- }
6755
- return lines.join("\n");
6756
- }
6757
- /**
6758
- * Format warning changes for display
6759
- * @param {WarningChangeInfo[]} warnings - Warning changes to format
6760
- * @returns {string} Formatted warning changes string
6761
- */
6762
- function formatWarnings(warnings) {
6763
- if (warnings.length === 0) return "";
6764
- const lines = ["Warning: data loss possible:", ""];
6765
- for (const w of warnings) {
6766
- const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
6767
- lines.push(` - ${location}: ${w.reason}`);
6768
- }
6769
- return lines.join("\n");
6770
- }
6771
- const DIFF_CHANGE_LABELS = {
6772
- type_added: "type(s) added",
6773
- type_removed: "type(s) removed",
6774
- type_modified: "type(s) modified",
6775
- type_settings_modified: "type setting(s) modified",
6776
- field_added: "field(s) added",
6777
- field_removed: "field(s) removed",
6778
- field_modified: "field(s) modified",
6779
- index_added: "index(es) added",
6780
- index_removed: "index(es) removed",
6781
- index_modified: "index(es) modified",
6782
- file_added: "file field(s) added",
6783
- file_removed: "file field(s) removed",
6784
- file_modified: "file field(s) modified",
6785
- relationship_added: "relationship(s) added",
6786
- relationship_removed: "relationship(s) removed",
6787
- relationship_modified: "relationship(s) modified",
6788
- permission_modified: "permission(s) modified",
6789
- type_scripts_modified: "type script(s) modified"
6790
- };
6791
- /**
6792
- * Format a summary of the migration diff
6793
- * @param {MigrationDiff} diff - Migration diff to summarize
6794
- * @returns {string} Formatted summary string
6795
- */
6796
- function formatDiffSummary(diff) {
6797
- const stats = {};
6798
- for (const change of diff.changes) stats[change.kind] = (stats[change.kind] ?? 0) + 1;
6799
- const parts = Object.keys(stats).map((kind) => `${stats[kind]} ${DIFF_CHANGE_LABELS[kind]}`);
6800
- return parts.length > 0 ? parts.join(", ") : "No changes";
6801
- }
6802
-
6803
- //#endregion
6804
- //#region src/parser/service/tailordb/type-script.ts
6805
- const INPUT = "_input";
6806
- const NEW_RECORD = "_newRecord";
6807
- const OLD_RECORD = "_oldRecord";
6808
- const NOW = "_now";
6809
- const SOURCE_HASH_PREFIX = "// @sdk-source-hash:";
6810
- const TIME_TYPES = /* @__PURE__ */ new Set([
6811
- "datetime",
6812
- "date",
6813
- "time"
6814
- ]);
6815
- const key = (name) => JSON.stringify(name);
6816
- function collectFieldScriptSources(fields) {
6817
- const entries = [];
6818
- for (const [name, config] of Object.entries(fields).toSorted(([a], [b]) => a.localeCompare(b))) {
6819
- const data = {};
6820
- if (config.hooks?.create?.expr) data.hc = config.hooks.create.expr;
6821
- if (config.hooks?.update?.expr) data.hu = config.hooks.update.expr;
6822
- if (config.validate?.some((v) => v.script?.expr)) data.v = config.validate.filter((v) => v.script?.expr).map((v) => [v.script?.expr, v.errorMessage]);
6823
- if (config.default !== void 0) {
6824
- data.d = config.default instanceof Date ? config.default.toISOString() : config.default;
6825
- data.t = config.type;
7257
+
7258
+ //#endregion
7259
+ //#region src/parser/service/tailordb/type-script.ts
7260
+ const INPUT = "_input";
7261
+ const NEW_RECORD = "_newRecord";
7262
+ const OLD_RECORD = "_oldRecord";
7263
+ const NOW = "_now";
7264
+ const SOURCE_HASH_PREFIX = "// @sdk-source-hash:";
7265
+ const TIME_TYPES = /* @__PURE__ */ new Set([
7266
+ "datetime",
7267
+ "date",
7268
+ "time"
7269
+ ]);
7270
+ const key = (name) => JSON.stringify(name);
7271
+ function collectFieldScriptSources(fields) {
7272
+ const entries = [];
7273
+ for (const [name, config] of Object.entries(fields).toSorted(([a], [b]) => a.localeCompare(b))) {
7274
+ const data = {};
7275
+ if (config.hooks?.create?.expr) data.hc = config.hooks.create.expr;
7276
+ if (config.hooks?.update?.expr) data.hu = config.hooks.update.expr;
7277
+ if (config.validate?.some((v) => v.script?.expr)) data.v = config.validate.filter((v) => v.script?.expr).map((v) => [v.script?.expr, v.errorMessage]);
7278
+ if (config.default !== void 0) {
7279
+ data.d = config.default instanceof Date ? config.default.toISOString() : config.default;
7280
+ data.t = config.type;
6826
7281
  }
6827
7282
  if (config.fields) {
6828
7283
  const nested = collectFieldScriptSources(config.fields);
@@ -6976,77 +7431,244 @@ function buildTypeScripts(fields, options) {
6976
7431
  }
6977
7432
 
6978
7433
  //#endregion
6979
- //#region src/cli/commands/tailordb/migrate/migration-number.ts
7434
+ //#region src/cli/commands/tailordb/migrate/diff-calculator.ts
6980
7435
  /**
6981
- * Format migration number as 4-digit string.
6982
- * @param num - Migration number
6983
- * @returns 4-digit padded string
7436
+ * Check if a migration diff has any changes
7437
+ * @param {MigrationDiff} diff - Migration diff to check
7438
+ * @returns {boolean} True if diff has changes
6984
7439
  */
6985
- function formatMigrationNumber(num) {
6986
- return num.toString().padStart(4, "0");
7440
+ function hasChanges(diff) {
7441
+ return diff.changes.length > 0;
6987
7442
  }
6988
7443
  /**
6989
- * Parse a migration number CLI argument.
6990
- *
6991
- * Accepts the canonical 4-digit form ("0001") or a bare integer without
6992
- * leading zeros ("0"–"9999"). Commands that disallow the baseline reject
6993
- * 0 themselves with a context-specific message.
6994
- * @param numberStr - Raw CLI argument
6995
- * @returns Parsed migration number
7444
+ * Format a migration diff for display
7445
+ * @param {MigrationDiff} diff - Migration diff to format
7446
+ * @returns {string} Formatted diff string
6996
7447
  */
6997
- function parseMigrationNumberArg(numberStr) {
6998
- if (/^\d{4}$/.test(numberStr)) return parseInt(numberStr, 10);
6999
- if (/^(0|[1-9]\d*)$/.test(numberStr)) {
7000
- const parsed = parseInt(numberStr, 10);
7001
- if (parsed > 9999) throw new Error(`Migration number ${numberStr} is out of range. Expected 0-9999.`);
7002
- return parsed;
7448
+ function formatMigrationDiff(diff) {
7449
+ if (diff.changes.length === 0) return "No schema differences detected.";
7450
+ const lines = [];
7451
+ const changesByType = /* @__PURE__ */ new Map();
7452
+ for (const change of diff.changes) {
7453
+ const existing = changesByType.get(change.typeName) ?? [];
7454
+ existing.push(change);
7455
+ changesByType.set(change.typeName, existing);
7003
7456
  }
7004
- throw new Error(`Invalid migration number format: ${numberStr}. Expected 4-digit format (e.g., 0001) or integer 0-9999 (e.g., 1).`);
7457
+ for (const [typeName, changes] of changesByType) {
7458
+ lines.push(`${diff.namespace}.${typeName}:`);
7459
+ for (const change of changes) lines.push(formatDiffChange(change));
7460
+ }
7461
+ return lines.join("\n");
7005
7462
  }
7006
-
7007
- //#endregion
7008
- //#region src/cli/commands/tailordb/migrate/snapshot-schema.ts
7009
7463
  /**
7010
- * Zod schemas for TailorDB migration snapshot and diff files.
7011
- *
7012
- * Each schema mirrors the corresponding hand-written interface from
7013
- * snapshot-types.ts / diff-calculator.ts. Schemas are cast to
7014
- * `z.ZodType<T>` to keep them aligned with the interfaces at compile time.
7015
- *
7016
- * All object schemas use `z.looseObject` so that unknown keys written
7017
- * by newer CLI versions survive a load → save round-trip.
7464
+ * Format a single diff change for display
7465
+ * @param {DiffChange} change - Diff change to format
7466
+ * @returns {string} Formatted change string
7018
7467
  */
7019
- function snapshotRecordSchema(valueSchema) {
7020
- return z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), { message: "Expected record" }).transform((value, ctx) => {
7021
- const record = Object.create(null);
7022
- for (const key of Object.keys(value)) {
7023
- const result = valueSchema.safeParse(value[key]);
7024
- if (!result.success) {
7025
- for (const issue of result.error.issues) ctx.addIssue({
7026
- ...issue,
7027
- path: [key, ...issue.path]
7028
- });
7029
- continue;
7030
- }
7031
- record[key] = result.data;
7468
+ function formatDiffChange(change) {
7469
+ switch (change.kind) {
7470
+ case "type_added": return ` + [Type] ${change.typeName} (new type)`;
7471
+ case "type_removed": return ` - [Type] ${change.typeName} (removed)`;
7472
+ case "type_modified": return ` ~ [Type] ${change.typeName}: ${change.reason}`;
7473
+ case "type_settings_modified": return ` ~ [Type Settings] ${change.typeName}: ${change.reason ?? "settings changed"}`;
7474
+ case "field_added": {
7475
+ const typeStr = formatFieldType(change.after);
7476
+ return ` + ${change.fieldName}: ${typeStr}`;
7032
7477
  }
7033
- return record;
7034
- });
7478
+ case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
7479
+ case "field_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
7480
+ case "index_added": return ` + [Index] ${change.indexName}`;
7481
+ case "index_removed": return ` - [Index] ${change.indexName}`;
7482
+ case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
7483
+ case "file_added": return ` + [File] ${change.fieldName}`;
7484
+ case "file_removed": return ` - [File] ${change.fieldName}`;
7485
+ case "file_modified": return ` ~ [File] ${change.fieldName}: ${change.reason ?? "modified"}`;
7486
+ case "relationship_added": return ` + [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
7487
+ case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
7488
+ case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
7489
+ case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
7490
+ case "type_scripts_modified": return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`;
7491
+ default: {
7492
+ const unknown = change;
7493
+ return ` ? ${unknown.typeName}.${unknown.fieldName ?? ""}`;
7494
+ }
7495
+ }
7035
7496
  }
7036
- const snapshotHookSchema = z.looseObject({ expr: z.string() });
7037
- const snapshotValidationSchema = z.looseObject({
7038
- script: z.looseObject({ expr: z.string() }).optional(),
7039
- errorMessage: z.string()
7040
- });
7041
- const snapshotSerialSchema = z.looseObject({
7042
- start: z.number(),
7043
- maxValue: z.number().optional(),
7044
- format: z.string().optional()
7045
- });
7046
- const snapshotEnumValueSchema = z.looseObject({
7047
- value: z.string(),
7048
- description: z.string().optional()
7049
- });
7497
+ /**
7498
+ * Format field type with attributes
7499
+ * @param {SnapshotFieldConfig} field - Field configuration
7500
+ * @returns {string} Formatted field type string
7501
+ */
7502
+ function formatFieldType(field) {
7503
+ let type = field.type;
7504
+ if (field.array) type += "[]";
7505
+ if (field.required) type += " (required)";
7506
+ else type += " (optional)";
7507
+ return type;
7508
+ }
7509
+ /**
7510
+ * Format field modification details
7511
+ * @param {SnapshotFieldConfig} before - Before field configuration
7512
+ * @param {SnapshotFieldConfig} after - After field configuration
7513
+ * @returns {string} Formatted modification details
7514
+ */
7515
+ function formatFieldModification(before, after) {
7516
+ const changes = [];
7517
+ if (before.type !== after.type) changes.push(`type: ${before.type} → ${after.type}`);
7518
+ if (before.required !== after.required) changes.push(`required: ${before.required} → ${after.required}`);
7519
+ if (Boolean(before.array) !== Boolean(after.array)) changes.push(`array: ${before.array ?? false} → ${after.array ?? false}`);
7520
+ if (Boolean(before.index) !== Boolean(after.index)) changes.push(`index: ${before.index ?? false} → ${after.index ?? false}`);
7521
+ if (Boolean(before.unique) !== Boolean(after.unique)) changes.push(`unique: ${before.unique ?? false} → ${after.unique ?? false}`);
7522
+ if (Boolean(before.vector) !== Boolean(after.vector)) changes.push(`vector: ${before.vector ?? false} → ${after.vector ?? false}`);
7523
+ const beforeAllowed = before.allowedValues ?? [];
7524
+ const afterAllowed = after.allowedValues ?? [];
7525
+ const afterSet = new Set(afterAllowed.map((v) => v.value));
7526
+ if (beforeAllowed.length !== afterAllowed.length || beforeAllowed.some((v) => !afterSet.has(v.value))) {
7527
+ const beforeValues = beforeAllowed.map((v) => v.value).join(", ");
7528
+ const afterValues = afterAllowed.map((v) => v.value).join(", ");
7529
+ changes.push(`allowedValues: [${beforeValues}] → [${afterValues}]`);
7530
+ }
7531
+ const beforeHooks = before.hooks;
7532
+ const afterHooks = after.hooks;
7533
+ if ((beforeHooks?.create?.expr ?? "") !== (afterHooks?.create?.expr ?? "") || (beforeHooks?.update?.expr ?? "") !== (afterHooks?.update?.expr ?? "")) changes.push("hooks modified");
7534
+ const beforeValidate = before.validate ?? [];
7535
+ const afterValidate = after.validate ?? [];
7536
+ if (beforeValidate.length !== afterValidate.length) changes.push(`validations: ${beforeValidate.length} → ${afterValidate.length}`);
7537
+ if (Boolean(before.serial) !== Boolean(after.serial)) changes.push(`serial: ${before.serial ? "enabled" : "disabled"} → ${after.serial ? "enabled" : "disabled"}`);
7538
+ return changes.join(", ");
7539
+ }
7540
+ /**
7541
+ * Format breaking changes for display
7542
+ * @param {BreakingChangeInfo[]} breakingChanges - Breaking changes to format
7543
+ * @returns {string} Formatted breaking changes string
7544
+ */
7545
+ function formatBreakingChanges(breakingChanges) {
7546
+ if (breakingChanges.length === 0) return "";
7547
+ const lines = ["Breaking changes detected:", ""];
7548
+ for (const bc of breakingChanges) {
7549
+ const location = bc.fieldName ? `${bc.typeName}.${bc.fieldName}` : bc.typeName;
7550
+ lines.push(` - ${location}: ${bc.reason}`);
7551
+ }
7552
+ return lines.join("\n");
7553
+ }
7554
+ /**
7555
+ * Format warning changes for display
7556
+ * @param {WarningChangeInfo[]} warnings - Warning changes to format
7557
+ * @returns {string} Formatted warning changes string
7558
+ */
7559
+ function formatWarnings(warnings) {
7560
+ if (warnings.length === 0) return "";
7561
+ const lines = ["Warning: data loss possible:", ""];
7562
+ for (const w of warnings) {
7563
+ const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
7564
+ lines.push(` - ${location}: ${w.reason}`);
7565
+ }
7566
+ return lines.join("\n");
7567
+ }
7568
+ const DIFF_CHANGE_LABELS = {
7569
+ type_added: "type(s) added",
7570
+ type_removed: "type(s) removed",
7571
+ type_modified: "type(s) modified",
7572
+ type_settings_modified: "type setting(s) modified",
7573
+ field_added: "field(s) added",
7574
+ field_removed: "field(s) removed",
7575
+ field_modified: "field(s) modified",
7576
+ index_added: "index(es) added",
7577
+ index_removed: "index(es) removed",
7578
+ index_modified: "index(es) modified",
7579
+ file_added: "file field(s) added",
7580
+ file_removed: "file field(s) removed",
7581
+ file_modified: "file field(s) modified",
7582
+ relationship_added: "relationship(s) added",
7583
+ relationship_removed: "relationship(s) removed",
7584
+ relationship_modified: "relationship(s) modified",
7585
+ permission_modified: "permission(s) modified",
7586
+ type_scripts_modified: "type script(s) modified"
7587
+ };
7588
+ /**
7589
+ * Format a summary of the migration diff
7590
+ * @param {MigrationDiff} diff - Migration diff to summarize
7591
+ * @returns {string} Formatted summary string
7592
+ */
7593
+ function formatDiffSummary(diff) {
7594
+ const stats = {};
7595
+ for (const change of diff.changes) stats[change.kind] = (stats[change.kind] ?? 0) + 1;
7596
+ const parts = Object.keys(stats).map((kind) => `${stats[kind]} ${DIFF_CHANGE_LABELS[kind]}`);
7597
+ return parts.length > 0 ? parts.join(", ") : "No changes";
7598
+ }
7599
+
7600
+ //#endregion
7601
+ //#region src/cli/commands/tailordb/migrate/migration-number.ts
7602
+ /**
7603
+ * Format migration number as 4-digit string.
7604
+ * @param num - Migration number
7605
+ * @returns 4-digit padded string
7606
+ */
7607
+ function formatMigrationNumber(num) {
7608
+ return num.toString().padStart(4, "0");
7609
+ }
7610
+ /**
7611
+ * Parse a migration number CLI argument.
7612
+ *
7613
+ * Accepts the canonical 4-digit form ("0001") or a bare integer without
7614
+ * leading zeros ("0"–"9999"). Commands that disallow the baseline reject
7615
+ * 0 themselves with a context-specific message.
7616
+ * @param numberStr - Raw CLI argument
7617
+ * @returns Parsed migration number
7618
+ */
7619
+ function parseMigrationNumberArg(numberStr) {
7620
+ if (/^\d{4}$/.test(numberStr)) return parseInt(numberStr, 10);
7621
+ if (/^(0|[1-9]\d*)$/.test(numberStr)) {
7622
+ const parsed = parseInt(numberStr, 10);
7623
+ if (parsed > 9999) throw new Error(`Migration number ${numberStr} is out of range. Expected 0-9999.`);
7624
+ return parsed;
7625
+ }
7626
+ throw new Error(`Invalid migration number format: ${numberStr}. Expected 4-digit format (e.g., 0001) or integer 0-9999 (e.g., 1).`);
7627
+ }
7628
+
7629
+ //#endregion
7630
+ //#region src/cli/commands/tailordb/migrate/snapshot-schema.ts
7631
+ /**
7632
+ * Zod schemas for TailorDB migration snapshot and diff files.
7633
+ *
7634
+ * Each schema mirrors the corresponding hand-written interface from
7635
+ * snapshot-types.ts / diff-calculator.ts. Schemas are cast to
7636
+ * `z.ZodType<T>` to keep them aligned with the interfaces at compile time.
7637
+ *
7638
+ * All object schemas use `z.looseObject` so that unknown keys written
7639
+ * by newer CLI versions survive a load → save round-trip.
7640
+ */
7641
+ function snapshotRecordSchema(valueSchema) {
7642
+ return z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), { message: "Expected record" }).transform((value, ctx) => {
7643
+ const record = Object.create(null);
7644
+ for (const key of Object.keys(value)) {
7645
+ const result = valueSchema.safeParse(value[key]);
7646
+ if (!result.success) {
7647
+ for (const issue of result.error.issues) ctx.addIssue({
7648
+ ...issue,
7649
+ path: [key, ...issue.path]
7650
+ });
7651
+ continue;
7652
+ }
7653
+ record[key] = result.data;
7654
+ }
7655
+ return record;
7656
+ });
7657
+ }
7658
+ const snapshotHookSchema = z.looseObject({ expr: z.string() });
7659
+ const snapshotValidationSchema = z.looseObject({
7660
+ script: z.looseObject({ expr: z.string() }).optional(),
7661
+ errorMessage: z.string()
7662
+ });
7663
+ const snapshotSerialSchema = z.looseObject({
7664
+ start: z.number(),
7665
+ maxValue: z.number().optional(),
7666
+ format: z.string().optional()
7667
+ });
7668
+ const snapshotEnumValueSchema = z.looseObject({
7669
+ value: z.string(),
7670
+ description: z.string().optional()
7671
+ });
7050
7672
  const snapshotFieldConfigSchema = z.looseObject({
7051
7673
  type: z.string(),
7052
7674
  required: z.boolean().default(true),
@@ -7960,11 +8582,29 @@ function reconstructSnapshotFromMigrations(migrationsDir, maxVersion) {
7960
8582
  * @returns {number} Latest migration number or 0 if no migrations exist
7961
8583
  */
7962
8584
  function getLatestMigrationNumber(migrationsDir) {
7963
- const files = getMigrationFiles(migrationsDir);
8585
+ return latestMigrationNumber(getMigrationFiles(migrationsDir));
8586
+ }
8587
+ function latestMigrationNumber(files) {
7964
8588
  if (files.length === 0) return 0;
7965
8589
  return Math.max(...files.map((f) => f.number));
7966
8590
  }
7967
8591
  /**
8592
+ * Assert that a migration number exists in the local migration history.
8593
+ * 0 is always accepted as the baseline snapshot.
8594
+ *
8595
+ * Returns the latest migration number so callers that need it (e.g. sync's
8596
+ * post-sync hint) can reuse this function's directory scan instead of
8597
+ * scanning the migrations directory a second time.
8598
+ * @param {string} migrationsDir - Migrations directory path
8599
+ * @param {number} migrationNumber - Migration number to check
8600
+ * @returns {number} The latest migration number in the history
8601
+ */
8602
+ function assertMigrationNumberExists(migrationsDir, migrationNumber) {
8603
+ const files = getMigrationFiles(migrationsDir);
8604
+ if (migrationNumber !== 0 && !files.some((f) => f.number === migrationNumber)) throw new Error(`Migration ${formatMigrationNumber(migrationNumber)} does not exist in working tree (latest is ${formatMigrationNumber(latestMigrationNumber(files))}).`);
8605
+ return latestMigrationNumber(files);
8606
+ }
8607
+ /**
7968
8608
  * Compare two field configs and determine if they are different
7969
8609
  * @param {SnapshotFieldConfig} oldField - Old field configuration
7970
8610
  * @param {SnapshotFieldConfig} newField - New field configuration
@@ -9206,7 +9846,11 @@ function generateTailorDBTypeManifestFromSnapshot(snapshotType, options = {}) {
9206
9846
  defaultQueryLimitSize: 100n,
9207
9847
  maxBulkUpsertSize: 1000n,
9208
9848
  pluralForm,
9209
- publishRecordEvents: snapshotType.settings?.publishEvents ?? options.publishRecordEvents ?? false
9849
+ publishRecordEvents: resolvePublishEvents({
9850
+ explicit: snapshotType.settings?.publishEvents,
9851
+ subscribed: options.subscribed ?? false,
9852
+ conflict: publishEventsConflict.tailorDBType(snapshotType.name)
9853
+ })
9210
9854
  };
9211
9855
  const ops = snapshotType.settings?.gqlOperations ?? options.namespaceGqlOperations;
9212
9856
  if (ops) defaultSettings.disableGqlOperations = {
@@ -9473,14 +10117,9 @@ function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
9473
10117
  const manifests = /* @__PURE__ */ new Map();
9474
10118
  const { executorUsedTypes, ...baseOptions } = options;
9475
10119
  for (const [typeName, snapshotType] of Object.entries(snapshot.types)) {
9476
- if (executorUsedTypes?.has(typeName) && snapshotType.settings?.publishEvents === false) throw new Error(`Type "${typeName}" has publishEvents set to false, but it is used by an executor with a record trigger. Either remove the publishEvents: false setting or remove the executor trigger for this type.`);
9477
- let publishRecordEvents;
9478
- if (snapshotType.settings?.publishEvents !== void 0) publishRecordEvents = snapshotType.settings.publishEvents;
9479
- else if (executorUsedTypes?.has(typeName)) publishRecordEvents = true;
9480
- else publishRecordEvents = baseOptions.publishRecordEvents ?? false;
9481
10120
  const typeOptions = {
9482
10121
  ...baseOptions,
9483
- publishRecordEvents
10122
+ subscribed: executorUsedTypes?.has(typeName) ?? false
9484
10123
  };
9485
10124
  manifests.set(typeName, generateTailorDBTypeManifestFromSnapshot(snapshotType, typeOptions));
9486
10125
  }
@@ -9751,6 +10390,11 @@ function applyPreMigrationIndexAdjustments(indexes, typeIndexChanges) {
9751
10390
  * Types for TailorDB migration execution
9752
10391
  */
9753
10392
  /**
10393
+ * Maximum length for Kubernetes label values
10394
+ * Labels must match pattern: ^[a-z][a-z0-9_-]{0,62}
10395
+ */
10396
+ const MAX_LABEL_LENGTH = 63;
10397
+ /**
9754
10398
  * Prefix added to migration numbers in labels (required because migration names start with numbers)
9755
10399
  */
9756
10400
  const MIGRATION_LABEL_PREFIX = "m";
@@ -9759,12 +10403,23 @@ const MIGRATION_LABEL_PREFIX = "m";
9759
10403
  */
9760
10404
  const MIGRATION_LABEL_KEY = "sdk-migration";
9761
10405
  /**
10406
+ * Sanitize migration number for use as label value
10407
+ * Label pattern: ^[a-z][a-z0-9_-]{0,62}
10408
+ * - Must start with lowercase letter (add prefix since migration numbers start with digits)
10409
+ * - Max 63 characters
10410
+ * @param {number} migrationNumber - Migration number to sanitize
10411
+ * @returns {string} Sanitized label value
10412
+ */
10413
+ function sanitizeMigrationLabel(migrationNumber) {
10414
+ return (MIGRATION_LABEL_PREFIX + formatMigrationNumber(migrationNumber)).slice(0, 63);
10415
+ }
10416
+ /**
9762
10417
  * Parse migration number from label value
9763
10418
  * @param {string} label - Label value (e.g., "m0001")
9764
10419
  * @returns {number | null} Parsed number or null if invalid
9765
10420
  */
9766
10421
  function parseMigrationLabelNumber(label) {
9767
- if (!label.startsWith("m")) return null;
10422
+ if (!label.startsWith(MIGRATION_LABEL_PREFIX)) return null;
9768
10423
  const numStr = label.slice(1);
9769
10424
  if (!/^\d+$/.test(numStr)) return null;
9770
10425
  const num = parseInt(numStr, 10);
@@ -9784,6 +10439,283 @@ function handleOptionalToRequiredError(error, messages) {
9784
10439
  throw error;
9785
10440
  }
9786
10441
 
10442
+ //#endregion
10443
+ //#region src/cli/commands/tailordb/migrate/schema-checks.ts
10444
+ /**
10445
+ * Schema validation checks shared between `deploy` and
10446
+ * `tailordb migration validate`.
10447
+ *
10448
+ * These checks are read-only: they compare local type definitions, the
10449
+ * migration snapshot history, and the remote schema without mutating any
10450
+ * state.
10451
+ */
10452
+ /**
10453
+ * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
10454
+ * @param service - Loaded TailorDB service (after `loadTypes()`)
10455
+ * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
10456
+ */
10457
+ function toTailorDBDeployInput(service) {
10458
+ const types = {};
10459
+ for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
10460
+ return {
10461
+ namespace: service.namespace,
10462
+ config: service.config,
10463
+ types
10464
+ };
10465
+ }
10466
+ /**
10467
+ * Fetch all TailorDB types from remote for a namespace
10468
+ * @param {OperatorClient} client - Operator client instance
10469
+ * @param {string} workspaceId - Workspace ID
10470
+ * @param {string} namespace - TailorDB namespace
10471
+ * @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
10472
+ */
10473
+ async function fetchRemoteTypes(client, workspaceId, namespace) {
10474
+ return fetchAllTolerant(async (pageToken, maxPageSize) => {
10475
+ const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
10476
+ workspaceId,
10477
+ namespaceName: namespace,
10478
+ pageToken,
10479
+ pageSize: maxPageSize
10480
+ });
10481
+ return [tailordbTypes, nextPageToken];
10482
+ });
10483
+ }
10484
+ async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
10485
+ return fetchAllTolerant(async (pageToken, maxPageSize) => {
10486
+ const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
10487
+ workspaceId,
10488
+ namespaceName: namespace,
10489
+ pageToken,
10490
+ pageSize: maxPageSize
10491
+ });
10492
+ return [permissions, nextPageToken];
10493
+ });
10494
+ }
10495
+ function definedWhenNotEmpty(value) {
10496
+ return Object.keys(value).length > 0 ? value : void 0;
10497
+ }
10498
+ function namespaceConfig(config, tailorDBInputs, namespace) {
10499
+ return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
10500
+ }
10501
+ function namespaceGqlOperations(config, tailorDBInputs, namespace) {
10502
+ return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
10503
+ }
10504
+ const GQL_OPERATION_KEYS = [
10505
+ "create",
10506
+ "update",
10507
+ "delete",
10508
+ "read"
10509
+ ];
10510
+ function configuredDisabledGqlOperations(operations) {
10511
+ if (!operations) return void 0;
10512
+ if (operations === "query") return {
10513
+ create: false,
10514
+ update: false,
10515
+ delete: false
10516
+ };
10517
+ const disabled = {};
10518
+ for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
10519
+ return definedWhenNotEmpty(disabled);
10520
+ }
10521
+ function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
10522
+ if (!remoteDisabled || !configuredDisabled) return void 0;
10523
+ const disabled = {};
10524
+ for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
10525
+ return definedWhenNotEmpty(disabled);
10526
+ }
10527
+ function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10528
+ const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
10529
+ const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
10530
+ const types = {};
10531
+ for (const [typeName, type] of Object.entries(snapshot.types)) {
10532
+ const settings = { ...type.settings };
10533
+ const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
10534
+ if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
10535
+ if (type.settings?.gqlOperations === void 0) {
10536
+ const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
10537
+ if (disabled) settings.gqlOperations = disabled;
10538
+ }
10539
+ const comparableType = { ...type };
10540
+ const comparableSettings = definedWhenNotEmpty(settings);
10541
+ if (comparableSettings) comparableType.settings = comparableSettings;
10542
+ else delete comparableType.settings;
10543
+ types[typeName] = comparableType;
10544
+ }
10545
+ return {
10546
+ ...snapshot,
10547
+ types
10548
+ };
10549
+ }
10550
+ async function getRemoteMigrationNumberState(client, workspaceId, namespace) {
10551
+ const trn = resourceTrn(workspaceId, "tailordb", namespace);
10552
+ const metadata = await getOrNull(async () => {
10553
+ const { metadata } = await client.getMetadata({ trn });
10554
+ return metadata;
10555
+ });
10556
+ if (!metadata) return {
10557
+ metadataExists: false,
10558
+ number: null
10559
+ };
10560
+ const label = metadata.labels[MIGRATION_LABEL_KEY];
10561
+ if (!label) return {
10562
+ metadataExists: true,
10563
+ number: null
10564
+ };
10565
+ return {
10566
+ metadataExists: true,
10567
+ number: parseMigrationLabelNumber(label)
10568
+ };
10569
+ }
10570
+ /**
10571
+ * Get the current migration number from remote metadata
10572
+ * @param {OperatorClient} client - Operator client instance
10573
+ * @param {string} workspaceId - Workspace ID
10574
+ * @param {string} namespace - TailorDB namespace
10575
+ * @returns {Promise<number | null>} Current migration number, or null if no migration label exists
10576
+ */
10577
+ async function getRemoteMigrationNumber(client, workspaceId, namespace) {
10578
+ return (await getRemoteMigrationNumberState(client, workspaceId, namespace)).number;
10579
+ }
10580
+ /**
10581
+ * Verify remote schema matches the expected snapshot state
10582
+ * @param {OperatorClient} client - Operator client instance
10583
+ * @param {string} workspaceId - Workspace ID
10584
+ * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
10585
+ * @param {LoadedConfig} config - Loaded application config
10586
+ * @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
10587
+ * @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
10588
+ */
10589
+ async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
10590
+ const results = [];
10591
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10592
+ const { metadataExists, number: remoteMigrationNumber } = await getRemoteMigrationNumberState(client, workspaceId, namespace);
10593
+ if (remoteMigrationNumber === null) {
10594
+ results.push({
10595
+ namespace,
10596
+ remoteMigrationNumber: 0,
10597
+ drifts: [],
10598
+ hasDrift: false,
10599
+ skipped: metadataExists ? "no_migration_label" : "not_deployed"
10600
+ });
10601
+ continue;
10602
+ }
10603
+ if (remoteMigrationNumber > getLatestMigrationNumber(migrationsDir)) {
10604
+ results.push({
10605
+ namespace,
10606
+ remoteMigrationNumber,
10607
+ drifts: [],
10608
+ hasDrift: false,
10609
+ checkpointMissingLocal: true
10610
+ });
10611
+ continue;
10612
+ }
10613
+ const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
10614
+ if (!expectedSnapshot) {
10615
+ results.push({
10616
+ namespace,
10617
+ remoteMigrationNumber,
10618
+ drifts: [],
10619
+ hasDrift: false,
10620
+ skipped: "no_snapshot"
10621
+ });
10622
+ continue;
10623
+ }
10624
+ const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
10625
+ const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
10626
+ results.push({
10627
+ namespace,
10628
+ remoteMigrationNumber,
10629
+ drifts,
10630
+ hasDrift: drifts.length > 0
10631
+ });
10632
+ }
10633
+ return results;
10634
+ }
10635
+ /**
10636
+ * Log common causes of remote schema drift and how to resolve them
10637
+ */
10638
+ function logRemoteDriftGuidance() {
10639
+ logger.info("This may indicate:");
10640
+ logger.info(" - Another developer applied different migrations", { mode: "plain" });
10641
+ logger.info(" - Manual schema changes were made directly", { mode: "plain" });
10642
+ logger.info(" - Migration history is out of sync", { mode: "plain" });
10643
+ logger.newline();
10644
+ logger.info("To resolve:");
10645
+ logger.info(" - Run 'tailor tailordb migration status' to compare local vs remote.", { mode: "plain" });
10646
+ logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
10647
+ logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
10648
+ logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
10649
+ }
10650
+ /**
10651
+ * Format remote schema verification results for display
10652
+ * @param {RemoteSchemaVerificationResult[]} results - Verification results
10653
+ * @returns {string} Formatted results string
10654
+ */
10655
+ function formatRemoteVerificationResults(results) {
10656
+ const lines = [];
10657
+ for (const result of results) {
10658
+ if (!result.hasDrift) continue;
10659
+ lines.push(`Namespace: ${result.namespace}`);
10660
+ lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
10661
+ lines.push(` Differences:`);
10662
+ lines.push(formatSchemaDrifts(result.drifts));
10663
+ lines.push("");
10664
+ }
10665
+ return lines.join("\n");
10666
+ }
10667
+ /**
10668
+ * Check if there are schema differences between migration snapshots and local definitions
10669
+ * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
10670
+ * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
10671
+ * @returns {Promise<MigrationCheckResult[]>} Results for each namespace
10672
+ */
10673
+ async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
10674
+ const results = [];
10675
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10676
+ const localTypes = typesByNamespace.get(namespace);
10677
+ if (!localTypes) continue;
10678
+ const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
10679
+ if (!previousSnapshot) {
10680
+ results.push({
10681
+ namespace,
10682
+ migrationsDir,
10683
+ hasDiff: true,
10684
+ diff: void 0
10685
+ });
10686
+ continue;
10687
+ }
10688
+ const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
10689
+ results.push({
10690
+ namespace,
10691
+ migrationsDir,
10692
+ hasDiff: hasChanges(diff),
10693
+ diff: hasChanges(diff) ? diff : void 0
10694
+ });
10695
+ }
10696
+ return results;
10697
+ }
10698
+ /**
10699
+ * Format migration check results for display
10700
+ * @param {MigrationCheckResult[]} results - Migration check results
10701
+ * @returns {string} Formatted results string
10702
+ */
10703
+ function formatMigrationCheckResults(results) {
10704
+ const lines = [];
10705
+ for (const result of results) {
10706
+ if (!result.hasDiff) continue;
10707
+ lines.push(`Namespace: ${result.namespace}`);
10708
+ if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor tailordb migration generate' first.");
10709
+ else {
10710
+ lines.push(` ${formatDiffSummary(result.diff)}`);
10711
+ lines.push("");
10712
+ lines.push(formatMigrationDiff(result.diff));
10713
+ }
10714
+ lines.push("");
10715
+ }
10716
+ return lines.join("\n");
10717
+ }
10718
+
9787
10719
  //#endregion
9788
10720
  //#region src/cli/commands/tailordb/migrate/bundler.ts
9789
10721
  /**
@@ -9832,31 +10764,39 @@ async function bundleMigrationScript(sourceFile, namespace, migrationNumber, env
9832
10764
  }
9833
10765
  `;
9834
10766
  fs$2.writeFileSync(entryPath, entryContent);
9835
- const tsconfig = await resolveTSConfigWithFallback(baseDir ?? path.dirname(absoluteSourcePath));
10767
+ const projectDir = baseDir ?? path.dirname(absoluteSourcePath);
10768
+ const tsconfig = await resolveTSConfigWithFallback(projectDir);
10769
+ const bundleLog = createBundleLog({ tsconfig });
10770
+ const result = await rolldown.build({
10771
+ plugins: [
10772
+ createGeneratedEntryResolverPlugin(entryPath, projectDir),
10773
+ createTsconfigPathsPlugin(),
10774
+ platformBundleDefinePlugin
10775
+ ],
10776
+ input: entryPath,
10777
+ write: false,
10778
+ output: {
10779
+ format: "esm",
10780
+ sourcemap: false,
10781
+ minify: false,
10782
+ codeSplitting: false,
10783
+ globals: { tailordb: "tailordb" }
10784
+ },
10785
+ external: ["tailordb"],
10786
+ resolve: { conditionNames: ["node", "import"] },
10787
+ tsconfig,
10788
+ treeshake: {
10789
+ moduleSideEffects: false,
10790
+ annotations: true,
10791
+ unknownGlobalSideEffects: false
10792
+ },
10793
+ ...bundleLog.options
10794
+ });
10795
+ bundleLog.assertAllResolved();
9836
10796
  return {
9837
10797
  namespace,
9838
10798
  migrationNumber,
9839
- bundledCode: (await rolldown.build({
9840
- plugins: [platformBundleDefinePlugin],
9841
- input: entryPath,
9842
- write: false,
9843
- output: {
9844
- format: "esm",
9845
- sourcemap: false,
9846
- minify: false,
9847
- codeSplitting: false,
9848
- globals: { tailordb: "tailordb" }
9849
- },
9850
- external: ["tailordb"],
9851
- resolve: { conditionNames: ["node", "import"] },
9852
- tsconfig,
9853
- treeshake: {
9854
- moduleSideEffects: false,
9855
- annotations: true,
9856
- unknownGlobalSideEffects: false
9857
- },
9858
- logLevel: "silent"
9859
- })).output[0].code
10799
+ bundledCode: result.output[0].code
9860
10800
  };
9861
10801
  }
9862
10802
 
@@ -10201,16 +11141,9 @@ async function executeSingleMigration(options, migration) {
10201
11141
  * @returns {Promise<void>}
10202
11142
  */
10203
11143
  async function updateMigrationLabel(client, workspaceId, namespace, migrationNumber) {
10204
- const trn = resourceTrn(workspaceId, "tailordb", namespace);
10205
- const { metadata } = await client.getMetadata({ trn });
10206
- const existingLabels = metadata?.labels ?? {};
10207
- const newLabel = `m${formatMigrationNumber(migrationNumber)}`;
10208
- await client.setMetadata({
10209
- trn,
10210
- labels: {
10211
- ...existingLabels,
10212
- [MIGRATION_LABEL_KEY]: newLabel
10213
- }
11144
+ await writeMetadataLabels(client, {
11145
+ trn: resourceTrn(workspaceId, "tailordb", namespace),
11146
+ labels: { [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber) }
10214
11147
  });
10215
11148
  }
10216
11149
  /**
@@ -10285,169 +11218,6 @@ function groupMigrationsByNamespace(migrations) {
10285
11218
 
10286
11219
  //#endregion
10287
11220
  //#region src/cli/commands/deploy/tailordb/index.ts
10288
- /**
10289
- * Fetch all TailorDB types from remote for a namespace
10290
- * @param {OperatorClient} client - Operator client instance
10291
- * @param {string} workspaceId - Workspace ID
10292
- * @param {string} namespace - TailorDB namespace
10293
- * @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
10294
- */
10295
- async function fetchRemoteTypes(client, workspaceId, namespace) {
10296
- return fetchAllTolerant(async (pageToken, maxPageSize) => {
10297
- const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
10298
- workspaceId,
10299
- namespaceName: namespace,
10300
- pageToken,
10301
- pageSize: maxPageSize
10302
- });
10303
- return [tailordbTypes, nextPageToken];
10304
- });
10305
- }
10306
- async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
10307
- return fetchAllTolerant(async (pageToken, maxPageSize) => {
10308
- const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
10309
- workspaceId,
10310
- namespaceName: namespace,
10311
- pageToken,
10312
- pageSize: maxPageSize
10313
- });
10314
- return [permissions, nextPageToken];
10315
- });
10316
- }
10317
- function definedWhenNotEmpty(value) {
10318
- return Object.keys(value).length > 0 ? value : void 0;
10319
- }
10320
- function namespaceConfig(config, tailorDBInputs, namespace) {
10321
- return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
10322
- }
10323
- function namespaceGqlOperations(config, tailorDBInputs, namespace) {
10324
- return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
10325
- }
10326
- const GQL_OPERATION_KEYS = [
10327
- "create",
10328
- "update",
10329
- "delete",
10330
- "read"
10331
- ];
10332
- function configuredDisabledGqlOperations(operations) {
10333
- if (!operations) return void 0;
10334
- if (operations === "query") return {
10335
- create: false,
10336
- update: false,
10337
- delete: false
10338
- };
10339
- const disabled = {};
10340
- for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
10341
- return definedWhenNotEmpty(disabled);
10342
- }
10343
- function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
10344
- if (!remoteDisabled || !configuredDisabled) return void 0;
10345
- const disabled = {};
10346
- for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
10347
- return definedWhenNotEmpty(disabled);
10348
- }
10349
- function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10350
- const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
10351
- const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
10352
- const types = {};
10353
- for (const [typeName, type] of Object.entries(snapshot.types)) {
10354
- const settings = { ...type.settings };
10355
- const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
10356
- if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
10357
- if (type.settings?.gqlOperations === void 0) {
10358
- const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
10359
- if (disabled) settings.gqlOperations = disabled;
10360
- }
10361
- const comparableType = { ...type };
10362
- const comparableSettings = definedWhenNotEmpty(settings);
10363
- if (comparableSettings) comparableType.settings = comparableSettings;
10364
- else delete comparableType.settings;
10365
- types[typeName] = comparableType;
10366
- }
10367
- return {
10368
- ...snapshot,
10369
- types
10370
- };
10371
- }
10372
- /**
10373
- * Get the current migration number from remote metadata
10374
- * @param {OperatorClient} client - Operator client instance
10375
- * @param {string} workspaceId - Workspace ID
10376
- * @param {string} namespace - TailorDB namespace
10377
- * @returns {Promise<number | null>} Current migration number, or null if no migration label exists
10378
- */
10379
- async function getRemoteMigrationNumber(client, workspaceId, namespace) {
10380
- try {
10381
- const trn = resourceTrn(workspaceId, "tailordb", namespace);
10382
- const { metadata } = await client.getMetadata({ trn });
10383
- const label = metadata?.labels["sdk-migration"];
10384
- if (!label) return null;
10385
- const match = label.match(/^m(\d+)$/);
10386
- return match ? parseInt(assertDefined(match[1], "migration label capture group missing"), 10) : null;
10387
- } catch {
10388
- return null;
10389
- }
10390
- }
10391
- /**
10392
- * Verify remote schema matches the expected snapshot state
10393
- * @param {OperatorClient} client - Operator client instance
10394
- * @param {string} workspaceId - Workspace ID
10395
- * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
10396
- * @param {LoadedConfig} config - Loaded application config
10397
- * @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
10398
- * @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
10399
- */
10400
- async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
10401
- const results = [];
10402
- for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10403
- const remoteMigrationNumber = await getRemoteMigrationNumber(client, workspaceId, namespace);
10404
- if (remoteMigrationNumber === null) {
10405
- results.push({
10406
- namespace,
10407
- remoteMigrationNumber: 0,
10408
- drifts: [],
10409
- hasDrift: false
10410
- });
10411
- continue;
10412
- }
10413
- const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
10414
- if (!expectedSnapshot) {
10415
- results.push({
10416
- namespace,
10417
- remoteMigrationNumber,
10418
- drifts: [],
10419
- hasDrift: false
10420
- });
10421
- continue;
10422
- }
10423
- const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
10424
- const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
10425
- results.push({
10426
- namespace,
10427
- remoteMigrationNumber,
10428
- drifts,
10429
- hasDrift: drifts.length > 0
10430
- });
10431
- }
10432
- return results;
10433
- }
10434
- /**
10435
- * Format remote schema verification results for display
10436
- * @param {RemoteSchemaVerificationResult[]} results - Verification results
10437
- * @returns {string} Formatted results string
10438
- */
10439
- function formatRemoteVerificationResults(results) {
10440
- const lines = [];
10441
- for (const result of results) {
10442
- if (!result.hasDrift) continue;
10443
- lines.push(`Namespace: ${result.namespace}`);
10444
- lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
10445
- lines.push(` Differences:`);
10446
- lines.push(formatSchemaDrifts(result.drifts));
10447
- lines.push("");
10448
- }
10449
- return lines.join("\n");
10450
- }
10451
11221
  const MIGRATION_FILE_KINDS = [
10452
11222
  "schema",
10453
11223
  "diff",
@@ -10506,20 +11276,19 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
10506
11276
  throw new Error("Schema migration check failed");
10507
11277
  }
10508
11278
  const remoteVerificationResults = await verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs);
11279
+ const missingCheckpointResults = remoteVerificationResults.filter((result) => result.checkpointMissingLocal);
11280
+ if (missingCheckpointResults.length > 0) {
11281
+ logger.error("Remote migration checkpoint is not in the local migration history:");
11282
+ for (const result of missingCheckpointResults) logger.log(` ${result.namespace}: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
11283
+ logger.newline();
11284
+ logger.info("Pull the latest migration files, or run 'tailor tailordb migration status' to compare.");
11285
+ throw new Error("Remote migration checkpoint verification failed");
11286
+ }
10509
11287
  if (remoteVerificationResults.some((r) => r.hasDrift)) {
10510
11288
  logger.error("Remote schema drift detected:");
10511
11289
  logger.log(formatRemoteVerificationResults(remoteVerificationResults));
10512
11290
  logger.newline();
10513
- logger.info("This may indicate:");
10514
- logger.info(" - Another developer applied different migrations", { mode: "plain" });
10515
- logger.info(" - Manual schema changes were made directly", { mode: "plain" });
10516
- logger.info(" - Migration history is out of sync", { mode: "plain" });
10517
- logger.newline();
10518
- logger.info("To resolve:");
10519
- logger.info(" - Run 'tailor tailordb migration status' to compare local vs remote.", { mode: "plain" });
10520
- logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
10521
- logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
10522
- logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
11291
+ logRemoteDriftGuidance();
10523
11292
  logger.newline();
10524
11293
  logger.info("Use '--no-schema-check' to skip this check (not recommended).");
10525
11294
  throw new Error("Remote schema verification failed");
@@ -10557,7 +11326,7 @@ async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigra
10557
11326
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10558
11327
  if (getMigrationFiles(migrationsDir).length === 0) continue;
10559
11328
  const targetVersion = getLatestMigrationNumber(migrationsDir);
10560
- const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace);
11329
+ const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace).catch(() => null);
10561
11330
  if (currentVersion === targetVersion) continue;
10562
11331
  await updateMigrationLabel(client, workspaceId, namespace, targetVersion);
10563
11332
  const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
@@ -10663,14 +11432,26 @@ async function applyTailorDB(client, result, phase = "create-update") {
10663
11432
  });
10664
11433
  if (remainingGqlPermissionDeletes.length > 0) await Promise.all(remainingGqlPermissionDeletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
10665
11434
  await Promise.all(changeSet.type.deletes.filter((del) => isOutsideMigrations(del.request.namespaceName) && !deletedResources.types.has(del.name)).map((del) => client.deleteTailorDBType(del.request)));
11435
+ await Promise.all([
11436
+ ...changeSet.type.creates,
11437
+ ...changeSet.type.updates,
11438
+ ...changeSet.type.unchanged
11439
+ ].filter((entry) => entry.metaRequest && !deletedResources.types.has(entry.name)).flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : []));
10666
11440
  } else {
10667
11441
  await Promise.all([...changeSet.service.creates.map(async (create) => {
10668
11442
  await client.createTailorDBService(create.request);
10669
- await client.setMetadata(create.metaRequest);
10670
- }), ...changeSet.service.updates.map((update) => client.setMetadata(update.metaRequest))]);
11443
+ await writeMetadataLabels(client, create.metaRequest);
11444
+ }), ...changeSet.service.updates.map((update) => writeMetadataLabels(client, update.metaRequest))]);
10671
11445
  try {
10672
- for (const create of changeSet.type.creates) await client.createTailorDBType(create.request);
10673
- for (const update of changeSet.type.updates) await client.updateTailorDBType(update.request);
11446
+ for (const create of changeSet.type.creates) {
11447
+ await client.createTailorDBType(create.request);
11448
+ await writeMetadataLabels(client, create.metaRequest);
11449
+ }
11450
+ for (const update of changeSet.type.updates) {
11451
+ await client.updateTailorDBType(update.request);
11452
+ await writeMetadataLabels(client, update.metaRequest);
11453
+ }
11454
+ await Promise.all(changeSet.type.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : []));
10674
11455
  } catch (error) {
10675
11456
  handleOptionalToRequiredError(error, ["Run 'tailor tailordb migration generate' to create migration files.", "Migration scripts allow you to handle existing data before applying the schema change."]);
10676
11457
  }
@@ -10713,8 +11494,8 @@ function getDeletedTypeNames(migration) {
10713
11494
  async function executeServicesCreation(client, changeSet) {
10714
11495
  await Promise.all([...changeSet.service.creates.map(async (create) => {
10715
11496
  await client.createTailorDBService(create.request);
10716
- await client.setMetadata(create.metaRequest);
10717
- }), ...changeSet.service.updates.map((update) => client.setMetadata(update.metaRequest))]);
11497
+ await writeMetadataLabels(client, create.metaRequest);
11498
+ }), ...changeSet.service.updates.map((update) => writeMetadataLabels(client, update.metaRequest))]);
10718
11499
  }
10719
11500
  /**
10720
11501
  * Track which types have been created/updated across migrations
@@ -10759,7 +11540,7 @@ function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executor
10759
11540
  if (!snapshotType) return void 0;
10760
11541
  const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
10761
11542
  return generateTailorDBTypeManifestFromSnapshot(snapshotType, {
10762
- publishRecordEvents: executorUsedTypes.has(snapshotType.name),
11543
+ subscribed: executorUsedTypes.has(snapshotType.name),
10763
11544
  namespaceGqlOperations: input?.config.gqlOperations
10764
11545
  });
10765
11546
  }
@@ -10962,7 +11743,7 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
10962
11743
  try {
10963
11744
  if (priorType) {
10964
11745
  const manifest = generateTailorDBTypeManifestFromSnapshot(priorType, {
10965
- publishRecordEvents: executorUsedTypes.has(priorType.name),
11746
+ subscribed: executorUsedTypes.has(priorType.name),
10966
11747
  namespaceGqlOperations: input?.config.gqlOperations
10967
11748
  });
10968
11749
  await client.updateTailorDBType({
@@ -10988,19 +11769,10 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
10988
11769
  }
10989
11770
  }
10990
11771
  /**
10991
- * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
10992
- * @param service - Loaded TailorDB service (after `loadTypes()`)
10993
- * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
11772
+ * Plan TailorDB-related changes based on current and desired state.
11773
+ * @param context - Planning context
11774
+ * @returns Planned changes
10994
11775
  */
10995
- function toTailorDBDeployInput(service) {
10996
- const types = {};
10997
- for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
10998
- return {
10999
- namespace: service.namespace,
11000
- config: service.config,
11001
- types
11002
- };
11003
- }
11004
11776
  async function planTailorDB(context) {
11005
11777
  const { client, workspaceId, application, forRemoval, config, noSchemaCheck, forceApplyAll = false } = context;
11006
11778
  const tailordbs = [];
@@ -11019,7 +11791,12 @@ async function planTailorDB(context) {
11019
11791
  } : await validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck ?? false, tailordbs);
11020
11792
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
11021
11793
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
11022
- const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
11794
+ const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll, {
11795
+ appName: application.name,
11796
+ appId: application.id,
11797
+ dependentApps: context.dependentApps,
11798
+ runAppIds: context.runAppIds
11799
+ }), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
11023
11800
  typeChangeSet.creates.sort(byName);
11024
11801
  typeChangeSet.updates.sort(byName);
11025
11802
  typeChangeSet.deletes.sort(byName);
@@ -11118,8 +11895,7 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
11118
11895
  const metaRequest = await buildMetaRequest({
11119
11896
  trn: resourceTrn(workspaceId, "tailordb", tailordb.namespace),
11120
11897
  appName,
11121
- appId,
11122
- existingLabels: existing?.allLabels
11898
+ appId
11123
11899
  });
11124
11900
  if (existing) {
11125
11901
  if (trackDesiredResourceOwnership({
@@ -11170,8 +11946,29 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
11170
11946
  resourceOwners
11171
11947
  };
11172
11948
  }
11173
- async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, filteredTypesByNamespace, forceApplyAll = false) {
11949
+ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, filteredTypesByNamespace, forceApplyAll = false, records = {}) {
11174
11950
  const changeSet = createChangeSet("TailorDB types");
11951
+ const { appName, appId, dependentApps, runAppIds } = records;
11952
+ /**
11953
+ * Build one type's metadata write, carrying the dependency records that belong
11954
+ * to it. The type is what publishes record events, so the record lives there.
11955
+ * @param namespace - Namespace holding the type
11956
+ * @param typeName - Type name
11957
+ * @param explicitPublishEvents - `publishEvents` declared on the type, if any
11958
+ * @returns The type's metadata write
11959
+ */
11960
+ const typeMetaRequest = async (namespace, typeName, explicitPublishEvents) => {
11961
+ return addDependencyRecords(await buildMetaRequest({
11962
+ trn: tailorDBTypeTrn(workspaceId, namespace, typeName),
11963
+ appName: appName ?? "",
11964
+ appId
11965
+ }), {
11966
+ key: eventSourceKey.tailorDBType(namespace, typeName),
11967
+ dependentApps,
11968
+ runAppIds,
11969
+ pinned: explicitPublishEvents !== void 0
11970
+ });
11971
+ };
11175
11972
  const fetchTypes = (namespaceName) => {
11176
11973
  return fetchAllTolerant(async (pageToken, maxPageSize) => {
11177
11974
  const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
@@ -11185,27 +11982,36 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
11185
11982
  };
11186
11983
  for (const tailordb of tailordbs) {
11187
11984
  const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
11188
- for (const [typeName, type] of Object.entries(types)) if (executorUsedTypes.has(typeName) && type.settings?.publishEvents === false) throw new Error(`Type "${typeName}" has publishEvents set to false, but it is used by an executor with a record trigger. Either remove the publishEvents: false setting or remove the executor trigger for this type.`);
11985
+ for (const [typeName, type] of Object.entries(types)) assertNoPublishEventsConflict({
11986
+ explicit: type.settings?.publishEvents,
11987
+ subscribed: executorUsedTypes.has(typeName),
11988
+ conflict: publishEventsConflict.tailorDBType(typeName)
11989
+ });
11189
11990
  }
11190
11991
  for (const tailordb of tailordbs) {
11191
11992
  const existingTypes = await fetchTypes(tailordb.namespace);
11192
11993
  const existingTypesMap = new Map(existingTypes.map((type) => [type.name, type]));
11193
11994
  const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
11995
+ const typeMeta = (typeName) => typeMetaRequest(tailordb.namespace, typeName, types[typeName]?.settings?.publishEvents);
11194
11996
  for (const [typeName, tailordbTypeSnapshot] of Object.entries(types)) {
11195
11997
  const tailordbType = generateTailorDBTypeManifestFromSnapshot(tailordbTypeSnapshot, {
11196
- publishRecordEvents: executorUsedTypes.has(typeName),
11998
+ subscribed: executorUsedTypes.has(typeName),
11197
11999
  namespaceGqlOperations: tailordb.config.gqlOperations
11198
12000
  });
11199
12001
  const existingType = existingTypesMap.get(typeName);
11200
12002
  if (existingType) {
11201
- if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({ name: typeName });
12003
+ if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({
12004
+ name: typeName,
12005
+ metaRequest: await typeMeta(typeName)
12006
+ });
11202
12007
  else changeSet.updates.push({
11203
12008
  name: typeName,
11204
12009
  request: {
11205
12010
  workspaceId,
11206
12011
  namespaceName: tailordb.namespace,
11207
12012
  tailordbType
11208
- }
12013
+ },
12014
+ metaRequest: await typeMeta(typeName)
11209
12015
  });
11210
12016
  existingTypesMap.delete(typeName);
11211
12017
  } else changeSet.creates.push({
@@ -11214,7 +12020,8 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
11214
12020
  workspaceId,
11215
12021
  namespaceName: tailordb.namespace,
11216
12022
  tailordbType
11217
- }
12023
+ },
12024
+ metaRequest: await typeMeta(typeName)
11218
12025
  });
11219
12026
  }
11220
12027
  existingTypesMap.forEach((_type, name) => {
@@ -11375,83 +12182,32 @@ async function planGqlPermissions(client, workspaceId, tailordbs, deletedService
11375
12182
  }
11376
12183
  existingNameSet.forEach((name) => {
11377
12184
  changeSet.deletes.push({
11378
- name,
11379
- request: {
11380
- workspaceId,
11381
- namespaceName: tailordb.namespace,
11382
- typeName: name
11383
- }
11384
- });
11385
- });
11386
- }
11387
- for (const namespaceName of deletedServices) (await fetchGqlPermissions(namespaceName)).forEach((gqlPermission) => {
11388
- changeSet.deletes.push({
11389
- name: gqlPermission.typeName,
11390
- request: {
11391
- workspaceId,
11392
- namespaceName,
11393
- typeName: gqlPermission.typeName
11394
- }
11395
- });
11396
- });
11397
- return changeSet;
11398
- }
11399
- function normalizeComparableGqlPermission(permission) {
11400
- return { policies: (normalizeProtoConfig(permission)?.policies ?? []).map((policy) => ({
11401
- ...policy,
11402
- actions: (policy.actions ?? []).toSorted((left, right) => left - right)
11403
- })) };
11404
- }
11405
- /**
11406
- * Check if there are schema differences between migration snapshots and local definitions
11407
- * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
11408
- * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
11409
- * @returns {Promise<MigrationCheckResult[]>} Results for each namespace
11410
- */
11411
- async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
11412
- const results = [];
11413
- for (const { namespace, migrationsDir } of namespacesWithMigrations) {
11414
- const localTypes = typesByNamespace.get(namespace);
11415
- if (!localTypes) continue;
11416
- const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
11417
- if (!previousSnapshot) {
11418
- results.push({
11419
- namespace,
11420
- migrationsDir,
11421
- hasDiff: true,
11422
- diff: void 0
11423
- });
11424
- continue;
11425
- }
11426
- const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
11427
- results.push({
11428
- namespace,
11429
- migrationsDir,
11430
- hasDiff: hasChanges(diff),
11431
- diff: hasChanges(diff) ? diff : void 0
12185
+ name,
12186
+ request: {
12187
+ workspaceId,
12188
+ namespaceName: tailordb.namespace,
12189
+ typeName: name
12190
+ }
12191
+ });
11432
12192
  });
11433
12193
  }
11434
- return results;
12194
+ for (const namespaceName of deletedServices) (await fetchGqlPermissions(namespaceName)).forEach((gqlPermission) => {
12195
+ changeSet.deletes.push({
12196
+ name: gqlPermission.typeName,
12197
+ request: {
12198
+ workspaceId,
12199
+ namespaceName,
12200
+ typeName: gqlPermission.typeName
12201
+ }
12202
+ });
12203
+ });
12204
+ return changeSet;
11435
12205
  }
11436
- /**
11437
- * Format migration check results for display
11438
- * @param {MigrationCheckResult[]} results - Migration check results
11439
- * @returns {string} Formatted results string
11440
- */
11441
- function formatMigrationCheckResults(results) {
11442
- const lines = [];
11443
- for (const result of results) {
11444
- if (!result.hasDiff) continue;
11445
- lines.push(`Namespace: ${result.namespace}`);
11446
- if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor tailordb migration generate' first.");
11447
- else {
11448
- lines.push(` ${formatDiffSummary(result.diff)}`);
11449
- lines.push("");
11450
- lines.push(formatMigrationDiff(result.diff));
11451
- }
11452
- lines.push("");
11453
- }
11454
- return lines.join("\n");
12206
+ function normalizeComparableGqlPermission(permission) {
12207
+ return { policies: (normalizeProtoConfig(permission)?.policies ?? []).map((policy) => ({
12208
+ ...policy,
12209
+ actions: (policy.actions ?? []).toSorted((left, right) => left - right)
12210
+ })) };
11455
12211
  }
11456
12212
 
11457
12213
  //#endregion
@@ -11466,32 +12222,38 @@ function formatMigrationCheckResults(results) {
11466
12222
  async function applyWorkflow(client, result, phase = "create-update") {
11467
12223
  const { changeSet, appName, appId } = result;
11468
12224
  if (phase === "create-update") {
11469
- const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames);
11470
- await Promise.all([...changeSet.creates.map(async (create) => {
11471
- const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
11472
- const shape = buildWorkflowValidationShape(create.workspaceId, create.workflow);
11473
- await client.createWorkflow({
11474
- workspaceId: shape.workspaceId,
11475
- workflowName: shape.workflowName,
11476
- mainJobFunctionName: shape.mainJobFunctionName,
11477
- retryPolicy: shape.retryPolicy,
11478
- concurrencyPolicy: shape.concurrencyPolicy,
11479
- jobFunctions: filteredVersions
11480
- });
11481
- await client.setMetadata(create.metaRequest);
11482
- }), ...changeSet.updates.map(async (update) => {
11483
- const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, update.usedJobNames);
11484
- const shape = buildWorkflowValidationShape(update.workspaceId, update.workflow);
11485
- await client.updateWorkflow({
11486
- workspaceId: shape.workspaceId,
11487
- workflowName: shape.workflowName,
11488
- mainJobFunctionName: shape.mainJobFunctionName,
11489
- retryPolicy: shape.retryPolicy,
11490
- concurrencyPolicy: shape.concurrencyPolicy,
11491
- jobFunctions: filteredVersions
11492
- });
11493
- await client.setMetadata(update.metaRequest);
11494
- })]);
12225
+ const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames, result.jobFunctionPublishEvents);
12226
+ await Promise.all([
12227
+ ...changeSet.creates.map(async (create) => {
12228
+ const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
12229
+ const shape = buildWorkflowValidationShape(create.workspaceId, create.workflow);
12230
+ await client.createWorkflow({
12231
+ workspaceId: shape.workspaceId,
12232
+ workflowName: shape.workflowName,
12233
+ mainJobFunctionName: shape.mainJobFunctionName,
12234
+ retryPolicy: shape.retryPolicy,
12235
+ concurrencyPolicy: shape.concurrencyPolicy,
12236
+ jobFunctions: filteredVersions,
12237
+ publishExecutionEvents: shape.publishExecutionEvents
12238
+ });
12239
+ await writeMetadataLabels(client, create.metaRequest);
12240
+ }),
12241
+ ...changeSet.updates.map(async (update) => {
12242
+ const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, update.usedJobNames);
12243
+ const shape = buildWorkflowValidationShape(update.workspaceId, update.workflow);
12244
+ await client.updateWorkflow({
12245
+ workspaceId: shape.workspaceId,
12246
+ workflowName: shape.workflowName,
12247
+ mainJobFunctionName: shape.mainJobFunctionName,
12248
+ retryPolicy: shape.retryPolicy,
12249
+ concurrencyPolicy: shape.concurrencyPolicy,
12250
+ jobFunctions: filteredVersions,
12251
+ publishExecutionEvents: shape.publishExecutionEvents
12252
+ });
12253
+ await writeMetadataLabels(client, update.metaRequest);
12254
+ }),
12255
+ ...changeSet.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
12256
+ ]);
11495
12257
  } else {
11496
12258
  await deleteAllSettled(changeSet.deletes.map((del) => ({
11497
12259
  resourceType: "workflow",
@@ -11549,9 +12311,10 @@ function filterJobFunctionVersions(allVersions, usedJobNames) {
11549
12311
  * @param appName - Application name
11550
12312
  * @param appId - Application ID used for job function metadata when available
11551
12313
  * @param unchangedWorkflowJobNames - Job function names used by unchanged workflows
12314
+ * @param jobFunctionPublishEvents - Resolved `publishExecutionEvents` keyed by job function name
11552
12315
  * @returns Map of job function names to versions
11553
12316
  */
11554
- async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
12317
+ async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set(), jobFunctionPublishEvents = /* @__PURE__ */ new Map()) {
11555
12318
  const jobFunctionVersions = {};
11556
12319
  const firstWorkflow = changeSet.creates[0] || changeSet.updates[0] || changeSet.deletes[0];
11557
12320
  if (!firstWorkflow) return jobFunctionVersions;
@@ -11570,16 +12333,15 @@ async function registerJobFunctions(client, changeSet, appName, appId, unchanged
11570
12333
  const existingJobNamesSet = new Set(existingJobFunctions);
11571
12334
  if (changeSet.creates.length > 0 || changeSet.updates.length > 0) {
11572
12335
  const results = await Promise.all(Array.from(allUsedJobNames).map(async (jobName) => {
11573
- const response = existingJobNamesSet.has(jobName) ? await client.updateWorkflowJobFunction({
11574
- workspaceId,
11575
- jobFunctionName: jobName,
11576
- scriptRef: workflowJobFunctionName(jobName)
11577
- }) : await client.createWorkflowJobFunction({
12336
+ const isExisting = existingJobNamesSet.has(jobName);
12337
+ const request = {
11578
12338
  workspaceId,
11579
12339
  jobFunctionName: jobName,
11580
- scriptRef: workflowJobFunctionName(jobName)
11581
- });
11582
- await client.setMetadata(await buildMetaRequest({
12340
+ scriptRef: workflowJobFunctionName(jobName),
12341
+ publishExecutionEvents: jobFunctionPublishEvents.get(jobName) ?? false
12342
+ };
12343
+ const response = isExisting ? await client.updateWorkflowJobFunction(request) : await client.createWorkflowJobFunction(request);
12344
+ await writeMetadataLabels(client, await buildMetaRequest({
11583
12345
  trn: resourceTrn(workspaceId, "workflow_job_function", jobName),
11584
12346
  appName,
11585
12347
  appId
@@ -11625,9 +12387,77 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11625
12387
  workflowName: workflow.name,
11626
12388
  mainJobFunctionName: workflow.mainJob.name,
11627
12389
  ...workflow.retryPolicy && { retryPolicy: toRetryPolicy(workflow.retryPolicy) },
11628
- ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) }
12390
+ ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) },
12391
+ publishExecutionEvents: workflow.publishEvents ?? false
11629
12392
  };
11630
12393
  }
12394
+ const NO_EVENT_SUBSCRIBERS = { workflowNames: /* @__PURE__ */ new Set() };
12395
+ function isSubscribed(subscribers, workflowName) {
12396
+ return subscribers.workflowNames.has(workflowName);
12397
+ }
12398
+ /**
12399
+ * Job names the given workflows' subscriptions enable.
12400
+ *
12401
+ * A job execution trigger names a workflow rather than a job, so the value is
12402
+ * resolved per job name over the union of the workflows that run it: a job two
12403
+ * workflows share stays on while either one is subscribed. Whoever asks whether a
12404
+ * job still publishes has to apply that same union, which is why this is shared
12405
+ * rather than restated — the two answering it differently is what let a shared
12406
+ * job be reported as turning off while a peer subscription kept it on.
12407
+ * @param params - Workflows, the jobs each runs, and which are subscribed
12408
+ * @param params.workflows - Workflows whose job sets are considered
12409
+ * @param params.mainJobDeps - Job names each workflow runs, keyed by its main job
12410
+ * @param params.isSubscribed - Whether a workflow's jobs are subscribed in this run
12411
+ * @returns Job names those subscriptions enable
12412
+ */
12413
+ function subscribedWorkflowJobNames(params) {
12414
+ const { workflows, mainJobDeps, isSubscribed } = params;
12415
+ const jobNames = /* @__PURE__ */ new Set();
12416
+ for (const workflow of workflows) {
12417
+ if (!isSubscribed(workflow.name)) continue;
12418
+ for (const jobName of mainJobDeps[workflow.mainJob.name] ?? []) jobNames.add(jobName);
12419
+ }
12420
+ return jobNames;
12421
+ }
12422
+ /**
12423
+ * Resolve `publishExecutionEvents` for every job function used by a workflow.
12424
+ *
12425
+ * A job execution trigger names a workflow rather than a job, so a subscription
12426
+ * opts in every job that workflow runs.
12427
+ * @param params - Workflows, their job dependencies, subscribers, and explicit job flags
12428
+ * @returns Resolved flags keyed by job function name
12429
+ */
12430
+ function resolveJobPublishEvents(params) {
12431
+ const { workflows, mainJobDeps, subscribers, explicit } = params;
12432
+ const usedJobNames = /* @__PURE__ */ new Set();
12433
+ for (const workflow of Object.values(workflows)) for (const jobName of mainJobDeps[workflow.mainJob.name] ?? []) usedJobNames.add(jobName);
12434
+ const subscribedJobNames = subscribedWorkflowJobNames({
12435
+ workflows: Object.values(workflows),
12436
+ mainJobDeps,
12437
+ isSubscribed: (workflowName) => isSubscribed(subscribers, workflowName)
12438
+ });
12439
+ const resolved = /* @__PURE__ */ new Map();
12440
+ for (const jobName of usedJobNames) resolved.set(jobName, resolvePublishEvents({
12441
+ explicit: explicit.get(jobName),
12442
+ subscribed: subscribedJobNames.has(jobName),
12443
+ conflict: publishEventsConflict.workflowJob(jobName)
12444
+ }));
12445
+ return resolved;
12446
+ }
12447
+ /**
12448
+ * Collect job functions whose remote publishing flag no longer matches the plan.
12449
+ * @param existing - Existing job functions keyed by name
12450
+ * @param resolved - Resolved `publishExecutionEvents` keyed by job function name
12451
+ * @returns Job function names needing re-registration
12452
+ */
12453
+ function collectStaleJobFunctionNames(existing, resolved) {
12454
+ const stale = /* @__PURE__ */ new Set();
12455
+ for (const [jobName, publishEvents] of resolved) {
12456
+ const remote = existing.get(jobName);
12457
+ if (remote && remote.publishExecutionEvents !== publishEvents) stale.add(jobName);
12458
+ }
12459
+ return stale;
12460
+ }
11631
12461
  /**
11632
12462
  * Plan workflow changes and job functions based on current and desired state.
11633
12463
  * @param client - Operator client instance
@@ -11637,15 +12467,31 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11637
12467
  * @param workflows - Parsed workflows
11638
12468
  * @param mainJobDeps - Main job dependencies by workflow
11639
12469
  * @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan
12470
+ * @param eventPublishing - Executor subscriptions and explicit job flags driving execution event publishing
11640
12471
  * @returns Planned workflow changes
11641
12472
  */
11642
- async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
12473
+ async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set(), eventPublishing = {}) {
11643
12474
  const changeSet = createChangeSet("Workflows");
11644
12475
  const conflicts = [];
11645
12476
  const unmanaged = [];
11646
12477
  const resourceOwners = /* @__PURE__ */ new Set();
11647
12478
  const unchangedWorkflowJobNames = /* @__PURE__ */ new Set();
11648
12479
  const retainedWorkflowJobNames = /* @__PURE__ */ new Set();
12480
+ const executionSubscribers = eventPublishing.execution ?? NO_EVENT_SUBSCRIBERS;
12481
+ const { dependentApps, runAppIds } = eventPublishing;
12482
+ const everyJobDeclaresPublishEvents = (workflowName) => {
12483
+ const jobNames = mainJobDeps[workflows[workflowName]?.mainJob.name ?? ""] ?? [];
12484
+ const explicit = eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map();
12485
+ return jobNames.length > 0 && jobNames.every((jobName) => explicit.has(jobName));
12486
+ };
12487
+ const existingJobFunctions = await fetchExistingJobFunctions(client, workspaceId);
12488
+ const jobFunctionPublishEvents = resolveJobPublishEvents({
12489
+ workflows,
12490
+ mainJobDeps,
12491
+ subscribers: eventPublishing.jobExecution ?? NO_EVENT_SUBSCRIBERS,
12492
+ explicit: eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map()
12493
+ });
12494
+ const staleJobFunctionNames = collectStaleJobFunctionNames(existingJobFunctions, jobFunctionPublishEvents);
11649
12495
  const existingWorkflows = await fetchExistingResourcesWithLabels({
11650
12496
  client,
11651
12497
  fetchPage: async (pageToken, pageSize) => {
@@ -11661,14 +12507,34 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11661
12507
  });
11662
12508
  for (const workflow of Object.values(workflows)) {
11663
12509
  const existing = existingWorkflows[workflow.name];
11664
- const metaRequest = await buildMetaRequest({
12510
+ const metaRequest = addDependencyRecords(await buildMetaRequest({
11665
12511
  trn: resourceTrn(workspaceId, "workflow", workflow.name),
11666
12512
  appName,
11667
12513
  appId
12514
+ }), {
12515
+ key: eventSourceKey.workflow(workflow.name),
12516
+ dependentApps,
12517
+ runAppIds,
12518
+ pinned: workflow.publishEvents !== void 0
12519
+ });
12520
+ addDependencyRecords(metaRequest, {
12521
+ key: eventSourceKey.workflowJobs(workflow.name),
12522
+ dependentApps,
12523
+ runAppIds,
12524
+ pinned: everyJobDeclaresPublishEvents(workflow.name),
12525
+ scope: "jobs"
11668
12526
  });
11669
12527
  const usedJobNames = mainJobDeps[workflow.mainJob.name];
11670
12528
  if (!usedJobNames) throw new Error(`Job "${workflow.mainJob.name}" (mainJob of workflow "${workflow.name}") was not found.\n\nPossible causes:\n - The job is not exported as a named export\n - The file containing the job is not included in workflow.files glob pattern\n\nSolution:\n export const ${workflow.mainJob.name} = createWorkflowJob({ name: "${workflow.mainJob.name}", ... })`);
11671
12529
  usedJobNames.forEach((jobName) => retainedWorkflowJobNames.add(jobName));
12530
+ const desiredWorkflow = {
12531
+ ...workflow,
12532
+ publishEvents: resolvePublishEvents({
12533
+ explicit: workflow.publishEvents,
12534
+ subscribed: isSubscribed(executionSubscribers, workflow.name),
12535
+ conflict: publishEventsConflict.workflow(workflow.name)
12536
+ })
12537
+ };
11672
12538
  if (existing) {
11673
12539
  if (trackDesiredResourceOwnership({
11674
12540
  labels: existing.allLabels,
@@ -11679,13 +12545,22 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11679
12545
  resourceName: workflow.name,
11680
12546
  conflicts,
11681
12547
  unmanaged
11682
- }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(existing.resource, workflow, usedJobNames, unchangedJobFunctions)) {
11683
- changeSet.unchanged.push({ name: workflow.name });
12548
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged({
12549
+ existing: existing.resource,
12550
+ workflow: desiredWorkflow,
12551
+ usedJobNames,
12552
+ unchangedJobFunctions,
12553
+ staleJobFunctionNames
12554
+ })) {
12555
+ changeSet.unchanged.push({
12556
+ name: workflow.name,
12557
+ metaRequest
12558
+ });
11684
12559
  for (const jobName of usedJobNames) unchangedWorkflowJobNames.add(jobName);
11685
12560
  } else changeSet.updates.push({
11686
12561
  name: workflow.name,
11687
12562
  workspaceId,
11688
- workflow,
12563
+ workflow: desiredWorkflow,
11689
12564
  usedJobNames,
11690
12565
  metaRequest
11691
12566
  });
@@ -11693,7 +12568,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11693
12568
  } else changeSet.creates.push({
11694
12569
  name: workflow.name,
11695
12570
  workspaceId,
11696
- workflow,
12571
+ workflow: desiredWorkflow,
11697
12572
  usedJobNames,
11698
12573
  metaRequest
11699
12574
  });
@@ -11723,7 +12598,9 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11723
12598
  workspaceId,
11724
12599
  appName,
11725
12600
  appId,
11726
- retainedWorkflowJobNames
12601
+ existingJobFunctionNames: [...existingJobFunctions.keys()],
12602
+ retainedWorkflowJobNames,
12603
+ resourceOwners
11727
12604
  });
11728
12605
  const deletableJobNames = new Set(jobFunctionDeletes.map((del) => del.jobFunctionName));
11729
12606
  for (const del of deleteWorkflows) changeSet.deletes.push({
@@ -11738,23 +12615,33 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11738
12615
  appName,
11739
12616
  appId,
11740
12617
  unchangedWorkflowJobNames,
11741
- jobFunctionDeletes
12618
+ jobFunctionDeletes,
12619
+ jobFunctionPublishEvents
11742
12620
  };
11743
12621
  }
11744
- async function planWorkflowJobFunctionDeletes(params) {
11745
- const { client, workspaceId, appName, appId, retainedWorkflowJobNames } = params;
11746
- const existingJobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
12622
+ async function fetchExistingJobFunctions(client, workspaceId) {
12623
+ const jobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
11747
12624
  const response = await client.listWorkflowJobFunctions({
11748
12625
  workspaceId,
11749
12626
  pageToken,
11750
12627
  pageSize: maxPageSize
11751
12628
  });
11752
- return [response.jobFunctions.map((jobFunction) => jobFunction.name), response.nextPageToken];
12629
+ return [response.jobFunctions, response.nextPageToken];
11753
12630
  });
11754
- const candidates = [...new Set(existingJobFunctions)].filter((jobName) => !retainedWorkflowJobNames.has(jobName));
12631
+ return new Map(jobFunctions.map((jobFunction) => [jobFunction.name, jobFunction]));
12632
+ }
12633
+ async function planWorkflowJobFunctionDeletes(params) {
12634
+ const { client, workspaceId, appName, appId, existingJobFunctionNames, retainedWorkflowJobNames, resourceOwners } = params;
12635
+ const candidates = existingJobFunctionNames.filter((jobName) => !retainedWorkflowJobNames.has(jobName));
11755
12636
  return (await Promise.all(candidates.map(async (jobFunctionName) => {
11756
12637
  const { metadata } = await client.getMetadata({ trn: resourceTrn(workspaceId, "workflow_job_function", jobFunctionName) });
11757
- return isOwnedByApp(metadata?.labels, appName, appId) ? {
12638
+ return trackRemainingResourceOwner({
12639
+ labels: metadata?.labels,
12640
+ ownerLabel: metadata?.labels["sdk-name"],
12641
+ appName,
12642
+ appId,
12643
+ resourceOwners
12644
+ }) ? {
11758
12645
  workspaceId,
11759
12646
  jobFunctionName
11760
12647
  } : void 0;
@@ -11769,12 +12656,14 @@ async function planWorkflowJobFunctionDeletes(params) {
11769
12656
  function formatWorkflowChangeEntries(changeSet, workflowJobFunctionChanges) {
11770
12657
  return formatChangeEntriesWithFunctionRegistry("workflow", changeSet, workflowJobFunctionChanges, (item) => "usedJobNames" in item ? item.usedJobNames.map((jobName) => workflowJobFunctionName(jobName)) : []);
11771
12658
  }
11772
- function canTreatWorkflowAsUnchanged(existing, workflow, usedJobNames, unchangedJobFunctions) {
12659
+ function canTreatWorkflowAsUnchanged(params) {
12660
+ const { existing, workflow, usedJobNames, unchangedJobFunctions, staleJobFunctionNames } = params;
11773
12661
  if (!usedJobNames.every((jobName) => unchangedJobFunctions.has(jobName))) return false;
12662
+ if (usedJobNames.some((jobName) => staleJobFunctionNames.has(jobName))) return false;
11774
12663
  return areWorkflowsEqual(existing, workflow, usedJobNames);
11775
12664
  }
11776
12665
  function areWorkflowsEqual(existing, workflow, usedJobNames) {
11777
- return existing.mainJobFunctionName === workflow.mainJob.name && areNormalizedEqual(normalizeComparableExistingWorkflowRetryPolicy(existing.retryPolicy), normalizeComparableWorkflowRetryPolicy(workflow.retryPolicy)) && areNormalizedEqual(normalizeComparableConcurrencyPolicy(existing.concurrencyPolicy), normalizeComparableConcurrencyPolicy(workflow.concurrencyPolicy)) && areNormalizedEqual(normalizeComparableWorkflowJobNames(existing.jobFunctions), normalizeComparableWorkflowJobNames(usedJobNames));
12666
+ return existing.mainJobFunctionName === workflow.mainJob.name && (existing.publishExecutionEvents ?? false) === (workflow.publishEvents ?? false) && areNormalizedEqual(normalizeComparableExistingWorkflowRetryPolicy(existing.retryPolicy), normalizeComparableWorkflowRetryPolicy(workflow.retryPolicy)) && areNormalizedEqual(normalizeComparableConcurrencyPolicy(existing.concurrencyPolicy), normalizeComparableConcurrencyPolicy(workflow.concurrencyPolicy)) && areNormalizedEqual(normalizeComparableWorkflowJobNames(existing.jobFunctions), normalizeComparableWorkflowJobNames(usedJobNames));
11778
12667
  }
11779
12668
  function normalizeComparableExistingWorkflowRetryPolicy(policy) {
11780
12669
  if (!policy) return;
@@ -11993,7 +12882,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
11993
12882
  executionPolicyKey: toPlatformExecutionPolicyKey(replace.policy),
11994
12883
  concurrencyPolicy: toConcurrencyPolicyInit(replace.policy.concurrencyPolicy)
11995
12884
  });
11996
- await client.setMetadata(replace.metaRequest);
12885
+ await writeMetadataLabels(client, replace.metaRequest);
11997
12886
  }),
11998
12887
  ...changeSet.creates.map(async (create) => {
11999
12888
  await client.createWorkflowJobFunctionExecutionPolicy({
@@ -12002,7 +12891,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
12002
12891
  executionPolicyKey: toPlatformExecutionPolicyKey(create.policy),
12003
12892
  concurrencyPolicy: toConcurrencyPolicyInit(create.policy.concurrencyPolicy)
12004
12893
  });
12005
- await client.setMetadata(create.metaRequest);
12894
+ await writeMetadataLabels(client, create.metaRequest);
12006
12895
  }),
12007
12896
  ...changeSet.updates.map(async (update) => {
12008
12897
  await client.updateWorkflowJobFunctionExecutionPolicy({
@@ -12010,7 +12899,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
12010
12899
  executionPolicyName: update.policy.name,
12011
12900
  concurrencyPolicy: toConcurrencyPolicyInit(update.policy.concurrencyPolicy)
12012
12901
  });
12013
- await client.setMetadata(update.metaRequest);
12902
+ await writeMetadataLabels(client, update.metaRequest);
12014
12903
  })
12015
12904
  ]);
12016
12905
  else await Promise.all(changeSet.deletes.map((del) => client.deleteWorkflowJobFunctionExecutionPolicy({
@@ -12082,6 +12971,21 @@ async function applyDeploymentPlans(client, workspaceId, deployments) {
12082
12971
 
12083
12972
  //#endregion
12084
12973
  //#region src/cli/commands/deploy/config-id-injector.ts
12974
+ /**
12975
+ * Warn when a command that decides resource ownership resolved a config
12976
+ * without an app id.
12977
+ *
12978
+ * Injection only reaches an inline `defineConfig({...})` call, so a config
12979
+ * that re-exports one from another file resolves without an id and nothing
12980
+ * says so. Ownership then falls back to the application name, and resources
12981
+ * tagged with an id from an earlier deploy read as another application's.
12982
+ * @param appId - Application id from the resolved config, when it has one
12983
+ */
12984
+ function warnMissingAppId(appId) {
12985
+ if (appId) return;
12986
+ logger.warn("The config resolved without an 'id'.");
12987
+ logger.log(" Resources tagged with an id from an earlier deploy read as another application's:\n deploy asks before taking them over, and remove leaves them in place. Only resources\n carrying no id are matched by application name.\n Add an 'id' to the object passed to defineConfig() — a config that re-exports it from\n another file cannot have one injected automatically. 'tailor setup' can add it for you.");
12988
+ }
12085
12989
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12086
12990
  function findDefineConfigCalls(node, results) {
12087
12991
  if (!node || typeof node !== "object") return;
@@ -12237,27 +13141,26 @@ function insertIdProperty(source, configObj, id) {
12237
13141
  //#region src/cli/commands/deploy/confirm.ts
12238
13142
  /**
12239
13143
  * Confirm reassignment of resources when owner conflicts are detected.
12240
- * Splits into two scenarios: id regeneration (same sdk-name, different
12241
- * sdk-app-id) and name mismatch (different sdk-name). Each gets its own
12242
- * prompt because the user-facing meaning is different.
13144
+ * Splits into three scenarios, each with its own prompt because the
13145
+ * user-facing meaning is different: the resource carries the same sdk-name and
13146
+ * an sdk-app-id the config does not match, either because the config now holds
13147
+ * a different id (regeneration) or because it holds none at all; or the
13148
+ * resource carries a different sdk-name (name mismatch).
12243
13149
  * @param conflicts - Detected owner conflicts
12244
13150
  * @param appName - Target application name
12245
13151
  * @param yes - Whether to auto-confirm without prompting
13152
+ * @param appId - Target application id, when the config resolves to one
12246
13153
  * @returns Promise that resolves when confirmation completes
12247
13154
  */
12248
- async function confirmOwnerConflict(conflicts, appName, yes) {
13155
+ async function confirmOwnerConflict(conflicts, appName, yes, appId) {
12249
13156
  if (conflicts.length === 0) return;
12250
- const idRegenerated = conflicts.filter((c) => c.currentOwner === appName);
13157
+ const idMismatches = conflicts.filter((c) => c.currentOwner === appName);
12251
13158
  const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
12252
- if (idRegenerated.length > 0) await confirmIdRegeneration(idRegenerated, appName, yes);
13159
+ if (idMismatches.length > 0) await (appId ? confirmIdRegeneration(idMismatches, appName, yes) : confirmMissingConfigId(idMismatches, appName, yes));
12253
13160
  if (nameMismatches.length > 0) await confirmNameMismatch(nameMismatches, appName, yes);
12254
13161
  }
12255
13162
  async function confirmIdRegeneration(conflicts, appName, yes) {
12256
- logger.warn(`Application id was regenerated for "${appName}":`);
12257
- logger.log(" These resources still carry the previous id.");
12258
- logger.newline();
12259
- logger.log(` ${styles.info("Resources")}:`);
12260
- for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
13163
+ logIdMismatch(`Application id was regenerated for "${appName}":`, conflicts);
12261
13164
  if (yes) {
12262
13165
  logger.success("Re-tagging resources with the new id (--yes flag specified)...", { mode: "plain" });
12263
13166
  return;
@@ -12270,6 +13173,27 @@ async function confirmIdRegeneration(conflicts, appName, yes) {
12270
13173
  To override, run again and confirm, or use --yes flag.
12271
13174
  `);
12272
13175
  }
13176
+ function logIdMismatch(heading, conflicts) {
13177
+ logger.warn(heading);
13178
+ logger.log(" These resources are tagged with an id from an earlier deploy.");
13179
+ logger.newline();
13180
+ logger.log(` ${styles.info("Resources")}:`);
13181
+ for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
13182
+ }
13183
+ async function confirmMissingConfigId(conflicts, appName, yes) {
13184
+ logIdMismatch(`No application id resolved for "${appName}":`, conflicts);
13185
+ if (yes) {
13186
+ logger.success("Managing these resources by name (--yes flag specified)...", { mode: "plain" });
13187
+ return;
13188
+ }
13189
+ if (!await prompt.confirm({
13190
+ message: `Drop that id and manage these resources by name for "${appName}"?\n${styles.dim("(the config resolves without an 'id', so ownership falls back to the application name)")}`,
13191
+ default: false
13192
+ })) throw new Error(multiline`
13193
+ Apply cancelled. Resources remain tagged with their current id.
13194
+ Restore the 'id' in your config to keep owning them by id, or run again and confirm to own them by name.
13195
+ `);
13196
+ }
12273
13197
  async function confirmNameMismatch(conflicts, appName, yes) {
12274
13198
  const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
12275
13199
  logger.warn("Application name mismatch detected:");
@@ -12344,6 +13268,125 @@ async function confirmImportantResourceDeletion(resources, yes) {
12344
13268
  To override, run again and confirm, or use --yes flag.
12345
13269
  `);
12346
13270
  }
13271
+ /**
13272
+ * Confirm continuing without an application recorded as a dependency.
13273
+ *
13274
+ * A previous deploy recorded that another config's executors make this config's
13275
+ * resources publish events. Applying this config alone resolves those flags from
13276
+ * a smaller set of executors, which turns publishing off.
13277
+ * @param missing - Recorded dependencies absent from this deploy
13278
+ * @param yes - Whether `--yes` was passed
13279
+ * @returns Promise that resolves when the deploy may continue
13280
+ */
13281
+ async function confirmMissingDependentApps(missing, yes) {
13282
+ if (missing.length === 0) return;
13283
+ logger.warn("Applications recorded as depending on this deploy are missing:");
13284
+ for (const entry of missing) logger.log(` • application id ${styles.info(entry.appId)} depends on ${styles.bold(entry.resource)} (${entry.reason})`);
13285
+ logger.newline();
13286
+ logger.log(" Applying without them turns off event publishing on the resources above:");
13287
+ logger.log(" nothing in this deploy subscribes to them, so the value resolves to false.");
13288
+ logger.log(" To keep it, add their configs to --config, or set publishEvents on");
13289
+ logger.log(" the resources above.");
13290
+ if (yes) {
13291
+ logger.warn("Continuing without them (--yes flag specified); applying turns publishing off.");
13292
+ return;
13293
+ }
13294
+ if (!await prompt.confirm({
13295
+ message: "Continue without them?",
13296
+ default: false
13297
+ })) throw new Error(multiline`
13298
+ Apply cancelled. Add the missing configs to --config, or set publishEvents
13299
+ explicitly on the resources that should keep publishing.
13300
+ `);
13301
+ }
13302
+
13303
+ //#endregion
13304
+ //#region src/cli/commands/deploy/dependency-records.ts
13305
+ /**
13306
+ * List the resources whose `publishEvents` this run recomputes.
13307
+ *
13308
+ * A resource that declares the value keeps it whatever the run covers, so no
13309
+ * absent config can change it and its records cannot matter. Skipping those is
13310
+ * what keeps a declared value from prompting forever.
13311
+ * @param workspaceId - Workspace being deployed to
13312
+ * @param application - Application whose resources are listed
13313
+ * @param jobsByWorkflow - Job names each workflow runs, keyed by its main job
13314
+ * @param subscribedKeys - Resources this run subscribes to, by resource key
13315
+ * @returns One entry per value that leaves `publishEvents` unset
13316
+ */
13317
+ function recomputedResources(workspaceId, application, jobsByWorkflow, subscribedKeys) {
13318
+ const resources = [];
13319
+ for (const service of application.tailorDBServices) for (const [typeName, type] of Object.entries(service.types)) if (type.settings.publishEvents === void 0) resources.push({
13320
+ trn: tailorDBTypeTrn(workspaceId, service.namespace, typeName),
13321
+ key: eventSourceKey.tailorDBType(service.namespace, typeName),
13322
+ scope: "resource",
13323
+ label: eventSourceLabel.tailorDBType(typeName)
13324
+ });
13325
+ for (const service of application.resolverServices) for (const resolver of Object.values(service.resolvers)) if (resolver.publishEvents === void 0) resources.push({
13326
+ trn: resolverTrn(workspaceId, service.namespace, resolver.name),
13327
+ key: eventSourceKey.resolver(service.namespace, resolver.name),
13328
+ scope: "resource",
13329
+ label: eventSourceLabel.resolver(resolver.name)
13330
+ });
13331
+ for (const idp of application.idpServices) if (idp.publishEvents === void 0) resources.push({
13332
+ trn: resourceTrn(workspaceId, "idp", idp.name),
13333
+ key: eventSourceKey.idp(idp.name),
13334
+ scope: "resource",
13335
+ label: eventSourceLabel.idpService(idp.name)
13336
+ });
13337
+ const explicitByJob = new Map((application.workflowService?.jobs ?? []).map((job) => [job.name, job.publishEvents]));
13338
+ const subscribedJobNames = subscribedWorkflowJobNames({
13339
+ workflows: Object.values(application.workflowService?.workflows ?? {}),
13340
+ mainJobDeps: jobsByWorkflow,
13341
+ isSubscribed: (workflowName) => subscribedKeys.has(eventSourceKey.workflowJobs(workflowName))
13342
+ });
13343
+ for (const workflow of Object.values(application.workflowService?.workflows ?? {})) {
13344
+ const trn = resourceTrn(workspaceId, "workflow", workflow.name);
13345
+ if (workflow.publishEvents === void 0) resources.push({
13346
+ trn,
13347
+ key: eventSourceKey.workflow(workflow.name),
13348
+ scope: "resource",
13349
+ label: eventSourceLabel.workflow(workflow.name)
13350
+ });
13351
+ if ((jobsByWorkflow[workflow.mainJob.name] ?? []).some((jobName) => explicitByJob.get(jobName) === void 0 && !subscribedJobNames.has(jobName))) resources.push({
13352
+ trn,
13353
+ key: eventSourceKey.workflowJobs(workflow.name),
13354
+ scope: "jobs",
13355
+ label: eventSourceLabel.workflowJobs(workflow.name)
13356
+ });
13357
+ }
13358
+ return resources;
13359
+ }
13360
+ /**
13361
+ * Read the applications recorded as depending on this config's resources but
13362
+ * absent from the current deploy.
13363
+ *
13364
+ * Records live on the resources rather than on the application, so this survives
13365
+ * the application being renamed and stops reporting a resource that is gone.
13366
+ *
13367
+ * A resource this run still subscribes to is skipped: its value resolves to `true`
13368
+ * from the run's own executors, so the absent config changes nothing about it and
13369
+ * asking would be asking about something that cannot happen. What is left over is
13370
+ * a resource that really does turn off.
13371
+ * @param params - Client, workspace, application, and the run's inputs
13372
+ * @param params.client - Operator client instance
13373
+ * @param params.workspaceId - Workspace being deployed to
13374
+ * @param params.application - Application being planned
13375
+ * @param params.runAppIds - Stable ids of every application in the run
13376
+ * @param params.subscribedKeys - Resources this run subscribes to, by resource key
13377
+ * @param params.jobsByWorkflow - Job names each workflow runs, keyed by its main job
13378
+ * @returns Recorded dependencies missing from the run
13379
+ */
13380
+ async function fetchMissingDependentApps(params) {
13381
+ const { client, workspaceId, application, runAppIds, subscribedKeys, jobsByWorkflow } = params;
13382
+ return (await Promise.all(recomputedResources(workspaceId, application, jobsByWorkflow, subscribedKeys).filter(({ key }) => !subscribedKeys.has(key)).map(async ({ trn, scope, label }) => {
13383
+ return recordedDependencies((await getOrNull(() => client.getMetadata({ trn })))?.metadata?.labels, scope).filter((dependency) => !runAppIds.has(dependency.appId)).map((dependency) => ({
13384
+ resource: label,
13385
+ appId: dependency.appId,
13386
+ reason: dependency.reason
13387
+ }));
13388
+ }))).flat();
13389
+ }
12347
13390
 
12348
13391
  //#endregion
12349
13392
  //#region src/cli/commands/deploy/validate-plan.ts
@@ -13421,57 +14464,187 @@ async function resolveDeployWorkspace(options = {}) {
13421
14464
  //#endregion
13422
14465
  //#region src/cli/commands/deploy/deploy.ts
13423
14466
  /**
13424
- * Resolve the set of IdP names that have at least one executor subscribed to
13425
- * their user events. When an executor's idpUser trigger omits the `idp` option
13426
- * and exactly one IdP is configured, that IdP is implicitly the target.
13427
- * Executors that omit `idp` while multiple IdPs exist are skipped here; the
13428
- * apply pipeline throws a clearer error for them later.
13429
- * @param application - Loaded application
13430
- * @returns Set of IdP names targeted by idpUser triggers
14467
+ * Resolve which IdP an `idpUser` trigger subscribes to. A trigger that omits the
14468
+ * `idp` option means the only configured IdP; when several exist the target is
14469
+ * ambiguous and the apply pipeline throws a clearer error for it later.
14470
+ * @param application - Application declaring the executor
14471
+ * @param trigger - The executor's `idpUser` trigger
14472
+ * @returns The subscribed IdP name, or undefined when ambiguous
13431
14473
  */
13432
- function collectIdpUserTriggerTargets(application) {
13433
- const targets = /* @__PURE__ */ new Set();
14474
+ function subscribedIdpName(application, trigger) {
14475
+ if (trigger.idp != null) return trigger.idp;
13434
14476
  const idps = collectApplicationIdpNames(application);
13435
- for (const executor of Object.values(application.executorService?.executors ?? {})) {
13436
- if (executor.trigger.kind !== "idpUser") continue;
13437
- if (executor.trigger.idp != null) targets.add(executor.trigger.idp);
13438
- else if (idps.size === 1) {
13439
- const [idp] = idps;
13440
- if (idp) targets.add(idp);
13441
- }
13442
- }
13443
- return targets;
13444
- }
13445
- function collectDeployIdpUserTriggerTargets(targets) {
13446
- const triggerTargets = /* @__PURE__ */ new Set();
13447
- for (const target of targets) for (const idpName of collectIdpUserTriggerTargets(target.application)) triggerTargets.add(idpName);
13448
- return triggerTargets;
14477
+ if (idps.size !== 1) return;
14478
+ const [only] = idps;
14479
+ return only;
13449
14480
  }
13450
- function collectExecutorUsedTailorDBTypes(owner, targets) {
13451
- const usedTypes = /* @__PURE__ */ new Set();
13452
- const ownerNamespaces = new Set(owner.application.tailorDBServices.map((service) => service.namespace));
14481
+ /**
14482
+ * Build the error for a subscription whose resource no config in the run declares.
14483
+ *
14484
+ * The advice differs by cause: a config that declares the resource as owned
14485
+ * elsewhere is missing its peer from `--config`, while one that declares nothing
14486
+ * external is naming a resource that does not exist.
14487
+ * @param executorName - Name of the subscribing executor
14488
+ * @param lookup - How the subscribed resource is found
14489
+ * @param subscriber - Deployment target declaring the executor
14490
+ * @returns Error message
14491
+ */
14492
+ function missingOwnerMessage(executorName, lookup, subscriber) {
14493
+ const subject = `Executor "${executorName}" subscribes to ${lookup.resource}, which no config in this deploy declares.`;
14494
+ const external = lookup.externalHint(subscriber);
14495
+ if (external) return `${subject} This config declares ${external}, so add the config that owns it to --config.`;
14496
+ if (lookup.trigger.kind === "workflowExecution" || lookup.trigger.kind === "workflowJobExecution") return `${subject} A workflow has no "external" declaration, unlike a TailorDB namespace or resolver, so nothing in this config points at the one that owns it. Check the name, or add the config that declares the workflow to --config.`;
14497
+ return `${subject} This config declares nothing external that could hold it, so check the name.`;
14498
+ }
14499
+ /**
14500
+ * Resolve every event subscription in the run to the config that declares the
14501
+ * subscribed resource.
14502
+ *
14503
+ * Doing this once keeps the flags the planners resolve and the cross-config
14504
+ * dependencies recorded on the application in agreement — both are read off the
14505
+ * same list.
14506
+ * @param targets - Built deployment targets in the run
14507
+ * @returns One entry per executor whose trigger names a publishing resource
14508
+ */
14509
+ function collectEventSubscriptions(targets) {
14510
+ const subscriptions = [];
13453
14511
  const applications = targets.map((target) => target.application);
13454
- for (const target of targets) {
13455
- const visibleNamespaces = collectVisibleTailorDBTypeNamespaces(target.application, applications);
13456
- for (const executor of Object.values(target.application.executorService?.executors ?? {})) if (executor.trigger.kind === "tailordb") {
13457
- const namespace = findTailorDBNamespace(target.application, executor.trigger.typeName) ?? resolveSameRunNamespace(visibleNamespaces, executor.trigger.typeName, "TailorDB type");
13458
- if (namespace && ownerNamespaces.has(namespace)) usedTypes.add(executor.trigger.typeName);
14512
+ for (const subscriber of targets) {
14513
+ const executors = subscriber.application.executorService?.executors ?? {};
14514
+ const visibility = collectSubscriberVisibility(subscriber, applications);
14515
+ for (const executor of Object.values(executors)) {
14516
+ const lookup = eventSourceLookup(executor, subscriber, visibility);
14517
+ if (!lookup) continue;
14518
+ const entry = {
14519
+ subscriber,
14520
+ executorName: executor.name,
14521
+ resource: lookup.resource,
14522
+ trigger: lookup.trigger
14523
+ };
14524
+ if (lookup.declaredBy(subscriber)) {
14525
+ subscriptions.push({
14526
+ ...entry,
14527
+ owner: subscriber,
14528
+ pinned: lookup.pinned(subscriber),
14529
+ key: lookup.keyIn(subscriber)
14530
+ });
14531
+ continue;
14532
+ }
14533
+ const peers = targets.filter((target) => target.config.path !== subscriber.config.path && lookup.declaredBy(target));
14534
+ const owners = lookup.narrowOwners(peers);
14535
+ if (owners === "ambiguous") continue;
14536
+ const [owner] = owners;
14537
+ if (!owner) throw new Error(missingOwnerMessage(executor.name, lookup, subscriber));
14538
+ subscriptions.push({
14539
+ ...entry,
14540
+ owner,
14541
+ pinned: lookup.pinned(owner),
14542
+ key: lookup.keyIn(owner)
14543
+ });
13459
14544
  }
13460
14545
  }
13461
- return usedTypes;
14546
+ return subscriptions;
13462
14547
  }
13463
- function collectExecutorUsedResolvers(owner, targets) {
13464
- const usedResolvers = /* @__PURE__ */ new Set();
13465
- const ownerNamespaces = new Set(owner.application.resolverServices.map((service) => service.namespace));
13466
- const applications = targets.map((target) => target.application);
13467
- for (const target of targets) {
13468
- const visibleNamespaces = collectVisibleResolverNamespaces(target.application, applications);
13469
- for (const executor of Object.values(target.application.executorService?.executors ?? {})) if (executor.trigger.kind === "resolverExecuted") {
13470
- const namespace = findResolverNamespace(target.application, executor.trigger.resolverName) ?? resolveSameRunNamespace(visibleNamespaces, executor.trigger.resolverName, "Resolver");
13471
- if (namespace && ownerNamespaces.has(namespace)) usedResolvers.add(executor.trigger.resolverName);
13472
- }
13473
- }
13474
- return usedResolvers;
14548
+ /**
14549
+ * Key the resources this run subscribes to among the target's own.
14550
+ *
14551
+ * A resource with a subscriber in the run resolves to `true` from that subscriber
14552
+ * alone, so no absent config can change it.
14553
+ * @param subscriptions - Subscriptions resolved across the run
14554
+ * @param owner - Deployment target being planned
14555
+ * @returns Resource keys the run subscribes to
14556
+ */
14557
+ function subscribedResourceKeys(subscriptions, owner) {
14558
+ return new Set(ownedSubscriptions(subscriptions, owner).flatMap((subscription) => subscription.key ?? []));
14559
+ }
14560
+ /**
14561
+ * Select the subscriptions pointing at resources the given target declares.
14562
+ * @param subscriptions - Subscriptions resolved across the run
14563
+ * @param owner - Deployment target being planned
14564
+ * @returns Subscriptions whose subscribed resource belongs to `owner`
14565
+ */
14566
+ function ownedSubscriptions(subscriptions, owner) {
14567
+ return subscriptions.filter((subscription) => subscription.owner.config.path === owner.config.path);
14568
+ }
14569
+ /**
14570
+ * Collect the TailorDB types subscribed to among the given subscriptions.
14571
+ * @param subscriptions - Subscriptions owned by the target being planned
14572
+ * @returns Subscribed type names
14573
+ */
14574
+ function subscribedTailorDBTypes(subscriptions) {
14575
+ return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "tailordb" ? [subscription.trigger.typeName] : []));
14576
+ }
14577
+ /**
14578
+ * Collect the resolvers subscribed to among the given subscriptions.
14579
+ * @param subscriptions - Subscriptions owned by the target being planned
14580
+ * @returns Subscribed resolver names
14581
+ */
14582
+ function subscribedResolvers(subscriptions) {
14583
+ return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "resolverExecuted" ? [subscription.trigger.resolverName] : []));
14584
+ }
14585
+ /**
14586
+ * Collect the IdPs subscribed to among the given subscriptions.
14587
+ * @param subscriptions - Subscriptions owned by the target being planned
14588
+ * @returns Subscribed IdP names
14589
+ */
14590
+ function subscribedIdps(subscriptions) {
14591
+ return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "idpUser" ? [subscribedIdpName(subscription.subscriber.application, subscription.trigger) ?? []].flat() : []));
14592
+ }
14593
+ /**
14594
+ * Collect the workflows subscribed to, per event granularity level.
14595
+ * @param subscriptions - Subscriptions owned by the target being planned
14596
+ * @returns Subscribers keyed by event granularity level
14597
+ */
14598
+ function subscribedWorkflows(subscriptions) {
14599
+ const execution = { workflowNames: /* @__PURE__ */ new Set() };
14600
+ const jobExecution = { workflowNames: /* @__PURE__ */ new Set() };
14601
+ for (const { trigger } of subscriptions) if (trigger.kind === "workflowExecution") execution.workflowNames.add(trigger.workflowName);
14602
+ else if (trigger.kind === "workflowJobExecution") jobExecution.workflowNames.add(trigger.workflowName);
14603
+ return {
14604
+ execution,
14605
+ jobExecution
14606
+ };
14607
+ }
14608
+ /**
14609
+ * Collect the applications that have to take part in the same deploy for this
14610
+ * target's resources to be applied the same way.
14611
+ *
14612
+ * An executor in another config makes the resource it subscribes to publish
14613
+ * events, so deploying this config without that one would resolve the flag from
14614
+ * a smaller set of executors and turn publishing off.
14615
+ *
14616
+ * A resource that declares `publishEvents` keeps its value either way, so
14617
+ * recording a dependency for it would ask about a partial deploy that changes
14618
+ * nothing — and `prompt.confirm` rejects outright where it cannot ask, failing a
14619
+ * deploy that was never at risk.
14620
+ *
14621
+ * Records are keyed by resource rather than by application: the resource is what
14622
+ * carries them, so a record survives the owner being renamed and disappears with
14623
+ * the resource itself.
14624
+ * @param subscriptions - Subscriptions owned by the target being planned
14625
+ * @returns Dependent application ids and reasons, keyed by resource
14626
+ */
14627
+ function collectDependentApps(subscriptions) {
14628
+ const byResource = /* @__PURE__ */ new Map();
14629
+ for (const { subscriber, owner, pinned, key } of subscriptions) {
14630
+ if (subscriber.config.path === owner.config.path || pinned || key === void 0) continue;
14631
+ const appId = subscriber.application.id;
14632
+ if (appId === void 0) continue;
14633
+ const dependents = byResource.get(key) ?? /* @__PURE__ */ new Map();
14634
+ dependents.set(appId, "publish-events");
14635
+ byResource.set(key, dependents);
14636
+ }
14637
+ return byResource;
14638
+ }
14639
+ /**
14640
+ * Collect explicit `publishEvents` values declared on this target's workflow jobs.
14641
+ * @param target - Deployment target being planned
14642
+ * @returns Explicit flags keyed by job name
14643
+ */
14644
+ function collectWorkflowJobPublishEvents(target) {
14645
+ const jobPublishEvents = /* @__PURE__ */ new Map();
14646
+ for (const job of target.application.workflowService?.jobs ?? []) if (job.publishEvents !== void 0) jobPublishEvents.set(job.name, job.publishEvents);
14647
+ return jobPublishEvents;
13475
14648
  }
13476
14649
  function collectExpectedLocalStaticWebsiteNames(targets) {
13477
14650
  const websiteNames = /* @__PURE__ */ new Set();
@@ -13929,7 +15102,9 @@ async function loadDeployConfig(params) {
13929
15102
  buildOnly
13930
15103
  });
13931
15104
  }
13932
- return loadConfig(configPath);
15105
+ const loaded = await loadConfig(configPath);
15106
+ if (!buildOnly) warnMissingAppId(loaded.config.id);
15107
+ return loaded;
13933
15108
  });
13934
15109
  }
13935
15110
  async function buildDeploymentTargets(params) {
@@ -14005,6 +15180,161 @@ function assertUniqueGlobalResourceNames(targets) {
14005
15180
  }
14006
15181
  }
14007
15182
  }
15183
+ function collectSubscriberVisibility(subscriber, applications) {
15184
+ return {
15185
+ tailorDBTypes: collectVisibleTailorDBTypeNamespaces(subscriber.application, applications),
15186
+ resolvers: collectVisibleResolverNamespaces(subscriber.application, applications),
15187
+ idps: collectVisibleIdpNames(subscriber.application, applications)
15188
+ };
15189
+ }
15190
+ /**
15191
+ * TailorDB namespaces the config declares as owned elsewhere.
15192
+ * @param target - Deployment target declaring the executor
15193
+ * @returns Namespace names declared with `external: true`
15194
+ */
15195
+ function externalTailorDBNamespaces(target) {
15196
+ return target.application.externalTailorDBNamespaces;
15197
+ }
15198
+ function externalSubgraphNames(target, subgraphType, localNames) {
15199
+ return target.application.subgraphs.filter((subgraph) => subgraph.Type === subgraphType && !localNames.has(subgraph.Name)).map((subgraph) => subgraph.Name);
15200
+ }
15201
+ function externalResolverNamespaces(target) {
15202
+ return externalSubgraphNames(target, "pipeline", new Set(target.application.resolverServices.map((service) => service.namespace)));
15203
+ }
15204
+ function externalIdpNames(target) {
15205
+ return externalSubgraphNames(target, "idp", new Set(target.application.idpServices.map((idp) => idp.name)));
15206
+ }
15207
+ /**
15208
+ * Describe the external declarations that could hold a missing resource.
15209
+ * @param kind - Resource kind named in the message, e.g. `TailorDB namespace`
15210
+ * @param names - External names the subscribing config declares
15211
+ * @returns A phrase for the error message, or undefined when it declares none
15212
+ */
15213
+ function describeExternal(kind, names) {
15214
+ if (names.length === 0) return;
15215
+ return `external ${names.length === 1 ? kind : `${kind}s`} ${names.map((name) => `"${name}"`).join(", ")}`;
15216
+ }
15217
+ function declaresTailorDBType(target, typeName) {
15218
+ return target.application.tailorDBServices.some((service) => service.types[typeName]) || void 0;
15219
+ }
15220
+ function declaresResolver(target, resolverName) {
15221
+ return target.application.resolverServices.some((service) => Object.values(service.resolvers).some((resolver) => resolver.name === resolverName)) || void 0;
15222
+ }
15223
+ function declaresIdp(target, idpName) {
15224
+ return target.application.idpServices.some((entry) => entry.name === idpName) || void 0;
15225
+ }
15226
+ function declaresWorkflow(target, workflowName) {
15227
+ return Object.values(target.application.workflowService?.workflows ?? {}).some((entry) => entry.name === workflowName) || void 0;
15228
+ }
15229
+ function pinsTailorDBType(target, typeName) {
15230
+ for (const service of target.application.tailorDBServices) {
15231
+ const type = service.types[typeName];
15232
+ if (type) return type.settings.publishEvents !== void 0;
15233
+ }
15234
+ return false;
15235
+ }
15236
+ function pinsResolverIn(target, namespace, resolverName) {
15237
+ return target.application.resolverServices.some((service) => service.namespace === namespace && Object.values(service.resolvers).some((resolver) => resolver.name === resolverName && resolver.publishEvents !== void 0));
15238
+ }
15239
+ function pinsIdp(target, idpName) {
15240
+ return target.application.idpServices.some((entry) => entry.name === idpName && entry.publishEvents !== void 0);
15241
+ }
15242
+ function pinsWorkflow(target, workflowName) {
15243
+ return Object.values(target.application.workflowService?.workflows ?? {}).some((entry) => entry.name === workflowName && entry.publishEvents !== void 0);
15244
+ }
15245
+ function pinsEveryJobOfWorkflow(target, workflowName) {
15246
+ const workflow = Object.values(target.application.workflowService?.workflows ?? {}).find((entry) => entry.name === workflowName);
15247
+ if (workflow === void 0) return false;
15248
+ const jobNames = target.workflowBuildResult?.mainJobDeps[workflow.mainJob.name] ?? [];
15249
+ const declared = collectWorkflowJobPublishEvents(target);
15250
+ return jobNames.length > 0 && jobNames.every((jobName) => declared.has(jobName));
15251
+ }
15252
+ function tailorDBTypeNamespaceIn(target, typeName) {
15253
+ return target.application.tailorDBServices.find((service) => service.types[typeName])?.namespace;
15254
+ }
15255
+ function narrowByVisibleNamespace(params) {
15256
+ const { candidates, visible, resourceKey, declaresIn } = params;
15257
+ if (!visible.has(resourceKey)) return [];
15258
+ const namespace = visible.get(resourceKey);
15259
+ if (namespace === void 0) return "ambiguous";
15260
+ return candidates.filter((target) => declaresIn(target, namespace));
15261
+ }
15262
+ function declaresTailorDBTypeIn(target, namespace, typeName) {
15263
+ return target.application.tailorDBServices.some((service) => service.namespace === namespace && Boolean(service.types[typeName]));
15264
+ }
15265
+ function declaresResolverIn(target, namespace, resolverName) {
15266
+ return target.application.resolverServices.some((service) => service.namespace === namespace && Object.values(service.resolvers).some((resolver) => resolver.name === resolverName));
15267
+ }
15268
+ /**
15269
+ * Resolve how to find the resource an executor's event trigger subscribes to.
15270
+ * @param executor - Executor declared by the subscribing config
15271
+ * @param subscriber - Deployment target declaring the executor
15272
+ * @param visibility - What the subscribing config can see, by resource kind
15273
+ * @returns The lookup, or undefined for triggers with no publishing resource
15274
+ */
15275
+ function eventSourceLookup(executor, subscriber, visibility) {
15276
+ const { trigger } = executor;
15277
+ switch (trigger.kind) {
15278
+ case "tailordb": return {
15279
+ resource: publishEventsConflict.tailorDBType(trigger.typeName).resource,
15280
+ trigger,
15281
+ declaredBy: (target) => declaresTailorDBType(target, trigger.typeName),
15282
+ pinned: (target) => pinsTailorDBType(target, trigger.typeName),
15283
+ keyIn: (owner) => {
15284
+ const namespace = tailorDBTypeNamespaceIn(owner, trigger.typeName);
15285
+ return namespace && eventSourceKey.tailorDBType(namespace, trigger.typeName);
15286
+ },
15287
+ narrowOwners: (candidates) => narrowByVisibleNamespace({
15288
+ candidates,
15289
+ visible: visibility.tailorDBTypes,
15290
+ resourceKey: trigger.typeName,
15291
+ declaresIn: (target, namespace) => declaresTailorDBTypeIn(target, namespace, trigger.typeName)
15292
+ }),
15293
+ externalHint: (target) => describeExternal("TailorDB namespace", externalTailorDBNamespaces(target))
15294
+ };
15295
+ case "resolverExecuted": {
15296
+ const namespace = findResolverNamespace(subscriber.application, trigger.resolverName) ?? visibility.resolvers.get(trigger.resolverName);
15297
+ return {
15298
+ resource: publishEventsConflict.resolver(trigger.resolverName).resource,
15299
+ trigger,
15300
+ declaredBy: (target) => declaresResolver(target, trigger.resolverName),
15301
+ pinned: (target) => namespace !== void 0 && pinsResolverIn(target, namespace, trigger.resolverName),
15302
+ keyIn: () => namespace && eventSourceKey.resolver(namespace, trigger.resolverName),
15303
+ narrowOwners: (candidates) => narrowByVisibleNamespace({
15304
+ candidates,
15305
+ visible: visibility.resolvers,
15306
+ resourceKey: trigger.resolverName,
15307
+ declaresIn: (target, namespace) => declaresResolverIn(target, namespace, trigger.resolverName)
15308
+ }),
15309
+ externalHint: (target) => describeExternal("resolver namespace", externalResolverNamespaces(target))
15310
+ };
15311
+ }
15312
+ case "idpUser": {
15313
+ const idpName = subscribedIdpName(subscriber.application, trigger);
15314
+ if (idpName === void 0) return;
15315
+ return {
15316
+ resource: publishEventsConflict.idpService(idpName).resource,
15317
+ trigger,
15318
+ declaredBy: (target) => declaresIdp(target, idpName),
15319
+ pinned: (target) => pinsIdp(target, idpName),
15320
+ keyIn: () => eventSourceKey.idp(idpName),
15321
+ narrowOwners: (candidates) => visibility.idps.has(idpName) ? candidates.filter((target) => declaresIdp(target, idpName)) : [],
15322
+ externalHint: (target) => describeExternal("IdP", externalIdpNames(target).filter((name) => name === idpName))
15323
+ };
15324
+ }
15325
+ case "workflowExecution":
15326
+ case "workflowJobExecution": return {
15327
+ resource: trigger.kind === "workflowExecution" ? eventSourceLabel.workflow(trigger.workflowName) : eventSourceLabel.workflowJobs(trigger.workflowName),
15328
+ trigger,
15329
+ declaredBy: (target) => declaresWorkflow(target, trigger.workflowName),
15330
+ pinned: (target) => trigger.kind === "workflowExecution" ? pinsWorkflow(target, trigger.workflowName) : pinsEveryJobOfWorkflow(target, trigger.workflowName),
15331
+ keyIn: () => trigger.kind === "workflowExecution" ? eventSourceKey.workflow(trigger.workflowName) : eventSourceKey.workflowJobs(trigger.workflowName),
15332
+ narrowOwners: (candidates) => candidates.filter((target) => declaresWorkflow(target, trigger.workflowName)),
15333
+ externalHint: () => void 0
15334
+ };
15335
+ default: return;
15336
+ }
15337
+ }
14008
15338
  function collectPlannedExternalTailorDBServices(target, targets) {
14009
15339
  const externalNamespaces = new Set(target.application.externalTailorDBNamespaces);
14010
15340
  if (externalNamespaces.size === 0) return [];
@@ -14022,9 +15352,37 @@ function collectExternalAuthIdpConfigNames(targets) {
14022
15352
  }
14023
15353
  return idpConfigNames;
14024
15354
  }
14025
- function collectDeployRunPlanInputs(targets) {
15355
+ /**
15356
+ * Reject a cross-config subscription whose dependency cannot be recorded.
15357
+ *
15358
+ * Records live on the subscribed resource, so there is always somewhere to put
15359
+ * one. What can be missing is the dependent's name: the record identifies it by
15360
+ * application id, and a subscriber that resolves without one cannot be recorded,
15361
+ * leaving the next deploy of the owner alone to turn publishing off unannounced.
15362
+ *
15363
+ * The check applies only to a run that writes: `--dry-run` leaves every id
15364
+ * uninjected, so demanding one there would reject configs a real deploy gives one.
15365
+ * @param subscriptions - Every event subscription resolved for the run
15366
+ * @param writes - Whether this run applies changes rather than only reporting them
15367
+ */
15368
+ function assertRecordableDependencies(subscriptions, writes) {
15369
+ for (const { subscriber, owner, executorName, resource, pinned } of subscriptions) {
15370
+ if (subscriber.config.path === owner.config.path) continue;
15371
+ if (pinned) continue;
15372
+ if (!writes) continue;
15373
+ const appId = subscriber.application.id;
15374
+ if (appId !== void 0 && dependedByAppLabelKey(appId) !== void 0) continue;
15375
+ const cause = appId === void 0 ? `${subscriber.config.path} resolves without an "id" — a config that re-exports defineConfig() from another file never gets one` : `${subscriber.config.path} resolves to the id "${appId}", which is not the lowercase UUID deploy writes`;
15376
+ const fix = appId === void 0 ? `Call defineConfig() inline in ${subscriber.config.path} so deploy can manage its "id".` : `Restore the generated value in ${subscriber.config.path}'s "id".`;
15377
+ throw new Error(`Executor "${executorName}" in ${subscriber.config.path} subscribes to ${resource} in ${owner.config.path}, which would enable event publishing on it for this deploy only. ${cause} — so deploy cannot record which config the dependency belongs to, and deploying ${owner.config.path} alone later would turn publishing back off without asking.\n\n${fix}`);
15378
+ }
15379
+ }
15380
+ function collectDeployRunPlanInputs(targets, writes) {
15381
+ const eventSubscriptions = collectEventSubscriptions(targets);
15382
+ assertRecordableDependencies(eventSubscriptions, writes);
14026
15383
  return {
14027
- idpUserTriggerTargets: collectDeployIdpUserTriggerTargets(targets),
15384
+ eventSubscriptions,
15385
+ runAppIds: new Set(targets.map((target) => target.application.id).filter((id) => id !== void 0)),
14028
15386
  expectedLocalStaticWebsiteNames: collectExpectedLocalStaticWebsiteNames(targets),
14029
15387
  externalAuthIdpConfigNames: collectExternalAuthIdpConfigNames(targets)
14030
15388
  };
@@ -14032,6 +15390,7 @@ function collectDeployRunPlanInputs(targets) {
14032
15390
  async function planDeploymentTarget(params) {
14033
15391
  const { target, targets, runInputs, client, workspaceId, noSchemaCheck } = params;
14034
15392
  const { config, application, workflowBuildResult, httpAdapterBuildResult, bundledScripts } = target;
15393
+ const owned = ownedSubscriptions(runInputs.eventSubscriptions, target);
14035
15394
  await withSpan("plan.validateTailorDBTypeNames", () => assertUniqueTailorDBTypeNamesWithExternal({
14036
15395
  client,
14037
15396
  workspaceId,
@@ -14056,8 +15415,10 @@ async function planDeploymentTarget(params) {
14056
15415
  noSchemaCheck,
14057
15416
  forceApplyAll,
14058
15417
  ...runInputs,
14059
- executorUsedTailorDBTypes: collectExecutorUsedTailorDBTypes(target, targets),
14060
- executorUsedResolvers: collectExecutorUsedResolvers(target, targets),
15418
+ idpUserTriggerTargets: subscribedIdps(owned),
15419
+ executorUsedTailorDBTypes: subscribedTailorDBTypes(owned),
15420
+ executorUsedResolvers: subscribedResolvers(owned),
15421
+ dependentApps: collectDependentApps(owned),
14061
15422
  tailorDBTypeNamespaces,
14062
15423
  resolverNamespaces,
14063
15424
  idpNames
@@ -14073,7 +15434,12 @@ async function planDeploymentTarget(params) {
14073
15434
  withSpan("plan.pipeline", () => planPipeline(ctx)),
14074
15435
  withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
14075
15436
  withSpan("plan.executor", () => planExecutor(ctx)),
14076
- withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
15437
+ withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs, {
15438
+ ...subscribedWorkflows(owned),
15439
+ jobPublishEvents: collectWorkflowJobPublishEvents(target),
15440
+ dependentApps: ctx.dependentApps,
15441
+ runAppIds: ctx.runAppIds
15442
+ })),
14077
15443
  withSpan("plan.workflowExecutionPolicy", () => planWorkflowJobFunctionExecutionPolicy(client, workspaceId, application.name, application.id, config.workflow?.executionPolicies ?? {})),
14078
15444
  withSpan("plan.secretManager", () => planSecretManager(ctx))
14079
15445
  ]);
@@ -14107,15 +15473,14 @@ function deploymentPlanResults(deployment) {
14107
15473
  return results;
14108
15474
  }
14109
15475
  /**
14110
- * Every `PlanResults` entry except `app` carries ownership-tracking fields;
14111
- * deriving the list from `results` itself (instead of naming each key) keeps
14112
- * these collectors in sync with `PlannedDeployment` as resource types are added.
15476
+ * Every `PlanResults` entry carries ownership-tracking fields; deriving the
15477
+ * list from `results` itself (instead of naming each key) keeps these
15478
+ * collectors in sync with `PlannedDeployment` as resource types are added.
14113
15479
  * @param results - Plan results for a single deployment
14114
- * @returns The ownership-tracking plan entries, `app` excluded
15480
+ * @returns The ownership-tracking plan entries
14115
15481
  */
14116
15482
  function ownershipTrackedPlans(results) {
14117
- const { app: _app, ...rest } = results;
14118
- return Object.values(rest);
15483
+ return Object.values(results);
14119
15484
  }
14120
15485
  function collectOwnerConflicts(results) {
14121
15486
  return ownershipTrackedPlans(results).flatMap((plan) => plan.conflicts);
@@ -14454,7 +15819,8 @@ function collectDeploymentResourceOwners(deployments) {
14454
15819
  return owners;
14455
15820
  }
14456
15821
  async function confirmDeploymentPlans(params) {
14457
- const { deployments, yes } = params;
15822
+ const { deployments, yes, missingDependentApps = [] } = params;
15823
+ await confirmMissingDependentApps(missingDependentApps, yes);
14458
15824
  const targetAppNames = new Set(deployments.map((deployment) => deployment.application.name));
14459
15825
  const resourceOwners = collectDeploymentResourceOwners(deployments);
14460
15826
  const scheduledRenamedAppDeletes = /* @__PURE__ */ new Set();
@@ -14462,7 +15828,7 @@ async function confirmDeploymentPlans(params) {
14462
15828
  for (const deployment of deployments) {
14463
15829
  const results = deploymentPlanResults(deployment);
14464
15830
  const conflicts = collectOwnerConflicts(results);
14465
- await confirmOwnerConflict(conflicts, deployment.application.name, yes);
15831
+ await confirmOwnerConflict(conflicts, deployment.application.name, yes, deployment.application.id);
14466
15832
  await confirmUnmanagedResources(collectUnmanagedResources(results), deployment.application.name, yes);
14467
15833
  importantDeletions.push(...collectImportantResourceDeletions(results));
14468
15834
  const emptyApps = computeRenamedAppDeletions({
@@ -14577,19 +15943,29 @@ async function deployInternal(options, cliContext) {
14577
15943
  const { client, workspaceId } = workspace;
14578
15944
  rootSpan.setAttribute("app.name", targets.map((target) => target.application.name).join(","));
14579
15945
  rootSpan.setAttribute("workspace.id", workspaceId);
15946
+ const runInputs = collectDeployRunPlanInputs(targets, !options?.dryRun);
14580
15947
  const deployments = await planDeploymentTargets({
14581
15948
  targets,
14582
- runInputs: collectDeployRunPlanInputs(targets),
15949
+ runInputs,
14583
15950
  client,
14584
15951
  workspaceId,
14585
15952
  noSchemaCheck: options?.noSchemaCheck
14586
15953
  });
14587
15954
  const yes = options?.yes ?? false;
14588
15955
  dropCrossDeploymentManagedDeletes(deployments);
15956
+ const missingDependentApps = (await Promise.all(targets.map((target) => fetchMissingDependentApps({
15957
+ client,
15958
+ workspaceId,
15959
+ application: target.application,
15960
+ runAppIds: runInputs.runAppIds ?? /* @__PURE__ */ new Set(),
15961
+ subscribedKeys: subscribedResourceKeys(runInputs.eventSubscriptions, target),
15962
+ jobsByWorkflow: target.workflowBuildResult?.mainJobDeps ?? {}
15963
+ })))).flat();
14589
15964
  await withSpan("confirm", async () => {
14590
15965
  await confirmDeploymentPlans({
14591
15966
  deployments,
14592
- yes
15967
+ yes,
15968
+ missingDependentApps
14593
15969
  });
14594
15970
  });
14595
15971
  const planSummary = printDeploymentPlans(deployments, { dryRun: options?.dryRun });
@@ -14888,6 +16264,12 @@ function formatEventTriggerConfig(config) {
14888
16264
  eventType: config.eventType,
14889
16265
  condition: config.condition?.expr || ""
14890
16266
  };
16267
+ if (typedConfig.case === "workflow") return {
16268
+ kind: typedConfig.case,
16269
+ eventTypes: typedConfig.value.eventTypes,
16270
+ condition: typedConfig.value.condition?.expr || "",
16271
+ ...typedConfig.value.workflowName && { workflowName: typedConfig.value.workflowName }
16272
+ };
14891
16273
  const base = {
14892
16274
  kind: typedConfig.case,
14893
16275
  eventTypes: typedConfig.value.eventTypes,
@@ -14979,8 +16361,13 @@ async function resolveExecutor(client, workspaceId, name) {
14979
16361
  if (!executor) throw new Error(`Executor '${name}' not found.`);
14980
16362
  return executor;
14981
16363
  }
16364
+ /**
16365
+ * Get an executor by name and return CLI-friendly info.
16366
+ * @param options - Executor lookup options
16367
+ * @returns Executor information
16368
+ */
14982
16369
  async function getExecutor(options) {
14983
- const name = "name" in options ? options.name : options.executor.name;
16370
+ const name = options.executor.name;
14984
16371
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
14985
16372
  const workspaceId = await loadWorkspaceId({
14986
16373
  workspaceId: options.workspaceId,
@@ -15002,7 +16389,7 @@ const getCommand$6 = defineAppCommand({
15002
16389
  }),
15003
16390
  run: async (args) => {
15004
16391
  const executor = await getExecutor({
15005
- name: args.name,
16392
+ executor: { name: args.name },
15006
16393
  workspaceId: args["workspace-id"],
15007
16394
  profile: args.profile
15008
16395
  });
@@ -15486,8 +16873,18 @@ function parseStatus(status) {
15486
16873
  default: throw new Error(`Invalid status: ${status}. Valid values: UNSPECIFIED, PENDING, PENDING_RESUME, RUNNING, SUCCESS, FAILED, PENDING_RETRY, WAITING`);
15487
16874
  }
15488
16875
  }
16876
+ /**
16877
+ * List workflow executions with optional filters.
16878
+ *
16879
+ * Returns at most `options.limit` items. When `limit` is omitted or 0 the
16880
+ * function pages through every execution. The CLI caps this at 50 by
16881
+ * default via `pagedLogArgs`; programmatic callers that want the same
16882
+ * cap should pass `limit: 50` explicitly.
16883
+ * @param options - Workflow execution listing options
16884
+ * @returns List of workflow executions
16885
+ */
15489
16886
  async function listWorkflowExecutions(options) {
15490
- const workflowName = options && "workflowName" in options ? options.workflowName : options && "workflow" in options ? options.workflow?.name : void 0;
16887
+ const workflowName = options?.workflow?.name;
15491
16888
  const client = await initOperatorClient(await loadAccessToken({ profile: options?.profile }));
15492
16889
  const workspaceId = await loadWorkspaceId({
15493
16890
  workspaceId: options?.workspaceId,
@@ -15708,7 +17105,7 @@ const executionsCommand = defineAppCommand({
15708
17105
  const executions = await listWorkflowExecutions({
15709
17106
  workspaceId: args["workspace-id"],
15710
17107
  profile: args.profile,
15711
- workflowName: args["workflow-name"],
17108
+ workflow: args["workflow-name"] === void 0 ? void 0 : { name: args["workflow-name"] },
15712
17109
  status: args.status,
15713
17110
  order: args.order,
15714
17111
  limit: args.limit
@@ -15735,8 +17132,13 @@ async function resolveWorkflow(client, workspaceId, name) {
15735
17132
  if (!workflow) throw new Error(`Workflow '${name}' not found.`);
15736
17133
  return workflow;
15737
17134
  }
17135
+ /**
17136
+ * Get a workflow by name and return CLI-friendly info.
17137
+ * @param options - Workflow lookup options
17138
+ * @returns Workflow information
17139
+ */
15738
17140
  async function getWorkflow(options) {
15739
- const name = "name" in options ? options.name : options.workflow.name;
17141
+ const name = options.workflow.name;
15740
17142
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
15741
17143
  const workspaceId = await loadWorkspaceId({
15742
17144
  workspaceId: options.workspaceId,
@@ -15758,7 +17160,7 @@ const getCommand$5 = defineAppCommand({
15758
17160
  }),
15759
17161
  run: async (args) => {
15760
17162
  const workflow = await getWorkflow({
15761
- name: args.name,
17163
+ workflow: { name: args.name },
15762
17164
  workspaceId: args["workspace-id"],
15763
17165
  profile: args.profile
15764
17166
  });
@@ -15808,6 +17210,13 @@ async function resolveApplicationAuthNamespace(options) {
15808
17210
  if (!authNamespace) throw new Error(`Application ${config.name} does not have an auth configuration.`);
15809
17211
  return authNamespace;
15810
17212
  }
17213
+ /**
17214
+ * Start a workflow looked up by name, resolving the machine user from the
17215
+ * flag, environment, or profile default. Backs the `workflow start` command;
17216
+ * programmatic callers use {@link startWorkflow} with a workflow definition.
17217
+ * @param options - Start options keyed by workflow name
17218
+ * @returns Start result with wait helper
17219
+ */
15811
17220
  async function startWorkflowByName(options) {
15812
17221
  const machineUser = await loadMachineUserName({
15813
17222
  machineUser: options.machineUser,
@@ -15837,8 +17246,12 @@ async function startWorkflowByName(options) {
15837
17246
  interval: options.interval
15838
17247
  });
15839
17248
  }
17249
+ /**
17250
+ * Start a workflow and return a handle to wait for completion.
17251
+ * @param options - Start options
17252
+ * @returns Start result with wait helper
17253
+ */
15840
17254
  async function startWorkflow(options) {
15841
- if ("name" in options) return await startWorkflowByName(options);
15842
17255
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
15843
17256
  const workspaceId = await loadWorkspaceId({
15844
17257
  workspaceId: options.workspaceId,
@@ -15938,8 +17351,18 @@ function createUnknownExecutorJob(executorName, jobId) {
15938
17351
  updatedAt: "N/A"
15939
17352
  };
15940
17353
  }
17354
+ /**
17355
+ * List executor jobs for a given executor.
17356
+ *
17357
+ * Returns at most `options.limit` items. When `limit` is omitted or 0 the
17358
+ * function pages through every job. The CLI caps this at 50 by default
17359
+ * via `pagedLogArgs`; programmatic callers that want the same cap should
17360
+ * pass `limit: 50` explicitly.
17361
+ * @param options - Options for listing executor jobs
17362
+ * @returns List of executor job information
17363
+ */
15941
17364
  async function listExecutorJobs(options) {
15942
- const executorName = "executorName" in options ? options.executorName : options.executor.name;
17365
+ const executorName = options.executor.name;
15943
17366
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
15944
17367
  const workspaceId = await loadWorkspaceId({
15945
17368
  workspaceId: options.workspaceId,
@@ -15976,8 +17399,13 @@ async function listExecutorJobs(options) {
15976
17399
  throw error;
15977
17400
  }
15978
17401
  }
17402
+ /**
17403
+ * Get details of a specific executor job.
17404
+ * @param options - Options for getting executor job details
17405
+ * @returns Executor job detail information
17406
+ */
15979
17407
  async function getExecutorJob(options) {
15980
- const executorName = "executorName" in options ? options.executorName : options.executor.name;
17408
+ const executorName = options.executor.name;
15981
17409
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
15982
17410
  const workspaceId = await loadWorkspaceId({
15983
17411
  workspaceId: options.workspaceId,
@@ -16013,8 +17441,13 @@ async function getExecutorJob(options) {
16013
17441
  throw error;
16014
17442
  }
16015
17443
  }
17444
+ /**
17445
+ * Watch an executor job until completion, including downstream executions.
17446
+ * @param options - Options for watching executor job
17447
+ * @returns Result including job details and downstream execution info
17448
+ */
16016
17449
  async function watchExecutorJob(options) {
16017
- const executorName = "executorName" in options ? options.executorName : options.executor.name;
17450
+ const executorName = options.executor.name;
16018
17451
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
16019
17452
  const workspaceId = await loadWorkspaceId({
16020
17453
  workspaceId: options.workspaceId,
@@ -16333,7 +17766,7 @@ const jobsCommand = defineAppCommand({
16333
17766
  if (args.jobId) {
16334
17767
  if (args.wait) {
16335
17768
  const result = await watchExecutorJob({
16336
- executorName: args.executorName,
17769
+ executor: { name: args.executorName },
16337
17770
  jobId: args.jobId,
16338
17771
  workspaceId: args["workspace-id"],
16339
17772
  profile: args.profile,
@@ -16382,7 +17815,7 @@ const jobsCommand = defineAppCommand({
16382
17815
  return;
16383
17816
  }
16384
17817
  const job = await getExecutorJob({
16385
- executorName: args.executorName,
17818
+ executor: { name: args.executorName },
16386
17819
  jobId: args.jobId,
16387
17820
  attempts: args.attempts,
16388
17821
  workspaceId: args["workspace-id"],
@@ -16393,7 +17826,7 @@ const jobsCommand = defineAppCommand({
16393
17826
  } else {
16394
17827
  if (args.wait) logger.warn("--wait flag is ignored in list mode. Specify a job ID to wait.");
16395
17828
  const jobs = await listExecutorJobs({
16396
- executorName: args.executorName,
17829
+ executor: { name: args.executorName },
16397
17830
  status: args.status,
16398
17831
  order: args.order,
16399
17832
  limit: args.limit,
@@ -16503,8 +17936,12 @@ async function triggerExecutorByName(options) {
16503
17936
  throw error;
16504
17937
  }
16505
17938
  }
17939
+ /**
17940
+ * Trigger an executor and return the job ID.
17941
+ * @param options - Options for triggering executor
17942
+ * @returns Result containing the job ID if available
17943
+ */
16506
17944
  async function triggerExecutor(options) {
16507
- if ("executorName" in options) return await triggerExecutorByName(options);
16508
17945
  if (options.executor.trigger.kind !== "incomingWebhook" && options.payload !== void 0) throw new Error(`Executor '${options.executor.name}' has '${options.executor.trigger.kind}' trigger type. The payload is only available for 'incomingWebhook' trigger type.`);
16509
17946
  return await triggerExecutorByName({
16510
17947
  executorName: options.executor.name,
@@ -16621,7 +18058,7 @@ The \`--logs\` option displays logs from the downstream execution when available
16621
18058
  logger.success(`Executor '${args.executorName}' triggered successfully. Job ID: ${result.jobId}`);
16622
18059
  if (args.wait) {
16623
18060
  const watchResult = await watchExecutorJob({
16624
- executorName: args.executorName,
18061
+ executor: { name: args.executorName },
16625
18062
  jobId: result.jobId,
16626
18063
  workspaceId: args["workspace-id"],
16627
18064
  profile: args.profile,
@@ -17958,10 +19395,12 @@ async function loadOptions$10(options) {
17958
19395
  profile: options?.profile
17959
19396
  });
17960
19397
  const { config } = await loadConfig(options?.configPath);
19398
+ const application = defineApplication({ config });
19399
+ warnMissingAppId(application.id);
17961
19400
  return {
17962
19401
  client,
17963
19402
  workspaceId,
17964
- application: defineApplication({ config }),
19403
+ application,
17965
19404
  config
17966
19405
  };
17967
19406
  }
@@ -17987,6 +19426,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
17987
19426
  functionRegistry: await planFunctionRegistry(client, workspaceId, application.name, application.id, []),
17988
19427
  secretManager: await planSecretManager(ctx)
17989
19428
  };
19429
+ const leftBehind = Object.values(plans).some((plan) => "resourceOwners" in plan && plan.resourceOwners.has(application.name));
17990
19430
  const removeLines = [
17991
19431
  ...plans.functionRegistry.changeSet.lines(),
17992
19432
  ...plans.staticWebsite.changeSet.lines(),
@@ -18016,7 +19456,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
18016
19456
  ...plans.secretManager.secretChangeSet.lines()
18017
19457
  ];
18018
19458
  if (removeLines.length > 0) logger.log(removeLines.join("\n"));
18019
- if (plans.tailorDB.changeSet.service.deletes.length === 0 && plans.staticWebsite.changeSet.deletes.length === 0 && plans.aiGateway.changeSet.deletes.length === 0 && plans.idp.changeSet.service.deletes.length === 0 && plans.auth.changeSet.service.deletes.length === 0 && plans.pipeline.changeSet.service.deletes.length === 0 && plans.app.deletes.length === 0 && plans.executor.changeSet.deletes.length === 0 && plans.workflow.changeSet.deletes.length === 0 && plans.workflowExecutionPolicy.changeSet.deletes.length === 0 && plans.functionRegistry.changeSet.deletes.length === 0 && plans.secretManager.vaultChangeSet.deletes.length === 0 && plans.secretManager.secretChangeSet.deletes.length === 0) return;
19459
+ if (plans.tailorDB.changeSet.service.deletes.length === 0 && plans.staticWebsite.changeSet.deletes.length === 0 && plans.aiGateway.changeSet.deletes.length === 0 && plans.idp.changeSet.service.deletes.length === 0 && plans.auth.changeSet.service.deletes.length === 0 && plans.pipeline.changeSet.service.deletes.length === 0 && plans.app.deletes.length === 0 && plans.executor.changeSet.deletes.length === 0 && plans.workflow.changeSet.deletes.length === 0 && plans.workflowExecutionPolicy.changeSet.deletes.length === 0 && plans.functionRegistry.changeSet.deletes.length === 0 && plans.secretManager.vaultChangeSet.deletes.length === 0 && plans.secretManager.secretChangeSet.deletes.length === 0) return { leftBehind };
18020
19460
  if (confirm) await confirm();
18021
19461
  await applyWorkflow(client, plans.workflow, "delete");
18022
19462
  await applyWorkflowJobFunctionExecutionPolicy(client, plans.workflowExecutionPolicy, "delete");
@@ -18034,6 +19474,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
18034
19474
  await applyTailorDB(client, plans.tailorDB, "delete-services");
18035
19475
  await applyFunctionRegistry(client, workspaceId, plans.functionRegistry, "delete");
18036
19476
  await applySecretManager(client, plans.secretManager, "delete");
19477
+ return { leftBehind };
18037
19478
  }
18038
19479
  /**
18039
19480
  * Remove all resources managed by the current application.
@@ -18060,7 +19501,7 @@ const removeCommand$1 = defineAppCommand({
18060
19501
  });
18061
19502
  logger.info(`Planning removal of resources managed by "${application.name}"...`);
18062
19503
  logger.newline();
18063
- await execRemove(client, workspaceId, application, config, async () => {
19504
+ const { leftBehind } = await execRemove(client, workspaceId, application, config, async () => {
18064
19505
  if (!args.yes) {
18065
19506
  if (!await prompt.confirm({
18066
19507
  message: "Are you sure you want to remove all resources?",
@@ -18071,6 +19512,13 @@ const removeCommand$1 = defineAppCommand({
18071
19512
  `);
18072
19513
  } else logger.success("Removing all resources (--yes flag specified)...");
18073
19514
  });
19515
+ if (leftBehind) {
19516
+ logger.warn(multiline`
19517
+ Resources tagged with "${application.name}" were left in place: they carry an application id this config does not match.
19518
+ Put that id in your config, or run deploy to take them over first, then remove again.
19519
+ `);
19520
+ return;
19521
+ }
18074
19522
  logger.success(`Successfully removed all resources managed by "${application.name}".`);
18075
19523
  }
18076
19524
  });
@@ -19094,7 +20542,7 @@ async function generate(options) {
19094
20542
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
19095
20543
  let pluginManager;
19096
20544
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
19097
- const { defineApplication } = await import("./application-DArFTTJ9.mjs");
20545
+ const { defineApplication } = await import("./application-CXNaUNhv.mjs");
19098
20546
  const application = defineApplication({
19099
20547
  config,
19100
20548
  pluginManager
@@ -20308,8 +21756,13 @@ async function bundleQueryScript(engine, baseDir) {
20308
21756
  const entryContent = engine === "sql" ? createSqlEntry() : createGqlEntry();
20309
21757
  fs$2.writeFileSync(entryPath, entryContent);
20310
21758
  const tsconfig = await resolveTSConfigWithFallback(baseDir);
20311
- return (await rolldown.build({
20312
- plugins: [platformBundleDefinePlugin],
21759
+ const bundleLog = createBundleLog({ tsconfig });
21760
+ const result = await rolldown.build({
21761
+ plugins: [
21762
+ createGeneratedEntryResolverPlugin(entryPath, baseDir),
21763
+ createTsconfigPathsPlugin(),
21764
+ platformBundleDefinePlugin
21765
+ ],
20313
21766
  input: entryPath,
20314
21767
  write: false,
20315
21768
  output: {
@@ -20327,8 +21780,10 @@ async function bundleQueryScript(engine, baseDir) {
20327
21780
  annotations: true,
20328
21781
  unknownGlobalSideEffects: false
20329
21782
  },
20330
- logLevel: "silent"
20331
- })).output[0].code;
21783
+ ...bundleLog.options
21784
+ });
21785
+ bundleLog.assertAllResolved();
21786
+ return result.output[0].code;
20332
21787
  }
20333
21788
 
20334
21789
  //#endregion
@@ -21246,5 +22701,5 @@ async function registerTsHook(tsHookUrl) {
21246
22701
  }
21247
22702
 
21248
22703
  //#endregion
21249
- export { getOrganization as $, removeLegacyUserAlias as $n, ensureConfigId as $t, truncate as A, hasChanges as An, getExecutorJob as At, show as B, deleteUserTokens as Bn, getWorkflowExecution as Bt, healthCommand as C, isValidMigrationNumber as Cn, getFunctionRegistry as Ct, resumeWorkflow as D, parseMigrationNumberArg as Dn, triggerExecutor as Dt, resumeCommand as E, formatMigrationNumber as En, triggerCommand as Et, writeDbTypesFile as F, generateUserTypes as Fn, startCommand as Ft, removeCommand$1 as G, loadConfigPath as Gn, deploy as Gt, extractOwnedNamespaces as H, hasAnyUserTokenEntry as Hn, functionExecutionStatusToString as Ht, getConfiguredEditorCommand as I, prompt as In, startWorkflow as It, organizationTree as J, loadPlatformClientConfig as Jn, listWorkspaces as Jt, updateCommand$1 as K, loadConsoleBaseUrl as Kn, deployFromCLI as Kt, openInConfiguredEditor as L, apiCall as Ln, getCommand$5 as Lt, generate as M, resourceTrn as Mn, jobsCommand as Mt, generateCommand as N, sdkNameLabelKey as Nn, listExecutorJobs as Nt, listCommand$2 as O, formatDiffSummary as On, listCommand$8 as Ot, generateMigrationScript as P, PluginManager as Pn, watchExecutorJob as Pt, getCommand$1 as Q, readPlatformConfig as Qn, workspaceNameSchema as Qt, deployCommand as R, assertWritable as Rn, getWorkflow as Rt, getAppHealth as S, getNextMigrationNumber as Sn, getCommand$4 as St, waitWorkflowExecution as T, reconstructSnapshotFromMigrations as Tn, webhookCommand as Tt, logBetaWarning as U, hasUserTokenEntry as Un, getCommand$6 as Ut, showCommand as V, fetchLatestToken as Vn, listWorkflowExecutions as Vt, remove as W, loadAccessToken as Wn, getExecutor as Wt, listCommand$3 as X, loadWorkspaceId as Xn, createWorkspace$1 as Xt, treeCommand as Y, loadStoredUserTokens as Yn, createCommand$1 as Yt, listOrganizations as Z, platformConfigFromProfile as Zn, formatKeyValueTable as Zt, getWorkspace as _, createSnapshotFromLocalTypes as _n, listMachineUsers as _t, queryCommand as a, parseMigrationLabelNumber as an, commonArgs as ar, getFolder as at, listApps as b, getMigrationFilePath as bn, listCommand$7 as bt, removeCommand as c, protoGqlPermission as cn, deploymentArgs as cr, createCommand as ct, listUsers as d, INITIAL_SCHEMA_NUMBER as dn, pagedLogArgs as dr, listOAuth2Clients as dt, executeScript as en, resolveConfigUser as er, updateCommand$2 as et, inviteCommand as f, MIGRATE_FILE_NAME as fn, paginationArgs as fr, getCommand$3 as ft, getCommand as g, compareSnapshots as gn, listCommand$6 as gt, restoreWorkspace as h, compareLocalTypesWithSnapshot as hn, workspaceArgs as hr, tokenCommand as ht, query as i, handleOptionalToRequiredError as in, defineAppCommand as ir, getCommand$2 as it, truncateCommand as j, getNamespacesWithMigrations as jn, getExecutorWaitFailureMessage as jt, listWorkflows as k, formatMigrationDiff as kn, listExecutors as kt, removeUser as l, DB_TYPES_FILE_NAME as ln, isVerbose as lr, createFolder as lt, restoreCommand as m, assertValidMigrationFiles as mn, toPageDirection as mr, getMachineUserToken as mt, errorToJson as n, bundleMigrationScript as nn, saveUserTokens as nr, listCommand$4 as nt, updateCommand as o, compareSnapshotWithRemote as on, configArg as or, deleteCommand$1 as ot, inviteUser as p, SCHEMA_FILE_NAME as pn, resolveMachineUserInputSource as pr, getOAuth2Client as pt, updateOrganization as q, loadMachineUserName as qn, listCommand$9 as qt, serializeError as r, MIGRATION_LABEL_KEY as rn, writePlatformConfig as rr, listFolders as rt, updateUser as s, generateAllTypeManifestsFromSnapshot as sn, confirmationArgs as sr, deleteFolder as st, registerTsHook as t, waitForExecution as tn, resolveUserTokenKey as tr, updateFolder as tt, listCommand as u, DIFF_FILE_NAME as un, multiConfigArg as ur, listCommand$5 as ut, deleteCommand as v, getLatestMigrationNumber as vn, generate$1 as vt, waitCommand as w, loadDiff as wn, listWebhookExecutors as wt, listCommand$1 as x, getMigrationFiles as xn, listFunctionRegistries as xt, deleteWorkspace as y, getMigrationDirPath as yn, getAuthInput as yt, deployStaticWebsite as z, loadConfig as zn, executionsCommand as zt };
21250
- //# sourceMappingURL=register-ts-hook-BAHMwxfG.mjs.map
22704
+ export { getOrganization as $, hasUserTokenEntry as $n, ensureConfigId as $t, truncate as A, loadDiff as An, getExecutorJob as At, show as B, sdkAppIdLabelKey as Bn, getWorkflowExecution as Bt, healthCommand as C, createSnapshotFromLocalTypes as Cn, resolveMachineUserInputSource as Cr, getFunctionRegistry as Ct, resumeWorkflow as D, getMigrationFilePath as Dn, triggerExecutor as Dt, resumeCommand as E, getMigrationDirPath as En, triggerCommand as Et, writeDbTypesFile as F, formatMigrationDiff as Fn, startCommand as Ft, removeCommand$1 as G, generateUserTypes as Gn, deploy as Gt, extractOwnedNamespaces as H, sdkVersionLabelKey as Hn, functionExecutionStatusToString as Ht, getConfiguredEditorCommand as I, hasChanges as In, startWorkflow as It, organizationTree as J, assertWritable as Jn, listWorkspaces as Jt, updateCommand$1 as K, prompt as Kn, deployFromCLI as Kt, openInConfiguredEditor as L, getNamespacesWithMigrations as Ln, getCommand$5 as Lt, generate as M, formatMigrationNumber as Mn, jobsCommand as Mt, generateCommand as N, parseMigrationNumberArg as Nn, listExecutorJobs as Nt, listCommand$2 as O, getMigrationFiles as On, listCommand$8 as Ot, generateMigrationScript as P, formatDiffSummary as Pn, watchExecutorJob as Pt, getCommand$1 as Q, hasAnyUserTokenEntry as Qn, workspaceNameSchema as Qt, deployCommand as R, selectTargetNamespace as Rn, getWorkflow as Rt, getAppHealth as S, compareSnapshots as Sn, paginationArgs as Sr, getCommand$4 as St, waitWorkflowExecution as T, getLatestMigrationNumber as Tn, workspaceArgs as Tr, webhookCommand as Tt, logBetaWarning as U, writeMetadataLabels as Un, getCommand$6 as Ut, showCommand as V, sdkNameLabelKey as Vn, listWorkflowExecutions as Vt, remove as W, PluginManager as Wn, getExecutor as Wt, listCommand$3 as X, deleteUserTokens as Xn, createWorkspace$1 as Xt, treeCommand as Y, loadConfig as Yn, createCommand$1 as Yt, listOrganizations as Z, fetchLatestToken as Zn, formatKeyValueTable as Zt, getWorkspace as _, MIGRATE_FILE_NAME as _n, confirmationArgs as _r, listMachineUsers as _t, queryCommand as a, toTailorDBDeployInput as an, loadStoredUserTokens as ar, getFolder as at, listApps as b, assertValidMigrationFiles as bn, multiConfigArg as br, listCommand$7 as bt, removeCommand as c, handleOptionalToRequiredError as cn, readPlatformConfig as cr, createCommand as ct, listUsers as d, compareSnapshotWithRemote as dn, resolveUserTokenKey as dr, listOAuth2Clients as dt, executeScript as en, loadAccessToken as er, updateCommand$2 as et, inviteCommand as f, generateAllTypeManifestsFromSnapshot as fn, saveUserTokens as fr, getCommand$3 as ft, getCommand as g, INITIAL_SCHEMA_NUMBER as gn, configArg as gr, listCommand$6 as gt, restoreWorkspace as h, DIFF_FILE_NAME as hn, commonArgs as hr, tokenCommand as ht, query as i, logRemoteDriftGuidance as in, loadPlatformClientConfig as ir, getCommand$2 as it, truncateCommand as j, reconstructSnapshotFromMigrations as jn, getExecutorWaitFailureMessage as jt, listWorkflows as k, getNextMigrationNumber as kn, listExecutors as kt, removeUser as l, parseMigrationLabelNumber as ln, removeLegacyUserAlias as lr, createFolder as lt, restoreCommand as m, DB_TYPES_FILE_NAME as mn, defineAppCommand as mr, getMachineUserToken as mt, errorToJson as n, bundleMigrationScript as nn, loadConsoleBaseUrl as nr, listCommand$4 as nt, updateCommand as o, verifyRemoteSchema as on, loadWorkspaceId as or, deleteCommand$1 as ot, inviteUser as p, protoGqlPermission as pn, writePlatformConfig as pr, getOAuth2Client as pt, updateOrganization as q, apiCall as qn, listCommand$9 as qt, serializeError as r, checkMigrationDiffs as rn, loadMachineUserName as rr, listFolders as rt, updateUser as s, MIGRATION_LABEL_KEY as sn, platformConfigFromProfile as sr, deleteFolder as st, registerTsHook as t, waitForExecution as tn, loadConfigPath as tr, updateFolder as tt, listCommand as u, sanitizeMigrationLabel as un, resolveConfigUser as ur, listCommand$5 as ut, deleteCommand as v, SCHEMA_FILE_NAME as vn, deploymentArgs as vr, generate$1 as vt, waitCommand as w, formatSchemaDrifts as wn, toPageDirection as wr, listWebhookExecutors as wt, listCommand$1 as x, compareLocalTypesWithSnapshot as xn, pagedLogArgs as xr, listFunctionRegistries as xt, deleteWorkspace as y, assertMigrationNumberExists as yn, isVerbose as yr, getAuthInput as yt, deployStaticWebsite as z, resourceTrn as zn, executionsCommand as zt };
22705
+ //# sourceMappingURL=register-ts-hook-LYV7zH-e.mjs.map