@tailor-platform/sdk 2.0.1 → 2.2.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 (92) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/{dist/cli/index.mjs → bin/tailor.mjs} +1 -1
  3. package/dist/{application-CM6hKnSK.mjs → application-BpLeawBg.mjs} +126 -28
  4. package/dist/application-BpLeawBg.mjs.map +1 -0
  5. package/dist/application-BxLLiLsr.mjs +3 -0
  6. package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
  7. package/dist/cli/commands/deploy/types.d.mts +2 -1
  8. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +11 -4
  9. package/dist/cli/commands/tailordb/migrate/file-state.d.mts +1 -0
  10. package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +2 -2
  11. package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +8 -1
  12. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +5 -3
  13. package/dist/cli/lib.d.mts +4 -4
  14. package/dist/cli/lib.mjs +3 -482
  15. package/dist/cli/lib.mjs.map +1 -1
  16. package/dist/cli/main.mjs +997 -202
  17. package/dist/cli/main.mjs.map +1 -1
  18. package/dist/completion/zsh-worker.zsh +145 -2
  19. package/dist/configure/config/types.d.mts +12 -1
  20. package/dist/configure/index.d.mts +3 -3
  21. package/dist/configure/services/index.d.mts +3 -2
  22. package/dist/configure/services/resolver/index.d.mts +1 -1
  23. package/dist/configure/services/resolver/resolver.d.mts +1 -1
  24. package/dist/configure/services/tailordb/index.d.mts +2 -1
  25. package/dist/configure/services/tailordb/schema.d.mts +46 -46
  26. package/dist/configure/services/tailordb/types.d.mts +30 -4
  27. package/dist/{crashreport-B5LqCTfF.mjs → crashreport-CIIfLnoO.mjs} +2 -2
  28. package/dist/{crashreport-B5LqCTfF.mjs.map → crashreport-CIIfLnoO.mjs.map} +1 -1
  29. package/dist/{crashreport-eI7ty7ok.mjs → crashreport-CINZ5dVN.mjs} +1 -1
  30. package/dist/errors-CWj21238.mjs +75 -0
  31. package/dist/errors-CWj21238.mjs.map +1 -0
  32. package/dist/{globals-TfAVItuK.mjs → globals-B2nlxBrz.mjs} +2 -10
  33. package/dist/globals-B2nlxBrz.mjs.map +1 -0
  34. package/dist/kysely/index.d.mts +52 -5
  35. package/dist/kysely/index.mjs +7 -2
  36. package/dist/kysely/index.mjs.map +1 -1
  37. package/dist/{logger-CXQq9YIp.mjs → logger-DSdExfor.mjs} +83 -42
  38. package/dist/logger-DSdExfor.mjs.map +1 -0
  39. package/dist/plugin/builtin/seed/index.mjs +1 -1
  40. package/dist/{register-ts-hook-LYV7zH-e.mjs → register-ts-hook-CTth1eqj.mjs} +1587 -272
  41. package/dist/register-ts-hook-CTth1eqj.mjs.map +1 -0
  42. package/dist/runtime/globals.d.mts +0 -7
  43. package/dist/runtime/workflow.d.mts +2 -7
  44. package/dist/schema--xYWRGfe.mjs.map +1 -1
  45. package/dist/seed/index.d.mts +52 -3
  46. package/dist/seed/index.mjs +200 -44
  47. package/dist/seed/index.mjs.map +1 -1
  48. package/dist/{seed-B-yckgQY.mjs → seed-xqM9WIHP.mjs} +8 -8
  49. package/dist/seed-xqM9WIHP.mjs.map +1 -0
  50. package/dist/{service-B5WynBHQ.mjs → service-CCwl3Avt.mjs} +3 -3
  51. package/dist/{service-B5WynBHQ.mjs.map → service-CCwl3Avt.mjs.map} +1 -1
  52. package/dist/service-_XmjSEGr.mjs +3 -0
  53. package/dist/{service-k1xy_A5G.mjs → service-goqlJJgA.mjs} +2 -2
  54. package/dist/{service-k1xy_A5G.mjs.map → service-goqlJJgA.mjs.map} +1 -1
  55. package/dist/{service_pb-D5--My_M.mjs → service_pb-BEWj1pOV.mjs} +2 -2
  56. package/dist/{service_pb-D5--My_M.mjs.map → service_pb-BEWj1pOV.mjs.map} +1 -1
  57. package/dist/{service_pb-i8CmnQP4.mjs → service_pb-BjlhvK9E.mjs} +1 -1
  58. package/dist/utils/test/index.d.mts +9 -3
  59. package/dist/utils/test/index.mjs +49 -25
  60. package/dist/utils/test/index.mjs.map +1 -1
  61. package/dist/vitest/environment.mjs +1 -1
  62. package/dist/vitest/index.d.mts +2 -1
  63. package/dist/vitest/index.mjs +84 -2
  64. package/dist/vitest/index.mjs.map +1 -1
  65. package/dist/vitest/mocks/file.d.mts +1 -1
  66. package/dist/vitest/pglite-kysely.d.mts +43 -0
  67. package/dist/vitest/setup.mjs +1 -1
  68. package/dist/workflow-Bamae_Yc.mjs.map +1 -1
  69. package/dist/{workspace_resource_pb-DXnO2AT-.mjs → workspace_resource_pb-HNIL_Vfj.mjs} +10 -2
  70. package/dist/{workspace_resource_pb-DXnO2AT-.mjs.map → workspace_resource_pb-HNIL_Vfj.mjs.map} +1 -1
  71. package/docs/cli/tailordb.md +87 -22
  72. package/docs/cli-reference.md +13 -11
  73. package/docs/configuration.md +4 -1
  74. package/docs/migration/v2.md +1 -1
  75. package/docs/plugin/custom.md +48 -48
  76. package/docs/plugin/index.md +24 -24
  77. package/docs/services/auth.md +10 -10
  78. package/docs/services/executor.md +1 -1
  79. package/docs/services/resolver.md +34 -2
  80. package/docs/services/tailordb-migration.md +375 -76
  81. package/docs/services/tailordb.md +28 -28
  82. package/docs/testing.md +3 -1
  83. package/package.json +13 -8
  84. package/dist/application-CM6hKnSK.mjs.map +0 -1
  85. package/dist/application-CXNaUNhv.mjs +0 -3
  86. package/dist/errors-D7583Zz7.mjs +0 -58
  87. package/dist/errors-D7583Zz7.mjs.map +0 -1
  88. package/dist/globals-TfAVItuK.mjs.map +0 -1
  89. package/dist/logger-CXQq9YIp.mjs.map +0 -1
  90. package/dist/register-ts-hook-LYV7zH-e.mjs.map +0 -1
  91. package/dist/seed-B-yckgQY.mjs.map +0 -1
  92. package/dist/service-M7OYziQP.mjs +0 -3
@@ -1,18 +1,20 @@
1
1
  import { t as db } from "./schema--xYWRGfe.mjs";
2
- import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as Subgraph_ServiceType, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, Ft as CreateApplicationRequestSchema, H as IdPPermissionPermit, Ht as UpdateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, Jt as FilterSchema, K as CreateExecutorExecutorRequestSchema, Kt as ConditionSchema, L as CreateIdPServiceRequestSchema, Lt as UpdateApplicationRequestSchema, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, V as IdPPermissionOperator, Vt as CreateAIGatewayRequestSchema, X as ExecutorTargetType, Y as ExecutorJobStatus, Yt as PageDirection, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as Condition_Operator, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status, zt as ApplicationSchemaUpdateAttemptStatus } from "./workspace_resource_pb-DXnO2AT-.mjs";
2
+ import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as ApplicationSchemaUpdateAttemptStatus, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, H as IdPPermissionPermit, Ht as CreateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as CreateApplicationRequestSchema, Jt as Condition_Operator, K as CreateExecutorExecutorRequestSchema, L as CreateIdPServiceRequestSchema, Lt as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, 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, Rt as UpdateApplicationRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, Ut as UpdateAIGatewayRequestSchema, V as IdPPermissionOperator, Vt as Subgraph_ServiceType, X as ExecutorTargetType, Xt as PageDirection, Y as ExecutorJobStatus, Yt as FilterSchema, 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 ConditionSchema, 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 } from "./workspace_resource_pb-HNIL_Vfj.mjs";
3
3
  import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
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 { $ as LOG_LEVELS, A as hashContent, B as getConsoleBaseUrl, D as hasGenerationHooks, E as getPluginGenerationDependencies, F as fetchAllTolerant, G as initOperatorClient, H as getOrNull, I as fetchMachineUserToken, J as normalizeBaseUrl, K as isDefaultPlatform, L as fetchPaged, N as defaultPlatformBaseUrl, O as createBundleCache, P as fetchAll, Q as createApplyLimiter, T as createBundleLog, U as getPlatformBaseUrl, V as getOAuth2ClientId, W as initOAuth2Client, X as resolveStaticWebsiteUrls, Y as rememberPlatformConfigForToken, Z as byName, _ as stringifyFunction, b as resolveTSConfigWithFallback, f as buildExecutorArgsExpr, g as assertUniqueTailorDBTypeNamesWithExternal, h as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as hashFile, k as getDistDir, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildResolverOperationHookExpr, q as isNotFoundError, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, u as HTTP_METHODS, v as createGeneratedEntryResolverPlugin, x as platformBundleDefinePlugin, y as createTsconfigPathsPlugin, z as fetchUserInfo } from "./application-CM6hKnSK.mjs";
6
- import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-k1xy_A5G.mjs";
4
+ import { a as parseBoolean, i as symbols, n as logger, o as renderTable, r as styles, s as renderFor, t as CIPromptError } from "./logger-DSdExfor.mjs";
5
+ import { $ as createApplyLimiter, A as getDistDir, B as fetchUserInfo, D as getPluginGenerationDependencies, E as createBundleLog, F as fetchAll, G as initOAuth2Client, H as getOAuth2ClientId, I as fetchAllTolerant, J as isNotFoundError, K as initOperatorClient, L as fetchMachineUserToken, M as hashFile, O as hasGenerationHooks, P as defaultPlatformBaseUrl, Q as byName, R as fetchPaged, S as platformBundleDefinePlugin, U as getOrNull, V as getConsoleBaseUrl, W as getPlatformBaseUrl, X as rememberPlatformConfigForToken, Y as normalizeBaseUrl, Z as resolveStaticWebsiteUrls, _ as assertUniqueTailorDBTypeNamesWithExternal, b as createTsconfigPathsPlugin, d as HTTP_METHODS, et as LOG_LEVELS, g as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as hashContent, k as createBundleCache, m as buildResolverOperationHookExpr, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildExecutorArgsExpr, q as isDefaultPlatform, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, v as stringifyFunction, x as resolveTSConfigWithFallback, y as createGeneratedEntryResolverPlugin } from "./application-BpLeawBg.mjs";
6
+ import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-goqlJJgA.mjs";
7
7
  import { t as multiline } from "./multiline-sfHpTZZK.mjs";
8
8
  import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
9
9
  import { t as userAgent } from "./user-agent-Bgsszb5I.mjs";
10
- import { n as isCLIError, t as createCLIError } from "./errors-D7583Zz7.mjs";
10
+ import { i as isCLIError, n as formatCopyableCommand, t as createCLIError } from "./errors-CWj21238.mjs";
11
11
  import { n as tightenSecretFilePermissions, r as writeSecretFile } from "./secret-file-C3J4Gy8r.mjs";
12
12
  import { r as withSpan } from "./telemetry-BRVdwh14.mjs";
13
+ import { a as generateIdpTruncateScriptCode, i as generateIdpSeedScriptCode, o as processIdpUser, r as processLinesDb, t as SeedGeneratorID } from "./seed-xqM9WIHP.mjs";
13
14
  import * as mod from "node:module";
14
15
  import * as path from "pathe";
15
16
  import { basename, dirname, join, resolve } from "pathe";
17
+ import { resolveTSConfig } from "pkg-types";
16
18
  import { arg, createDefineCommand, defineCommand, runCommand } from "politty";
17
19
  import { z } from "zod";
18
20
  import { create, fromJson, toJson } from "@bufbuild/protobuf";
@@ -206,9 +208,26 @@ const workspaceArgs = {
206
208
  })
207
209
  };
208
210
  /**
211
+ * Default config file path used when --config is not passed
212
+ */
213
+ const DEFAULT_CONFIG_PATH = "tailor.config.ts";
214
+ /**
215
+ * Format the --config argument for remediation command hints so they target
216
+ * the same config the current run used. The `--config=<value>` form keeps a
217
+ * leading-hyphen path bound as the option value.
218
+ * @param {string} [configPath] - Config path the current run used, if any
219
+ * @returns {string | undefined} `--config=<path>` argument, or undefined when the default config is in use
220
+ */
221
+ function formatConfigArg(configPath) {
222
+ if (!configPath) return void 0;
223
+ const relativeConfigPath = path.relative(process.cwd(), configPath);
224
+ if (relativeConfigPath === "tailor.config.ts") return void 0;
225
+ return `--config=${relativeConfigPath}`;
226
+ }
227
+ /**
209
228
  * Shared config arg for commands that accept a config file path
210
229
  */
211
- const configArg = { config: arg(z.string().default("tailor.config.ts"), {
230
+ const configArg = { config: arg(z.string().default(DEFAULT_CONFIG_PATH), {
212
231
  alias: "c",
213
232
  description: "Path to Tailor config file",
214
233
  env: "TAILOR_CONFIG_PATH",
@@ -220,7 +239,7 @@ const configArg = { config: arg(z.string().default("tailor.config.ts"), {
220
239
  /**
221
240
  * Shared config arg for commands that accept one or more comma-separated config file paths
222
241
  */
223
- const multiConfigArg = { config: arg(z.string().default("tailor.config.ts"), {
242
+ const multiConfigArg = { config: arg(z.string().default(DEFAULT_CONFIG_PATH), {
224
243
  alias: "c",
225
244
  description: "Path to SDK config file. Use comma-separated paths to deploy multiple apps together.",
226
245
  env: "TAILOR_PLATFORM_SDK_CONFIG_PATH",
@@ -1675,6 +1694,35 @@ function createCacheManager(options) {
1675
1694
  };
1676
1695
  }
1677
1696
 
1697
+ //#endregion
1698
+ //#region src/cli/commands/tailordb/migrate/migration-number.ts
1699
+ /**
1700
+ * Format migration number as 4-digit string.
1701
+ * @param num - Migration number
1702
+ * @returns 4-digit padded string
1703
+ */
1704
+ function formatMigrationNumber(num) {
1705
+ return num.toString().padStart(4, "0");
1706
+ }
1707
+ /**
1708
+ * Parse a migration number CLI argument.
1709
+ *
1710
+ * Accepts the canonical 4-digit form ("0001") or a bare integer without
1711
+ * leading zeros ("0"–"9999"). Commands that disallow the baseline reject
1712
+ * 0 themselves with a context-specific message.
1713
+ * @param numberStr - Raw CLI argument
1714
+ * @returns Parsed migration number
1715
+ */
1716
+ function parseMigrationNumberArg(numberStr) {
1717
+ if (/^\d{4}$/.test(numberStr)) return parseInt(numberStr, 10);
1718
+ if (/^(0|[1-9]\d*)$/.test(numberStr)) {
1719
+ const parsed = parseInt(numberStr, 10);
1720
+ if (parsed > 9999) throw new Error(`Migration number ${numberStr} is out of range. Expected 0-9999.`);
1721
+ return parsed;
1722
+ }
1723
+ throw new Error(`Invalid migration number format: ${numberStr}. Expected 4-digit format (e.g., 0001) or integer 0-9999 (e.g., 1).`);
1724
+ }
1725
+
1678
1726
  //#endregion
1679
1727
  //#region src/cli/shared/publish-events.ts
1680
1728
  /**
@@ -4983,6 +5031,9 @@ async function applyAuth(client, result, phase = "create-update") {
4983
5031
  const applyMachineUsers = async () => {
4984
5032
  await Promise.all([...changeSet.machineUser.creates.map((create) => client.createAuthMachineUser(create.request)), ...changeSet.machineUser.updates.map((update) => client.updateAuthMachineUser(update.request))]);
4985
5033
  };
5034
+ const applyUserProfileDeletes = async () => {
5035
+ await Promise.all(changeSet.userProfileConfig.deletes.map((del) => client.deleteUserProfileConfig(del.request)));
5036
+ };
4986
5037
  const applyCreateUpdateDependents = async () => {
4987
5038
  await applyAuthConnections(client, {
4988
5039
  changeSet: changeSet.connection,
@@ -5028,7 +5079,7 @@ async function applyAuth(client, result, phase = "create-update") {
5028
5079
  await Promise.all(changeSet.authHook.deletes.map((del) => client.deleteAuthHook(del.request)));
5029
5080
  await Promise.all(changeSet.machineUser.deletes.map((del) => client.deleteAuthMachineUser(del.request)));
5030
5081
  await Promise.all(changeSet.tenantConfig.deletes.map((del) => client.deleteTenantConfig(del.request)));
5031
- await Promise.all(changeSet.userProfileConfig.deletes.map((del) => client.deleteUserProfileConfig(del.request)));
5082
+ await applyUserProfileDeletes();
5032
5083
  await Promise.all(changeSet.idpConfig.deletes.map((del) => client.deleteAuthIDPConfig(del.request)));
5033
5084
  await applyAuthConnections(client, {
5034
5085
  changeSet: changeSet.connection,
@@ -7476,7 +7527,8 @@ function formatDiffChange(change) {
7476
7527
  return ` + ${change.fieldName}: ${typeStr}`;
7477
7528
  }
7478
7529
  case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
7479
- case "field_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
7530
+ case "field_modified":
7531
+ case "field_type_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
7480
7532
  case "index_added": return ` + [Index] ${change.indexName}`;
7481
7533
  case "index_removed": return ` - [Index] ${change.indexName}`;
7482
7534
  case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
@@ -7573,6 +7625,7 @@ const DIFF_CHANGE_LABELS = {
7573
7625
  field_added: "field(s) added",
7574
7626
  field_removed: "field(s) removed",
7575
7627
  field_modified: "field(s) modified",
7628
+ field_type_modified: "field type(s) modified",
7576
7629
  index_added: "index(es) added",
7577
7630
  index_removed: "index(es) removed",
7578
7631
  index_modified: "index(es) modified",
@@ -7598,32 +7651,108 @@ function formatDiffSummary(diff) {
7598
7651
  }
7599
7652
 
7600
7653
  //#endregion
7601
- //#region src/cli/commands/tailordb/migrate/migration-number.ts
7654
+ //#region src/cli/commands/tailordb/migrate/field-type-change.ts
7602
7655
  /**
7603
- * Format migration number as 4-digit string.
7604
- * @param num - Migration number
7605
- * @returns 4-digit padded string
7656
+ * Scalar type changes whose complete source domain is accepted by the target
7657
+ * representation without relying on per-row index recreation.
7658
+ *
7659
+ * Keep this list deliberately narrow. Pair-specific platform experiments can
7660
+ * extend it once both indexed and unindexed read paths have been verified.
7661
+ */
7662
+ const IN_PLACE_TYPE_CHANGES = /* @__PURE__ */ new Set([
7663
+ "decimal:string",
7664
+ "enum:string",
7665
+ "integer:float",
7666
+ "uuid:string"
7667
+ ]);
7668
+ /**
7669
+ * Determine whether a field type change can use a single phased migration.
7670
+ * @param before - Previous field configuration
7671
+ * @param after - Target field configuration
7672
+ * @returns Whether the migration can normalize data before applying the type
7606
7673
  */
7607
- function formatMigrationNumber(num) {
7608
- return num.toString().padStart(4, "0");
7674
+ function supportsInPlaceFieldTypeChange(before, after) {
7675
+ if (before.type === after.type) return false;
7676
+ if (before.array || after.array) return false;
7677
+ if (before.type === "nested" || after.type === "nested") return false;
7678
+ if (before.serial || after.serial) return false;
7679
+ if (before.vector || after.vector) return false;
7680
+ if (before.foreignKey || after.foreignKey) return false;
7681
+ return IN_PLACE_TYPE_CHANGES.has(`${before.type}:${after.type}`);
7609
7682
  }
7683
+
7684
+ //#endregion
7685
+ //#region src/cli/commands/tailordb/migrate/types.ts
7610
7686
  /**
7611
- * Parse a migration number CLI argument.
7612
- *
7613
- * Accepts the canonical 4-digit form ("0001") or a bare integer without
7614
- * leading zeros ("0"–"9999"). Commands that disallow the baseline reject
7615
- * 0 themselves with a context-specific message.
7616
- * @param numberStr - Raw CLI argument
7617
- * @returns Parsed migration number
7687
+ * Types for TailorDB migration execution
7618
7688
  */
7619
- function parseMigrationNumberArg(numberStr) {
7620
- if (/^\d{4}$/.test(numberStr)) return parseInt(numberStr, 10);
7621
- if (/^(0|[1-9]\d*)$/.test(numberStr)) {
7622
- const parsed = parseInt(numberStr, 10);
7623
- if (parsed > 9999) throw new Error(`Migration number ${numberStr} is out of range. Expected 0-9999.`);
7624
- return parsed;
7689
+ /**
7690
+ * Maximum length for Kubernetes label values
7691
+ * Labels must match pattern: ^[a-z][a-z0-9_-]{0,62}
7692
+ */
7693
+ const MAX_LABEL_LENGTH = 63;
7694
+ /**
7695
+ * Prefix added to migration numbers in labels (required because migration names start with numbers)
7696
+ */
7697
+ const MIGRATION_LABEL_PREFIX = "m";
7698
+ /**
7699
+ * Label key for storing migration state in TailorDB Service metadata
7700
+ */
7701
+ const MIGRATION_LABEL_KEY = "sdk-migration";
7702
+ /** Label key identifying which migration history ID is deployed. */
7703
+ const MIGRATION_HISTORY_LABEL_KEY = "sdk-migration-history";
7704
+ /** Valid migration history ID syntax for metadata label values. */
7705
+ const MIGRATION_HISTORY_ID_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
7706
+ /**
7707
+ * Sanitize migration number for use as label value
7708
+ * Label pattern: ^[a-z][a-z0-9_-]{0,62}
7709
+ * - Must start with lowercase letter (add prefix since migration numbers start with digits)
7710
+ * - Max 63 characters
7711
+ * @param {number} migrationNumber - Migration number to sanitize
7712
+ * @returns {string} Sanitized label value
7713
+ */
7714
+ function sanitizeMigrationLabel(migrationNumber) {
7715
+ return (MIGRATION_LABEL_PREFIX + formatMigrationNumber(migrationNumber)).slice(0, 63);
7716
+ }
7717
+ /**
7718
+ * Parse migration number from label value
7719
+ * @param {string} label - Label value (e.g., "m0001")
7720
+ * @returns {number | null} Parsed number or null if invalid
7721
+ */
7722
+ function parseMigrationLabelNumber(label) {
7723
+ if (!label.startsWith(MIGRATION_LABEL_PREFIX)) return null;
7724
+ const numStr = label.slice(1);
7725
+ if (!/^\d+$/.test(numStr)) return null;
7726
+ const num = parseInt(numStr, 10);
7727
+ return num > 9999 ? null : num;
7728
+ }
7729
+ /**
7730
+ * Parse and validate a migration history ID stored in metadata.
7731
+ * @param label - Metadata label value
7732
+ * @returns Valid history ID, or null for malformed input
7733
+ */
7734
+ function parseMigrationHistoryId(label) {
7735
+ return MIGRATION_HISTORY_ID_PATTERN.test(label) ? label : null;
7736
+ }
7737
+ /**
7738
+ * Create a migration history ID that is valid as a metadata label value.
7739
+ * @returns New migration history ID
7740
+ */
7741
+ function createMigrationHistoryId() {
7742
+ return `h${randomUUID().replaceAll("-", "")}`;
7743
+ }
7744
+ /**
7745
+ * Handle optional-to-required field change error with helpful message
7746
+ * @param {unknown} error - Error to handle
7747
+ * @param {string[]} messages - Additional messages to display
7748
+ */
7749
+ function handleOptionalToRequiredError(error, messages) {
7750
+ if (error instanceof ConnectError && error.code === Code.FailedPrecondition && error.message.includes("cannot be updated from non-required to required when records exist")) {
7751
+ logger.error("Schema change failed: Cannot change field from optional to required when records exist.");
7752
+ logger.newline();
7753
+ for (const message of messages) logger.info(message);
7625
7754
  }
7626
- throw new Error(`Invalid migration number format: ${numberStr}. Expected 4-digit format (e.g., 0001) or integer 0-9999 (e.g., 1).`);
7755
+ throw error;
7627
7756
  }
7628
7757
 
7629
7758
  //#endregion
@@ -7781,11 +7910,17 @@ const tailorDBSnapshotTypeSchema = z.looseObject({
7781
7910
  gql: snapshotGqlPermissionSchema.optional()
7782
7911
  }).optional()
7783
7912
  });
7913
+ const rebaselineMarkerSchema = z.looseObject({
7914
+ historyId: z.string().regex(MIGRATION_HISTORY_ID_PATTERN),
7915
+ replacedHistoryId: z.string().regex(MIGRATION_HISTORY_ID_PATTERN).nullable(),
7916
+ replacedLatestMigration: z.number().int().min(0).max(9999)
7917
+ });
7784
7918
  const schemaSnapshotSchema = z.looseObject({
7785
7919
  version: z.number(),
7786
7920
  namespace: z.string(),
7787
7921
  createdAt: z.string(),
7788
- types: snapshotRecordSchema(tailorDBSnapshotTypeSchema)
7922
+ types: snapshotRecordSchema(tailorDBSnapshotTypeSchema),
7923
+ rebaseline: rebaselineMarkerSchema.optional()
7789
7924
  });
7790
7925
  const typeSettingsPatchSchema = z.looseObject({
7791
7926
  indexes: snapshotRecordSchema(snapshotIndexConfigSchema).optional(),
@@ -7858,6 +7993,14 @@ const fieldModifiedChangeSchema = z.looseObject({
7858
7993
  before: snapshotFieldConfigSchema,
7859
7994
  after: snapshotFieldConfigSchema
7860
7995
  });
7996
+ const fieldTypeModifiedChangeSchema = z.looseObject({
7997
+ kind: z.literal("field_type_modified"),
7998
+ typeName: z.string(),
7999
+ reason: z.string().optional(),
8000
+ fieldName: z.string(),
8001
+ before: snapshotFieldConfigSchema,
8002
+ after: snapshotFieldConfigSchema
8003
+ });
7861
8004
  const indexAddedChangeSchema = z.looseObject({
7862
8005
  kind: z.literal("index_added"),
7863
8006
  typeName: z.string(),
@@ -7956,6 +8099,7 @@ const diffChangeSchema = z.discriminatedUnion("kind", [
7956
8099
  fieldAddedChangeSchema,
7957
8100
  fieldRemovedChangeSchema,
7958
8101
  fieldModifiedChangeSchema,
8102
+ fieldTypeModifiedChangeSchema,
7959
8103
  indexAddedChangeSchema,
7960
8104
  indexRemovedChangeSchema,
7961
8105
  indexModifiedChangeSchema,
@@ -8026,6 +8170,8 @@ const SCHEMA_FILE_NAME = "schema.json";
8026
8170
  const DIFF_FILE_NAME = "diff.json";
8027
8171
  /** File name for migration script. */
8028
8172
  const MIGRATE_FILE_NAME = "migrate.ts";
8173
+ /** File name for migration script unit test. */
8174
+ const MIGRATE_TEST_FILE_NAME = "migrate.test.ts";
8029
8175
  /** File name for generated DB type definitions. */
8030
8176
  const DB_TYPES_FILE_NAME = "db.ts";
8031
8177
  /**
@@ -8038,6 +8184,19 @@ const MIGRATION_NUMBER_PATTERN = /^\d{4}$/;
8038
8184
  * Must stay in sync with the platform's default decimal scale.
8039
8185
  */
8040
8186
  const DEFAULT_DECIMAL_SCALE = 6;
8187
+ var UnsupportedMigrationFileVersionError = class extends Error {};
8188
+ function assertSupportedMigrationFileVersion(filePath, raw) {
8189
+ if (typeof raw !== "object" || raw === null || !("version" in raw)) return;
8190
+ const version = raw.version;
8191
+ if (typeof version !== "number") return;
8192
+ if (Number.isInteger(version) && version >= 1 && version <= 2) return;
8193
+ const supportedRange = `${1}-${2}`;
8194
+ let guidance;
8195
+ if (version > 2) guidance = `Upgrade to an SDK that supports migration file format version ${version}.`;
8196
+ else if (version < 1) guidance = "Re-baseline with an SDK that still supports this migration history, then upgrade the SDK.";
8197
+ else guidance = "Restore the migration file from version control or regenerate it.";
8198
+ throw new UnsupportedMigrationFileVersionError(`Unsupported migration file format version ${version} at ${filePath}. This SDK supports migration file format versions ${supportedRange}. ${guidance}`);
8199
+ }
8041
8200
  function createSnapshotRecord() {
8042
8201
  return Object.create(null);
8043
8202
  }
@@ -8113,6 +8272,7 @@ const MIGRATION_FILE_NAMES = {
8113
8272
  schema: SCHEMA_FILE_NAME,
8114
8273
  diff: DIFF_FILE_NAME,
8115
8274
  migrate: MIGRATE_FILE_NAME,
8275
+ test: MIGRATE_TEST_FILE_NAME,
8116
8276
  db: DB_TYPES_FILE_NAME
8117
8277
  };
8118
8278
  /**
@@ -8278,7 +8438,7 @@ function createSnapshotFromLocalTypes(types, namespace) {
8278
8438
  const snapshotTypes = createSnapshotRecord();
8279
8439
  for (const [typeName, type] of Object.entries(types)) snapshotTypes[typeName] = createSnapshotType(type);
8280
8440
  return normalizeSchemaSnapshot({
8281
- version: 1,
8441
+ version: 2,
8282
8442
  namespace,
8283
8443
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8284
8444
  types: snapshotTypes
@@ -8297,6 +8457,7 @@ function loadSnapshot(filePath) {
8297
8457
  } catch (error) {
8298
8458
  throw new Error(`Invalid schema snapshot at ${filePath}: ${String(error)}`, { cause: error });
8299
8459
  }
8460
+ assertSupportedMigrationFileVersion(filePath, raw);
8300
8461
  const result = schemaSnapshotSchema.safeParse(raw);
8301
8462
  if (!result.success) throw new Error(`Invalid schema snapshot at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
8302
8463
  const snapshot = result.data;
@@ -8315,16 +8476,38 @@ function loadDiff(filePath) {
8315
8476
  } catch (error) {
8316
8477
  throw new Error(`Invalid migration diff at ${filePath}: ${String(error)}`, { cause: error });
8317
8478
  }
8479
+ assertSupportedMigrationFileVersion(filePath, raw);
8318
8480
  const result = migrationDiffSchema.safeParse(raw);
8319
8481
  if (!result.success) throw new Error(`Invalid migration diff at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
8320
8482
  const parsed = result.data;
8321
- const warnings = parsed.warnings ?? [];
8483
+ const warnings = parsed.warnings ?? deriveWarningsFromChanges(parsed);
8322
8484
  return {
8323
8485
  ...parsed,
8324
8486
  warnings,
8325
8487
  hasWarnings: warnings.length > 0
8326
8488
  };
8327
8489
  }
8490
+ const FIELD_REMOVED_WARNING_REASON = "Field removed (existing data will no longer be accessible through the schema)";
8491
+ const TYPE_REMOVED_WARNING_REASON = "Type removed (all records of this type will be deleted during post-migration cleanup)";
8492
+ /**
8493
+ * Reconstruct data-loss warnings from removal changes for diff.json files
8494
+ * written before the warning tier existed
8495
+ * @param {MigrationDiff} diff - Parsed legacy migration diff
8496
+ * @returns {WarningChangeInfo[]} Warnings equivalent to what diff generation would have recorded
8497
+ */
8498
+ function deriveWarningsFromChanges(diff) {
8499
+ const warnings = [];
8500
+ for (const change of diff.changes) if (change.kind === "field_removed") warnings.push({
8501
+ typeName: change.typeName,
8502
+ fieldName: change.fieldName,
8503
+ reason: FIELD_REMOVED_WARNING_REASON
8504
+ });
8505
+ else if (change.kind === "type_removed") warnings.push({
8506
+ typeName: change.typeName,
8507
+ reason: TYPE_REMOVED_WARNING_REASON
8508
+ });
8509
+ return warnings;
8510
+ }
8328
8511
  /**
8329
8512
  * Get all migration directories and their files, sorted by number
8330
8513
  * @param {string} migrationsDir - Migrations directory path
@@ -8415,7 +8598,8 @@ function applyDiffToSnapshot(snapshot, diff) {
8415
8598
  break;
8416
8599
  }
8417
8600
  case "field_added":
8418
- case "field_modified": {
8601
+ case "field_modified":
8602
+ case "field_type_modified": {
8419
8603
  const existing = types[change.typeName];
8420
8604
  if (existing) {
8421
8605
  const fields = copySnapshotRecord(existing.fields);
@@ -8688,13 +8872,18 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
8688
8872
  fieldName,
8689
8873
  reason: "Required field added"
8690
8874
  });
8691
- if (oldField && newField && oldField.type !== newField.type) breakingChanges.push({
8692
- typeName,
8693
- fieldName,
8694
- reason: `Field type changed from ${oldField.type} to ${newField.type}`,
8695
- unsupported: true,
8696
- showThreeStepHint: true
8697
- });
8875
+ if (oldField && newField && oldField.type !== newField.type) {
8876
+ const supported = supportsInPlaceFieldTypeChange(oldField, newField);
8877
+ breakingChanges.push({
8878
+ typeName,
8879
+ fieldName,
8880
+ reason: `Field type changed from ${oldField.type} to ${newField.type}`,
8881
+ ...!supported && {
8882
+ unsupported: true,
8883
+ showThreeStepHint: true
8884
+ }
8885
+ });
8886
+ }
8698
8887
  if (oldField && newField && !oldField.required && newField.required) breakingChanges.push({
8699
8888
  typeName,
8700
8889
  fieldName,
@@ -8754,7 +8943,7 @@ function addChange(ctx, change, oldField, newField) {
8754
8943
  if (change.kind === "field_removed") ctx.warnings.push({
8755
8944
  typeName: change.typeName,
8756
8945
  fieldName: change.fieldName,
8757
- reason: "Field removed (existing data will be dropped in the post-migration phase)"
8946
+ reason: FIELD_REMOVED_WARNING_REASON
8758
8947
  });
8759
8948
  }
8760
8949
  function compareTypeFields(ctx, typeName, prevType, currType) {
@@ -8783,7 +8972,7 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
8783
8972
  const prevField = assertDefined(prevType.fields[fieldName], `field "${fieldName}" missing from prevType`);
8784
8973
  const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
8785
8974
  if (areFieldsDifferent(prevField, currField)) addChange(ctx, {
8786
- kind: "field_modified",
8975
+ kind: prevField.type === currField.type ? "field_modified" : "field_type_modified",
8787
8976
  typeName,
8788
8977
  fieldName,
8789
8978
  before: prevField,
@@ -9105,7 +9294,7 @@ function compareNormalizedSnapshots(previous, current) {
9105
9294
  });
9106
9295
  ctx.warnings.push({
9107
9296
  typeName,
9108
- reason: "Type removed (all records of this type will be dropped in the post-migration phase)"
9297
+ reason: TYPE_REMOVED_WARNING_REASON
9109
9298
  });
9110
9299
  }
9111
9300
  for (const typeName of currentTypeNames) {
@@ -9122,7 +9311,7 @@ function compareNormalizedSnapshots(previous, current) {
9122
9311
  comparePermissions(ctx, typeName, prevType.permissions?.record, currType.permissions?.record, prevType.permissions?.gql, currType.permissions?.gql);
9123
9312
  }
9124
9313
  return {
9125
- version: 1,
9314
+ version: 2,
9126
9315
  namespace: current.namespace,
9127
9316
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9128
9317
  changes: ctx.changes,
@@ -9146,7 +9335,7 @@ function compareNormalizedSnapshots(previous, current) {
9146
9335
  */
9147
9336
  function compareLocalTypesWithSnapshot(snapshot, localTypes, namespace) {
9148
9337
  return compareSnapshots(snapshot, {
9149
- version: 1,
9338
+ version: 2,
9150
9339
  namespace,
9151
9340
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9152
9341
  types: localTypes
@@ -9465,7 +9654,7 @@ function createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissi
9465
9654
  };
9466
9655
  }
9467
9656
  return normalizeSchemaSnapshot({
9468
- version: 1,
9657
+ version: 2,
9469
9658
  namespace,
9470
9659
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9471
9660
  types
@@ -9721,7 +9910,8 @@ function schemaDriftFromDiffChange(change) {
9721
9910
  fieldName: change.fieldName,
9722
9911
  details: `Field '${change.fieldName}' exists in remote but not in snapshot`
9723
9912
  };
9724
- case "field_modified": return fieldDriftFromChange(change);
9913
+ case "field_modified":
9914
+ case "field_type_modified": return fieldDriftFromChange(change);
9725
9915
  case "index_added": return {
9726
9916
  typeName: change.typeName,
9727
9917
  kind: "index_missing_remote",
@@ -9822,6 +10012,95 @@ function formatSchemaDrifts(drifts) {
9822
10012
  return lines.join("\n");
9823
10013
  }
9824
10014
 
10015
+ //#endregion
10016
+ //#region src/cli/commands/tailordb/migrate/file-state.ts
10017
+ const MIGRATION_FILE_KINDS = [
10018
+ "schema",
10019
+ "diff",
10020
+ "migrate",
10021
+ "db"
10022
+ ];
10023
+ function getMigrationArtifactNumbers(migrationsDir) {
10024
+ if (!fs$2.existsSync(migrationsDir)) return [];
10025
+ return fs$2.readdirSync(migrationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && MIGRATION_NUMBER_PATTERN.test(entry.name)).map((entry) => Number.parseInt(entry.name, 10)).filter((migrationNumber) => MIGRATION_FILE_KINDS.some((kind) => fs$2.existsSync(getMigrationFilePath(migrationsDir, migrationNumber, kind)))).toSorted((a, b) => a - b);
10026
+ }
10027
+ function updateHashWithDirectory(hash, directoryPath, prefix = "", activeDirectories = /* @__PURE__ */ new Set()) {
10028
+ const realDirectoryPath = fs$2.realpathSync(directoryPath);
10029
+ if (activeDirectories.has(realDirectoryPath)) {
10030
+ hash.update(`directory-cycle\0${prefix}\0${realDirectoryPath}\0`);
10031
+ return;
10032
+ }
10033
+ activeDirectories.add(realDirectoryPath);
10034
+ const entries = fs$2.readdirSync(directoryPath, { withFileTypes: true }).toSorted((a, b) => a.name.localeCompare(b.name));
10035
+ try {
10036
+ for (const entry of entries) {
10037
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
10038
+ const entryPath = path.join(directoryPath, entry.name);
10039
+ if (entry.isDirectory()) {
10040
+ hash.update(`directory\0${relativePath}\0`);
10041
+ updateHashWithDirectory(hash, entryPath, relativePath, activeDirectories);
10042
+ } else if (entry.isFile()) {
10043
+ hash.update(`file\0${relativePath}\0`);
10044
+ hash.update(fs$2.readFileSync(entryPath));
10045
+ hash.update("\0");
10046
+ } else if (entry.isSymbolicLink()) {
10047
+ hash.update(`symlink\0${relativePath}\0${fs$2.readlinkSync(entryPath)}\0`);
10048
+ try {
10049
+ const target = fs$2.statSync(entryPath);
10050
+ if (target.isDirectory()) {
10051
+ hash.update("target-directory\0");
10052
+ updateHashWithDirectory(hash, entryPath, `${relativePath}@target`, activeDirectories);
10053
+ } else if (target.isFile()) {
10054
+ hash.update("target-file\0");
10055
+ hash.update(fs$2.readFileSync(entryPath));
10056
+ hash.update("\0");
10057
+ } else hash.update("target-other\0");
10058
+ } catch (error) {
10059
+ if (error.code !== "ENOENT") throw error;
10060
+ hash.update("target-missing\0");
10061
+ }
10062
+ } else hash.update(`other\0${relativePath}\0`);
10063
+ }
10064
+ } finally {
10065
+ activeDirectories.delete(realDirectoryPath);
10066
+ }
10067
+ }
10068
+ /**
10069
+ * Capture an exact set of input files.
10070
+ * @param filePaths - Files whose names and contents affect an operation
10071
+ * @returns SHA-256 digest of the sorted file set
10072
+ */
10073
+ function captureFileState(filePaths) {
10074
+ const hash = createHash("sha256");
10075
+ for (const filePath of [...new Set(filePaths)].toSorted()) {
10076
+ hash.update(filePath);
10077
+ hash.update("\0");
10078
+ if (fs$2.existsSync(filePath)) hash.update(fs$2.readFileSync(filePath));
10079
+ else hash.update("<missing>");
10080
+ hash.update("\0");
10081
+ }
10082
+ return hash.digest("hex");
10083
+ }
10084
+ /**
10085
+ * Capture the migration files that an operation depends on.
10086
+ * @param namespacesWithMigrations - Configured migration directories by namespace
10087
+ * @returns SHA-256 digest by namespace
10088
+ */
10089
+ function captureMigrationFileState(namespacesWithMigrations) {
10090
+ const state = Object.create(null);
10091
+ for (const { namespace, migrationsDir } of namespacesWithMigrations.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
10092
+ const hash = createHash("sha256");
10093
+ const migrationNumbers = getMigrationArtifactNumbers(migrationsDir);
10094
+ for (const migrationNumber of migrationNumbers) {
10095
+ const migrationDirectoryName = formatMigrationNumber(migrationNumber);
10096
+ hash.update(`migration\0${migrationDirectoryName}\0`);
10097
+ updateHashWithDirectory(hash, path.join(migrationsDir, migrationDirectoryName));
10098
+ }
10099
+ state[namespace] = hash.digest("hex");
10100
+ }
10101
+ return state;
10102
+ }
10103
+
9825
10104
  //#endregion
9826
10105
  //#region src/cli/commands/tailordb/migrate/snapshot-manifest.ts
9827
10106
  /**
@@ -9859,11 +10138,7 @@ function generateTailorDBTypeManifestFromSnapshot(snapshotType, options = {}) {
9859
10138
  delete: ops.delete === false,
9860
10139
  read: ops.read === false
9861
10140
  };
9862
- const fields = {};
9863
- for (const [fieldName, fieldConfig] of Object.entries(snapshotType.fields)) {
9864
- if (fieldName === "id") continue;
9865
- fields[fieldName] = convertFieldConfigToProto(fieldConfig);
9866
- }
10141
+ const fields = Object.fromEntries(Object.entries(snapshotType.fields).filter(([fieldName]) => fieldName !== "id").map(([fieldName, fieldConfig]) => [fieldName, convertFieldConfigToProto(fieldConfig)]));
9867
10142
  const relationships = {};
9868
10143
  if (snapshotType.forwardRelationships) for (const [relationName, rel] of Object.entries(snapshotType.forwardRelationships)) relationships[relationName] = convertRelationshipToProto(rel, "forward");
9869
10144
  if (snapshotType.backwardRelationships) for (const [relationName, rel] of Object.entries(snapshotType.backwardRelationships)) relationships[relationName] = convertRelationshipToProto(rel, "backward");
@@ -10256,6 +10531,8 @@ function protoGqlOperand(operand) {
10256
10531
  * - `field_added` with `required: true`: relax to `required: false`.
10257
10532
  * - `field_modified` optional→required, unique constraint added, enum
10258
10533
  * value removed: keep the looser side until Post-phase.
10534
+ * - `field_type_modified`: keep the complete previous field config until
10535
+ * Post-phase so migrate.ts runs against the previous type contract.
10259
10536
  *
10260
10537
  * and the type-level index adjustments:
10261
10538
  *
@@ -10275,6 +10552,7 @@ function protoGqlOperand(operand) {
10275
10552
  const PRE_MIGRATION_FIELD_KINDS = /* @__PURE__ */ new Set([
10276
10553
  "field_added",
10277
10554
  "field_modified",
10555
+ "field_type_modified",
10278
10556
  "field_removed"
10279
10557
  ]);
10280
10558
  /**
@@ -10287,6 +10565,38 @@ function isPreMigrationFieldChange(change) {
10287
10565
  return PRE_MIGRATION_FIELD_KINDS.has(change.kind);
10288
10566
  }
10289
10567
  /**
10568
+ * Create the type snapshot used to build a Pre-phase manifest.
10569
+ *
10570
+ * This adjustment happens before manifest generation because field hooks and
10571
+ * validators are aggregated into type-level scripts by the manifest builder.
10572
+ * Replacing only the generated field proto would leave those scripts on the
10573
+ * target field contract while migrate.ts still runs against the previous one.
10574
+ * @param snapshotType - Final snapshot state for this migration
10575
+ * @param typeChanges - Field changes for this type, keyed by field name
10576
+ * @param typeScriptsChange - Type-level scripts changed by the same migration
10577
+ * @returns A snapshot with Pre-phase field contracts
10578
+ */
10579
+ function createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsChange) {
10580
+ const fields = structuredClone(snapshotType.fields);
10581
+ let hasFieldTypeChange = false;
10582
+ for (const [fieldName, change] of typeChanges) {
10583
+ if (change.kind !== "field_type_modified") continue;
10584
+ hasFieldTypeChange = true;
10585
+ fields[fieldName] = structuredClone(change.before);
10586
+ }
10587
+ const preSnapshotType = {
10588
+ ...snapshotType,
10589
+ fields
10590
+ };
10591
+ if (!hasFieldTypeChange || !typeScriptsChange) return preSnapshotType;
10592
+ const { typeHookExpr: _targetHook, typeValidateExpr: _targetValidate, ...withoutTypeScripts } = preSnapshotType;
10593
+ return {
10594
+ ...withoutTypeScripts,
10595
+ ...typeScriptsChange.before.typeHookExpr && { typeHookExpr: structuredClone(typeScriptsChange.before.typeHookExpr) },
10596
+ ...typeScriptsChange.before.typeValidateExpr !== void 0 && { typeValidateExpr: typeScriptsChange.before.typeValidateExpr }
10597
+ };
10598
+ }
10599
+ /**
10290
10600
  * Build a map of field changes that require pre-migration schema adjustment.
10291
10601
  * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
10292
10602
  * @returns {PreMigrationChangesMap} Map of changes keyed by typeName/fieldName
@@ -10327,6 +10637,10 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
10327
10637
  if (change.after.required) field.required = false;
10328
10638
  continue;
10329
10639
  }
10640
+ if (change.kind === "field_type_modified") {
10641
+ fields[fieldName] = convertFieldConfigToProto(change.before);
10642
+ continue;
10643
+ }
10330
10644
  const { before, after } = change;
10331
10645
  if (!before.required && after.required) field.required = false;
10332
10646
  if (!(before.unique ?? false) && (after.unique ?? false)) field.unique = false;
@@ -10385,83 +10699,72 @@ function applyPreMigrationIndexAdjustments(indexes, typeIndexChanges) {
10385
10699
  }
10386
10700
 
10387
10701
  //#endregion
10388
- //#region src/cli/commands/tailordb/migrate/types.ts
10389
- /**
10390
- * Types for TailorDB migration execution
10391
- */
10702
+ //#region src/cli/commands/tailordb/migrate/remote-state.ts
10392
10703
  /**
10393
- * Maximum length for Kubernetes label values
10394
- * Labels must match pattern: ^[a-z][a-z0-9_-]{0,62}
10704
+ * Fetch the namespace's migration checkpoint and history ID.
10705
+ * @param client - Operator client
10706
+ * @param trn - Namespace TRN
10707
+ * @returns Parsed migration state with invalid history labels kept distinct from missing labels
10395
10708
  */
10396
- const MAX_LABEL_LENGTH = 63;
10709
+ async function fetchRemoteMigrationState(client, trn) {
10710
+ const metadata = await getOrNull(async () => {
10711
+ const { metadata } = await client.getMetadata({ trn });
10712
+ return metadata;
10713
+ });
10714
+ if (!metadata) return {
10715
+ metadataExists: false,
10716
+ number: null,
10717
+ historyId: null,
10718
+ historyIdInvalid: false
10719
+ };
10720
+ const migrationLabel = metadata.labels[MIGRATION_LABEL_KEY];
10721
+ const historyLabel = metadata.labels[MIGRATION_HISTORY_LABEL_KEY];
10722
+ const historyId = historyLabel ? parseMigrationHistoryId(historyLabel) : null;
10723
+ return {
10724
+ metadataExists: true,
10725
+ number: migrationLabel ? parseMigrationLabelNumber(migrationLabel) : null,
10726
+ historyId,
10727
+ historyIdInvalid: historyLabel !== void 0 && historyId === null
10728
+ };
10729
+ }
10397
10730
  /**
10398
- * Prefix added to migration numbers in labels (required because migration names start with numbers)
10731
+ * Fetch the namespace's current migration number from its metadata labels.
10732
+ *
10733
+ * Only a namespace that has not been deployed yet reads as "no current
10734
+ * migration". Every other lookup failure must propagate: treating it as
10735
+ * unset would misreport a transient error as "no checkpoint", showing
10736
+ * every migration as pending to callers.
10737
+ * @param client - Operator client
10738
+ * @param trn - Namespace TRN
10739
+ * @returns Parsed current migration number, or null when unset or unparseable
10399
10740
  */
10400
- const MIGRATION_LABEL_PREFIX = "m";
10741
+ async function fetchRemoteMigrationNumber(client, trn) {
10742
+ return (await fetchRemoteMigrationState(client, trn)).number;
10743
+ }
10744
+
10745
+ //#endregion
10746
+ //#region src/cli/commands/tailordb/migrate/schema-checks.ts
10401
10747
  /**
10402
- * Label key for storing migration state in TailorDB Service metadata
10748
+ * Schema validation checks shared between `deploy` and
10749
+ * `tailordb migration validate`.
10750
+ *
10751
+ * These checks are read-only: they compare local type definitions, the
10752
+ * migration snapshot history, and the remote schema without mutating any
10753
+ * state.
10403
10754
  */
10404
- const MIGRATION_LABEL_KEY = "sdk-migration";
10405
10755
  /**
10406
- * Sanitize migration number for use as label value
10407
- * Label pattern: ^[a-z][a-z0-9_-]{0,62}
10408
- * - Must start with lowercase letter (add prefix since migration numbers start with digits)
10409
- * - Max 63 characters
10410
- * @param {number} migrationNumber - Migration number to sanitize
10411
- * @returns {string} Sanitized label value
10756
+ * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
10757
+ * @param service - Loaded TailorDB service (after `loadTypes()`)
10758
+ * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
10412
10759
  */
10413
- function sanitizeMigrationLabel(migrationNumber) {
10414
- return (MIGRATION_LABEL_PREFIX + formatMigrationNumber(migrationNumber)).slice(0, 63);
10415
- }
10416
- /**
10417
- * Parse migration number from label value
10418
- * @param {string} label - Label value (e.g., "m0001")
10419
- * @returns {number | null} Parsed number or null if invalid
10420
- */
10421
- function parseMigrationLabelNumber(label) {
10422
- if (!label.startsWith(MIGRATION_LABEL_PREFIX)) return null;
10423
- const numStr = label.slice(1);
10424
- if (!/^\d+$/.test(numStr)) return null;
10425
- const num = parseInt(numStr, 10);
10426
- return num > 9999 ? null : num;
10427
- }
10428
- /**
10429
- * Handle optional-to-required field change error with helpful message
10430
- * @param {unknown} error - Error to handle
10431
- * @param {string[]} messages - Additional messages to display
10432
- */
10433
- function handleOptionalToRequiredError(error, messages) {
10434
- if (error instanceof ConnectError && error.code === Code.FailedPrecondition && error.message.includes("cannot be updated from non-required to required when records exist")) {
10435
- logger.error("Schema change failed: Cannot change field from optional to required when records exist.");
10436
- logger.newline();
10437
- for (const message of messages) logger.info(message);
10438
- }
10439
- throw error;
10440
- }
10441
-
10442
- //#endregion
10443
- //#region src/cli/commands/tailordb/migrate/schema-checks.ts
10444
- /**
10445
- * Schema validation checks shared between `deploy` and
10446
- * `tailordb migration validate`.
10447
- *
10448
- * These checks are read-only: they compare local type definitions, the
10449
- * migration snapshot history, and the remote schema without mutating any
10450
- * state.
10451
- */
10452
- /**
10453
- * Convert a runtime TailorDBService to the snapshot-shaped deploy input.
10454
- * @param service - Loaded TailorDB service (after `loadTypes()`)
10455
- * @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
10456
- */
10457
- function toTailorDBDeployInput(service) {
10458
- const types = {};
10459
- for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
10460
- return {
10461
- namespace: service.namespace,
10462
- config: service.config,
10463
- types
10464
- };
10760
+ function toTailorDBDeployInput(service) {
10761
+ const types = {};
10762
+ for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
10763
+ return {
10764
+ namespace: service.namespace,
10765
+ config: service.config,
10766
+ types
10767
+ };
10465
10768
  }
10466
10769
  /**
10467
10770
  * Fetch all TailorDB types from remote for a namespace
@@ -10492,6 +10795,17 @@ async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
10492
10795
  return [permissions, nextPageToken];
10493
10796
  });
10494
10797
  }
10798
+ /**
10799
+ * Fetch a namespace's deployed schema as a normalized snapshot.
10800
+ * @param client - Operator client instance
10801
+ * @param workspaceId - Workspace ID
10802
+ * @param namespace - TailorDB namespace
10803
+ * @returns The deployed schema snapshot
10804
+ */
10805
+ async function fetchRemoteSchemaSnapshot(client, workspaceId, namespace) {
10806
+ const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
10807
+ return createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissions);
10808
+ }
10495
10809
  function definedWhenNotEmpty(value) {
10496
10810
  return Object.keys(value).length > 0 ? value : void 0;
10497
10811
  }
@@ -10547,36 +10861,6 @@ function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10547
10861
  types
10548
10862
  };
10549
10863
  }
10550
- async function getRemoteMigrationNumberState(client, workspaceId, namespace) {
10551
- const trn = resourceTrn(workspaceId, "tailordb", namespace);
10552
- const metadata = await getOrNull(async () => {
10553
- const { metadata } = await client.getMetadata({ trn });
10554
- return metadata;
10555
- });
10556
- if (!metadata) return {
10557
- metadataExists: false,
10558
- number: null
10559
- };
10560
- const label = metadata.labels[MIGRATION_LABEL_KEY];
10561
- if (!label) return {
10562
- metadataExists: true,
10563
- number: null
10564
- };
10565
- return {
10566
- metadataExists: true,
10567
- number: parseMigrationLabelNumber(label)
10568
- };
10569
- }
10570
- /**
10571
- * Get the current migration number from remote metadata
10572
- * @param {OperatorClient} client - Operator client instance
10573
- * @param {string} workspaceId - Workspace ID
10574
- * @param {string} namespace - TailorDB namespace
10575
- * @returns {Promise<number | null>} Current migration number, or null if no migration label exists
10576
- */
10577
- async function getRemoteMigrationNumber(client, workspaceId, namespace) {
10578
- return (await getRemoteMigrationNumberState(client, workspaceId, namespace)).number;
10579
- }
10580
10864
  /**
10581
10865
  * Verify remote schema matches the expected snapshot state
10582
10866
  * @param {OperatorClient} client - Operator client instance
@@ -10589,7 +10873,18 @@ async function getRemoteMigrationNumber(client, workspaceId, namespace) {
10589
10873
  async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
10590
10874
  const results = [];
10591
10875
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
10592
- const { metadataExists, number: remoteMigrationNumber } = await getRemoteMigrationNumberState(client, workspaceId, namespace);
10876
+ const remoteState = await fetchRemoteMigrationState(client, resourceTrn(workspaceId, "tailordb", namespace));
10877
+ const { metadataExists, number: remoteMigrationNumber } = remoteState;
10878
+ if (remoteState.historyIdInvalid || remoteMigrationNumber === null && remoteState.historyId !== null) {
10879
+ results.push({
10880
+ namespace,
10881
+ remoteMigrationNumber: remoteMigrationNumber ?? 0,
10882
+ drifts: [],
10883
+ hasDrift: false,
10884
+ checkpointMissingLocal: true
10885
+ });
10886
+ continue;
10887
+ }
10593
10888
  if (remoteMigrationNumber === null) {
10594
10889
  results.push({
10595
10890
  namespace,
@@ -10600,7 +10895,17 @@ async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations,
10600
10895
  });
10601
10896
  continue;
10602
10897
  }
10603
- if (remoteMigrationNumber > getLatestMigrationNumber(migrationsDir)) {
10898
+ const latestMigrationNumber = getLatestMigrationNumber(migrationsDir);
10899
+ const rebaseline = reconstructSnapshotFromMigrations(migrationsDir, 0)?.rebaseline;
10900
+ const historyMatchesCurrent = rebaseline ? remoteState.historyId === rebaseline.historyId : remoteState.historyId === null;
10901
+ const historyMatchesReplaced = rebaseline ? remoteState.historyId === rebaseline.replacedHistoryId : false;
10902
+ const checkpointRepair = rebaseline && historyMatchesReplaced && remoteMigrationNumber === rebaseline.replacedLatestMigration ? {
10903
+ from: remoteMigrationNumber,
10904
+ to: 0,
10905
+ fromHistoryId: remoteState.historyId,
10906
+ toHistoryId: rebaseline.historyId
10907
+ } : void 0;
10908
+ if (!historyMatchesCurrent && !checkpointRepair || remoteMigrationNumber > latestMigrationNumber && !checkpointRepair) {
10604
10909
  results.push({
10605
10910
  namespace,
10606
10911
  remoteMigrationNumber,
@@ -10610,7 +10915,7 @@ async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations,
10610
10915
  });
10611
10916
  continue;
10612
10917
  }
10613
- const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
10918
+ const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, checkpointRepair?.to ?? remoteMigrationNumber);
10614
10919
  if (!expectedSnapshot) {
10615
10920
  results.push({
10616
10921
  namespace,
@@ -10627,7 +10932,8 @@ async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations,
10627
10932
  namespace,
10628
10933
  remoteMigrationNumber,
10629
10934
  drifts,
10630
- hasDrift: drifts.length > 0
10935
+ hasDrift: drifts.length > 0,
10936
+ ...checkpointRepair && drifts.length === 0 ? { checkpointRepair } : {}
10631
10937
  });
10632
10938
  }
10633
10939
  return results;
@@ -10800,6 +11106,34 @@ async function bundleMigrationScript(sourceFile, namespace, migrationNumber, env
10800
11106
  };
10801
11107
  }
10802
11108
 
11109
+ //#endregion
11110
+ //#region src/cli/commands/tailordb/migrate/hints.ts
11111
+ /**
11112
+ * Copyable command hints for migration remediation
11113
+ */
11114
+ /**
11115
+ * Build the copyable `tailor tailordb migration script` command for
11116
+ * remediation hints, reproducing the current run's invocation context.
11117
+ * @param {MigrationScriptCommandOptions} options - Target migration and invocation context
11118
+ * @returns {string} Command line quoted for the current platform's shell
11119
+ */
11120
+ function formatMigrationScriptCommand(options) {
11121
+ const { migrationNumber, namespace, configPath, noScript } = options;
11122
+ const argv = [
11123
+ "tailor",
11124
+ "tailordb",
11125
+ "migration",
11126
+ "script",
11127
+ formatMigrationNumber(migrationNumber),
11128
+ "--namespace",
11129
+ namespace
11130
+ ];
11131
+ const configArg = formatConfigArg(configPath);
11132
+ if (configArg !== void 0) argv.push(configArg);
11133
+ if (noScript) argv.push("--no-script", "--reason", "<reason>");
11134
+ return formatCopyableCommand(argv);
11135
+ }
11136
+
10803
11137
  //#endregion
10804
11138
  //#region src/cli/shared/script-executor.ts
10805
11139
  /**
@@ -11007,7 +11341,7 @@ var Spinner = class {
11007
11341
  const frame = styles.info(FRAMES[this.#frame] ?? assertDefined(FRAMES[0], "spinner frames empty"));
11008
11342
  this.#frame = (this.#frame + 1) % FRAMES.length;
11009
11343
  const line = `${" ".repeat(this.#indent)}${frame} ${this.text}`;
11010
- this.#stream.write(line);
11344
+ this.#stream.write(renderFor(this.#stream, line));
11011
11345
  this.#stream.write(SYNC_END);
11012
11346
  const cols = this.#stream.columns || 80;
11013
11347
  this.#linesDrawn = Math.max(1, Math.ceil(visibleLength(line) / cols));
@@ -11024,7 +11358,7 @@ var Spinner = class {
11024
11358
  }
11025
11359
  #writeLine(content) {
11026
11360
  const indent = " ".repeat(this.#indent);
11027
- this.#stream.write(`${indent}${content}\n`);
11361
+ this.#stream.write(renderFor(this.#stream, `${indent}${content}\n`));
11028
11362
  }
11029
11363
  };
11030
11364
  /**
@@ -11069,12 +11403,13 @@ async function getCurrentMigrationNumber(client, workspaceId, namespace) {
11069
11403
  * @param {string} workspaceId - Workspace ID
11070
11404
  * @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
11071
11405
  * @param {string} [configPath] - Config file path, included in remediation guidance when provided
11406
+ * @param {ReadonlyMap<string, number>} [currentMigrationOverrides] - Confirmed current migration numbers to use instead of remote metadata
11072
11407
  * @returns {Promise<PendingMigration[]>} List of pending migrations
11073
11408
  */
11074
- async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations, configPath) {
11409
+ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrations, configPath, currentMigrationOverrides) {
11075
11410
  const pendingMigrations = [];
11076
11411
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
11077
- const currentMigration = await getCurrentMigrationNumber(client, workspaceId, namespace);
11412
+ const currentMigration = currentMigrationOverrides?.get(namespace) ?? await getCurrentMigrationNumber(client, workspaceId, namespace);
11078
11413
  const migrationFiles = getMigrationFiles(migrationsDir);
11079
11414
  for (const file of migrationFiles) {
11080
11415
  if (file.number <= currentMigration) continue;
@@ -11084,9 +11419,15 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
11084
11419
  const scriptPath = getMigrationFilePath(migrationsDir, file.number, "migrate");
11085
11420
  const hasScript = fs$2.existsSync(scriptPath);
11086
11421
  if (diff.requiresMigrationScript && !hasScript && !diff.scriptSkipped) {
11087
- const configArg = configPath ? ` --config "${path.relative(process.cwd(), configPath) || configPath}"` : "";
11088
- const scriptCmd = `tailor tailordb migration script ${file.number} --namespace ${namespace}${configArg}`;
11089
- 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>"`);
11422
+ const commandOptions = {
11423
+ migrationNumber: file.number,
11424
+ namespace,
11425
+ configPath
11426
+ };
11427
+ 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: ${formatMigrationScriptCommand(commandOptions)}\n - Or record that no script is needed: ${formatMigrationScriptCommand({
11428
+ ...commandOptions,
11429
+ noScript: true
11430
+ })}`);
11090
11431
  }
11091
11432
  if (diff.scriptSkipped) {
11092
11433
  const migrationLabel = `${namespace}/${formatMigrationNumber(file.number)}`;
@@ -11138,12 +11479,17 @@ async function executeSingleMigration(options, migration) {
11138
11479
  * @param {string} workspaceId - Workspace ID
11139
11480
  * @param {string} namespace - TailorDB namespace
11140
11481
  * @param {number} migrationNumber - Migration number to set
11482
+ * @param historyId - Optional migration history ID to set atomically with the checkpoint
11141
11483
  * @returns {Promise<void>}
11142
11484
  */
11143
- async function updateMigrationLabel(client, workspaceId, namespace, migrationNumber) {
11485
+ async function updateMigrationLabel(client, workspaceId, namespace, migrationNumber, historyId) {
11144
11486
  await writeMetadataLabels(client, {
11145
11487
  trn: resourceTrn(workspaceId, "tailordb", namespace),
11146
- labels: { [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber) }
11488
+ labels: {
11489
+ [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber),
11490
+ ...historyId ? { [MIGRATION_HISTORY_LABEL_KEY]: historyId } : {}
11491
+ },
11492
+ remove: historyId ? void 0 : [MIGRATION_HISTORY_LABEL_KEY]
11147
11493
  });
11148
11494
  }
11149
11495
  /**
@@ -11218,33 +11564,6 @@ function groupMigrationsByNamespace(migrations) {
11218
11564
 
11219
11565
  //#endregion
11220
11566
  //#region src/cli/commands/deploy/tailordb/index.ts
11221
- const MIGRATION_FILE_KINDS = [
11222
- "schema",
11223
- "diff",
11224
- "migrate",
11225
- "db"
11226
- ];
11227
- /**
11228
- * Capture the migration files that a deployment plan depends on.
11229
- * @param namespacesWithMigrations - Configured migration directories by namespace
11230
- * @returns SHA-256 digest by namespace
11231
- */
11232
- function captureMigrationFileState(namespacesWithMigrations) {
11233
- const state = {};
11234
- for (const { namespace, migrationsDir } of namespacesWithMigrations.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
11235
- const hash = createHash("sha256");
11236
- const migrationNumbers = [...new Set(getMigrationFiles(migrationsDir).map(({ number }) => number))].toSorted((a, b) => a - b);
11237
- for (const migrationNumber of migrationNumbers) for (const kind of MIGRATION_FILE_KINDS) {
11238
- const filePath = getMigrationFilePath(migrationsDir, migrationNumber, kind);
11239
- hash.update(`${formatMigrationNumber(migrationNumber)}/${kind}\0`);
11240
- if (fs$2.existsSync(filePath)) hash.update(fs$2.readFileSync(filePath));
11241
- else hash.update("<missing>");
11242
- hash.update("\0");
11243
- }
11244
- state[namespace] = hash.digest("hex");
11245
- }
11246
- return state;
11247
- }
11248
11567
  function migrationFileStatesEqual(planned, current) {
11249
11568
  const plannedNamespaces = Object.keys(planned).toSorted();
11250
11569
  const currentNamespaces = Object.keys(current).toSorted();
@@ -11263,8 +11582,13 @@ function migrationFileStatesEqual(planned, current) {
11263
11582
  async function validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck, tailorDBInputs) {
11264
11583
  const namespacesWithMigrations = getNamespacesWithMigrations(config, path.dirname(config.path));
11265
11584
  let pendingMigrations = [];
11585
+ let checkpointRepairs = [];
11586
+ const migrationHistoryIds = Object.create(null);
11266
11587
  if (namespacesWithMigrations.length > 0) {
11267
- for (const { namespace, migrationsDir } of namespacesWithMigrations) assertValidMigrationFiles(migrationsDir, namespace);
11588
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
11589
+ assertValidMigrationFiles(migrationsDir, namespace);
11590
+ migrationHistoryIds[namespace] = reconstructSnapshotFromMigrations(migrationsDir)?.rebaseline?.historyId ?? null;
11591
+ }
11268
11592
  if (!noSchemaCheck) {
11269
11593
  const migrationResults = await checkMigrationDiffs(typesByNamespace, namespacesWithMigrations);
11270
11594
  if (migrationResults.some((r) => r.hasDiff)) {
@@ -11276,6 +11600,10 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
11276
11600
  throw new Error("Schema migration check failed");
11277
11601
  }
11278
11602
  const remoteVerificationResults = await verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs);
11603
+ checkpointRepairs = remoteVerificationResults.flatMap((result) => result.checkpointRepair ? [{
11604
+ namespace: result.namespace,
11605
+ ...result.checkpointRepair
11606
+ }] : []);
11279
11607
  const missingCheckpointResults = remoteVerificationResults.filter((result) => result.checkpointMissingLocal);
11280
11608
  if (missingCheckpointResults.length > 0) {
11281
11609
  logger.error("Remote migration checkpoint is not in the local migration history:");
@@ -11293,8 +11621,10 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
11293
11621
  logger.info("Use '--no-schema-check' to skip this check (not recommended).");
11294
11622
  throw new Error("Remote schema verification failed");
11295
11623
  }
11624
+ for (const repair of checkpointRepairs) logger.warn(`Remote migration checkpoint for ${repair.namespace} will be reset to 0000 after confirmation (${formatMigrationNumber(repair.from)} → 0000); the remote schema already matches the local baseline.`);
11296
11625
  }
11297
- pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations, config.path);
11626
+ const currentMigrationOverrides = new Map(checkpointRepairs.map((repair) => [repair.namespace, repair.to]));
11627
+ pendingMigrations = await detectPendingMigrations(client, workspaceId, namespacesWithMigrations, config.path, currentMigrationOverrides);
11298
11628
  if (pendingMigrations.length > 0) {
11299
11629
  logger.newline();
11300
11630
  const withScripts = pendingMigrations.filter((m) => m.hasScript);
@@ -11306,13 +11636,15 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
11306
11636
  }
11307
11637
  return {
11308
11638
  pendingMigrations,
11639
+ checkpointRepairs,
11309
11640
  namespacesWithMigrations,
11310
- migrationFileState: captureMigrationFileState(namespacesWithMigrations)
11641
+ migrationFileState: captureMigrationFileState(namespacesWithMigrations),
11642
+ migrationHistoryIds
11311
11643
  };
11312
11644
  }
11313
11645
  /**
11314
- * Force each namespace's `sdk-migration` label to the working tree's latest
11315
- * migration number after a create-update apply.
11646
+ * Reconcile each namespace's migration checkpoint and history ID to
11647
+ * the working tree after a create-update apply.
11316
11648
  *
11317
11649
  * This records the initial baseline (`0000`), which is deployed via the normal
11318
11650
  * flow and never bumps the label itself, and keeps the label `<= working_tree_max`
@@ -11321,16 +11653,21 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
11321
11653
  * @param client - Operator client instance
11322
11654
  * @param workspaceId - Workspace ID
11323
11655
  * @param namespacesWithMigrations - Namespaces that have migration directories configured
11656
+ * @param migrationHistoryIds - Migration history ID captured during preflight for each namespace
11324
11657
  */
11325
- async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigrations) {
11658
+ async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigrations, migrationHistoryIds) {
11326
11659
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
11327
11660
  if (getMigrationFiles(migrationsDir).length === 0) continue;
11328
11661
  const targetVersion = getLatestMigrationNumber(migrationsDir);
11329
- const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace).catch(() => null);
11330
- if (currentVersion === targetVersion) continue;
11331
- await updateMigrationLabel(client, workspaceId, namespace, targetVersion);
11332
- const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
11333
- logger.info(`Migration label for namespace ${namespace} reconciled: ${from} ${formatMigrationNumber(targetVersion)}.`);
11662
+ const historyId = migrationHistoryIds[namespace] ?? null;
11663
+ const remoteState = await fetchRemoteMigrationState(client, resourceTrn(workspaceId, "tailordb", namespace)).catch(() => null);
11664
+ const currentVersion = remoteState?.number ?? null;
11665
+ if (remoteState && currentVersion === targetVersion && remoteState.historyId === historyId) continue;
11666
+ await updateMigrationLabel(client, workspaceId, namespace, targetVersion, historyId ?? void 0);
11667
+ if (remoteState) {
11668
+ const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
11669
+ logger.info(`Migration label for namespace ${namespace} reconciled: ${from} → ${formatMigrationNumber(targetVersion)}.`);
11670
+ } else logger.info(`Migration label for namespace ${namespace} reconciled to ${formatMigrationNumber(targetVersion)}.`);
11334
11671
  }
11335
11672
  }
11336
11673
  /**
@@ -11357,9 +11694,22 @@ function buildMigrationContextForScripts(client, migrationContext, migrationsReq
11357
11694
  }
11358
11695
  async function validateTailorDBMigrationState(client, result) {
11359
11696
  const { context } = result;
11697
+ if (context.migrationTestBaselines) {
11698
+ const currentMigrationFileState = captureMigrationFileState(getNamespacesWithMigrations(context.config, path.dirname(context.config.path)));
11699
+ if (!migrationFileStatesEqual(context.migrationFileState, currentMigrationFileState)) throw new Error("Migration files changed after deployment planning. Run the migration test again to create a fresh plan.");
11700
+ return {
11701
+ pendingMigrations: [],
11702
+ checkpointRepairs: [],
11703
+ namespacesWithMigrations: [],
11704
+ migrationFileState: currentMigrationFileState,
11705
+ migrationHistoryIds: {}
11706
+ };
11707
+ }
11360
11708
  const typesByNamespace = /* @__PURE__ */ new Map();
11361
11709
  for (const tailordb of context.tailorDBInputs) typesByNamespace.set(tailordb.namespace, tailordb.types);
11362
11710
  const validation = await validateAndDetectMigrations(client, context.workspaceId, typesByNamespace, context.config, context.noSchemaCheck, context.tailorDBInputs);
11711
+ const approvedRepairs = context.checkpointRepairs;
11712
+ if (approvedRepairs.length !== validation.checkpointRepairs.length || validation.checkpointRepairs.some((repair) => !approvedRepairs.some((approved) => approved.namespace === repair.namespace && approved.from === repair.from && approved.fromHistoryId === repair.fromHistoryId && approved.toHistoryId === repair.toHistoryId))) throw new Error("Remote migration checkpoint repair changed after deployment planning. Run the deployment again to review the updated repair.");
11363
11713
  if (!migrationFileStatesEqual(context.migrationFileState, validation.migrationFileState)) throw new Error("Migration files changed after deployment planning. Run the deployment again to create a fresh plan.");
11364
11714
  return validation;
11365
11715
  }
@@ -11380,7 +11730,11 @@ async function preflightTailorDB(client, result) {
11380
11730
  async function applyTailorDB(client, result, phase = "create-update") {
11381
11731
  const { changeSet, context: migrationContext } = result;
11382
11732
  if (phase === "create-update") {
11383
- const { pendingMigrations, namespacesWithMigrations } = await validateTailorDBMigrationState(client, result);
11733
+ const { pendingMigrations, checkpointRepairs, namespacesWithMigrations, migrationHistoryIds } = await validateTailorDBMigrationState(client, result);
11734
+ for (const repair of checkpointRepairs) {
11735
+ await updateMigrationLabel(client, migrationContext.workspaceId, repair.namespace, repair.to, repair.toHistoryId);
11736
+ logger.info(`Migration checkpoint for namespace ${repair.namespace} reset: ${formatMigrationNumber(repair.from)} → 0000.`);
11737
+ }
11384
11738
  if (pendingMigrations.length > 0) {
11385
11739
  processedTypes.reset();
11386
11740
  deletedResources.reset();
@@ -11412,15 +11766,37 @@ async function applyTailorDB(client, result, phase = "create-update") {
11412
11766
  await executeSingleMigrationPrePhase(client, changeSet, migration, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11413
11767
  if (migration.hasScript && migrationCtx) await executeMigrations(migrationCtx, [migration]);
11414
11768
  } catch (error) {
11769
+ await rollbackSingleMigrationAfterFailure(client, changeSet, migration, migrationContext.workspaceId, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11770
+ throw error;
11771
+ }
11772
+ try {
11773
+ await executeSingleMigrationPostPhase(client, changeSet, migration, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11774
+ } catch (error) {
11775
+ await rollbackSingleMigrationAfterFailure(client, changeSet, migration, migrationContext.workspaceId, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11776
+ throw error;
11777
+ }
11778
+ try {
11779
+ await updateMigrationLabel(client, migrationContext.workspaceId, migration.namespace, migration.number, migrationHistoryIds[migration.namespace] ?? void 0);
11780
+ } catch (error) {
11781
+ let remoteMigrationNumber;
11415
11782
  try {
11416
- await rollbackSingleMigrationPrePhase(client, changeSet, migration, migrationContext.workspaceId, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11417
- } catch (rollbackError) {
11418
- logger.warn(`Failed to roll back migration ${migration.namespace}/${formatMigrationNumber(migration.number)}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
11783
+ remoteMigrationNumber = (await fetchRemoteMigrationState(client, resourceTrn(migrationContext.workspaceId, "tailordb", migration.namespace))).number ?? void 0;
11784
+ } catch (readbackError) {
11785
+ logger.warn(`Could not verify migration checkpoint ${migration.namespace}/${formatMigrationNumber(migration.number)} after its update failed: ${readbackError instanceof Error ? readbackError.message : String(readbackError)}. Leaving the post-migration schema unchanged to avoid rolling back a committed checkpoint.`);
11786
+ throw error;
11419
11787
  }
11788
+ if (remoteMigrationNumber !== void 0 && remoteMigrationNumber > migration.number) throw new Error(`Migration checkpoint ${migration.namespace}/${formatMigrationNumber(migration.number)} advanced concurrently to ${formatMigrationNumber(remoteMigrationNumber)}. Leaving the post-migration schema unchanged and aborting this deployment.`, { cause: error });
11789
+ if (remoteMigrationNumber !== migration.number) {
11790
+ logger.warn(`Migration checkpoint ${migration.namespace}/${formatMigrationNumber(migration.number)} could not be confirmed after its update failed; remote remains at ${remoteMigrationNumber === void 0 ? "<unset>" : formatMigrationNumber(remoteMigrationNumber)}. Leaving the post-migration schema unchanged to avoid rolling back a concurrent deployment. Repair the checkpoint before retrying.`);
11791
+ throw error;
11792
+ }
11793
+ }
11794
+ try {
11795
+ await executeSingleMigrationPostPhaseDeletions(client, changeSet, migration);
11796
+ } catch (error) {
11797
+ logger.warn(`Migration checkpoint ${migration.namespace}/${formatMigrationNumber(migration.number)} was committed, but post-checkpoint cleanup failed. Remove the leftover resources manually before the next deployment; remote schema verification will fail closed until then.`);
11420
11798
  throw error;
11421
11799
  }
11422
- await executeSingleMigrationPostPhase(client, changeSet, migration, migrationContext.tailorDBInputs, migrationContext.executorUsedTypes);
11423
- await updateMigrationLabel(client, migrationContext.workspaceId, migration.namespace, migration.number);
11424
11800
  }
11425
11801
  if (migrationsRequiringScripts.length > 0) {
11426
11802
  logger.newline();
@@ -11459,7 +11835,7 @@ async function applyTailorDB(client, result, phase = "create-update") {
11459
11835
  await Promise.all(changeSet.gqlPermission.deletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
11460
11836
  await Promise.all(changeSet.type.deletes.map((del) => client.deleteTailorDBType(del.request)));
11461
11837
  }
11462
- if (namespacesWithMigrations.length > 0 && (migrationContext.noSchemaCheck || pendingMigrations.length === 0)) await reconcileMigrationLabels(client, migrationContext.workspaceId, namespacesWithMigrations);
11838
+ if (namespacesWithMigrations.length > 0 && (migrationContext.noSchemaCheck || pendingMigrations.length === 0)) await reconcileMigrationLabels(client, migrationContext.workspaceId, namespacesWithMigrations, migrationHistoryIds);
11463
11839
  } else if (phase === "delete-resources") {
11464
11840
  await Promise.all(changeSet.gqlPermission.deletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
11465
11841
  await Promise.all(changeSet.type.deletes.map((del) => client.deleteTailorDBType(del.request)));
@@ -11535,11 +11911,12 @@ const migrationSnapshotCache = {
11535
11911
  return snapshot;
11536
11912
  }
11537
11913
  };
11538
- function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes) {
11914
+ function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges) {
11539
11915
  const snapshotType = migrationSnapshotCache.load(migration).types[typeName];
11540
11916
  if (!snapshotType) return void 0;
11541
11917
  const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
11542
- return generateTailorDBTypeManifestFromSnapshot(snapshotType, {
11918
+ const typeScriptsChange = migration.diff.changes.find((change) => change.kind === "type_scripts_modified" && change.typeName === typeName);
11919
+ return generateTailorDBTypeManifestFromSnapshot(typeChanges ? createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsChange) : snapshotType, {
11543
11920
  subscribed: executorUsedTypes.has(snapshotType.name),
11544
11921
  namespaceGqlOperations: input?.config.gqlOperations
11545
11922
  });
@@ -11572,11 +11949,11 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11572
11949
  for (const create of changeSet.type.creates) {
11573
11950
  const typeName = create.request.tailordbType?.name;
11574
11951
  if (!typeName || !affectedTypes.has(typeName) || createdBeforeMigration.has(typeName)) continue;
11575
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
11952
+ const typeChanges = preMigrationChanges.get(typeName);
11953
+ const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
11576
11954
  if (!snapshotType) continue;
11577
11955
  const clonedRequest = structuredClone(create.request);
11578
11956
  clonedRequest.tailordbType = snapshotType;
11579
- const typeChanges = preMigrationChanges.get(typeName);
11580
11957
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
11581
11958
  const indexChanges = preMigrationIndexChanges.get(typeName);
11582
11959
  if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
@@ -11586,10 +11963,10 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11586
11963
  for (const create of changeSet.type.creates) {
11587
11964
  const typeName = create.request.tailordbType?.name;
11588
11965
  if (!typeName || !affectedTypes.has(typeName) || !createdBeforeMigration.has(typeName)) continue;
11589
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
11966
+ const typeChanges = preMigrationChanges.get(typeName);
11967
+ const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
11590
11968
  if (!snapshotType) continue;
11591
11969
  const clonedTypeRequest = structuredClone(snapshotType);
11592
- const typeChanges = preMigrationChanges.get(typeName);
11593
11970
  if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges);
11594
11971
  const indexChanges = preMigrationIndexChanges.get(typeName);
11595
11972
  if (indexChanges && indexChanges.size > 0 && clonedTypeRequest.schema?.indexes) applyPreMigrationIndexAdjustments(clonedTypeRequest.schema.indexes, indexChanges);
@@ -11603,11 +11980,11 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11603
11980
  for (const update of changeSet.type.updates) {
11604
11981
  const typeName = update.request.tailordbType?.name;
11605
11982
  if (!typeName || !affectedTypes.has(typeName)) continue;
11606
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
11983
+ const typeChanges = preMigrationChanges.get(typeName);
11984
+ const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
11607
11985
  if (!snapshotType) continue;
11608
11986
  const clonedRequest = structuredClone(update.request);
11609
11987
  clonedRequest.tailordbType = snapshotType;
11610
- const typeChanges = preMigrationChanges.get(typeName);
11611
11988
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
11612
11989
  const indexChanges = preMigrationIndexChanges.get(typeName);
11613
11990
  if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
@@ -11642,8 +12019,15 @@ const deletedResources = {
11642
12019
  this.gqlPermissions.clear();
11643
12020
  }
11644
12021
  };
12022
+ async function rollbackSingleMigrationAfterFailure(client, changeSet, migration, workspaceId, tailorDBInputs, executorUsedTypes) {
12023
+ try {
12024
+ await rollbackSingleMigrationPrePhase(client, changeSet, migration, workspaceId, tailorDBInputs, executorUsedTypes);
12025
+ } catch (rollbackError) {
12026
+ logger.warn(`Failed to roll back migration ${migration.namespace}/${formatMigrationNumber(migration.number)}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
12027
+ }
12028
+ }
11645
12029
  /**
11646
- * Execute post-migration phase for a single migration: Apply final types (with required: true) and deletions
12030
+ * Execute post-migration phase for a single migration: Apply final types (with required: true)
11647
12031
  * @param {OperatorClient} client - Operator client instance
11648
12032
  * @param {TailorDBChangeSet} changeSet - TailorDB change set
11649
12033
  * @param {PendingMigration} migration - Single pending migration
@@ -11656,7 +12040,6 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
11656
12040
  const preMigrationIndexChanges = buildPreMigrationIndexChangesMap([migration]);
11657
12041
  const adjustedTypes = /* @__PURE__ */ new Set([...preMigrationChanges.keys(), ...preMigrationIndexChanges.keys()]);
11658
12042
  const affectedTypes = getAffectedTypeNames(migration);
11659
- const deletedTypeNames = getDeletedTypeNames(migration);
11660
12043
  try {
11661
12044
  for (const create of changeSet.type.creates) {
11662
12045
  const typeName = create.request.tailordbType?.name;
@@ -11683,28 +12066,29 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
11683
12066
  } catch (error) {
11684
12067
  handleOptionalToRequiredError(error, ["This error occurred during post-migration phase. Please check your migration script.", "Ensure all existing records have values for fields being changed to required."]);
11685
12068
  }
12069
+ }
12070
+ async function executeSingleMigrationPostPhaseDeletions(client, changeSet, migration) {
12071
+ const deletedTypeNames = getDeletedTypeNames(migration);
11686
12072
  if (deletedTypeNames.size > 0) {
11687
12073
  const gqlPermissionsToDelete = changeSet.gqlPermission.deletes.filter((del) => {
11688
12074
  const permKey = `${del.request.namespaceName}/${del.name}`;
11689
12075
  if (deletedResources.gqlPermissions.has(permKey)) return false;
11690
12076
  const typeName = del.name;
11691
- if (typeName && deletedTypeNames.has(typeName)) {
11692
- deletedResources.gqlPermissions.add(permKey);
11693
- return true;
11694
- }
11695
- return false;
12077
+ return deletedTypeNames.has(typeName);
11696
12078
  });
11697
- await Promise.all(gqlPermissionsToDelete.map((del) => client.deleteTailorDBGQLPermission(del.request)));
12079
+ for (const del of gqlPermissionsToDelete) {
12080
+ await client.deleteTailorDBGQLPermission(del.request);
12081
+ deletedResources.gqlPermissions.add(`${del.request.namespaceName}/${del.name}`);
12082
+ }
11698
12083
  const typesToDelete = changeSet.type.deletes.filter((del) => {
11699
12084
  const typeName = del.name;
11700
12085
  if (!typeName || deletedResources.types.has(typeName)) return false;
11701
- if (deletedTypeNames.has(typeName)) {
11702
- deletedResources.types.add(typeName);
11703
- return true;
11704
- }
11705
- return false;
12086
+ return deletedTypeNames.has(typeName);
11706
12087
  });
11707
- await Promise.all(typesToDelete.map((del) => client.deleteTailorDBType(del.request)));
12088
+ for (const del of typesToDelete) {
12089
+ await client.deleteTailorDBType(del.request);
12090
+ deletedResources.types.add(del.name);
12091
+ }
11708
12092
  }
11709
12093
  }
11710
12094
  /**
@@ -11776,18 +12160,40 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
11776
12160
  async function planTailorDB(context) {
11777
12161
  const { client, workspaceId, application, forRemoval, config, noSchemaCheck, forceApplyAll = false } = context;
11778
12162
  const tailordbs = [];
12163
+ const migrationTestSnapshots = context.migrationTestSnapshots ?? (context.migrationTestBaselines ? new Map([...context.migrationTestBaselines].map(([namespace, baseline]) => [namespace, baseline.snapshot])) : void 0);
11779
12164
  if (!forRemoval) for (const tailordb of application.tailorDBServices) {
11780
12165
  await tailordb.loadTypes();
11781
- tailordbs.push(toTailorDBDeployInput(tailordb));
12166
+ const input = toTailorDBDeployInput(tailordb);
12167
+ const snapshot = migrationTestSnapshots?.get(tailordb.namespace);
12168
+ tailordbs.push(snapshot ? {
12169
+ ...input,
12170
+ types: snapshot.types
12171
+ } : input);
11782
12172
  }
11783
12173
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
11784
12174
  const executorUsedTypes = new Set(context.executorUsedTailorDBTypes ?? []);
11785
12175
  for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
11786
12176
  const typesByNamespace = /* @__PURE__ */ new Map();
11787
12177
  for (const tailordb of tailordbs) typesByNamespace.set(tailordb.namespace, tailordb.types);
11788
- const { namespacesWithMigrations, migrationFileState } = forRemoval ? {
12178
+ const migrationTestBaselines = context.migrationTestBaselines;
12179
+ if (migrationTestSnapshots) {
12180
+ for (const namespace of migrationTestSnapshots.keys()) if (!tailordbs.some((tailordb) => tailordb.namespace === namespace)) throw new Error(`Migration test snapshot targets unknown TailorDB namespace "${namespace}".`);
12181
+ const namespaceByType = /* @__PURE__ */ new Map();
12182
+ for (const tailordb of tailordbs) for (const typeName of Object.keys(tailordb.types)) {
12183
+ const existingNamespace = namespaceByType.get(typeName);
12184
+ if (existingNamespace) throw new Error(`Migration test snapshot has duplicate TailorDB type name "${typeName}" in namespaces "${existingNamespace}" and "${tailordb.namespace}".`);
12185
+ namespaceByType.set(typeName, tailordb.namespace);
12186
+ }
12187
+ }
12188
+ const migrationConfig = getNamespacesWithMigrations(config, path.dirname(config.path));
12189
+ const { namespacesWithMigrations, migrationFileState, checkpointRepairs } = forRemoval ? {
11789
12190
  namespacesWithMigrations: [],
11790
- migrationFileState: {}
12191
+ migrationFileState: {},
12192
+ checkpointRepairs: []
12193
+ } : migrationTestBaselines ? {
12194
+ namespacesWithMigrations: migrationConfig,
12195
+ migrationFileState: captureMigrationFileState(migrationConfig),
12196
+ checkpointRepairs: []
11791
12197
  } : await validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck ?? false, tailordbs);
11792
12198
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
11793
12199
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
@@ -11816,8 +12222,10 @@ async function planTailorDB(context) {
11816
12222
  executorUsedTypes,
11817
12223
  config,
11818
12224
  noSchemaCheck: noSchemaCheck ?? false,
12225
+ ...migrationTestBaselines ? { migrationTestBaselines } : {},
11819
12226
  namespacesWithMigrations,
11820
- migrationFileState
12227
+ migrationFileState,
12228
+ checkpointRepairs
11821
12229
  }
11822
12230
  };
11823
12231
  }
@@ -13139,6 +13547,22 @@ function insertIdProperty(source, configObj, id) {
13139
13547
 
13140
13548
  //#endregion
13141
13549
  //#region src/cli/commands/deploy/confirm.ts
13550
+ async function confirmMigrationCheckpointRepairs(repairs, yes) {
13551
+ if (repairs.length === 0) return;
13552
+ logger.warn("TailorDB migration checkpoints and history IDs need to be updated before this deployment:");
13553
+ for (const repair of repairs.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
13554
+ logger.log(` ${repair.namespace}:`);
13555
+ logger.log(` Checkpoint: ${formatMigrationNumber(repair.from)} → ${formatMigrationNumber(repair.to)}`);
13556
+ logger.log(` Migration history ID: ${repair.fromHistoryId ?? "<unset>"} → ${repair.toHistoryId}`);
13557
+ }
13558
+ logger.log(" The checkpoint reset itself changes only metadata.");
13559
+ logger.log(" This deployment may still apply pending schema or data migrations afterward.");
13560
+ if (yes) return;
13561
+ if (!await prompt.confirm({
13562
+ message: "Reset these migration checkpoints, align their history IDs, and continue with the deployment?",
13563
+ default: false
13564
+ })) throw new Error("Apply cancelled: migration checkpoint reset was not confirmed.");
13565
+ }
13142
13566
  /**
13143
13567
  * Confirm reassignment of resources when owner conflicts are detected.
13144
13568
  * Splits into three scenarios, each with its own prompt because the
@@ -14798,7 +15222,15 @@ function buildPlanReport(results) {
14798
15222
  const workflowEntries = formatWorkflowChangeEntries(results.workflow.changeSet, results.functionRegistry.workflowJobChanges);
14799
15223
  const workflowExecutionPolicyEntries = formatChangeSetEntries(results.workflowExecutionPolicy.changeSet, ["executionPolicy"]);
14800
15224
  const authHookEntries = formatAuthHookChangeEntries(results.auth.changeSet.authHook, results.functionRegistry.authHookFunctionChanges);
14801
- const tailorDBEntries = [...formatTailorDBResourceChangeEntries(results.tailorDB.changeSet.type, results.tailorDB.changeSet.gqlPermission)];
15225
+ const tailorDBResourceEntries = formatTailorDBResourceChangeEntries(results.tailorDB.changeSet.type, results.tailorDB.changeSet.gqlPermission);
15226
+ const checkpointRepairEntries = results.tailorDB.context.checkpointRepairs.map((repair) => ({
15227
+ action: "update",
15228
+ symbol: ACTION_SYMBOLS.update,
15229
+ name: `migration checkpoint ${formatMigrationNumber(repair.from)} → ${formatMigrationNumber(repair.to)}`,
15230
+ labels: ["migrationCheckpoint"],
15231
+ namespace: repair.namespace
15232
+ }));
15233
+ const tailorDBEntries = [...tailorDBResourceEntries, ...checkpointRepairEntries];
14802
15234
  const pipelineEntries = [...resolverEntries];
14803
15235
  const namespaceOf = (item) => {
14804
15236
  if ("request" in item && item.request && typeof item.request === "object" && "namespaceName" in item.request) return item.request.namespaceName;
@@ -15388,13 +15820,21 @@ function collectDeployRunPlanInputs(targets, writes) {
15388
15820
  };
15389
15821
  }
15390
15822
  async function planDeploymentTarget(params) {
15391
- const { target, targets, runInputs, client, workspaceId, noSchemaCheck } = params;
15823
+ const { target, targets, runInputs, client, workspaceId, noSchemaCheck, migrationTestBaselines, migrationTestSnapshots } = params;
15392
15824
  const { config, application, workflowBuildResult, httpAdapterBuildResult, bundledScripts } = target;
15393
15825
  const owned = ownedSubscriptions(runInputs.eventSubscriptions, target);
15826
+ const migrationTestServices = application.tailorDBServices.map((service) => {
15827
+ const snapshot = migrationTestSnapshots?.get(service.namespace);
15828
+ return snapshot ? {
15829
+ ...service,
15830
+ types: snapshot.types,
15831
+ typeSourceInfo: {}
15832
+ } : service;
15833
+ });
15394
15834
  await withSpan("plan.validateTailorDBTypeNames", () => assertUniqueTailorDBTypeNamesWithExternal({
15395
15835
  client,
15396
15836
  workspaceId,
15397
- tailorDBServices: application.tailorDBServices,
15837
+ tailorDBServices: migrationTestServices,
15398
15838
  externalTailorDBNamespaces: application.externalTailorDBNamespaces,
15399
15839
  plannedExternalTailorDBServices: collectPlannedExternalTailorDBServices(target, targets)
15400
15840
  }));
@@ -15413,6 +15853,8 @@ async function planDeploymentTarget(params) {
15413
15853
  forRemoval: false,
15414
15854
  config,
15415
15855
  noSchemaCheck,
15856
+ migrationTestBaselines,
15857
+ migrationTestSnapshots,
15416
15858
  forceApplyAll,
15417
15859
  ...runInputs,
15418
15860
  idpUserTriggerTargets: subscribedIdps(owned),
@@ -15819,7 +16261,8 @@ function collectDeploymentResourceOwners(deployments) {
15819
16261
  return owners;
15820
16262
  }
15821
16263
  async function confirmDeploymentPlans(params) {
15822
- const { deployments, yes, missingDependentApps = [] } = params;
16264
+ const { deployments, yes, dryRun = false, missingDependentApps = [] } = params;
16265
+ if (!dryRun) await confirmMigrationCheckpointRepairs(deployments.flatMap((deployment) => deployment.tailorDB.context.checkpointRepairs), yes);
15823
16266
  await confirmMissingDependentApps(missingDependentApps, yes);
15824
16267
  const targetAppNames = new Set(deployments.map((deployment) => deployment.application.name));
15825
16268
  const resourceOwners = collectDeploymentResourceOwners(deployments);
@@ -15881,12 +16324,44 @@ async function validateDeploymentPlans(deployments) {
15881
16324
  for (const deployment of deployments) await validatePlan(deploymentPlanResults(deployment));
15882
16325
  }
15883
16326
  /**
16327
+ * Strip the services a migration test deploy must not manage, so plan modules
16328
+ * see an application that already reflects the deploy's scope. Baseline deploys
16329
+ * omit executors and Auth user profiles (data loading must not trigger current
16330
+ * event handlers or reference the final schema); every migration test deploy
16331
+ * omits workspace-bound static website custom domains.
16332
+ * @param application - Application built from the user's config
16333
+ * @param internalContext - Internal deployment behavior used by composed CLI workflows
16334
+ * @returns The application as the migration test deploy manages it
16335
+ */
16336
+ function adjustApplicationForMigrationTest(application, internalContext) {
16337
+ if (!internalContext?.migrationTestSnapshots) return application;
16338
+ const forBaseline = internalContext.migrationTestBaselines !== void 0;
16339
+ const authService = forBaseline && application.authService ? {
16340
+ ...application.authService,
16341
+ userProfile: void 0
16342
+ } : application.authService;
16343
+ const adjusted = {
16344
+ ...application,
16345
+ executorService: forBaseline ? void 0 : application.executorService,
16346
+ authService,
16347
+ staticWebsiteServices: application.staticWebsiteServices.map((website) => ({
16348
+ ...website,
16349
+ customDomains: void 0
16350
+ })),
16351
+ get applications() {
16352
+ return [adjusted];
16353
+ }
16354
+ };
16355
+ return adjusted;
16356
+ }
16357
+ /**
15884
16358
  * Deploy the configured application to the Tailor platform.
15885
16359
  * @param options - Deploy execution options
15886
16360
  * @param cliContext - Global CLI arguments to preserve in recovery actions
16361
+ * @param internalContext - Internal deployment behavior used by composed CLI workflows
15887
16362
  * @returns Promise that resolves to `{ bundledScripts }` when `buildOnly` is true, otherwise void
15888
16363
  */
15889
- async function deployInternal(options, cliContext) {
16364
+ async function deployInternal(options, cliContext, internalContext) {
15890
16365
  return withSpan("deploy", async (rootSpan) => {
15891
16366
  rootSpan.setAttribute("deploy.dry_run", options?.dryRun ?? false);
15892
16367
  const configPaths = parseDeployConfigPaths(options?.configPath);
@@ -15943,17 +16418,23 @@ async function deployInternal(options, cliContext) {
15943
16418
  const { client, workspaceId } = workspace;
15944
16419
  rootSpan.setAttribute("app.name", targets.map((target) => target.application.name).join(","));
15945
16420
  rootSpan.setAttribute("workspace.id", workspaceId);
15946
- const runInputs = collectDeployRunPlanInputs(targets, !options?.dryRun);
16421
+ const planTargets = targets.map((target) => ({
16422
+ ...target,
16423
+ application: adjustApplicationForMigrationTest(target.application, internalContext)
16424
+ }));
16425
+ const runInputs = collectDeployRunPlanInputs(planTargets, !options?.dryRun);
15947
16426
  const deployments = await planDeploymentTargets({
15948
- targets,
16427
+ targets: planTargets,
15949
16428
  runInputs,
15950
16429
  client,
15951
16430
  workspaceId,
15952
- noSchemaCheck: options?.noSchemaCheck
16431
+ noSchemaCheck: options?.noSchemaCheck,
16432
+ migrationTestBaselines: internalContext?.migrationTestBaselines,
16433
+ migrationTestSnapshots: internalContext?.migrationTestSnapshots
15953
16434
  });
15954
16435
  const yes = options?.yes ?? false;
15955
16436
  dropCrossDeploymentManagedDeletes(deployments);
15956
- const missingDependentApps = (await Promise.all(targets.map((target) => fetchMissingDependentApps({
16437
+ const missingDependentApps = (await Promise.all(planTargets.map((target) => fetchMissingDependentApps({
15957
16438
  client,
15958
16439
  workspaceId,
15959
16440
  application: target.application,
@@ -15965,6 +16446,7 @@ async function deployInternal(options, cliContext) {
15965
16446
  await confirmDeploymentPlans({
15966
16447
  deployments,
15967
16448
  yes,
16449
+ dryRun,
15968
16450
  missingDependentApps
15969
16451
  });
15970
16452
  });
@@ -15976,7 +16458,7 @@ async function deployInternal(options, cliContext) {
15976
16458
  return;
15977
16459
  }
15978
16460
  await applyDeploymentPlans(client, workspaceId, deployments);
15979
- if (logger.jsonMode) logger.out({
16461
+ if (!internalContext?.suppressResultOutput) if (logger.jsonMode) logger.out({
15980
16462
  summary: planSummary,
15981
16463
  status: "applied"
15982
16464
  });
@@ -15992,6 +16474,32 @@ function deploy(options) {
15992
16474
  return deployInternal(options);
15993
16475
  }
15994
16476
  /**
16477
+ * Deploy TailorDB baseline snapshots for an isolated migration test.
16478
+ * @param options - Deploy execution options
16479
+ * @param baselines - Baseline snapshots keyed by TailorDB namespace
16480
+ * @param baselineSnapshots - All schema snapshots that must match the source before data loading
16481
+ * @returns Deploy result
16482
+ */
16483
+ function deployMigrationTestBaseline(options, baselines, baselineSnapshots) {
16484
+ return deployInternal(options, void 0, {
16485
+ migrationTestBaselines: baselines,
16486
+ migrationTestSnapshots: baselineSnapshots,
16487
+ suppressResultOutput: true
16488
+ });
16489
+ }
16490
+ /**
16491
+ * Deploy pending migrations without emitting deploy's standalone result payload.
16492
+ * @param options - Deploy execution options
16493
+ * @param snapshots - Final committed snapshots keyed by TailorDB namespace
16494
+ * @returns Deploy result
16495
+ */
16496
+ function deployMigrationTestTarget(options, snapshots) {
16497
+ return deployInternal(options, void 0, {
16498
+ migrationTestSnapshots: snapshots,
16499
+ suppressResultOutput: true
16500
+ });
16501
+ }
16502
+ /**
15995
16503
  * Deploy from the command adapter while preserving global CLI arguments in recovery actions.
15996
16504
  * @param options - Deploy execution options
15997
16505
  * @param cliContext - Global CLI arguments already applied by the command runner
@@ -19926,7 +20434,7 @@ function extractBreakingChangeFields(diff) {
19926
20434
  const optionalToRequired = /* @__PURE__ */ new Map();
19927
20435
  const addedRequiredFields = /* @__PURE__ */ new Map();
19928
20436
  const enumValueChanges = /* @__PURE__ */ new Map();
19929
- for (const change of diff.changes) if (change.kind === "field_modified") {
20437
+ for (const change of diff.changes) if (change.kind === "field_modified" || change.kind === "field_type_modified") {
19930
20438
  const { before, after } = change;
19931
20439
  if (!before.required && after.required) {
19932
20440
  if (!optionalToRequired.has(change.typeName)) optionalToRequired.set(change.typeName, /* @__PURE__ */ new Set());
@@ -19997,7 +20505,7 @@ function generateDbTypesFromSnapshot(snapshot, diff) {
19997
20505
  "",
19998
20506
  ...utilityTypeDeclarations,
19999
20507
  "",
20000
- "interface Database {",
20508
+ "export interface Database {",
20001
20509
  ...typeDefinitions,
20002
20510
  "}",
20003
20511
  "",
@@ -20027,7 +20535,7 @@ function generateEmptyDbTypes(namespace) {
20027
20535
  "import type { Env } from \"@tailor-platform/sdk\";",
20028
20536
  "",
20029
20537
  "// eslint-disable-next-line @typescript-eslint/no-empty-object-type",
20030
- "interface Database {}",
20538
+ "export interface Database {}",
20031
20539
  "",
20032
20540
  "export type Transaction = KyselyTransaction<Database>;",
20033
20541
  "",
@@ -20194,6 +20702,8 @@ async function writeDbTypesFile(snapshot, migrationsDir, migrationNumber, diff)
20194
20702
  * - XXXX/migrate.ts - Data migration script (when breaking changes exist)
20195
20703
  * - XXXX/db.ts - Generated types for migration script
20196
20704
  */
20705
+ /** Marker left in generated migration scripts until their normalization logic is reviewed. */
20706
+ const MIGRATION_REVIEW_REQUIRED_MARKER = "TODO(tailor-migration-review)";
20197
20707
  /**
20198
20708
  * Check if a file exists
20199
20709
  * @param {string} filePath - Path to check
@@ -20310,6 +20820,46 @@ ${updates.join("\n\n")}
20310
20820
  `;
20311
20821
  }
20312
20822
  /**
20823
+ * Generate migration test file content
20824
+ * @param {MigrationDiff} diff - Migration diff
20825
+ * @returns {string} Migration test file content
20826
+ */
20827
+ function generateMigrationTestScript(diff) {
20828
+ return `/**
20829
+ * Unit test for the ${diff.namespace} migration script.
20830
+ *
20831
+ * The mock compiles queries to the same SQL as the deployed migration, so the
20832
+ * test verifies the exact statements migrate.ts issues. Stage the rows each
20833
+ * query returns, run main() inside a transaction, then assert the executed
20834
+ * statements.
20835
+ */
20836
+
20837
+ import { createKyselyMock } from "@tailor-platform/sdk/vitest";
20838
+ import { describe, expect, test } from "vitest";
20839
+ import type { Database } from "./db";
20840
+ import { main } from "./migrate";
20841
+
20842
+ describe(${JSON.stringify(`${diff.namespace} migration`)}, () => {
20843
+ test("issues the intended statements", async () => {
20844
+ const mock = createKyselyMock<Database>();
20845
+
20846
+ // Stage the rows each query returns, in execution order:
20847
+ // mock.enqueueResult([{ id: "record-1" }]);
20848
+
20849
+ // Pass a MigrationContext when your main uses env: main(trx, { env: { ... } })
20850
+ await mock.withTx((trx) => main(trx));
20851
+
20852
+ // Replace with assertions on the statements the script must issue:
20853
+ // expect(mock.updates).toHaveLength(1);
20854
+ // expect(mock.updates[0]?.updateValues()).toEqual({ field: "value" });
20855
+ expect(
20856
+ mock.executedQueries.map((query) => ({ sql: query.sql, parameters: query.parameters })),
20857
+ ).toMatchSnapshot();
20858
+ });
20859
+ });
20860
+ `;
20861
+ }
20862
+ /**
20313
20863
  * Generate scripts for a single change
20314
20864
  * @param {DiffChange} change - Diff change to generate script for
20315
20865
  * @param {boolean} deferUniqueConstraint - Generate the unique check after decimal re-serialization
@@ -20357,9 +20907,10 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
20357
20907
  .execute();`];
20358
20908
  return [];
20359
20909
  }
20360
- if (change.kind !== "field_modified") return [];
20910
+ if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return [];
20361
20911
  const { before, after } = change;
20362
20912
  const scripts = [];
20913
+ if (change.kind === "field_type_modified") scripts.push(generateFieldTypeChangeScript(change));
20363
20914
  if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.typeName} records where it is null
20364
20915
  await trx
20365
20916
  .updateTable("${change.typeName}")
@@ -20406,10 +20957,56 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
20406
20957
  }`);
20407
20958
  return scripts;
20408
20959
  }
20960
+ function generateFieldTypeChangeScript(change) {
20961
+ return ` // Normalize ${change.typeName}.${change.fieldName} from ${change.before.type} to ${change.after.type} while the previous type is still active
20962
+ {
20963
+ let lastId: string | undefined;
20964
+ while (true) {
20965
+ let query = trx
20966
+ .selectFrom("${change.typeName}")
20967
+ .select(["id", "${change.fieldName}"])
20968
+ .where("${change.fieldName}", "is not", null)
20969
+ .orderBy("id", "asc")
20970
+ .limit(100);
20971
+ if (lastId) {
20972
+ query = query.where("id", ">", lastId);
20973
+ }
20974
+ const rows = await query.execute();
20975
+ if (rows.length === 0) break;
20976
+
20977
+ for (const row of rows) {
20978
+ // ${MIGRATION_REVIEW_REQUIRED_MARKER}: Remove this marker and the \`never\` annotation after reviewing the normalization.
20979
+ // Keep the value accepted by the active ${change.before.type} type and castable to ${change.after.type}.
20980
+ const sourceValue = row.${change.fieldName};
20981
+ if (sourceValue === null) continue;
20982
+ const normalizedValue: never = sourceValue;
20983
+ if (Object.is(normalizedValue, sourceValue)) continue;
20984
+ await trx
20985
+ .updateTable("${change.typeName}")
20986
+ .set({ [${JSON.stringify(change.fieldName)}]: normalizedValue })
20987
+ .where("id", "=", row.id)
20988
+ .execute();
20989
+ }
20990
+ lastId = rows[rows.length - 1]!.id;
20991
+ }
20992
+ }`;
20993
+ }
20409
20994
  function generateUniqueConstraintScript(change) {
20410
- if (change.kind !== "field_modified") return null;
20995
+ if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return null;
20411
20996
  const { before, after } = change;
20412
20997
  if ((before.unique ?? false) || !(after.unique ?? false)) return null;
20998
+ const duplicateResolution = change.kind === "field_type_modified" ? ` if (records.length > 1) {
20999
+ throw new Error(
21000
+ "TODO: Resolve duplicate ${change.typeName}.${change.fieldName} values before adding the unique constraint",
21001
+ );
21002
+ }` : ` // Keep first record, add suffix to others
21003
+ for (let i = 1; i < records.length; i++) {
21004
+ await trx
21005
+ .updateTable("${change.typeName}")
21006
+ .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
21007
+ .where("id", "=", records[i].id)
21008
+ .execute();
21009
+ }`;
20413
21010
  return ` // Ensure ${change.fieldName} values are unique before adding constraint
20414
21011
  {
20415
21012
  const duplicates = await trx
@@ -20419,19 +21016,13 @@ function generateUniqueConstraintScript(change) {
20419
21016
  .having((eb) => eb.fn.count("id"), ">", 1)
20420
21017
  .execute();
20421
21018
  for (const dup of duplicates) {
20422
- // Keep first record, add suffix to others
21019
+ // Load every record in this duplicate group before resolving it
20423
21020
  const records = await trx
20424
21021
  .selectFrom("${change.typeName}")
20425
21022
  .select(["id", "${change.fieldName}"])
20426
21023
  .where("${change.fieldName}", "=", dup.${change.fieldName})
20427
21024
  .execute();
20428
- for (let i = 1; i < records.length; i++) {
20429
- await trx
20430
- .updateTable("${change.typeName}")
20431
- .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
20432
- .where("id", "=", records[i].id)
20433
- .execute();
20434
- }
21025
+ ${duplicateResolution}
20435
21026
  }
20436
21027
  }`;
20437
21028
  }
@@ -20476,6 +21067,202 @@ function generateDecimalScaleChangeScript(change) {
20476
21067
  }`;
20477
21068
  }
20478
21069
 
21070
+ //#endregion
21071
+ //#region src/cli/commands/tailordb/migrate/script.ts
21072
+ /**
21073
+ * Script command for TailorDB migrations
21074
+ *
21075
+ * Adds a `migrate.ts` (and supporting `db.ts`) template to an existing
21076
+ * migration directory, optionally with a `migrate.test.ts` unit-test
21077
+ * scaffold. Useful for warning-tier changes where users may want to
21078
+ * write a custom data migration even though the change does not
21079
+ * automatically require one.
21080
+ */
21081
+ /**
21082
+ * Record in diff.json that a migration requiring a script, or one with
21083
+ * data-loss warnings, intentionally has none.
21084
+ * @param {MarkScriptSkippedOptions} options - Target migration and skip reason
21085
+ * @returns {ScriptSkippedInfo} The recorded acknowledgment
21086
+ */
21087
+ function markMigrationScriptSkipped(options) {
21088
+ const { migrationsDir, migrationNumber, reason } = options;
21089
+ const label = formatMigrationNumber(migrationNumber);
21090
+ const normalizedReason = reason.trim();
21091
+ if (!normalizedReason) throw new Error("Migration script skip reason must not be empty.");
21092
+ const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
21093
+ if (!fs$2.existsSync(diffPath)) throw new Error(`Migration ${label} not found in ${migrationsDir}. Expected ${diffPath}.`);
21094
+ const diff = loadDiff(diffPath);
21095
+ if (!diff.requiresMigrationScript && !diff.hasWarnings) throw new Error(`Migration ${label} does not require a migration script and has no data-loss warnings; nothing to skip.`);
21096
+ if (diff.scriptSkipped) throw new Error(`Migration ${label} already has a script skip recorded (${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason}).`);
21097
+ const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
21098
+ if (fs$2.existsSync(migratePath)) throw new Error(`Migration script exists at ${migratePath}. Delete migrate.ts first if this migration should run without a script.`);
21099
+ const scriptSkipped = {
21100
+ reason: normalizedReason,
21101
+ acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString()
21102
+ };
21103
+ const raw = JSON.parse(fs$2.readFileSync(diffPath, "utf-8"));
21104
+ raw.scriptSkipped = scriptSkipped;
21105
+ fs$2.writeFileSync(diffPath, JSON.stringify(raw, null, 2));
21106
+ return scriptSkipped;
21107
+ }
21108
+ /**
21109
+ * Remove a script skip acknowledgment after a real migration script is created.
21110
+ * @param diffPath - Migration diff file to update
21111
+ */
21112
+ function clearMigrationScriptSkipped(diffPath) {
21113
+ const raw = JSON.parse(fs$2.readFileSync(diffPath, "utf-8"));
21114
+ if (!Object.hasOwn(raw, "scriptSkipped")) return;
21115
+ delete raw.scriptSkipped;
21116
+ fs$2.writeFileSync(diffPath, JSON.stringify(raw, null, 2));
21117
+ }
21118
+ /**
21119
+ * Create migration script files (migrate.ts, db.ts, and optionally migrate.test.ts)
21120
+ * in an existing migration directory. When migrate.ts already exists and withTest
21121
+ * is set, only the test file is added. An existing migrate.ts clears a stale
21122
+ * --no-script acknowledgment instead of failing.
21123
+ * @param {AddMigrationScriptFilesOptions} options - Target migration and file selection
21124
+ * @returns {Promise<AddMigrationScriptFilesResult>} Paths of the created files
21125
+ */
21126
+ async function addMigrationScriptFiles(options) {
21127
+ const { migrationsDir, migrationNumber, withTest = false } = options;
21128
+ const label = formatMigrationNumber(migrationNumber);
21129
+ const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
21130
+ if (!fs$2.existsSync(diffPath)) throw new Error(`Migration ${label} not found in ${migrationsDir}. Expected ${diffPath}.`);
21131
+ const diff = loadDiff(diffPath);
21132
+ const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
21133
+ const migrateExists = fs$2.existsSync(migratePath);
21134
+ const result = {};
21135
+ if (migrateExists && diff.scriptSkipped) {
21136
+ clearMigrationScriptSkipped(diffPath);
21137
+ result.clearedScriptSkip = true;
21138
+ if (!withTest) return result;
21139
+ } else if (migrateExists && !withTest) throw new Error(`Migration script already exists at ${migratePath}.`);
21140
+ const testPath = getMigrationFilePath(migrationsDir, migrationNumber, "test");
21141
+ if (withTest && fs$2.existsSync(testPath)) throw new Error(`Migration test already exists at ${testPath}.`);
21142
+ if (migrateExists && withTest) {
21143
+ const dbTypesPath = getMigrationFilePath(migrationsDir, migrationNumber, "db");
21144
+ if (!fs$2.existsSync(dbTypesPath)) throw new Error(`Generated types not found at ${dbTypesPath}. The test scaffold imports Database from ./db; restore db.ts before adding a test.`);
21145
+ }
21146
+ if (!migrateExists) {
21147
+ const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir, migrationNumber - 1);
21148
+ if (!previousSnapshot) throw new Error(`Could not reconstruct previous schema for migration ${label}. Make sure migration ${0} exists.`);
21149
+ await fs.writeFile(migratePath, generateMigrationScript(diff));
21150
+ result.migratePath = migratePath;
21151
+ result.dbTypesPath = await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff);
21152
+ clearMigrationScriptSkipped(diffPath);
21153
+ }
21154
+ if (withTest) {
21155
+ await fs.writeFile(testPath, generateMigrationTestScript(diff));
21156
+ result.testPath = testPath;
21157
+ }
21158
+ return result;
21159
+ }
21160
+ /**
21161
+ * Add a migrate.ts template to an existing migration directory.
21162
+ * @param {ScriptOptions} options - Command options
21163
+ */
21164
+ async function script(options) {
21165
+ logBetaWarning("tailordb migration");
21166
+ const migrationNumber = parseMigrationNumberArg(options.number);
21167
+ if (migrationNumber === 0) throw new Error(`Migration ${options.number} is the initial schema snapshot and cannot have a migration script.`);
21168
+ const { config } = await loadConfig(options.configPath);
21169
+ const namespacesWithMigrations = getNamespacesWithMigrations(config, path.dirname(config.path));
21170
+ if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
21171
+ const targetNamespace = resolveTargetNamespace(namespacesWithMigrations, options.namespace);
21172
+ const { migrationsDir } = assertDefined(namespacesWithMigrations.find((ns) => ns.namespace === targetNamespace), "namespace with migrations not found");
21173
+ if (options.noScript) {
21174
+ if (options.withTest) throw new Error("--with-test cannot be used together with --no-script.");
21175
+ const reason = options.reason?.trim();
21176
+ if (!reason) throw new Error("--reason is required with --no-script.");
21177
+ const scriptSkipped = markMigrationScriptSkipped({
21178
+ migrationsDir,
21179
+ migrationNumber,
21180
+ reason
21181
+ });
21182
+ logger.success(`Recorded that migration ${styles.bold(options.number)} in namespace ${styles.bold(targetNamespace)} intentionally has no migration script`);
21183
+ logger.info(` Reason: ${scriptSkipped.reason}`);
21184
+ logger.info(` Diff file: ${getMigrationFilePath(migrationsDir, migrationNumber, "diff")}`);
21185
+ return;
21186
+ }
21187
+ if (options.reason !== void 0) throw new Error("--reason can only be used together with --no-script.");
21188
+ const result = await addMigrationScriptFiles({
21189
+ migrationsDir,
21190
+ migrationNumber,
21191
+ withTest: options.withTest
21192
+ });
21193
+ if (result.clearedScriptSkip) {
21194
+ logger.success(`Cleared the stale script skip record for migration ${styles.bold(options.number)} in namespace ${styles.bold(targetNamespace)}`);
21195
+ if (!result.testPath) {
21196
+ logger.info(` Migration script: ${getMigrationFilePath(migrationsDir, migrationNumber, "migrate")}`);
21197
+ return;
21198
+ }
21199
+ }
21200
+ const added = [result.migratePath && "migration script", result.testPath && "migration test"].filter(Boolean).join(" and ");
21201
+ logger.success(`Added ${added} for migration ${styles.bold(options.number)} in namespace ${styles.bold(targetNamespace)}`);
21202
+ if (result.migratePath) {
21203
+ logger.info(` Migration script: ${result.migratePath}`);
21204
+ logger.info(` DB types: ${result.dbTypesPath}`);
21205
+ }
21206
+ if (result.testPath) logger.info(` Migration test: ${result.testPath}`);
21207
+ logger.newline();
21208
+ if (result.migratePath) {
21209
+ logger.log("Edit the script to implement your data migration logic.");
21210
+ logger.log("It will be executed by 'tailor deploy' between Pre and Post phases.");
21211
+ }
21212
+ if (result.testPath) logger.log("Fill in the test with the rows to stage and the statements to assert.");
21213
+ const fileToOpen = result.migratePath ?? result.testPath;
21214
+ if (!fileToOpen) return;
21215
+ const editor = getConfiguredEditorCommand();
21216
+ if (!editor) return;
21217
+ logger.newline();
21218
+ logger.info(`Opening ${path.basename(fileToOpen)} in ${editor}...`);
21219
+ try {
21220
+ await openInConfiguredEditor(fileToOpen);
21221
+ } catch {
21222
+ return;
21223
+ }
21224
+ }
21225
+ function resolveTargetNamespace(namespacesWithMigrations, requested) {
21226
+ if (requested) {
21227
+ if (!namespacesWithMigrations.some((ns) => ns.namespace === requested)) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
21228
+ return requested;
21229
+ }
21230
+ if (namespacesWithMigrations.length === 1) {
21231
+ const [ns] = namespacesWithMigrations;
21232
+ return assertDefined(ns, "namespace with migrations missing").namespace;
21233
+ }
21234
+ throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
21235
+ }
21236
+ const scriptCommand = defineAppCommand({
21237
+ name: "script",
21238
+ description: "Add a migration script (migrate.ts) template to an existing migration directory, or record with --no-script that a migration intentionally has none.",
21239
+ notes: `When \`migrate.ts\` already exists, running the command clears a previously recorded \`--no-script\` acknowledgment.`,
21240
+ args: z.strictObject({
21241
+ ...configArg,
21242
+ number: arg(z.string(), {
21243
+ positional: true,
21244
+ description: "Migration number to add a script to (e.g., 0001 or 1)"
21245
+ }),
21246
+ namespace: arg(z.string().optional(), {
21247
+ alias: "n",
21248
+ description: "Target TailorDB namespace (required if multiple namespaces exist)"
21249
+ }),
21250
+ "no-script": arg(z.boolean().optional(), { description: "Record that this migration intentionally runs without a migration script (requires --reason)" }),
21251
+ reason: arg(z.string().optional(), { description: "Reason why no migration script is needed (used with --no-script)" }),
21252
+ "with-test": arg(z.boolean().optional(), { description: "Also add a migrate.test.ts unit-test scaffold; when migrate.ts already exists, only the test is added" })
21253
+ }),
21254
+ run: async (args) => {
21255
+ await script({
21256
+ configPath: args.config,
21257
+ number: args.number,
21258
+ namespace: args.namespace,
21259
+ noScript: args["no-script"],
21260
+ reason: args.reason,
21261
+ withTest: args["with-test"]
21262
+ });
21263
+ }
21264
+ });
21265
+
20479
21266
  //#endregion
20480
21267
  //#region src/cli/commands/tailordb/migrate/generate.ts
20481
21268
  /**
@@ -20486,6 +21273,24 @@ function generateDecimalScaleChangeScript(change) {
20486
21273
  * - Subsequent runs: Creates diff from previous snapshot (0001/diff.json, etc.)
20487
21274
  */
20488
21275
  /**
21276
+ * Build the safety-critical manual migration guidance for unsupported changes.
21277
+ * @returns Lines to write through the CLI logger
21278
+ */
21279
+ function getUnsupportedMigrationHintLines() {
21280
+ return [
21281
+ "These changes require a manual 3-step migration process:",
21282
+ " Migration 1: Add an optional temporary field with the desired structure",
21283
+ " If the old field is required, make the old field optional",
21284
+ " For each non-null old value, copy and convert it to the temporary field",
21285
+ " and set the old field to null in the same update",
21286
+ " Verify every old value is null before continuing",
21287
+ " Migration 2: Remove the old field",
21288
+ " Migration 3: Add the field with the original name and new structure,",
21289
+ " migrate data from the temporary field, then remove it",
21290
+ " Important: Reusing the name while stored old values remain can make subsequent reads fail"
21291
+ ];
21292
+ }
21293
+ /**
20489
21294
  * Handle --init option: delete existing migrations directories
20490
21295
  * @param {NamespaceWithMigrations[]} namespaces - Namespaces with migrations
20491
21296
  * @param {boolean} skipConfirmation - Whether to skip confirmation prompt
@@ -20542,7 +21347,7 @@ async function generate(options) {
20542
21347
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
20543
21348
  let pluginManager;
20544
21349
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
20545
- const { defineApplication } = await import("./application-CXNaUNhv.mjs");
21350
+ const { defineApplication } = await import("./application-BxLLiLsr.mjs");
20546
21351
  const application = defineApplication({
20547
21352
  config,
20548
21353
  pluginManager
@@ -20604,12 +21409,7 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
20604
21409
  }
20605
21410
  if (unsupportedChanges.some((change) => change.showThreeStepHint)) {
20606
21411
  logger.newline();
20607
- logger.info("These changes require a manual 3-step migration process:");
20608
- logger.info(" Migration 1: Add a new field with the desired structure");
20609
- logger.info(" and migrate data from old field to new field");
20610
- logger.info(" Migration 2: Remove the old field");
20611
- logger.info(" Migration 3: Add the field with the original name and new structure,");
20612
- logger.info(" migrate data from temporary field, then remove temporary field");
21412
+ for (const line of getUnsupportedMigrationHintLines()) logger.info(line);
20613
21413
  }
20614
21414
  const details = unsupportedChanges.map((c) => ` - ${c.typeName}.${c.fieldName}: ${c.reason}`).join("\n");
20615
21415
  throw new Error(`Unsupported schema changes detected:\n${details}`);
@@ -20633,7 +21433,7 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
20633
21433
  logger.warn(formatWarnings(diff.warnings));
20634
21434
  }
20635
21435
  const result = await generateDiffFiles(diff, migrationsDir, getNextMigrationNumber(migrationsDir), previousSnapshot, options.name);
20636
- logger.success(`Generated migration ${styles.bold(result.migrationNumber.toString().padStart(4, "0"))}`);
21436
+ logger.success(`Generated migration ${styles.bold(formatMigrationNumber(result.migrationNumber))}`);
20637
21437
  logger.info(` Diff file: ${result.diffFilePath}`);
20638
21438
  if (result.migrateFilePath) {
20639
21439
  logger.info(` Migration script: ${result.migrateFilePath}`);
@@ -20655,11 +21455,54 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
20655
21455
  } catch {
20656
21456
  return;
20657
21457
  }
20658
- } else if (diff.hasWarnings) {
20659
- logger.newline();
20660
- logger.log(`Data loss is possible for this migration but no script was generated. To add a custom migrate.ts, run:`);
20661
- logger.log(` ${styles.bold(`tailor tailordb migration script ${result.migrationNumber.toString().padStart(4, "0")} --namespace ${diff.namespace}`)}`);
21458
+ } else if (diff.hasWarnings) await acknowledgeWarnings({
21459
+ namespace: diff.namespace,
21460
+ migrationsDir,
21461
+ migrationNumber: result.migrationNumber,
21462
+ skipPrompt: options.yes,
21463
+ configPath: options.configPath
21464
+ });
21465
+ }
21466
+ /**
21467
+ * Offer to record a --no-script acknowledgment for a warning-only migration,
21468
+ * or print the follow-up commands when the session is non-interactive
21469
+ * @param {AcknowledgeWarningsOptions} options - Target migration and prompt behavior
21470
+ */
21471
+ async function acknowledgeWarnings(options) {
21472
+ const { namespace, migrationsDir, migrationNumber, skipPrompt, configPath } = options;
21473
+ const label = formatMigrationNumber(migrationNumber);
21474
+ logger.newline();
21475
+ logger.log("Data loss is possible for this migration but no script was generated.");
21476
+ if (!skipPrompt && canPrompt()) {
21477
+ if (await prompt.confirm({
21478
+ message: "Record a reason acknowledging that this migration intentionally has no script?",
21479
+ default: true
21480
+ })) {
21481
+ const scriptSkipped = markMigrationScriptSkipped({
21482
+ migrationsDir,
21483
+ migrationNumber,
21484
+ reason: await prompt.text({
21485
+ message: "Reason:",
21486
+ validate: (value) => value.trim() !== "" || "Reason must not be empty."
21487
+ })
21488
+ });
21489
+ logger.success(`Recorded that migration ${styles.bold(label)} intentionally has no migration script`);
21490
+ logger.info(` Reason: ${scriptSkipped.reason}`);
21491
+ return;
21492
+ }
20662
21493
  }
21494
+ const commandOptions = {
21495
+ migrationNumber,
21496
+ namespace,
21497
+ configPath
21498
+ };
21499
+ logger.log("To add a custom migrate.ts, run:");
21500
+ logger.log(` ${styles.bold(formatMigrationScriptCommand(commandOptions))}`);
21501
+ logger.log("To record that this migration intentionally has no script, run:");
21502
+ logger.log(` ${styles.bold(formatMigrationScriptCommand({
21503
+ ...commandOptions,
21504
+ noScript: true
21505
+ }))}`);
20663
21506
  }
20664
21507
  /**
20665
21508
  * CLI command definition for generate
@@ -20686,6 +21529,478 @@ const generateCommand = defineAppCommand({
20686
21529
  }
20687
21530
  });
20688
21531
 
21532
+ //#endregion
21533
+ //#region src/cli/commands/generate/seed/bundler.ts
21534
+ /**
21535
+ * Seed script bundler for TailorDB seed data
21536
+ *
21537
+ * Bundles seed scripts to be executed via TestExecScript API
21538
+ */
21539
+ const BATCH_SIZE = 100;
21540
+ /**
21541
+ * Generate seed script content for server-side execution
21542
+ * @param namespace - TailorDB namespace
21543
+ * @returns Generated seed script content
21544
+ */
21545
+ function generateSeedScriptContent(namespace) {
21546
+ return multiline`
21547
+ import { Kysely, TailordbDialect } from "@tailor-platform/sdk/kysely";
21548
+
21549
+ type SeedInput = {
21550
+ data: Record<string, Record<string, unknown>[]>;
21551
+ order: string[];
21552
+ selfRefTypes: string[];
21553
+ upsert?: boolean;
21554
+ };
21555
+
21556
+ type SeedResult = {
21557
+ success: boolean;
21558
+ processed: Record<string, { inserted: number; updated: number; skipped: number }>;
21559
+ errors: string[];
21560
+ };
21561
+
21562
+ function getDB(namespace: string) {
21563
+ const client = new tailordb.Client({ namespace });
21564
+ return new Kysely<Record<string, Record<string, unknown>>>({
21565
+ dialect: new TailordbDialect(client),
21566
+ });
21567
+ }
21568
+
21569
+ export async function main(input: SeedInput): Promise<SeedResult> {
21570
+ const db = getDB("${namespace}");
21571
+ const processed: Record<
21572
+ string,
21573
+ { inserted: number; updated: number; skipped: number }
21574
+ > = {};
21575
+ const errors: string[] = [];
21576
+ const BATCH_SIZE = ${String(BATCH_SIZE)};
21577
+ const upsert = input.upsert === true;
21578
+
21579
+ for (const typeName of input.order) {
21580
+ const records = input.data[typeName];
21581
+ if (!records || records.length === 0) {
21582
+ console.log(\`[${namespace}] \${typeName}: skipped (no data)\`);
21583
+ continue;
21584
+ }
21585
+
21586
+ processed[typeName] = { inserted: 0, updated: 0, skipped: 0 };
21587
+ const hasSelfRef = (input.selfRefTypes || []).includes(typeName);
21588
+
21589
+ try {
21590
+ let recordsToInsert = records;
21591
+ let recordsToUpdate: Record<string, unknown>[] = [];
21592
+ if (upsert) {
21593
+ const existing = await db
21594
+ .selectFrom(typeName)
21595
+ .select("id")
21596
+ .where(
21597
+ "id",
21598
+ "in",
21599
+ records.map((record) => record.id),
21600
+ )
21601
+ .execute();
21602
+ const existingIds = new Set(existing.map((record) => record.id));
21603
+ recordsToInsert = records.filter((record) => !existingIds.has(record.id));
21604
+ recordsToUpdate = records.filter((record) => existingIds.has(record.id));
21605
+ }
21606
+
21607
+ if (hasSelfRef) {
21608
+ // Insert one-by-one to respect self-referencing foreign key order
21609
+ for (const record of recordsToInsert) {
21610
+ await db.insertInto(typeName).values(record).execute();
21611
+ processed[typeName].inserted += 1;
21612
+ }
21613
+ if (!upsert) {
21614
+ console.log(
21615
+ \`[${namespace}] \${typeName}: \${processed[typeName].inserted}/\${records.length} (one-by-one)\`,
21616
+ );
21617
+ }
21618
+ } else {
21619
+ for (let i = 0; i < recordsToInsert.length; i += BATCH_SIZE) {
21620
+ const batch = recordsToInsert.slice(i, i + BATCH_SIZE);
21621
+ await db.insertInto(typeName).values(batch).execute();
21622
+ processed[typeName].inserted += batch.length;
21623
+ if (!upsert) {
21624
+ console.log(
21625
+ \`[${namespace}] \${typeName}: \${processed[typeName].inserted}/\${records.length}\`,
21626
+ );
21627
+ }
21628
+ }
21629
+ }
21630
+
21631
+ for (const record of recordsToUpdate) {
21632
+ const { id, ...values } = record;
21633
+ if (Object.keys(values).length === 0) {
21634
+ processed[typeName].skipped += 1;
21635
+ continue;
21636
+ }
21637
+ await db.updateTable(typeName).set(values).where("id", "=", id).execute();
21638
+ processed[typeName].updated += 1;
21639
+ }
21640
+
21641
+ const counts = processed[typeName];
21642
+ if (upsert) {
21643
+ const skipped = counts.skipped > 0 ? \`, \${counts.skipped} skipped\` : "";
21644
+ console.log(
21645
+ \`[${namespace}] \${typeName}: \${counts.inserted} inserted, \${counts.updated} updated\${skipped}\`,
21646
+ );
21647
+ }
21648
+ } catch (error) {
21649
+ const message = error instanceof Error ? error.message : String(error);
21650
+ errors.push(\`\${typeName}: \${message}\`);
21651
+ console.error(\`[${namespace}] \${typeName}: failed - \${message}\`);
21652
+ }
21653
+ }
21654
+
21655
+ return {
21656
+ success: errors.length === 0,
21657
+ processed,
21658
+ errors,
21659
+ };
21660
+ }
21661
+ `;
21662
+ }
21663
+ /**
21664
+ * Bundle a seed script for server-side execution
21665
+ *
21666
+ * Creates an entry that:
21667
+ * 1. Defines getDB() function inline
21668
+ * 2. Processes data in batches using Kysely
21669
+ * 3. Reports progress via console.log
21670
+ * 4. Exports as main() for TestExecScript
21671
+ * @param namespace - TailorDB namespace
21672
+ * @param typeNames - List of type names to include in the seed
21673
+ * @param baseDir - Directory whose dependencies and tsconfig the generated entry uses
21674
+ * @returns Bundled seed script result
21675
+ */
21676
+ async function bundleSeedScript(namespace, typeNames, baseDir = process.cwd()) {
21677
+ const outputDir = path.resolve(getDistDir(), "seed");
21678
+ fs$2.mkdirSync(outputDir, { recursive: true });
21679
+ const entryPath = path.join(outputDir, `seed_${namespace}.entry.ts`);
21680
+ const entryContent = generateSeedScriptContent(namespace);
21681
+ fs$2.writeFileSync(entryPath, entryContent);
21682
+ let tsconfig;
21683
+ try {
21684
+ tsconfig = await resolveTSConfig(baseDir);
21685
+ } catch {
21686
+ tsconfig = void 0;
21687
+ }
21688
+ const bundleLog = createBundleLog({ tsconfig });
21689
+ const result = await rolldown.build({
21690
+ plugins: [
21691
+ createGeneratedEntryResolverPlugin(entryPath, baseDir),
21692
+ createTsconfigPathsPlugin(),
21693
+ platformBundleDefinePlugin
21694
+ ],
21695
+ input: entryPath,
21696
+ write: false,
21697
+ output: {
21698
+ format: "esm",
21699
+ sourcemap: false,
21700
+ minify: false,
21701
+ codeSplitting: false,
21702
+ globals: { tailordb: "tailordb" }
21703
+ },
21704
+ external: ["tailordb"],
21705
+ resolve: { conditionNames: ["node", "import"] },
21706
+ tsconfig,
21707
+ treeshake: {
21708
+ moduleSideEffects: false,
21709
+ annotations: true,
21710
+ unknownGlobalSideEffects: false
21711
+ },
21712
+ ...bundleLog.options
21713
+ });
21714
+ bundleLog.assertAllResolved();
21715
+ return {
21716
+ namespace,
21717
+ bundledCode: result.output[0].code,
21718
+ typesIncluded: typeNames
21719
+ };
21720
+ }
21721
+
21722
+ //#endregion
21723
+ //#region src/cli/shared/seed-chunker.ts
21724
+ /**
21725
+ * Seed data chunker for splitting large seed data into manageable message sizes.
21726
+ *
21727
+ * When seed data exceeds the gRPC message size limit, this module splits the data
21728
+ * into multiple chunks at type boundaries (or record boundaries for large types).
21729
+ */
21730
+ /** Default maximum message size: 3.5MB (conservative limit for gRPC) */
21731
+ const DEFAULT_MAX_MESSAGE_SIZE = 3.5 * 1024 * 1024;
21732
+ /** Reserved bytes for message metadata overhead */
21733
+ const METADATA_OVERHEAD = 1024;
21734
+ /**
21735
+ * Split seed data into chunks that fit within the gRPC message size limit.
21736
+ *
21737
+ * Algorithm:
21738
+ * 1. Calculate the available budget for the arg field (maxMessageSize - codeByteSize - overhead)
21739
+ * 2. If all data fits in one message, return a single chunk
21740
+ * 3. Otherwise, iterate through types in dependency order:
21741
+ * - If a type fits in the current chunk, add it
21742
+ * - If adding a type would exceed the budget, finalize the current chunk and start a new one
21743
+ * - If a single type exceeds the budget, split its records across multiple chunks
21744
+ * - If a single record exceeds the budget, throw an error
21745
+ * @param options - Chunking options
21746
+ * @returns Array of seed chunks
21747
+ */
21748
+ function chunkSeedData(options) {
21749
+ const { data, order, codeByteSize, maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE } = options;
21750
+ const argBudget = maxMessageSize - codeByteSize - METADATA_OVERHEAD;
21751
+ if (argBudget <= 0) throw new Error(`Code size (${codeByteSize} bytes) exceeds the message size limit (${maxMessageSize} bytes). No space left for seed data.`);
21752
+ const typesWithData = order.filter((type) => (data[type]?.length ?? 0) > 0);
21753
+ if (typesWithData.length === 0) return [];
21754
+ if (byteSize(JSON.stringify({
21755
+ data,
21756
+ order
21757
+ })) <= argBudget) return [{
21758
+ data,
21759
+ order,
21760
+ index: 0,
21761
+ total: 1
21762
+ }];
21763
+ const chunks = [];
21764
+ let currentData = {};
21765
+ let currentOrder = [];
21766
+ for (const type of typesWithData) {
21767
+ const typeRecords = assertDefined(data[type], `seed data missing for type: ${type}`);
21768
+ if (currentOrder.length > 0) {
21769
+ const testData = {
21770
+ ...currentData,
21771
+ [type]: typeRecords
21772
+ };
21773
+ const testOrder = [...currentOrder, type];
21774
+ if (byteSize(JSON.stringify({
21775
+ data: testData,
21776
+ order: testOrder
21777
+ })) > argBudget) {
21778
+ chunks.push({
21779
+ data: currentData,
21780
+ order: currentOrder,
21781
+ index: chunks.length
21782
+ });
21783
+ currentData = {};
21784
+ currentOrder = [];
21785
+ }
21786
+ }
21787
+ if (byteSize(JSON.stringify({
21788
+ data: { [type]: typeRecords },
21789
+ order: [type]
21790
+ })) <= argBudget) {
21791
+ currentData[type] = typeRecords;
21792
+ currentOrder.push(type);
21793
+ continue;
21794
+ }
21795
+ if (currentOrder.length > 0) {
21796
+ chunks.push({
21797
+ data: currentData,
21798
+ order: currentOrder,
21799
+ index: chunks.length
21800
+ });
21801
+ currentData = {};
21802
+ currentOrder = [];
21803
+ }
21804
+ let recordBatch = [];
21805
+ for (const record of typeRecords) {
21806
+ if (byteSize(JSON.stringify({
21807
+ data: { [type]: [record] },
21808
+ order: [type]
21809
+ })) > argBudget) {
21810
+ const singleRecordSize = byteSize(JSON.stringify(record));
21811
+ throw new Error(`A single record in type "${type}" (${singleRecordSize} bytes) exceeds the message size budget (${argBudget} bytes). Consider increasing maxMessageSize or reducing the record size.`);
21812
+ }
21813
+ const testBatch = [...recordBatch, record];
21814
+ const testData = {
21815
+ ...currentData,
21816
+ [type]: testBatch
21817
+ };
21818
+ const testOrder = currentOrder.includes(type) ? currentOrder : [...currentOrder, type];
21819
+ if (byteSize(JSON.stringify({
21820
+ data: testData,
21821
+ order: testOrder
21822
+ })) > argBudget && recordBatch.length > 0) {
21823
+ currentData[type] = recordBatch;
21824
+ if (!currentOrder.includes(type)) currentOrder.push(type);
21825
+ chunks.push({
21826
+ data: currentData,
21827
+ order: currentOrder,
21828
+ index: chunks.length
21829
+ });
21830
+ currentData = {};
21831
+ currentOrder = [];
21832
+ recordBatch = [record];
21833
+ } else recordBatch = testBatch;
21834
+ }
21835
+ if (recordBatch.length > 0) {
21836
+ currentData[type] = recordBatch;
21837
+ if (!currentOrder.includes(type)) currentOrder.push(type);
21838
+ }
21839
+ }
21840
+ if (currentOrder.length > 0) chunks.push({
21841
+ data: currentData,
21842
+ order: currentOrder,
21843
+ index: chunks.length
21844
+ });
21845
+ const total = chunks.length;
21846
+ return chunks.map((chunk) => ({
21847
+ ...chunk,
21848
+ total
21849
+ }));
21850
+ }
21851
+ function byteSize(str) {
21852
+ return new TextEncoder().encode(str).length;
21853
+ }
21854
+
21855
+ //#endregion
21856
+ //#region src/plugin/builtin/seed/seed-type-processor.ts
21857
+ /**
21858
+ * Processes TailorDB types to extract seed type information
21859
+ * @param type - Parsed TailorDB type
21860
+ * @param namespace - Namespace of the type
21861
+ * @returns Seed type information
21862
+ */
21863
+ function processSeedTypeInfo(type, namespace) {
21864
+ const dependencies = /* @__PURE__ */ new Set();
21865
+ const selfRefFields = [];
21866
+ for (const [fieldName, field] of Object.entries(type.fields)) {
21867
+ const targetType = field.relation?.targetType ?? field.config.foreignKeyType;
21868
+ if (!targetType) continue;
21869
+ if (targetType === type.name) selfRefFields.push(fieldName);
21870
+ else dependencies.add(targetType);
21871
+ }
21872
+ return {
21873
+ name: type.name,
21874
+ namespace,
21875
+ dependencies: Array.from(dependencies),
21876
+ selfRefFields,
21877
+ dataFile: `data/${type.name}.jsonl`
21878
+ };
21879
+ }
21880
+ /**
21881
+ * Build per-namespace seed ordering information from TailorDB namespace data.
21882
+ * @param tailordb - TailorDB namespaces with their types
21883
+ * @returns Seed namespace configs, in namespace order
21884
+ */
21885
+ function buildSeedNamespaceConfigs(tailordb) {
21886
+ return tailordb.map((ns) => {
21887
+ const types = [];
21888
+ const dependencies = {};
21889
+ const selfRefTypes = [];
21890
+ const requiredFields = {};
21891
+ for (const [typeName, type] of Object.entries(ns.types)) {
21892
+ const typeInfo = processSeedTypeInfo(type, ns.namespace);
21893
+ types.push(typeInfo.name);
21894
+ dependencies[typeInfo.name] = typeInfo.dependencies;
21895
+ if (typeInfo.selfRefFields.length > 0) selfRefTypes.push(typeInfo.name);
21896
+ const linesDb = processLinesDb(type, assertDefined(ns.sourceInfo.get(typeName), `source info missing for type: ${typeName}`));
21897
+ requiredFields[typeInfo.name] = Object.entries(type.fields).filter(([fieldName, field]) => field.config.required !== false && !linesDb.optionalFields.includes(fieldName) && !linesDb.omitFields.includes(fieldName)).map(([fieldName]) => fieldName);
21898
+ }
21899
+ return {
21900
+ namespace: ns.namespace,
21901
+ types,
21902
+ dependencies,
21903
+ selfRefTypes,
21904
+ requiredFields
21905
+ };
21906
+ });
21907
+ }
21908
+
21909
+ //#endregion
21910
+ //#region src/cli/shared/tailordb-namespaces.ts
21911
+ /**
21912
+ * Load local TailorDB namespaces along with the config plugins and the
21913
+ * defined application. Internal superset of {@link loadTailorDBNamespaces}.
21914
+ * @param options - Namespace loading options.
21915
+ * @returns The loaded config, plugins, application, and TailorDB namespace data.
21916
+ */
21917
+ async function loadApplicationNamespaces(options = {}) {
21918
+ const { config, plugins } = await loadConfig(options.configPath);
21919
+ await generateUserTypes({
21920
+ config,
21921
+ configPath: config.path
21922
+ });
21923
+ const application = defineApplication({
21924
+ config,
21925
+ pluginManager: plugins.length > 0 ? new PluginManager(plugins) : void 0
21926
+ });
21927
+ const namespaceNames = typeof options.namespaces === "function" ? options.namespaces(config, plugins) : options.namespaces;
21928
+ const namespaceFilter = namespaceNames ? new Set(namespaceNames) : void 0;
21929
+ const services = namespaceFilter ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) : application.tailorDBServices;
21930
+ if (namespaceFilter && services.length !== namespaceFilter.size) {
21931
+ const found = new Set(services.map((db) => db.namespace));
21932
+ const missing = [...namespaceFilter].filter((ns) => !found.has(ns)).join(", ");
21933
+ const available = application.tailorDBServices.map((db) => db.namespace).join(", ");
21934
+ throw new Error(`TailorDB namespace "${missing}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
21935
+ }
21936
+ const namespaces = [];
21937
+ for (const db of services) {
21938
+ await db.loadTypes();
21939
+ await db.processNamespacePlugins();
21940
+ namespaces.push({
21941
+ namespace: db.namespace,
21942
+ types: { ...db.types },
21943
+ sourceInfo: new Map(Object.entries(db.typeSourceInfo)),
21944
+ pluginAttachments: db.pluginAttachments
21945
+ });
21946
+ }
21947
+ return {
21948
+ config,
21949
+ plugins,
21950
+ application,
21951
+ namespaces
21952
+ };
21953
+ }
21954
+ /**
21955
+ * Load local TailorDB namespaces exactly as SDK generation/deploy sees them:
21956
+ * the config is loaded, user types are generated, and each selected
21957
+ * namespace's types are loaded with namespace plugins applied.
21958
+ * @param options - Namespace loading options.
21959
+ * @returns The loaded config and TailorDB namespace data.
21960
+ */
21961
+ async function loadTailorDBNamespaces(options = {}) {
21962
+ const { config, plugins, namespaces } = await loadApplicationNamespaces(options);
21963
+ return {
21964
+ config,
21965
+ plugins,
21966
+ namespaces
21967
+ };
21968
+ }
21969
+
21970
+ //#endregion
21971
+ //#region src/cli/shared/seed-context.ts
21972
+ /**
21973
+ * Load the seed context from the local config. Requires `seedPlugin` to be
21974
+ * configured in the config's plugins. A relative `distPath` in the seedPlugin
21975
+ * options is resolved against the current working directory — the same base
21976
+ * `tailor generate` writes it to.
21977
+ * @param options - Seed context loading options.
21978
+ * @returns The seed context computed from the local config.
21979
+ */
21980
+ async function loadSeedContext(options = {}) {
21981
+ const { config, plugins, application, namespaces } = await loadApplicationNamespaces({ configPath: options.configPath });
21982
+ assertUniqueLocalTailorDBTypeNames({ tailorDBServices: application.tailorDBServices });
21983
+ const seedPlugin = plugins.find((plugin) => plugin.id === SeedGeneratorID);
21984
+ if (!seedPlugin) throw new Error(`seedPlugin is not configured in ${config.path}. Add seedPlugin({ distPath: "./seed" }) from "@tailor-platform/sdk/plugin/seed" to definePlugins().`);
21985
+ const pluginOptions = seedPlugin.pluginConfig;
21986
+ if (typeof pluginOptions?.distPath !== "string" || pluginOptions.distPath === "") throw new Error(`seedPlugin in ${config.path} has no distPath option. Pass seedPlugin({ distPath: "./seed" }) so seed data has a location.`);
21987
+ await application.authService?.resolveNamespaces();
21988
+ const authInput = getAuthInput(application);
21989
+ const idpUserMeta = authInput ? processIdpUser(authInput) : void 0;
21990
+ const idpUser = idpUserMeta ? {
21991
+ idpNamespace: idpUserMeta.idpNamespace,
21992
+ seedScriptCode: generateIdpSeedScriptCode(idpUserMeta.idpNamespace),
21993
+ truncateScriptCode: generateIdpTruncateScriptCode(idpUserMeta.idpNamespace)
21994
+ } : null;
21995
+ return {
21996
+ config,
21997
+ distPath: path.resolve(pluginOptions.distPath),
21998
+ machineUserName: pluginOptions.machineUserName,
21999
+ namespaces: buildSeedNamespaceConfigs(namespaces),
22000
+ idpUser
22001
+ };
22002
+ }
22003
+
20689
22004
  //#endregion
20690
22005
  //#region src/cli/shared/tailordb-namespace.ts
20691
22006
  /**
@@ -22701,5 +24016,5 @@ async function registerTsHook(tsHookUrl) {
22701
24016
  }
22702
24017
 
22703
24018
  //#endregion
22704
- export { getOrganization as $, hasUserTokenEntry as $n, ensureConfigId as $t, truncate as A, loadDiff as An, getExecutorJob as At, show as B, sdkAppIdLabelKey as Bn, getWorkflowExecution as Bt, healthCommand as C, createSnapshotFromLocalTypes as Cn, resolveMachineUserInputSource as Cr, getFunctionRegistry as Ct, resumeWorkflow as D, getMigrationFilePath as Dn, triggerExecutor as Dt, resumeCommand as E, getMigrationDirPath as En, triggerCommand as Et, writeDbTypesFile as F, formatMigrationDiff as Fn, startCommand as Ft, removeCommand$1 as G, generateUserTypes as Gn, deploy as Gt, extractOwnedNamespaces as H, sdkVersionLabelKey as Hn, functionExecutionStatusToString as Ht, getConfiguredEditorCommand as I, hasChanges as In, startWorkflow as It, organizationTree as J, assertWritable as Jn, listWorkspaces as Jt, updateCommand$1 as K, prompt as Kn, deployFromCLI as Kt, openInConfiguredEditor as L, getNamespacesWithMigrations as Ln, getCommand$5 as Lt, generate as M, formatMigrationNumber as Mn, jobsCommand as Mt, generateCommand as N, parseMigrationNumberArg as Nn, listExecutorJobs as Nt, listCommand$2 as O, getMigrationFiles as On, listCommand$8 as Ot, generateMigrationScript as P, formatDiffSummary as Pn, watchExecutorJob as Pt, getCommand$1 as Q, hasAnyUserTokenEntry as Qn, workspaceNameSchema as Qt, deployCommand as R, selectTargetNamespace as Rn, getWorkflow as Rt, getAppHealth as S, compareSnapshots as Sn, paginationArgs as Sr, getCommand$4 as St, waitWorkflowExecution as T, getLatestMigrationNumber as Tn, workspaceArgs as Tr, webhookCommand as Tt, logBetaWarning as U, writeMetadataLabels as Un, getCommand$6 as Ut, showCommand as V, sdkNameLabelKey as Vn, listWorkflowExecutions as Vt, remove as W, PluginManager as Wn, getExecutor as Wt, listCommand$3 as X, deleteUserTokens as Xn, createWorkspace$1 as Xt, treeCommand as Y, loadConfig as Yn, createCommand$1 as Yt, listOrganizations as Z, fetchLatestToken as Zn, formatKeyValueTable as Zt, getWorkspace as _, MIGRATE_FILE_NAME as _n, confirmationArgs as _r, listMachineUsers as _t, queryCommand as a, toTailorDBDeployInput as an, loadStoredUserTokens as ar, getFolder as at, listApps as b, assertValidMigrationFiles as bn, multiConfigArg as br, listCommand$7 as bt, removeCommand as c, handleOptionalToRequiredError as cn, readPlatformConfig as cr, createCommand as ct, listUsers as d, compareSnapshotWithRemote as dn, resolveUserTokenKey as dr, listOAuth2Clients as dt, executeScript as en, loadAccessToken as er, updateCommand$2 as et, inviteCommand as f, generateAllTypeManifestsFromSnapshot as fn, saveUserTokens as fr, getCommand$3 as ft, getCommand as g, INITIAL_SCHEMA_NUMBER as gn, configArg as gr, listCommand$6 as gt, restoreWorkspace as h, DIFF_FILE_NAME as hn, commonArgs as hr, tokenCommand as ht, query as i, logRemoteDriftGuidance as in, loadPlatformClientConfig as ir, getCommand$2 as it, truncateCommand as j, reconstructSnapshotFromMigrations as jn, getExecutorWaitFailureMessage as jt, listWorkflows as k, getNextMigrationNumber as kn, listExecutors as kt, removeUser as l, parseMigrationLabelNumber as ln, removeLegacyUserAlias as lr, createFolder as lt, restoreCommand as m, DB_TYPES_FILE_NAME as mn, defineAppCommand as mr, getMachineUserToken as mt, errorToJson as n, bundleMigrationScript as nn, loadConsoleBaseUrl as nr, listCommand$4 as nt, updateCommand as o, verifyRemoteSchema as on, loadWorkspaceId as or, deleteCommand$1 as ot, inviteUser as p, protoGqlPermission as pn, writePlatformConfig as pr, getOAuth2Client as pt, updateOrganization as q, apiCall as qn, listCommand$9 as qt, serializeError as r, checkMigrationDiffs as rn, loadMachineUserName as rr, listFolders as rt, updateUser as s, MIGRATION_LABEL_KEY as sn, platformConfigFromProfile as sr, deleteFolder as st, registerTsHook as t, waitForExecution as tn, loadConfigPath as tr, updateFolder as tt, listCommand as u, sanitizeMigrationLabel as un, resolveConfigUser as ur, listCommand$5 as ut, deleteCommand as v, SCHEMA_FILE_NAME as vn, deploymentArgs as vr, generate$1 as vt, waitCommand as w, formatSchemaDrifts as wn, toPageDirection as wr, listWebhookExecutors as wt, listCommand$1 as x, compareLocalTypesWithSnapshot as xn, pagedLogArgs as xr, listFunctionRegistries as xt, deleteWorkspace as y, assertMigrationNumberExists as yn, isVerbose as yr, getAuthInput as yt, deployStaticWebsite as z, resourceTrn as zn, executionsCommand as zt };
22705
- //# sourceMappingURL=register-ts-hook-LYV7zH-e.mjs.map
24019
+ export { treeCommand as $, formatDiffSummary as $n, listWorkspaces as $t, truncate as A, MIGRATION_NUMBER_PATTERN as An, removeLegacyUserAlias as Ar, triggerExecutor as At, MIGRATION_REVIEW_REQUIRED_MARKER as B, getMigrationDirPath as Bn, isVerbose as Br, getCommand$5 as Bt, healthCommand as C, captureFileState as Cn, loadConsoleBaseUrl as Cr, listCommand$7 as Ct, resumeWorkflow as D, INITIAL_SCHEMA_NUMBER as Dn, loadWorkspaceId as Dr, listWebhookExecutors as Dt, resumeCommand as E, DIFF_FILE_NAME as En, loadStoredUserTokens as Er, getFunctionRegistry as Et, chunkSeedData as F, compareLocalTypesWithSnapshot as Fn, defineAppCommand as Fr, jobsCommand as Ft, showCommand as G, loadSnapshot as Gn, toPageDirection as Gr, functionExecutionStatusToString as Gt, deployCommand as H, getMigrationFiles as Hn, pagedLogArgs as Hr, executionsCommand as Ht, bundleSeedScript as I, compareSnapshots as In, commonArgs as Ir, listExecutorJobs as It, remove as J, MIGRATION_HISTORY_LABEL_KEY as Jn, deploy as Jt, extractOwnedNamespaces as K, normalizeSchemaSnapshot as Kn, workspaceArgs as Kr, getCommand$6 as Kt, generate as L, createSnapshotFromLocalTypes as Ln, configArg as Lr, watchExecutorJob as Lt, loadSeedContext as M, UnsupportedMigrationFileVersionError as Mn, resolveUserTokenKey as Mr, listExecutors as Mt, loadApplicationNamespaces as N, assertMigrationNumberExists as Nn, saveUserTokens as Nr, getExecutorJob as Nt, listCommand$2 as O, MIGRATE_FILE_NAME as On, platformConfigFromProfile as Or, webhookCommand as Ot, loadTailorDBNamespaces as P, assertValidMigrationFiles as Pn, writePlatformConfig as Pr, getExecutorWaitFailureMessage as Pt, organizationTree as Q, sanitizeMigrationLabel as Qn, listCommand$9 as Qt, generateCommand as R, formatSchemaDrifts as Rn, confirmationArgs as Rr, startCommand as Rt, getAppHealth as S, protoGqlPermission as Sn, loadConfigPath as Sr, generate$1 as St, waitWorkflowExecution as T, DB_TYPES_FILE_NAME as Tn, loadPlatformClientConfig as Tr, getCommand$4 as Tt, deployStaticWebsite as U, getNextMigrationNumber as Un, paginationArgs as Ur, getWorkflowExecution as Ut, generateSchemaFile as V, getMigrationFilePath as Vn, multiConfigArg as Vr, getWorkflow as Vt, show as W, loadDiff as Wn, resolveMachineUserInputSource as Wr, listWorkflowExecutions as Wt, updateCommand$1 as X, createMigrationHistoryId as Xn, deployMigrationTestBaseline as Xt, removeCommand$1 as Y, MIGRATION_LABEL_KEY as Yn, deployFromCLI as Yt, updateOrganization as Z, handleOptionalToRequiredError as Zn, deployMigrationTestTarget as Zt, getWorkspace as _, verifyRemoteSchema as _n, deleteUserTokens as _r, getOAuth2Client as _t, queryCommand as a, ensureConfigId as an, sdkAppIdLabelKey as ar, updateFolder as at, listApps as b, compareSnapshotWithRemote as bn, hasUserTokenEntry as br, listCommand$6 as bt, removeCommand as c, executeScript as cn, writeMetadataLabels as cr, getCommand$2 as ct, listUsers as d, bundleMigrationScript as dn, formatMigrationNumber as dr, deleteFolder as dt, createCommand$1 as en, formatMigrationDiff as er, listCommand$3 as et, inviteCommand as f, checkMigrationDiffs as fn, parseMigrationNumberArg as fr, createCommand as ft, getCommand as g, toTailorDBDeployInput as gn, loadConfig as gr, getCommand$3 as gt, restoreWorkspace as h, logRemoteDriftGuidance as hn, assertWritable as hr, listOAuth2Clients as ht, query as i, workspaceNameSchema as in, resourceTrn as ir, updateCommand$2 as it, truncateCommand as j, SCHEMA_FILE_NAME as jn, resolveConfigUser as jr, listCommand$8 as jt, listWorkflows as k, MIGRATE_TEST_FILE_NAME as kn, readPlatformConfig as kr, triggerCommand as kt, removeUser as l, waitForExecution as ln, PluginManager as lr, getFolder as lt, restoreCommand as m, formatRemoteVerificationResults as mn, apiCall as mr, listCommand$5 as mt, errorToJson as n, createWorkspace$1 as nn, getNamespacesWithMigrations as nr, getCommand$1 as nt, updateCommand as o, getMigrationMachineUser as on, sdkNameLabelKey as or, listCommand$4 as ot, inviteUser as p, fetchRemoteSchemaSnapshot as pn, prompt as pr, createFolder as pt, logBetaWarning as q, reconstructSnapshotFromMigrations as qn, getExecutor as qt, serializeError as r, formatKeyValueTable as rn, selectTargetNamespace as rr, getOrganization as rt, updateUser as s, updateMigrationLabel as sn, sdkVersionLabelKey as sr, listFolders as st, registerTsHook as t, createValidatedWorkspaceWithClient as tn, hasChanges as tr, listOrganizations as tt, listCommand as u, formatMigrationScriptCommand as un, generateUserTypes as ur, deleteCommand$1 as ut, deleteCommand as v, fetchRemoteMigrationNumber as vn, fetchLatestToken as vr, getMachineUserToken as vt, waitCommand as w, captureMigrationFileState as wn, loadMachineUserName as wr, listFunctionRegistries as wt, listCommand$1 as x, generateAllTypeManifestsFromSnapshot as xn, loadAccessToken as xr, listMachineUsers as xt, deleteWorkspace as y, fetchRemoteMigrationState as yn, hasAnyUserTokenEntry as yr, tokenCommand as yt, scriptCommand as z, getLatestMigrationNumber as zn, deploymentArgs as zr, startWorkflow as zt };
24020
+ //# sourceMappingURL=register-ts-hook-CTth1eqj.mjs.map