@tailor-platform/sdk 1.83.0 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/application-C-zsrNjn.mjs +3 -0
  3. package/dist/{application-Cp0FxIm2.mjs → application-m5tYtz0-.mjs} +246 -68
  4. package/dist/application-m5tYtz0-.mjs.map +1 -0
  5. package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -0
  6. package/dist/cli/commands/generate/seed/bundler.d.mts +2 -1
  7. package/dist/cli/commands/generate/service.d.mts +1 -1
  8. package/dist/cli/commands/generate/types.d.mts +1 -1
  9. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +11 -1
  10. package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +5 -0
  11. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +1 -1
  12. package/dist/cli/index.mjs +291 -29
  13. package/dist/cli/index.mjs.map +1 -1
  14. package/dist/cli/lib.d.mts +1 -1
  15. package/dist/cli/lib.mjs +92 -36
  16. package/dist/cli/lib.mjs.map +1 -1
  17. package/dist/cli/shared/client.d.mts +28 -3
  18. package/dist/cli/skills.d.mts +1 -1
  19. package/dist/completion/zsh-worker.zsh +85 -22
  20. package/dist/configure/index.d.mts +2 -2
  21. package/dist/configure/index.mjs +170 -2
  22. package/dist/configure/index.mjs.map +1 -1
  23. package/dist/configure/services/executor/index.d.mts +2 -2
  24. package/dist/configure/services/executor/trigger/event.d.mts +245 -2
  25. package/dist/configure/services/executor/trigger/index.d.mts +3 -3
  26. package/dist/configure/services/index.d.mts +2 -2
  27. package/dist/configure/services/tailordb/index.d.mts +0 -1
  28. package/dist/configure/services/workflow/job.d.mts +9 -0
  29. package/dist/configure/services/workflow/workflow.d.mts +10 -0
  30. package/dist/node-builtins-CmaL2Cbq.mjs +36 -0
  31. package/dist/node-builtins-CmaL2Cbq.mjs.map +1 -0
  32. package/dist/plugin/builtin/seed/index.mjs +1 -1
  33. package/dist/{register-typescript-runtime-CCHyKeXO.mjs → register-typescript-runtime-nG-n9lPw.mjs} +752 -403
  34. package/dist/register-typescript-runtime-nG-n9lPw.mjs.map +1 -0
  35. package/dist/{seed-DFFigL8V.mjs → seed-6eTFj-3Q.mjs} +158 -41
  36. package/dist/seed-6eTFj-3Q.mjs.map +1 -0
  37. package/dist/{service-B0MaodSg.mjs → service-CPJWhTLn.mjs} +29 -2
  38. package/dist/service-CPJWhTLn.mjs.map +1 -0
  39. package/dist/service-Cigg_U-v.mjs +3 -0
  40. package/dist/{service_pb-BwZRiNug.mjs → service_pb-D5--My_M.mjs} +5 -5
  41. package/dist/service_pb-D5--My_M.mjs.map +1 -0
  42. package/dist/{service_pb-wRkxUta6.mjs → service_pb-i8CmnQP4.mjs} +1 -1
  43. package/dist/tailor-proto/src/tailor/v1/application_pb.d.mts +106 -1
  44. package/dist/tailor-proto/src/tailor/v1/executor_resource_pb.d.mts +24 -1
  45. package/dist/tailor-proto/src/tailor/v1/function_pb.d.mts +49 -1
  46. package/dist/tailor-proto/src/tailor/v1/service_pb.d.mts +89 -6
  47. package/dist/tailor-proto/src/tailor/v1/telemetryrouter_resource_pb.d.mts +26 -1
  48. package/dist/tailor-proto/src/tailor/v1/workflow_pb.d.mts +101 -1
  49. package/dist/tailor-proto/src/tailor/v1/workflow_resource_pb.d.mts +22 -1
  50. package/dist/types/executor.generated.d.mts +23 -3
  51. package/dist/vitest/index.mjs +3 -35
  52. package/dist/vitest/index.mjs.map +1 -1
  53. package/dist/{workspace_resource_pb-UGK1SSn_.mjs → workspace_resource_pb-DXnO2AT-.mjs} +7 -7
  54. package/dist/workspace_resource_pb-DXnO2AT-.mjs.map +1 -0
  55. package/docs/cli/tailordb.md +36 -12
  56. package/docs/cli-reference.md +16 -15
  57. package/docs/generator/builtin.md +7 -0
  58. package/docs/services/executor.md +83 -3
  59. package/docs/services/idp.md +3 -3
  60. package/docs/services/tailordb-migration.md +24 -4
  61. package/docs/services/workflow.md +26 -0
  62. package/package.json +17 -18
  63. package/dist/application-CP7JKG9R.mjs +0 -3
  64. package/dist/application-Cp0FxIm2.mjs.map +0 -1
  65. package/dist/register-typescript-runtime-CCHyKeXO.mjs.map +0 -1
  66. package/dist/seed-DFFigL8V.mjs.map +0 -1
  67. package/dist/service-B0MaodSg.mjs.map +0 -1
  68. package/dist/service-KnAiVofD.mjs +0 -3
  69. package/dist/service_pb-BwZRiNug.mjs.map +0 -1
  70. package/dist/workspace_resource_pb-UGK1SSn_.mjs.map +0 -1
@@ -1,14 +1,14 @@
1
1
  import { t as db } from "./schema-CYrle8JZ.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 rememberPlatformConfigForToken, H as initOperatorClient, J as createApplyLimiter, K as resolveStaticWebsiteUrls, 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 normalizeBaseUrl, Y as LOG_LEVELS, _ as stringifyFunction, d as buildExecutorArgsExpr, f as buildResolverOperationHookExpr, g as TailorDBTypeSchema$1, h as assertUniqueTailorDBTypeNamesWithExternal, i as resolverBundleKey, j as fetchAll, l as HTTP_METHODS, m as assertUniqueLocalTailorDBTypeNames, n as generatePluginFilesIfNeeded, o as WorkflowJobFunctionExecutionPolicySchema, q as byName, r as loadApplication, t as defineApplication, v as resolveTSConfigWithFallback, w as hasGenerationHooks, y as platformBundleDefinePlugin, z as getOrNull } from "./application-Cp0FxIm2.mjs";
6
- import { o as loadFilesWithIgnores, s as functionSchema, t as createExecutorService } from "./service-B0MaodSg.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, d as buildExecutorArgsExpr, f as buildResolverOperationHookExpr, g as TailorDBTypeSchema$1, h as assertUniqueTailorDBTypeNamesWithExternal, i as resolverBundleKey, j as hashFile, k as getDistDir, l as HTTP_METHODS, m as assertUniqueLocalTailorDBTypeNames, n as generatePluginFilesIfNeeded, o as WorkflowJobFunctionExecutionPolicySchema, q as isNotFoundError, r as loadApplication, t as defineApplication, v as createGeneratedEntryResolverPlugin, x as platformBundleDefinePlugin, y as createTsconfigPathsPlugin } from "./application-m5tYtz0-.mjs";
6
+ import { o as loadFilesWithIgnores, s as functionSchema, t as createExecutorService } from "./service-CPJWhTLn.mjs";
7
7
  import { n as enumConstantsPlugin, t as EnumConstantsGeneratorID } from "./enum-constants-j9QBF0cB.mjs";
8
8
  import { t as multiline } from "./multiline-sfHpTZZK.mjs";
9
9
  import { n as fileUtilsPlugin, t as FileUtilsGeneratorID } from "./file-utils-DcyIPFQh.mjs";
10
10
  import { n as kyselyTypePlugin, t as KyselyGeneratorID } from "./kysely-type-D-zflMGf.mjs";
11
- import { n as seedPlugin, t as SeedGeneratorID } from "./seed-DFFigL8V.mjs";
11
+ import { n as seedPlugin, t as SeedGeneratorID } from "./seed-6eTFj-3Q.mjs";
12
12
  import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
13
13
  import { t as userAgent } from "./user-agent-DX2Jnw0-.mjs";
14
14
  import { r as isCLIError, t as createCLIError } from "./errors-DVFS0AZk.mjs";
@@ -5926,6 +5926,18 @@ function protoExecutor(context, executor) {
5926
5926
  }
5927
5927
  });
5928
5928
  break;
5929
+ case "workflowExecution":
5930
+ case "workflowJobExecution":
5931
+ triggerType = ExecutorTriggerType.EVENT;
5932
+ triggerConfig = typedEventTrigger({
5933
+ case: "workflow",
5934
+ value: {
5935
+ eventTypes: trigger.events,
5936
+ workflowName: trigger.workflowName,
5937
+ ...trigger.condition ? { condition: { expr: `(${stringifyFunction(trigger.condition)})(${argsExpr})` } } : {}
5938
+ }
5939
+ });
5940
+ break;
5929
5941
  default: throw new Error(`Unknown trigger: ${trigger}`);
5930
5942
  }
5931
5943
  const target = executor.operation;
@@ -7200,6 +7212,10 @@ const warningChangeInfoSchema = z.looseObject({
7200
7212
  fieldName: z.string().optional(),
7201
7213
  reason: z.string()
7202
7214
  });
7215
+ const scriptSkippedInfoSchema = z.looseObject({
7216
+ reason: z.string().trim().min(1),
7217
+ acknowledgedAt: z.string()
7218
+ });
7203
7219
  const migrationDiffSchema = z.looseObject({
7204
7220
  version: z.number(),
7205
7221
  namespace: z.string(),
@@ -7210,7 +7226,8 @@ const migrationDiffSchema = z.looseObject({
7210
7226
  breakingChanges: z.array(breakingChangeInfoSchema),
7211
7227
  hasWarnings: z.boolean().optional(),
7212
7228
  warnings: z.array(warningChangeInfoSchema).optional(),
7213
- requiresMigrationScript: z.boolean()
7229
+ requiresMigrationScript: z.boolean(),
7230
+ scriptSkipped: scriptSkippedInfoSchema.optional()
7214
7231
  });
7215
7232
 
7216
7233
  //#endregion
@@ -7852,75 +7869,81 @@ function areFieldsDifferent(oldField, newField) {
7852
7869
  return false;
7853
7870
  }
7854
7871
  /**
7855
- * Determine if a field change is a breaking change
7872
+ * Collect breaking changes for a field change
7856
7873
  * @param {string} typeName - Name of the type containing the field
7857
7874
  * @param {string} fieldName - Name of the field being changed
7858
7875
  * @param {SnapshotFieldConfig | undefined} oldField - Old field configuration
7859
7876
  * @param {SnapshotFieldConfig | undefined} newField - New field configuration
7860
- * @returns {BreakingChangeInfo | null} Breaking change info or null if not breaking
7877
+ * @returns {BreakingChangeInfo[]} Breaking change information
7861
7878
  */
7862
- function isBreakingFieldChange(typeName, fieldName, oldField, newField) {
7863
- if (!oldField && newField && newField.required) return {
7879
+ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
7880
+ const breakingChanges = [];
7881
+ if (!oldField && newField && newField.required) breakingChanges.push({
7864
7882
  typeName,
7865
7883
  fieldName,
7866
7884
  reason: "Required field added"
7867
- };
7868
- if (oldField && newField && oldField.type !== newField.type) return {
7885
+ });
7886
+ if (oldField && newField && oldField.type !== newField.type) breakingChanges.push({
7869
7887
  typeName,
7870
7888
  fieldName,
7871
7889
  reason: `Field type changed from ${oldField.type} to ${newField.type}`,
7872
7890
  unsupported: true,
7873
7891
  showThreeStepHint: true
7874
- };
7875
- if (oldField && newField && !oldField.required && newField.required) return {
7892
+ });
7893
+ if (oldField && newField && !oldField.required && newField.required) breakingChanges.push({
7876
7894
  typeName,
7877
7895
  fieldName,
7878
7896
  reason: "Field changed from optional to required"
7879
- };
7897
+ });
7880
7898
  if (oldField && newField && (oldField.array ?? false) !== (newField.array ?? false)) {
7881
7899
  const [fromType, toType] = oldField.array ? ["array", "single value"] : ["single value", "array"];
7882
- return {
7900
+ breakingChanges.push({
7883
7901
  typeName,
7884
7902
  fieldName,
7885
7903
  reason: `Field changed from ${fromType} to ${toType}`,
7886
7904
  unsupported: true,
7887
7905
  showThreeStepHint: true
7888
- };
7906
+ });
7889
7907
  }
7890
7908
  if (oldField && newField) {
7891
7909
  const oldForeignKeyType = oldField.foreignKeyType;
7892
7910
  const newForeignKeyType = newField.foreignKeyType;
7893
- if (oldForeignKeyType && newForeignKeyType && oldForeignKeyType !== newForeignKeyType) return {
7911
+ if (oldForeignKeyType && newForeignKeyType && oldForeignKeyType !== newForeignKeyType) breakingChanges.push({
7894
7912
  typeName,
7895
7913
  fieldName,
7896
7914
  reason: `Foreign key target type changed from ${oldForeignKeyType} to ${newForeignKeyType}`
7897
- };
7915
+ });
7898
7916
  }
7899
- if (oldField && newField && !(oldField.unique ?? false) && (newField.unique ?? false)) return {
7917
+ if (oldField && newField && !(oldField.unique ?? false) && (newField.unique ?? false)) breakingChanges.push({
7900
7918
  typeName,
7901
7919
  fieldName,
7902
7920
  reason: "Unique constraint added to field"
7903
- };
7921
+ });
7922
+ if (oldField?.type === "decimal" && newField?.type === "decimal" && oldField.scale !== newField.scale) breakingChanges.push({
7923
+ typeName,
7924
+ fieldName,
7925
+ reason: `Decimal scale changed from ${oldField.scale} to ${newField.scale}`
7926
+ });
7904
7927
  if (oldField && newField && oldField.type === "enum" && newField.type === "enum") {
7905
7928
  const oldAllowed = oldField.allowedValues ?? [];
7906
7929
  const newAllowed = newField.allowedValues ?? [];
7907
7930
  const oldValues = oldAllowed.map((v) => v.value);
7908
7931
  const newValuesSet = new Set(newAllowed.map((v) => v.value));
7909
7932
  const removedValues = oldValues.filter((v) => !newValuesSet.has(v));
7910
- if (removedValues.length > 0) return {
7933
+ if (removedValues.length > 0) breakingChanges.push({
7911
7934
  typeName,
7912
7935
  fieldName,
7913
7936
  reason: `Enum values removed: ${removedValues.join(", ")}`
7914
- };
7937
+ });
7915
7938
  }
7916
- return null;
7939
+ return breakingChanges;
7917
7940
  }
7918
7941
  function addChange(ctx, change, oldField, newField) {
7919
7942
  ctx.changes.push(change);
7920
7943
  if (!change.fieldName) return;
7921
- const breaking = isBreakingFieldChange(change.typeName, change.fieldName, oldField, newField);
7922
- if (breaking) {
7923
- ctx.breakingChanges.push(breaking);
7944
+ const breakingChanges = getBreakingFieldChanges(change.typeName, change.fieldName, oldField, newField);
7945
+ if (breakingChanges.length > 0) {
7946
+ ctx.breakingChanges.push(...breakingChanges);
7924
7947
  return;
7925
7948
  }
7926
7949
  if (change.kind === "field_removed") ctx.warnings.push({
@@ -9521,6 +9544,283 @@ function handleOptionalToRequiredError(error, messages) {
9521
9544
  throw error;
9522
9545
  }
9523
9546
 
9547
+ //#endregion
9548
+ //#region src/cli/commands/tailordb/migrate/schema-checks.ts
9549
+ /**
9550
+ * Schema validation checks shared between `deploy` and
9551
+ * `tailordb migration validate`.
9552
+ *
9553
+ * These checks are read-only: they compare local type definitions, the
9554
+ * migration snapshot history, and the remote schema without mutating any
9555
+ * state.
9556
+ */
9557
+ /**
9558
+ * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
9559
+ * @param service - Loaded TailorDB service (after `loadTypes()`)
9560
+ * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
9561
+ */
9562
+ function toTailorDBDeployInput(service) {
9563
+ const types = {};
9564
+ for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
9565
+ return {
9566
+ namespace: service.namespace,
9567
+ config: service.config,
9568
+ types
9569
+ };
9570
+ }
9571
+ /**
9572
+ * Fetch all TailorDB types from remote for a namespace
9573
+ * @param {OperatorClient} client - Operator client instance
9574
+ * @param {string} workspaceId - Workspace ID
9575
+ * @param {string} namespace - TailorDB namespace
9576
+ * @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
9577
+ */
9578
+ async function fetchRemoteTypes(client, workspaceId, namespace) {
9579
+ return fetchAllTolerant(async (pageToken, maxPageSize) => {
9580
+ const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
9581
+ workspaceId,
9582
+ namespaceName: namespace,
9583
+ pageToken,
9584
+ pageSize: maxPageSize
9585
+ });
9586
+ return [tailordbTypes, nextPageToken];
9587
+ });
9588
+ }
9589
+ async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
9590
+ return fetchAllTolerant(async (pageToken, maxPageSize) => {
9591
+ const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
9592
+ workspaceId,
9593
+ namespaceName: namespace,
9594
+ pageToken,
9595
+ pageSize: maxPageSize
9596
+ });
9597
+ return [permissions, nextPageToken];
9598
+ });
9599
+ }
9600
+ function definedWhenNotEmpty(value) {
9601
+ return Object.keys(value).length > 0 ? value : void 0;
9602
+ }
9603
+ function namespaceConfig(config, tailorDBInputs, namespace) {
9604
+ return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
9605
+ }
9606
+ function namespaceGqlOperations(config, tailorDBInputs, namespace) {
9607
+ return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
9608
+ }
9609
+ const GQL_OPERATION_KEYS = [
9610
+ "create",
9611
+ "update",
9612
+ "delete",
9613
+ "read"
9614
+ ];
9615
+ function configuredDisabledGqlOperations(operations) {
9616
+ if (!operations) return void 0;
9617
+ if (operations === "query") return {
9618
+ create: false,
9619
+ update: false,
9620
+ delete: false
9621
+ };
9622
+ const disabled = {};
9623
+ for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
9624
+ return definedWhenNotEmpty(disabled);
9625
+ }
9626
+ function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
9627
+ if (!remoteDisabled || !configuredDisabled) return void 0;
9628
+ const disabled = {};
9629
+ for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
9630
+ return definedWhenNotEmpty(disabled);
9631
+ }
9632
+ function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
9633
+ const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
9634
+ const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
9635
+ const types = {};
9636
+ for (const [typeName, type] of Object.entries(snapshot.types)) {
9637
+ const settings = { ...type.settings };
9638
+ const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
9639
+ if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
9640
+ if (type.settings?.gqlOperations === void 0) {
9641
+ const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
9642
+ if (disabled) settings.gqlOperations = disabled;
9643
+ }
9644
+ const comparableType = { ...type };
9645
+ const comparableSettings = definedWhenNotEmpty(settings);
9646
+ if (comparableSettings) comparableType.settings = comparableSettings;
9647
+ else delete comparableType.settings;
9648
+ types[typeName] = comparableType;
9649
+ }
9650
+ return {
9651
+ ...snapshot,
9652
+ types
9653
+ };
9654
+ }
9655
+ async function getRemoteMigrationNumberState(client, workspaceId, namespace) {
9656
+ const trn = resourceTrn(workspaceId, "tailordb", namespace);
9657
+ const metadata = await getOrNull(async () => {
9658
+ const { metadata } = await client.getMetadata({ trn });
9659
+ return metadata;
9660
+ });
9661
+ if (!metadata) return {
9662
+ metadataExists: false,
9663
+ number: null
9664
+ };
9665
+ const label = metadata.labels[MIGRATION_LABEL_KEY];
9666
+ if (!label) return {
9667
+ metadataExists: true,
9668
+ number: null
9669
+ };
9670
+ return {
9671
+ metadataExists: true,
9672
+ number: parseMigrationLabelNumber(label)
9673
+ };
9674
+ }
9675
+ /**
9676
+ * Get the current migration number from remote metadata
9677
+ * @param {OperatorClient} client - Operator client instance
9678
+ * @param {string} workspaceId - Workspace ID
9679
+ * @param {string} namespace - TailorDB namespace
9680
+ * @returns {Promise<number | null>} Current migration number, or null if no migration label exists
9681
+ */
9682
+ async function getRemoteMigrationNumber(client, workspaceId, namespace) {
9683
+ return (await getRemoteMigrationNumberState(client, workspaceId, namespace)).number;
9684
+ }
9685
+ /**
9686
+ * Verify remote schema matches the expected snapshot state
9687
+ * @param {OperatorClient} client - Operator client instance
9688
+ * @param {string} workspaceId - Workspace ID
9689
+ * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
9690
+ * @param {LoadedConfig} config - Loaded application config
9691
+ * @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
9692
+ * @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
9693
+ */
9694
+ async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
9695
+ const results = [];
9696
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
9697
+ const { metadataExists, number: remoteMigrationNumber } = await getRemoteMigrationNumberState(client, workspaceId, namespace);
9698
+ if (remoteMigrationNumber === null) {
9699
+ results.push({
9700
+ namespace,
9701
+ remoteMigrationNumber: 0,
9702
+ drifts: [],
9703
+ hasDrift: false,
9704
+ skipped: metadataExists ? "no_migration_label" : "not_deployed"
9705
+ });
9706
+ continue;
9707
+ }
9708
+ if (remoteMigrationNumber > getLatestMigrationNumber(migrationsDir)) {
9709
+ results.push({
9710
+ namespace,
9711
+ remoteMigrationNumber,
9712
+ drifts: [],
9713
+ hasDrift: false,
9714
+ checkpointMissingLocal: true
9715
+ });
9716
+ continue;
9717
+ }
9718
+ const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
9719
+ if (!expectedSnapshot) {
9720
+ results.push({
9721
+ namespace,
9722
+ remoteMigrationNumber,
9723
+ drifts: [],
9724
+ hasDrift: false,
9725
+ skipped: "no_snapshot"
9726
+ });
9727
+ continue;
9728
+ }
9729
+ const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
9730
+ const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
9731
+ results.push({
9732
+ namespace,
9733
+ remoteMigrationNumber,
9734
+ drifts,
9735
+ hasDrift: drifts.length > 0
9736
+ });
9737
+ }
9738
+ return results;
9739
+ }
9740
+ /**
9741
+ * Log common causes of remote schema drift and how to resolve them
9742
+ */
9743
+ function logRemoteDriftGuidance() {
9744
+ logger.info("This may indicate:");
9745
+ logger.info(" - Another developer applied different migrations", { mode: "plain" });
9746
+ logger.info(" - Manual schema changes were made directly", { mode: "plain" });
9747
+ logger.info(" - Migration history is out of sync", { mode: "plain" });
9748
+ logger.newline();
9749
+ logger.info("To resolve:");
9750
+ logger.info(" - Run 'tailor-sdk tailordb migration status' to compare local vs remote.", { mode: "plain" });
9751
+ logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
9752
+ logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
9753
+ logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
9754
+ }
9755
+ /**
9756
+ * Format remote schema verification results for display
9757
+ * @param {RemoteSchemaVerificationResult[]} results - Verification results
9758
+ * @returns {string} Formatted results string
9759
+ */
9760
+ function formatRemoteVerificationResults(results) {
9761
+ const lines = [];
9762
+ for (const result of results) {
9763
+ if (!result.hasDrift) continue;
9764
+ lines.push(`Namespace: ${result.namespace}`);
9765
+ lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
9766
+ lines.push(` Differences:`);
9767
+ lines.push(formatSchemaDrifts(result.drifts));
9768
+ lines.push("");
9769
+ }
9770
+ return lines.join("\n");
9771
+ }
9772
+ /**
9773
+ * Check if there are schema differences between migration snapshots and local definitions
9774
+ * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
9775
+ * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
9776
+ * @returns {Promise<MigrationCheckResult[]>} Results for each namespace
9777
+ */
9778
+ async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
9779
+ const results = [];
9780
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
9781
+ const localTypes = typesByNamespace.get(namespace);
9782
+ if (!localTypes) continue;
9783
+ const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
9784
+ if (!previousSnapshot) {
9785
+ results.push({
9786
+ namespace,
9787
+ migrationsDir,
9788
+ hasDiff: true,
9789
+ diff: void 0
9790
+ });
9791
+ continue;
9792
+ }
9793
+ const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
9794
+ results.push({
9795
+ namespace,
9796
+ migrationsDir,
9797
+ hasDiff: hasChanges(diff),
9798
+ diff: hasChanges(diff) ? diff : void 0
9799
+ });
9800
+ }
9801
+ return results;
9802
+ }
9803
+ /**
9804
+ * Format migration check results for display
9805
+ * @param {MigrationCheckResult[]} results - Migration check results
9806
+ * @returns {string} Formatted results string
9807
+ */
9808
+ function formatMigrationCheckResults(results) {
9809
+ const lines = [];
9810
+ for (const result of results) {
9811
+ if (!result.hasDiff) continue;
9812
+ lines.push(`Namespace: ${result.namespace}`);
9813
+ if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor-sdk tailordb migration generate' first.");
9814
+ else {
9815
+ lines.push(` ${formatDiffSummary(result.diff)}`);
9816
+ lines.push("");
9817
+ lines.push(formatMigrationDiff(result.diff));
9818
+ }
9819
+ lines.push("");
9820
+ }
9821
+ return lines.join("\n");
9822
+ }
9823
+
9524
9824
  //#endregion
9525
9825
  //#region src/cli/commands/tailordb/migrate/bundler.ts
9526
9826
  /**
@@ -9569,31 +9869,39 @@ async function bundleMigrationScript(sourceFile, namespace, migrationNumber, env
9569
9869
  }
9570
9870
  `;
9571
9871
  fs$1.writeFileSync(entryPath, entryContent);
9572
- const tsconfig = await resolveTSConfigWithFallback(baseDir ?? path.dirname(absoluteSourcePath));
9872
+ const projectDir = baseDir ?? path.dirname(absoluteSourcePath);
9873
+ const tsconfig = await resolveTSConfigWithFallback(projectDir);
9874
+ const bundleLog = createBundleLog({ tsconfig });
9875
+ const result = await rolldown.build({
9876
+ plugins: [
9877
+ createGeneratedEntryResolverPlugin(entryPath, projectDir),
9878
+ createTsconfigPathsPlugin(),
9879
+ platformBundleDefinePlugin
9880
+ ],
9881
+ input: entryPath,
9882
+ write: false,
9883
+ output: {
9884
+ format: "esm",
9885
+ sourcemap: false,
9886
+ minify: false,
9887
+ codeSplitting: false,
9888
+ globals: { tailordb: "tailordb" }
9889
+ },
9890
+ external: ["tailordb"],
9891
+ resolve: { conditionNames: ["node", "import"] },
9892
+ tsconfig,
9893
+ treeshake: {
9894
+ moduleSideEffects: false,
9895
+ annotations: true,
9896
+ unknownGlobalSideEffects: false
9897
+ },
9898
+ ...bundleLog.options
9899
+ });
9900
+ bundleLog.assertAllResolved();
9573
9901
  return {
9574
9902
  namespace,
9575
9903
  migrationNumber,
9576
- bundledCode: (await rolldown.build({
9577
- plugins: [platformBundleDefinePlugin],
9578
- input: entryPath,
9579
- write: false,
9580
- output: {
9581
- format: "esm",
9582
- sourcemap: false,
9583
- minify: false,
9584
- codeSplitting: false,
9585
- globals: { tailordb: "tailordb" }
9586
- },
9587
- external: ["tailordb"],
9588
- resolve: { conditionNames: ["node", "import"] },
9589
- tsconfig,
9590
- treeshake: {
9591
- moduleSideEffects: false,
9592
- annotations: true,
9593
- unknownGlobalSideEffects: false
9594
- },
9595
- logLevel: "silent"
9596
- })).output[0].code
9904
+ bundledCode: result.output[0].code
9597
9905
  };
9598
9906
  }
9599
9907
 
@@ -9855,8 +10163,9 @@ async function getCurrentMigrationNumber(client, workspaceId, namespace) {
9855
10163
  const label = metadata?.labels[MIGRATION_LABEL_KEY];
9856
10164
  if (!label) return 0;
9857
10165
  return parseMigrationLabelNumber(label) ?? 0;
9858
- } catch {
9859
- return 0;
10166
+ } catch (error) {
10167
+ if (isNotFoundError(error)) return 0;
10168
+ throw error;
9860
10169
  }
9861
10170
  }
9862
10171
  /**
@@ -9864,9 +10173,10 @@ async function getCurrentMigrationNumber(client, workspaceId, namespace) {
9864
10173
  * @param {OperatorClient} client - Operator client instance
9865
10174
  * @param {string} workspaceId - Workspace ID
9866
10175
  * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
10176
+ * @param {string} [configPath] - Config file path, included in remediation guidance when provided
9867
10177
  * @returns {Promise<PendingMigration[]>} List of pending migrations
9868
10178
  */
9869
- async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations) {
10179
+ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations, configPath) {
9870
10180
  const pendingMigrations = [];
9871
10181
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
9872
10182
  const currentMigration = await getCurrentMigrationNumber(client, workspaceId, namespace);
@@ -9878,9 +10188,15 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
9878
10188
  const diff = loadDiff(diffPath);
9879
10189
  const scriptPath = getMigrationFilePath(migrationsDir, file.number, "migrate");
9880
10190
  const hasScript = fs$1.existsSync(scriptPath);
9881
- if (diff.requiresMigrationScript && !hasScript) {
9882
- logger.warn(`Migration ${namespace}/${file.number} requires a script but migrate.ts not found`);
9883
- continue;
10191
+ if (diff.requiresMigrationScript && !hasScript && !diff.scriptSkipped) {
10192
+ const configArg = configPath ? ` --config "${path.relative(process.cwd(), configPath) || configPath}"` : "";
10193
+ const scriptCmd = `tailor-sdk tailordb migration script ${file.number} --namespace ${namespace}${configArg}`;
10194
+ throw new Error(`Migration ${namespace}/${formatMigrationNumber(file.number)} requires a migration script but migrate.ts was not found.\nTo resolve, either:\n - Add a script: ${scriptCmd}\n - Or record that no script is needed: ${scriptCmd} --no-script --reason "<why no data migration is needed>"`);
10195
+ }
10196
+ if (diff.scriptSkipped) {
10197
+ const migrationLabel = `${namespace}/${formatMigrationNumber(file.number)}`;
10198
+ if (hasScript) logger.warn(`Migration ${migrationLabel} has both a skip acknowledgment and migrate.ts; executing migrate.ts.`);
10199
+ else logger.info(`Migration ${migrationLabel} runs without a script (skip acknowledged at ${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason})`);
9884
10200
  }
9885
10201
  pendingMigrations.push({
9886
10202
  number: file.number,
@@ -9984,198 +10300,67 @@ async function executeMigrations(context, migrations) {
9984
10300
  }
9985
10301
  }
9986
10302
  /**
9987
- * Get the machine user name for migration execution
9988
- *
9989
- * Priority:
9990
- * 1. machineUser from migration config (if set)
9991
- * 2. First machine user from auth config
9992
- * @param {object | undefined} migrationConfig - Migration config for namespace
9993
- * @param {string[] | undefined} machineUsers - Machine users from auth config
9994
- * @returns {string | undefined} Machine user name or undefined if none available
9995
- */
9996
- function getMigrationMachineUser(migrationConfig, machineUsers) {
9997
- if (migrationConfig?.machineUser) return migrationConfig.machineUser;
9998
- if (machineUsers && machineUsers.length > 0) return machineUsers[0];
9999
- }
10000
- /**
10001
- * Group migrations by namespace
10002
- * @param {PendingMigration[]} migrations - Migrations to group
10003
- * @returns {Map<string, PendingMigration[]>} Migrations grouped by namespace
10004
- */
10005
- function groupMigrationsByNamespace(migrations) {
10006
- const grouped = /* @__PURE__ */ new Map();
10007
- for (const migration of migrations) {
10008
- const existing = grouped.get(migration.namespace) ?? [];
10009
- existing.push(migration);
10010
- grouped.set(migration.namespace, existing);
10011
- }
10012
- return grouped;
10013
- }
10014
-
10015
- //#endregion
10016
- //#region src/cli/commands/deploy/tailordb/index.ts
10017
- /**
10018
- * Fetch all TailorDB types from remote for a namespace
10019
- * @param {OperatorClient} client - Operator client instance
10020
- * @param {string} workspaceId - Workspace ID
10021
- * @param {string} namespace - TailorDB namespace
10022
- * @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
10023
- */
10024
- async function fetchRemoteTypes(client, workspaceId, namespace) {
10025
- return fetchAllTolerant(async (pageToken, maxPageSize) => {
10026
- const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
10027
- workspaceId,
10028
- namespaceName: namespace,
10029
- pageToken,
10030
- pageSize: maxPageSize
10031
- });
10032
- return [tailordbTypes, nextPageToken];
10033
- });
10034
- }
10035
- async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
10036
- return fetchAllTolerant(async (pageToken, maxPageSize) => {
10037
- const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
10038
- workspaceId,
10039
- namespaceName: namespace,
10040
- pageToken,
10041
- pageSize: maxPageSize
10042
- });
10043
- return [permissions, nextPageToken];
10044
- });
10045
- }
10046
- function definedWhenNotEmpty(value) {
10047
- return Object.keys(value).length > 0 ? value : void 0;
10048
- }
10049
- function namespaceConfig(config, tailorDBInputs, namespace) {
10050
- return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
10051
- }
10052
- function namespaceGqlOperations(config, tailorDBInputs, namespace) {
10053
- return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
10054
- }
10055
- const GQL_OPERATION_KEYS = [
10056
- "create",
10057
- "update",
10058
- "delete",
10059
- "read"
10060
- ];
10061
- function configuredDisabledGqlOperations(operations) {
10062
- if (!operations) return void 0;
10063
- if (operations === "query") return {
10064
- create: false,
10065
- update: false,
10066
- delete: false
10067
- };
10068
- const disabled = {};
10069
- for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
10070
- return definedWhenNotEmpty(disabled);
10071
- }
10072
- function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
10073
- if (!remoteDisabled || !configuredDisabled) return void 0;
10074
- const disabled = {};
10075
- for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
10076
- return definedWhenNotEmpty(disabled);
10077
- }
10078
- function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10079
- const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
10080
- const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
10081
- const types = {};
10082
- for (const [typeName, type] of Object.entries(snapshot.types)) {
10083
- const settings = { ...type.settings };
10084
- const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
10085
- if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
10086
- if (type.settings?.gqlOperations === void 0) {
10087
- const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
10088
- if (disabled) settings.gqlOperations = disabled;
10089
- }
10090
- const comparableType = { ...type };
10091
- const comparableSettings = definedWhenNotEmpty(settings);
10092
- if (comparableSettings) comparableType.settings = comparableSettings;
10093
- else delete comparableType.settings;
10094
- types[typeName] = comparableType;
10095
- }
10096
- return {
10097
- ...snapshot,
10098
- types
10099
- };
10100
- }
10101
- /**
10102
- * Get the current migration number from remote metadata
10103
- * @param {OperatorClient} client - Operator client instance
10104
- * @param {string} workspaceId - Workspace ID
10105
- * @param {string} namespace - TailorDB namespace
10106
- * @returns {Promise<number | null>} Current migration number, or null if no migration label exists
10303
+ * Get the machine user name for migration execution
10304
+ *
10305
+ * Priority:
10306
+ * 1. machineUser from migration config (if set)
10307
+ * 2. First machine user from auth config
10308
+ * @param {object | undefined} migrationConfig - Migration config for namespace
10309
+ * @param {string[] | undefined} machineUsers - Machine users from auth config
10310
+ * @returns {string | undefined} Machine user name or undefined if none available
10107
10311
  */
10108
- async function getRemoteMigrationNumber(client, workspaceId, namespace) {
10109
- try {
10110
- const trn = resourceTrn(workspaceId, "tailordb", namespace);
10111
- const { metadata } = await client.getMetadata({ trn });
10112
- const label = metadata?.labels["sdk-migration"];
10113
- if (!label) return null;
10114
- const match = label.match(/^m(\d+)$/);
10115
- return match ? parseInt(assertDefined(match[1], "migration label capture group missing"), 10) : null;
10116
- } catch {
10117
- return null;
10118
- }
10312
+ function getMigrationMachineUser(migrationConfig, machineUsers) {
10313
+ if (migrationConfig?.machineUser) return migrationConfig.machineUser;
10314
+ if (machineUsers && machineUsers.length > 0) return machineUsers[0];
10119
10315
  }
10120
10316
  /**
10121
- * Verify remote schema matches the expected snapshot state
10122
- * @param {OperatorClient} client - Operator client instance
10123
- * @param {string} workspaceId - Workspace ID
10124
- * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
10125
- * @param {LoadedConfig} config - Loaded application config
10126
- * @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
10127
- * @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
10317
+ * Group migrations by namespace
10318
+ * @param {PendingMigration[]} migrations - Migrations to group
10319
+ * @returns {Map<string, PendingMigration[]>} Migrations grouped by namespace
10128
10320
  */
10129
- async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
10130
- const results = [];
10131
- for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10132
- const remoteMigrationNumber = await getRemoteMigrationNumber(client, workspaceId, namespace);
10133
- if (remoteMigrationNumber === null) {
10134
- results.push({
10135
- namespace,
10136
- remoteMigrationNumber: 0,
10137
- drifts: [],
10138
- hasDrift: false
10139
- });
10140
- continue;
10141
- }
10142
- const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
10143
- if (!expectedSnapshot) {
10144
- results.push({
10145
- namespace,
10146
- remoteMigrationNumber,
10147
- drifts: [],
10148
- hasDrift: false
10149
- });
10150
- continue;
10151
- }
10152
- const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
10153
- const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
10154
- results.push({
10155
- namespace,
10156
- remoteMigrationNumber,
10157
- drifts,
10158
- hasDrift: drifts.length > 0
10159
- });
10321
+ function groupMigrationsByNamespace(migrations) {
10322
+ const grouped = /* @__PURE__ */ new Map();
10323
+ for (const migration of migrations) {
10324
+ const existing = grouped.get(migration.namespace) ?? [];
10325
+ existing.push(migration);
10326
+ grouped.set(migration.namespace, existing);
10160
10327
  }
10161
- return results;
10328
+ return grouped;
10162
10329
  }
10330
+
10331
+ //#endregion
10332
+ //#region src/cli/commands/deploy/tailordb/index.ts
10333
+ const MIGRATION_FILE_KINDS = [
10334
+ "schema",
10335
+ "diff",
10336
+ "migrate",
10337
+ "db"
10338
+ ];
10163
10339
  /**
10164
- * Format remote schema verification results for display
10165
- * @param {RemoteSchemaVerificationResult[]} results - Verification results
10166
- * @returns {string} Formatted results string
10167
- */
10168
- function formatRemoteVerificationResults(results) {
10169
- const lines = [];
10170
- for (const result of results) {
10171
- if (!result.hasDrift) continue;
10172
- lines.push(`Namespace: ${result.namespace}`);
10173
- lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
10174
- lines.push(` Differences:`);
10175
- lines.push(formatSchemaDrifts(result.drifts));
10176
- lines.push("");
10340
+ * Capture the migration files that a deployment plan depends on.
10341
+ * @param namespacesWithMigrations - Configured migration directories by namespace
10342
+ * @returns SHA-256 digest by namespace
10343
+ */
10344
+ function captureMigrationFileState(namespacesWithMigrations) {
10345
+ const state = {};
10346
+ for (const { namespace, migrationsDir } of namespacesWithMigrations.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
10347
+ const hash = createHash("sha256");
10348
+ const migrationNumbers = [...new Set(getMigrationFiles(migrationsDir).map(({ number }) => number))].toSorted((a, b) => a - b);
10349
+ for (const migrationNumber of migrationNumbers) for (const kind of MIGRATION_FILE_KINDS) {
10350
+ const filePath = getMigrationFilePath(migrationsDir, migrationNumber, kind);
10351
+ hash.update(`${formatMigrationNumber(migrationNumber)}/${kind}\0`);
10352
+ if (fs$1.existsSync(filePath)) hash.update(fs$1.readFileSync(filePath));
10353
+ else hash.update("<missing>");
10354
+ hash.update("\0");
10355
+ }
10356
+ state[namespace] = hash.digest("hex");
10177
10357
  }
10178
- return lines.join("\n");
10358
+ return state;
10359
+ }
10360
+ function migrationFileStatesEqual(planned, current) {
10361
+ const plannedNamespaces = Object.keys(planned).toSorted();
10362
+ const currentNamespaces = Object.keys(current).toSorted();
10363
+ return plannedNamespaces.length === currentNamespaces.length && plannedNamespaces.every((namespace, index) => namespace === currentNamespaces[index] && planned[namespace] === current[namespace]);
10179
10364
  }
10180
10365
  /**
10181
10366
  * Validate migration files and detect pending migrations
@@ -10203,38 +10388,38 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
10203
10388
  throw new Error("Schema migration check failed");
10204
10389
  }
10205
10390
  const remoteVerificationResults = await verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs);
10391
+ const missingCheckpointResults = remoteVerificationResults.filter((result) => result.checkpointMissingLocal);
10392
+ if (missingCheckpointResults.length > 0) {
10393
+ logger.error("Remote migration checkpoint is not in the local migration history:");
10394
+ for (const result of missingCheckpointResults) logger.log(` ${result.namespace}: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
10395
+ logger.newline();
10396
+ logger.info("Pull the latest migration files, or run 'tailor-sdk tailordb migration status' to compare.");
10397
+ throw new Error("Remote migration checkpoint verification failed");
10398
+ }
10206
10399
  if (remoteVerificationResults.some((r) => r.hasDrift)) {
10207
10400
  logger.error("Remote schema drift detected:");
10208
10401
  logger.log(formatRemoteVerificationResults(remoteVerificationResults));
10209
10402
  logger.newline();
10210
- logger.info("This may indicate:");
10211
- logger.info(" - Another developer applied different migrations", { mode: "plain" });
10212
- logger.info(" - Manual schema changes were made directly", { mode: "plain" });
10213
- logger.info(" - Migration history is out of sync", { mode: "plain" });
10214
- logger.newline();
10215
- logger.info("To resolve:");
10216
- logger.info(" - Run 'tailor-sdk tailordb migration status' to compare local vs remote.", { mode: "plain" });
10217
- logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
10218
- logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
10219
- logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
10403
+ logRemoteDriftGuidance();
10220
10404
  logger.newline();
10221
10405
  logger.info("Use '--no-schema-check' to skip this check (not recommended).");
10222
10406
  throw new Error("Remote schema verification failed");
10223
10407
  }
10224
10408
  }
10225
- pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations);
10409
+ pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations, config.path);
10226
10410
  if (pendingMigrations.length > 0) {
10227
10411
  logger.newline();
10228
10412
  const withScripts = pendingMigrations.filter((m) => m.hasScript);
10229
10413
  const withoutScripts = pendingMigrations.filter((m) => !m.hasScript);
10230
- logger.info(`Applying ${pendingMigrations.length} migration(s):`);
10414
+ logger.info(`${pendingMigrations.length} pending migration(s) will be applied:`);
10231
10415
  if (withoutScripts.length > 0) logger.info(` • ${withoutScripts.length} schema change(s) (applied automatically with schema deployment)`, { mode: "plain" });
10232
10416
  if (withScripts.length > 0) logger.info(` • ${withScripts.length} data migration(s) (requires migration script execution)`, { mode: "plain" });
10233
10417
  }
10234
10418
  }
10235
10419
  return {
10236
10420
  pendingMigrations,
10237
- namespacesWithMigrations
10421
+ namespacesWithMigrations,
10422
+ migrationFileState: captureMigrationFileState(namespacesWithMigrations)
10238
10423
  };
10239
10424
  }
10240
10425
  /**
@@ -10253,7 +10438,7 @@ async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigra
10253
10438
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10254
10439
  if (getMigrationFiles(migrationsDir).length === 0) continue;
10255
10440
  const targetVersion = getLatestMigrationNumber(migrationsDir);
10256
- const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace);
10441
+ const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace).catch(() => null);
10257
10442
  if (currentVersion === targetVersion) continue;
10258
10443
  await updateMigrationLabel(client, workspaceId, namespace, targetVersion);
10259
10444
  const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
@@ -10282,6 +10467,22 @@ function buildMigrationContextForScripts(client, migrationContext, migrationsReq
10282
10467
  configDir: path.dirname(migrationContext.config.path)
10283
10468
  };
10284
10469
  }
10470
+ async function validateTailorDBMigrationState(client, result) {
10471
+ const { context } = result;
10472
+ const typesByNamespace = /* @__PURE__ */ new Map();
10473
+ for (const tailordb of context.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10474
+ const validation = await validateAndDetectMigrations(client, context.workspaceId, typesByNamespace, context.config, context.noSchemaCheck, context.tailorDBInputs);
10475
+ if (!migrationFileStatesEqual(context.migrationFileState, validation.migrationFileState)) throw new Error("Migration files changed after deployment planning. Run the deployment again to create a fresh plan.");
10476
+ return validation;
10477
+ }
10478
+ /**
10479
+ * Revalidate migration state before the deployment enters any mutation phase.
10480
+ * @param client - Operator client instance
10481
+ * @param result - Planned TailorDB changes
10482
+ */
10483
+ async function preflightTailorDB(client, result) {
10484
+ await validateTailorDBMigrationState(client, result);
10485
+ }
10285
10486
  /**
10286
10487
  * Apply TailorDB-related changes for the given phase.
10287
10488
  * @param client - Operator client instance
@@ -10291,9 +10492,7 @@ function buildMigrationContextForScripts(client, migrationContext, migrationsReq
10291
10492
  async function applyTailorDB(client, result, phase = "create-update") {
10292
10493
  const { changeSet, context: migrationContext } = result;
10293
10494
  if (phase === "create-update") {
10294
- const typesByNamespace = /* @__PURE__ */ new Map();
10295
- for (const tailordb of migrationContext.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10296
- const { pendingMigrations, namespacesWithMigrations } = await validateAndDetectMigrations(client, migrationContext.workspaceId, typesByNamespace, migrationContext.config, migrationContext.noSchemaCheck, migrationContext.tailorDBInputs);
10495
+ const { pendingMigrations, namespacesWithMigrations } = await validateTailorDBMigrationState(client, result);
10297
10496
  if (pendingMigrations.length > 0) {
10298
10497
  processedTypes.reset();
10299
10498
  deletedResources.reset();
@@ -10678,19 +10877,10 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
10678
10877
  }
10679
10878
  }
10680
10879
  /**
10681
- * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
10682
- * @param service - Loaded TailorDB service (after `loadTypes()`)
10683
- * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
10880
+ * Plan TailorDB-related changes based on current and desired state.
10881
+ * @param context - Planning context
10882
+ * @returns Planned changes
10684
10883
  */
10685
- function toTailorDBDeployInput(service) {
10686
- const types = {};
10687
- for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
10688
- return {
10689
- namespace: service.namespace,
10690
- config: service.config,
10691
- types
10692
- };
10693
- }
10694
10884
  async function planTailorDB(context) {
10695
10885
  const { client, workspaceId, application, forRemoval, config, noSchemaCheck, forceApplyAll = false } = context;
10696
10886
  const tailordbs = [];
@@ -10701,6 +10891,12 @@ async function planTailorDB(context) {
10701
10891
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
10702
10892
  const executorUsedTypes = new Set(context.executorUsedTailorDBTypes ?? []);
10703
10893
  for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
10894
+ const typesByNamespace = /* @__PURE__ */ new Map();
10895
+ for (const tailordb of tailordbs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10896
+ const { namespacesWithMigrations, migrationFileState } = forRemoval ? {
10897
+ namespacesWithMigrations: [],
10898
+ migrationFileState: {}
10899
+ } : await validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck ?? false, tailordbs);
10704
10900
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
10705
10901
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
10706
10902
  const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
@@ -10722,7 +10918,9 @@ async function planTailorDB(context) {
10722
10918
  tailorDBInputs: tailordbs,
10723
10919
  executorUsedTypes,
10724
10920
  config,
10725
- noSchemaCheck: noSchemaCheck ?? false
10921
+ noSchemaCheck: noSchemaCheck ?? false,
10922
+ namespacesWithMigrations,
10923
+ migrationFileState
10726
10924
  }
10727
10925
  };
10728
10926
  }
@@ -11070,57 +11268,6 @@ function normalizeComparableGqlPermission(permission) {
11070
11268
  actions: (policy.actions ?? []).toSorted((left, right) => left - right)
11071
11269
  })) };
11072
11270
  }
11073
- /**
11074
- * Check if there are schema differences between migration snapshots and local definitions
11075
- * @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
11076
- * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
11077
- * @returns {Promise<MigrationCheckResult[]>} Results for each namespace
11078
- */
11079
- async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
11080
- const results = [];
11081
- for (const { namespace, migrationsDir } of namespacesWithMigrations) {
11082
- const localTypes = typesByNamespace.get(namespace);
11083
- if (!localTypes) continue;
11084
- const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
11085
- if (!previousSnapshot) {
11086
- results.push({
11087
- namespace,
11088
- migrationsDir,
11089
- hasDiff: true,
11090
- diff: void 0
11091
- });
11092
- continue;
11093
- }
11094
- const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
11095
- results.push({
11096
- namespace,
11097
- migrationsDir,
11098
- hasDiff: hasChanges(diff),
11099
- diff: hasChanges(diff) ? diff : void 0
11100
- });
11101
- }
11102
- return results;
11103
- }
11104
- /**
11105
- * Format migration check results for display
11106
- * @param {MigrationCheckResult[]} results - Migration check results
11107
- * @returns {string} Formatted results string
11108
- */
11109
- function formatMigrationCheckResults(results) {
11110
- const lines = [];
11111
- for (const result of results) {
11112
- if (!result.hasDiff) continue;
11113
- lines.push(`Namespace: ${result.namespace}`);
11114
- if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor-sdk tailordb migration generate' first.");
11115
- else {
11116
- lines.push(` ${formatDiffSummary(result.diff)}`);
11117
- lines.push("");
11118
- lines.push(formatMigrationDiff(result.diff));
11119
- }
11120
- lines.push("");
11121
- }
11122
- return lines.join("\n");
11123
- }
11124
11271
 
11125
11272
  //#endregion
11126
11273
  //#region src/cli/commands/deploy/workflow.ts
@@ -11134,7 +11281,7 @@ function formatMigrationCheckResults(results) {
11134
11281
  async function applyWorkflow(client, result, phase = "create-update") {
11135
11282
  const { changeSet, appName, appId } = result;
11136
11283
  if (phase === "create-update") {
11137
- const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames);
11284
+ const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames, result.jobFunctionPublishEvents);
11138
11285
  await Promise.all([...changeSet.creates.map(async (create) => {
11139
11286
  const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
11140
11287
  const shape = buildWorkflowValidationShape(create.workspaceId, create.workflow);
@@ -11144,7 +11291,8 @@ async function applyWorkflow(client, result, phase = "create-update") {
11144
11291
  mainJobFunctionName: shape.mainJobFunctionName,
11145
11292
  retryPolicy: shape.retryPolicy,
11146
11293
  concurrencyPolicy: shape.concurrencyPolicy,
11147
- jobFunctions: filteredVersions
11294
+ jobFunctions: filteredVersions,
11295
+ publishExecutionEvents: shape.publishExecutionEvents
11148
11296
  });
11149
11297
  await client.setMetadata(create.metaRequest);
11150
11298
  }), ...changeSet.updates.map(async (update) => {
@@ -11156,7 +11304,8 @@ async function applyWorkflow(client, result, phase = "create-update") {
11156
11304
  mainJobFunctionName: shape.mainJobFunctionName,
11157
11305
  retryPolicy: shape.retryPolicy,
11158
11306
  concurrencyPolicy: shape.concurrencyPolicy,
11159
- jobFunctions: filteredVersions
11307
+ jobFunctions: filteredVersions,
11308
+ publishExecutionEvents: shape.publishExecutionEvents
11160
11309
  });
11161
11310
  await client.setMetadata(update.metaRequest);
11162
11311
  })]);
@@ -11217,9 +11366,10 @@ function filterJobFunctionVersions(allVersions, usedJobNames) {
11217
11366
  * @param appName - Application name
11218
11367
  * @param appId - Application ID used for job function metadata when available
11219
11368
  * @param unchangedWorkflowJobNames - Job function names used by unchanged workflows
11369
+ * @param jobFunctionPublishEvents - Resolved `publishExecutionEvents` keyed by job function name
11220
11370
  * @returns Map of job function names to versions
11221
11371
  */
11222
- async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
11372
+ async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set(), jobFunctionPublishEvents = /* @__PURE__ */ new Map()) {
11223
11373
  const jobFunctionVersions = {};
11224
11374
  const firstWorkflow = changeSet.creates[0] || changeSet.updates[0] || changeSet.deletes[0];
11225
11375
  if (!firstWorkflow) return jobFunctionVersions;
@@ -11238,15 +11388,14 @@ async function registerJobFunctions(client, changeSet, appName, appId, unchanged
11238
11388
  const existingJobNamesSet = new Set(existingJobFunctions);
11239
11389
  if (changeSet.creates.length > 0 || changeSet.updates.length > 0) {
11240
11390
  const results = await Promise.all(Array.from(allUsedJobNames).map(async (jobName) => {
11241
- const response = existingJobNamesSet.has(jobName) ? await client.updateWorkflowJobFunction({
11242
- workspaceId,
11243
- jobFunctionName: jobName,
11244
- scriptRef: workflowJobFunctionName(jobName)
11245
- }) : await client.createWorkflowJobFunction({
11391
+ const isExisting = existingJobNamesSet.has(jobName);
11392
+ const request = {
11246
11393
  workspaceId,
11247
11394
  jobFunctionName: jobName,
11248
- scriptRef: workflowJobFunctionName(jobName)
11249
- });
11395
+ scriptRef: workflowJobFunctionName(jobName),
11396
+ publishExecutionEvents: jobFunctionPublishEvents.get(jobName) ?? false
11397
+ };
11398
+ const response = isExisting ? await client.updateWorkflowJobFunction(request) : await client.createWorkflowJobFunction(request);
11250
11399
  await client.setMetadata(await buildMetaRequest({
11251
11400
  trn: resourceTrn(workspaceId, "workflow_job_function", jobName),
11252
11401
  appName,
@@ -11293,9 +11442,64 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11293
11442
  workflowName: workflow.name,
11294
11443
  mainJobFunctionName: workflow.mainJob.name,
11295
11444
  ...workflow.retryPolicy && { retryPolicy: toRetryPolicy(workflow.retryPolicy) },
11296
- ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) }
11445
+ ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) },
11446
+ publishExecutionEvents: workflow.publishEvents ?? false
11297
11447
  };
11298
11448
  }
11449
+ const NO_EVENT_SUBSCRIBERS = { workflowNames: /* @__PURE__ */ new Set() };
11450
+ function isSubscribed(subscribers, workflowName) {
11451
+ return subscribers.workflowNames.has(workflowName);
11452
+ }
11453
+ function resolvePublishEvents(params) {
11454
+ const { explicit, subscribed, remote, conflictError } = params;
11455
+ if (explicit === false && subscribed) throw new Error(conflictError);
11456
+ if (explicit !== void 0) return explicit;
11457
+ return subscribed || (remote ?? false);
11458
+ }
11459
+ /**
11460
+ * Resolve `publishExecutionEvents` for every job function used by a workflow.
11461
+ *
11462
+ * A job execution trigger names a workflow rather than a job, so a subscription
11463
+ * opts in every job that workflow runs.
11464
+ * @param params - Workflows, their job dependencies, subscribers, explicit job flags, and existing job functions
11465
+ * @returns Resolved flags keyed by job function name
11466
+ */
11467
+ function resolveJobPublishEvents(params) {
11468
+ const { workflows, mainJobDeps, subscribers, explicit, existing } = params;
11469
+ const usedJobNames = /* @__PURE__ */ new Set();
11470
+ const subscribedJobNames = /* @__PURE__ */ new Set();
11471
+ for (const workflow of Object.values(workflows)) {
11472
+ const jobNames = mainJobDeps[workflow.mainJob.name];
11473
+ if (!jobNames) continue;
11474
+ const subscribed = isSubscribed(subscribers, workflow.name);
11475
+ for (const jobName of jobNames) {
11476
+ usedJobNames.add(jobName);
11477
+ if (subscribed) subscribedJobNames.add(jobName);
11478
+ }
11479
+ }
11480
+ const resolved = /* @__PURE__ */ new Map();
11481
+ for (const jobName of usedJobNames) resolved.set(jobName, resolvePublishEvents({
11482
+ explicit: explicit.get(jobName),
11483
+ subscribed: subscribedJobNames.has(jobName),
11484
+ remote: existing.get(jobName)?.publishExecutionEvents,
11485
+ conflictError: `Job "${jobName}" has "publishEvents: false", but executors with a workflowJobExecution trigger subscribe to a workflow that runs it. Either remove "publishEvents: false" or remove the matching executor triggers.`
11486
+ }));
11487
+ return resolved;
11488
+ }
11489
+ /**
11490
+ * Collect job functions whose remote publishing flag no longer matches the plan.
11491
+ * @param existing - Existing job functions keyed by name
11492
+ * @param resolved - Resolved `publishExecutionEvents` keyed by job function name
11493
+ * @returns Job function names needing re-registration
11494
+ */
11495
+ function collectStaleJobFunctionNames(existing, resolved) {
11496
+ const stale = /* @__PURE__ */ new Set();
11497
+ for (const [jobName, publishEvents] of resolved) {
11498
+ const remote = existing.get(jobName);
11499
+ if (remote && remote.publishExecutionEvents !== publishEvents) stale.add(jobName);
11500
+ }
11501
+ return stale;
11502
+ }
11299
11503
  /**
11300
11504
  * Plan workflow changes and job functions based on current and desired state.
11301
11505
  * @param client - Operator client instance
@@ -11305,15 +11509,26 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11305
11509
  * @param workflows - Parsed workflows
11306
11510
  * @param mainJobDeps - Main job dependencies by workflow
11307
11511
  * @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan
11512
+ * @param eventPublishing - Executor subscriptions and explicit job flags driving execution event publishing
11308
11513
  * @returns Planned workflow changes
11309
11514
  */
11310
- async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
11515
+ async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set(), eventPublishing = {}) {
11311
11516
  const changeSet = createChangeSet("Workflows");
11312
11517
  const conflicts = [];
11313
11518
  const unmanaged = [];
11314
11519
  const resourceOwners = /* @__PURE__ */ new Set();
11315
11520
  const unchangedWorkflowJobNames = /* @__PURE__ */ new Set();
11316
11521
  const retainedWorkflowJobNames = /* @__PURE__ */ new Set();
11522
+ const executionSubscribers = eventPublishing.execution ?? NO_EVENT_SUBSCRIBERS;
11523
+ const existingJobFunctions = await fetchExistingJobFunctions(client, workspaceId);
11524
+ const jobFunctionPublishEvents = resolveJobPublishEvents({
11525
+ workflows,
11526
+ mainJobDeps,
11527
+ subscribers: eventPublishing.jobExecution ?? NO_EVENT_SUBSCRIBERS,
11528
+ explicit: eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map(),
11529
+ existing: existingJobFunctions
11530
+ });
11531
+ const staleJobFunctionNames = collectStaleJobFunctionNames(existingJobFunctions, jobFunctionPublishEvents);
11317
11532
  const existingWorkflows = await fetchExistingResourcesWithLabels({
11318
11533
  client,
11319
11534
  fetchPage: async (pageToken, pageSize) => {
@@ -11337,6 +11552,15 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11337
11552
  const usedJobNames = mainJobDeps[workflow.mainJob.name];
11338
11553
  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}", ... })`);
11339
11554
  usedJobNames.forEach((jobName) => retainedWorkflowJobNames.add(jobName));
11555
+ const desiredWorkflow = {
11556
+ ...workflow,
11557
+ publishEvents: resolvePublishEvents({
11558
+ explicit: workflow.publishEvents,
11559
+ subscribed: isSubscribed(executionSubscribers, workflow.name),
11560
+ remote: existing?.resource.publishExecutionEvents,
11561
+ conflictError: `Workflow "${workflow.name}" has "publishEvents: false", but executors with a workflowExecution trigger subscribe to it. Either remove "publishEvents: false" or remove the matching executor triggers.`
11562
+ })
11563
+ };
11340
11564
  if (existing) {
11341
11565
  if (trackDesiredResourceOwnership({
11342
11566
  labels: existing.allLabels,
@@ -11347,13 +11571,19 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11347
11571
  resourceName: workflow.name,
11348
11572
  conflicts,
11349
11573
  unmanaged
11350
- }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(existing.resource, workflow, usedJobNames, unchangedJobFunctions)) {
11574
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged({
11575
+ existing: existing.resource,
11576
+ workflow: desiredWorkflow,
11577
+ usedJobNames,
11578
+ unchangedJobFunctions,
11579
+ staleJobFunctionNames
11580
+ })) {
11351
11581
  changeSet.unchanged.push({ name: workflow.name });
11352
11582
  for (const jobName of usedJobNames) unchangedWorkflowJobNames.add(jobName);
11353
11583
  } else changeSet.updates.push({
11354
11584
  name: workflow.name,
11355
11585
  workspaceId,
11356
- workflow,
11586
+ workflow: desiredWorkflow,
11357
11587
  usedJobNames,
11358
11588
  metaRequest
11359
11589
  });
@@ -11361,7 +11591,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11361
11591
  } else changeSet.creates.push({
11362
11592
  name: workflow.name,
11363
11593
  workspaceId,
11364
- workflow,
11594
+ workflow: desiredWorkflow,
11365
11595
  usedJobNames,
11366
11596
  metaRequest
11367
11597
  });
@@ -11391,6 +11621,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11391
11621
  workspaceId,
11392
11622
  appName,
11393
11623
  appId,
11624
+ existingJobFunctionNames: [...existingJobFunctions.keys()],
11394
11625
  retainedWorkflowJobNames
11395
11626
  });
11396
11627
  const deletableJobNames = new Set(jobFunctionDeletes.map((del) => del.jobFunctionName));
@@ -11406,20 +11637,24 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11406
11637
  appName,
11407
11638
  appId,
11408
11639
  unchangedWorkflowJobNames,
11409
- jobFunctionDeletes
11640
+ jobFunctionDeletes,
11641
+ jobFunctionPublishEvents
11410
11642
  };
11411
11643
  }
11412
- async function planWorkflowJobFunctionDeletes(params) {
11413
- const { client, workspaceId, appName, appId, retainedWorkflowJobNames } = params;
11414
- const existingJobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
11644
+ async function fetchExistingJobFunctions(client, workspaceId) {
11645
+ const jobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
11415
11646
  const response = await client.listWorkflowJobFunctions({
11416
11647
  workspaceId,
11417
11648
  pageToken,
11418
11649
  pageSize: maxPageSize
11419
11650
  });
11420
- return [response.jobFunctions.map((jobFunction) => jobFunction.name), response.nextPageToken];
11651
+ return [response.jobFunctions, response.nextPageToken];
11421
11652
  });
11422
- const candidates = [...new Set(existingJobFunctions)].filter((jobName) => !retainedWorkflowJobNames.has(jobName));
11653
+ return new Map(jobFunctions.map((jobFunction) => [jobFunction.name, jobFunction]));
11654
+ }
11655
+ async function planWorkflowJobFunctionDeletes(params) {
11656
+ const { client, workspaceId, appName, appId, existingJobFunctionNames, retainedWorkflowJobNames } = params;
11657
+ const candidates = existingJobFunctionNames.filter((jobName) => !retainedWorkflowJobNames.has(jobName));
11423
11658
  return (await Promise.all(candidates.map(async (jobFunctionName) => {
11424
11659
  const { metadata } = await client.getMetadata({ trn: resourceTrn(workspaceId, "workflow_job_function", jobFunctionName) });
11425
11660
  return isOwnedByApp(metadata?.labels, appName, appId) ? {
@@ -11437,12 +11672,14 @@ async function planWorkflowJobFunctionDeletes(params) {
11437
11672
  function formatWorkflowChangeEntries(changeSet, workflowJobFunctionChanges) {
11438
11673
  return formatChangeEntriesWithFunctionRegistry("workflow", changeSet, workflowJobFunctionChanges, (item) => "usedJobNames" in item ? item.usedJobNames.map((jobName) => workflowJobFunctionName(jobName)) : []);
11439
11674
  }
11440
- function canTreatWorkflowAsUnchanged(existing, workflow, usedJobNames, unchangedJobFunctions) {
11675
+ function canTreatWorkflowAsUnchanged(params) {
11676
+ const { existing, workflow, usedJobNames, unchangedJobFunctions, staleJobFunctionNames } = params;
11441
11677
  if (!usedJobNames.every((jobName) => unchangedJobFunctions.has(jobName))) return false;
11678
+ if (usedJobNames.some((jobName) => staleJobFunctionNames.has(jobName))) return false;
11442
11679
  return areWorkflowsEqual(existing, workflow, usedJobNames);
11443
11680
  }
11444
11681
  function areWorkflowsEqual(existing, workflow, usedJobNames) {
11445
- 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));
11682
+ 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));
11446
11683
  }
11447
11684
  function normalizeComparableExistingWorkflowRetryPolicy(policy) {
11448
11685
  if (!policy) return;
@@ -11699,6 +11936,9 @@ async function applyDeploymentPlans(client, workspaceId, deployments) {
11699
11936
  const forEachDeployment = async (apply) => {
11700
11937
  for (const deployment of deployments) await apply(deployment);
11701
11938
  };
11939
+ await withSpan("apply.preflight", async () => {
11940
+ await forEachDeployment((d) => preflightTailorDB(client, d.tailorDB));
11941
+ });
11702
11942
  await withSpan("apply.createUpdateServices", async () => {
11703
11943
  await forEachDeployment((d) => applySecretManager(client, d.secretManager, "create-update", d.application));
11704
11944
  await forEachDeployment((d) => applyFunctionRegistry(client, workspaceId, d.functionRegistry, "create-update"));
@@ -13137,6 +13377,39 @@ function collectExecutorUsedResolvers(owner, targets) {
13137
13377
  }
13138
13378
  return usedResolvers;
13139
13379
  }
13380
+ /**
13381
+ * Resolve which workflows this target's executors subscribe to, per event
13382
+ * granularity level.
13383
+ *
13384
+ * Scoped to the owning target: unlike TailorDB, Auth and IdP, a workflow has no
13385
+ * `external` declaration, so no supported configuration shares a workflow and a
13386
+ * subscribing executor across configs.
13387
+ * @param target - Deployment target being planned
13388
+ * @returns Subscribers keyed by event granularity level
13389
+ */
13390
+ function collectWorkflowEventSubscribers(target) {
13391
+ const execution = { workflowNames: /* @__PURE__ */ new Set() };
13392
+ const jobExecution = { workflowNames: /* @__PURE__ */ new Set() };
13393
+ for (const executor of Object.values(target.application.executorService?.executors ?? {})) {
13394
+ const { trigger } = executor;
13395
+ if (trigger.kind !== "workflowExecution" && trigger.kind !== "workflowJobExecution") continue;
13396
+ (trigger.kind === "workflowExecution" ? execution : jobExecution).workflowNames.add(trigger.workflowName);
13397
+ }
13398
+ return {
13399
+ execution,
13400
+ jobExecution
13401
+ };
13402
+ }
13403
+ /**
13404
+ * Collect explicit `publishEvents` values declared on this target's workflow jobs.
13405
+ * @param target - Deployment target being planned
13406
+ * @returns Explicit flags keyed by job name
13407
+ */
13408
+ function collectWorkflowJobPublishEvents(target) {
13409
+ const jobPublishEvents = /* @__PURE__ */ new Map();
13410
+ for (const job of target.application.workflowService?.jobs ?? []) if (job.publishEvents !== void 0) jobPublishEvents.set(job.name, job.publishEvents);
13411
+ return jobPublishEvents;
13412
+ }
13140
13413
  function collectExpectedLocalStaticWebsiteNames(targets) {
13141
13414
  const websiteNames = /* @__PURE__ */ new Set();
13142
13415
  for (const target of targets) for (const website of target.application.staticWebsiteServices) websiteNames.add(website.name);
@@ -13738,7 +14011,10 @@ async function planDeploymentTarget(params) {
13738
14011
  withSpan("plan.pipeline", () => planPipeline(ctx)),
13739
14012
  withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
13740
14013
  withSpan("plan.executor", () => planExecutor(ctx)),
13741
- withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
14014
+ withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs, {
14015
+ ...collectWorkflowEventSubscribers(target),
14016
+ jobPublishEvents: collectWorkflowJobPublishEvents(target)
14017
+ })),
13742
14018
  withSpan("plan.workflowExecutionPolicy", () => planWorkflowJobFunctionExecutionPolicy(client, workspaceId, application.name, application.id, config.workflow?.executionPolicies ?? {})),
13743
14019
  withSpan("plan.secretManager", () => planSecretManager(ctx))
13744
14020
  ]);
@@ -14553,6 +14829,12 @@ function formatEventTriggerConfig(config) {
14553
14829
  eventType: config.eventType,
14554
14830
  condition: config.condition?.expr || ""
14555
14831
  };
14832
+ if (typedConfig.case === "workflow") return {
14833
+ kind: typedConfig.case,
14834
+ eventTypes: typedConfig.value.eventTypes,
14835
+ condition: typedConfig.value.condition?.expr || "",
14836
+ ...typedConfig.value.workflowName && { workflowName: typedConfig.value.workflowName }
14837
+ };
14556
14838
  const base = {
14557
14839
  kind: typedConfig.case,
14558
14840
  eventTypes: typedConfig.value.eventTypes,
@@ -18860,8 +19142,13 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
18860
19142
  function generateMigrationScript(diff) {
18861
19143
  const updates = [];
18862
19144
  for (const change of diff.changes) {
18863
- const script = generateChangeScript(change);
18864
- if (script) updates.push(script);
19145
+ const decimalScaleScript = generateDecimalScaleChangeScript(change);
19146
+ updates.push(...generateChangeScripts(change, decimalScaleScript !== null));
19147
+ if (decimalScaleScript) {
19148
+ updates.push(decimalScaleScript);
19149
+ const uniqueConstraintScript = generateUniqueConstraintScript(change);
19150
+ if (uniqueConstraintScript) updates.push(uniqueConstraintScript);
19151
+ }
18865
19152
  }
18866
19153
  if (updates.length === 0) updates.push(` // No data migration needed for this schema change
18867
19154
  // Add custom data transformations if required`);
@@ -18886,18 +19173,19 @@ ${updates.join("\n\n")}
18886
19173
  `;
18887
19174
  }
18888
19175
  /**
18889
- * Generate script for a single change
19176
+ * Generate scripts for a single change
18890
19177
  * @param {DiffChange} change - Diff change to generate script for
18891
- * @returns {string | null} Script content or null if no script needed
19178
+ * @param {boolean} deferUniqueConstraint - Generate the unique check after decimal re-serialization
19179
+ * @returns {string[]} Script contents, or an empty array if no script is needed
18892
19180
  */
18893
- function generateChangeScript(change) {
19181
+ function generateChangeScripts(change, deferUniqueConstraint = false) {
18894
19182
  if (change.kind === "index_added" || change.kind === "index_modified") {
18895
19183
  const before = change.kind === "index_modified" ? change.before : void 0;
18896
- if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) return null;
19184
+ if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) return [];
18897
19185
  const fields = change.after.fields;
18898
19186
  const fieldList = fields.map((f) => `"${f}"`).join(", ");
18899
19187
  const whereClauses = fields.map((f) => `.where("${f}", "=", dup.${f})`).join("\n ");
18900
- return ` // Resolve duplicate (${fields.join(", ")}) combinations before unique index "${change.indexName}" is enforced
19188
+ return [` // Resolve duplicate (${fields.join(", ")}) combinations before unique index "${change.indexName}" is enforced
18901
19189
  {
18902
19190
  const duplicates = await trx
18903
19191
  .selectFrom("${change.typeName}")
@@ -18920,81 +19208,135 @@ function generateChangeScript(change) {
18920
19208
  .execute();
18921
19209
  }
18922
19210
  }
18923
- }`;
19211
+ }`];
18924
19212
  }
18925
19213
  if (change.kind === "field_added") {
18926
- if (change.after.required) return ` // Populate ${change.fieldName} for existing ${change.typeName} records
19214
+ if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.typeName} records
18927
19215
  await trx
18928
19216
  .updateTable("${change.typeName}")
18929
19217
  .set({
18930
19218
  ${change.fieldName}: null, // TODO: Set appropriate default value
18931
19219
  })
18932
- .execute();`;
18933
- return null;
19220
+ .execute();`];
19221
+ return [];
18934
19222
  }
18935
- if (change.kind !== "field_modified") return null;
19223
+ if (change.kind !== "field_modified") return [];
18936
19224
  const { before, after } = change;
18937
- if (!before.required && after.required) return ` // Set ${change.fieldName} for ${change.typeName} records where it is null
19225
+ const scripts = [];
19226
+ if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.typeName} records where it is null
18938
19227
  await trx
18939
19228
  .updateTable("${change.typeName}")
18940
19229
  .set({
18941
19230
  ${change.fieldName}: null, // TODO: Set appropriate default value
18942
19231
  })
18943
19232
  .where("${change.fieldName}", "is", null)
18944
- .execute();`;
18945
- if (!(before.unique ?? false) && (after.unique ?? false)) return ` // Ensure ${change.fieldName} values are unique before adding constraint
18946
- const duplicates = await trx
18947
- .selectFrom("${change.typeName}")
18948
- .select(["${change.fieldName}"])
18949
- .groupBy("${change.fieldName}")
18950
- .having((eb) => eb.fn.count("id"), ">", 1)
18951
- .execute();
18952
- for (const dup of duplicates) {
18953
- // Keep first record, add suffix to others
18954
- const records = await trx
18955
- .selectFrom("${change.typeName}")
18956
- .select(["id", "${change.fieldName}"])
18957
- .where("${change.fieldName}", "=", dup.${change.fieldName})
18958
- .execute();
18959
- for (let i = 1; i < records.length; i++) {
18960
- await trx
18961
- .updateTable("${change.typeName}")
18962
- .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
18963
- .where("id", "=", records[i].id)
18964
- .execute();
18965
- }
18966
- }`;
19233
+ .execute();`);
19234
+ if (!deferUniqueConstraint) {
19235
+ const uniqueConstraintScript = generateUniqueConstraintScript(change);
19236
+ if (uniqueConstraintScript) scripts.push(uniqueConstraintScript);
19237
+ }
18967
19238
  if (before.type === "enum" && after.type === "enum") {
18968
19239
  const beforeValues = (before.allowedValues ?? []).map((v) => v.value);
18969
19240
  const afterValues = (after.allowedValues ?? []).map((v) => v.value);
18970
19241
  const removedValues = beforeValues.filter((v) => !afterValues.includes(v));
18971
19242
  if (removedValues.length > 0) {
18972
19243
  const defaultValue = afterValues[0] ?? "NEW_VALUE";
18973
- return ` // Migrate records with removed enum values: ${removedValues.join(", ")}
19244
+ scripts.push(` // Migrate records with removed enum values: ${removedValues.join(", ")}
18974
19245
  await trx
18975
19246
  .updateTable("${change.typeName}")
18976
19247
  .set({ ${change.fieldName}: "${defaultValue}" }) // TODO: Set appropriate value
18977
19248
  .where("${change.fieldName}", "in", [${removedValues.map((v) => `"${v}"`).join(", ")}])
18978
- .execute();`;
19249
+ .execute();`);
18979
19250
  }
18980
19251
  }
18981
- if (before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType) return ` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
19252
+ if (before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType) scripts.push(` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
18982
19253
  // Find records that don't have a valid reference in the new target table
18983
- const orphanedRecords = await trx
18984
- .selectFrom("${change.typeName}")
18985
- .leftJoin("${after.foreignKeyType}", "${change.typeName}.${change.fieldName}", "${after.foreignKeyType}.id")
18986
- .select(["${change.typeName}.id", "${change.typeName}.${change.fieldName}"])
18987
- .where("${after.foreignKeyType}.id", "is", null)
18988
- .where("${change.typeName}.${change.fieldName}", "is not", null)
18989
- .execute();
18990
- for (const record of orphanedRecords) {
18991
- await trx
18992
- .updateTable("${change.typeName}")
18993
- .set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
18994
- .where("id", "=", record.id)
19254
+ {
19255
+ const orphanedRecords = await trx
19256
+ .selectFrom("${change.typeName}")
19257
+ .leftJoin("${after.foreignKeyType}", "${change.typeName}.${change.fieldName}", "${after.foreignKeyType}.id")
19258
+ .select(["${change.typeName}.id", "${change.typeName}.${change.fieldName}"])
19259
+ .where("${after.foreignKeyType}.id", "is", null)
19260
+ .where("${change.typeName}.${change.fieldName}", "is not", null)
19261
+ .execute();
19262
+ for (const record of orphanedRecords) {
19263
+ await trx
19264
+ .updateTable("${change.typeName}")
19265
+ .set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
19266
+ .where("id", "=", record.id)
19267
+ .execute();
19268
+ }
19269
+ }`);
19270
+ return scripts;
19271
+ }
19272
+ function generateUniqueConstraintScript(change) {
19273
+ if (change.kind !== "field_modified") return null;
19274
+ const { before, after } = change;
19275
+ if ((before.unique ?? false) || !(after.unique ?? false)) return null;
19276
+ return ` // Ensure ${change.fieldName} values are unique before adding constraint
19277
+ {
19278
+ const duplicates = await trx
19279
+ .selectFrom("${change.typeName}")
19280
+ .select(["${change.fieldName}"])
19281
+ .groupBy("${change.fieldName}")
19282
+ .having((eb) => eb.fn.count("id"), ">", 1)
18995
19283
  .execute();
19284
+ for (const dup of duplicates) {
19285
+ // Keep first record, add suffix to others
19286
+ const records = await trx
19287
+ .selectFrom("${change.typeName}")
19288
+ .select(["id", "${change.fieldName}"])
19289
+ .where("${change.fieldName}", "=", dup.${change.fieldName})
19290
+ .execute();
19291
+ for (let i = 1; i < records.length; i++) {
19292
+ await trx
19293
+ .updateTable("${change.typeName}")
19294
+ .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
19295
+ .where("id", "=", records[i].id)
19296
+ .execute();
19297
+ }
19298
+ }
19299
+ }`;
19300
+ }
19301
+ function generateDecimalScaleChangeScript(change) {
19302
+ if (change.kind !== "field_modified") return null;
19303
+ const { before, after } = change;
19304
+ if (before.type !== "decimal" || after.type !== "decimal" || before.scale === after.scale) return null;
19305
+ const valueExpression = !before.required && after.required ? `row.${change.fieldName}!` : `row.${change.fieldName}`;
19306
+ const beforeScale = before.scale ?? 6;
19307
+ const roundingWarning = (after.scale ?? 6) < beforeScale ? `
19308
+ // Values that exceed the new scale may be rounded half-up, so review the
19309
+ // resulting precision before deploying.` : "";
19310
+ return ` // Re-save existing ${change.typeName} rows so ${change.fieldName} is stored under the new scale.
19311
+ // This is a workaround for a platform-side gap where rows written under the
19312
+ // previous scale could fail on later updates until re-saved. Keep it unless
19313
+ // your platform is confirmed to handle stored values across scale changes.${roundingWarning}
19314
+ {
19315
+ let lastId: string | undefined;
19316
+ while (true) {
19317
+ let query = trx
19318
+ .selectFrom("${change.typeName}")
19319
+ .select(["id", "${change.fieldName}"])
19320
+ .where("${change.fieldName}", "is not", null)
19321
+ .orderBy("id", "asc")
19322
+ .limit(100);
19323
+ if (lastId) {
19324
+ query = query.where("id", ">", lastId);
19325
+ }
19326
+ const rows = await query.execute();
19327
+ if (rows.length === 0) break;
19328
+
19329
+ for (const row of rows) {
19330
+ await trx
19331
+ .updateTable("${change.typeName}")
19332
+ .set({ ${change.fieldName}: ${valueExpression} })
19333
+ .where("id", "=", row.id)
19334
+ .where("${change.fieldName}", "=", ${valueExpression})
19335
+ .execute();
19336
+ }
19337
+ lastId = rows[rows.length - 1]!.id;
19338
+ }
18996
19339
  }`;
18997
- return null;
18998
19340
  }
18999
19341
 
19000
19342
  //#endregion
@@ -19063,7 +19405,7 @@ async function generate(options) {
19063
19405
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
19064
19406
  let pluginManager;
19065
19407
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
19066
- const { defineApplication } = await import("./application-CP7JKG9R.mjs");
19408
+ const { defineApplication } = await import("./application-C-zsrNjn.mjs");
19067
19409
  const application = defineApplication({
19068
19410
  config,
19069
19411
  pluginManager
@@ -20277,8 +20619,13 @@ async function bundleQueryScript(engine, baseDir) {
20277
20619
  const entryContent = engine === "sql" ? createSqlEntry() : createGqlEntry();
20278
20620
  fs$1.writeFileSync(entryPath, entryContent);
20279
20621
  const tsconfig = await resolveTSConfigWithFallback(baseDir);
20280
- return (await rolldown.build({
20281
- plugins: [platformBundleDefinePlugin],
20622
+ const bundleLog = createBundleLog({ tsconfig });
20623
+ const result = await rolldown.build({
20624
+ plugins: [
20625
+ createGeneratedEntryResolverPlugin(entryPath, baseDir),
20626
+ createTsconfigPathsPlugin(),
20627
+ platformBundleDefinePlugin
20628
+ ],
20282
20629
  input: entryPath,
20283
20630
  write: false,
20284
20631
  output: {
@@ -20296,8 +20643,10 @@ async function bundleQueryScript(engine, baseDir) {
20296
20643
  annotations: true,
20297
20644
  unknownGlobalSideEffects: false
20298
20645
  },
20299
- logLevel: "silent"
20300
- })).output[0].code;
20646
+ ...bundleLog.options
20647
+ });
20648
+ bundleLog.assertAllResolved();
20649
+ return result.output[0].code;
20301
20650
  }
20302
20651
 
20303
20652
  //#endregion
@@ -21191,5 +21540,5 @@ async function registerTypeScriptRuntime(hookUrl) {
21191
21540
  }
21192
21541
 
21193
21542
  //#endregion
21194
- export { listFolders as $, deploymentArgs as $n, handleOptionalToRequiredError as $t, generate as A, generateUserTypes as An, startCommand as At, remove as B, loadConsoleBaseUrl as Bn, deploy as Bt, waitWorkflowExecution as C, formatDiffSummary as Cn, listCommand$8 as Ct, listWorkflows as D, resourceTrn as Dn, jobsCommand as Dt, listCommand$2 as E, getNamespacesWithMigrations as En, getExecutorWaitFailureMessage as Et, openInConfiguredEditor as F, deleteUserTokens as Fn, getWorkflowExecution as Ft, treeCommand as G, readPlatformConfig as Gn, createWorkspace$1 as Gt, updateCommand$1 as H, loadStoredUserTokens as Hn, listCommand$9 as Ht, show as I, fetchLatestToken as In, listWorkflowExecutions as It, getCommand$1 as J, writePlatformConfig as Jn, ensureConfigId as Jt, listCommand$3 as K, resolveUserTokenKey as Kn, formatKeyValueTable as Kt, showCommand as L, hasAnyUserTokenEntry as Ln, functionExecutionStatusToString as Lt, generateMigrationScript as M, apiCall as Mn, getCommand$5 as Mt, writeDbTypesFile as N, assertWritable as Nn, getWorkflow as Nt, truncate as O, sdkNameLabelKey as On, listExecutorJobs as Ot, getConfiguredEditorCommand as P, loadConfig as Pn, executionsCommand as Pt, listCommand$4 as Q, confirmationArgs as Qn, MIGRATION_LABEL_KEY as Qt, extractOwnedNamespaces as R, hasUserTokenEntry as Rn, getCommand$6 as Rt, waitCommand as S, parseMigrationNumberArg as Sn, triggerExecutor as St, resumeWorkflow as T, hasChanges as Tn, getExecutorJob as Tt, updateOrganization as U, loadWorkspaceId as Un, listWorkspaces as Ut, removeCommand$1 as V, loadMachineUserName as Vn, deployFromCLI as Vt, organizationTree as W, platformConfigFromProfile as Wn, createCommand$1 as Wt, updateCommand$2 as X, commonArgs as Xn, waitForExecution as Xt, getOrganization as Y, defineAppCommand as Yn, executeScript as Yt, updateFolder as Z, configArg as Zn, bundleMigrationScript as Zt, deleteWorkspace as _, getNextMigrationNumber as _n, getCommand$4 as _t, updateUser as a, DIFF_FILE_NAME as an, toPageDirection as ar, createFolder as at, getAppHealth as b, reconstructSnapshotFromMigrations as bn, webhookCommand as bt, listCommand as c, SCHEMA_FILE_NAME as cn, getCommand$3 as ct, inviteUser as d, compareSnapshots as dn, tokenCommand as dt, parseMigrationLabelNumber as en, isVerbose as er, getCommand$2 as et, restoreCommand as f, createSnapshotFromLocalTypes as fn, listCommand$6 as ft, deleteCommand as g, getMigrationFiles as gn, listFunctionRegistries as gt, getWorkspace as h, getMigrationFilePath as hn, listCommand$7 as ht, updateCommand as i, DB_TYPES_FILE_NAME as in, resolveMachineUserInputSource as ir, createCommand as it, generateCommand as j, prompt as jn, startWorkflow as jt, truncateCommand as k, PluginManager as kn, watchExecutorJob as kt, listUsers as l, assertValidMigrationFiles as ln, getOAuth2Client as lt, getCommand as m, getMigrationDirPath as mn, generate$1 as mt, query as n, generateAllTypeManifestsFromSnapshot as nn, pagedLogArgs as nr, deleteCommand$1 as nt, removeCommand as o, INITIAL_SCHEMA_NUMBER as on, workspaceArgs as or, listCommand$5 as ot, restoreWorkspace as p, getLatestMigrationNumber as pn, listMachineUsers as pt, listOrganizations as q, saveUserTokens as qn, workspaceNameSchema as qt, queryCommand as r, protoGqlPermission as rn, paginationArgs as rr, deleteFolder as rt, removeUser as s, MIGRATE_FILE_NAME as sn, listOAuth2Clients as st, registerTypeScriptRuntime as t, compareSnapshotWithRemote as tn, multiConfigArg as tr, getFolder as tt, inviteCommand as u, compareLocalTypesWithSnapshot as un, getMachineUserToken as ut, listApps as v, isValidMigrationNumber as vn, getFunctionRegistry as vt, resumeCommand as w, formatMigrationDiff as wn, listExecutors as wt, healthCommand as x, formatMigrationNumber as xn, triggerCommand as xt, listCommand$1 as y, loadDiff as yn, listWebhookExecutors as yt, logBetaWarning as z, loadAccessToken as zn, getExecutor as zt };
21195
- //# sourceMappingURL=register-typescript-runtime-CCHyKeXO.mjs.map
21543
+ export { listFolders as $, writePlatformConfig as $n, logRemoteDriftGuidance as $t, generate as A, hasChanges as An, startCommand as At, remove as B, deleteUserTokens as Bn, deploy as Bt, waitWorkflowExecution as C, isValidMigrationNumber as Cn, listCommand$8 as Ct, listWorkflows as D, parseMigrationNumberArg as Dn, jobsCommand as Dt, listCommand$2 as E, formatMigrationNumber as En, getExecutorWaitFailureMessage as Et, openInConfiguredEditor as F, generateUserTypes as Fn, getWorkflowExecution as Ft, treeCommand as G, loadConsoleBaseUrl as Gn, createWorkspace$1 as Gt, updateCommand$1 as H, hasAnyUserTokenEntry as Hn, listCommand$9 as Ht, show as I, prompt as In, listWorkflowExecutions as It, getCommand$1 as J, loadWorkspaceId as Jn, ensureConfigId as Jt, listCommand$3 as K, loadMachineUserName as Kn, formatKeyValueTable as Kt, showCommand as L, apiCall as Ln, functionExecutionStatusToString as Lt, generateMigrationScript as M, resourceTrn as Mn, getCommand$5 as Mt, writeDbTypesFile as N, sdkNameLabelKey as Nn, getWorkflow as Nt, truncate as O, formatDiffSummary as On, listExecutorJobs as Ot, getConfiguredEditorCommand as P, PluginManager as Pn, executionsCommand as Pt, listCommand$4 as Q, saveUserTokens as Qn, checkMigrationDiffs as Qt, extractOwnedNamespaces as R, assertWritable as Rn, getCommand$6 as Rt, waitCommand as S, getNextMigrationNumber as Sn, triggerExecutor as St, resumeWorkflow as T, reconstructSnapshotFromMigrations as Tn, getExecutorJob as Tt, updateOrganization as U, hasUserTokenEntry as Un, listWorkspaces as Ut, removeCommand$1 as V, fetchLatestToken as Vn, deployFromCLI as Vt, organizationTree as W, loadAccessToken as Wn, createCommand$1 as Wt, updateCommand$2 as X, readPlatformConfig as Xn, waitForExecution as Xt, getOrganization as Y, platformConfigFromProfile as Yn, executeScript as Yt, updateFolder as Z, resolveUserTokenKey as Zn, bundleMigrationScript as Zt, deleteWorkspace as _, formatSchemaDrifts as _n, getCommand$4 as _t, updateUser as a, compareSnapshotWithRemote as an, isVerbose as ar, createFolder as at, getAppHealth as b, getMigrationFilePath as bn, webhookCommand as bt, listCommand as c, DB_TYPES_FILE_NAME as cn, paginationArgs as cr, getCommand$3 as ct, inviteUser as d, MIGRATE_FILE_NAME as dn, workspaceArgs as dr, tokenCommand as dt, toTailorDBDeployInput as en, defineAppCommand as er, getCommand$2 as et, restoreCommand as f, SCHEMA_FILE_NAME as fn, listCommand$6 as ft, deleteCommand as g, createSnapshotFromLocalTypes as gn, listFunctionRegistries as gt, getWorkspace as h, compareSnapshots as hn, listCommand$7 as ht, updateCommand as i, parseMigrationLabelNumber as in, deploymentArgs as ir, createCommand as it, generateCommand as j, getNamespacesWithMigrations as jn, startWorkflow as jt, truncateCommand as k, formatMigrationDiff as kn, watchExecutorJob as kt, listUsers as l, DIFF_FILE_NAME as ln, resolveMachineUserInputSource as lr, getOAuth2Client as lt, getCommand as m, compareLocalTypesWithSnapshot as mn, generate$1 as mt, query as n, MIGRATION_LABEL_KEY as nn, configArg as nr, deleteCommand$1 as nt, removeCommand as o, generateAllTypeManifestsFromSnapshot as on, multiConfigArg as or, listCommand$5 as ot, restoreWorkspace as p, assertValidMigrationFiles as pn, listMachineUsers as pt, listOrganizations as q, loadStoredUserTokens as qn, workspaceNameSchema as qt, queryCommand as r, handleOptionalToRequiredError as rn, confirmationArgs as rr, deleteFolder as rt, removeUser as s, protoGqlPermission as sn, pagedLogArgs as sr, listOAuth2Clients as st, registerTypeScriptRuntime as t, verifyRemoteSchema as tn, commonArgs as tr, getFolder as tt, inviteCommand as u, INITIAL_SCHEMA_NUMBER as un, toPageDirection as ur, getMachineUserToken as ut, listApps as v, getLatestMigrationNumber as vn, getFunctionRegistry as vt, resumeCommand as w, loadDiff as wn, listExecutors as wt, healthCommand as x, getMigrationFiles as xn, triggerCommand as xt, listCommand$1 as y, getMigrationDirPath as yn, listWebhookExecutors as yt, logBetaWarning as z, loadConfig as zn, getExecutor as zt };
21544
+ //# sourceMappingURL=register-typescript-runtime-nG-n9lPw.mjs.map