@tailor-platform/sdk 1.82.0 → 1.84.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 (82) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/application-5FOWIxFQ.mjs +3 -0
  3. package/dist/{application-CVeA53ok.mjs → application-CnCljDwk.mjs} +15 -10
  4. package/dist/application-CnCljDwk.mjs.map +1 -0
  5. package/dist/cli/commands/deploy/tailordb/index.d.mts +1 -0
  6. package/dist/cli/commands/generate/service.d.mts +1 -1
  7. package/dist/cli/commands/generate/types.d.mts +1 -1
  8. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +11 -1
  9. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +1 -1
  10. package/dist/cli/index.mjs +65 -10
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/lib.d.mts +1 -1
  13. package/dist/cli/lib.mjs +2 -2
  14. package/dist/cli/shared/client.d.mts +28 -3
  15. package/dist/cli/skills.d.mts +1 -1
  16. package/dist/completion/zsh-worker.zsh +35 -22
  17. package/dist/configure/index.d.mts +2 -2
  18. package/dist/configure/index.mjs +173 -5
  19. package/dist/configure/index.mjs.map +1 -1
  20. package/dist/configure/services/executor/index.d.mts +2 -2
  21. package/dist/configure/services/executor/trigger/event.d.mts +245 -2
  22. package/dist/configure/services/executor/trigger/index.d.mts +3 -3
  23. package/dist/configure/services/index.d.mts +2 -2
  24. package/dist/configure/services/tailordb/index.d.mts +0 -1
  25. package/dist/configure/services/workflow/execution-policy.d.mts +2 -2
  26. package/dist/configure/services/workflow/job.d.mts +9 -0
  27. package/dist/configure/services/workflow/workflow.d.mts +10 -0
  28. package/dist/{globals-DUqEetIh.mjs → globals-DraXTdqg.mjs} +6 -5
  29. package/dist/globals-DraXTdqg.mjs.map +1 -0
  30. package/dist/{register-typescript-runtime-O7zQNif0.mjs → register-typescript-runtime-BCvWR3HC.mjs} +518 -123
  31. package/dist/register-typescript-runtime-BCvWR3HC.mjs.map +1 -0
  32. package/dist/{registry-CXi7rJq7.mjs → registry-Ct0Wgxp1.mjs} +3 -3
  33. package/dist/registry-Ct0Wgxp1.mjs.map +1 -0
  34. package/dist/runtime/index.mjs +1 -1
  35. package/dist/runtime/workflow.d.mts +39 -21
  36. package/dist/runtime/workflow.mjs +2 -2
  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/utils/test/index.mjs +18 -12
  52. package/dist/utils/test/index.mjs.map +1 -1
  53. package/dist/utils/test/mock.d.mts +2 -2
  54. package/dist/vitest/environment.mjs +1 -1
  55. package/dist/vitest/index.mjs +30 -24
  56. package/dist/vitest/index.mjs.map +1 -1
  57. package/dist/vitest/mocks/file.d.mts +1 -1
  58. package/dist/vitest/mocks/workflow.d.mts +20 -15
  59. package/dist/vitest/setup.mjs +1 -1
  60. package/dist/{workflow-CkIDJpdg.mjs → workflow-B2mwc7aT.mjs} +17 -9
  61. package/dist/workflow-B2mwc7aT.mjs.map +1 -0
  62. package/dist/{workspace_resource_pb-UGK1SSn_.mjs → workspace_resource_pb-DXnO2AT-.mjs} +7 -7
  63. package/dist/workspace_resource_pb-DXnO2AT-.mjs.map +1 -0
  64. package/docs/cli/tailordb.md +14 -12
  65. package/docs/cli-reference.md +15 -15
  66. package/docs/runtime.md +1 -1
  67. package/docs/services/executor.md +83 -3
  68. package/docs/services/idp.md +3 -3
  69. package/docs/services/tailordb-migration.md +18 -5
  70. package/docs/services/workflow.md +28 -2
  71. package/docs/testing.md +1 -1
  72. package/package.json +16 -16
  73. package/dist/application-CP0W2sQo.mjs +0 -3
  74. package/dist/application-CVeA53ok.mjs.map +0 -1
  75. package/dist/globals-DUqEetIh.mjs.map +0 -1
  76. package/dist/register-typescript-runtime-O7zQNif0.mjs.map +0 -1
  77. package/dist/registry-CXi7rJq7.mjs.map +0 -1
  78. package/dist/service-B0MaodSg.mjs.map +0 -1
  79. package/dist/service-KnAiVofD.mjs +0 -3
  80. package/dist/service_pb-BwZRiNug.mjs.map +0 -1
  81. package/dist/workflow-CkIDJpdg.mjs.map +0 -1
  82. package/dist/workspace_resource_pb-UGK1SSn_.mjs.map +0 -1
@@ -1,9 +1,9 @@
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-CVeA53ok.mjs";
6
- import { o as loadFilesWithIgnores, s as functionSchema, t as createExecutorService } from "./service-B0MaodSg.mjs";
5
+ import { A as defaultPlatformBaseUrl, B as getPlatformBaseUrl, C as getPluginGenerationDependencies, D as hashContent, E as getDistDir, G as normalizeBaseUrl, H as initOperatorClient, J as byName, K as rememberPlatformConfigForToken, L as getConsoleBaseUrl, M as fetchAllTolerant, N as fetchMachineUserToken, O as hashFile, P as fetchPaged, R as getOAuth2ClientId, T as createBundleCache, U as isDefaultPlatform, V as initOAuth2Client, W as isNotFoundError, X as LOG_LEVELS, Y as createApplyLimiter, _ as stringifyFunction, 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 resolveStaticWebsiteUrls, r as loadApplication, t as defineApplication, v as resolveTSConfigWithFallback, w as hasGenerationHooks, y as platformBundleDefinePlugin, z as getOrNull } from "./application-CnCljDwk.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";
@@ -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({
@@ -7964,6 +7987,29 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
7964
7987
  }
7965
7988
  }
7966
7989
  /**
7990
+ * Determine if a type-level index change is breaking. Mirrors the field-level
7991
+ * unique reasoning: enforcing a unique constraint over existing rows can fail
7992
+ * on duplicates, so both adding a unique index and re-pointing an existing
7993
+ * unique index at a different field set require a data migration.
7994
+ * @param {string} typeName - Name of the type containing the index
7995
+ * @param {string} indexName - Name of the index being changed
7996
+ * @param {SnapshotIndexConfig | undefined} oldIndex - Old index configuration
7997
+ * @param {SnapshotIndexConfig | undefined} newIndex - New index configuration
7998
+ * @returns {BreakingChangeInfo | null} Breaking change info or null if not breaking
7999
+ */
8000
+ function isBreakingIndexChange(typeName, indexName, oldIndex, newIndex) {
8001
+ if (!newIndex || !(newIndex.unique ?? false)) return null;
8002
+ if (!oldIndex || !(oldIndex.unique ?? false)) return {
8003
+ typeName,
8004
+ reason: `Unique constraint added to index "${indexName}"`
8005
+ };
8006
+ if (JSON.stringify(oldIndex.fields.toSorted()) !== JSON.stringify(newIndex.fields.toSorted())) return {
8007
+ typeName,
8008
+ reason: `Unique index fields changed on index "${indexName}"`
8009
+ };
8010
+ return null;
8011
+ }
8012
+ /**
7967
8013
  * Compare type-level indexes
7968
8014
  * @param {DiffContext} ctx - Diff context
7969
8015
  * @param {string} typeName - Type name
@@ -7974,12 +8020,16 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
7974
8020
  function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
7975
8021
  const oldKeys = new Set(Object.keys(oldIndexes || {}));
7976
8022
  const newKeys = new Set(Object.keys(newIndexes || {}));
7977
- for (const [indexName, indexConfig] of Object.entries(newIndexes ?? {})) if (!oldKeys.has(indexName)) ctx.changes.push({
7978
- kind: "index_added",
7979
- typeName,
7980
- indexName,
7981
- after: indexConfig
7982
- });
8023
+ for (const [indexName, indexConfig] of Object.entries(newIndexes ?? {})) if (!oldKeys.has(indexName)) {
8024
+ ctx.changes.push({
8025
+ kind: "index_added",
8026
+ typeName,
8027
+ indexName,
8028
+ after: indexConfig
8029
+ });
8030
+ const breaking = isBreakingIndexChange(typeName, indexName, void 0, indexConfig);
8031
+ if (breaking) ctx.breakingChanges.push(breaking);
8032
+ }
7983
8033
  for (const [indexName, indexConfig] of Object.entries(oldIndexes ?? {})) if (!newKeys.has(indexName)) ctx.changes.push({
7984
8034
  kind: "index_removed",
7985
8035
  typeName,
@@ -8002,6 +8052,8 @@ function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
8002
8052
  before: oldIndex,
8003
8053
  after: newIndex
8004
8054
  });
8055
+ const breaking = isBreakingIndexChange(typeName, indexName, oldIndex, newIndex);
8056
+ if (breaking) ctx.breakingChanges.push(breaking);
8005
8057
  }
8006
8058
  }
8007
8059
  }
@@ -9326,6 +9378,12 @@ function protoGqlOperand(operand) {
9326
9378
  * - `field_modified` optional→required, unique constraint added, enum
9327
9379
  * value removed: keep the looser side until Post-phase.
9328
9380
  *
9381
+ * and the type-level index adjustments:
9382
+ *
9383
+ * - `index_added` with `unique: true`: withhold the index until Post-phase.
9384
+ * - `index_modified` that gains a unique constraint or re-points a unique
9385
+ * index at different fields: keep the previous definition until Post-phase.
9386
+ *
9329
9387
  * Type-level deletions (`type_removed`) are handled by the deploy flow,
9330
9388
  * which retains the type until Post-phase rather than via this module.
9331
9389
  *
@@ -9407,6 +9465,45 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
9407
9465
  }
9408
9466
  }
9409
9467
  }
9468
+ /**
9469
+ * Build a map of type-level index changes that require pre-migration schema
9470
+ * adjustment (the breaking ones — see {@link isBreakingIndexChange}).
9471
+ * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
9472
+ * @returns {PreMigrationIndexChangesMap} Map of changes keyed by typeName/indexName
9473
+ */
9474
+ function buildPreMigrationIndexChangesMap(pendingMigrations) {
9475
+ const map = /* @__PURE__ */ new Map();
9476
+ for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
9477
+ if (change.kind !== "index_added" && change.kind !== "index_modified") continue;
9478
+ const before = change.kind === "index_modified" ? change.before : void 0;
9479
+ if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) continue;
9480
+ const perType = map.get(change.typeName) ?? /* @__PURE__ */ new Map();
9481
+ perType.set(change.indexName, change);
9482
+ map.set(change.typeName, perType);
9483
+ }
9484
+ return map;
9485
+ }
9486
+ /**
9487
+ * Apply pre-migration schema adjustments to a type's index map in place.
9488
+ *
9489
+ * The indexes map is the proto-shape `TailorDBType.schema.indexes` that will
9490
+ * be sent in the Pre-phase. We mutate it so that:
9491
+ *
9492
+ * - Newly added unique indexes are withheld until Post-phase.
9493
+ * - Modified indexes keep their previous definition until Post-phase.
9494
+ *
9495
+ * @param {Record<string, MessageInitShape<typeof TailorDBType_IndexSchema>>} indexes - Index map to adjust (mutated in place)
9496
+ * @param {Map<string, IndexDiffChange>} typeIndexChanges - Changes for this type, keyed by indexName
9497
+ */
9498
+ function applyPreMigrationIndexAdjustments(indexes, typeIndexChanges) {
9499
+ for (const [indexName, change] of typeIndexChanges) {
9500
+ if (change.kind === "index_added") {
9501
+ delete indexes[indexName];
9502
+ continue;
9503
+ }
9504
+ if (change.kind === "index_modified") indexes[indexName] = convertIndexToProto(change.before);
9505
+ }
9506
+ }
9410
9507
 
9411
9508
  //#endregion
9412
9509
  //#region src/cli/commands/tailordb/migrate/types.ts
@@ -9781,8 +9878,9 @@ async function getCurrentMigrationNumber(client, workspaceId, namespace) {
9781
9878
  const label = metadata?.labels[MIGRATION_LABEL_KEY];
9782
9879
  if (!label) return 0;
9783
9880
  return parseMigrationLabelNumber(label) ?? 0;
9784
- } catch {
9785
- return 0;
9881
+ } catch (error) {
9882
+ if (isNotFoundError(error)) return 0;
9883
+ throw error;
9786
9884
  }
9787
9885
  }
9788
9886
  /**
@@ -9790,9 +9888,10 @@ async function getCurrentMigrationNumber(client, workspaceId, namespace) {
9790
9888
  * @param {OperatorClient} client - Operator client instance
9791
9889
  * @param {string} workspaceId - Workspace ID
9792
9890
  * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
9891
+ * @param {string} [configPath] - Config file path, included in remediation guidance when provided
9793
9892
  * @returns {Promise<PendingMigration[]>} List of pending migrations
9794
9893
  */
9795
- async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations) {
9894
+ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations, configPath) {
9796
9895
  const pendingMigrations = [];
9797
9896
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
9798
9897
  const currentMigration = await getCurrentMigrationNumber(client, workspaceId, namespace);
@@ -9804,9 +9903,15 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
9804
9903
  const diff = loadDiff(diffPath);
9805
9904
  const scriptPath = getMigrationFilePath(migrationsDir, file.number, "migrate");
9806
9905
  const hasScript = fs$1.existsSync(scriptPath);
9807
- if (diff.requiresMigrationScript && !hasScript) {
9808
- logger.warn(`Migration ${namespace}/${file.number} requires a script but migrate.ts not found`);
9809
- continue;
9906
+ if (diff.requiresMigrationScript && !hasScript && !diff.scriptSkipped) {
9907
+ const configArg = configPath ? ` --config "${path.relative(process.cwd(), configPath) || configPath}"` : "";
9908
+ const scriptCmd = `tailor-sdk tailordb migration script ${file.number} --namespace ${namespace}${configArg}`;
9909
+ 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>"`);
9910
+ }
9911
+ if (diff.scriptSkipped) {
9912
+ const migrationLabel = `${namespace}/${formatMigrationNumber(file.number)}`;
9913
+ if (hasScript) logger.warn(`Migration ${migrationLabel} has both a skip acknowledgment and migrate.ts; executing migrate.ts.`);
9914
+ else logger.info(`Migration ${migrationLabel} runs without a script (skip acknowledged at ${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason})`);
9810
9915
  }
9811
9916
  pendingMigrations.push({
9812
9917
  number: file.number,
@@ -10103,6 +10208,38 @@ function formatRemoteVerificationResults(results) {
10103
10208
  }
10104
10209
  return lines.join("\n");
10105
10210
  }
10211
+ const MIGRATION_FILE_KINDS = [
10212
+ "schema",
10213
+ "diff",
10214
+ "migrate",
10215
+ "db"
10216
+ ];
10217
+ /**
10218
+ * Capture the migration files that a deployment plan depends on.
10219
+ * @param namespacesWithMigrations - Configured migration directories by namespace
10220
+ * @returns SHA-256 digest by namespace
10221
+ */
10222
+ function captureMigrationFileState(namespacesWithMigrations) {
10223
+ const state = {};
10224
+ for (const { namespace, migrationsDir } of namespacesWithMigrations.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
10225
+ const hash = createHash("sha256");
10226
+ const migrationNumbers = [...new Set(getMigrationFiles(migrationsDir).map(({ number }) => number))].toSorted((a, b) => a - b);
10227
+ for (const migrationNumber of migrationNumbers) for (const kind of MIGRATION_FILE_KINDS) {
10228
+ const filePath = getMigrationFilePath(migrationsDir, migrationNumber, kind);
10229
+ hash.update(`${formatMigrationNumber(migrationNumber)}/${kind}\0`);
10230
+ if (fs$1.existsSync(filePath)) hash.update(fs$1.readFileSync(filePath));
10231
+ else hash.update("<missing>");
10232
+ hash.update("\0");
10233
+ }
10234
+ state[namespace] = hash.digest("hex");
10235
+ }
10236
+ return state;
10237
+ }
10238
+ function migrationFileStatesEqual(planned, current) {
10239
+ const plannedNamespaces = Object.keys(planned).toSorted();
10240
+ const currentNamespaces = Object.keys(current).toSorted();
10241
+ return plannedNamespaces.length === currentNamespaces.length && plannedNamespaces.every((namespace, index) => namespace === currentNamespaces[index] && planned[namespace] === current[namespace]);
10242
+ }
10106
10243
  /**
10107
10244
  * Validate migration files and detect pending migrations
10108
10245
  * @param {OperatorClient} client - Operator client instance
@@ -10148,19 +10285,20 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
10148
10285
  throw new Error("Remote schema verification failed");
10149
10286
  }
10150
10287
  }
10151
- pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations);
10288
+ pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations, config.path);
10152
10289
  if (pendingMigrations.length > 0) {
10153
10290
  logger.newline();
10154
10291
  const withScripts = pendingMigrations.filter((m) => m.hasScript);
10155
10292
  const withoutScripts = pendingMigrations.filter((m) => !m.hasScript);
10156
- logger.info(`Applying ${pendingMigrations.length} migration(s):`);
10293
+ logger.info(`${pendingMigrations.length} pending migration(s) will be applied:`);
10157
10294
  if (withoutScripts.length > 0) logger.info(` • ${withoutScripts.length} schema change(s) (applied automatically with schema deployment)`, { mode: "plain" });
10158
10295
  if (withScripts.length > 0) logger.info(` • ${withScripts.length} data migration(s) (requires migration script execution)`, { mode: "plain" });
10159
10296
  }
10160
10297
  }
10161
10298
  return {
10162
10299
  pendingMigrations,
10163
- namespacesWithMigrations
10300
+ namespacesWithMigrations,
10301
+ migrationFileState: captureMigrationFileState(namespacesWithMigrations)
10164
10302
  };
10165
10303
  }
10166
10304
  /**
@@ -10208,6 +10346,22 @@ function buildMigrationContextForScripts(client, migrationContext, migrationsReq
10208
10346
  configDir: path.dirname(migrationContext.config.path)
10209
10347
  };
10210
10348
  }
10349
+ async function validateTailorDBMigrationState(client, result) {
10350
+ const { context } = result;
10351
+ const typesByNamespace = /* @__PURE__ */ new Map();
10352
+ for (const tailordb of context.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10353
+ const validation = await validateAndDetectMigrations(client, context.workspaceId, typesByNamespace, context.config, context.noSchemaCheck, context.tailorDBInputs);
10354
+ if (!migrationFileStatesEqual(context.migrationFileState, validation.migrationFileState)) throw new Error("Migration files changed after deployment planning. Run the deployment again to create a fresh plan.");
10355
+ return validation;
10356
+ }
10357
+ /**
10358
+ * Revalidate migration state before the deployment enters any mutation phase.
10359
+ * @param client - Operator client instance
10360
+ * @param result - Planned TailorDB changes
10361
+ */
10362
+ async function preflightTailorDB(client, result) {
10363
+ await validateTailorDBMigrationState(client, result);
10364
+ }
10211
10365
  /**
10212
10366
  * Apply TailorDB-related changes for the given phase.
10213
10367
  * @param client - Operator client instance
@@ -10217,9 +10371,7 @@ function buildMigrationContextForScripts(client, migrationContext, migrationsReq
10217
10371
  async function applyTailorDB(client, result, phase = "create-update") {
10218
10372
  const { changeSet, context: migrationContext } = result;
10219
10373
  if (phase === "create-update") {
10220
- const typesByNamespace = /* @__PURE__ */ new Map();
10221
- for (const tailordb of migrationContext.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10222
- const { pendingMigrations, namespacesWithMigrations } = await validateAndDetectMigrations(client, migrationContext.workspaceId, typesByNamespace, migrationContext.config, migrationContext.noSchemaCheck, migrationContext.tailorDBInputs);
10374
+ const { pendingMigrations, namespacesWithMigrations } = await validateTailorDBMigrationState(client, result);
10223
10375
  if (pendingMigrations.length > 0) {
10224
10376
  processedTypes.reset();
10225
10377
  deletedResources.reset();
@@ -10401,6 +10553,7 @@ async function awaitAllSettledOrThrow(promises) {
10401
10553
  */
10402
10554
  async function executeSingleMigrationPrePhase(client, changeSet, migration, tailorDBInputs, executorUsedTypes) {
10403
10555
  const preMigrationChanges = buildPreMigrationChangesMap([migration]);
10556
+ const preMigrationIndexChanges = buildPreMigrationIndexChangesMap([migration]);
10404
10557
  const affectedTypes = getAffectedTypeNames(migration);
10405
10558
  const createdBeforeMigration = new Set(processedTypes.created);
10406
10559
  for (const create of changeSet.type.creates) {
@@ -10412,6 +10565,8 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
10412
10565
  clonedRequest.tailordbType = snapshotType;
10413
10566
  const typeChanges = preMigrationChanges.get(typeName);
10414
10567
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
10568
+ const indexChanges = preMigrationIndexChanges.get(typeName);
10569
+ if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
10415
10570
  processedTypes.created.add(typeName);
10416
10571
  await client.createTailorDBType(clonedRequest);
10417
10572
  }
@@ -10423,6 +10578,8 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
10423
10578
  const clonedTypeRequest = structuredClone(snapshotType);
10424
10579
  const typeChanges = preMigrationChanges.get(typeName);
10425
10580
  if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges);
10581
+ const indexChanges = preMigrationIndexChanges.get(typeName);
10582
+ if (indexChanges && indexChanges.size > 0 && clonedTypeRequest.schema?.indexes) applyPreMigrationIndexAdjustments(clonedTypeRequest.schema.indexes, indexChanges);
10426
10583
  processedTypes.updated.add(typeName);
10427
10584
  await client.updateTailorDBType({
10428
10585
  workspaceId: create.request.workspaceId,
@@ -10439,6 +10596,8 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
10439
10596
  clonedRequest.tailordbType = snapshotType;
10440
10597
  const typeChanges = preMigrationChanges.get(typeName);
10441
10598
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
10599
+ const indexChanges = preMigrationIndexChanges.get(typeName);
10600
+ if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
10442
10601
  processedTypes.updated.add(typeName);
10443
10602
  await client.updateTailorDBType(clonedRequest);
10444
10603
  }
@@ -10481,12 +10640,14 @@ const deletedResources = {
10481
10640
  */
10482
10641
  async function executeSingleMigrationPostPhase(client, changeSet, migration, tailorDBInputs, executorUsedTypes) {
10483
10642
  const preMigrationChanges = buildPreMigrationChangesMap([migration]);
10643
+ const preMigrationIndexChanges = buildPreMigrationIndexChangesMap([migration]);
10644
+ const adjustedTypes = /* @__PURE__ */ new Set([...preMigrationChanges.keys(), ...preMigrationIndexChanges.keys()]);
10484
10645
  const affectedTypes = getAffectedTypeNames(migration);
10485
10646
  const deletedTypeNames = getDeletedTypeNames(migration);
10486
10647
  try {
10487
10648
  for (const create of changeSet.type.creates) {
10488
10649
  const typeName = create.request.tailordbType?.name;
10489
- if (!typeName || !affectedTypes.has(typeName) || !preMigrationChanges.has(typeName)) continue;
10650
+ if (!typeName || !affectedTypes.has(typeName) || !adjustedTypes.has(typeName)) continue;
10490
10651
  const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
10491
10652
  if (!snapshotType) continue;
10492
10653
  await client.updateTailorDBType({
@@ -10497,7 +10658,7 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
10497
10658
  }
10498
10659
  for (const update of changeSet.type.updates) {
10499
10660
  const typeName = update.request.tailordbType?.name;
10500
- if (!typeName || !affectedTypes.has(typeName) || !preMigrationChanges.has(typeName)) continue;
10661
+ if (!typeName || !affectedTypes.has(typeName) || !adjustedTypes.has(typeName)) continue;
10501
10662
  const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
10502
10663
  if (!snapshotType) continue;
10503
10664
  await client.updateTailorDBType({
@@ -10618,6 +10779,12 @@ async function planTailorDB(context) {
10618
10779
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
10619
10780
  const executorUsedTypes = new Set(context.executorUsedTailorDBTypes ?? []);
10620
10781
  for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
10782
+ const typesByNamespace = /* @__PURE__ */ new Map();
10783
+ for (const tailordb of tailordbs) typesByNamespace.set(tailordb.namespace, tailordb.types);
10784
+ const { namespacesWithMigrations, migrationFileState } = forRemoval ? {
10785
+ namespacesWithMigrations: [],
10786
+ migrationFileState: {}
10787
+ } : await validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck ?? false, tailordbs);
10621
10788
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
10622
10789
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
10623
10790
  const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
@@ -10639,7 +10806,9 @@ async function planTailorDB(context) {
10639
10806
  tailorDBInputs: tailordbs,
10640
10807
  executorUsedTypes,
10641
10808
  config,
10642
- noSchemaCheck: noSchemaCheck ?? false
10809
+ noSchemaCheck: noSchemaCheck ?? false,
10810
+ namespacesWithMigrations,
10811
+ migrationFileState
10643
10812
  }
10644
10813
  };
10645
10814
  }
@@ -11051,7 +11220,7 @@ function formatMigrationCheckResults(results) {
11051
11220
  async function applyWorkflow(client, result, phase = "create-update") {
11052
11221
  const { changeSet, appName, appId } = result;
11053
11222
  if (phase === "create-update") {
11054
- const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames);
11223
+ const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames, result.jobFunctionPublishEvents);
11055
11224
  await Promise.all([...changeSet.creates.map(async (create) => {
11056
11225
  const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
11057
11226
  const shape = buildWorkflowValidationShape(create.workspaceId, create.workflow);
@@ -11061,7 +11230,8 @@ async function applyWorkflow(client, result, phase = "create-update") {
11061
11230
  mainJobFunctionName: shape.mainJobFunctionName,
11062
11231
  retryPolicy: shape.retryPolicy,
11063
11232
  concurrencyPolicy: shape.concurrencyPolicy,
11064
- jobFunctions: filteredVersions
11233
+ jobFunctions: filteredVersions,
11234
+ publishExecutionEvents: shape.publishExecutionEvents
11065
11235
  });
11066
11236
  await client.setMetadata(create.metaRequest);
11067
11237
  }), ...changeSet.updates.map(async (update) => {
@@ -11073,7 +11243,8 @@ async function applyWorkflow(client, result, phase = "create-update") {
11073
11243
  mainJobFunctionName: shape.mainJobFunctionName,
11074
11244
  retryPolicy: shape.retryPolicy,
11075
11245
  concurrencyPolicy: shape.concurrencyPolicy,
11076
- jobFunctions: filteredVersions
11246
+ jobFunctions: filteredVersions,
11247
+ publishExecutionEvents: shape.publishExecutionEvents
11077
11248
  });
11078
11249
  await client.setMetadata(update.metaRequest);
11079
11250
  })]);
@@ -11134,9 +11305,10 @@ function filterJobFunctionVersions(allVersions, usedJobNames) {
11134
11305
  * @param appName - Application name
11135
11306
  * @param appId - Application ID used for job function metadata when available
11136
11307
  * @param unchangedWorkflowJobNames - Job function names used by unchanged workflows
11308
+ * @param jobFunctionPublishEvents - Resolved `publishExecutionEvents` keyed by job function name
11137
11309
  * @returns Map of job function names to versions
11138
11310
  */
11139
- async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
11311
+ async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set(), jobFunctionPublishEvents = /* @__PURE__ */ new Map()) {
11140
11312
  const jobFunctionVersions = {};
11141
11313
  const firstWorkflow = changeSet.creates[0] || changeSet.updates[0] || changeSet.deletes[0];
11142
11314
  if (!firstWorkflow) return jobFunctionVersions;
@@ -11155,15 +11327,14 @@ async function registerJobFunctions(client, changeSet, appName, appId, unchanged
11155
11327
  const existingJobNamesSet = new Set(existingJobFunctions);
11156
11328
  if (changeSet.creates.length > 0 || changeSet.updates.length > 0) {
11157
11329
  const results = await Promise.all(Array.from(allUsedJobNames).map(async (jobName) => {
11158
- const response = existingJobNamesSet.has(jobName) ? await client.updateWorkflowJobFunction({
11159
- workspaceId,
11160
- jobFunctionName: jobName,
11161
- scriptRef: workflowJobFunctionName(jobName)
11162
- }) : await client.createWorkflowJobFunction({
11330
+ const isExisting = existingJobNamesSet.has(jobName);
11331
+ const request = {
11163
11332
  workspaceId,
11164
11333
  jobFunctionName: jobName,
11165
- scriptRef: workflowJobFunctionName(jobName)
11166
- });
11334
+ scriptRef: workflowJobFunctionName(jobName),
11335
+ publishExecutionEvents: jobFunctionPublishEvents.get(jobName) ?? false
11336
+ };
11337
+ const response = isExisting ? await client.updateWorkflowJobFunction(request) : await client.createWorkflowJobFunction(request);
11167
11338
  await client.setMetadata(await buildMetaRequest({
11168
11339
  trn: resourceTrn(workspaceId, "workflow_job_function", jobName),
11169
11340
  appName,
@@ -11210,9 +11381,64 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11210
11381
  workflowName: workflow.name,
11211
11382
  mainJobFunctionName: workflow.mainJob.name,
11212
11383
  ...workflow.retryPolicy && { retryPolicy: toRetryPolicy(workflow.retryPolicy) },
11213
- ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) }
11384
+ ...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) },
11385
+ publishExecutionEvents: workflow.publishEvents ?? false
11214
11386
  };
11215
11387
  }
11388
+ const NO_EVENT_SUBSCRIBERS = { workflowNames: /* @__PURE__ */ new Set() };
11389
+ function isSubscribed(subscribers, workflowName) {
11390
+ return subscribers.workflowNames.has(workflowName);
11391
+ }
11392
+ function resolvePublishEvents(params) {
11393
+ const { explicit, subscribed, remote, conflictError } = params;
11394
+ if (explicit === false && subscribed) throw new Error(conflictError);
11395
+ if (explicit !== void 0) return explicit;
11396
+ return subscribed || (remote ?? false);
11397
+ }
11398
+ /**
11399
+ * Resolve `publishExecutionEvents` for every job function used by a workflow.
11400
+ *
11401
+ * A job execution trigger names a workflow rather than a job, so a subscription
11402
+ * opts in every job that workflow runs.
11403
+ * @param params - Workflows, their job dependencies, subscribers, explicit job flags, and existing job functions
11404
+ * @returns Resolved flags keyed by job function name
11405
+ */
11406
+ function resolveJobPublishEvents(params) {
11407
+ const { workflows, mainJobDeps, subscribers, explicit, existing } = params;
11408
+ const usedJobNames = /* @__PURE__ */ new Set();
11409
+ const subscribedJobNames = /* @__PURE__ */ new Set();
11410
+ for (const workflow of Object.values(workflows)) {
11411
+ const jobNames = mainJobDeps[workflow.mainJob.name];
11412
+ if (!jobNames) continue;
11413
+ const subscribed = isSubscribed(subscribers, workflow.name);
11414
+ for (const jobName of jobNames) {
11415
+ usedJobNames.add(jobName);
11416
+ if (subscribed) subscribedJobNames.add(jobName);
11417
+ }
11418
+ }
11419
+ const resolved = /* @__PURE__ */ new Map();
11420
+ for (const jobName of usedJobNames) resolved.set(jobName, resolvePublishEvents({
11421
+ explicit: explicit.get(jobName),
11422
+ subscribed: subscribedJobNames.has(jobName),
11423
+ remote: existing.get(jobName)?.publishExecutionEvents,
11424
+ 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.`
11425
+ }));
11426
+ return resolved;
11427
+ }
11428
+ /**
11429
+ * Collect job functions whose remote publishing flag no longer matches the plan.
11430
+ * @param existing - Existing job functions keyed by name
11431
+ * @param resolved - Resolved `publishExecutionEvents` keyed by job function name
11432
+ * @returns Job function names needing re-registration
11433
+ */
11434
+ function collectStaleJobFunctionNames(existing, resolved) {
11435
+ const stale = /* @__PURE__ */ new Set();
11436
+ for (const [jobName, publishEvents] of resolved) {
11437
+ const remote = existing.get(jobName);
11438
+ if (remote && remote.publishExecutionEvents !== publishEvents) stale.add(jobName);
11439
+ }
11440
+ return stale;
11441
+ }
11216
11442
  /**
11217
11443
  * Plan workflow changes and job functions based on current and desired state.
11218
11444
  * @param client - Operator client instance
@@ -11222,15 +11448,26 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
11222
11448
  * @param workflows - Parsed workflows
11223
11449
  * @param mainJobDeps - Main job dependencies by workflow
11224
11450
  * @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan
11451
+ * @param eventPublishing - Executor subscriptions and explicit job flags driving execution event publishing
11225
11452
  * @returns Planned workflow changes
11226
11453
  */
11227
- async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
11454
+ async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set(), eventPublishing = {}) {
11228
11455
  const changeSet = createChangeSet("Workflows");
11229
11456
  const conflicts = [];
11230
11457
  const unmanaged = [];
11231
11458
  const resourceOwners = /* @__PURE__ */ new Set();
11232
11459
  const unchangedWorkflowJobNames = /* @__PURE__ */ new Set();
11233
11460
  const retainedWorkflowJobNames = /* @__PURE__ */ new Set();
11461
+ const executionSubscribers = eventPublishing.execution ?? NO_EVENT_SUBSCRIBERS;
11462
+ const existingJobFunctions = await fetchExistingJobFunctions(client, workspaceId);
11463
+ const jobFunctionPublishEvents = resolveJobPublishEvents({
11464
+ workflows,
11465
+ mainJobDeps,
11466
+ subscribers: eventPublishing.jobExecution ?? NO_EVENT_SUBSCRIBERS,
11467
+ explicit: eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map(),
11468
+ existing: existingJobFunctions
11469
+ });
11470
+ const staleJobFunctionNames = collectStaleJobFunctionNames(existingJobFunctions, jobFunctionPublishEvents);
11234
11471
  const existingWorkflows = await fetchExistingResourcesWithLabels({
11235
11472
  client,
11236
11473
  fetchPage: async (pageToken, pageSize) => {
@@ -11254,6 +11491,15 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11254
11491
  const usedJobNames = mainJobDeps[workflow.mainJob.name];
11255
11492
  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}", ... })`);
11256
11493
  usedJobNames.forEach((jobName) => retainedWorkflowJobNames.add(jobName));
11494
+ const desiredWorkflow = {
11495
+ ...workflow,
11496
+ publishEvents: resolvePublishEvents({
11497
+ explicit: workflow.publishEvents,
11498
+ subscribed: isSubscribed(executionSubscribers, workflow.name),
11499
+ remote: existing?.resource.publishExecutionEvents,
11500
+ 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.`
11501
+ })
11502
+ };
11257
11503
  if (existing) {
11258
11504
  if (trackDesiredResourceOwnership({
11259
11505
  labels: existing.allLabels,
@@ -11264,13 +11510,19 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11264
11510
  resourceName: workflow.name,
11265
11511
  conflicts,
11266
11512
  unmanaged
11267
- }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(existing.resource, workflow, usedJobNames, unchangedJobFunctions)) {
11513
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged({
11514
+ existing: existing.resource,
11515
+ workflow: desiredWorkflow,
11516
+ usedJobNames,
11517
+ unchangedJobFunctions,
11518
+ staleJobFunctionNames
11519
+ })) {
11268
11520
  changeSet.unchanged.push({ name: workflow.name });
11269
11521
  for (const jobName of usedJobNames) unchangedWorkflowJobNames.add(jobName);
11270
11522
  } else changeSet.updates.push({
11271
11523
  name: workflow.name,
11272
11524
  workspaceId,
11273
- workflow,
11525
+ workflow: desiredWorkflow,
11274
11526
  usedJobNames,
11275
11527
  metaRequest
11276
11528
  });
@@ -11278,7 +11530,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11278
11530
  } else changeSet.creates.push({
11279
11531
  name: workflow.name,
11280
11532
  workspaceId,
11281
- workflow,
11533
+ workflow: desiredWorkflow,
11282
11534
  usedJobNames,
11283
11535
  metaRequest
11284
11536
  });
@@ -11308,6 +11560,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11308
11560
  workspaceId,
11309
11561
  appName,
11310
11562
  appId,
11563
+ existingJobFunctionNames: [...existingJobFunctions.keys()],
11311
11564
  retainedWorkflowJobNames
11312
11565
  });
11313
11566
  const deletableJobNames = new Set(jobFunctionDeletes.map((del) => del.jobFunctionName));
@@ -11323,20 +11576,24 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
11323
11576
  appName,
11324
11577
  appId,
11325
11578
  unchangedWorkflowJobNames,
11326
- jobFunctionDeletes
11579
+ jobFunctionDeletes,
11580
+ jobFunctionPublishEvents
11327
11581
  };
11328
11582
  }
11329
- async function planWorkflowJobFunctionDeletes(params) {
11330
- const { client, workspaceId, appName, appId, retainedWorkflowJobNames } = params;
11331
- const existingJobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
11583
+ async function fetchExistingJobFunctions(client, workspaceId) {
11584
+ const jobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
11332
11585
  const response = await client.listWorkflowJobFunctions({
11333
11586
  workspaceId,
11334
11587
  pageToken,
11335
11588
  pageSize: maxPageSize
11336
11589
  });
11337
- return [response.jobFunctions.map((jobFunction) => jobFunction.name), response.nextPageToken];
11590
+ return [response.jobFunctions, response.nextPageToken];
11338
11591
  });
11339
- const candidates = [...new Set(existingJobFunctions)].filter((jobName) => !retainedWorkflowJobNames.has(jobName));
11592
+ return new Map(jobFunctions.map((jobFunction) => [jobFunction.name, jobFunction]));
11593
+ }
11594
+ async function planWorkflowJobFunctionDeletes(params) {
11595
+ const { client, workspaceId, appName, appId, existingJobFunctionNames, retainedWorkflowJobNames } = params;
11596
+ const candidates = existingJobFunctionNames.filter((jobName) => !retainedWorkflowJobNames.has(jobName));
11340
11597
  return (await Promise.all(candidates.map(async (jobFunctionName) => {
11341
11598
  const { metadata } = await client.getMetadata({ trn: resourceTrn(workspaceId, "workflow_job_function", jobFunctionName) });
11342
11599
  return isOwnedByApp(metadata?.labels, appName, appId) ? {
@@ -11354,12 +11611,14 @@ async function planWorkflowJobFunctionDeletes(params) {
11354
11611
  function formatWorkflowChangeEntries(changeSet, workflowJobFunctionChanges) {
11355
11612
  return formatChangeEntriesWithFunctionRegistry("workflow", changeSet, workflowJobFunctionChanges, (item) => "usedJobNames" in item ? item.usedJobNames.map((jobName) => workflowJobFunctionName(jobName)) : []);
11356
11613
  }
11357
- function canTreatWorkflowAsUnchanged(existing, workflow, usedJobNames, unchangedJobFunctions) {
11614
+ function canTreatWorkflowAsUnchanged(params) {
11615
+ const { existing, workflow, usedJobNames, unchangedJobFunctions, staleJobFunctionNames } = params;
11358
11616
  if (!usedJobNames.every((jobName) => unchangedJobFunctions.has(jobName))) return false;
11617
+ if (usedJobNames.some((jobName) => staleJobFunctionNames.has(jobName))) return false;
11359
11618
  return areWorkflowsEqual(existing, workflow, usedJobNames);
11360
11619
  }
11361
11620
  function areWorkflowsEqual(existing, workflow, usedJobNames) {
11362
- 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));
11621
+ 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));
11363
11622
  }
11364
11623
  function normalizeComparableExistingWorkflowRetryPolicy(policy) {
11365
11624
  if (!policy) return;
@@ -11616,6 +11875,9 @@ async function applyDeploymentPlans(client, workspaceId, deployments) {
11616
11875
  const forEachDeployment = async (apply) => {
11617
11876
  for (const deployment of deployments) await apply(deployment);
11618
11877
  };
11878
+ await withSpan("apply.preflight", async () => {
11879
+ await forEachDeployment((d) => preflightTailorDB(client, d.tailorDB));
11880
+ });
11619
11881
  await withSpan("apply.createUpdateServices", async () => {
11620
11882
  await forEachDeployment((d) => applySecretManager(client, d.secretManager, "create-update", d.application));
11621
11883
  await forEachDeployment((d) => applyFunctionRegistry(client, workspaceId, d.functionRegistry, "create-update"));
@@ -13054,6 +13316,39 @@ function collectExecutorUsedResolvers(owner, targets) {
13054
13316
  }
13055
13317
  return usedResolvers;
13056
13318
  }
13319
+ /**
13320
+ * Resolve which workflows this target's executors subscribe to, per event
13321
+ * granularity level.
13322
+ *
13323
+ * Scoped to the owning target: unlike TailorDB, Auth and IdP, a workflow has no
13324
+ * `external` declaration, so no supported configuration shares a workflow and a
13325
+ * subscribing executor across configs.
13326
+ * @param target - Deployment target being planned
13327
+ * @returns Subscribers keyed by event granularity level
13328
+ */
13329
+ function collectWorkflowEventSubscribers(target) {
13330
+ const execution = { workflowNames: /* @__PURE__ */ new Set() };
13331
+ const jobExecution = { workflowNames: /* @__PURE__ */ new Set() };
13332
+ for (const executor of Object.values(target.application.executorService?.executors ?? {})) {
13333
+ const { trigger } = executor;
13334
+ if (trigger.kind !== "workflowExecution" && trigger.kind !== "workflowJobExecution") continue;
13335
+ (trigger.kind === "workflowExecution" ? execution : jobExecution).workflowNames.add(trigger.workflowName);
13336
+ }
13337
+ return {
13338
+ execution,
13339
+ jobExecution
13340
+ };
13341
+ }
13342
+ /**
13343
+ * Collect explicit `publishEvents` values declared on this target's workflow jobs.
13344
+ * @param target - Deployment target being planned
13345
+ * @returns Explicit flags keyed by job name
13346
+ */
13347
+ function collectWorkflowJobPublishEvents(target) {
13348
+ const jobPublishEvents = /* @__PURE__ */ new Map();
13349
+ for (const job of target.application.workflowService?.jobs ?? []) if (job.publishEvents !== void 0) jobPublishEvents.set(job.name, job.publishEvents);
13350
+ return jobPublishEvents;
13351
+ }
13057
13352
  function collectExpectedLocalStaticWebsiteNames(targets) {
13058
13353
  const websiteNames = /* @__PURE__ */ new Set();
13059
13354
  for (const target of targets) for (const website of target.application.staticWebsiteServices) websiteNames.add(website.name);
@@ -13655,7 +13950,10 @@ async function planDeploymentTarget(params) {
13655
13950
  withSpan("plan.pipeline", () => planPipeline(ctx)),
13656
13951
  withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
13657
13952
  withSpan("plan.executor", () => planExecutor(ctx)),
13658
- withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
13953
+ withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs, {
13954
+ ...collectWorkflowEventSubscribers(target),
13955
+ jobPublishEvents: collectWorkflowJobPublishEvents(target)
13956
+ })),
13659
13957
  withSpan("plan.workflowExecutionPolicy", () => planWorkflowJobFunctionExecutionPolicy(client, workspaceId, application.name, application.id, config.workflow?.executionPolicies ?? {})),
13660
13958
  withSpan("plan.secretManager", () => planSecretManager(ctx))
13661
13959
  ]);
@@ -14470,6 +14768,12 @@ function formatEventTriggerConfig(config) {
14470
14768
  eventType: config.eventType,
14471
14769
  condition: config.condition?.expr || ""
14472
14770
  };
14771
+ if (typedConfig.case === "workflow") return {
14772
+ kind: typedConfig.case,
14773
+ eventTypes: typedConfig.value.eventTypes,
14774
+ condition: typedConfig.value.condition?.expr || "",
14775
+ ...typedConfig.value.workflowName && { workflowName: typedConfig.value.workflowName }
14776
+ };
14473
14777
  const base = {
14474
14778
  kind: typedConfig.case,
14475
14779
  eventTypes: typedConfig.value.eventTypes,
@@ -18777,8 +19081,13 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
18777
19081
  function generateMigrationScript(diff) {
18778
19082
  const updates = [];
18779
19083
  for (const change of diff.changes) {
18780
- const script = generateChangeScript(change);
18781
- if (script) updates.push(script);
19084
+ const decimalScaleScript = generateDecimalScaleChangeScript(change);
19085
+ updates.push(...generateChangeScripts(change, decimalScaleScript !== null));
19086
+ if (decimalScaleScript) {
19087
+ updates.push(decimalScaleScript);
19088
+ const uniqueConstraintScript = generateUniqueConstraintScript(change);
19089
+ if (uniqueConstraintScript) updates.push(uniqueConstraintScript);
19090
+ }
18782
19091
  }
18783
19092
  if (updates.length === 0) updates.push(` // No data migration needed for this schema change
18784
19093
  // Add custom data transformations if required`);
@@ -18803,84 +19112,170 @@ ${updates.join("\n\n")}
18803
19112
  `;
18804
19113
  }
18805
19114
  /**
18806
- * Generate script for a single change
19115
+ * Generate scripts for a single change
18807
19116
  * @param {DiffChange} change - Diff change to generate script for
18808
- * @returns {string | null} Script content or null if no script needed
18809
- */
18810
- function generateChangeScript(change) {
19117
+ * @param {boolean} deferUniqueConstraint - Generate the unique check after decimal re-serialization
19118
+ * @returns {string[]} Script contents, or an empty array if no script is needed
19119
+ */
19120
+ function generateChangeScripts(change, deferUniqueConstraint = false) {
19121
+ if (change.kind === "index_added" || change.kind === "index_modified") {
19122
+ const before = change.kind === "index_modified" ? change.before : void 0;
19123
+ if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) return [];
19124
+ const fields = change.after.fields;
19125
+ const fieldList = fields.map((f) => `"${f}"`).join(", ");
19126
+ const whereClauses = fields.map((f) => `.where("${f}", "=", dup.${f})`).join("\n ");
19127
+ return [` // Resolve duplicate (${fields.join(", ")}) combinations before unique index "${change.indexName}" is enforced
19128
+ {
19129
+ const duplicates = await trx
19130
+ .selectFrom("${change.typeName}")
19131
+ .select([${fieldList}])
19132
+ .groupBy([${fieldList}])
19133
+ .having((eb) => eb.fn.count("id"), ">", 1)
19134
+ .execute();
19135
+ for (const dup of duplicates) {
19136
+ const records = await trx
19137
+ .selectFrom("${change.typeName}")
19138
+ .select(["id"])
19139
+ ${whereClauses}
19140
+ .execute();
19141
+ // Keep the first record; update or delete the others so the combination becomes unique
19142
+ for (let i = 1; i < records.length; i++) {
19143
+ await trx
19144
+ .updateTable("${change.typeName}")
19145
+ .set({ ${fields[0]}: null }) // TODO: Set appropriate unique value
19146
+ .where("id", "=", records[i].id)
19147
+ .execute();
19148
+ }
19149
+ }
19150
+ }`];
19151
+ }
18811
19152
  if (change.kind === "field_added") {
18812
- if (change.after.required) return ` // Populate ${change.fieldName} for existing ${change.typeName} records
19153
+ if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.typeName} records
18813
19154
  await trx
18814
19155
  .updateTable("${change.typeName}")
18815
19156
  .set({
18816
19157
  ${change.fieldName}: null, // TODO: Set appropriate default value
18817
19158
  })
18818
- .execute();`;
18819
- return null;
19159
+ .execute();`];
19160
+ return [];
18820
19161
  }
18821
- if (change.kind !== "field_modified") return null;
19162
+ if (change.kind !== "field_modified") return [];
18822
19163
  const { before, after } = change;
18823
- if (!before.required && after.required) return ` // Set ${change.fieldName} for ${change.typeName} records where it is null
19164
+ const scripts = [];
19165
+ if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.typeName} records where it is null
18824
19166
  await trx
18825
19167
  .updateTable("${change.typeName}")
18826
19168
  .set({
18827
19169
  ${change.fieldName}: null, // TODO: Set appropriate default value
18828
19170
  })
18829
19171
  .where("${change.fieldName}", "is", null)
18830
- .execute();`;
18831
- if (!(before.unique ?? false) && (after.unique ?? false)) return ` // Ensure ${change.fieldName} values are unique before adding constraint
18832
- const duplicates = await trx
18833
- .selectFrom("${change.typeName}")
18834
- .select(["${change.fieldName}"])
18835
- .groupBy("${change.fieldName}")
18836
- .having((eb) => eb.fn.count("id"), ">", 1)
18837
- .execute();
18838
- for (const dup of duplicates) {
18839
- // Keep first record, add suffix to others
18840
- const records = await trx
18841
- .selectFrom("${change.typeName}")
18842
- .select(["id", "${change.fieldName}"])
18843
- .where("${change.fieldName}", "=", dup.${change.fieldName})
18844
- .execute();
18845
- for (let i = 1; i < records.length; i++) {
18846
- await trx
18847
- .updateTable("${change.typeName}")
18848
- .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
18849
- .where("id", "=", records[i].id)
18850
- .execute();
18851
- }
18852
- }`;
19172
+ .execute();`);
19173
+ if (!deferUniqueConstraint) {
19174
+ const uniqueConstraintScript = generateUniqueConstraintScript(change);
19175
+ if (uniqueConstraintScript) scripts.push(uniqueConstraintScript);
19176
+ }
18853
19177
  if (before.type === "enum" && after.type === "enum") {
18854
19178
  const beforeValues = (before.allowedValues ?? []).map((v) => v.value);
18855
19179
  const afterValues = (after.allowedValues ?? []).map((v) => v.value);
18856
19180
  const removedValues = beforeValues.filter((v) => !afterValues.includes(v));
18857
19181
  if (removedValues.length > 0) {
18858
19182
  const defaultValue = afterValues[0] ?? "NEW_VALUE";
18859
- return ` // Migrate records with removed enum values: ${removedValues.join(", ")}
19183
+ scripts.push(` // Migrate records with removed enum values: ${removedValues.join(", ")}
18860
19184
  await trx
18861
19185
  .updateTable("${change.typeName}")
18862
19186
  .set({ ${change.fieldName}: "${defaultValue}" }) // TODO: Set appropriate value
18863
19187
  .where("${change.fieldName}", "in", [${removedValues.map((v) => `"${v}"`).join(", ")}])
18864
- .execute();`;
19188
+ .execute();`);
18865
19189
  }
18866
19190
  }
18867
- if (before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType) return ` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
19191
+ if (before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType) scripts.push(` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
18868
19192
  // Find records that don't have a valid reference in the new target table
18869
- const orphanedRecords = await trx
18870
- .selectFrom("${change.typeName}")
18871
- .leftJoin("${after.foreignKeyType}", "${change.typeName}.${change.fieldName}", "${after.foreignKeyType}.id")
18872
- .select(["${change.typeName}.id", "${change.typeName}.${change.fieldName}"])
18873
- .where("${after.foreignKeyType}.id", "is", null)
18874
- .where("${change.typeName}.${change.fieldName}", "is not", null)
18875
- .execute();
18876
- for (const record of orphanedRecords) {
18877
- await trx
18878
- .updateTable("${change.typeName}")
18879
- .set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
18880
- .where("id", "=", record.id)
19193
+ {
19194
+ const orphanedRecords = await trx
19195
+ .selectFrom("${change.typeName}")
19196
+ .leftJoin("${after.foreignKeyType}", "${change.typeName}.${change.fieldName}", "${after.foreignKeyType}.id")
19197
+ .select(["${change.typeName}.id", "${change.typeName}.${change.fieldName}"])
19198
+ .where("${after.foreignKeyType}.id", "is", null)
19199
+ .where("${change.typeName}.${change.fieldName}", "is not", null)
18881
19200
  .execute();
19201
+ for (const record of orphanedRecords) {
19202
+ await trx
19203
+ .updateTable("${change.typeName}")
19204
+ .set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
19205
+ .where("id", "=", record.id)
19206
+ .execute();
19207
+ }
19208
+ }`);
19209
+ return scripts;
19210
+ }
19211
+ function generateUniqueConstraintScript(change) {
19212
+ if (change.kind !== "field_modified") return null;
19213
+ const { before, after } = change;
19214
+ if ((before.unique ?? false) || !(after.unique ?? false)) return null;
19215
+ return ` // Ensure ${change.fieldName} values are unique before adding constraint
19216
+ {
19217
+ const duplicates = await trx
19218
+ .selectFrom("${change.typeName}")
19219
+ .select(["${change.fieldName}"])
19220
+ .groupBy("${change.fieldName}")
19221
+ .having((eb) => eb.fn.count("id"), ">", 1)
19222
+ .execute();
19223
+ for (const dup of duplicates) {
19224
+ // Keep first record, add suffix to others
19225
+ const records = await trx
19226
+ .selectFrom("${change.typeName}")
19227
+ .select(["id", "${change.fieldName}"])
19228
+ .where("${change.fieldName}", "=", dup.${change.fieldName})
19229
+ .execute();
19230
+ for (let i = 1; i < records.length; i++) {
19231
+ await trx
19232
+ .updateTable("${change.typeName}")
19233
+ .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
19234
+ .where("id", "=", records[i].id)
19235
+ .execute();
19236
+ }
19237
+ }
19238
+ }`;
19239
+ }
19240
+ function generateDecimalScaleChangeScript(change) {
19241
+ if (change.kind !== "field_modified") return null;
19242
+ const { before, after } = change;
19243
+ if (before.type !== "decimal" || after.type !== "decimal" || before.scale === after.scale) return null;
19244
+ const valueExpression = !before.required && after.required ? `row.${change.fieldName}!` : `row.${change.fieldName}`;
19245
+ const beforeScale = before.scale ?? 6;
19246
+ const roundingWarning = (after.scale ?? 6) < beforeScale ? `
19247
+ // Values that exceed the new scale may be rounded half-up, so review the
19248
+ // resulting precision before deploying.` : "";
19249
+ return ` // Re-save existing ${change.typeName} rows so ${change.fieldName} is stored under the new scale.
19250
+ // This is a workaround for a platform-side gap where rows written under the
19251
+ // previous scale could fail on later updates until re-saved. Keep it unless
19252
+ // your platform is confirmed to handle stored values across scale changes.${roundingWarning}
19253
+ {
19254
+ let lastId: string | undefined;
19255
+ while (true) {
19256
+ let query = trx
19257
+ .selectFrom("${change.typeName}")
19258
+ .select(["id", "${change.fieldName}"])
19259
+ .where("${change.fieldName}", "is not", null)
19260
+ .orderBy("id", "asc")
19261
+ .limit(100);
19262
+ if (lastId) {
19263
+ query = query.where("id", ">", lastId);
19264
+ }
19265
+ const rows = await query.execute();
19266
+ if (rows.length === 0) break;
19267
+
19268
+ for (const row of rows) {
19269
+ await trx
19270
+ .updateTable("${change.typeName}")
19271
+ .set({ ${change.fieldName}: ${valueExpression} })
19272
+ .where("id", "=", row.id)
19273
+ .where("${change.fieldName}", "=", ${valueExpression})
19274
+ .execute();
19275
+ }
19276
+ lastId = rows[rows.length - 1]!.id;
19277
+ }
18882
19278
  }`;
18883
- return null;
18884
19279
  }
18885
19280
 
18886
19281
  //#endregion
@@ -18949,7 +19344,7 @@ async function generate(options) {
18949
19344
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
18950
19345
  let pluginManager;
18951
19346
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
18952
- const { defineApplication } = await import("./application-CP0W2sQo.mjs");
19347
+ const { defineApplication } = await import("./application-5FOWIxFQ.mjs");
18953
19348
  const application = defineApplication({
18954
19349
  config,
18955
19350
  pluginManager
@@ -21078,4 +21473,4 @@ async function registerTypeScriptRuntime(hookUrl) {
21078
21473
 
21079
21474
  //#endregion
21080
21475
  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 };
21081
- //# sourceMappingURL=register-typescript-runtime-O7zQNif0.mjs.map
21476
+ //# sourceMappingURL=register-typescript-runtime-BCvWR3HC.mjs.map