@tailor-platform/sdk 2.1.0 → 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.
- package/CHANGELOG.md +84 -0
- package/{dist/cli/index.mjs → bin/tailor.mjs} +1 -1
- package/dist/{application-D77KJFKD.mjs → application-BpLeawBg.mjs} +6 -6
- package/dist/{application-D77KJFKD.mjs.map → application-BpLeawBg.mjs.map} +1 -1
- package/dist/application-BxLLiLsr.mjs +3 -0
- package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
- package/dist/cli/commands/deploy/types.d.mts +2 -1
- package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +11 -4
- package/dist/cli/commands/tailordb/migrate/file-state.d.mts +1 -0
- package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +2 -2
- package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +8 -1
- package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +5 -3
- package/dist/cli/lib.d.mts +4 -4
- package/dist/cli/lib.mjs +3 -482
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/cli/main.mjs +975 -193
- package/dist/cli/main.mjs.map +1 -1
- package/dist/completion/zsh-worker.zsh +145 -2
- package/dist/configure/index.d.mts +2 -2
- package/dist/configure/services/index.d.mts +2 -1
- package/dist/configure/services/tailordb/index.d.mts +2 -1
- package/dist/configure/services/tailordb/schema.d.mts +46 -46
- package/dist/configure/services/tailordb/types.d.mts +30 -4
- package/dist/{crashreport-B5LqCTfF.mjs → crashreport-CIIfLnoO.mjs} +2 -2
- package/dist/{crashreport-B5LqCTfF.mjs.map → crashreport-CIIfLnoO.mjs.map} +1 -1
- package/dist/{crashreport-eI7ty7ok.mjs → crashreport-CINZ5dVN.mjs} +1 -1
- package/dist/errors-CWj21238.mjs +75 -0
- package/dist/errors-CWj21238.mjs.map +1 -0
- package/dist/kysely/index.d.mts +52 -5
- package/dist/kysely/index.mjs +7 -2
- package/dist/kysely/index.mjs.map +1 -1
- package/dist/{logger-CXQq9YIp.mjs → logger-DSdExfor.mjs} +83 -42
- package/dist/logger-DSdExfor.mjs.map +1 -0
- package/dist/plugin/builtin/seed/index.mjs +1 -1
- package/dist/{register-ts-hook-BU18uU44.mjs → register-ts-hook-CTth1eqj.mjs} +1587 -272
- package/dist/register-ts-hook-CTth1eqj.mjs.map +1 -0
- package/dist/schema--xYWRGfe.mjs.map +1 -1
- package/dist/seed/index.d.mts +52 -3
- package/dist/seed/index.mjs +200 -44
- package/dist/seed/index.mjs.map +1 -1
- package/dist/{seed-B-yckgQY.mjs → seed-xqM9WIHP.mjs} +8 -8
- package/dist/seed-xqM9WIHP.mjs.map +1 -0
- package/dist/{service-B5WynBHQ.mjs → service-CCwl3Avt.mjs} +3 -3
- package/dist/{service-B5WynBHQ.mjs.map → service-CCwl3Avt.mjs.map} +1 -1
- package/dist/service-_XmjSEGr.mjs +3 -0
- package/dist/{service-k1xy_A5G.mjs → service-goqlJJgA.mjs} +2 -2
- package/dist/{service-k1xy_A5G.mjs.map → service-goqlJJgA.mjs.map} +1 -1
- package/dist/{service_pb-D5--My_M.mjs → service_pb-BEWj1pOV.mjs} +2 -2
- package/dist/{service_pb-D5--My_M.mjs.map → service_pb-BEWj1pOV.mjs.map} +1 -1
- package/dist/{service_pb-i8CmnQP4.mjs → service_pb-BjlhvK9E.mjs} +1 -1
- package/dist/utils/test/index.d.mts +6 -3
- package/dist/utils/test/index.mjs +25 -13
- package/dist/utils/test/index.mjs.map +1 -1
- package/dist/vitest/index.d.mts +2 -1
- package/dist/vitest/index.mjs +84 -2
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mocks/file.d.mts +1 -1
- package/dist/vitest/pglite-kysely.d.mts +43 -0
- package/dist/{workspace_resource_pb-DXnO2AT-.mjs → workspace_resource_pb-HNIL_Vfj.mjs} +10 -2
- package/dist/{workspace_resource_pb-DXnO2AT-.mjs.map → workspace_resource_pb-HNIL_Vfj.mjs.map} +1 -1
- package/docs/cli/tailordb.md +87 -22
- package/docs/cli-reference.md +13 -11
- package/docs/configuration.md +1 -1
- package/docs/migration/v2.md +1 -1
- package/docs/plugin/custom.md +48 -48
- package/docs/plugin/index.md +24 -24
- package/docs/services/auth.md +10 -10
- package/docs/services/executor.md +1 -1
- package/docs/services/resolver.md +2 -2
- package/docs/services/tailordb-migration.md +375 -76
- package/docs/services/tailordb.md +28 -28
- package/docs/testing.md +2 -0
- package/package.json +12 -7
- package/dist/application-Af1zIqSI.mjs +0 -3
- package/dist/errors-D7583Zz7.mjs +0 -58
- package/dist/errors-D7583Zz7.mjs.map +0 -1
- package/dist/logger-CXQq9YIp.mjs.map +0 -1
- package/dist/register-ts-hook-BU18uU44.mjs.map +0 -1
- package/dist/seed-B-yckgQY.mjs.map +0 -1
- 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
|
|
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-
|
|
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-
|
|
6
|
-
import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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":
|
|
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/
|
|
7654
|
+
//#region src/cli/commands/tailordb/migrate/field-type-change.ts
|
|
7602
7655
|
/**
|
|
7603
|
-
*
|
|
7604
|
-
*
|
|
7605
|
-
*
|
|
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
|
|
7608
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
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
|
|
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:
|
|
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)
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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":
|
|
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/
|
|
10389
|
-
/**
|
|
10390
|
-
* Types for TailorDB migration execution
|
|
10391
|
-
*/
|
|
10702
|
+
//#region src/cli/commands/tailordb/migrate/remote-state.ts
|
|
10392
10703
|
/**
|
|
10393
|
-
*
|
|
10394
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10407
|
-
*
|
|
10408
|
-
*
|
|
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
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
11088
|
-
|
|
11089
|
-
|
|
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: {
|
|
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)
|
|
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
|
-
|
|
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
|
-
*
|
|
11315
|
-
*
|
|
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
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
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
|
|
11417
|
-
} catch (
|
|
11418
|
-
logger.warn(`
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
11692
|
-
deletedResources.gqlPermissions.add(permKey);
|
|
11693
|
-
return true;
|
|
11694
|
-
}
|
|
11695
|
-
return false;
|
|
12077
|
+
return deletedTypeNames.has(typeName);
|
|
11696
12078
|
});
|
|
11697
|
-
|
|
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
|
-
|
|
11702
|
-
deletedResources.types.add(typeName);
|
|
11703
|
-
return true;
|
|
11704
|
-
}
|
|
11705
|
-
return false;
|
|
12086
|
+
return deletedTypeNames.has(typeName);
|
|
11706
12087
|
});
|
|
11707
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
20660
|
-
|
|
20661
|
-
|
|
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 {
|
|
22705
|
-
//# sourceMappingURL=register-ts-hook-
|
|
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
|