@tailor-platform/sdk 2.2.0 → 2.3.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 +115 -0
- package/dist/application-BZfk4HKm.mjs +3 -0
- package/dist/{application-BpLeawBg.mjs → application-F-nl107y.mjs} +118 -24
- package/dist/application-F-nl107y.mjs.map +1 -0
- package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +43 -20
- package/dist/cli/commands/tailordb/migrate/generate.d.mts +6 -0
- package/dist/cli/commands/tailordb/migrate/rename-detection.d.mts +22 -0
- package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +1 -1
- package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +26 -7
- package/dist/cli/lib.mjs +2 -2
- package/dist/cli/main.mjs +203 -42
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/ts-hook.mjs +52 -7
- package/dist/completion/zsh-worker.zsh +23 -3
- package/dist/configure/index.d.mts +2 -2
- package/dist/configure/index.mjs +90 -23
- package/dist/configure/index.mjs.map +1 -1
- package/dist/configure/services/index.d.mts +2 -2
- package/dist/configure/services/workflow/index.d.mts +2 -2
- package/dist/configure/services/workflow/wait-point.d.mts +72 -13
- package/dist/{errors-CWj21238.mjs → errors-BVb6vYGy.mjs} +16 -2
- package/dist/{errors-CWj21238.mjs.map → errors-BVb6vYGy.mjs.map} +1 -1
- package/dist/kysely/index.d.mts +2 -1
- package/dist/kysely/index.mjs.map +1 -1
- package/dist/{register-ts-hook-CTth1eqj.mjs → register-ts-hook-ClI226n2.mjs} +2034 -511
- package/dist/register-ts-hook-ClI226n2.mjs.map +1 -0
- package/dist/{service-goqlJJgA.mjs → service-C_WpbKHu.mjs} +38 -4
- package/dist/service-C_WpbKHu.mjs.map +1 -0
- package/dist/service-D1RCdzIL.mjs +3 -0
- package/dist/{service-CCwl3Avt.mjs → service-hZskxZmg.mjs} +2 -2
- package/dist/{service-CCwl3Avt.mjs.map → service-hZskxZmg.mjs.map} +1 -1
- package/dist/vitest/index.mjs +83 -1
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mocks/file.d.mts +1 -1
- package/dist/vitest/mocks/workflow.d.mts +13 -1
- package/dist/wait-point-invoker-__oE88_P.mjs +148 -0
- package/dist/wait-point-invoker-__oE88_P.mjs.map +1 -0
- package/dist/wait-point-registry-TL99zotw.mjs +47 -0
- package/dist/wait-point-registry-TL99zotw.mjs.map +1 -0
- package/docs/cli/setup.md +14 -1
- package/docs/cli/tailordb.md +9 -6
- package/docs/cli-reference.md +2 -1
- package/docs/github-actions.md +73 -28
- package/docs/migration/v2.md +54 -0
- package/docs/services/tailordb-migration.md +137 -35
- package/docs/services/workflow.md +52 -2
- package/docs/testing.md +12 -0
- package/package.json +4 -4
- package/dist/application-BpLeawBg.mjs.map +0 -1
- package/dist/application-BxLLiLsr.mjs +0 -3
- package/dist/register-ts-hook-CTth1eqj.mjs.map +0 -1
- package/dist/service-_XmjSEGr.mjs +0 -3
- package/dist/service-goqlJJgA.mjs.map +0 -1
- package/dist/test-env-key-D7UkZp99.mjs +0 -75
- package/dist/test-env-key-D7UkZp99.mjs.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { t as db } from "./schema--xYWRGfe.mjs";
|
|
2
|
+
import { t as beginWaitPointScope } from "./wait-point-registry-TL99zotw.mjs";
|
|
2
3
|
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
4
|
import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
|
|
4
5
|
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
|
|
6
|
-
import { c as stripTailorDBTypeBuilderHelpers, g as
|
|
6
|
+
import { $ as resolveStaticWebsiteUrls, A as hasGenerationHooks, B as fetchPaged, C as resolveTSConfigWithFallback, G as getOrNull, H as fetchUserInfo, I as defaultPlatformBaseUrl, J as initOperatorClient, K as getPlatformBaseUrl, L as fetchAll, M as getDistDir, N as hashContent, O as createBundleLog, P as hashFile, Q as rememberPlatformConfigForToken, R as fetchAllTolerant, S as createTsconfigPathsPlugin, U as getConsoleBaseUrl, W as getOAuth2ClientId, X as isNotFoundError, Y as isDefaultPlatform, Z as normalizeBaseUrl, _ as assertUniqueTailorDBTypeNamesWithExternal, b as tailorPrincipalMap, d as HTTP_METHODS, et as byName, g as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as createBundleCache, k as getPluginGenerationDependencies, m as buildResolverOperationHookExpr, n as generatePluginFilesIfNeeded, nt as LOG_LEVELS, o as getApplicationAuthNamespace, p as buildExecutorArgsExpr, q as initOAuth2Client, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, tt as createApplyLimiter, v as PRINCIPAL_VAR, w as platformBundleDefinePlugin, x as createGeneratedEntryResolverPlugin, y as stringifyFunction, z as fetchMachineUserToken } from "./application-F-nl107y.mjs";
|
|
7
|
+
import { _ as beginUserModuleRun, b as functionSchema, c as stripTailorDBTypeBuilderHelpers, g as IMPORT_NONCE_PARAM, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1, v as currentImportNonce } from "./service-C_WpbKHu.mjs";
|
|
7
8
|
import { t as multiline } from "./multiline-sfHpTZZK.mjs";
|
|
8
9
|
import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
|
|
9
10
|
import { t as userAgent } from "./user-agent-Bgsszb5I.mjs";
|
|
10
|
-
import { i as isCLIError, n as formatCopyableCommand, t as createCLIError } from "./errors-
|
|
11
|
+
import { a as typeOnlyImportHint, i as isCLIError, n as formatCopyableCommand, t as createCLIError } from "./errors-BVb6vYGy.mjs";
|
|
11
12
|
import { n as tightenSecretFilePermissions, r as writeSecretFile } from "./secret-file-C3J4Gy8r.mjs";
|
|
12
13
|
import { r as withSpan } from "./telemetry-BRVdwh14.mjs";
|
|
13
14
|
import { a as generateIdpTruncateScriptCode, i as generateIdpSeedScriptCode, o as processIdpUser, r as processLinesDb, t as SeedGeneratorID } from "./seed-xqM9WIHP.mjs";
|
|
@@ -1375,7 +1376,8 @@ async function loadConfig(configPath, options = {}) {
|
|
|
1375
1376
|
const resolvedPath = path.resolve(process.cwd(), foundPath);
|
|
1376
1377
|
if (!fs$2.existsSync(resolvedPath)) throw new Error(`Configuration file not found: ${configPath}`);
|
|
1377
1378
|
const configUrl = pathToFileURL(resolvedPath);
|
|
1378
|
-
|
|
1379
|
+
const importNonce = options.importNonce ?? currentImportNonce();
|
|
1380
|
+
if (importNonce) configUrl.searchParams.set(IMPORT_NONCE_PARAM, importNonce);
|
|
1379
1381
|
const configModule = await import(configUrl.href);
|
|
1380
1382
|
if (!configModule || !configModule.default) throw new Error("Invalid Tailor config module: default export not found");
|
|
1381
1383
|
const validated = AppConfigSchema.safeParse(configModule.default);
|
|
@@ -2421,9 +2423,9 @@ function toComparableProtoJson(schema, value) {
|
|
|
2421
2423
|
* @param value - Value to serialize
|
|
2422
2424
|
* @returns Stable serialized string
|
|
2423
2425
|
*/
|
|
2424
|
-
function stableStringify(value) {
|
|
2425
|
-
if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : stableStringify(item)).join(",")}]`;
|
|
2426
|
-
if (value && typeof value === "object") return `{${Object.entries(value).filter(([key, entryValue]) => key !== "$typeName" && entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
|
|
2426
|
+
function stableStringify$1(value) {
|
|
2427
|
+
if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : stableStringify$1(item)).join(",")}]`;
|
|
2428
|
+
if (value && typeof value === "object") return `{${Object.entries(value).filter(([key, entryValue]) => key !== "$typeName" && entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify$1(entryValue)}`).join(",")}}`;
|
|
2427
2429
|
if (typeof value === "bigint") return JSON.stringify(value.toString());
|
|
2428
2430
|
return JSON.stringify(value);
|
|
2429
2431
|
}
|
|
@@ -2434,7 +2436,7 @@ function stableStringify(value) {
|
|
|
2434
2436
|
*/
|
|
2435
2437
|
function normalizeProtoConfig(value) {
|
|
2436
2438
|
if (value === void 0 || value === null) return value;
|
|
2437
|
-
return JSON.parse(stableStringify(value));
|
|
2439
|
+
return JSON.parse(stableStringify$1(value));
|
|
2438
2440
|
}
|
|
2439
2441
|
/**
|
|
2440
2442
|
* Sort a string array for order-insensitive comparison.
|
|
@@ -2451,7 +2453,7 @@ function normalizeStringArray(values) {
|
|
|
2451
2453
|
* @returns True when normalized values are equal
|
|
2452
2454
|
*/
|
|
2453
2455
|
function areNormalizedEqual(left, right) {
|
|
2454
|
-
return stableStringify(normalizeProtoConfig(left)) === stableStringify(normalizeProtoConfig(right));
|
|
2456
|
+
return stableStringify$1(normalizeProtoConfig(left)) === stableStringify$1(normalizeProtoConfig(right));
|
|
2455
2457
|
}
|
|
2456
2458
|
|
|
2457
2459
|
//#endregion
|
|
@@ -7435,13 +7437,17 @@ function buildValidateStatements(fields, accessExpr, keyPrefix) {
|
|
|
7435
7437
|
}
|
|
7436
7438
|
return statements;
|
|
7437
7439
|
}
|
|
7440
|
+
function principalDeclIfReferenced(...exprs) {
|
|
7441
|
+
return exprs.some((expr) => expr?.includes("_principal")) ? ` const ${PRINCIPAL_VAR} = (${tailorPrincipalMap});` : "";
|
|
7442
|
+
}
|
|
7438
7443
|
function wrapHook(objectExpr) {
|
|
7439
|
-
return `((_invoker) => { const ${NOW} = new Date()
|
|
7444
|
+
return `((_invoker) => { const ${NOW} = new Date();${principalDeclIfReferenced(objectExpr)} return ${objectExpr}; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
|
|
7440
7445
|
}
|
|
7441
7446
|
function wrapValidate(statements, typeValidateExpr) {
|
|
7442
7447
|
const issuesFn = typeValidateExpr ? " const __issues = (f, m) => { __errs[f] = m; };" : "";
|
|
7448
|
+
const principalDecl = principalDeclIfReferenced(typeValidateExpr, ...statements);
|
|
7443
7449
|
const typeValidateStmt = typeValidateExpr ? ` ${typeValidateExpr};` : "";
|
|
7444
|
-
return `((_invoker) => { const __errs = {};${issuesFn}\n${statements.join("\n")}${typeValidateStmt}\nreturn __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
|
|
7450
|
+
return `((_invoker) => { const __errs = {};${issuesFn}${principalDecl}\n${statements.join("\n")}${typeValidateStmt}\nreturn __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
|
|
7445
7451
|
}
|
|
7446
7452
|
/**
|
|
7447
7453
|
* Aggregate every field's create/update hook, default, and validate into
|
|
@@ -7464,8 +7470,10 @@ function buildTypeScripts(fields, options) {
|
|
|
7464
7470
|
const perFieldExpr = buildHookObject(fields, INPUT, OLD_RECORD, operation);
|
|
7465
7471
|
const typeLevelExpr = typeHookExpr?.[operation];
|
|
7466
7472
|
let expr;
|
|
7467
|
-
if (perFieldExpr !== null && typeLevelExpr)
|
|
7468
|
-
|
|
7473
|
+
if (perFieldExpr !== null && typeLevelExpr) {
|
|
7474
|
+
const principalDecl = principalDeclIfReferenced(perFieldExpr, typeLevelExpr);
|
|
7475
|
+
expr = `((_invoker) => { const ${NOW} = new Date();${principalDecl} const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
|
|
7476
|
+
} else if (typeLevelExpr) expr = wrapHook(typeLevelExpr);
|
|
7469
7477
|
else if (perFieldExpr !== null) expr = wrapHook(perFieldExpr);
|
|
7470
7478
|
if (expr) hook[operation] = { expr: expr + hashSuffix };
|
|
7471
7479
|
}
|
|
@@ -7501,12 +7509,12 @@ function formatMigrationDiff(diff) {
|
|
|
7501
7509
|
const lines = [];
|
|
7502
7510
|
const changesByType = /* @__PURE__ */ new Map();
|
|
7503
7511
|
for (const change of diff.changes) {
|
|
7504
|
-
const existing = changesByType.get(change.
|
|
7512
|
+
const existing = changesByType.get(change.tableName) ?? [];
|
|
7505
7513
|
existing.push(change);
|
|
7506
|
-
changesByType.set(change.
|
|
7514
|
+
changesByType.set(change.tableName, existing);
|
|
7507
7515
|
}
|
|
7508
|
-
for (const [
|
|
7509
|
-
lines.push(`${diff.namespace}.${
|
|
7516
|
+
for (const [tableName, changes] of changesByType) {
|
|
7517
|
+
lines.push(`${diff.namespace}.${tableName}:`);
|
|
7510
7518
|
for (const change of changes) lines.push(formatDiffChange(change));
|
|
7511
7519
|
}
|
|
7512
7520
|
return lines.join("\n");
|
|
@@ -7518,10 +7526,11 @@ function formatMigrationDiff(diff) {
|
|
|
7518
7526
|
*/
|
|
7519
7527
|
function formatDiffChange(change) {
|
|
7520
7528
|
switch (change.kind) {
|
|
7521
|
-
case "
|
|
7522
|
-
case "
|
|
7523
|
-
case "
|
|
7524
|
-
case "
|
|
7529
|
+
case "table_added": return ` + [Table] ${change.tableName} (new table)`;
|
|
7530
|
+
case "table_removed": return ` - [Table] ${change.tableName} (removed)`;
|
|
7531
|
+
case "table_renamed": return ` ~ [Table] ${change.previousTableName} → ${change.tableName} (renamed)`;
|
|
7532
|
+
case "table_modified": return ` ~ [Table] ${change.tableName}: ${change.reason}`;
|
|
7533
|
+
case "table_settings_modified": return ` ~ [Table Settings] ${change.tableName}: ${change.reason ?? "settings changed"}`;
|
|
7525
7534
|
case "field_added": {
|
|
7526
7535
|
const typeStr = formatFieldType(change.after);
|
|
7527
7536
|
return ` + ${change.fieldName}: ${typeStr}`;
|
|
@@ -7529,6 +7538,7 @@ function formatDiffChange(change) {
|
|
|
7529
7538
|
case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
|
|
7530
7539
|
case "field_modified":
|
|
7531
7540
|
case "field_type_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
|
|
7541
|
+
case "field_renamed": return ` ~ ${change.previousFieldName} → ${change.fieldName}: ${formatFieldType(change.after)} (renamed)`;
|
|
7532
7542
|
case "index_added": return ` + [Index] ${change.indexName}`;
|
|
7533
7543
|
case "index_removed": return ` - [Index] ${change.indexName}`;
|
|
7534
7544
|
case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
|
|
@@ -7539,10 +7549,10 @@ function formatDiffChange(change) {
|
|
|
7539
7549
|
case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
|
|
7540
7550
|
case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
|
|
7541
7551
|
case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
|
|
7542
|
-
case "
|
|
7552
|
+
case "table_scripts_modified": return ` ~ [Table Scripts] ${change.tableName}: ${change.reason ?? "table-level hooks/validate changed"}`;
|
|
7543
7553
|
default: {
|
|
7544
7554
|
const unknown = change;
|
|
7545
|
-
return ` ? ${unknown.
|
|
7555
|
+
return ` ? ${unknown.tableName}.${unknown.fieldName ?? ""}`;
|
|
7546
7556
|
}
|
|
7547
7557
|
}
|
|
7548
7558
|
}
|
|
@@ -7598,7 +7608,7 @@ function formatBreakingChanges(breakingChanges) {
|
|
|
7598
7608
|
if (breakingChanges.length === 0) return "";
|
|
7599
7609
|
const lines = ["Breaking changes detected:", ""];
|
|
7600
7610
|
for (const bc of breakingChanges) {
|
|
7601
|
-
const location = bc.fieldName ? `${bc.
|
|
7611
|
+
const location = bc.fieldName ? `${bc.tableName}.${bc.fieldName}` : bc.tableName;
|
|
7602
7612
|
lines.push(` - ${location}: ${bc.reason}`);
|
|
7603
7613
|
}
|
|
7604
7614
|
return lines.join("\n");
|
|
@@ -7612,19 +7622,21 @@ function formatWarnings(warnings) {
|
|
|
7612
7622
|
if (warnings.length === 0) return "";
|
|
7613
7623
|
const lines = ["Warning: data loss possible:", ""];
|
|
7614
7624
|
for (const w of warnings) {
|
|
7615
|
-
const location = w.fieldName ? `${w.
|
|
7625
|
+
const location = w.fieldName ? `${w.tableName}.${w.fieldName}` : w.tableName;
|
|
7616
7626
|
lines.push(` - ${location}: ${w.reason}`);
|
|
7617
7627
|
}
|
|
7618
7628
|
return lines.join("\n");
|
|
7619
7629
|
}
|
|
7620
7630
|
const DIFF_CHANGE_LABELS = {
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7631
|
+
table_added: "table(s) added",
|
|
7632
|
+
table_removed: "table(s) removed",
|
|
7633
|
+
table_renamed: "table(s) renamed",
|
|
7634
|
+
table_modified: "table(s) modified",
|
|
7635
|
+
table_settings_modified: "table setting(s) modified",
|
|
7625
7636
|
field_added: "field(s) added",
|
|
7626
7637
|
field_removed: "field(s) removed",
|
|
7627
7638
|
field_modified: "field(s) modified",
|
|
7639
|
+
field_renamed: "field(s) renamed",
|
|
7628
7640
|
field_type_modified: "field type(s) modified",
|
|
7629
7641
|
index_added: "index(es) added",
|
|
7630
7642
|
index_removed: "index(es) removed",
|
|
@@ -7636,7 +7648,7 @@ const DIFF_CHANGE_LABELS = {
|
|
|
7636
7648
|
relationship_removed: "relationship(s) removed",
|
|
7637
7649
|
relationship_modified: "relationship(s) modified",
|
|
7638
7650
|
permission_modified: "permission(s) modified",
|
|
7639
|
-
|
|
7651
|
+
table_scripts_modified: "table script(s) modified"
|
|
7640
7652
|
};
|
|
7641
7653
|
/**
|
|
7642
7654
|
* Format a summary of the migration diff
|
|
@@ -7656,13 +7668,32 @@ function formatDiffSummary(diff) {
|
|
|
7656
7668
|
* Scalar type changes whose complete source domain is accepted by the target
|
|
7657
7669
|
* representation without relying on per-row index recreation.
|
|
7658
7670
|
*
|
|
7671
|
+
* The whole source domain has to cast, not just the values present when the
|
|
7672
|
+
* script runs: the field keeps its previous type until the post-migration
|
|
7673
|
+
* phase, so an application can still write any value the source type allows
|
|
7674
|
+
* after the script has passed over the table. A pair that needs the script to
|
|
7675
|
+
* rewrite values first — `string` to `integer`, say, where `"abc"` never
|
|
7676
|
+
* casts — would fail that late write at the post-migration phase.
|
|
7677
|
+
*
|
|
7678
|
+
* Values must also read back with their meaning intact. Date, datetime, and
|
|
7679
|
+
* time values are not stored in the textual form they were written in, so
|
|
7680
|
+
* converting them to or from another type reads back as a different instant;
|
|
7681
|
+
* those pairs stay off this list even though the platform accepts the schema
|
|
7682
|
+
* change.
|
|
7683
|
+
*
|
|
7659
7684
|
* Keep this list deliberately narrow. Pair-specific platform experiments can
|
|
7660
7685
|
* extend it once both indexed and unindexed read paths have been verified.
|
|
7661
7686
|
*/
|
|
7662
7687
|
const IN_PLACE_TYPE_CHANGES = /* @__PURE__ */ new Set([
|
|
7688
|
+
"boolean:string",
|
|
7689
|
+
"decimal:float",
|
|
7663
7690
|
"decimal:string",
|
|
7664
7691
|
"enum:string",
|
|
7692
|
+
"float:decimal",
|
|
7693
|
+
"float:string",
|
|
7694
|
+
"integer:decimal",
|
|
7665
7695
|
"integer:float",
|
|
7696
|
+
"integer:string",
|
|
7666
7697
|
"uuid:string"
|
|
7667
7698
|
]);
|
|
7668
7699
|
/**
|
|
@@ -7678,7 +7709,448 @@ function supportsInPlaceFieldTypeChange(before, after) {
|
|
|
7678
7709
|
if (before.serial || after.serial) return false;
|
|
7679
7710
|
if (before.vector || after.vector) return false;
|
|
7680
7711
|
if (before.foreignKey || after.foreignKey) return false;
|
|
7681
|
-
|
|
7712
|
+
if (!IN_PLACE_TYPE_CHANGES.has(`${before.type}:${after.type}`)) return false;
|
|
7713
|
+
return !(before.unique ?? false) || after.type !== "decimal" || before.type !== "float";
|
|
7714
|
+
}
|
|
7715
|
+
/**
|
|
7716
|
+
* Explain whether a field type change can be carried by a pair of migrations
|
|
7717
|
+
* that move values through a temporary field.
|
|
7718
|
+
*
|
|
7719
|
+
* The copy is a whole-value overwrite, so it can only carry a field whose value
|
|
7720
|
+
* stands on its own: a serial number belongs to a sequence the copy cannot
|
|
7721
|
+
* reproduce, a foreign key would dangle while both fields exist, a vector
|
|
7722
|
+
* belongs to an index built from it, and a nested value would need its members
|
|
7723
|
+
* converted individually. Arrays are excluded because collapsing one into a
|
|
7724
|
+
* single value has no answer the generated script could choose, and unique
|
|
7725
|
+
* fields because the duplicate-resolution scaffold the rename half would emit
|
|
7726
|
+
* only produces string values.
|
|
7727
|
+
* @param before - Previous field configuration
|
|
7728
|
+
* @param after - Target field configuration
|
|
7729
|
+
* @returns Eligibility and, when ineligible, the reason
|
|
7730
|
+
*/
|
|
7731
|
+
function getExpandContractFieldChangeEligibility(before, after) {
|
|
7732
|
+
if (before.type === after.type) return {
|
|
7733
|
+
eligible: false,
|
|
7734
|
+
reason: "the field type did not change"
|
|
7735
|
+
};
|
|
7736
|
+
if (supportsInPlaceFieldTypeChange(before, after)) return {
|
|
7737
|
+
eligible: false,
|
|
7738
|
+
reason: `the ${before.type} to ${after.type} change is already supported in place`
|
|
7739
|
+
};
|
|
7740
|
+
if (before.unique || after.unique) return {
|
|
7741
|
+
eligible: false,
|
|
7742
|
+
reason: "the field is unique"
|
|
7743
|
+
};
|
|
7744
|
+
if (before.array || after.array) return {
|
|
7745
|
+
eligible: false,
|
|
7746
|
+
reason: "the field is an array"
|
|
7747
|
+
};
|
|
7748
|
+
if (before.type === "nested" || after.type === "nested") return {
|
|
7749
|
+
eligible: false,
|
|
7750
|
+
reason: "the field is nested"
|
|
7751
|
+
};
|
|
7752
|
+
if (before.serial || after.serial) return {
|
|
7753
|
+
eligible: false,
|
|
7754
|
+
reason: "the field uses a serial sequence"
|
|
7755
|
+
};
|
|
7756
|
+
if (before.vector || after.vector) return {
|
|
7757
|
+
eligible: false,
|
|
7758
|
+
reason: "the field is a vector"
|
|
7759
|
+
};
|
|
7760
|
+
if (before.foreignKey || after.foreignKey) return {
|
|
7761
|
+
eligible: false,
|
|
7762
|
+
reason: "the field is a foreign key"
|
|
7763
|
+
};
|
|
7764
|
+
return { eligible: true };
|
|
7765
|
+
}
|
|
7766
|
+
|
|
7767
|
+
//#endregion
|
|
7768
|
+
//#region src/cli/commands/tailordb/migrate/snapshot-types.ts
|
|
7769
|
+
/**
|
|
7770
|
+
* Optional boolean properties of {@link SnapshotFieldConfig}, absent meaning
|
|
7771
|
+
* `false`. Every comparator over snapshot fields iterates this single list so
|
|
7772
|
+
* a newly added property cannot be silently skipped by one of them; a
|
|
7773
|
+
* comparator that ignores some of these must declare its exclusions
|
|
7774
|
+
* explicitly.
|
|
7775
|
+
*/
|
|
7776
|
+
const SNAPSHOT_FIELD_BOOLEAN_PROPS = [
|
|
7777
|
+
"array",
|
|
7778
|
+
"index",
|
|
7779
|
+
"unique",
|
|
7780
|
+
"foreignKey",
|
|
7781
|
+
"vector"
|
|
7782
|
+
];
|
|
7783
|
+
/**
|
|
7784
|
+
* Type guard: is the operand a field-reference (object) operand?
|
|
7785
|
+
* @param {SnapshotPermissionOperand} operand - Operand to test
|
|
7786
|
+
* @returns {boolean} True if operand is a field-ref (not a value operand)
|
|
7787
|
+
*/
|
|
7788
|
+
function isSnapshotFieldRefOperand(operand) {
|
|
7789
|
+
return typeof operand === "object" && operand !== null && !Array.isArray(operand);
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7792
|
+
//#endregion
|
|
7793
|
+
//#region src/cli/commands/tailordb/migrate/rename-detection.ts
|
|
7794
|
+
/**
|
|
7795
|
+
* Field and type rename detection for TailorDB migrations
|
|
7796
|
+
*
|
|
7797
|
+
* A rename has no dedicated platform API, so the diff decomposes it into
|
|
7798
|
+
* `field_removed` + `field_added` (`table_removed` + `table_added` for types).
|
|
7799
|
+
* This module detects removed/added pairs that are compatible enough to be a
|
|
7800
|
+
* rename so that `migration generate` can ask the user (or accept
|
|
7801
|
+
* `--rename Type.old:new` / `--rename OldType:NewType`) and record a single
|
|
7802
|
+
* `field_renamed` / `table_renamed` change instead.
|
|
7803
|
+
*/
|
|
7804
|
+
/** Modifiers a rename may change, since the Pre-phase relaxes them. */
|
|
7805
|
+
const RENAME_TOLERATED_BOOLEAN_PROPS = /* @__PURE__ */ new Set([
|
|
7806
|
+
"index",
|
|
7807
|
+
"unique",
|
|
7808
|
+
"vector"
|
|
7809
|
+
]);
|
|
7810
|
+
/**
|
|
7811
|
+
* Whether copying values from `before` into `after` preserves their meaning,
|
|
7812
|
+
* i.e. the removed + added pair can be treated as a rename.
|
|
7813
|
+
*
|
|
7814
|
+
* Serial fields are excluded because their values are platform-generated and
|
|
7815
|
+
* cannot be written by a migration script.
|
|
7816
|
+
* @param {SnapshotFieldConfig} before - Removed field's configuration
|
|
7817
|
+
* @param {SnapshotFieldConfig} after - Added field's configuration
|
|
7818
|
+
* @returns {boolean} True if the pair is rename-compatible
|
|
7819
|
+
*/
|
|
7820
|
+
function isRenameCompatible(before, after) {
|
|
7821
|
+
if (before.type !== after.type) return false;
|
|
7822
|
+
for (const prop of SNAPSHOT_FIELD_BOOLEAN_PROPS) {
|
|
7823
|
+
if (RENAME_TOLERATED_BOOLEAN_PROPS.has(prop)) continue;
|
|
7824
|
+
if ((before[prop] ?? false) !== (after[prop] ?? false)) return false;
|
|
7825
|
+
}
|
|
7826
|
+
if ((before.foreignKeyType ?? "") !== (after.foreignKeyType ?? "")) return false;
|
|
7827
|
+
if ((before.foreignKeyField ?? "") !== (after.foreignKeyField ?? "")) return false;
|
|
7828
|
+
if (before.serial || after.serial) return false;
|
|
7829
|
+
if (before.type === "enum") {
|
|
7830
|
+
const afterValues = new Set((after.allowedValues ?? []).map((v) => v.value));
|
|
7831
|
+
if ((before.allowedValues ?? []).some((v) => !afterValues.has(v.value))) return false;
|
|
7832
|
+
}
|
|
7833
|
+
const beforeNested = before.fields ?? {};
|
|
7834
|
+
const afterNested = after.fields ?? {};
|
|
7835
|
+
const beforeNames = Object.keys(beforeNested);
|
|
7836
|
+
const afterNames = Object.keys(afterNested);
|
|
7837
|
+
if (beforeNames.length !== afterNames.length) return false;
|
|
7838
|
+
for (const name of beforeNames) {
|
|
7839
|
+
const beforeMember = beforeNested[name];
|
|
7840
|
+
const afterMember = afterNested[name];
|
|
7841
|
+
if (!beforeMember || !afterMember) return false;
|
|
7842
|
+
if (beforeMember.required !== afterMember.required) return false;
|
|
7843
|
+
if (!isRenameCompatible(beforeMember, afterMember)) return false;
|
|
7844
|
+
}
|
|
7845
|
+
return true;
|
|
7846
|
+
}
|
|
7847
|
+
/**
|
|
7848
|
+
* Find removed + added field pairs in a diff that could be renames.
|
|
7849
|
+
* @param {MigrationDiff} diff - Migration diff to scan
|
|
7850
|
+
* @returns {FieldRenameCandidate[]} Candidates in diff order (may be empty)
|
|
7851
|
+
*/
|
|
7852
|
+
function findRenameCandidates(diff) {
|
|
7853
|
+
const removedByType = /* @__PURE__ */ new Map();
|
|
7854
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
7855
|
+
for (const change of diff.changes) if (change.kind === "field_removed") {
|
|
7856
|
+
const list = removedByType.get(change.tableName) ?? [];
|
|
7857
|
+
list.push(change);
|
|
7858
|
+
removedByType.set(change.tableName, list);
|
|
7859
|
+
} else if (change.kind === "field_added") {
|
|
7860
|
+
const list = addedByType.get(change.tableName) ?? [];
|
|
7861
|
+
list.push(change);
|
|
7862
|
+
addedByType.set(change.tableName, list);
|
|
7863
|
+
}
|
|
7864
|
+
const candidates = [];
|
|
7865
|
+
for (const [tableName, removedChanges] of removedByType) {
|
|
7866
|
+
const addedChanges = addedByType.get(tableName);
|
|
7867
|
+
if (!addedChanges) continue;
|
|
7868
|
+
for (const removed of removedChanges) {
|
|
7869
|
+
const added = addedChanges.filter((a) => isRenameCompatible(removed.before, a.after));
|
|
7870
|
+
if (added.length > 0) candidates.push({
|
|
7871
|
+
tableName,
|
|
7872
|
+
removed,
|
|
7873
|
+
added
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
return candidates;
|
|
7878
|
+
}
|
|
7879
|
+
/**
|
|
7880
|
+
* Whether a rename spec matches a removed + added field pair between two
|
|
7881
|
+
* snapshots: the old field existed before and is gone now, and the new field
|
|
7882
|
+
* exists now but did not before. Field compatibility is checked separately
|
|
7883
|
+
* when the diff is recomputed with the spec.
|
|
7884
|
+
* @param {FieldRenameSpec} spec - Rename spec to test
|
|
7885
|
+
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
|
|
7886
|
+
* @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
|
|
7887
|
+
* @returns {boolean} True if the spec matches a removed + added pair
|
|
7888
|
+
*/
|
|
7889
|
+
function renameSpecApplies(spec, previousSnapshot, currentSnapshot) {
|
|
7890
|
+
const prevFields = previousSnapshot.tables[spec.tableName]?.fields;
|
|
7891
|
+
const currFields = currentSnapshot.tables[spec.tableName]?.fields;
|
|
7892
|
+
return Boolean(prevFields?.[spec.previousFieldName] && !currFields?.[spec.previousFieldName] && currFields?.[spec.fieldName] && !prevFields[spec.fieldName]);
|
|
7893
|
+
}
|
|
7894
|
+
/**
|
|
7895
|
+
* Assert that every rename spec matches a compatible removed + added field
|
|
7896
|
+
* pair between the two normalized snapshots.
|
|
7897
|
+
* @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
|
|
7898
|
+
* @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
|
|
7899
|
+
* @param {readonly FieldRenameSpec[]} fieldRenames - Rename specs to validate
|
|
7900
|
+
*/
|
|
7901
|
+
function assertValidFieldRenames(previous, current, fieldRenames) {
|
|
7902
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7903
|
+
for (const rename of fieldRenames) {
|
|
7904
|
+
const { tableName, previousFieldName, fieldName } = rename;
|
|
7905
|
+
const label = `${tableName}.${previousFieldName}:${fieldName}`;
|
|
7906
|
+
for (const key of [`${tableName}.${previousFieldName}`, `${tableName}.${fieldName}`]) {
|
|
7907
|
+
if (seen.has(key)) throw new Error(`Field "${key}" appears in more than one rename.`);
|
|
7908
|
+
seen.add(key);
|
|
7909
|
+
}
|
|
7910
|
+
const prevType = previous.tables[tableName];
|
|
7911
|
+
const currType = current.tables[tableName];
|
|
7912
|
+
if (!prevType || !currType) throw new Error(`Cannot rename ${label}: type "${tableName}" must exist in both the previous and the current schema.`);
|
|
7913
|
+
const prevField = prevType.fields[previousFieldName];
|
|
7914
|
+
if (!prevField) throw new Error(`Cannot rename ${label}: field "${previousFieldName}" does not exist in the previous schema.`);
|
|
7915
|
+
if (currType.fields[previousFieldName]) throw new Error(`Cannot rename ${label}: field "${previousFieldName}" still exists in the current schema.`);
|
|
7916
|
+
const currField = currType.fields[fieldName];
|
|
7917
|
+
if (!currField) throw new Error(`Cannot rename ${label}: field "${fieldName}" does not exist in the current schema.`);
|
|
7918
|
+
if (prevType.fields[fieldName]) throw new Error(`Cannot rename ${label}: field "${fieldName}" already exists in the previous schema.`);
|
|
7919
|
+
if (!isRenameCompatible(prevField, currField)) throw new Error(`Cannot rename ${label}: the fields are not rename-compatible (the field type, array-ness, and foreign key target must match, enum values must not be removed, nested member names, requiredness, and types must match recursively, and serial fields cannot be renamed).`);
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
const RENAME_OPTION_PATTERN = /^([^.:\s]+)\.([^.:\s]+):([^.:\s]+)$/;
|
|
7923
|
+
/**
|
|
7924
|
+
* Parse a `--rename` option value of the form `Type.old:new`.
|
|
7925
|
+
* @param {string} value - Raw option value
|
|
7926
|
+
* @returns {FieldRenameSpec} Parsed rename spec
|
|
7927
|
+
*/
|
|
7928
|
+
function parseRenameOption(value) {
|
|
7929
|
+
const [, tableName, previousFieldName, fieldName] = value.match(RENAME_OPTION_PATTERN) ?? [];
|
|
7930
|
+
if (!tableName || !previousFieldName || !fieldName) throw new Error(`Invalid --rename value "${value}". Expected format: "Type.oldField:newField".`);
|
|
7931
|
+
if (previousFieldName === fieldName) throw new Error(`Invalid --rename value "${value}": old and new field names are identical.`);
|
|
7932
|
+
return {
|
|
7933
|
+
tableName,
|
|
7934
|
+
previousFieldName,
|
|
7935
|
+
fieldName
|
|
7936
|
+
};
|
|
7937
|
+
}
|
|
7938
|
+
const DROP_OPTION_PATTERN = /^([^.:\s]+)\.([^.:\s]+)$/;
|
|
7939
|
+
/**
|
|
7940
|
+
* Parse a `--drop` option value of the form `Type.field`.
|
|
7941
|
+
* @param {string} value - Raw option value
|
|
7942
|
+
* @returns {FieldDropSpec} Parsed drop spec
|
|
7943
|
+
*/
|
|
7944
|
+
function parseDropOption(value) {
|
|
7945
|
+
const [, tableName, fieldName] = value.match(DROP_OPTION_PATTERN) ?? [];
|
|
7946
|
+
if (!tableName || !fieldName) throw new Error(`Invalid --drop value "${value}". Expected format: "Type.field".`);
|
|
7947
|
+
return {
|
|
7948
|
+
tableName,
|
|
7949
|
+
fieldName
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
/**
|
|
7953
|
+
* Whether a drop spec matches a field that was removed between two snapshots:
|
|
7954
|
+
* the field existed before and is gone now.
|
|
7955
|
+
* @param {FieldDropSpec} spec - Drop spec to test
|
|
7956
|
+
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
|
|
7957
|
+
* @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
|
|
7958
|
+
* @returns {boolean} True if the spec matches a removed field
|
|
7959
|
+
*/
|
|
7960
|
+
function dropSpecApplies(spec, previousSnapshot, currentSnapshot) {
|
|
7961
|
+
const prevFields = previousSnapshot.tables[spec.tableName]?.fields;
|
|
7962
|
+
const currFields = currentSnapshot.tables[spec.tableName]?.fields;
|
|
7963
|
+
return Boolean(prevFields?.[spec.fieldName] && !currFields?.[spec.fieldName]);
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* Parse an `--expand-contract` option value of the form `Type.field`.
|
|
7967
|
+
* @param {string} value - Raw option value
|
|
7968
|
+
* @returns {FieldExpandContractSpec} Parsed spec
|
|
7969
|
+
*/
|
|
7970
|
+
function parseExpandContractOption(value) {
|
|
7971
|
+
const [, tableName, fieldName] = value.match(DROP_OPTION_PATTERN) ?? [];
|
|
7972
|
+
if (!tableName || !fieldName) throw new Error(`Invalid --expand-contract value "${value}". Expected format: "Type.field".`);
|
|
7973
|
+
return {
|
|
7974
|
+
tableName,
|
|
7975
|
+
fieldName
|
|
7976
|
+
};
|
|
7977
|
+
}
|
|
7978
|
+
/**
|
|
7979
|
+
* Return a copy of a field config with foreign key references to the old type
|
|
7980
|
+
* name retargeted at the new name, so a self-referential type compares equal
|
|
7981
|
+
* to its renamed shape.
|
|
7982
|
+
* @param {SnapshotFieldConfig} field - Field configuration to retarget
|
|
7983
|
+
* @param {string} previousTableName - Type name before the rename
|
|
7984
|
+
* @param {string} tableName - Type name after the rename
|
|
7985
|
+
* @returns {SnapshotFieldConfig} Retargeted copy of the field
|
|
7986
|
+
*/
|
|
7987
|
+
function retargetSelfReferences(field, previousTableName, tableName) {
|
|
7988
|
+
const nested = field.fields ? Object.fromEntries(Object.entries(field.fields).map(([name, member]) => [name, retargetSelfReferences(member, previousTableName, tableName)])) : void 0;
|
|
7989
|
+
return {
|
|
7990
|
+
...field,
|
|
7991
|
+
...field.foreignKeyType === previousTableName && { foreignKeyType: tableName },
|
|
7992
|
+
...nested && { fields: nested }
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
/**
|
|
7996
|
+
* JSON serialization with recursively sorted object keys, for deep equality.
|
|
7997
|
+
* @param {unknown} value - Value to serialize
|
|
7998
|
+
* @returns {string} Canonical JSON representation
|
|
7999
|
+
*/
|
|
8000
|
+
function stableStringify(value) {
|
|
8001
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
8002
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, v]) => v !== void 0).toSorted(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
8003
|
+
return JSON.stringify(value);
|
|
8004
|
+
}
|
|
8005
|
+
function isTypeRenameFieldCompatible(before, after) {
|
|
8006
|
+
if (!isRenameCompatible(before, after)) return false;
|
|
8007
|
+
if (before.required !== after.required) return false;
|
|
8008
|
+
if ((before.unique ?? false) !== (after.unique ?? false)) return false;
|
|
8009
|
+
if ((before.scale ?? null) !== (after.scale ?? null)) return false;
|
|
8010
|
+
for (const [name, beforeMember] of Object.entries(before.fields ?? {})) {
|
|
8011
|
+
const afterMember = after.fields?.[name];
|
|
8012
|
+
if (!afterMember || !isTypeRenameFieldCompatible(beforeMember, afterMember)) return false;
|
|
8013
|
+
}
|
|
8014
|
+
return true;
|
|
8015
|
+
}
|
|
8016
|
+
/**
|
|
8017
|
+
* Whether copying every row of `before` into `after` preserves the data, i.e.
|
|
8018
|
+
* the removed + added type pair can be treated as a rename.
|
|
8019
|
+
*
|
|
8020
|
+
* A renamed type is created with its full constraints in the Pre-phase (there
|
|
8021
|
+
* is no relaxation machinery for a fresh type), so the shape must match
|
|
8022
|
+
* strictly: same field names with the same type, array-ness, required/unique
|
|
8023
|
+
* constraints, foreign key target (self references compare against the new
|
|
8024
|
+
* name), and scale; enum values must not be removed; indexes must match.
|
|
8025
|
+
* Serial values cannot be written by a script and file contents are not
|
|
8026
|
+
* copied by SQL, so types with serial or file fields are never candidates.
|
|
8027
|
+
* Name-derived and data-independent surfaces (pluralForm, description,
|
|
8028
|
+
* settings, permissions, hooks, validations, relationships) may differ.
|
|
8029
|
+
* @param {TailorDBSnapshotType} before - Removed type's snapshot
|
|
8030
|
+
* @param {TailorDBSnapshotType} after - Added type's snapshot
|
|
8031
|
+
* @returns {boolean} True if the pair is rename-compatible
|
|
8032
|
+
*/
|
|
8033
|
+
function isTypeRenameCompatible(before, after) {
|
|
8034
|
+
if (Object.keys(before.files ?? {}).length > 0 || Object.keys(after.files ?? {}).length > 0) return false;
|
|
8035
|
+
const beforeFieldNames = Object.keys(before.fields);
|
|
8036
|
+
const afterFieldNames = Object.keys(after.fields);
|
|
8037
|
+
if (beforeFieldNames.length !== afterFieldNames.length) return false;
|
|
8038
|
+
for (const fieldName of beforeFieldNames) {
|
|
8039
|
+
const beforeField = before.fields[fieldName];
|
|
8040
|
+
const afterField = after.fields[fieldName];
|
|
8041
|
+
if (!beforeField || !afterField) return false;
|
|
8042
|
+
if (beforeField.foreignKeyType === before.name && beforeField.required) return false;
|
|
8043
|
+
if (!isTypeRenameFieldCompatible(retargetSelfReferences(beforeField, before.name, after.name), afterField)) return false;
|
|
8044
|
+
}
|
|
8045
|
+
return stableStringify(before.indexes ?? {}) === stableStringify(after.indexes ?? {});
|
|
8046
|
+
}
|
|
8047
|
+
/**
|
|
8048
|
+
* Find removed + added type pairs in a diff that could be renames.
|
|
8049
|
+
* @param {MigrationDiff} diff - Migration diff to scan
|
|
8050
|
+
* @returns {TypeRenameCandidate[]} Candidates in diff order (may be empty)
|
|
8051
|
+
*/
|
|
8052
|
+
function findTypeRenameCandidates(diff) {
|
|
8053
|
+
const removedChanges = diff.changes.filter((change) => change.kind === "table_removed");
|
|
8054
|
+
const addedChanges = diff.changes.filter((change) => change.kind === "table_added");
|
|
8055
|
+
const candidates = [];
|
|
8056
|
+
for (const removed of removedChanges) {
|
|
8057
|
+
const added = addedChanges.filter((a) => isTypeRenameCompatible(removed.before, a.after));
|
|
8058
|
+
if (added.length > 0) candidates.push({
|
|
8059
|
+
removed,
|
|
8060
|
+
added
|
|
8061
|
+
});
|
|
8062
|
+
}
|
|
8063
|
+
return candidates;
|
|
8064
|
+
}
|
|
8065
|
+
/**
|
|
8066
|
+
* Whether a type rename spec matches a removed + added type pair between two
|
|
8067
|
+
* snapshots: the old type existed before and is gone now, and the new type
|
|
8068
|
+
* exists now but did not before. Type compatibility is checked separately
|
|
8069
|
+
* when the diff is recomputed with the spec.
|
|
8070
|
+
* @param {TypeRenameSpec} spec - Rename spec to test
|
|
8071
|
+
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
|
|
8072
|
+
* @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
|
|
8073
|
+
* @returns {boolean} True if the spec matches a removed + added pair
|
|
8074
|
+
*/
|
|
8075
|
+
function typeRenameSpecApplies(spec, previousSnapshot, currentSnapshot) {
|
|
8076
|
+
return Boolean(previousSnapshot.tables[spec.previousTableName] && !currentSnapshot.tables[spec.previousTableName] && currentSnapshot.tables[spec.tableName] && !previousSnapshot.tables[spec.tableName]);
|
|
8077
|
+
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Assert that every type rename spec matches a compatible removed + added
|
|
8080
|
+
* type pair between the two normalized snapshots.
|
|
8081
|
+
* @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
|
|
8082
|
+
* @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
|
|
8083
|
+
* @param {readonly TypeRenameSpec[]} typeRenames - Rename specs to validate
|
|
8084
|
+
*/
|
|
8085
|
+
function assertValidTypeRenames(previous, current, typeRenames) {
|
|
8086
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8087
|
+
for (const rename of typeRenames) {
|
|
8088
|
+
const { previousTableName, tableName } = rename;
|
|
8089
|
+
const label = `${previousTableName}:${tableName}`;
|
|
8090
|
+
for (const key of [previousTableName, tableName]) {
|
|
8091
|
+
if (seen.has(key)) throw new Error(`Type "${key}" appears in more than one rename.`);
|
|
8092
|
+
seen.add(key);
|
|
8093
|
+
}
|
|
8094
|
+
const prevType = previous.tables[previousTableName];
|
|
8095
|
+
if (!prevType) throw new Error(`Cannot rename ${label}: type "${previousTableName}" does not exist in the previous schema.`);
|
|
8096
|
+
if (current.tables[previousTableName]) throw new Error(`Cannot rename ${label}: type "${previousTableName}" still exists in the current schema.`);
|
|
8097
|
+
const currType = current.tables[tableName];
|
|
8098
|
+
if (!currType) throw new Error(`Cannot rename ${label}: type "${tableName}" does not exist in the current schema.`);
|
|
8099
|
+
if (previous.tables[tableName]) throw new Error(`Cannot rename ${label}: type "${tableName}" already exists in the previous schema.`);
|
|
8100
|
+
if (!isTypeRenameCompatible(prevType, currType)) throw new Error(`Cannot rename ${label}: the types are not rename-compatible (every field must keep its name, type, array-ness, required/unique constraints, foreign key target, and scale, enum values must not be removed, indexes must match, self-referential foreign keys must be optional, and types with serial or file fields cannot be renamed).`);
|
|
8101
|
+
}
|
|
8102
|
+
}
|
|
8103
|
+
/**
|
|
8104
|
+
* Whether a field's foreign key target changed in a way that is not explained
|
|
8105
|
+
* by a confirmed type rename. Such a retarget is breaking (stored references
|
|
8106
|
+
* may become invalid) and needs a reference fixup script; a retarget that
|
|
8107
|
+
* follows a rename does not, because record ids are preserved by the copy —
|
|
8108
|
+
* provided the referenced field is unchanged, since the copy only guarantees
|
|
8109
|
+
* that the same ids exist under the new type name.
|
|
8110
|
+
* @param {SnapshotFieldConfig} before - Field configuration before the change
|
|
8111
|
+
* @param {SnapshotFieldConfig} after - Field configuration after the change
|
|
8112
|
+
* @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
|
|
8113
|
+
* @returns {boolean} True if the retarget is breaking
|
|
8114
|
+
*/
|
|
8115
|
+
function isBreakingForeignKeyRetarget(before, after, typeRenameTargets) {
|
|
8116
|
+
return Boolean(before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType && (typeRenameTargets?.get(before.foreignKeyType) !== after.foreignKeyType || (before.foreignKeyField ?? "") !== (after.foreignKeyField ?? "")));
|
|
8117
|
+
}
|
|
8118
|
+
const TYPE_RENAME_OPTION_PATTERN = /^([^.:\s]+):([^.:\s]+)$/;
|
|
8119
|
+
/**
|
|
8120
|
+
* Parse a `--rename` option value of the form `OldType:NewType`.
|
|
8121
|
+
* @param {string} value - Raw option value
|
|
8122
|
+
* @returns {TypeRenameSpec} Parsed rename spec
|
|
8123
|
+
*/
|
|
8124
|
+
function parseTypeRenameOption(value) {
|
|
8125
|
+
const [, previousTableName, tableName] = value.match(TYPE_RENAME_OPTION_PATTERN) ?? [];
|
|
8126
|
+
if (!previousTableName || !tableName) throw new Error(`Invalid --rename value "${value}". Expected format: "Type.oldField:newField" or "OldType:NewType".`);
|
|
8127
|
+
if (previousTableName === tableName) throw new Error(`Invalid --rename value "${value}": old and new type names are identical.`);
|
|
8128
|
+
return {
|
|
8129
|
+
previousTableName,
|
|
8130
|
+
tableName
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
const TYPE_DROP_OPTION_PATTERN = /^([^.:\s]+)$/;
|
|
8134
|
+
/**
|
|
8135
|
+
* Parse a `--drop` option value of the form `Type`.
|
|
8136
|
+
* @param {string} value - Raw option value
|
|
8137
|
+
* @returns {TypeDropSpec} Parsed drop spec
|
|
8138
|
+
*/
|
|
8139
|
+
function parseTypeDropOption(value) {
|
|
8140
|
+
const [, tableName] = value.match(TYPE_DROP_OPTION_PATTERN) ?? [];
|
|
8141
|
+
if (!tableName) throw new Error(`Invalid --drop value "${value}". Expected format: "Type.field" or "Type".`);
|
|
8142
|
+
return { tableName };
|
|
8143
|
+
}
|
|
8144
|
+
/**
|
|
8145
|
+
* Whether a type drop spec matches a type that was removed between two
|
|
8146
|
+
* snapshots: the type existed before and is gone now.
|
|
8147
|
+
* @param {TypeDropSpec} spec - Drop spec to test
|
|
8148
|
+
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
|
|
8149
|
+
* @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
|
|
8150
|
+
* @returns {boolean} True if the spec matches a removed type
|
|
8151
|
+
*/
|
|
8152
|
+
function typeDropSpecApplies(spec, previousSnapshot, currentSnapshot) {
|
|
8153
|
+
return Boolean(previousSnapshot.tables[spec.tableName] && !currentSnapshot.tables[spec.tableName]);
|
|
7682
8154
|
}
|
|
7683
8155
|
|
|
7684
8156
|
//#endregion
|
|
@@ -7919,7 +8391,7 @@ const schemaSnapshotSchema = z.looseObject({
|
|
|
7919
8391
|
version: z.number(),
|
|
7920
8392
|
namespace: z.string(),
|
|
7921
8393
|
createdAt: z.string(),
|
|
7922
|
-
|
|
8394
|
+
tables: snapshotRecordSchema(tailorDBSnapshotTypeSchema),
|
|
7923
8395
|
rebaseline: rebaselineMarkerSchema.optional()
|
|
7924
8396
|
});
|
|
7925
8397
|
const typeSettingsPatchSchema = z.looseObject({
|
|
@@ -7946,56 +8418,73 @@ const snapshotPermissionStateSchema = z.looseObject({
|
|
|
7946
8418
|
gqlPermission: snapshotGqlPermissionSchema.optional()
|
|
7947
8419
|
});
|
|
7948
8420
|
const typeAddedChangeSchema = z.looseObject({
|
|
7949
|
-
kind: z.literal("
|
|
7950
|
-
|
|
8421
|
+
kind: z.literal("table_added"),
|
|
8422
|
+
tableName: z.string(),
|
|
7951
8423
|
reason: z.string().optional(),
|
|
7952
8424
|
after: tailorDBSnapshotTypeSchema
|
|
7953
8425
|
});
|
|
7954
8426
|
const typeRemovedChangeSchema = z.looseObject({
|
|
7955
|
-
kind: z.literal("
|
|
7956
|
-
|
|
8427
|
+
kind: z.literal("table_removed"),
|
|
8428
|
+
tableName: z.string(),
|
|
7957
8429
|
reason: z.string().optional(),
|
|
7958
8430
|
before: tailorDBSnapshotTypeSchema
|
|
7959
8431
|
});
|
|
8432
|
+
const typeRenamedChangeSchema = z.looseObject({
|
|
8433
|
+
kind: z.literal("table_renamed"),
|
|
8434
|
+
tableName: z.string(),
|
|
8435
|
+
reason: z.string().optional(),
|
|
8436
|
+
previousTableName: z.string(),
|
|
8437
|
+
before: tailorDBSnapshotTypeSchema,
|
|
8438
|
+
after: tailorDBSnapshotTypeSchema
|
|
8439
|
+
});
|
|
7960
8440
|
const typeModifiedChangeSchema = z.looseObject({
|
|
7961
|
-
kind: z.literal("
|
|
7962
|
-
|
|
8441
|
+
kind: z.literal("table_modified"),
|
|
8442
|
+
tableName: z.string(),
|
|
7963
8443
|
reason: z.string().optional(),
|
|
7964
8444
|
before: typeSettingsPatchSchema.optional(),
|
|
7965
8445
|
after: typeSettingsPatchSchema.optional()
|
|
7966
8446
|
});
|
|
7967
8447
|
const typeSettingsModifiedChangeSchema = z.looseObject({
|
|
7968
|
-
kind: z.literal("
|
|
7969
|
-
|
|
8448
|
+
kind: z.literal("table_settings_modified"),
|
|
8449
|
+
tableName: z.string(),
|
|
7970
8450
|
reason: z.string().optional(),
|
|
7971
8451
|
before: snapshotTypeSettingsStateSchema,
|
|
7972
8452
|
after: snapshotTypeSettingsStateSchema
|
|
7973
8453
|
});
|
|
7974
8454
|
const fieldAddedChangeSchema = z.looseObject({
|
|
7975
8455
|
kind: z.literal("field_added"),
|
|
7976
|
-
|
|
8456
|
+
tableName: z.string(),
|
|
7977
8457
|
reason: z.string().optional(),
|
|
7978
8458
|
fieldName: z.string(),
|
|
7979
8459
|
after: snapshotFieldConfigSchema
|
|
7980
8460
|
});
|
|
7981
8461
|
const fieldRemovedChangeSchema = z.looseObject({
|
|
7982
8462
|
kind: z.literal("field_removed"),
|
|
7983
|
-
|
|
8463
|
+
tableName: z.string(),
|
|
7984
8464
|
reason: z.string().optional(),
|
|
7985
8465
|
fieldName: z.string(),
|
|
7986
8466
|
before: snapshotFieldConfigSchema
|
|
7987
8467
|
});
|
|
7988
8468
|
const fieldModifiedChangeSchema = z.looseObject({
|
|
7989
8469
|
kind: z.literal("field_modified"),
|
|
7990
|
-
|
|
8470
|
+
tableName: z.string(),
|
|
8471
|
+
reason: z.string().optional(),
|
|
8472
|
+
fieldName: z.string(),
|
|
8473
|
+
before: snapshotFieldConfigSchema,
|
|
8474
|
+
after: snapshotFieldConfigSchema
|
|
8475
|
+
});
|
|
8476
|
+
const fieldRenamedChangeSchema = z.looseObject({
|
|
8477
|
+
kind: z.literal("field_renamed"),
|
|
8478
|
+
tableName: z.string(),
|
|
7991
8479
|
reason: z.string().optional(),
|
|
7992
8480
|
fieldName: z.string(),
|
|
8481
|
+
previousFieldName: z.string(),
|
|
7993
8482
|
before: snapshotFieldConfigSchema,
|
|
7994
8483
|
after: snapshotFieldConfigSchema
|
|
7995
8484
|
});
|
|
7996
8485
|
const fieldTypeModifiedChangeSchema = z.looseObject({
|
|
7997
8486
|
kind: z.literal("field_type_modified"),
|
|
7998
|
-
|
|
8487
|
+
tableName: z.string(),
|
|
7999
8488
|
reason: z.string().optional(),
|
|
8000
8489
|
fieldName: z.string(),
|
|
8001
8490
|
before: snapshotFieldConfigSchema,
|
|
@@ -8003,21 +8492,21 @@ const fieldTypeModifiedChangeSchema = z.looseObject({
|
|
|
8003
8492
|
});
|
|
8004
8493
|
const indexAddedChangeSchema = z.looseObject({
|
|
8005
8494
|
kind: z.literal("index_added"),
|
|
8006
|
-
|
|
8495
|
+
tableName: z.string(),
|
|
8007
8496
|
reason: z.string().optional(),
|
|
8008
8497
|
indexName: z.string(),
|
|
8009
8498
|
after: snapshotIndexConfigSchema
|
|
8010
8499
|
});
|
|
8011
8500
|
const indexRemovedChangeSchema = z.looseObject({
|
|
8012
8501
|
kind: z.literal("index_removed"),
|
|
8013
|
-
|
|
8502
|
+
tableName: z.string(),
|
|
8014
8503
|
reason: z.string().optional(),
|
|
8015
8504
|
indexName: z.string(),
|
|
8016
8505
|
before: snapshotIndexConfigSchema
|
|
8017
8506
|
});
|
|
8018
8507
|
const indexModifiedChangeSchema = z.looseObject({
|
|
8019
8508
|
kind: z.literal("index_modified"),
|
|
8020
|
-
|
|
8509
|
+
tableName: z.string(),
|
|
8021
8510
|
reason: z.string().optional(),
|
|
8022
8511
|
indexName: z.string(),
|
|
8023
8512
|
before: snapshotIndexConfigSchema,
|
|
@@ -8025,21 +8514,21 @@ const indexModifiedChangeSchema = z.looseObject({
|
|
|
8025
8514
|
});
|
|
8026
8515
|
const fileAddedChangeSchema = z.looseObject({
|
|
8027
8516
|
kind: z.literal("file_added"),
|
|
8028
|
-
|
|
8517
|
+
tableName: z.string(),
|
|
8029
8518
|
reason: z.string().optional(),
|
|
8030
8519
|
fieldName: z.string(),
|
|
8031
8520
|
after: z.string()
|
|
8032
8521
|
});
|
|
8033
8522
|
const fileRemovedChangeSchema = z.looseObject({
|
|
8034
8523
|
kind: z.literal("file_removed"),
|
|
8035
|
-
|
|
8524
|
+
tableName: z.string(),
|
|
8036
8525
|
reason: z.string().optional(),
|
|
8037
8526
|
fieldName: z.string(),
|
|
8038
8527
|
before: z.string()
|
|
8039
8528
|
});
|
|
8040
8529
|
const fileModifiedChangeSchema = z.looseObject({
|
|
8041
8530
|
kind: z.literal("file_modified"),
|
|
8042
|
-
|
|
8531
|
+
tableName: z.string(),
|
|
8043
8532
|
reason: z.string().optional(),
|
|
8044
8533
|
fieldName: z.string(),
|
|
8045
8534
|
before: z.string(),
|
|
@@ -8047,7 +8536,7 @@ const fileModifiedChangeSchema = z.looseObject({
|
|
|
8047
8536
|
});
|
|
8048
8537
|
const relationshipAddedChangeSchema = z.looseObject({
|
|
8049
8538
|
kind: z.literal("relationship_added"),
|
|
8050
|
-
|
|
8539
|
+
tableName: z.string(),
|
|
8051
8540
|
reason: z.string().optional(),
|
|
8052
8541
|
relationshipName: z.string(),
|
|
8053
8542
|
relationshipType: z.enum(["forward", "backward"]).optional(),
|
|
@@ -8055,7 +8544,7 @@ const relationshipAddedChangeSchema = z.looseObject({
|
|
|
8055
8544
|
});
|
|
8056
8545
|
const relationshipRemovedChangeSchema = z.looseObject({
|
|
8057
8546
|
kind: z.literal("relationship_removed"),
|
|
8058
|
-
|
|
8547
|
+
tableName: z.string(),
|
|
8059
8548
|
reason: z.string().optional(),
|
|
8060
8549
|
relationshipName: z.string(),
|
|
8061
8550
|
relationshipType: z.enum(["forward", "backward"]).optional(),
|
|
@@ -8063,7 +8552,7 @@ const relationshipRemovedChangeSchema = z.looseObject({
|
|
|
8063
8552
|
});
|
|
8064
8553
|
const relationshipModifiedChangeSchema = z.looseObject({
|
|
8065
8554
|
kind: z.literal("relationship_modified"),
|
|
8066
|
-
|
|
8555
|
+
tableName: z.string(),
|
|
8067
8556
|
reason: z.string().optional(),
|
|
8068
8557
|
relationshipName: z.string(),
|
|
8069
8558
|
relationshipType: z.enum(["forward", "backward"]).optional(),
|
|
@@ -8072,7 +8561,7 @@ const relationshipModifiedChangeSchema = z.looseObject({
|
|
|
8072
8561
|
});
|
|
8073
8562
|
const permissionModifiedChangeSchema = z.looseObject({
|
|
8074
8563
|
kind: z.literal("permission_modified"),
|
|
8075
|
-
|
|
8564
|
+
tableName: z.string(),
|
|
8076
8565
|
reason: z.string().optional(),
|
|
8077
8566
|
before: snapshotPermissionStateSchema.optional(),
|
|
8078
8567
|
after: snapshotPermissionStateSchema.optional()
|
|
@@ -8085,8 +8574,8 @@ const typeScriptsStateSchema = z.looseObject({
|
|
|
8085
8574
|
typeValidateExpr: z.string().optional()
|
|
8086
8575
|
});
|
|
8087
8576
|
const typeScriptsModifiedChangeSchema = z.looseObject({
|
|
8088
|
-
kind: z.literal("
|
|
8089
|
-
|
|
8577
|
+
kind: z.literal("table_scripts_modified"),
|
|
8578
|
+
tableName: z.string(),
|
|
8090
8579
|
reason: z.string().optional(),
|
|
8091
8580
|
before: typeScriptsStateSchema,
|
|
8092
8581
|
after: typeScriptsStateSchema
|
|
@@ -8094,11 +8583,13 @@ const typeScriptsModifiedChangeSchema = z.looseObject({
|
|
|
8094
8583
|
const diffChangeSchema = z.discriminatedUnion("kind", [
|
|
8095
8584
|
typeAddedChangeSchema,
|
|
8096
8585
|
typeRemovedChangeSchema,
|
|
8586
|
+
typeRenamedChangeSchema,
|
|
8097
8587
|
typeModifiedChangeSchema,
|
|
8098
8588
|
typeSettingsModifiedChangeSchema,
|
|
8099
8589
|
fieldAddedChangeSchema,
|
|
8100
8590
|
fieldRemovedChangeSchema,
|
|
8101
8591
|
fieldModifiedChangeSchema,
|
|
8592
|
+
fieldRenamedChangeSchema,
|
|
8102
8593
|
fieldTypeModifiedChangeSchema,
|
|
8103
8594
|
indexAddedChangeSchema,
|
|
8104
8595
|
indexRemovedChangeSchema,
|
|
@@ -8113,14 +8604,14 @@ const diffChangeSchema = z.discriminatedUnion("kind", [
|
|
|
8113
8604
|
typeScriptsModifiedChangeSchema
|
|
8114
8605
|
]);
|
|
8115
8606
|
const breakingChangeInfoSchema = z.looseObject({
|
|
8116
|
-
|
|
8607
|
+
tableName: z.string(),
|
|
8117
8608
|
fieldName: z.string().optional(),
|
|
8118
8609
|
reason: z.string(),
|
|
8119
8610
|
unsupported: z.boolean().optional(),
|
|
8120
8611
|
showThreeStepHint: z.boolean().optional()
|
|
8121
8612
|
});
|
|
8122
8613
|
const warningChangeInfoSchema = z.looseObject({
|
|
8123
|
-
|
|
8614
|
+
tableName: z.string(),
|
|
8124
8615
|
fieldName: z.string().optional(),
|
|
8125
8616
|
reason: z.string()
|
|
8126
8617
|
});
|
|
@@ -8142,17 +8633,6 @@ const migrationDiffSchema = z.looseObject({
|
|
|
8142
8633
|
scriptSkipped: scriptSkippedInfoSchema.optional()
|
|
8143
8634
|
});
|
|
8144
8635
|
|
|
8145
|
-
//#endregion
|
|
8146
|
-
//#region src/cli/commands/tailordb/migrate/snapshot-types.ts
|
|
8147
|
-
/**
|
|
8148
|
-
* Type guard: is the operand a field-reference (object) operand?
|
|
8149
|
-
* @param {SnapshotPermissionOperand} operand - Operand to test
|
|
8150
|
-
* @returns {boolean} True if operand is a field-ref (not a value operand)
|
|
8151
|
-
*/
|
|
8152
|
-
function isSnapshotFieldRefOperand(operand) {
|
|
8153
|
-
return typeof operand === "object" && operand !== null && !Array.isArray(operand);
|
|
8154
|
-
}
|
|
8155
|
-
|
|
8156
8636
|
//#endregion
|
|
8157
8637
|
//#region src/cli/commands/tailordb/migrate/snapshot.ts
|
|
8158
8638
|
/**
|
|
@@ -8179,6 +8659,8 @@ const DB_TYPES_FILE_NAME = "db.ts";
|
|
|
8179
8659
|
* Examples: 0001, 0002, 0003, ...
|
|
8180
8660
|
*/
|
|
8181
8661
|
const MIGRATION_NUMBER_PATTERN = /^\d{4}$/;
|
|
8662
|
+
/** Highest migration number the four-digit directory name can hold. */
|
|
8663
|
+
const MAX_MIGRATION_NUMBER = 9999;
|
|
8182
8664
|
/**
|
|
8183
8665
|
* Platform default scale for decimal fields when scale is not explicitly specified.
|
|
8184
8666
|
* Must stay in sync with the platform's default decimal scale.
|
|
@@ -8189,14 +8671,111 @@ function assertSupportedMigrationFileVersion(filePath, raw) {
|
|
|
8189
8671
|
if (typeof raw !== "object" || raw === null || !("version" in raw)) return;
|
|
8190
8672
|
const version = raw.version;
|
|
8191
8673
|
if (typeof version !== "number") return;
|
|
8192
|
-
if (Number.isInteger(version) && version >= 1 && version <=
|
|
8193
|
-
const supportedRange = `${1}-${
|
|
8674
|
+
if (Number.isInteger(version) && version >= 1 && version <= 5) return;
|
|
8675
|
+
const supportedRange = `${1}-${5}`;
|
|
8194
8676
|
let guidance;
|
|
8195
|
-
if (version >
|
|
8677
|
+
if (version > 5) guidance = `Upgrade to an SDK that supports migration file format version ${version}.`;
|
|
8196
8678
|
else if (version < 1) guidance = "Re-baseline with an SDK that still supports this migration history, then upgrade the SDK.";
|
|
8197
8679
|
else guidance = "Restore the migration file from version control or regenerate it.";
|
|
8198
8680
|
throw new UnsupportedMigrationFileVersionError(`Unsupported migration file format version ${version} at ${filePath}. This SDK supports migration file format versions ${supportedRange}. ${guidance}`);
|
|
8199
8681
|
}
|
|
8682
|
+
/**
|
|
8683
|
+
* Diff change kinds renamed when TailorDB table terminology replaced "type".
|
|
8684
|
+
* Migration histories written before the rename persist the old names, so
|
|
8685
|
+
* diff.json files keep being read through this mapping.
|
|
8686
|
+
*/
|
|
8687
|
+
const LEGACY_CHANGE_KINDS = /* @__PURE__ */ new Map([
|
|
8688
|
+
["type_added", "table_added"],
|
|
8689
|
+
["type_removed", "table_removed"],
|
|
8690
|
+
["type_renamed", "table_renamed"],
|
|
8691
|
+
["type_modified", "table_modified"],
|
|
8692
|
+
["type_settings_modified", "table_settings_modified"],
|
|
8693
|
+
["type_scripts_modified", "table_scripts_modified"]
|
|
8694
|
+
]);
|
|
8695
|
+
/**
|
|
8696
|
+
* Rewrite pre-rename `type_*` change kinds to their current `table_*` names
|
|
8697
|
+
* so that the diff schema, which only knows the current names, accepts them.
|
|
8698
|
+
* @param {unknown} raw - Parsed diff.json contents
|
|
8699
|
+
* @returns {unknown} Diff contents with legacy change kinds rewritten
|
|
8700
|
+
*/
|
|
8701
|
+
function normalizeLegacyChangeKinds(raw) {
|
|
8702
|
+
if (typeof raw !== "object" || raw === null || !("changes" in raw)) return raw;
|
|
8703
|
+
const changes = raw.changes;
|
|
8704
|
+
if (!Array.isArray(changes)) return raw;
|
|
8705
|
+
const normalized = changes.map((change) => {
|
|
8706
|
+
if (typeof change !== "object" || change === null || !("kind" in change)) return change;
|
|
8707
|
+
const kind = change.kind;
|
|
8708
|
+
if (typeof kind !== "string") return change;
|
|
8709
|
+
const currentKind = LEGACY_CHANGE_KINDS.get(kind);
|
|
8710
|
+
return currentKind === void 0 ? change : {
|
|
8711
|
+
...change,
|
|
8712
|
+
kind: currentKind
|
|
8713
|
+
};
|
|
8714
|
+
});
|
|
8715
|
+
return {
|
|
8716
|
+
...raw,
|
|
8717
|
+
changes: normalized
|
|
8718
|
+
};
|
|
8719
|
+
}
|
|
8720
|
+
/**
|
|
8721
|
+
* Persisted field names renamed when TailorDB table terminology replaced
|
|
8722
|
+
* "type". Applied to every entry that carries a table name, so diff.json files
|
|
8723
|
+
* written before the rename keep validating against the current schema.
|
|
8724
|
+
*/
|
|
8725
|
+
const LEGACY_ENTRY_FIELDS = /* @__PURE__ */ new Map([["typeName", "tableName"], ["previousTypeName", "previousTableName"]]);
|
|
8726
|
+
function renameLegacyEntryFields(entry) {
|
|
8727
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
8728
|
+
const source = entry;
|
|
8729
|
+
const renamed = {};
|
|
8730
|
+
let changed = false;
|
|
8731
|
+
for (const [key, value] of Object.entries(source)) {
|
|
8732
|
+
const currentKey = LEGACY_ENTRY_FIELDS.get(key);
|
|
8733
|
+
if (currentKey !== void 0 && !(currentKey in source)) {
|
|
8734
|
+
renamed[currentKey] = value;
|
|
8735
|
+
changed = true;
|
|
8736
|
+
} else renamed[key] = value;
|
|
8737
|
+
}
|
|
8738
|
+
return changed ? renamed : entry;
|
|
8739
|
+
}
|
|
8740
|
+
/** Persisted arrays whose entries carry a table name. */
|
|
8741
|
+
const LEGACY_FIELD_CARRIERS = [
|
|
8742
|
+
"changes",
|
|
8743
|
+
"breakingChanges",
|
|
8744
|
+
"warnings"
|
|
8745
|
+
];
|
|
8746
|
+
/**
|
|
8747
|
+
* Move a pre-rename `types` record to `tables` so schema.json files written
|
|
8748
|
+
* before the rename keep validating against the current schema.
|
|
8749
|
+
* @param {unknown} raw - Parsed schema.json contents
|
|
8750
|
+
* @returns {unknown} Snapshot contents with the legacy key moved
|
|
8751
|
+
*/
|
|
8752
|
+
function normalizeLegacyTablesKey(raw) {
|
|
8753
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
8754
|
+
const source = raw;
|
|
8755
|
+
if (!("types" in source) || "tables" in source) return raw;
|
|
8756
|
+
const { types, ...rest } = source;
|
|
8757
|
+
return {
|
|
8758
|
+
...rest,
|
|
8759
|
+
tables: types
|
|
8760
|
+
};
|
|
8761
|
+
}
|
|
8762
|
+
/**
|
|
8763
|
+
* Rewrite the pre-rename `typeName` / `previousTypeName` keys to their current
|
|
8764
|
+
* names across every persisted position that carries a table name.
|
|
8765
|
+
* @param {unknown} raw - Parsed diff.json contents
|
|
8766
|
+
* @returns {unknown} Diff contents with legacy field names rewritten
|
|
8767
|
+
*/
|
|
8768
|
+
function normalizeLegacyFieldNames(raw) {
|
|
8769
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
8770
|
+
const source = raw;
|
|
8771
|
+
const normalized = { ...source };
|
|
8772
|
+
for (const key of LEGACY_FIELD_CARRIERS) {
|
|
8773
|
+
const entries = source[key];
|
|
8774
|
+
if (!Array.isArray(entries)) continue;
|
|
8775
|
+
normalized[key] = entries.map(renameLegacyEntryFields);
|
|
8776
|
+
}
|
|
8777
|
+
return normalized;
|
|
8778
|
+
}
|
|
8200
8779
|
function createSnapshotRecord() {
|
|
8201
8780
|
return Object.create(null);
|
|
8202
8781
|
}
|
|
@@ -8250,11 +8829,11 @@ function normalizeSnapshotType(type) {
|
|
|
8250
8829
|
* @returns {NormalizedSchemaSnapshot} A new schema snapshot object branded as normalized
|
|
8251
8830
|
*/
|
|
8252
8831
|
function normalizeSchemaSnapshot(snapshot) {
|
|
8253
|
-
const
|
|
8254
|
-
for (const [
|
|
8832
|
+
const tables = createSnapshotRecord();
|
|
8833
|
+
for (const [tableName, type] of Object.entries(snapshot.tables)) tables[tableName] = normalizeSnapshotType(type);
|
|
8255
8834
|
return {
|
|
8256
8835
|
...snapshot,
|
|
8257
|
-
|
|
8836
|
+
tables
|
|
8258
8837
|
};
|
|
8259
8838
|
}
|
|
8260
8839
|
/**
|
|
@@ -8436,12 +9015,12 @@ function convertActionPermission(permission) {
|
|
|
8436
9015
|
*/
|
|
8437
9016
|
function createSnapshotFromLocalTypes(types, namespace) {
|
|
8438
9017
|
const snapshotTypes = createSnapshotRecord();
|
|
8439
|
-
for (const [
|
|
9018
|
+
for (const [tableName, type] of Object.entries(types)) snapshotTypes[tableName] = createSnapshotType(type);
|
|
8440
9019
|
return normalizeSchemaSnapshot({
|
|
8441
|
-
version:
|
|
9020
|
+
version: 5,
|
|
8442
9021
|
namespace,
|
|
8443
9022
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8444
|
-
|
|
9023
|
+
tables: snapshotTypes
|
|
8445
9024
|
});
|
|
8446
9025
|
}
|
|
8447
9026
|
/**
|
|
@@ -8458,7 +9037,7 @@ function loadSnapshot(filePath) {
|
|
|
8458
9037
|
throw new Error(`Invalid schema snapshot at ${filePath}: ${String(error)}`, { cause: error });
|
|
8459
9038
|
}
|
|
8460
9039
|
assertSupportedMigrationFileVersion(filePath, raw);
|
|
8461
|
-
const result = schemaSnapshotSchema.safeParse(raw);
|
|
9040
|
+
const result = schemaSnapshotSchema.safeParse(normalizeLegacyTablesKey(raw));
|
|
8462
9041
|
if (!result.success) throw new Error(`Invalid schema snapshot at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
|
|
8463
9042
|
const snapshot = result.data;
|
|
8464
9043
|
return normalizeSchemaSnapshot(snapshot);
|
|
@@ -8477,7 +9056,7 @@ function loadDiff(filePath) {
|
|
|
8477
9056
|
throw new Error(`Invalid migration diff at ${filePath}: ${String(error)}`, { cause: error });
|
|
8478
9057
|
}
|
|
8479
9058
|
assertSupportedMigrationFileVersion(filePath, raw);
|
|
8480
|
-
const result = migrationDiffSchema.safeParse(raw);
|
|
9059
|
+
const result = migrationDiffSchema.safeParse(normalizeLegacyFieldNames(normalizeLegacyChangeKinds(raw)));
|
|
8481
9060
|
if (!result.success) throw new Error(`Invalid migration diff at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
|
|
8482
9061
|
const parsed = result.data;
|
|
8483
9062
|
const warnings = parsed.warnings ?? deriveWarningsFromChanges(parsed);
|
|
@@ -8488,7 +9067,7 @@ function loadDiff(filePath) {
|
|
|
8488
9067
|
};
|
|
8489
9068
|
}
|
|
8490
9069
|
const FIELD_REMOVED_WARNING_REASON = "Field removed (existing data will no longer be accessible through the schema)";
|
|
8491
|
-
const
|
|
9070
|
+
const TABLE_REMOVED_WARNING_REASON = "Type removed (all records of this type will be deleted during post-migration cleanup)";
|
|
8492
9071
|
/**
|
|
8493
9072
|
* Reconstruct data-loss warnings from removal changes for diff.json files
|
|
8494
9073
|
* written before the warning tier existed
|
|
@@ -8498,13 +9077,13 @@ const TYPE_REMOVED_WARNING_REASON = "Type removed (all records of this type will
|
|
|
8498
9077
|
function deriveWarningsFromChanges(diff) {
|
|
8499
9078
|
const warnings = [];
|
|
8500
9079
|
for (const change of diff.changes) if (change.kind === "field_removed") warnings.push({
|
|
8501
|
-
|
|
9080
|
+
tableName: change.tableName,
|
|
8502
9081
|
fieldName: change.fieldName,
|
|
8503
9082
|
reason: FIELD_REMOVED_WARNING_REASON
|
|
8504
9083
|
});
|
|
8505
|
-
else if (change.kind === "
|
|
8506
|
-
|
|
8507
|
-
reason:
|
|
9084
|
+
else if (change.kind === "table_removed") warnings.push({
|
|
9085
|
+
tableName: change.tableName,
|
|
9086
|
+
reason: TABLE_REMOVED_WARNING_REASON
|
|
8508
9087
|
});
|
|
8509
9088
|
return warnings;
|
|
8510
9089
|
}
|
|
@@ -8555,19 +9134,19 @@ function getNextMigrationNumber(migrationsDir) {
|
|
|
8555
9134
|
* @returns {NormalizedSchemaSnapshot} Normalized snapshot after applying diff
|
|
8556
9135
|
*/
|
|
8557
9136
|
function applyDiffToSnapshot(snapshot, diff) {
|
|
8558
|
-
const
|
|
9137
|
+
const tables = copySnapshotRecord(snapshot.tables);
|
|
8559
9138
|
for (const change of diff.changes) switch (change.kind) {
|
|
8560
|
-
case "
|
|
8561
|
-
|
|
9139
|
+
case "table_added":
|
|
9140
|
+
tables[change.tableName] = change.after;
|
|
8562
9141
|
break;
|
|
8563
|
-
case "
|
|
8564
|
-
delete
|
|
9142
|
+
case "table_removed":
|
|
9143
|
+
delete tables[change.tableName];
|
|
8565
9144
|
break;
|
|
8566
|
-
case "
|
|
8567
|
-
const existing =
|
|
9145
|
+
case "table_modified": {
|
|
9146
|
+
const existing = tables[change.tableName];
|
|
8568
9147
|
if (existing && change.after) {
|
|
8569
9148
|
const after = change.after;
|
|
8570
|
-
|
|
9149
|
+
tables[change.tableName] = {
|
|
8571
9150
|
...existing,
|
|
8572
9151
|
...after.indexes !== void 0 && { indexes: after.indexes },
|
|
8573
9152
|
...after.files !== void 0 && { files: after.files }
|
|
@@ -8575,9 +9154,9 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8575
9154
|
}
|
|
8576
9155
|
break;
|
|
8577
9156
|
}
|
|
8578
|
-
case "
|
|
8579
|
-
const existing =
|
|
8580
|
-
if (existing)
|
|
9157
|
+
case "table_settings_modified": {
|
|
9158
|
+
const existing = tables[change.tableName];
|
|
9159
|
+
if (existing) tables[change.tableName] = {
|
|
8581
9160
|
...existing,
|
|
8582
9161
|
description: change.after.description,
|
|
8583
9162
|
pluralForm: change.after.pluralForm,
|
|
@@ -8585,11 +9164,11 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8585
9164
|
};
|
|
8586
9165
|
break;
|
|
8587
9166
|
}
|
|
8588
|
-
case "
|
|
8589
|
-
const existing =
|
|
9167
|
+
case "table_scripts_modified": {
|
|
9168
|
+
const existing = tables[change.tableName];
|
|
8590
9169
|
if (existing) {
|
|
8591
9170
|
const { typeHookExpr: _, typeValidateExpr: __, ...rest } = existing;
|
|
8592
|
-
|
|
9171
|
+
tables[change.tableName] = {
|
|
8593
9172
|
...rest,
|
|
8594
9173
|
...change.after.typeHookExpr && { typeHookExpr: change.after.typeHookExpr },
|
|
8595
9174
|
...change.after.typeValidateExpr !== void 0 && { typeValidateExpr: change.after.typeValidateExpr }
|
|
@@ -8600,11 +9179,11 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8600
9179
|
case "field_added":
|
|
8601
9180
|
case "field_modified":
|
|
8602
9181
|
case "field_type_modified": {
|
|
8603
|
-
const existing =
|
|
9182
|
+
const existing = tables[change.tableName];
|
|
8604
9183
|
if (existing) {
|
|
8605
9184
|
const fields = copySnapshotRecord(existing.fields);
|
|
8606
9185
|
fields[change.fieldName] = change.after;
|
|
8607
|
-
|
|
9186
|
+
tables[change.tableName] = {
|
|
8608
9187
|
...existing,
|
|
8609
9188
|
fields
|
|
8610
9189
|
};
|
|
@@ -8612,24 +9191,41 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8612
9191
|
break;
|
|
8613
9192
|
}
|
|
8614
9193
|
case "field_removed": {
|
|
8615
|
-
const existing =
|
|
9194
|
+
const existing = tables[change.tableName];
|
|
8616
9195
|
if (existing) {
|
|
8617
9196
|
const remainingFields = copySnapshotRecord(existing.fields);
|
|
8618
9197
|
delete remainingFields[change.fieldName];
|
|
8619
|
-
|
|
9198
|
+
tables[change.tableName] = {
|
|
8620
9199
|
...existing,
|
|
8621
9200
|
fields: remainingFields
|
|
8622
9201
|
};
|
|
8623
9202
|
}
|
|
8624
9203
|
break;
|
|
8625
9204
|
}
|
|
9205
|
+
case "field_renamed": {
|
|
9206
|
+
const existing = tables[change.tableName];
|
|
9207
|
+
if (existing) {
|
|
9208
|
+
const fields = copySnapshotRecord(existing.fields);
|
|
9209
|
+
delete fields[change.previousFieldName];
|
|
9210
|
+
fields[change.fieldName] = change.after;
|
|
9211
|
+
tables[change.tableName] = {
|
|
9212
|
+
...existing,
|
|
9213
|
+
fields
|
|
9214
|
+
};
|
|
9215
|
+
}
|
|
9216
|
+
break;
|
|
9217
|
+
}
|
|
9218
|
+
case "table_renamed":
|
|
9219
|
+
delete tables[change.previousTableName];
|
|
9220
|
+
tables[change.tableName] = change.after;
|
|
9221
|
+
break;
|
|
8626
9222
|
case "index_added":
|
|
8627
9223
|
case "index_modified": {
|
|
8628
|
-
const existing =
|
|
9224
|
+
const existing = tables[change.tableName];
|
|
8629
9225
|
if (existing) {
|
|
8630
9226
|
const indexes = copySnapshotRecord(existing.indexes);
|
|
8631
9227
|
indexes[change.indexName] = change.after;
|
|
8632
|
-
|
|
9228
|
+
tables[change.tableName] = {
|
|
8633
9229
|
...existing,
|
|
8634
9230
|
indexes
|
|
8635
9231
|
};
|
|
@@ -8637,11 +9233,11 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8637
9233
|
break;
|
|
8638
9234
|
}
|
|
8639
9235
|
case "index_removed": {
|
|
8640
|
-
const existing =
|
|
9236
|
+
const existing = tables[change.tableName];
|
|
8641
9237
|
if (existing && existing.indexes) {
|
|
8642
9238
|
const remainingIndexes = copySnapshotRecord(existing.indexes);
|
|
8643
9239
|
delete remainingIndexes[change.indexName];
|
|
8644
|
-
|
|
9240
|
+
tables[change.tableName] = {
|
|
8645
9241
|
...existing,
|
|
8646
9242
|
indexes: Object.keys(remainingIndexes).length > 0 ? remainingIndexes : void 0
|
|
8647
9243
|
};
|
|
@@ -8650,11 +9246,11 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8650
9246
|
}
|
|
8651
9247
|
case "file_added":
|
|
8652
9248
|
case "file_modified": {
|
|
8653
|
-
const existing =
|
|
9249
|
+
const existing = tables[change.tableName];
|
|
8654
9250
|
if (existing) {
|
|
8655
9251
|
const files = copySnapshotRecord(existing.files);
|
|
8656
9252
|
files[change.fieldName] = change.after;
|
|
8657
|
-
|
|
9253
|
+
tables[change.tableName] = {
|
|
8658
9254
|
...existing,
|
|
8659
9255
|
files
|
|
8660
9256
|
};
|
|
@@ -8662,11 +9258,11 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8662
9258
|
break;
|
|
8663
9259
|
}
|
|
8664
9260
|
case "file_removed": {
|
|
8665
|
-
const existing =
|
|
9261
|
+
const existing = tables[change.tableName];
|
|
8666
9262
|
if (existing && existing.files) {
|
|
8667
9263
|
const remainingFiles = copySnapshotRecord(existing.files);
|
|
8668
9264
|
delete remainingFiles[change.fieldName];
|
|
8669
|
-
|
|
9265
|
+
tables[change.tableName] = {
|
|
8670
9266
|
...existing,
|
|
8671
9267
|
files: Object.keys(remainingFiles).length > 0 ? remainingFiles : void 0
|
|
8672
9268
|
};
|
|
@@ -8675,20 +9271,20 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8675
9271
|
}
|
|
8676
9272
|
case "relationship_added":
|
|
8677
9273
|
case "relationship_modified": {
|
|
8678
|
-
const existing =
|
|
9274
|
+
const existing = tables[change.tableName];
|
|
8679
9275
|
if (existing) {
|
|
8680
9276
|
const rel = change.after;
|
|
8681
9277
|
if ((change.relationshipType ?? (existing.forwardRelationships?.[change.relationshipName] ? "forward" : existing.backwardRelationships?.[change.relationshipName] ? "backward" : "forward")) === "forward") {
|
|
8682
9278
|
const forwardRelationships = copySnapshotRecord(existing.forwardRelationships);
|
|
8683
9279
|
forwardRelationships[change.relationshipName] = rel;
|
|
8684
|
-
|
|
9280
|
+
tables[change.tableName] = {
|
|
8685
9281
|
...existing,
|
|
8686
9282
|
forwardRelationships
|
|
8687
9283
|
};
|
|
8688
9284
|
} else {
|
|
8689
9285
|
const backwardRelationships = copySnapshotRecord(existing.backwardRelationships);
|
|
8690
9286
|
backwardRelationships[change.relationshipName] = rel;
|
|
8691
|
-
|
|
9287
|
+
tables[change.tableName] = {
|
|
8692
9288
|
...existing,
|
|
8693
9289
|
backwardRelationships
|
|
8694
9290
|
};
|
|
@@ -8697,20 +9293,20 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8697
9293
|
break;
|
|
8698
9294
|
}
|
|
8699
9295
|
case "relationship_removed": {
|
|
8700
|
-
const type =
|
|
9296
|
+
const type = tables[change.tableName];
|
|
8701
9297
|
if (type) {
|
|
8702
9298
|
const targetType = change.relationshipType ?? (type.forwardRelationships?.[change.relationshipName] ? "forward" : type.backwardRelationships?.[change.relationshipName] ? "backward" : null);
|
|
8703
9299
|
if (targetType === "forward" && type.forwardRelationships?.[change.relationshipName]) {
|
|
8704
9300
|
const remaining = copySnapshotRecord(type.forwardRelationships);
|
|
8705
9301
|
delete remaining[change.relationshipName];
|
|
8706
|
-
|
|
9302
|
+
tables[change.tableName] = {
|
|
8707
9303
|
...type,
|
|
8708
9304
|
forwardRelationships: Object.keys(remaining).length > 0 ? remaining : void 0
|
|
8709
9305
|
};
|
|
8710
9306
|
} else if (targetType === "backward" && type.backwardRelationships?.[change.relationshipName]) {
|
|
8711
9307
|
const remaining = copySnapshotRecord(type.backwardRelationships);
|
|
8712
9308
|
delete remaining[change.relationshipName];
|
|
8713
|
-
|
|
9309
|
+
tables[change.tableName] = {
|
|
8714
9310
|
...type,
|
|
8715
9311
|
backwardRelationships: Object.keys(remaining).length > 0 ? remaining : void 0
|
|
8716
9312
|
};
|
|
@@ -8719,10 +9315,10 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8719
9315
|
break;
|
|
8720
9316
|
}
|
|
8721
9317
|
case "permission_modified": {
|
|
8722
|
-
const existing =
|
|
9318
|
+
const existing = tables[change.tableName];
|
|
8723
9319
|
if (existing && change.after) {
|
|
8724
9320
|
const after = change.after;
|
|
8725
|
-
|
|
9321
|
+
tables[change.tableName] = {
|
|
8726
9322
|
...existing,
|
|
8727
9323
|
permissions: {
|
|
8728
9324
|
record: after.recordPermission,
|
|
@@ -8735,7 +9331,7 @@ function applyDiffToSnapshot(snapshot, diff) {
|
|
|
8735
9331
|
}
|
|
8736
9332
|
return normalizeSchemaSnapshot({
|
|
8737
9333
|
...snapshot,
|
|
8738
|
-
|
|
9334
|
+
tables,
|
|
8739
9335
|
createdAt: diff.createdAt
|
|
8740
9336
|
});
|
|
8741
9337
|
}
|
|
@@ -8797,13 +9393,7 @@ function assertMigrationNumberExists(migrationsDir, migrationNumber) {
|
|
|
8797
9393
|
function areFieldsDifferent(oldField, newField) {
|
|
8798
9394
|
if (oldField.type !== newField.type) return true;
|
|
8799
9395
|
if (oldField.required !== newField.required) return true;
|
|
8800
|
-
for (const prop of [
|
|
8801
|
-
"array",
|
|
8802
|
-
"index",
|
|
8803
|
-
"unique",
|
|
8804
|
-
"foreignKey",
|
|
8805
|
-
"vector"
|
|
8806
|
-
]) if ((oldField[prop] ?? false) !== (newField[prop] ?? false)) return true;
|
|
9396
|
+
for (const prop of SNAPSHOT_FIELD_BOOLEAN_PROPS) if ((oldField[prop] ?? false) !== (newField[prop] ?? false)) return true;
|
|
8807
9397
|
if (oldField.foreignKeyType !== newField.foreignKeyType) return true;
|
|
8808
9398
|
if (oldField.foreignKeyField !== newField.foreignKeyField) return true;
|
|
8809
9399
|
if ((oldField.description ?? "") !== (newField.description ?? "")) return true;
|
|
@@ -8859,23 +9449,24 @@ function areFieldsDifferent(oldField, newField) {
|
|
|
8859
9449
|
}
|
|
8860
9450
|
/**
|
|
8861
9451
|
* Collect breaking changes for a field change
|
|
8862
|
-
* @param {string}
|
|
9452
|
+
* @param {string} tableName - Name of the type containing the field
|
|
8863
9453
|
* @param {string} fieldName - Name of the field being changed
|
|
8864
9454
|
* @param {SnapshotFieldConfig | undefined} oldField - Old field configuration
|
|
8865
9455
|
* @param {SnapshotFieldConfig | undefined} newField - New field configuration
|
|
9456
|
+
* @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
|
|
8866
9457
|
* @returns {BreakingChangeInfo[]} Breaking change information
|
|
8867
9458
|
*/
|
|
8868
|
-
function getBreakingFieldChanges(
|
|
9459
|
+
function getBreakingFieldChanges(tableName, fieldName, oldField, newField, typeRenameTargets) {
|
|
8869
9460
|
const breakingChanges = [];
|
|
8870
9461
|
if (!oldField && newField && newField.required) breakingChanges.push({
|
|
8871
|
-
|
|
9462
|
+
tableName,
|
|
8872
9463
|
fieldName,
|
|
8873
9464
|
reason: "Required field added"
|
|
8874
9465
|
});
|
|
8875
9466
|
if (oldField && newField && oldField.type !== newField.type) {
|
|
8876
9467
|
const supported = supportsInPlaceFieldTypeChange(oldField, newField);
|
|
8877
9468
|
breakingChanges.push({
|
|
8878
|
-
|
|
9469
|
+
tableName,
|
|
8879
9470
|
fieldName,
|
|
8880
9471
|
reason: `Field type changed from ${oldField.type} to ${newField.type}`,
|
|
8881
9472
|
...!supported && {
|
|
@@ -8885,36 +9476,32 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
|
|
|
8885
9476
|
});
|
|
8886
9477
|
}
|
|
8887
9478
|
if (oldField && newField && !oldField.required && newField.required) breakingChanges.push({
|
|
8888
|
-
|
|
9479
|
+
tableName,
|
|
8889
9480
|
fieldName,
|
|
8890
9481
|
reason: "Field changed from optional to required"
|
|
8891
9482
|
});
|
|
8892
9483
|
if (oldField && newField && (oldField.array ?? false) !== (newField.array ?? false)) {
|
|
8893
9484
|
const [fromType, toType] = oldField.array ? ["array", "single value"] : ["single value", "array"];
|
|
8894
9485
|
breakingChanges.push({
|
|
8895
|
-
|
|
9486
|
+
tableName,
|
|
8896
9487
|
fieldName,
|
|
8897
9488
|
reason: `Field changed from ${fromType} to ${toType}`,
|
|
8898
9489
|
unsupported: true,
|
|
8899
9490
|
showThreeStepHint: true
|
|
8900
9491
|
});
|
|
8901
9492
|
}
|
|
8902
|
-
if (oldField && newField) {
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
fieldName,
|
|
8908
|
-
reason: `Foreign key target type changed from ${oldForeignKeyType} to ${newForeignKeyType}`
|
|
8909
|
-
});
|
|
8910
|
-
}
|
|
9493
|
+
if (oldField && newField && isBreakingForeignKeyRetarget(oldField, newField, typeRenameTargets)) breakingChanges.push({
|
|
9494
|
+
tableName,
|
|
9495
|
+
fieldName,
|
|
9496
|
+
reason: `Foreign key target type changed from ${oldField.foreignKeyType} to ${newField.foreignKeyType}`
|
|
9497
|
+
});
|
|
8911
9498
|
if (oldField && newField && !(oldField.unique ?? false) && (newField.unique ?? false)) breakingChanges.push({
|
|
8912
|
-
|
|
9499
|
+
tableName,
|
|
8913
9500
|
fieldName,
|
|
8914
9501
|
reason: "Unique constraint added to field"
|
|
8915
9502
|
});
|
|
8916
9503
|
if (oldField?.type === "decimal" && newField?.type === "decimal" && oldField.scale !== newField.scale) breakingChanges.push({
|
|
8917
|
-
|
|
9504
|
+
tableName,
|
|
8918
9505
|
fieldName,
|
|
8919
9506
|
reason: `Decimal scale changed from ${oldField.scale} to ${newField.scale}`
|
|
8920
9507
|
});
|
|
@@ -8925,7 +9512,7 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
|
|
|
8925
9512
|
const newValuesSet = new Set(newAllowed.map((v) => v.value));
|
|
8926
9513
|
const removedValues = oldValues.filter((v) => !newValuesSet.has(v));
|
|
8927
9514
|
if (removedValues.length > 0) breakingChanges.push({
|
|
8928
|
-
|
|
9515
|
+
tableName,
|
|
8929
9516
|
fieldName,
|
|
8930
9517
|
reason: `Enum values removed: ${removedValues.join(", ")}`
|
|
8931
9518
|
});
|
|
@@ -8935,37 +9522,62 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
|
|
|
8935
9522
|
function addChange(ctx, change, oldField, newField) {
|
|
8936
9523
|
ctx.changes.push(change);
|
|
8937
9524
|
if (!change.fieldName) return;
|
|
8938
|
-
const breakingChanges = getBreakingFieldChanges(change.
|
|
9525
|
+
const breakingChanges = getBreakingFieldChanges(change.tableName, change.fieldName, oldField, newField, ctx.typeRenameTargets);
|
|
8939
9526
|
if (breakingChanges.length > 0) {
|
|
8940
9527
|
ctx.breakingChanges.push(...breakingChanges);
|
|
8941
9528
|
return;
|
|
8942
9529
|
}
|
|
8943
9530
|
if (change.kind === "field_removed") ctx.warnings.push({
|
|
8944
|
-
|
|
9531
|
+
tableName: change.tableName,
|
|
8945
9532
|
fieldName: change.fieldName,
|
|
8946
9533
|
reason: FIELD_REMOVED_WARNING_REASON
|
|
8947
9534
|
});
|
|
8948
9535
|
}
|
|
8949
|
-
function compareTypeFields(ctx,
|
|
9536
|
+
function compareTypeFields(ctx, tableName, prevType, currType, fieldRenames = []) {
|
|
8950
9537
|
const prevFieldNames = new Set(Object.keys(prevType.fields));
|
|
8951
9538
|
const currFieldNames = new Set(Object.keys(currType.fields));
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
9539
|
+
const renamedFromNames = new Set(fieldRenames.map((r) => r.previousFieldName));
|
|
9540
|
+
const renamedToNames = new Set(fieldRenames.map((r) => r.fieldName));
|
|
9541
|
+
for (const rename of fieldRenames) {
|
|
9542
|
+
const prevField = assertDefined(prevType.fields[rename.previousFieldName], `renamed field "${rename.previousFieldName}" missing from prevType`);
|
|
9543
|
+
const currField = assertDefined(currType.fields[rename.fieldName], `renamed field "${rename.fieldName}" missing from currType`);
|
|
9544
|
+
ctx.changes.push({
|
|
9545
|
+
kind: "field_renamed",
|
|
9546
|
+
tableName,
|
|
9547
|
+
fieldName: rename.fieldName,
|
|
9548
|
+
previousFieldName: rename.previousFieldName,
|
|
9549
|
+
before: prevField,
|
|
8958
9550
|
after: currField
|
|
8959
|
-
}
|
|
9551
|
+
});
|
|
9552
|
+
ctx.breakingChanges.push({
|
|
9553
|
+
tableName,
|
|
9554
|
+
fieldName: rename.fieldName,
|
|
9555
|
+
reason: `Field renamed from ${rename.previousFieldName} to ${rename.fieldName} (existing values must be copied by the migration script)`
|
|
9556
|
+
});
|
|
8960
9557
|
}
|
|
8961
|
-
for (const fieldName of
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
9558
|
+
for (const fieldName of currFieldNames) {
|
|
9559
|
+
if (renamedToNames.has(fieldName)) continue;
|
|
9560
|
+
if (!prevFieldNames.has(fieldName)) {
|
|
9561
|
+
const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
|
|
9562
|
+
addChange(ctx, {
|
|
9563
|
+
kind: "field_added",
|
|
9564
|
+
tableName,
|
|
9565
|
+
fieldName,
|
|
9566
|
+
after: currField
|
|
9567
|
+
}, void 0, currField);
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
for (const fieldName of prevFieldNames) {
|
|
9571
|
+
if (renamedFromNames.has(fieldName)) continue;
|
|
9572
|
+
if (!currFieldNames.has(fieldName)) {
|
|
9573
|
+
const prevField = assertDefined(prevType.fields[fieldName], `field "${fieldName}" missing from prevType`);
|
|
9574
|
+
addChange(ctx, {
|
|
9575
|
+
kind: "field_removed",
|
|
9576
|
+
tableName,
|
|
9577
|
+
fieldName,
|
|
9578
|
+
before: prevField
|
|
9579
|
+
}, prevField, void 0);
|
|
9580
|
+
}
|
|
8969
9581
|
}
|
|
8970
9582
|
for (const fieldName of currFieldNames) {
|
|
8971
9583
|
if (!prevFieldNames.has(fieldName)) continue;
|
|
@@ -8973,7 +9585,7 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
|
|
|
8973
9585
|
const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
|
|
8974
9586
|
if (areFieldsDifferent(prevField, currField)) addChange(ctx, {
|
|
8975
9587
|
kind: prevField.type === currField.type ? "field_modified" : "field_type_modified",
|
|
8976
|
-
|
|
9588
|
+
tableName,
|
|
8977
9589
|
fieldName,
|
|
8978
9590
|
before: prevField,
|
|
8979
9591
|
after: currField
|
|
@@ -8985,20 +9597,20 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
|
|
|
8985
9597
|
* unique reasoning: enforcing a unique constraint over existing rows can fail
|
|
8986
9598
|
* on duplicates, so both adding a unique index and re-pointing an existing
|
|
8987
9599
|
* unique index at a different field set require a data migration.
|
|
8988
|
-
* @param {string}
|
|
9600
|
+
* @param {string} tableName - Name of the type containing the index
|
|
8989
9601
|
* @param {string} indexName - Name of the index being changed
|
|
8990
9602
|
* @param {SnapshotIndexConfig | undefined} oldIndex - Old index configuration
|
|
8991
9603
|
* @param {SnapshotIndexConfig | undefined} newIndex - New index configuration
|
|
8992
9604
|
* @returns {BreakingChangeInfo | null} Breaking change info or null if not breaking
|
|
8993
9605
|
*/
|
|
8994
|
-
function isBreakingIndexChange(
|
|
9606
|
+
function isBreakingIndexChange(tableName, indexName, oldIndex, newIndex) {
|
|
8995
9607
|
if (!newIndex || !(newIndex.unique ?? false)) return null;
|
|
8996
9608
|
if (!oldIndex || !(oldIndex.unique ?? false)) return {
|
|
8997
|
-
|
|
9609
|
+
tableName,
|
|
8998
9610
|
reason: `Unique constraint added to index "${indexName}"`
|
|
8999
9611
|
};
|
|
9000
9612
|
if (JSON.stringify(oldIndex.fields.toSorted()) !== JSON.stringify(newIndex.fields.toSorted())) return {
|
|
9001
|
-
|
|
9613
|
+
tableName,
|
|
9002
9614
|
reason: `Unique index fields changed on index "${indexName}"`
|
|
9003
9615
|
};
|
|
9004
9616
|
return null;
|
|
@@ -9006,27 +9618,27 @@ function isBreakingIndexChange(typeName, indexName, oldIndex, newIndex) {
|
|
|
9006
9618
|
/**
|
|
9007
9619
|
* Compare type-level indexes
|
|
9008
9620
|
* @param {DiffContext} ctx - Diff context
|
|
9009
|
-
* @param {string}
|
|
9621
|
+
* @param {string} tableName - Type name
|
|
9010
9622
|
* @param {Record<string, SnapshotIndexConfig> | undefined} oldIndexes - Previous indexes
|
|
9011
9623
|
* @param {Record<string, SnapshotIndexConfig> | undefined} newIndexes - Current indexes
|
|
9012
9624
|
* @returns {void}
|
|
9013
9625
|
*/
|
|
9014
|
-
function compareIndexes(ctx,
|
|
9626
|
+
function compareIndexes(ctx, tableName, oldIndexes, newIndexes) {
|
|
9015
9627
|
const oldKeys = new Set(Object.keys(oldIndexes || {}));
|
|
9016
9628
|
const newKeys = new Set(Object.keys(newIndexes || {}));
|
|
9017
9629
|
for (const [indexName, indexConfig] of Object.entries(newIndexes ?? {})) if (!oldKeys.has(indexName)) {
|
|
9018
9630
|
ctx.changes.push({
|
|
9019
9631
|
kind: "index_added",
|
|
9020
|
-
|
|
9632
|
+
tableName,
|
|
9021
9633
|
indexName,
|
|
9022
9634
|
after: indexConfig
|
|
9023
9635
|
});
|
|
9024
|
-
const breaking = isBreakingIndexChange(
|
|
9636
|
+
const breaking = isBreakingIndexChange(tableName, indexName, void 0, indexConfig);
|
|
9025
9637
|
if (breaking) ctx.breakingChanges.push(breaking);
|
|
9026
9638
|
}
|
|
9027
9639
|
for (const [indexName, indexConfig] of Object.entries(oldIndexes ?? {})) if (!newKeys.has(indexName)) ctx.changes.push({
|
|
9028
9640
|
kind: "index_removed",
|
|
9029
|
-
|
|
9641
|
+
tableName,
|
|
9030
9642
|
indexName,
|
|
9031
9643
|
before: indexConfig
|
|
9032
9644
|
});
|
|
@@ -9040,13 +9652,13 @@ function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
|
|
|
9040
9652
|
if ((oldIndex.unique ?? false) !== (newIndex.unique ?? false)) reasons.push("unique constraint changed");
|
|
9041
9653
|
ctx.changes.push({
|
|
9042
9654
|
kind: "index_modified",
|
|
9043
|
-
|
|
9655
|
+
tableName,
|
|
9044
9656
|
indexName,
|
|
9045
9657
|
reason: reasons.join(", "),
|
|
9046
9658
|
before: oldIndex,
|
|
9047
9659
|
after: newIndex
|
|
9048
9660
|
});
|
|
9049
|
-
const breaking = isBreakingIndexChange(
|
|
9661
|
+
const breaking = isBreakingIndexChange(tableName, indexName, oldIndex, newIndex);
|
|
9050
9662
|
if (breaking) ctx.breakingChanges.push(breaking);
|
|
9051
9663
|
}
|
|
9052
9664
|
}
|
|
@@ -9054,23 +9666,23 @@ function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
|
|
|
9054
9666
|
/**
|
|
9055
9667
|
* Compare type-level file fields
|
|
9056
9668
|
* @param {DiffContext} ctx - Diff context
|
|
9057
|
-
* @param {string}
|
|
9669
|
+
* @param {string} tableName - Type name
|
|
9058
9670
|
* @param {Record<string, string> | undefined} oldFiles - Previous file fields
|
|
9059
9671
|
* @param {Record<string, string> | undefined} newFiles - Current file fields
|
|
9060
9672
|
* @returns {void}
|
|
9061
9673
|
*/
|
|
9062
|
-
function compareFiles(ctx,
|
|
9674
|
+
function compareFiles(ctx, tableName, oldFiles, newFiles) {
|
|
9063
9675
|
const oldKeys = new Set(Object.keys(oldFiles || {}));
|
|
9064
9676
|
const newKeys = new Set(Object.keys(newFiles || {}));
|
|
9065
9677
|
for (const [fileName, fileDesc] of Object.entries(newFiles ?? {})) if (!oldKeys.has(fileName)) ctx.changes.push({
|
|
9066
9678
|
kind: "file_added",
|
|
9067
|
-
|
|
9679
|
+
tableName,
|
|
9068
9680
|
fieldName: fileName,
|
|
9069
9681
|
after: fileDesc
|
|
9070
9682
|
});
|
|
9071
9683
|
for (const [fileName, fileDesc] of Object.entries(oldFiles ?? {})) if (!newKeys.has(fileName)) ctx.changes.push({
|
|
9072
9684
|
kind: "file_removed",
|
|
9073
|
-
|
|
9685
|
+
tableName,
|
|
9074
9686
|
fieldName: fileName,
|
|
9075
9687
|
before: fileDesc
|
|
9076
9688
|
});
|
|
@@ -9078,7 +9690,7 @@ function compareFiles(ctx, typeName, oldFiles, newFiles) {
|
|
|
9078
9690
|
const oldDesc = assertDefined(assertDefined(oldFiles, "oldFiles is undefined when oldKeys has entry")[fileName], `file "${fileName}" missing from oldFiles`);
|
|
9079
9691
|
if (oldDesc !== newDesc) ctx.changes.push({
|
|
9080
9692
|
kind: "file_modified",
|
|
9081
|
-
|
|
9693
|
+
tableName,
|
|
9082
9694
|
fieldName: fileName,
|
|
9083
9695
|
reason: "description changed",
|
|
9084
9696
|
before: oldDesc,
|
|
@@ -9089,25 +9701,25 @@ function compareFiles(ctx, typeName, oldFiles, newFiles) {
|
|
|
9089
9701
|
/**
|
|
9090
9702
|
* Compare type-level relationships
|
|
9091
9703
|
* @param {DiffContext} ctx - Diff context
|
|
9092
|
-
* @param {string}
|
|
9704
|
+
* @param {string} tableName - Type name
|
|
9093
9705
|
* @param {"forward" | "backward"} relationshipType - Relationship direction to compare
|
|
9094
9706
|
* @param {Record<string, SnapshotRelationship> | undefined} oldRelationships - Previous relationships
|
|
9095
9707
|
* @param {Record<string, SnapshotRelationship> | undefined} newRelationships - Current relationships
|
|
9096
9708
|
* @returns {void}
|
|
9097
9709
|
*/
|
|
9098
|
-
function compareRelationships(ctx,
|
|
9710
|
+
function compareRelationships(ctx, tableName, relationshipType, oldRelationships, newRelationships) {
|
|
9099
9711
|
const oldKeys = new Set(Object.keys(oldRelationships || {}));
|
|
9100
9712
|
const newKeys = new Set(Object.keys(newRelationships || {}));
|
|
9101
9713
|
for (const [relName, rel] of Object.entries(newRelationships ?? {})) if (!oldKeys.has(relName)) ctx.changes.push({
|
|
9102
9714
|
kind: "relationship_added",
|
|
9103
|
-
|
|
9715
|
+
tableName,
|
|
9104
9716
|
relationshipName: relName,
|
|
9105
9717
|
relationshipType,
|
|
9106
9718
|
after: rel
|
|
9107
9719
|
});
|
|
9108
9720
|
for (const [relName, rel] of Object.entries(oldRelationships ?? {})) if (!newKeys.has(relName)) ctx.changes.push({
|
|
9109
9721
|
kind: "relationship_removed",
|
|
9110
|
-
|
|
9722
|
+
tableName,
|
|
9111
9723
|
relationshipName: relName,
|
|
9112
9724
|
relationshipType,
|
|
9113
9725
|
before: rel
|
|
@@ -9122,7 +9734,7 @@ function compareRelationships(ctx, typeName, relationshipType, oldRelationships,
|
|
|
9122
9734
|
if (oldRel.description !== newRel.description) reasons.push("description changed");
|
|
9123
9735
|
if (reasons.length > 0) ctx.changes.push({
|
|
9124
9736
|
kind: "relationship_modified",
|
|
9125
|
-
|
|
9737
|
+
tableName,
|
|
9126
9738
|
relationshipName: relName,
|
|
9127
9739
|
relationshipType,
|
|
9128
9740
|
reason: reasons.join(", "),
|
|
@@ -9134,14 +9746,14 @@ function compareRelationships(ctx, typeName, relationshipType, oldRelationships,
|
|
|
9134
9746
|
/**
|
|
9135
9747
|
* Compare type-level permissions
|
|
9136
9748
|
* @param {DiffContext} ctx - Diff context
|
|
9137
|
-
* @param {string}
|
|
9749
|
+
* @param {string} tableName - Type name
|
|
9138
9750
|
* @param {SnapshotRecordPermission | undefined} oldRecordPerm - Previous record permission
|
|
9139
9751
|
* @param {SnapshotRecordPermission | undefined} newRecordPerm - Current record permission
|
|
9140
9752
|
* @param {SnapshotGqlPermission | undefined} oldGqlPerm - Previous GQL permission
|
|
9141
9753
|
* @param {SnapshotGqlPermission | undefined} newGqlPerm - Current GQL permission
|
|
9142
9754
|
* @returns {void}
|
|
9143
9755
|
*/
|
|
9144
|
-
function comparePermissions(ctx,
|
|
9756
|
+
function comparePermissions(ctx, tableName, oldRecordPerm, newRecordPerm, oldGqlPerm, newGqlPerm) {
|
|
9145
9757
|
const oldComparableRecordPerm = comparableRecordPermission(oldRecordPerm);
|
|
9146
9758
|
const newComparableRecordPerm = comparableRecordPermission(newRecordPerm);
|
|
9147
9759
|
const recordPermChanged = JSON.stringify(oldComparableRecordPerm ?? null) !== JSON.stringify(newComparableRecordPerm ?? null);
|
|
@@ -9154,7 +9766,7 @@ function comparePermissions(ctx, typeName, oldRecordPerm, newRecordPerm, oldGqlP
|
|
|
9154
9766
|
if (gqlPermChanged) reasons.push("GQL permission");
|
|
9155
9767
|
ctx.changes.push({
|
|
9156
9768
|
kind: "permission_modified",
|
|
9157
|
-
|
|
9769
|
+
tableName,
|
|
9158
9770
|
reason: `${reasons.join(" and ")} changed`,
|
|
9159
9771
|
before: {
|
|
9160
9772
|
recordPermission: oldComparableRecordPerm,
|
|
@@ -9234,13 +9846,13 @@ function comparableTypeSettings(type) {
|
|
|
9234
9846
|
function snapshotTypeSettingsState(type) {
|
|
9235
9847
|
return typeSettingsState(type.description, type.pluralForm, type.settings ?? {});
|
|
9236
9848
|
}
|
|
9237
|
-
function compareTypeSettings(ctx,
|
|
9849
|
+
function compareTypeSettings(ctx, tableName, previous, current) {
|
|
9238
9850
|
const previousComparable = comparableTypeSettings(previous);
|
|
9239
9851
|
const currentComparable = comparableTypeSettings(current);
|
|
9240
9852
|
if (JSON.stringify(previousComparable) === JSON.stringify(currentComparable)) return;
|
|
9241
9853
|
ctx.changes.push({
|
|
9242
|
-
kind: "
|
|
9243
|
-
|
|
9854
|
+
kind: "table_settings_modified",
|
|
9855
|
+
tableName,
|
|
9244
9856
|
reason: "settings changed",
|
|
9245
9857
|
before: snapshotTypeSettingsState(previous),
|
|
9246
9858
|
after: snapshotTypeSettingsState(current)
|
|
@@ -9252,66 +9864,190 @@ function typeScriptsState(type) {
|
|
|
9252
9864
|
...type.typeValidateExpr !== void 0 && { typeValidateExpr: type.typeValidateExpr }
|
|
9253
9865
|
};
|
|
9254
9866
|
}
|
|
9255
|
-
function compareTypeScripts(ctx,
|
|
9867
|
+
function compareTypeScripts(ctx, tableName, previous, current) {
|
|
9256
9868
|
const prevState = typeScriptsState(previous);
|
|
9257
9869
|
const currState = typeScriptsState(current);
|
|
9258
9870
|
if (JSON.stringify(prevState) === JSON.stringify(currState)) return;
|
|
9259
9871
|
ctx.changes.push({
|
|
9260
|
-
kind: "
|
|
9261
|
-
|
|
9262
|
-
reason: "
|
|
9872
|
+
kind: "table_scripts_modified",
|
|
9873
|
+
tableName,
|
|
9874
|
+
reason: "table-level scripts changed",
|
|
9263
9875
|
before: prevState,
|
|
9264
9876
|
after: currState
|
|
9265
9877
|
});
|
|
9266
9878
|
}
|
|
9267
9879
|
/**
|
|
9268
|
-
*
|
|
9269
|
-
*
|
|
9270
|
-
*
|
|
9880
|
+
* Restate the schema an expand migration starts from, with each converted field
|
|
9881
|
+
* relaxed to optional.
|
|
9882
|
+
*
|
|
9883
|
+
* The expand script clears the original field once it has carried the value
|
|
9884
|
+
* across. That write reaches the field under the contract recorded on the
|
|
9885
|
+
* removal, which the deploy restores for the duration of the migration, so a
|
|
9886
|
+
* field left required would reject it.
|
|
9887
|
+
* @param previous - Snapshot the expand migration starts from
|
|
9888
|
+
* @param plans - Field changes carried through temporary fields
|
|
9889
|
+
* @returns Snapshot to compare the expand migration against
|
|
9890
|
+
*/
|
|
9891
|
+
function buildExpandBaseSnapshot(previous, plans) {
|
|
9892
|
+
const tables = copySnapshotRecord(previous.tables);
|
|
9893
|
+
for (const plan of plans) {
|
|
9894
|
+
const type = tables[plan.tableName];
|
|
9895
|
+
const original = type?.fields[plan.fieldName];
|
|
9896
|
+
if (!type || !original) continue;
|
|
9897
|
+
const fields = copySnapshotRecord(type.fields);
|
|
9898
|
+
fields[plan.fieldName] = {
|
|
9899
|
+
...original,
|
|
9900
|
+
required: false
|
|
9901
|
+
};
|
|
9902
|
+
tables[plan.tableName] = {
|
|
9903
|
+
...type,
|
|
9904
|
+
fields
|
|
9905
|
+
};
|
|
9906
|
+
}
|
|
9907
|
+
return normalizeSchemaSnapshot({
|
|
9908
|
+
...previous,
|
|
9909
|
+
tables
|
|
9910
|
+
});
|
|
9911
|
+
}
|
|
9912
|
+
/**
|
|
9913
|
+
* Build the diff for the migration that converts values into temporary fields.
|
|
9914
|
+
*
|
|
9915
|
+
* Adding an optional field and removing one are both non-breaking, so nothing
|
|
9916
|
+
* in the comparison marks the script as required — yet it is the only thing
|
|
9917
|
+
* carrying the values across before the original field is dropped.
|
|
9918
|
+
* @param previous - Snapshot the expand migration starts from
|
|
9919
|
+
* @param intermediate - Snapshot the expand migration produces
|
|
9920
|
+
* @param plans - Field changes carried through temporary fields
|
|
9921
|
+
* @returns Diff to write for the expand migration
|
|
9922
|
+
*/
|
|
9923
|
+
function buildExpandDiff(previous, intermediate, plans) {
|
|
9924
|
+
return {
|
|
9925
|
+
...compareSnapshots(buildExpandBaseSnapshot(previous, plans), intermediate),
|
|
9926
|
+
requiresMigrationScript: true
|
|
9927
|
+
};
|
|
9928
|
+
}
|
|
9929
|
+
/**
|
|
9930
|
+
* Build the schema state that sits between an expand and a contract migration:
|
|
9931
|
+
* each converted field is replaced by its temporary counterpart.
|
|
9932
|
+
*
|
|
9933
|
+
* The original field is dropped here rather than in the contract migration so
|
|
9934
|
+
* the contract can reuse its name. It stays readable while the expand script
|
|
9935
|
+
* runs, because a field removed by a migration is retained until that same
|
|
9936
|
+
* migration's post phase.
|
|
9937
|
+
*
|
|
9938
|
+
* The temporary field is optional and non-unique regardless of its final
|
|
9939
|
+
* contract, since the expand script fills it in batches.
|
|
9940
|
+
* @param previous - Snapshot the expand migration starts from
|
|
9941
|
+
* @param plans - Field changes carried through temporary fields
|
|
9942
|
+
* @returns Snapshot the contract migration compares against
|
|
9943
|
+
*/
|
|
9944
|
+
function buildIntermediateSnapshot(previous, plans) {
|
|
9945
|
+
const tables = copySnapshotRecord(previous.tables);
|
|
9946
|
+
for (const plan of plans) {
|
|
9947
|
+
const type = tables[plan.tableName];
|
|
9948
|
+
if (!type) continue;
|
|
9949
|
+
const fields = copySnapshotRecord(type.fields);
|
|
9950
|
+
const { hooks: _hooks, validate: _validate, ...carried } = plan.after;
|
|
9951
|
+
fields[plan.tempFieldName] = {
|
|
9952
|
+
...carried,
|
|
9953
|
+
required: false,
|
|
9954
|
+
unique: false
|
|
9955
|
+
};
|
|
9956
|
+
delete fields[plan.fieldName];
|
|
9957
|
+
tables[plan.tableName] = {
|
|
9958
|
+
...type,
|
|
9959
|
+
fields
|
|
9960
|
+
};
|
|
9961
|
+
}
|
|
9962
|
+
return normalizeSchemaSnapshot({
|
|
9963
|
+
...previous,
|
|
9964
|
+
tables
|
|
9965
|
+
});
|
|
9966
|
+
}
|
|
9967
|
+
/**
|
|
9968
|
+
* Compare two normalized snapshots and generate a diff
|
|
9969
|
+
* @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
|
|
9970
|
+
* @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
|
|
9971
|
+
* @param {CompareSnapshotsOptions} [options] - Comparison options
|
|
9271
9972
|
* @returns {MigrationDiff} Migration diff between snapshots
|
|
9272
9973
|
*/
|
|
9273
|
-
function compareSnapshots(previous, current) {
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9974
|
+
function compareSnapshots(previous, current, options) {
|
|
9975
|
+
const fieldRenames = options?.fieldRenames ?? [];
|
|
9976
|
+
assertValidFieldRenames(previous, current, fieldRenames);
|
|
9977
|
+
const renamesByType = /* @__PURE__ */ new Map();
|
|
9978
|
+
for (const rename of fieldRenames) {
|
|
9979
|
+
const list = renamesByType.get(rename.tableName) ?? [];
|
|
9980
|
+
list.push(rename);
|
|
9981
|
+
renamesByType.set(rename.tableName, list);
|
|
9982
|
+
}
|
|
9983
|
+
const typeRenames = options?.typeRenames ?? [];
|
|
9984
|
+
assertValidTypeRenames(previous, current, typeRenames);
|
|
9985
|
+
const typeRenameTargets = new Map(typeRenames.map((r) => [r.previousTableName, r.tableName]));
|
|
9986
|
+
const renamedToTypeNames = new Set(typeRenames.map((r) => r.tableName));
|
|
9277
9987
|
const ctx = {
|
|
9278
9988
|
changes: [],
|
|
9279
9989
|
breakingChanges: [],
|
|
9280
|
-
warnings: []
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
const
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
});
|
|
9289
|
-
for (const [typeName, type] of Object.entries(previous.types)) if (!currentTypeNames.has(typeName)) {
|
|
9990
|
+
warnings: [],
|
|
9991
|
+
typeRenameTargets
|
|
9992
|
+
};
|
|
9993
|
+
const previousTypeNames = new Set(Object.keys(previous.tables));
|
|
9994
|
+
const currentTypeNames = new Set(Object.keys(current.tables));
|
|
9995
|
+
for (const rename of typeRenames) {
|
|
9996
|
+
const prevType = assertDefined(previous.tables[rename.previousTableName], `renamed type "${rename.previousTableName}" missing from previous snapshot`);
|
|
9997
|
+
const currType = assertDefined(current.tables[rename.tableName], `renamed type "${rename.tableName}" missing from current snapshot`);
|
|
9290
9998
|
ctx.changes.push({
|
|
9291
|
-
kind: "
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9999
|
+
kind: "table_renamed",
|
|
10000
|
+
tableName: rename.tableName,
|
|
10001
|
+
previousTableName: rename.previousTableName,
|
|
10002
|
+
before: prevType,
|
|
10003
|
+
after: currType
|
|
10004
|
+
});
|
|
10005
|
+
ctx.breakingChanges.push({
|
|
10006
|
+
tableName: rename.tableName,
|
|
10007
|
+
reason: `Type renamed from ${rename.previousTableName} to ${rename.tableName} (existing records must be copied by the migration script)`
|
|
10008
|
+
});
|
|
10009
|
+
ctx.breakingChanges.push({
|
|
10010
|
+
tableName: rename.tableName,
|
|
10011
|
+
reason: `GraphQL API names derived from ${rename.previousTableName}/${prevType.pluralForm} change to ${rename.tableName}/${currType.pluralForm} — breaking for API clients`
|
|
10012
|
+
});
|
|
10013
|
+
}
|
|
10014
|
+
for (const [tableName, type] of Object.entries(current.tables)) {
|
|
10015
|
+
if (renamedToTypeNames.has(tableName)) continue;
|
|
10016
|
+
if (!previousTypeNames.has(tableName)) ctx.changes.push({
|
|
10017
|
+
kind: "table_added",
|
|
10018
|
+
tableName,
|
|
10019
|
+
after: type
|
|
10020
|
+
});
|
|
10021
|
+
}
|
|
10022
|
+
for (const [tableName, type] of Object.entries(previous.tables)) {
|
|
10023
|
+
if (typeRenameTargets.has(tableName)) continue;
|
|
10024
|
+
if (!currentTypeNames.has(tableName)) {
|
|
10025
|
+
ctx.changes.push({
|
|
10026
|
+
kind: "table_removed",
|
|
10027
|
+
tableName,
|
|
10028
|
+
before: type
|
|
10029
|
+
});
|
|
10030
|
+
ctx.warnings.push({
|
|
10031
|
+
tableName,
|
|
10032
|
+
reason: TABLE_REMOVED_WARNING_REASON
|
|
10033
|
+
});
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
for (const tableName of currentTypeNames) {
|
|
10037
|
+
if (!previousTypeNames.has(tableName)) continue;
|
|
10038
|
+
const prevType = assertDefined(previous.tables[tableName], `type "${tableName}" missing from previous snapshot`);
|
|
10039
|
+
const currType = assertDefined(current.tables[tableName], `type "${tableName}" missing from current snapshot`);
|
|
10040
|
+
compareTypeSettings(ctx, tableName, prevType, currType);
|
|
10041
|
+
compareTypeScripts(ctx, tableName, prevType, currType);
|
|
10042
|
+
compareTypeFields(ctx, tableName, prevType, currType, renamesByType.get(tableName));
|
|
10043
|
+
compareIndexes(ctx, tableName, prevType.indexes, currType.indexes);
|
|
10044
|
+
compareFiles(ctx, tableName, prevType.files, currType.files);
|
|
10045
|
+
compareRelationships(ctx, tableName, "forward", prevType.forwardRelationships, currType.forwardRelationships);
|
|
10046
|
+
compareRelationships(ctx, tableName, "backward", prevType.backwardRelationships, currType.backwardRelationships);
|
|
10047
|
+
comparePermissions(ctx, tableName, prevType.permissions?.record, currType.permissions?.record, prevType.permissions?.gql, currType.permissions?.gql);
|
|
9312
10048
|
}
|
|
9313
10049
|
return {
|
|
9314
|
-
version:
|
|
10050
|
+
version: 5,
|
|
9315
10051
|
namespace: current.namespace,
|
|
9316
10052
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9317
10053
|
changes: ctx.changes,
|
|
@@ -9325,8 +10061,8 @@ function compareNormalizedSnapshots(previous, current) {
|
|
|
9325
10061
|
/**
|
|
9326
10062
|
* Compare a snapshot against canonical TailorDBSnapshotType-shaped local types.
|
|
9327
10063
|
* Callers are expected to pre-convert TailorDBService.types to TailorDBSnapshotType via
|
|
9328
|
-
* `createSnapshotType`. As a safety net,
|
|
9329
|
-
* normalization
|
|
10064
|
+
* `createSnapshotType`. As a safety net, both sides are re-run through idempotent
|
|
10065
|
+
* normalization here, so a caller that forgets will still get correct
|
|
9330
10066
|
* comparisons (no silent false drift).
|
|
9331
10067
|
* @param {SchemaSnapshot} snapshot - Schema snapshot to compare against
|
|
9332
10068
|
* @param {Record<string, TailorDBSnapshotType>} localTypes - Local snapshot-shaped types
|
|
@@ -9334,12 +10070,13 @@ function compareNormalizedSnapshots(previous, current) {
|
|
|
9334
10070
|
* @returns {MigrationDiff} Migration diff
|
|
9335
10071
|
*/
|
|
9336
10072
|
function compareLocalTypesWithSnapshot(snapshot, localTypes, namespace) {
|
|
9337
|
-
|
|
9338
|
-
version:
|
|
10073
|
+
const currentSnapshot = {
|
|
10074
|
+
version: 5,
|
|
9339
10075
|
namespace,
|
|
9340
10076
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9341
|
-
|
|
9342
|
-
}
|
|
10077
|
+
tables: localTypes
|
|
10078
|
+
};
|
|
10079
|
+
return compareSnapshots(normalizeSchemaSnapshot(snapshot), normalizeSchemaSnapshot(currentSnapshot));
|
|
9343
10080
|
}
|
|
9344
10081
|
/**
|
|
9345
10082
|
* Validate migration files in a directory
|
|
@@ -9640,11 +10377,11 @@ function convertRemoteTypeToSnapshot(remoteType, expectedType) {
|
|
|
9640
10377
|
* @returns {NormalizedSchemaSnapshot} Normalized snapshot-shaped remote state
|
|
9641
10378
|
*/
|
|
9642
10379
|
function createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissions = [], expectedSnapshot) {
|
|
9643
|
-
const
|
|
9644
|
-
for (const remoteType of remoteTypes)
|
|
10380
|
+
const tables = createSnapshotRecord();
|
|
10381
|
+
for (const remoteType of remoteTypes) tables[remoteType.name] = convertRemoteTypeToSnapshot(remoteType, expectedSnapshot?.tables[remoteType.name]);
|
|
9645
10382
|
for (const permission of remoteGqlPermissions) {
|
|
9646
|
-
const { typeName } = permission;
|
|
9647
|
-
const snapshotType =
|
|
10383
|
+
const { typeName: tableName } = permission;
|
|
10384
|
+
const snapshotType = tables[tableName];
|
|
9648
10385
|
if (!snapshotType) continue;
|
|
9649
10386
|
const gqlPermission = convertRemoteGqlPermissionToSnapshot(permission.permission);
|
|
9650
10387
|
if (!gqlPermission) continue;
|
|
@@ -9654,10 +10391,10 @@ function createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissi
|
|
|
9654
10391
|
};
|
|
9655
10392
|
}
|
|
9656
10393
|
return normalizeSchemaSnapshot({
|
|
9657
|
-
version:
|
|
10394
|
+
version: 5,
|
|
9658
10395
|
namespace,
|
|
9659
10396
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9660
|
-
|
|
10397
|
+
tables
|
|
9661
10398
|
});
|
|
9662
10399
|
}
|
|
9663
10400
|
function fieldDifferenceValue(value) {
|
|
@@ -9744,13 +10481,7 @@ function addNestedFieldDifferences(differences, prefix, remoteField, snapshotFie
|
|
|
9744
10481
|
function addFieldDifferences(differences, prefix, remoteField, snapshotField) {
|
|
9745
10482
|
addFieldDifference(differences, prefix, "type", remoteField.type, snapshotField.type);
|
|
9746
10483
|
addFieldDifference(differences, prefix, "required", remoteField.required, snapshotField.required);
|
|
9747
|
-
for (const key of
|
|
9748
|
-
"array",
|
|
9749
|
-
"index",
|
|
9750
|
-
"unique",
|
|
9751
|
-
"foreignKey",
|
|
9752
|
-
"vector"
|
|
9753
|
-
]) addBooleanFieldDifference(differences, prefix, key, remoteField, snapshotField);
|
|
10484
|
+
for (const key of SNAPSHOT_FIELD_BOOLEAN_PROPS) addBooleanFieldDifference(differences, prefix, key, remoteField, snapshotField);
|
|
9754
10485
|
addFieldDifference(differences, prefix, "foreignKeyType", remoteField.foreignKeyType, snapshotField.foreignKeyType);
|
|
9755
10486
|
addFieldDifference(differences, prefix, "foreignKeyField", remoteField.foreignKeyField, snapshotField.foreignKeyField);
|
|
9756
10487
|
addFieldDifference(differences, prefix, "description", remoteField.description ?? "", snapshotField.description ?? "");
|
|
@@ -9763,17 +10494,17 @@ function addFieldDifferences(differences, prefix, remoteField, snapshotField) {
|
|
|
9763
10494
|
}
|
|
9764
10495
|
/**
|
|
9765
10496
|
* Compare a single field between remote and snapshot
|
|
9766
|
-
* @param {string}
|
|
10497
|
+
* @param {string} tableName - Name of the type
|
|
9767
10498
|
* @param {string} fieldName - Name of the field
|
|
9768
10499
|
* @param {SnapshotFieldConfig} remoteField - Remote field config
|
|
9769
10500
|
* @param {SnapshotFieldConfig} snapshotField - Snapshot field config
|
|
9770
10501
|
* @returns {SchemaDrift | null} Drift info or null if fields match
|
|
9771
10502
|
*/
|
|
9772
|
-
function compareFields(
|
|
10503
|
+
function compareFields(tableName, fieldName, remoteField, snapshotField) {
|
|
9773
10504
|
const differences = [];
|
|
9774
10505
|
addFieldDifferences(differences, "", remoteField, snapshotField);
|
|
9775
10506
|
if (differences.length > 0) return {
|
|
9776
|
-
|
|
10507
|
+
tableName,
|
|
9777
10508
|
kind: "field_mismatch",
|
|
9778
10509
|
fieldName,
|
|
9779
10510
|
details: differences.join("; ")
|
|
@@ -9819,24 +10550,24 @@ function remoteHasScripts(remoteType) {
|
|
|
9819
10550
|
function compareScriptHashes(remoteTypes, snapshot) {
|
|
9820
10551
|
const drifts = [];
|
|
9821
10552
|
const remoteByName = new Map(remoteTypes.map((t) => [t.name, t]));
|
|
9822
|
-
for (const [
|
|
10553
|
+
for (const [tableName, snapshotType] of Object.entries(snapshot.tables)) {
|
|
9823
10554
|
const localHash = computeSourceScriptHash(snapshotType.fields, {
|
|
9824
10555
|
typeHookExpr: snapshotType.typeHookExpr,
|
|
9825
10556
|
typeValidateExpr: snapshotType.typeValidateExpr
|
|
9826
10557
|
});
|
|
9827
|
-
const remoteType = remoteByName.get(
|
|
10558
|
+
const remoteType = remoteByName.get(tableName);
|
|
9828
10559
|
if (!remoteType) continue;
|
|
9829
10560
|
if (localHash) {
|
|
9830
10561
|
const remoteHash = extractRemoteScriptHash(remoteType);
|
|
9831
10562
|
if (localHash !== remoteHash) drifts.push({
|
|
9832
|
-
|
|
10563
|
+
tableName,
|
|
9833
10564
|
kind: "script_mismatch",
|
|
9834
|
-
details: remoteHash ? `Type '${
|
|
10565
|
+
details: remoteHash ? `Type '${tableName}' scripts differ between remote and snapshot` : `Type '${tableName}' has no script hash on remote`
|
|
9835
10566
|
});
|
|
9836
10567
|
} else if (remoteHasScripts(remoteType)) drifts.push({
|
|
9837
|
-
|
|
10568
|
+
tableName,
|
|
9838
10569
|
kind: "script_mismatch",
|
|
9839
|
-
details: `Type '${
|
|
10570
|
+
details: `Type '${tableName}' has scripts on remote but not in snapshot`
|
|
9840
10571
|
});
|
|
9841
10572
|
}
|
|
9842
10573
|
return drifts;
|
|
@@ -9854,27 +10585,27 @@ function stripFieldScriptProps(field) {
|
|
|
9854
10585
|
return rest;
|
|
9855
10586
|
}
|
|
9856
10587
|
function createRemoteComparableSnapshot(snapshot) {
|
|
9857
|
-
const
|
|
9858
|
-
for (const [
|
|
10588
|
+
const tables = createSnapshotRecord();
|
|
10589
|
+
for (const [tableName, type] of Object.entries(snapshot.tables)) {
|
|
9859
10590
|
const fields = createSnapshotRecord();
|
|
9860
10591
|
for (const [fieldName, field] of Object.entries(type.fields)) {
|
|
9861
10592
|
if (SYSTEM_FIELDS.has(fieldName)) continue;
|
|
9862
10593
|
fields[fieldName] = stripFieldScriptProps(field);
|
|
9863
10594
|
}
|
|
9864
10595
|
const { typeHookExpr: _, typeValidateExpr: __, ...typeRest } = type;
|
|
9865
|
-
|
|
10596
|
+
tables[tableName] = {
|
|
9866
10597
|
...typeRest,
|
|
9867
10598
|
fields
|
|
9868
10599
|
};
|
|
9869
10600
|
}
|
|
9870
10601
|
return normalizeSchemaSnapshot({
|
|
9871
10602
|
...snapshot,
|
|
9872
|
-
|
|
10603
|
+
tables
|
|
9873
10604
|
});
|
|
9874
10605
|
}
|
|
9875
10606
|
function fieldDriftFromChange(change) {
|
|
9876
|
-
return compareFields(change.
|
|
9877
|
-
|
|
10607
|
+
return compareFields(change.tableName, change.fieldName, change.before, change.after) ?? {
|
|
10608
|
+
tableName: change.tableName,
|
|
9878
10609
|
kind: "field_mismatch",
|
|
9879
10610
|
fieldName: change.fieldName,
|
|
9880
10611
|
details: `Field '${change.fieldName}' differs between remote and snapshot`
|
|
@@ -9882,100 +10613,111 @@ function fieldDriftFromChange(change) {
|
|
|
9882
10613
|
}
|
|
9883
10614
|
function schemaDriftFromDiffChange(change) {
|
|
9884
10615
|
switch (change.kind) {
|
|
9885
|
-
case "
|
|
9886
|
-
|
|
10616
|
+
case "table_added": return {
|
|
10617
|
+
tableName: change.tableName,
|
|
9887
10618
|
kind: "type_missing_remote",
|
|
9888
|
-
details: `Type '${change.
|
|
10619
|
+
details: `Type '${change.tableName}' exists in snapshot but not in remote`
|
|
9889
10620
|
};
|
|
9890
|
-
case "
|
|
9891
|
-
|
|
10621
|
+
case "table_removed": return {
|
|
10622
|
+
tableName: change.tableName,
|
|
9892
10623
|
kind: "type_missing_local",
|
|
9893
|
-
details: `Type '${change.
|
|
10624
|
+
details: `Type '${change.tableName}' exists in remote but not in snapshot`
|
|
10625
|
+
};
|
|
10626
|
+
case "table_renamed": return {
|
|
10627
|
+
tableName: change.tableName,
|
|
10628
|
+
kind: "type_settings_mismatch",
|
|
10629
|
+
details: `Type '${change.previousTableName}' was renamed to '${change.tableName}'`
|
|
9894
10630
|
};
|
|
9895
|
-
case "
|
|
9896
|
-
case "
|
|
9897
|
-
|
|
10631
|
+
case "table_settings_modified":
|
|
10632
|
+
case "table_modified": return {
|
|
10633
|
+
tableName: change.tableName,
|
|
9898
10634
|
kind: "type_settings_mismatch",
|
|
9899
10635
|
details: change.reason ?? "Type settings differ between remote and snapshot"
|
|
9900
10636
|
};
|
|
9901
10637
|
case "field_added": return {
|
|
9902
|
-
|
|
10638
|
+
tableName: change.tableName,
|
|
9903
10639
|
kind: "field_missing_remote",
|
|
9904
10640
|
fieldName: change.fieldName,
|
|
9905
10641
|
details: `Field '${change.fieldName}' exists in snapshot but not in remote`
|
|
9906
10642
|
};
|
|
9907
10643
|
case "field_removed": return {
|
|
9908
|
-
|
|
10644
|
+
tableName: change.tableName,
|
|
9909
10645
|
kind: "field_missing_local",
|
|
9910
10646
|
fieldName: change.fieldName,
|
|
9911
10647
|
details: `Field '${change.fieldName}' exists in remote but not in snapshot`
|
|
9912
10648
|
};
|
|
9913
10649
|
case "field_modified":
|
|
9914
10650
|
case "field_type_modified": return fieldDriftFromChange(change);
|
|
10651
|
+
case "field_renamed": return {
|
|
10652
|
+
tableName: change.tableName,
|
|
10653
|
+
kind: "field_mismatch",
|
|
10654
|
+
fieldName: change.fieldName,
|
|
10655
|
+
details: `Field '${change.previousFieldName}' was renamed to '${change.fieldName}'`
|
|
10656
|
+
};
|
|
9915
10657
|
case "index_added": return {
|
|
9916
|
-
|
|
10658
|
+
tableName: change.tableName,
|
|
9917
10659
|
kind: "index_missing_remote",
|
|
9918
10660
|
indexName: change.indexName,
|
|
9919
10661
|
details: `Index '${change.indexName}' exists in snapshot but not in remote`
|
|
9920
10662
|
};
|
|
9921
10663
|
case "index_removed": return {
|
|
9922
|
-
|
|
10664
|
+
tableName: change.tableName,
|
|
9923
10665
|
kind: "index_missing_local",
|
|
9924
10666
|
indexName: change.indexName,
|
|
9925
10667
|
details: `Index '${change.indexName}' exists in remote but not in snapshot`
|
|
9926
10668
|
};
|
|
9927
10669
|
case "index_modified": return {
|
|
9928
|
-
|
|
10670
|
+
tableName: change.tableName,
|
|
9929
10671
|
kind: "index_mismatch",
|
|
9930
10672
|
indexName: change.indexName,
|
|
9931
10673
|
details: change.reason ?? `Index '${change.indexName}' differs between remote and snapshot`
|
|
9932
10674
|
};
|
|
9933
10675
|
case "file_added": return {
|
|
9934
|
-
|
|
10676
|
+
tableName: change.tableName,
|
|
9935
10677
|
kind: "file_missing_remote",
|
|
9936
10678
|
fileName: change.fieldName,
|
|
9937
10679
|
details: `File '${change.fieldName}' exists in snapshot but not in remote`
|
|
9938
10680
|
};
|
|
9939
10681
|
case "file_removed": return {
|
|
9940
|
-
|
|
10682
|
+
tableName: change.tableName,
|
|
9941
10683
|
kind: "file_missing_local",
|
|
9942
10684
|
fileName: change.fieldName,
|
|
9943
10685
|
details: `File '${change.fieldName}' exists in remote but not in snapshot`
|
|
9944
10686
|
};
|
|
9945
10687
|
case "file_modified": return {
|
|
9946
|
-
|
|
10688
|
+
tableName: change.tableName,
|
|
9947
10689
|
kind: "file_mismatch",
|
|
9948
10690
|
fileName: change.fieldName,
|
|
9949
10691
|
details: change.reason ?? `File '${change.fieldName}' differs between remote and snapshot`
|
|
9950
10692
|
};
|
|
9951
10693
|
case "relationship_added": return {
|
|
9952
|
-
|
|
10694
|
+
tableName: change.tableName,
|
|
9953
10695
|
kind: "relationship_missing_remote",
|
|
9954
10696
|
relationshipName: change.relationshipName,
|
|
9955
10697
|
relationshipType: change.relationshipType,
|
|
9956
10698
|
details: `Relationship '${change.relationshipName}' exists in snapshot but not in remote`
|
|
9957
10699
|
};
|
|
9958
10700
|
case "relationship_removed": return {
|
|
9959
|
-
|
|
10701
|
+
tableName: change.tableName,
|
|
9960
10702
|
kind: "relationship_missing_local",
|
|
9961
10703
|
relationshipName: change.relationshipName,
|
|
9962
10704
|
relationshipType: change.relationshipType,
|
|
9963
10705
|
details: `Relationship '${change.relationshipName}' exists in remote but not in snapshot`
|
|
9964
10706
|
};
|
|
9965
10707
|
case "relationship_modified": return {
|
|
9966
|
-
|
|
10708
|
+
tableName: change.tableName,
|
|
9967
10709
|
kind: "relationship_mismatch",
|
|
9968
10710
|
relationshipName: change.relationshipName,
|
|
9969
10711
|
relationshipType: change.relationshipType,
|
|
9970
10712
|
details: change.reason ?? `Relationship '${change.relationshipName}' differs between remote and snapshot`
|
|
9971
10713
|
};
|
|
9972
10714
|
case "permission_modified": return {
|
|
9973
|
-
|
|
10715
|
+
tableName: change.tableName,
|
|
9974
10716
|
kind: "permission_mismatch",
|
|
9975
10717
|
details: change.reason ?? "Permissions differ between remote and snapshot"
|
|
9976
10718
|
};
|
|
9977
|
-
case "
|
|
9978
|
-
|
|
10719
|
+
case "table_scripts_modified": return {
|
|
10720
|
+
tableName: change.tableName,
|
|
9979
10721
|
kind: "script_mismatch",
|
|
9980
10722
|
details: change.reason ?? "Type-level scripts differ between remote and snapshot"
|
|
9981
10723
|
};
|
|
@@ -9983,7 +10725,7 @@ function schemaDriftFromDiffChange(change) {
|
|
|
9983
10725
|
}
|
|
9984
10726
|
}
|
|
9985
10727
|
function compareNormalizedRemoteWithSnapshot(remoteSnapshot, snapshot) {
|
|
9986
|
-
return
|
|
10728
|
+
return compareSnapshots(remoteSnapshot, snapshot).changes.map(schemaDriftFromDiffChange);
|
|
9987
10729
|
}
|
|
9988
10730
|
/**
|
|
9989
10731
|
* Format schema drifts for display
|
|
@@ -9995,12 +10737,12 @@ function formatSchemaDrifts(drifts) {
|
|
|
9995
10737
|
const lines = [];
|
|
9996
10738
|
const driftsByType = /* @__PURE__ */ new Map();
|
|
9997
10739
|
for (const drift of drifts) {
|
|
9998
|
-
const existing = driftsByType.get(drift.
|
|
10740
|
+
const existing = driftsByType.get(drift.tableName) ?? [];
|
|
9999
10741
|
existing.push(drift);
|
|
10000
|
-
driftsByType.set(drift.
|
|
10742
|
+
driftsByType.set(drift.tableName, existing);
|
|
10001
10743
|
}
|
|
10002
|
-
for (const [
|
|
10003
|
-
lines.push(` Type '${
|
|
10744
|
+
for (const [tableName, typeDrifts] of driftsByType) {
|
|
10745
|
+
lines.push(` Type '${tableName}':`);
|
|
10004
10746
|
for (const drift of typeDrifts) if (drift.fieldName) lines.push(` - Field '${drift.fieldName}': ${drift.details}`);
|
|
10005
10747
|
else if (drift.indexName) lines.push(` - Index '${drift.indexName}': ${drift.details}`);
|
|
10006
10748
|
else if (drift.fileName) lines.push(` - File '${drift.fileName}': ${drift.details}`);
|
|
@@ -10014,12 +10756,7 @@ function formatSchemaDrifts(drifts) {
|
|
|
10014
10756
|
|
|
10015
10757
|
//#endregion
|
|
10016
10758
|
//#region src/cli/commands/tailordb/migrate/file-state.ts
|
|
10017
|
-
const MIGRATION_FILE_KINDS =
|
|
10018
|
-
"schema",
|
|
10019
|
-
"diff",
|
|
10020
|
-
"migrate",
|
|
10021
|
-
"db"
|
|
10022
|
-
];
|
|
10759
|
+
const MIGRATION_FILE_KINDS = Object.keys(MIGRATION_FILE_NAMES);
|
|
10023
10760
|
function getMigrationArtifactNumbers(migrationsDir) {
|
|
10024
10761
|
if (!fs$2.existsSync(migrationsDir)) return [];
|
|
10025
10762
|
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);
|
|
@@ -10391,7 +11128,7 @@ function convertOperandToProto(operand) {
|
|
|
10391
11128
|
function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
|
|
10392
11129
|
const manifests = /* @__PURE__ */ new Map();
|
|
10393
11130
|
const { executorUsedTypes, ...baseOptions } = options;
|
|
10394
|
-
for (const [typeName, snapshotType] of Object.entries(snapshot.
|
|
11131
|
+
for (const [typeName, snapshotType] of Object.entries(snapshot.tables)) {
|
|
10395
11132
|
const typeOptions = {
|
|
10396
11133
|
...baseOptions,
|
|
10397
11134
|
subscribed: executorUsedTypes?.has(typeName) ?? false
|
|
@@ -10407,7 +11144,7 @@ function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
|
|
|
10407
11144
|
* @returns {SnapshotTypeComparison} Comparison result
|
|
10408
11145
|
*/
|
|
10409
11146
|
function compareSnapshotWithRemote(snapshot, existingTypeNames) {
|
|
10410
|
-
const snapshotTypeNames = new Set(Object.keys(snapshot.
|
|
11147
|
+
const snapshotTypeNames = new Set(Object.keys(snapshot.tables));
|
|
10411
11148
|
const creates = [];
|
|
10412
11149
|
const updates = [];
|
|
10413
11150
|
const deletes = [];
|
|
@@ -10531,6 +11268,8 @@ function protoGqlOperand(operand) {
|
|
|
10531
11268
|
* - `field_added` with `required: true`: relax to `required: false`.
|
|
10532
11269
|
* - `field_modified` optional→required, unique constraint added, enum
|
|
10533
11270
|
* value removed: keep the looser side until Post-phase.
|
|
11271
|
+
* - `field_renamed`: keep the old field (readable by migrate.ts) and relax
|
|
11272
|
+
* the new field's required/unique constraints until Post-phase.
|
|
10534
11273
|
* - `field_type_modified`: keep the complete previous field config until
|
|
10535
11274
|
* Post-phase so migrate.ts runs against the previous type contract.
|
|
10536
11275
|
*
|
|
@@ -10540,8 +11279,11 @@ function protoGqlOperand(operand) {
|
|
|
10540
11279
|
* - `index_modified` that gains a unique constraint or re-points a unique
|
|
10541
11280
|
* index at different fields: keep the previous definition until Post-phase.
|
|
10542
11281
|
*
|
|
10543
|
-
* Type-level deletions (`
|
|
10544
|
-
*
|
|
11282
|
+
* Type-level deletions (`table_removed`) and renames (`table_renamed`) are
|
|
11283
|
+
* handled by the deploy flow rather than via this module: the old type is
|
|
11284
|
+
* retained until Post-phase, and a renamed type's new type is created with
|
|
11285
|
+
* its full constraints in the Pre-phase (the copy script writes complete
|
|
11286
|
+
* rows, so nothing needs relaxing).
|
|
10545
11287
|
*
|
|
10546
11288
|
* Post-phase then sends the final schema, after migrate.ts has had a chance
|
|
10547
11289
|
* to fix up data.
|
|
@@ -10553,7 +11295,8 @@ const PRE_MIGRATION_FIELD_KINDS = /* @__PURE__ */ new Set([
|
|
|
10553
11295
|
"field_added",
|
|
10554
11296
|
"field_modified",
|
|
10555
11297
|
"field_type_modified",
|
|
10556
|
-
"field_removed"
|
|
11298
|
+
"field_removed",
|
|
11299
|
+
"field_renamed"
|
|
10557
11300
|
]);
|
|
10558
11301
|
/**
|
|
10559
11302
|
* Type guard: is the change a field-level change that needs pre-migration
|
|
@@ -10599,16 +11342,16 @@ function createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsCh
|
|
|
10599
11342
|
/**
|
|
10600
11343
|
* Build a map of field changes that require pre-migration schema adjustment.
|
|
10601
11344
|
* @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
|
|
10602
|
-
* @returns {PreMigrationChangesMap} Map of changes keyed by
|
|
11345
|
+
* @returns {PreMigrationChangesMap} Map of changes keyed by tableName/fieldName
|
|
10603
11346
|
*/
|
|
10604
11347
|
function buildPreMigrationChangesMap(pendingMigrations) {
|
|
10605
11348
|
const map = /* @__PURE__ */ new Map();
|
|
10606
11349
|
for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
|
|
10607
11350
|
if (!isPreMigrationFieldChange(change)) continue;
|
|
10608
11351
|
if (!change.fieldName) continue;
|
|
10609
|
-
const perType = map.get(change.
|
|
11352
|
+
const perType = map.get(change.tableName) ?? /* @__PURE__ */ new Map();
|
|
10610
11353
|
perType.set(change.fieldName, change);
|
|
10611
|
-
map.set(change.
|
|
11354
|
+
map.set(change.tableName, perType);
|
|
10612
11355
|
}
|
|
10613
11356
|
return map;
|
|
10614
11357
|
}
|
|
@@ -10631,6 +11374,15 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
|
|
|
10631
11374
|
fields[fieldName] = convertFieldConfigToProto(change.before);
|
|
10632
11375
|
continue;
|
|
10633
11376
|
}
|
|
11377
|
+
if (change.kind === "field_renamed") {
|
|
11378
|
+
fields[change.previousFieldName] = convertFieldConfigToProto(change.before);
|
|
11379
|
+
const newField = fields[fieldName];
|
|
11380
|
+
if (newField) {
|
|
11381
|
+
if (change.after.required) newField.required = false;
|
|
11382
|
+
if (change.after.unique ?? false) newField.unique = false;
|
|
11383
|
+
}
|
|
11384
|
+
continue;
|
|
11385
|
+
}
|
|
10634
11386
|
const field = fields[fieldName];
|
|
10635
11387
|
if (!field) continue;
|
|
10636
11388
|
if (change.kind === "field_added") {
|
|
@@ -10662,17 +11414,17 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
|
|
|
10662
11414
|
* Build a map of type-level index changes that require pre-migration schema
|
|
10663
11415
|
* adjustment (the breaking ones — see {@link isBreakingIndexChange}).
|
|
10664
11416
|
* @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
|
|
10665
|
-
* @returns {PreMigrationIndexChangesMap} Map of changes keyed by
|
|
11417
|
+
* @returns {PreMigrationIndexChangesMap} Map of changes keyed by tableName/indexName
|
|
10666
11418
|
*/
|
|
10667
11419
|
function buildPreMigrationIndexChangesMap(pendingMigrations) {
|
|
10668
11420
|
const map = /* @__PURE__ */ new Map();
|
|
10669
11421
|
for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
|
|
10670
11422
|
if (change.kind !== "index_added" && change.kind !== "index_modified") continue;
|
|
10671
11423
|
const before = change.kind === "index_modified" ? change.before : void 0;
|
|
10672
|
-
if (!isBreakingIndexChange(change.
|
|
10673
|
-
const perType = map.get(change.
|
|
11424
|
+
if (!isBreakingIndexChange(change.tableName, change.indexName, before, change.after)) continue;
|
|
11425
|
+
const perType = map.get(change.tableName) ?? /* @__PURE__ */ new Map();
|
|
10674
11426
|
perType.set(change.indexName, change);
|
|
10675
|
-
map.set(change.
|
|
11427
|
+
map.set(change.tableName, perType);
|
|
10676
11428
|
}
|
|
10677
11429
|
return map;
|
|
10678
11430
|
}
|
|
@@ -10841,8 +11593,8 @@ function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabl
|
|
|
10841
11593
|
function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
|
|
10842
11594
|
const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
|
|
10843
11595
|
const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
|
|
10844
|
-
const
|
|
10845
|
-
for (const [typeName, type] of Object.entries(snapshot.
|
|
11596
|
+
const tables = {};
|
|
11597
|
+
for (const [typeName, type] of Object.entries(snapshot.tables)) {
|
|
10846
11598
|
const settings = { ...type.settings };
|
|
10847
11599
|
const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
|
|
10848
11600
|
if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
|
|
@@ -10854,11 +11606,11 @@ function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
|
|
|
10854
11606
|
const comparableSettings = definedWhenNotEmpty(settings);
|
|
10855
11607
|
if (comparableSettings) comparableType.settings = comparableSettings;
|
|
10856
11608
|
else delete comparableType.settings;
|
|
10857
|
-
|
|
11609
|
+
tables[typeName] = comparableType;
|
|
10858
11610
|
}
|
|
10859
11611
|
return {
|
|
10860
11612
|
...snapshot,
|
|
10861
|
-
|
|
11613
|
+
tables
|
|
10862
11614
|
};
|
|
10863
11615
|
}
|
|
10864
11616
|
/**
|
|
@@ -11431,8 +12183,12 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
|
|
|
11431
12183
|
}
|
|
11432
12184
|
if (diff.scriptSkipped) {
|
|
11433
12185
|
const migrationLabel = `${namespace}/${formatMigrationNumber(file.number)}`;
|
|
11434
|
-
if (hasScript)
|
|
11435
|
-
|
|
12186
|
+
if (hasScript) throw new Error(`Migration ${migrationLabel} has both a --no-script skip acknowledgment and migrate.ts.\nTo resolve, either:\n - Keep the script and clear the stale acknowledgment: ${formatMigrationScriptCommand({
|
|
12187
|
+
migrationNumber: file.number,
|
|
12188
|
+
namespace,
|
|
12189
|
+
configPath
|
|
12190
|
+
})}\n - Or keep the skip: delete migrate.ts`);
|
|
12191
|
+
logger.info(`Migration ${migrationLabel} runs without a script (skip acknowledged at ${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason})`);
|
|
11436
12192
|
}
|
|
11437
12193
|
pendingMigrations.push({
|
|
11438
12194
|
number: file.number,
|
|
@@ -11848,17 +12604,20 @@ async function applyTailorDB(client, result, phase = "create-update") {
|
|
|
11848
12604
|
*/
|
|
11849
12605
|
function getAffectedTypeNames(migration) {
|
|
11850
12606
|
const typeNames = /* @__PURE__ */ new Set();
|
|
11851
|
-
for (const change of migration.diff.changes) typeNames.add(change.
|
|
12607
|
+
for (const change of migration.diff.changes) typeNames.add(change.tableName);
|
|
11852
12608
|
return typeNames;
|
|
11853
12609
|
}
|
|
11854
12610
|
/**
|
|
11855
|
-
* Get the set of type names to be deleted by a migration
|
|
12611
|
+
* Get the set of type names to be deleted by a migration. A renamed type's
|
|
12612
|
+
* old name is included: the old type survives the Pre-phase and the script
|
|
12613
|
+
* (which copies its rows into the new type), then is dropped here.
|
|
11856
12614
|
* @param {PendingMigration} migration - Pending migration
|
|
11857
12615
|
* @returns {Set<string>} Set of type names to delete
|
|
11858
12616
|
*/
|
|
11859
12617
|
function getDeletedTypeNames(migration) {
|
|
11860
12618
|
const typeNames = /* @__PURE__ */ new Set();
|
|
11861
|
-
for (const change of migration.diff.changes) if (change.kind === "
|
|
12619
|
+
for (const change of migration.diff.changes) if (change.kind === "table_removed") typeNames.add(change.tableName);
|
|
12620
|
+
else if (change.kind === "table_renamed") typeNames.add(change.previousTableName);
|
|
11862
12621
|
return typeNames;
|
|
11863
12622
|
}
|
|
11864
12623
|
/**
|
|
@@ -11911,11 +12670,11 @@ const migrationSnapshotCache = {
|
|
|
11911
12670
|
return snapshot;
|
|
11912
12671
|
}
|
|
11913
12672
|
};
|
|
11914
|
-
function buildSnapshotTypeManifest(migration,
|
|
11915
|
-
const snapshotType = migrationSnapshotCache.load(migration).
|
|
12673
|
+
function buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges) {
|
|
12674
|
+
const snapshotType = migrationSnapshotCache.load(migration).tables[tableName];
|
|
11916
12675
|
if (!snapshotType) return void 0;
|
|
11917
12676
|
const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
|
|
11918
|
-
const typeScriptsChange = migration.diff.changes.find((change) => change.kind === "
|
|
12677
|
+
const typeScriptsChange = migration.diff.changes.find((change) => change.kind === "table_scripts_modified" && change.tableName === tableName);
|
|
11919
12678
|
return generateTailorDBTypeManifestFromSnapshot(typeChanges ? createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsChange) : snapshotType, {
|
|
11920
12679
|
subscribed: executorUsedTypes.has(snapshotType.name),
|
|
11921
12680
|
namespaceGqlOperations: input?.config.gqlOperations
|
|
@@ -11947,30 +12706,30 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
|
|
|
11947
12706
|
const affectedTypes = getAffectedTypeNames(migration);
|
|
11948
12707
|
const createdBeforeMigration = new Set(processedTypes.created);
|
|
11949
12708
|
for (const create of changeSet.type.creates) {
|
|
11950
|
-
const
|
|
11951
|
-
if (!
|
|
11952
|
-
const typeChanges = preMigrationChanges.get(
|
|
11953
|
-
const snapshotType = buildSnapshotTypeManifest(migration,
|
|
12709
|
+
const tableName = create.request.tailordbType?.name;
|
|
12710
|
+
if (!tableName || !affectedTypes.has(tableName) || createdBeforeMigration.has(tableName)) continue;
|
|
12711
|
+
const typeChanges = preMigrationChanges.get(tableName);
|
|
12712
|
+
const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
|
|
11954
12713
|
if (!snapshotType) continue;
|
|
11955
12714
|
const clonedRequest = structuredClone(create.request);
|
|
11956
12715
|
clonedRequest.tailordbType = snapshotType;
|
|
11957
12716
|
if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
|
|
11958
|
-
const indexChanges = preMigrationIndexChanges.get(
|
|
12717
|
+
const indexChanges = preMigrationIndexChanges.get(tableName);
|
|
11959
12718
|
if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
|
|
11960
|
-
processedTypes.created.add(
|
|
12719
|
+
processedTypes.created.add(tableName);
|
|
11961
12720
|
await client.createTailorDBType(clonedRequest);
|
|
11962
12721
|
}
|
|
11963
12722
|
for (const create of changeSet.type.creates) {
|
|
11964
|
-
const
|
|
11965
|
-
if (!
|
|
11966
|
-
const typeChanges = preMigrationChanges.get(
|
|
11967
|
-
const snapshotType = buildSnapshotTypeManifest(migration,
|
|
12723
|
+
const tableName = create.request.tailordbType?.name;
|
|
12724
|
+
if (!tableName || !affectedTypes.has(tableName) || !createdBeforeMigration.has(tableName)) continue;
|
|
12725
|
+
const typeChanges = preMigrationChanges.get(tableName);
|
|
12726
|
+
const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
|
|
11968
12727
|
if (!snapshotType) continue;
|
|
11969
12728
|
const clonedTypeRequest = structuredClone(snapshotType);
|
|
11970
12729
|
if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges);
|
|
11971
|
-
const indexChanges = preMigrationIndexChanges.get(
|
|
12730
|
+
const indexChanges = preMigrationIndexChanges.get(tableName);
|
|
11972
12731
|
if (indexChanges && indexChanges.size > 0 && clonedTypeRequest.schema?.indexes) applyPreMigrationIndexAdjustments(clonedTypeRequest.schema.indexes, indexChanges);
|
|
11973
|
-
processedTypes.updated.add(
|
|
12732
|
+
processedTypes.updated.add(tableName);
|
|
11974
12733
|
await client.updateTailorDBType({
|
|
11975
12734
|
workspaceId: create.request.workspaceId,
|
|
11976
12735
|
namespaceName: create.request.namespaceName,
|
|
@@ -11978,17 +12737,17 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
|
|
|
11978
12737
|
});
|
|
11979
12738
|
}
|
|
11980
12739
|
for (const update of changeSet.type.updates) {
|
|
11981
|
-
const
|
|
11982
|
-
if (!
|
|
11983
|
-
const typeChanges = preMigrationChanges.get(
|
|
11984
|
-
const snapshotType = buildSnapshotTypeManifest(migration,
|
|
12740
|
+
const tableName = update.request.tailordbType?.name;
|
|
12741
|
+
if (!tableName || !affectedTypes.has(tableName)) continue;
|
|
12742
|
+
const typeChanges = preMigrationChanges.get(tableName);
|
|
12743
|
+
const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
|
|
11985
12744
|
if (!snapshotType) continue;
|
|
11986
12745
|
const clonedRequest = structuredClone(update.request);
|
|
11987
12746
|
clonedRequest.tailordbType = snapshotType;
|
|
11988
12747
|
if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
|
|
11989
|
-
const indexChanges = preMigrationIndexChanges.get(
|
|
12748
|
+
const indexChanges = preMigrationIndexChanges.get(tableName);
|
|
11990
12749
|
if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
|
|
11991
|
-
processedTypes.updated.add(
|
|
12750
|
+
processedTypes.updated.add(tableName);
|
|
11992
12751
|
await client.updateTailorDBType(clonedRequest);
|
|
11993
12752
|
}
|
|
11994
12753
|
if (!processedTypes.gqlPermissionsProcessed.has(migration.namespace)) {
|
|
@@ -11996,12 +12755,12 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
|
|
|
11996
12755
|
const gqlPermissionUpdatesForNamespace = changeSet.gqlPermission.updates.filter((update) => update.request.namespaceName === migration.namespace);
|
|
11997
12756
|
const gqlPermissionTypeNames = new Set(gqlPermissionCreatesForNamespace.map((create) => create.name));
|
|
11998
12757
|
const missingTypeCreates = changeSet.type.creates.filter((create) => {
|
|
11999
|
-
const
|
|
12000
|
-
return create.request.namespaceName === migration.namespace &&
|
|
12758
|
+
const tableName = create.request.tailordbType?.name;
|
|
12759
|
+
return create.request.namespaceName === migration.namespace && tableName && gqlPermissionTypeNames.has(tableName) && !processedTypes.created.has(tableName);
|
|
12001
12760
|
});
|
|
12002
12761
|
if (missingTypeCreates.length > 0) for (const create of missingTypeCreates) {
|
|
12003
|
-
const
|
|
12004
|
-
if (
|
|
12762
|
+
const tableName = create.request.tailordbType?.name;
|
|
12763
|
+
if (tableName) processedTypes.created.add(tableName);
|
|
12005
12764
|
await client.createTailorDBType(create.request);
|
|
12006
12765
|
}
|
|
12007
12766
|
processedTypes.gqlPermissionsProcessed.add(migration.namespace);
|
|
@@ -12042,9 +12801,9 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
|
|
|
12042
12801
|
const affectedTypes = getAffectedTypeNames(migration);
|
|
12043
12802
|
try {
|
|
12044
12803
|
for (const create of changeSet.type.creates) {
|
|
12045
|
-
const
|
|
12046
|
-
if (!
|
|
12047
|
-
const snapshotType = buildSnapshotTypeManifest(migration,
|
|
12804
|
+
const tableName = create.request.tailordbType?.name;
|
|
12805
|
+
if (!tableName || !affectedTypes.has(tableName) || !adjustedTypes.has(tableName)) continue;
|
|
12806
|
+
const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes);
|
|
12048
12807
|
if (!snapshotType) continue;
|
|
12049
12808
|
await client.updateTailorDBType({
|
|
12050
12809
|
workspaceId: create.request.workspaceId,
|
|
@@ -12053,9 +12812,9 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
|
|
|
12053
12812
|
});
|
|
12054
12813
|
}
|
|
12055
12814
|
for (const update of changeSet.type.updates) {
|
|
12056
|
-
const
|
|
12057
|
-
if (!
|
|
12058
|
-
const snapshotType = buildSnapshotTypeManifest(migration,
|
|
12815
|
+
const tableName = update.request.tailordbType?.name;
|
|
12816
|
+
if (!tableName || !affectedTypes.has(tableName) || !adjustedTypes.has(tableName)) continue;
|
|
12817
|
+
const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes);
|
|
12059
12818
|
if (!snapshotType) continue;
|
|
12060
12819
|
await client.updateTailorDBType({
|
|
12061
12820
|
workspaceId: update.request.workspaceId,
|
|
@@ -12073,17 +12832,17 @@ async function executeSingleMigrationPostPhaseDeletions(client, changeSet, migra
|
|
|
12073
12832
|
const gqlPermissionsToDelete = changeSet.gqlPermission.deletes.filter((del) => {
|
|
12074
12833
|
const permKey = `${del.request.namespaceName}/${del.name}`;
|
|
12075
12834
|
if (deletedResources.gqlPermissions.has(permKey)) return false;
|
|
12076
|
-
const
|
|
12077
|
-
return deletedTypeNames.has(
|
|
12835
|
+
const tableName = del.name;
|
|
12836
|
+
return deletedTypeNames.has(tableName);
|
|
12078
12837
|
});
|
|
12079
12838
|
for (const del of gqlPermissionsToDelete) {
|
|
12080
12839
|
await client.deleteTailorDBGQLPermission(del.request);
|
|
12081
12840
|
deletedResources.gqlPermissions.add(`${del.request.namespaceName}/${del.name}`);
|
|
12082
12841
|
}
|
|
12083
12842
|
const typesToDelete = changeSet.type.deletes.filter((del) => {
|
|
12084
|
-
const
|
|
12085
|
-
if (!
|
|
12086
|
-
return deletedTypeNames.has(
|
|
12843
|
+
const tableName = del.name;
|
|
12844
|
+
if (!tableName || deletedResources.types.has(tableName)) return false;
|
|
12845
|
+
return deletedTypeNames.has(tableName);
|
|
12087
12846
|
});
|
|
12088
12847
|
for (const del of typesToDelete) {
|
|
12089
12848
|
await client.deleteTailorDBType(del.request);
|
|
@@ -12122,34 +12881,40 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
|
|
|
12122
12881
|
}
|
|
12123
12882
|
const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
|
|
12124
12883
|
logger.warn(`Migration ${migration.namespace}/${formatMigrationNumber(migration.number)} failed; rolling back its pre-migration schema changes.`);
|
|
12125
|
-
|
|
12126
|
-
const priorType = priorSnapshot.
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12884
|
+
const restoredTypes = [...rollbackTypes].flatMap((tableName) => {
|
|
12885
|
+
const priorType = priorSnapshot.tables[tableName];
|
|
12886
|
+
return priorType ? [{
|
|
12887
|
+
tableName,
|
|
12888
|
+
priorType
|
|
12889
|
+
}] : [];
|
|
12890
|
+
});
|
|
12891
|
+
const newTypes = [...rollbackTypes].filter((tableName) => !priorSnapshot.tables[tableName]);
|
|
12892
|
+
for (const { tableName, priorType } of restoredTypes) try {
|
|
12893
|
+
const manifest = generateTailorDBTypeManifestFromSnapshot(priorType, {
|
|
12894
|
+
subscribed: executorUsedTypes.has(priorType.name),
|
|
12895
|
+
namespaceGqlOperations: input?.config.gqlOperations
|
|
12896
|
+
});
|
|
12897
|
+
await client.updateTailorDBType({
|
|
12898
|
+
workspaceId,
|
|
12899
|
+
namespaceName: migration.namespace,
|
|
12900
|
+
tailordbType: manifest
|
|
12901
|
+
});
|
|
12902
|
+
} catch (rollbackError) {
|
|
12903
|
+
logger.warn(`Failed to roll back type '${tableName}' in namespace '${migration.namespace}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
12904
|
+
}
|
|
12905
|
+
for (const tableName of newTypes) try {
|
|
12906
|
+
await client.deleteTailorDBGQLPermission({
|
|
12907
|
+
workspaceId,
|
|
12908
|
+
namespaceName: migration.namespace,
|
|
12909
|
+
typeName: tableName
|
|
12910
|
+
}).catch(() => void 0);
|
|
12911
|
+
await client.deleteTailorDBType({
|
|
12912
|
+
workspaceId,
|
|
12913
|
+
namespaceName: migration.namespace,
|
|
12914
|
+
tailordbTypeName: tableName
|
|
12915
|
+
});
|
|
12916
|
+
} catch (rollbackError) {
|
|
12917
|
+
logger.warn(`Failed to roll back type '${tableName}' in namespace '${migration.namespace}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
12153
12918
|
}
|
|
12154
12919
|
}
|
|
12155
12920
|
/**
|
|
@@ -12167,7 +12932,7 @@ async function planTailorDB(context) {
|
|
|
12167
12932
|
const snapshot = migrationTestSnapshots?.get(tailordb.namespace);
|
|
12168
12933
|
tailordbs.push(snapshot ? {
|
|
12169
12934
|
...input,
|
|
12170
|
-
types: snapshot.
|
|
12935
|
+
types: snapshot.tables
|
|
12171
12936
|
} : input);
|
|
12172
12937
|
}
|
|
12173
12938
|
const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
|
|
@@ -12179,10 +12944,10 @@ async function planTailorDB(context) {
|
|
|
12179
12944
|
if (migrationTestSnapshots) {
|
|
12180
12945
|
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
12946
|
const namespaceByType = /* @__PURE__ */ new Map();
|
|
12182
|
-
for (const tailordb of tailordbs) for (const
|
|
12183
|
-
const existingNamespace = namespaceByType.get(
|
|
12184
|
-
if (existingNamespace) throw new Error(`Migration test snapshot has duplicate TailorDB type name "${
|
|
12185
|
-
namespaceByType.set(
|
|
12947
|
+
for (const tailordb of tailordbs) for (const tableName of Object.keys(tailordb.types)) {
|
|
12948
|
+
const existingNamespace = namespaceByType.get(tableName);
|
|
12949
|
+
if (existingNamespace) throw new Error(`Migration test snapshot has duplicate TailorDB type name "${tableName}" in namespaces "${existingNamespace}" and "${tailordb.namespace}".`);
|
|
12950
|
+
namespaceByType.set(tableName, tailordb.namespace);
|
|
12186
12951
|
}
|
|
12187
12952
|
}
|
|
12188
12953
|
const migrationConfig = getNamespacesWithMigrations(config, path.dirname(config.path));
|
|
@@ -12361,17 +13126,17 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
|
|
|
12361
13126
|
* Build one type's metadata write, carrying the dependency records that belong
|
|
12362
13127
|
* to it. The type is what publishes record events, so the record lives there.
|
|
12363
13128
|
* @param namespace - Namespace holding the type
|
|
12364
|
-
* @param
|
|
13129
|
+
* @param tableName - Type name
|
|
12365
13130
|
* @param explicitPublishEvents - `publishEvents` declared on the type, if any
|
|
12366
13131
|
* @returns The type's metadata write
|
|
12367
13132
|
*/
|
|
12368
|
-
const typeMetaRequest = async (namespace,
|
|
13133
|
+
const typeMetaRequest = async (namespace, tableName, explicitPublishEvents) => {
|
|
12369
13134
|
return addDependencyRecords(await buildMetaRequest({
|
|
12370
|
-
trn: tailorDBTypeTrn(workspaceId, namespace,
|
|
13135
|
+
trn: tailorDBTypeTrn(workspaceId, namespace, tableName),
|
|
12371
13136
|
appName: appName ?? "",
|
|
12372
13137
|
appId
|
|
12373
13138
|
}), {
|
|
12374
|
-
key: eventSourceKey.tailorDBType(namespace,
|
|
13139
|
+
key: eventSourceKey.tailorDBType(namespace, tableName),
|
|
12375
13140
|
dependentApps,
|
|
12376
13141
|
runAppIds,
|
|
12377
13142
|
pinned: explicitPublishEvents !== void 0
|
|
@@ -12390,46 +13155,46 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
|
|
|
12390
13155
|
};
|
|
12391
13156
|
for (const tailordb of tailordbs) {
|
|
12392
13157
|
const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
|
|
12393
|
-
for (const [
|
|
13158
|
+
for (const [tableName, type] of Object.entries(types)) assertNoPublishEventsConflict({
|
|
12394
13159
|
explicit: type.settings?.publishEvents,
|
|
12395
|
-
subscribed: executorUsedTypes.has(
|
|
12396
|
-
conflict: publishEventsConflict.tailorDBType(
|
|
13160
|
+
subscribed: executorUsedTypes.has(tableName),
|
|
13161
|
+
conflict: publishEventsConflict.tailorDBType(tableName)
|
|
12397
13162
|
});
|
|
12398
13163
|
}
|
|
12399
13164
|
for (const tailordb of tailordbs) {
|
|
12400
13165
|
const existingTypes = await fetchTypes(tailordb.namespace);
|
|
12401
13166
|
const existingTypesMap = new Map(existingTypes.map((type) => [type.name, type]));
|
|
12402
13167
|
const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
|
|
12403
|
-
const typeMeta = (
|
|
12404
|
-
for (const [
|
|
13168
|
+
const typeMeta = (tableName) => typeMetaRequest(tailordb.namespace, tableName, types[tableName]?.settings?.publishEvents);
|
|
13169
|
+
for (const [tableName, tailordbTypeSnapshot] of Object.entries(types)) {
|
|
12405
13170
|
const tailordbType = generateTailorDBTypeManifestFromSnapshot(tailordbTypeSnapshot, {
|
|
12406
|
-
subscribed: executorUsedTypes.has(
|
|
13171
|
+
subscribed: executorUsedTypes.has(tableName),
|
|
12407
13172
|
namespaceGqlOperations: tailordb.config.gqlOperations
|
|
12408
13173
|
});
|
|
12409
|
-
const existingType = existingTypesMap.get(
|
|
13174
|
+
const existingType = existingTypesMap.get(tableName);
|
|
12410
13175
|
if (existingType) {
|
|
12411
13176
|
if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({
|
|
12412
|
-
name:
|
|
12413
|
-
metaRequest: await typeMeta(
|
|
13177
|
+
name: tableName,
|
|
13178
|
+
metaRequest: await typeMeta(tableName)
|
|
12414
13179
|
});
|
|
12415
13180
|
else changeSet.updates.push({
|
|
12416
|
-
name:
|
|
13181
|
+
name: tableName,
|
|
12417
13182
|
request: {
|
|
12418
13183
|
workspaceId,
|
|
12419
13184
|
namespaceName: tailordb.namespace,
|
|
12420
13185
|
tailordbType
|
|
12421
13186
|
},
|
|
12422
|
-
metaRequest: await typeMeta(
|
|
13187
|
+
metaRequest: await typeMeta(tableName)
|
|
12423
13188
|
});
|
|
12424
|
-
existingTypesMap.delete(
|
|
13189
|
+
existingTypesMap.delete(tableName);
|
|
12425
13190
|
} else changeSet.creates.push({
|
|
12426
|
-
name:
|
|
13191
|
+
name: tableName,
|
|
12427
13192
|
request: {
|
|
12428
13193
|
workspaceId,
|
|
12429
13194
|
namespaceName: tailordb.namespace,
|
|
12430
13195
|
tailordbType
|
|
12431
13196
|
},
|
|
12432
|
-
metaRequest: await typeMeta(
|
|
13197
|
+
metaRequest: await typeMeta(tableName)
|
|
12433
13198
|
});
|
|
12434
13199
|
}
|
|
12435
13200
|
existingTypesMap.forEach((_type, name) => {
|
|
@@ -12561,29 +13326,29 @@ async function planGqlPermissions(client, workspaceId, tailordbs, deletedService
|
|
|
12561
13326
|
existingNameSet.add(gqlPermission.typeName);
|
|
12562
13327
|
});
|
|
12563
13328
|
const types = tailordb.types;
|
|
12564
|
-
for (const [
|
|
13329
|
+
for (const [tableName, typeEntry] of Object.entries(types)) {
|
|
12565
13330
|
const gqlPermission = typeEntry.permissions?.gql;
|
|
12566
13331
|
if (!gqlPermission) continue;
|
|
12567
13332
|
const desiredPermission = protoGqlPermission(gqlPermission);
|
|
12568
|
-
const existingPermission = existingGqlPermissions.find((entry) => entry.typeName ===
|
|
12569
|
-
if (existingNameSet.has(
|
|
12570
|
-
if (!forceApplyAll && existingPermission && areNormalizedEqual(normalizeComparableGqlPermission(existingPermission.permission), normalizeComparableGqlPermission(desiredPermission))) changeSet.unchanged.push({ name:
|
|
13333
|
+
const existingPermission = existingGqlPermissions.find((entry) => entry.typeName === tableName);
|
|
13334
|
+
if (existingNameSet.has(tableName)) {
|
|
13335
|
+
if (!forceApplyAll && existingPermission && areNormalizedEqual(normalizeComparableGqlPermission(existingPermission.permission), normalizeComparableGqlPermission(desiredPermission))) changeSet.unchanged.push({ name: tableName });
|
|
12571
13336
|
else changeSet.updates.push({
|
|
12572
|
-
name:
|
|
13337
|
+
name: tableName,
|
|
12573
13338
|
request: {
|
|
12574
13339
|
workspaceId,
|
|
12575
13340
|
namespaceName: tailordb.namespace,
|
|
12576
|
-
typeName,
|
|
13341
|
+
typeName: tableName,
|
|
12577
13342
|
permission: desiredPermission
|
|
12578
13343
|
}
|
|
12579
13344
|
});
|
|
12580
|
-
existingNameSet.delete(
|
|
13345
|
+
existingNameSet.delete(tableName);
|
|
12581
13346
|
} else changeSet.creates.push({
|
|
12582
|
-
name:
|
|
13347
|
+
name: tableName,
|
|
12583
13348
|
request: {
|
|
12584
13349
|
workspaceId,
|
|
12585
13350
|
namespaceName: tailordb.namespace,
|
|
12586
|
-
typeName,
|
|
13351
|
+
typeName: tableName,
|
|
12587
13352
|
permission: desiredPermission
|
|
12588
13353
|
}
|
|
12589
13354
|
});
|
|
@@ -15827,7 +16592,7 @@ async function planDeploymentTarget(params) {
|
|
|
15827
16592
|
const snapshot = migrationTestSnapshots?.get(service.namespace);
|
|
15828
16593
|
return snapshot ? {
|
|
15829
16594
|
...service,
|
|
15830
|
-
types: snapshot.
|
|
16595
|
+
types: snapshot.tables,
|
|
15831
16596
|
typeSourceInfo: {}
|
|
15832
16597
|
} : service;
|
|
15833
16598
|
});
|
|
@@ -16364,6 +17129,8 @@ function adjustApplicationForMigrationTest(application, internalContext) {
|
|
|
16364
17129
|
async function deployInternal(options, cliContext, internalContext) {
|
|
16365
17130
|
return withSpan("deploy", async (rootSpan) => {
|
|
16366
17131
|
rootSpan.setAttribute("deploy.dry_run", options?.dryRun ?? false);
|
|
17132
|
+
beginUserModuleRun();
|
|
17133
|
+
beginWaitPointScope();
|
|
16367
17134
|
const configPaths = parseDeployConfigPaths(options?.configPath);
|
|
16368
17135
|
const dryRun = options?.dryRun ?? false;
|
|
16369
17136
|
const buildOnly = options?.buildOnly ?? parseBoolean(process.env.TAILOR_DEPLOY_BUILD_ONLY) === true;
|
|
@@ -20417,6 +21184,249 @@ async function openInConfiguredEditor(filePath) {
|
|
|
20417
21184
|
return await openInEditor(filePath, editor);
|
|
20418
21185
|
}
|
|
20419
21186
|
|
|
21187
|
+
//#endregion
|
|
21188
|
+
//#region src/cli/commands/tailordb/migrate/expand-contract.ts
|
|
21189
|
+
/**
|
|
21190
|
+
* Planning for field type changes that move values through a temporary field.
|
|
21191
|
+
*
|
|
21192
|
+
* A change that cannot be applied in place is split into two migrations: the
|
|
21193
|
+
* first adds a temporary field and converts values into it, the second renames
|
|
21194
|
+
* that field back over the original.
|
|
21195
|
+
*/
|
|
21196
|
+
/** Longest field name the platform accepts. */
|
|
21197
|
+
const MAX_FIELD_NAME_LENGTH = 63;
|
|
21198
|
+
const TEMP_FIELD_SUFFIX = "Migrate";
|
|
21199
|
+
/**
|
|
21200
|
+
* Key identifying a field across snapshots and user-supplied options.
|
|
21201
|
+
* @param tableName - Name of the type holding the field
|
|
21202
|
+
* @param fieldName - Name of the field
|
|
21203
|
+
* @returns Key in `Type.field` form
|
|
21204
|
+
*/
|
|
21205
|
+
function fieldKey(tableName, fieldName) {
|
|
21206
|
+
return `${tableName}.${fieldName}`;
|
|
21207
|
+
}
|
|
21208
|
+
/**
|
|
21209
|
+
* Derive the temporary field name to carry a field's converted values.
|
|
21210
|
+
*
|
|
21211
|
+
* Field names cannot hold an underscore, so the suffix is camelCase and a
|
|
21212
|
+
* collision is resolved by an ordinal rather than a separator.
|
|
21213
|
+
* @param fieldName - Field being converted
|
|
21214
|
+
* @param taken - Field names already in use for the same type
|
|
21215
|
+
* @returns Unused temporary field name
|
|
21216
|
+
* @throws {Error} When no candidate fits within the platform's length limit
|
|
21217
|
+
*/
|
|
21218
|
+
function buildTempFieldName(fieldName, taken) {
|
|
21219
|
+
const base = `${fieldName}${TEMP_FIELD_SUFFIX}`;
|
|
21220
|
+
for (let ordinal = 1; ordinal <= taken.size + 1; ordinal++) {
|
|
21221
|
+
const candidate = ordinal === 1 ? base : `${base}${ordinal}`;
|
|
21222
|
+
if (candidate.length > MAX_FIELD_NAME_LENGTH) break;
|
|
21223
|
+
if (!taken.has(candidate)) return candidate;
|
|
21224
|
+
}
|
|
21225
|
+
throw new Error(`Cannot derive a temporary field name for "${fieldName}": every candidate is taken or exceeds ${MAX_FIELD_NAME_LENGTH} characters.`);
|
|
21226
|
+
}
|
|
21227
|
+
/**
|
|
21228
|
+
* Whether a field type change can be carried by a generated migration pair.
|
|
21229
|
+
*
|
|
21230
|
+
* The single answer behind the prompt, the flag hint, and planning, so a run
|
|
21231
|
+
* cannot recommend a conversion it would then reject.
|
|
21232
|
+
* @param options - Snapshots and the field to test
|
|
21233
|
+
* @returns Whether the conversion can be generated
|
|
21234
|
+
*/
|
|
21235
|
+
function canConvertField(options) {
|
|
21236
|
+
return getExpandContractEligibility(options).eligible;
|
|
21237
|
+
}
|
|
21238
|
+
/**
|
|
21239
|
+
* Explain whether a field can be carried by a generated migration pair.
|
|
21240
|
+
* @param options - Snapshots and the field to test
|
|
21241
|
+
* @returns Eligibility and, when ineligible, the reason
|
|
21242
|
+
*/
|
|
21243
|
+
function getExpandContractEligibility(options) {
|
|
21244
|
+
const { previous, current, tableName, fieldName } = options;
|
|
21245
|
+
const before = previous.tables[tableName]?.fields[fieldName];
|
|
21246
|
+
const after = current.tables[tableName]?.fields[fieldName];
|
|
21247
|
+
if (!before || !after) return {
|
|
21248
|
+
eligible: false,
|
|
21249
|
+
reason: "the field does not exist in both schemas"
|
|
21250
|
+
};
|
|
21251
|
+
const fieldEligibility = getExpandContractFieldChangeEligibility(before, after);
|
|
21252
|
+
if (!fieldEligibility.eligible) return fieldEligibility;
|
|
21253
|
+
if (isFieldReferenced(previous.tables[tableName], fieldName) || isFieldReferenced(current.tables[tableName], fieldName)) return {
|
|
21254
|
+
eligible: false,
|
|
21255
|
+
reason: "another schema feature references the field"
|
|
21256
|
+
};
|
|
21257
|
+
return { eligible: true };
|
|
21258
|
+
}
|
|
21259
|
+
/**
|
|
21260
|
+
* Names a type already exposes, which a temporary field cannot reuse.
|
|
21261
|
+
* @param type - Type to enumerate
|
|
21262
|
+
* @returns Field, file, and relationship names
|
|
21263
|
+
*/
|
|
21264
|
+
function typeMemberNames(type) {
|
|
21265
|
+
if (!type) return [];
|
|
21266
|
+
return [
|
|
21267
|
+
...Object.keys(type.fields),
|
|
21268
|
+
...Object.keys(type.files ?? {}),
|
|
21269
|
+
...Object.keys(type.forwardRelationships ?? {}),
|
|
21270
|
+
...Object.keys(type.backwardRelationships ?? {})
|
|
21271
|
+
];
|
|
21272
|
+
}
|
|
21273
|
+
function staticPropertyName(key, computed) {
|
|
21274
|
+
if (!computed && key.type === "Identifier") return key.name;
|
|
21275
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
21276
|
+
if (key.type === "TemplateLiteral" && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? void 0;
|
|
21277
|
+
}
|
|
21278
|
+
const SCRIPT_CONTEXT_ARGUMENTS = /* @__PURE__ */ new Set([
|
|
21279
|
+
"input",
|
|
21280
|
+
"newRecord",
|
|
21281
|
+
"oldRecord",
|
|
21282
|
+
"invoker",
|
|
21283
|
+
"now"
|
|
21284
|
+
]);
|
|
21285
|
+
function walkScriptAst(node, visit, ancestors = []) {
|
|
21286
|
+
if (!node) return;
|
|
21287
|
+
visit(node, ancestors);
|
|
21288
|
+
const nestedAncestors = [...ancestors, node];
|
|
21289
|
+
const record = node;
|
|
21290
|
+
for (const [key, value] of Object.entries(record)) {
|
|
21291
|
+
if (key === "type" || key === "parent") continue;
|
|
21292
|
+
if (Array.isArray(value)) {
|
|
21293
|
+
for (const item of value) if (item && typeof item === "object" && "type" in item) walkScriptAst(item, visit, nestedAncestors);
|
|
21294
|
+
} else if (value && typeof value === "object" && "type" in value) walkScriptAst(value, visit, nestedAncestors);
|
|
21295
|
+
}
|
|
21296
|
+
}
|
|
21297
|
+
function unwrapFunction(node) {
|
|
21298
|
+
return node.type === "ParenthesizedExpression" ? unwrapFunction(node.expression) : node;
|
|
21299
|
+
}
|
|
21300
|
+
function collectIssueBindings(program) {
|
|
21301
|
+
const issueBindings = /* @__PURE__ */ new Set(["__issues"]);
|
|
21302
|
+
walkScriptAst(program, (node) => {
|
|
21303
|
+
if (node.type !== "CallExpression") return;
|
|
21304
|
+
const callee = unwrapFunction(node.callee);
|
|
21305
|
+
if (callee.type !== "ArrowFunctionExpression" && callee.type !== "FunctionExpression" || node.arguments[1]?.type !== "Identifier" || node.arguments[1].name !== "__issues") return;
|
|
21306
|
+
const issueParameter = callee.params[1];
|
|
21307
|
+
if (issueParameter?.type === "Identifier") issueBindings.add(issueParameter.name);
|
|
21308
|
+
});
|
|
21309
|
+
let addedBinding;
|
|
21310
|
+
do {
|
|
21311
|
+
const previousSize = issueBindings.size;
|
|
21312
|
+
walkScriptAst(program, (node) => {
|
|
21313
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init?.type === "Identifier" && issueBindings.has(node.init.name)) issueBindings.add(node.id.name);
|
|
21314
|
+
});
|
|
21315
|
+
addedBinding = issueBindings.size > previousSize;
|
|
21316
|
+
} while (addedBinding);
|
|
21317
|
+
return issueBindings;
|
|
21318
|
+
}
|
|
21319
|
+
function isWrapperArgumentProperty(node, ancestors) {
|
|
21320
|
+
if (node.type !== "Property") return false;
|
|
21321
|
+
const propertyName = staticPropertyName(node.key, node.computed);
|
|
21322
|
+
if (!propertyName || !SCRIPT_CONTEXT_ARGUMENTS.has(propertyName)) return false;
|
|
21323
|
+
const object = ancestors.at(-1);
|
|
21324
|
+
const call = ancestors.at(-2);
|
|
21325
|
+
if (object?.type !== "ObjectExpression" || call?.type !== "CallExpression") return false;
|
|
21326
|
+
return call.arguments[0] === object && ["ArrowFunctionExpression", "FunctionExpression"].includes(unwrapFunction(call.callee).type);
|
|
21327
|
+
}
|
|
21328
|
+
function isWrapperParameterProperty(node, ancestors) {
|
|
21329
|
+
if (node.type !== "Property") return false;
|
|
21330
|
+
const propertyName = staticPropertyName(node.key, node.computed);
|
|
21331
|
+
if (!propertyName || !SCRIPT_CONTEXT_ARGUMENTS.has(propertyName)) return false;
|
|
21332
|
+
const pattern = ancestors.at(-1);
|
|
21333
|
+
if (pattern?.type !== "ObjectPattern") return false;
|
|
21334
|
+
for (let index = ancestors.length - 2; index >= 0; index--) {
|
|
21335
|
+
const candidate = ancestors[index];
|
|
21336
|
+
if (candidate && (candidate.type === "ArrowFunctionExpression" || candidate.type === "FunctionExpression") && candidate.params[0] === pattern) return ancestors.slice(0, index).some((ancestor) => ancestor.type === "CallExpression" && unwrapFunction(ancestor.callee) === candidate && ancestor.arguments[0]?.type === "ObjectExpression" && ancestor.arguments[0].properties.some((property) => property.type === "Property" && staticPropertyName(property.key, property.computed) === propertyName));
|
|
21337
|
+
}
|
|
21338
|
+
return false;
|
|
21339
|
+
}
|
|
21340
|
+
function isIssueMessage(node, ancestors, issueBindings) {
|
|
21341
|
+
const call = ancestors.at(-1);
|
|
21342
|
+
return call?.type === "CallExpression" && call.callee.type === "Identifier" && issueBindings.has(call.callee.name) && call.arguments.findIndex((argument) => argument === node) > 0;
|
|
21343
|
+
}
|
|
21344
|
+
function scriptReferencesField(script, fieldName) {
|
|
21345
|
+
try {
|
|
21346
|
+
const { program, errors } = parseSync("expand-contract-reference.js", script, { sourceType: "module" });
|
|
21347
|
+
if (errors.length > 0) return true;
|
|
21348
|
+
const issueBindings = collectIssueBindings(program);
|
|
21349
|
+
let referenced = false;
|
|
21350
|
+
walkScriptAst(program, (node, ancestors) => {
|
|
21351
|
+
if (referenced) return;
|
|
21352
|
+
if (node.type === "MemberExpression" && (staticPropertyName(node.property, node.computed) === fieldName || node.computed && staticPropertyName(node.property, true) === void 0)) referenced = true;
|
|
21353
|
+
else if (node.type === "Property" && (staticPropertyName(node.key, node.computed) === fieldName || node.computed && staticPropertyName(node.key, true) === void 0) && (node.computed || !isWrapperArgumentProperty(node, ancestors) && !isWrapperParameterProperty(node, ancestors))) referenced = true;
|
|
21354
|
+
else if (node.type === "CallExpression" && node.callee.type === "Identifier" && issueBindings.has(node.callee.name) && node.arguments[0] !== void 0 && node.arguments[0].type !== "SpreadElement" && staticPropertyName(node.arguments[0], true) === fieldName) referenced = true;
|
|
21355
|
+
else if ((node.type === "Literal" || node.type === "TemplateLiteral") && staticPropertyName(node, true) === fieldName && !isIssueMessage(node, ancestors, issueBindings)) referenced = true;
|
|
21356
|
+
});
|
|
21357
|
+
return referenced;
|
|
21358
|
+
} catch {
|
|
21359
|
+
return true;
|
|
21360
|
+
}
|
|
21361
|
+
}
|
|
21362
|
+
function permissionsReferenceField(permissions, fieldName) {
|
|
21363
|
+
if (!permissions) return false;
|
|
21364
|
+
return [...permissions.record ? Object.values(permissions.record).flatMap((policies) => policies) : [], ...permissions.gql ?? []].some((policy) => policy.conditions.some((condition) => {
|
|
21365
|
+
const [left, , right] = condition;
|
|
21366
|
+
return [left, right].some((operand) => isSnapshotFieldRefOperand(operand) && ("record" in operand && operand.record === fieldName || "newRecord" in operand && operand.newRecord === fieldName || "oldRecord" in operand && operand.oldRecord === fieldName));
|
|
21367
|
+
}));
|
|
21368
|
+
}
|
|
21369
|
+
/**
|
|
21370
|
+
* Whether anything other than the field list names this field.
|
|
21371
|
+
*
|
|
21372
|
+
* The pair moves values through a differently named field, and only the field
|
|
21373
|
+
* list is rewritten. An index, relationship, permission, or script naming the
|
|
21374
|
+
* field would keep pointing at the name the pair drops.
|
|
21375
|
+
* @param type - Type holding the field
|
|
21376
|
+
* @param fieldName - Field being converted
|
|
21377
|
+
* @returns Whether another part of the type names the field
|
|
21378
|
+
*/
|
|
21379
|
+
function isFieldReferenced(type, fieldName) {
|
|
21380
|
+
if (!type) return false;
|
|
21381
|
+
if (Object.values(type.indexes ?? {}).some((index) => index.fields.includes(fieldName))) return true;
|
|
21382
|
+
if ([...Object.values(type.forwardRelationships ?? {}), ...Object.values(type.backwardRelationships ?? {})].some((relationship) => relationship.sourceField === fieldName || relationship.targetField === fieldName)) return true;
|
|
21383
|
+
if (permissionsReferenceField(type.permissions, fieldName)) return true;
|
|
21384
|
+
return [
|
|
21385
|
+
type.typeHookExpr?.create,
|
|
21386
|
+
type.typeHookExpr?.update,
|
|
21387
|
+
type.typeValidateExpr
|
|
21388
|
+
].filter((script) => script !== void 0).some((script) => scriptReferencesField(script, fieldName));
|
|
21389
|
+
}
|
|
21390
|
+
/**
|
|
21391
|
+
* Split unsupported field type changes into the ones a migration pair can carry
|
|
21392
|
+
* and the ones that must still fail.
|
|
21393
|
+
* @param options - Snapshots, diff, and the changes the user approved
|
|
21394
|
+
* @returns Plans to generate and breaking changes to reject
|
|
21395
|
+
*/
|
|
21396
|
+
function planExpandContract(options) {
|
|
21397
|
+
const { previous, current, diff, confirmed } = options;
|
|
21398
|
+
const plans = [];
|
|
21399
|
+
const planned = /* @__PURE__ */ new Set();
|
|
21400
|
+
for (const change of diff.changes) {
|
|
21401
|
+
if (change.kind !== "field_type_modified") continue;
|
|
21402
|
+
const key = fieldKey(change.tableName, change.fieldName);
|
|
21403
|
+
if (!confirmed.has(key)) continue;
|
|
21404
|
+
if (!canConvertField({
|
|
21405
|
+
previous,
|
|
21406
|
+
current,
|
|
21407
|
+
tableName: change.tableName,
|
|
21408
|
+
fieldName: change.fieldName
|
|
21409
|
+
})) continue;
|
|
21410
|
+
const taken = /* @__PURE__ */ new Set([
|
|
21411
|
+
...typeMemberNames(previous.tables[change.tableName]),
|
|
21412
|
+
...typeMemberNames(current.tables[change.tableName]),
|
|
21413
|
+
...plans.filter((plan) => plan.tableName === change.tableName).map((plan) => plan.tempFieldName)
|
|
21414
|
+
]);
|
|
21415
|
+
plans.push({
|
|
21416
|
+
tableName: change.tableName,
|
|
21417
|
+
fieldName: change.fieldName,
|
|
21418
|
+
tempFieldName: buildTempFieldName(change.fieldName, taken),
|
|
21419
|
+
before: change.before,
|
|
21420
|
+
after: change.after
|
|
21421
|
+
});
|
|
21422
|
+
planned.add(key);
|
|
21423
|
+
}
|
|
21424
|
+
return {
|
|
21425
|
+
plans,
|
|
21426
|
+
blocked: diff.breakingChanges.filter((change) => change.unsupported && !(change.fieldName && planned.has(fieldKey(change.tableName, change.fieldName))))
|
|
21427
|
+
};
|
|
21428
|
+
}
|
|
21429
|
+
|
|
20420
21430
|
//#endregion
|
|
20421
21431
|
//#region src/cli/commands/tailordb/migrate/db-types-generator.ts
|
|
20422
21432
|
/**
|
|
@@ -20434,11 +21444,13 @@ function extractBreakingChangeFields(diff) {
|
|
|
20434
21444
|
const optionalToRequired = /* @__PURE__ */ new Map();
|
|
20435
21445
|
const addedRequiredFields = /* @__PURE__ */ new Map();
|
|
20436
21446
|
const enumValueChanges = /* @__PURE__ */ new Map();
|
|
21447
|
+
const renamedFields = /* @__PURE__ */ new Map();
|
|
21448
|
+
const renamedTypes = /* @__PURE__ */ new Map();
|
|
20437
21449
|
for (const change of diff.changes) if (change.kind === "field_modified" || change.kind === "field_type_modified") {
|
|
20438
21450
|
const { before, after } = change;
|
|
20439
21451
|
if (!before.required && after.required) {
|
|
20440
|
-
if (!optionalToRequired.has(change.
|
|
20441
|
-
assertDefined(optionalToRequired.get(change.
|
|
21452
|
+
if (!optionalToRequired.has(change.tableName)) optionalToRequired.set(change.tableName, /* @__PURE__ */ new Set());
|
|
21453
|
+
assertDefined(optionalToRequired.get(change.tableName), "optionalToRequired entry missing").add(change.fieldName);
|
|
20442
21454
|
}
|
|
20443
21455
|
if (before.type === "enum" && after.type === "enum" && before.allowedValues && after.allowedValues) {
|
|
20444
21456
|
const beforeValues = before.allowedValues.map((v) => v.value);
|
|
@@ -20446,8 +21458,8 @@ function extractBreakingChangeFields(diff) {
|
|
|
20446
21458
|
const beforeSet = new Set(beforeValues);
|
|
20447
21459
|
const afterSet = new Set(afterValues);
|
|
20448
21460
|
if (beforeValues.some((v) => !afterSet.has(v)) || afterValues.some((v) => !beforeSet.has(v))) {
|
|
20449
|
-
if (!enumValueChanges.has(change.
|
|
20450
|
-
assertDefined(enumValueChanges.get(change.
|
|
21461
|
+
if (!enumValueChanges.has(change.tableName)) enumValueChanges.set(change.tableName, /* @__PURE__ */ new Map());
|
|
21462
|
+
assertDefined(enumValueChanges.get(change.tableName), "enumValueChanges entry missing").set(change.fieldName, {
|
|
20451
21463
|
beforeValues,
|
|
20452
21464
|
afterValues
|
|
20453
21465
|
});
|
|
@@ -20456,33 +21468,55 @@ function extractBreakingChangeFields(diff) {
|
|
|
20456
21468
|
} else if (change.kind === "field_added") {
|
|
20457
21469
|
const { after } = change;
|
|
20458
21470
|
if (after.required) {
|
|
20459
|
-
if (!addedRequiredFields.has(change.
|
|
20460
|
-
assertDefined(addedRequiredFields.get(change.
|
|
21471
|
+
if (!addedRequiredFields.has(change.tableName)) addedRequiredFields.set(change.tableName, /* @__PURE__ */ new Map());
|
|
21472
|
+
assertDefined(addedRequiredFields.get(change.tableName), "addedRequiredFields entry missing").set(change.fieldName, after);
|
|
20461
21473
|
}
|
|
20462
|
-
}
|
|
21474
|
+
} else if (change.kind === "field_renamed") {
|
|
21475
|
+
if (!renamedFields.has(change.tableName)) renamedFields.set(change.tableName, /* @__PURE__ */ new Map());
|
|
21476
|
+
assertDefined(renamedFields.get(change.tableName), "renamedFields entry missing").set(change.fieldName, change.after);
|
|
21477
|
+
} else if (change.kind === "table_renamed") renamedTypes.set(change.tableName, change.after);
|
|
20463
21478
|
return {
|
|
20464
21479
|
optionalToRequired,
|
|
20465
21480
|
addedRequiredFields,
|
|
20466
|
-
enumValueChanges
|
|
21481
|
+
enumValueChanges,
|
|
21482
|
+
renamedFields,
|
|
21483
|
+
clearedFields: /* @__PURE__ */ new Map(),
|
|
21484
|
+
renamedTypes
|
|
20467
21485
|
};
|
|
20468
21486
|
}
|
|
20469
21487
|
/**
|
|
20470
21488
|
* Generate the complete db.ts file content from a schema snapshot
|
|
20471
21489
|
* @param {SchemaSnapshot} snapshot - Schema snapshot to generate types from
|
|
20472
21490
|
* @param {MigrationDiff} [diff] - Optional migration diff for breaking change info
|
|
21491
|
+
* @param expandPlans - Field changes carried through temporary fields
|
|
20473
21492
|
* @returns {string} Generated db.ts file contents
|
|
20474
21493
|
*/
|
|
20475
|
-
function generateDbTypesFromSnapshot(snapshot, diff) {
|
|
20476
|
-
const types = Object.values(snapshot.types);
|
|
20477
|
-
if (types.length === 0) return generateEmptyDbTypes(snapshot.namespace);
|
|
21494
|
+
function generateDbTypesFromSnapshot(snapshot, diff, expandPlans = []) {
|
|
20478
21495
|
const breakingChangeFields = diff ? extractBreakingChangeFields(diff) : {
|
|
20479
21496
|
optionalToRequired: /* @__PURE__ */ new Map(),
|
|
20480
21497
|
addedRequiredFields: /* @__PURE__ */ new Map(),
|
|
20481
|
-
enumValueChanges: /* @__PURE__ */ new Map()
|
|
20482
|
-
|
|
21498
|
+
enumValueChanges: /* @__PURE__ */ new Map(),
|
|
21499
|
+
renamedFields: /* @__PURE__ */ new Map(),
|
|
21500
|
+
clearedFields: /* @__PURE__ */ new Map(),
|
|
21501
|
+
renamedTypes: /* @__PURE__ */ new Map()
|
|
21502
|
+
};
|
|
21503
|
+
for (const plan of expandPlans) {
|
|
21504
|
+
const injected = breakingChangeFields.renamedFields.get(plan.tableName) ?? /* @__PURE__ */ new Map();
|
|
21505
|
+
injected.set(plan.tempFieldName, {
|
|
21506
|
+
...plan.after,
|
|
21507
|
+
required: false,
|
|
21508
|
+
unique: false
|
|
21509
|
+
});
|
|
21510
|
+
breakingChangeFields.renamedFields.set(plan.tableName, injected);
|
|
21511
|
+
const cleared = breakingChangeFields.clearedFields.get(plan.tableName) ?? /* @__PURE__ */ new Set();
|
|
21512
|
+
cleared.add(plan.fieldName);
|
|
21513
|
+
breakingChangeFields.clearedFields.set(plan.tableName, cleared);
|
|
21514
|
+
}
|
|
21515
|
+
const tables = [...Object.values(snapshot.tables), ...breakingChangeFields.renamedTypes.values()];
|
|
21516
|
+
if (tables.length === 0) return generateEmptyDbTypes(snapshot.namespace);
|
|
20483
21517
|
const usedUtilityTypes = /* @__PURE__ */ new Set();
|
|
20484
21518
|
const typeDefinitions = [];
|
|
20485
|
-
for (const type of
|
|
21519
|
+
for (const type of tables) {
|
|
20486
21520
|
const result = generateTableType(type, breakingChangeFields);
|
|
20487
21521
|
if (result.usedTimestamp) usedUtilityTypes.add("Timestamp");
|
|
20488
21522
|
typeDefinitions.push(result.typeDef);
|
|
@@ -20559,12 +21593,14 @@ function generateTableType(type, breakingChangeFields) {
|
|
|
20559
21593
|
const optionalToRequiredFields = breakingChangeFields.optionalToRequired.get(type.name) || /* @__PURE__ */ new Set();
|
|
20560
21594
|
const addedRequiredFields = breakingChangeFields.addedRequiredFields.get(type.name) || /* @__PURE__ */ new Map();
|
|
20561
21595
|
const enumValueChangesForType = breakingChangeFields.enumValueChanges.get(type.name) || /* @__PURE__ */ new Map();
|
|
21596
|
+
const clearedFieldsForType = breakingChangeFields.clearedFields.get(type.name) ?? /* @__PURE__ */ new Set();
|
|
20562
21597
|
for (const [fieldName, fieldConfig] of Object.entries(type.fields)) {
|
|
20563
21598
|
if (fieldName === "id") continue;
|
|
20564
21599
|
const result = generateFieldType(fieldConfig, optionalToRequiredFields.has(fieldName), enumValueChangesForType.get(fieldName));
|
|
20565
|
-
|
|
21600
|
+
const clearable = clearedFieldsForType.has(fieldName);
|
|
21601
|
+
fieldLines.push(` ${fieldName}: ${clearable ? generateClearableFieldType(fieldConfig) : result.type};`);
|
|
20566
21602
|
usedTimestamp = usedTimestamp || result.usedTimestamp;
|
|
20567
|
-
usedColumnType = usedColumnType || result.usedColumnType;
|
|
21603
|
+
usedColumnType = usedColumnType || result.usedColumnType || clearable;
|
|
20568
21604
|
}
|
|
20569
21605
|
for (const [fieldName, fieldConfig] of addedRequiredFields) {
|
|
20570
21606
|
const result = generateFieldType(fieldConfig, true, void 0);
|
|
@@ -20572,6 +21608,13 @@ function generateTableType(type, breakingChangeFields) {
|
|
|
20572
21608
|
usedTimestamp = usedTimestamp || result.usedTimestamp;
|
|
20573
21609
|
usedColumnType = usedColumnType || result.usedColumnType;
|
|
20574
21610
|
}
|
|
21611
|
+
const renamedFieldsForType = breakingChangeFields.renamedFields.get(type.name) || /* @__PURE__ */ new Map();
|
|
21612
|
+
for (const [fieldName, fieldConfig] of renamedFieldsForType) {
|
|
21613
|
+
const result = generateFieldType(fieldConfig, fieldConfig.required, void 0);
|
|
21614
|
+
fieldLines.push(` ${fieldName}: ${result.type};`);
|
|
21615
|
+
usedTimestamp = usedTimestamp || result.usedTimestamp;
|
|
21616
|
+
usedColumnType = usedColumnType || result.usedColumnType;
|
|
21617
|
+
}
|
|
20575
21618
|
return {
|
|
20576
21619
|
typeDef: ` ${type.name}: {\n${fieldLines.join("\n")}\n }`,
|
|
20577
21620
|
usedTimestamp,
|
|
@@ -20622,6 +21665,19 @@ function generateEnumChangeColumnType(enumValueChange, config) {
|
|
|
20622
21665
|
if (!config.required) return `ColumnType<(${selectType}) | null, (${afterType}) | null, (${afterType}) | null>`;
|
|
20623
21666
|
return `ColumnType<${selectType}, ${afterType}, ${afterType}>`;
|
|
20624
21667
|
}
|
|
21668
|
+
/**
|
|
21669
|
+
* Column type for a field the migration script both reads and clears.
|
|
21670
|
+
*
|
|
21671
|
+
* Kysely takes the select, insert, and update types from the three slots in
|
|
21672
|
+
* turn, so the update slot has to accept the null the script writes.
|
|
21673
|
+
* @param config - Field configuration in the pre-migration snapshot
|
|
21674
|
+
* @returns {string} Generated column type
|
|
21675
|
+
*/
|
|
21676
|
+
function generateClearableFieldType(config) {
|
|
21677
|
+
const { type } = mapToTsType(config.type);
|
|
21678
|
+
const base = config.array ? `${type}[]` : type;
|
|
21679
|
+
return `ColumnType<${base} | null, ${base} | null, ${base} | null>`;
|
|
21680
|
+
}
|
|
20625
21681
|
function generateOptionalToRequiredDateColumnType(config) {
|
|
20626
21682
|
if (config.type !== "date" && config.type !== "datetime") return null;
|
|
20627
21683
|
if (config.type === "date") {
|
|
@@ -20682,10 +21738,11 @@ function generateFieldType(config, isOptionalToRequired, enumValueChange) {
|
|
|
20682
21738
|
* @param {string} migrationsDir - Migrations directory path
|
|
20683
21739
|
* @param {number} migrationNumber - Migration number
|
|
20684
21740
|
* @param {MigrationDiff} [diff] - Optional migration diff for breaking change info
|
|
21741
|
+
* @param expandPlans - Field changes carried through temporary fields
|
|
20685
21742
|
* @returns {Promise<string>} Path to the written file
|
|
20686
21743
|
*/
|
|
20687
|
-
async function writeDbTypesFile(snapshot, migrationsDir, migrationNumber, diff) {
|
|
20688
|
-
const content = generateDbTypesFromSnapshot(snapshot, diff);
|
|
21744
|
+
async function writeDbTypesFile(snapshot, migrationsDir, migrationNumber, diff, expandPlans = []) {
|
|
21745
|
+
const content = generateDbTypesFromSnapshot(snapshot, diff, expandPlans);
|
|
20689
21746
|
const filePath = getMigrationFilePath(migrationsDir, migrationNumber, "db");
|
|
20690
21747
|
await fs.writeFile(filePath, content);
|
|
20691
21748
|
return filePath;
|
|
@@ -20750,16 +21807,18 @@ async function generateSchemaFile(snapshot, migrationsDir, migrationNumber) {
|
|
|
20750
21807
|
* @param {number} migrationNumber - Migration number
|
|
20751
21808
|
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot (for db.ts generation)
|
|
20752
21809
|
* @param {string} [description] - Optional description for the migration
|
|
21810
|
+
* @param expandPlans - Field changes carried through temporary fields
|
|
20753
21811
|
* @returns {Promise<GenerateDiffResult>} Generated file info
|
|
20754
21812
|
*/
|
|
20755
|
-
async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousSnapshot, description) {
|
|
21813
|
+
async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousSnapshot, description, expandPlans = []) {
|
|
20756
21814
|
const migrationDir = getMigrationDirPath(migrationsDir, migrationNumber);
|
|
20757
21815
|
await fs.mkdir(migrationDir, { recursive: true });
|
|
20758
21816
|
const diffFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
|
|
20759
21817
|
const migrateFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
|
|
20760
21818
|
const dbTypesFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "db");
|
|
21819
|
+
const writeScript = diff.requiresMigrationScript;
|
|
20761
21820
|
await ensureFileNotExists(diffFilePath);
|
|
20762
|
-
if (
|
|
21821
|
+
if (writeScript) {
|
|
20763
21822
|
await ensureFileNotExists(migrateFilePath);
|
|
20764
21823
|
await ensureFileNotExists(dbTypesFilePath);
|
|
20765
21824
|
}
|
|
@@ -20772,11 +21831,11 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
|
|
|
20772
21831
|
diffFilePath,
|
|
20773
21832
|
migrationNumber
|
|
20774
21833
|
};
|
|
20775
|
-
if (
|
|
20776
|
-
const scriptContent = generateMigrationScript(diff);
|
|
21834
|
+
if (writeScript) {
|
|
21835
|
+
const scriptContent = generateMigrationScript(diff, expandPlans);
|
|
20777
21836
|
await fs.writeFile(migrateFilePath, scriptContent);
|
|
20778
21837
|
result.migrateFilePath = migrateFilePath;
|
|
20779
|
-
await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff);
|
|
21838
|
+
await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff, expandPlans);
|
|
20780
21839
|
result.dbTypesFilePath = dbTypesFilePath;
|
|
20781
21840
|
}
|
|
20782
21841
|
return result;
|
|
@@ -20784,13 +21843,16 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
|
|
|
20784
21843
|
/**
|
|
20785
21844
|
* Generate migration script content based on diff
|
|
20786
21845
|
* @param {MigrationDiff} diff - Migration diff
|
|
21846
|
+
* @param expandPlans - Field changes carried through temporary fields
|
|
20787
21847
|
* @returns {string} Migration script content
|
|
20788
21848
|
*/
|
|
20789
|
-
function generateMigrationScript(diff) {
|
|
21849
|
+
function generateMigrationScript(diff, expandPlans = []) {
|
|
20790
21850
|
const updates = [];
|
|
21851
|
+
const typeRenameTargets = new Map(diff.changes.filter((change) => change.kind === "table_renamed").map((change) => [change.previousTableName, change.tableName]));
|
|
21852
|
+
for (const plan of expandPlans) updates.push(generateExpandConversionScript(plan));
|
|
20791
21853
|
for (const change of diff.changes) {
|
|
20792
21854
|
const decimalScaleScript = generateDecimalScaleChangeScript(change);
|
|
20793
|
-
updates.push(...generateChangeScripts(change, decimalScaleScript !== null));
|
|
21855
|
+
updates.push(...generateChangeScripts(change, decimalScaleScript !== null, typeRenameTargets));
|
|
20794
21856
|
if (decimalScaleScript) {
|
|
20795
21857
|
updates.push(decimalScaleScript);
|
|
20796
21858
|
const uniqueConstraintScript = generateUniqueConstraintScript(change);
|
|
@@ -20863,33 +21925,34 @@ describe(${JSON.stringify(`${diff.namespace} migration`)}, () => {
|
|
|
20863
21925
|
* Generate scripts for a single change
|
|
20864
21926
|
* @param {DiffChange} change - Diff change to generate script for
|
|
20865
21927
|
* @param {boolean} deferUniqueConstraint - Generate the unique check after decimal re-serialization
|
|
21928
|
+
* @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
|
|
20866
21929
|
* @returns {string[]} Script contents, or an empty array if no script is needed
|
|
20867
21930
|
*/
|
|
20868
|
-
function generateChangeScripts(change, deferUniqueConstraint = false) {
|
|
21931
|
+
function generateChangeScripts(change, deferUniqueConstraint = false, typeRenameTargets) {
|
|
20869
21932
|
if (change.kind === "index_added" || change.kind === "index_modified") {
|
|
20870
21933
|
const before = change.kind === "index_modified" ? change.before : void 0;
|
|
20871
|
-
if (!isBreakingIndexChange(change.
|
|
21934
|
+
if (!isBreakingIndexChange(change.tableName, change.indexName, before, change.after)) return [];
|
|
20872
21935
|
const fields = change.after.fields;
|
|
20873
21936
|
const fieldList = fields.map((f) => `"${f}"`).join(", ");
|
|
20874
21937
|
const whereClauses = fields.map((f) => `.where("${f}", "=", dup.${f})`).join("\n ");
|
|
20875
21938
|
return [` // Resolve duplicate (${fields.join(", ")}) combinations before unique index "${change.indexName}" is enforced
|
|
20876
21939
|
{
|
|
20877
21940
|
const duplicates = await trx
|
|
20878
|
-
.selectFrom("${change.
|
|
21941
|
+
.selectFrom("${change.tableName}")
|
|
20879
21942
|
.select([${fieldList}])
|
|
20880
21943
|
.groupBy([${fieldList}])
|
|
20881
21944
|
.having((eb) => eb.fn.count("id"), ">", 1)
|
|
20882
21945
|
.execute();
|
|
20883
21946
|
for (const dup of duplicates) {
|
|
20884
21947
|
const records = await trx
|
|
20885
|
-
.selectFrom("${change.
|
|
21948
|
+
.selectFrom("${change.tableName}")
|
|
20886
21949
|
.select(["id"])
|
|
20887
21950
|
${whereClauses}
|
|
20888
21951
|
.execute();
|
|
20889
21952
|
// Keep the first record; update or delete the others so the combination becomes unique
|
|
20890
21953
|
for (let i = 1; i < records.length; i++) {
|
|
20891
21954
|
await trx
|
|
20892
|
-
.updateTable("${change.
|
|
21955
|
+
.updateTable("${change.tableName}")
|
|
20893
21956
|
.set({ ${fields[0]}: null }) // TODO: Set appropriate unique value
|
|
20894
21957
|
.where("id", "=", records[i].id)
|
|
20895
21958
|
.execute();
|
|
@@ -20898,22 +21961,28 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
|
|
|
20898
21961
|
}`];
|
|
20899
21962
|
}
|
|
20900
21963
|
if (change.kind === "field_added") {
|
|
20901
|
-
if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.
|
|
21964
|
+
if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.tableName} records
|
|
20902
21965
|
await trx
|
|
20903
|
-
.updateTable("${change.
|
|
21966
|
+
.updateTable("${change.tableName}")
|
|
20904
21967
|
.set({
|
|
20905
21968
|
${change.fieldName}: null, // TODO: Set appropriate default value
|
|
20906
21969
|
})
|
|
20907
21970
|
.execute();`];
|
|
20908
21971
|
return [];
|
|
20909
21972
|
}
|
|
21973
|
+
if (change.kind === "field_renamed") {
|
|
21974
|
+
const scripts = [generateFieldRenameCopyScript(change)];
|
|
21975
|
+
if ((change.after.unique ?? false) && (!(change.before.unique ?? false) || renameCopyCanCollapseValues(change))) scripts.push(generateUniqueDedupeScript(change.tableName, change.fieldName, "suffix"));
|
|
21976
|
+
return scripts;
|
|
21977
|
+
}
|
|
21978
|
+
if (change.kind === "table_renamed") return [generateTypeRenameCopyScript(change)];
|
|
20910
21979
|
if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return [];
|
|
20911
21980
|
const { before, after } = change;
|
|
20912
21981
|
const scripts = [];
|
|
20913
21982
|
if (change.kind === "field_type_modified") scripts.push(generateFieldTypeChangeScript(change));
|
|
20914
|
-
if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.
|
|
21983
|
+
if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.tableName} records where it is null
|
|
20915
21984
|
await trx
|
|
20916
|
-
.updateTable("${change.
|
|
21985
|
+
.updateTable("${change.tableName}")
|
|
20917
21986
|
.set({
|
|
20918
21987
|
${change.fieldName}: null, // TODO: Set appropriate default value
|
|
20919
21988
|
})
|
|
@@ -20931,25 +22000,25 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
|
|
|
20931
22000
|
const defaultValue = afterValues[0] ?? "NEW_VALUE";
|
|
20932
22001
|
scripts.push(` // Migrate records with removed enum values: ${removedValues.join(", ")}
|
|
20933
22002
|
await trx
|
|
20934
|
-
.updateTable("${change.
|
|
22003
|
+
.updateTable("${change.tableName}")
|
|
20935
22004
|
.set({ ${change.fieldName}: "${defaultValue}" }) // TODO: Set appropriate value
|
|
20936
22005
|
.where("${change.fieldName}", "in", [${removedValues.map((v) => `"${v}"`).join(", ")}])
|
|
20937
22006
|
.execute();`);
|
|
20938
22007
|
}
|
|
20939
22008
|
}
|
|
20940
|
-
if (before
|
|
22009
|
+
if (isBreakingForeignKeyRetarget(before, after, typeRenameTargets)) scripts.push(` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
|
|
20941
22010
|
// Find records that don't have a valid reference in the new target table
|
|
20942
22011
|
{
|
|
20943
22012
|
const orphanedRecords = await trx
|
|
20944
|
-
.selectFrom("${change.
|
|
20945
|
-
.leftJoin("${after.foreignKeyType}", "${change.
|
|
20946
|
-
.select(["${change.
|
|
22013
|
+
.selectFrom("${change.tableName}")
|
|
22014
|
+
.leftJoin("${after.foreignKeyType}", "${change.tableName}.${change.fieldName}", "${after.foreignKeyType}.id")
|
|
22015
|
+
.select(["${change.tableName}.id", "${change.tableName}.${change.fieldName}"])
|
|
20947
22016
|
.where("${after.foreignKeyType}.id", "is", null)
|
|
20948
|
-
.where("${change.
|
|
22017
|
+
.where("${change.tableName}.${change.fieldName}", "is not", null)
|
|
20949
22018
|
.execute();
|
|
20950
22019
|
for (const record of orphanedRecords) {
|
|
20951
22020
|
await trx
|
|
20952
|
-
.updateTable("${change.
|
|
22021
|
+
.updateTable("${change.tableName}")
|
|
20953
22022
|
.set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
|
|
20954
22023
|
.where("id", "=", record.id)
|
|
20955
22024
|
.execute();
|
|
@@ -20957,13 +22026,70 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
|
|
|
20957
22026
|
}`);
|
|
20958
22027
|
return scripts;
|
|
20959
22028
|
}
|
|
22029
|
+
function renameCopyCanCollapseValues(change) {
|
|
22030
|
+
const { before, after } = change;
|
|
22031
|
+
if (before.type !== "decimal" || after.type !== "decimal") return false;
|
|
22032
|
+
return (after.scale ?? 6) < (before.scale ?? 6);
|
|
22033
|
+
}
|
|
22034
|
+
function generateFieldRenameCopyScript(change) {
|
|
22035
|
+
const { tableName, fieldName, previousFieldName, before, after } = change;
|
|
22036
|
+
return ` // Copy ${tableName}.${previousFieldName} into ${fieldName} for every row.
|
|
22037
|
+
// Overwrite unconditionally: stored values of previously removed fields are
|
|
22038
|
+
// not pruned, so a stale value could otherwise resurface under ${fieldName}.${!before.required && after.required ? `
|
|
22039
|
+
// TODO: ${previousFieldName} is optional but ${fieldName} is required.
|
|
22040
|
+
// Resolve null values, or the post-migration phase will fail.` : ""}${renameCopyCanCollapseValues(change) ? `
|
|
22041
|
+
// WARNING: ${fieldName} has a smaller decimal scale than ${previousFieldName}, so
|
|
22042
|
+
// copied values that exceed it may be rounded half-up. Review the resulting
|
|
22043
|
+
// precision before deploying.` : ""}
|
|
22044
|
+
await trx
|
|
22045
|
+
.updateTable("${tableName}")
|
|
22046
|
+
.set((eb) => ({ ${fieldName}: eb.ref("${previousFieldName}") }))
|
|
22047
|
+
.execute();`;
|
|
22048
|
+
}
|
|
22049
|
+
function generateTypeRenameCopyScript(change) {
|
|
22050
|
+
const { tableName, previousTableName } = change;
|
|
22051
|
+
const columnList = ["id", ...Object.keys(change.before.fields).filter((name) => name !== "id")].map((name) => JSON.stringify(name)).join(", ");
|
|
22052
|
+
const selfRefColumns = Object.entries(change.after.fields).filter(([, field]) => field.foreignKeyType === tableName).map(([name]) => name);
|
|
22053
|
+
return ` // Copy every ${previousTableName} row into ${tableName}, preserving ids so that
|
|
22054
|
+
// stored foreign key references remain valid. ${previousTableName} stays readable
|
|
22055
|
+
// until the post-migration phase drops it.
|
|
22056
|
+
{
|
|
22057
|
+
let lastId: string | undefined;
|
|
22058
|
+
while (true) {
|
|
22059
|
+
let query = trx
|
|
22060
|
+
.selectFrom("${previousTableName}")
|
|
22061
|
+
.select([${columnList}])
|
|
22062
|
+
.orderBy("id", "asc")
|
|
22063
|
+
.limit(100);
|
|
22064
|
+
if (lastId) {
|
|
22065
|
+
query = query.where("id", ">", lastId);
|
|
22066
|
+
}
|
|
22067
|
+
const rows = await query.execute();
|
|
22068
|
+
if (rows.length === 0) break;
|
|
22069
|
+
|
|
22070
|
+
await trx.insertInto("${tableName}").values(${selfRefColumns.length > 0 ? `rows.map((row) => ({ ...row, ${selfRefColumns.map((name) => `${name}: null`).join(", ")} }))` : "rows"}).execute();
|
|
22071
|
+
lastId = rows[rows.length - 1]!.id;
|
|
22072
|
+
}
|
|
22073
|
+
}${selfRefColumns.length > 0 ? `
|
|
22074
|
+
|
|
22075
|
+
// Backfill the self-referential column(s) now that every row exists.
|
|
22076
|
+
await trx
|
|
22077
|
+
.updateTable("${tableName}")
|
|
22078
|
+
.set((eb) => ({
|
|
22079
|
+
${selfRefColumns.map((name) => ` ${name}: eb
|
|
22080
|
+
.selectFrom("${previousTableName}")
|
|
22081
|
+
.select("${previousTableName}.${name}")
|
|
22082
|
+
.whereRef("${previousTableName}.id", "=", "${tableName}.id"),`).join("\n")}
|
|
22083
|
+
}))
|
|
22084
|
+
.execute();` : ""}`;
|
|
22085
|
+
}
|
|
20960
22086
|
function generateFieldTypeChangeScript(change) {
|
|
20961
|
-
return ` // Normalize ${change.
|
|
22087
|
+
return ` // Normalize ${change.tableName}.${change.fieldName} from ${change.before.type} to ${change.after.type} while the previous type is still active
|
|
20962
22088
|
{
|
|
20963
22089
|
let lastId: string | undefined;
|
|
20964
22090
|
while (true) {
|
|
20965
22091
|
let query = trx
|
|
20966
|
-
.selectFrom("${change.
|
|
22092
|
+
.selectFrom("${change.tableName}")
|
|
20967
22093
|
.select(["id", "${change.fieldName}"])
|
|
20968
22094
|
.where("${change.fieldName}", "is not", null)
|
|
20969
22095
|
.orderBy("id", "asc")
|
|
@@ -20982,7 +22108,7 @@ function generateFieldTypeChangeScript(change) {
|
|
|
20982
22108
|
const normalizedValue: never = sourceValue;
|
|
20983
22109
|
if (Object.is(normalizedValue, sourceValue)) continue;
|
|
20984
22110
|
await trx
|
|
20985
|
-
.updateTable("${change.
|
|
22111
|
+
.updateTable("${change.tableName}")
|
|
20986
22112
|
.set({ [${JSON.stringify(change.fieldName)}]: normalizedValue })
|
|
20987
22113
|
.where("id", "=", row.id)
|
|
20988
22114
|
.execute();
|
|
@@ -20991,38 +22117,76 @@ function generateFieldTypeChangeScript(change) {
|
|
|
20991
22117
|
}
|
|
20992
22118
|
}`;
|
|
20993
22119
|
}
|
|
22120
|
+
function generateExpandConversionScript(plan) {
|
|
22121
|
+
return ` // Convert ${plan.tableName}.${plan.fieldName} into ${plan.tempFieldName}, which the next migration renames back to ${plan.fieldName}
|
|
22122
|
+
{
|
|
22123
|
+
let lastId: string | undefined;
|
|
22124
|
+
while (true) {
|
|
22125
|
+
let query = trx
|
|
22126
|
+
.selectFrom("${plan.tableName}")
|
|
22127
|
+
.select(["id", "${plan.fieldName}"])
|
|
22128
|
+
.where("${plan.fieldName}", "is not", null)
|
|
22129
|
+
.orderBy("id", "asc")
|
|
22130
|
+
.limit(100);
|
|
22131
|
+
if (lastId) {
|
|
22132
|
+
query = query.where("id", ">", lastId);
|
|
22133
|
+
}
|
|
22134
|
+
const rows = await query.execute();
|
|
22135
|
+
if (rows.length === 0) break;
|
|
22136
|
+
|
|
22137
|
+
for (const row of rows) {
|
|
22138
|
+
// ${MIGRATION_REVIEW_REQUIRED_MARKER}: Remove this marker and the \`never\` annotation after reviewing the conversion.
|
|
22139
|
+
// Produce a value accepted by the ${plan.after.type} type from the stored ${plan.before.type} value.
|
|
22140
|
+
const sourceValue = row.${plan.fieldName};
|
|
22141
|
+
const convertedValue: never = sourceValue;
|
|
22142
|
+
// Clearing ${plan.fieldName} keeps a re-run from converting the row twice.
|
|
22143
|
+
await trx
|
|
22144
|
+
.updateTable("${plan.tableName}")
|
|
22145
|
+
.set({
|
|
22146
|
+
[${JSON.stringify(plan.tempFieldName)}]: convertedValue,
|
|
22147
|
+
[${JSON.stringify(plan.fieldName)}]: null,
|
|
22148
|
+
})
|
|
22149
|
+
.where("id", "=", row.id)
|
|
22150
|
+
.execute();
|
|
22151
|
+
}
|
|
22152
|
+
lastId = rows[rows.length - 1]!.id;
|
|
22153
|
+
}
|
|
22154
|
+
}`;
|
|
22155
|
+
}
|
|
20994
22156
|
function generateUniqueConstraintScript(change) {
|
|
20995
22157
|
if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return null;
|
|
20996
22158
|
const { before, after } = change;
|
|
20997
22159
|
if ((before.unique ?? false) || !(after.unique ?? false)) return null;
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
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
|
-
}`;
|
|
21010
|
-
return ` // Ensure ${change.fieldName} values are unique before adding constraint
|
|
22160
|
+
return generateUniqueDedupeScript(change.tableName, change.fieldName, change.kind === "field_type_modified" ? "throw" : "suffix");
|
|
22161
|
+
}
|
|
22162
|
+
function generateUniqueDedupeScript(tableName, fieldName, resolution) {
|
|
22163
|
+
return ` // Ensure ${fieldName} values are unique before adding constraint
|
|
21011
22164
|
{
|
|
21012
22165
|
const duplicates = await trx
|
|
21013
|
-
.selectFrom("${
|
|
21014
|
-
.select(["${
|
|
21015
|
-
.groupBy("${
|
|
22166
|
+
.selectFrom("${tableName}")
|
|
22167
|
+
.select(["${fieldName}"])
|
|
22168
|
+
.groupBy("${fieldName}")
|
|
21016
22169
|
.having((eb) => eb.fn.count("id"), ">", 1)
|
|
21017
22170
|
.execute();
|
|
21018
22171
|
for (const dup of duplicates) {
|
|
21019
22172
|
// Load every record in this duplicate group before resolving it
|
|
21020
22173
|
const records = await trx
|
|
21021
|
-
.selectFrom("${
|
|
21022
|
-
.select(["id", "${
|
|
21023
|
-
.where("${
|
|
22174
|
+
.selectFrom("${tableName}")
|
|
22175
|
+
.select(["id", "${fieldName}"])
|
|
22176
|
+
.where("${fieldName}", "=", dup.${fieldName})
|
|
21024
22177
|
.execute();
|
|
21025
|
-
${
|
|
22178
|
+
${resolution === "throw" ? ` if (records.length > 1) {
|
|
22179
|
+
throw new Error(
|
|
22180
|
+
"TODO: Resolve duplicate ${tableName}.${fieldName} values before adding the unique constraint",
|
|
22181
|
+
);
|
|
22182
|
+
}` : ` // Keep first record, add suffix to others
|
|
22183
|
+
for (let i = 1; i < records.length; i++) {
|
|
22184
|
+
await trx
|
|
22185
|
+
.updateTable("${tableName}")
|
|
22186
|
+
.set({ ${fieldName}: \`\${records[i].${fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
|
|
22187
|
+
.where("id", "=", records[i].id)
|
|
22188
|
+
.execute();
|
|
22189
|
+
}`}
|
|
21026
22190
|
}
|
|
21027
22191
|
}`;
|
|
21028
22192
|
}
|
|
@@ -21033,9 +22197,9 @@ function generateDecimalScaleChangeScript(change) {
|
|
|
21033
22197
|
const valueExpression = !before.required && after.required ? `row.${change.fieldName}!` : `row.${change.fieldName}`;
|
|
21034
22198
|
const beforeScale = before.scale ?? 6;
|
|
21035
22199
|
const roundingWarning = (after.scale ?? 6) < beforeScale ? `
|
|
21036
|
-
// Values that exceed the new scale may be rounded half-up, so
|
|
21037
|
-
// resulting precision before deploying.` : "";
|
|
21038
|
-
return ` // Re-save existing ${change.
|
|
22200
|
+
// WARNING: Values that exceed the new scale may be rounded half-up, so
|
|
22201
|
+
// review the resulting precision before deploying.` : "";
|
|
22202
|
+
return ` // Re-save existing ${change.tableName} rows so ${change.fieldName} is stored under the new scale.
|
|
21039
22203
|
// This is a workaround for a platform-side gap where rows written under the
|
|
21040
22204
|
// previous scale could fail on later updates until re-saved. Keep it unless
|
|
21041
22205
|
// your platform is confirmed to handle stored values across scale changes.${roundingWarning}
|
|
@@ -21043,7 +22207,7 @@ function generateDecimalScaleChangeScript(change) {
|
|
|
21043
22207
|
let lastId: string | undefined;
|
|
21044
22208
|
while (true) {
|
|
21045
22209
|
let query = trx
|
|
21046
|
-
.selectFrom("${change.
|
|
22210
|
+
.selectFrom("${change.tableName}")
|
|
21047
22211
|
.select(["id", "${change.fieldName}"])
|
|
21048
22212
|
.where("${change.fieldName}", "is not", null)
|
|
21049
22213
|
.orderBy("id", "asc")
|
|
@@ -21056,7 +22220,7 @@ function generateDecimalScaleChangeScript(change) {
|
|
|
21056
22220
|
|
|
21057
22221
|
for (const row of rows) {
|
|
21058
22222
|
await trx
|
|
21059
|
-
.updateTable("${change.
|
|
22223
|
+
.updateTable("${change.tableName}")
|
|
21060
22224
|
.set({ ${change.fieldName}: ${valueExpression} })
|
|
21061
22225
|
.where("id", "=", row.id)
|
|
21062
22226
|
.where("${change.fieldName}", "=", ${valueExpression})
|
|
@@ -21344,14 +22508,46 @@ async function generate(options) {
|
|
|
21344
22508
|
logger.info("Add \"migration: { directory: \\\"./migrations\\\" }\" to your db config to enable migrations.");
|
|
21345
22509
|
return;
|
|
21346
22510
|
}
|
|
22511
|
+
const renameFlags = [];
|
|
22512
|
+
const typeRenameFlags = [];
|
|
22513
|
+
for (const raw of options.renames ?? []) if (raw.includes(".")) renameFlags.push({
|
|
22514
|
+
raw,
|
|
22515
|
+
spec: parseRenameOption(raw)
|
|
22516
|
+
});
|
|
22517
|
+
else typeRenameFlags.push({
|
|
22518
|
+
raw,
|
|
22519
|
+
spec: parseTypeRenameOption(raw)
|
|
22520
|
+
});
|
|
22521
|
+
const dropFlags = [];
|
|
22522
|
+
const typeDropFlags = [];
|
|
22523
|
+
for (const raw of options.drops ?? []) if (raw.includes(".")) dropFlags.push({
|
|
22524
|
+
raw,
|
|
22525
|
+
spec: parseDropOption(raw)
|
|
22526
|
+
});
|
|
22527
|
+
else typeDropFlags.push({
|
|
22528
|
+
raw,
|
|
22529
|
+
spec: parseTypeDropOption(raw)
|
|
22530
|
+
});
|
|
22531
|
+
const expandContractFlags = (options.expandContracts ?? []).map((raw) => ({
|
|
22532
|
+
raw,
|
|
22533
|
+
spec: parseExpandContractOption(raw)
|
|
22534
|
+
}));
|
|
22535
|
+
if (options.init && (renameFlags.length > 0 || typeRenameFlags.length > 0 || dropFlags.length > 0 || typeDropFlags.length > 0 || expandContractFlags.length > 0)) throw new Error("--rename, --drop, and --expand-contract cannot be used together with --init.");
|
|
22536
|
+
const droppedFieldKeys = new Set(dropFlags.map(({ spec }) => `${spec.tableName}.${spec.fieldName}`));
|
|
22537
|
+
const conflictingFlags = renameFlags.filter(({ spec }) => droppedFieldKeys.has(`${spec.tableName}.${spec.previousFieldName}`));
|
|
22538
|
+
if (conflictingFlags.length > 0) throw new Error(`--rename and --drop conflict for: ${conflictingFlags.map(({ spec }) => `${spec.tableName}.${spec.previousFieldName}`).join(", ")}`);
|
|
22539
|
+
const droppedTypeNames = new Set(typeDropFlags.map(({ spec }) => spec.tableName));
|
|
22540
|
+
const conflictingTypeFlags = typeRenameFlags.filter(({ spec }) => droppedTypeNames.has(spec.previousTableName));
|
|
22541
|
+
if (conflictingTypeFlags.length > 0) throw new Error(`--rename and --drop conflict for: ${conflictingTypeFlags.map(({ spec }) => spec.previousTableName).join(", ")}`);
|
|
21347
22542
|
if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
|
|
21348
22543
|
let pluginManager;
|
|
21349
22544
|
if (plugins.length > 0) pluginManager = new PluginManager(plugins);
|
|
21350
|
-
const { defineApplication } = await import("./application-
|
|
22545
|
+
const { defineApplication } = await import("./application-BZfk4HKm.mjs");
|
|
21351
22546
|
const application = defineApplication({
|
|
21352
22547
|
config,
|
|
21353
22548
|
pluginManager
|
|
21354
22549
|
});
|
|
22550
|
+
const generations = [];
|
|
21355
22551
|
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
21356
22552
|
logger.info(`Processing namespace: ${styles.bold(namespace)}`);
|
|
21357
22553
|
assertValidMigrationFiles(migrationsDir, namespace);
|
|
@@ -21363,11 +22559,110 @@ async function generate(options) {
|
|
|
21363
22559
|
await tailordbService.loadTypes();
|
|
21364
22560
|
await tailordbService.processNamespacePlugins();
|
|
21365
22561
|
const localTypesObj = tailordbService.types;
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
22562
|
+
generations.push({
|
|
22563
|
+
namespace,
|
|
22564
|
+
migrationsDir,
|
|
22565
|
+
currentSnapshot: createSnapshotFromLocalTypes(localTypesObj, namespace),
|
|
22566
|
+
previousSnapshot: reconstructSnapshotFromMigrations(migrationsDir)
|
|
22567
|
+
});
|
|
21370
22568
|
}
|
|
22569
|
+
const renameSpecsByNamespace = matchFlagsToNamespaces(renameFlags, generations, renameSpecApplies, "--rename does not match a removed + added field pair");
|
|
22570
|
+
const typeRenameSpecsByNamespace = matchFlagsToNamespaces(typeRenameFlags, generations, typeRenameSpecApplies, "--rename does not match a removed + added type pair");
|
|
22571
|
+
const dropSpecsByNamespace = matchFlagsToNamespaces(dropFlags, generations, dropSpecApplies, "--drop does not match a removed field");
|
|
22572
|
+
const typeDropSpecsByNamespace = matchFlagsToNamespaces(typeDropFlags, generations, typeDropSpecApplies, "--drop does not match a removed type");
|
|
22573
|
+
const expandContractKeysByNamespace = /* @__PURE__ */ new Map();
|
|
22574
|
+
const matchedExpandContractFlags = /* @__PURE__ */ new Set();
|
|
22575
|
+
const ineligibleExpandContracts = [];
|
|
22576
|
+
for (const { namespace, previousSnapshot, currentSnapshot } of generations) {
|
|
22577
|
+
if (!previousSnapshot) continue;
|
|
22578
|
+
const applicable = [];
|
|
22579
|
+
for (const flag of expandContractFlags) {
|
|
22580
|
+
const { spec } = flag;
|
|
22581
|
+
const before = previousSnapshot.tables[spec.tableName]?.fields[spec.fieldName];
|
|
22582
|
+
const after = currentSnapshot.tables[spec.tableName]?.fields[spec.fieldName];
|
|
22583
|
+
if (!before || !after || before.type === after.type) continue;
|
|
22584
|
+
matchedExpandContractFlags.add(flag);
|
|
22585
|
+
const eligibility = getExpandContractEligibility({
|
|
22586
|
+
previous: previousSnapshot,
|
|
22587
|
+
current: currentSnapshot,
|
|
22588
|
+
tableName: spec.tableName,
|
|
22589
|
+
fieldName: spec.fieldName
|
|
22590
|
+
});
|
|
22591
|
+
if (!eligibility.eligible) {
|
|
22592
|
+
ineligibleExpandContracts.push(`--expand-contract cannot convert ${flag.raw} (namespace: ${namespace}): ${eligibility.reason}`);
|
|
22593
|
+
continue;
|
|
22594
|
+
}
|
|
22595
|
+
applicable.push(flag);
|
|
22596
|
+
}
|
|
22597
|
+
expandContractKeysByNamespace.set(namespace, new Set(applicable.map(({ spec }) => fieldKey(spec.tableName, spec.fieldName))));
|
|
22598
|
+
}
|
|
22599
|
+
const unusedExpandContracts = expandContractFlags.filter((flag) => !matchedExpandContractFlags.has(flag));
|
|
22600
|
+
if (unusedExpandContracts.length > 0) throw new Error(`--expand-contract does not match a field whose type changed: ${unusedExpandContracts.map((flag) => flag.raw).join(", ")}`);
|
|
22601
|
+
if (ineligibleExpandContracts.length > 0) throw new Error(ineligibleExpandContracts.join("\n"));
|
|
22602
|
+
const unresolvedCandidates = [];
|
|
22603
|
+
for (const generation of generations) {
|
|
22604
|
+
const { namespace, previousSnapshot, currentSnapshot } = generation;
|
|
22605
|
+
if (!previousSnapshot) continue;
|
|
22606
|
+
const diff = compareSnapshots(previousSnapshot, currentSnapshot);
|
|
22607
|
+
if (!hasChanges(diff)) {
|
|
22608
|
+
generation.diff = diff;
|
|
22609
|
+
continue;
|
|
22610
|
+
}
|
|
22611
|
+
const resolution = await resolveRenames(previousSnapshot, currentSnapshot, diff, options, {
|
|
22612
|
+
fieldRenames: renameSpecsByNamespace.get(namespace) ?? [],
|
|
22613
|
+
typeRenames: typeRenameSpecsByNamespace.get(namespace) ?? [],
|
|
22614
|
+
fieldDrops: dropSpecsByNamespace.get(namespace) ?? [],
|
|
22615
|
+
typeDrops: typeDropSpecsByNamespace.get(namespace) ?? []
|
|
22616
|
+
});
|
|
22617
|
+
generation.diff = resolution.diff;
|
|
22618
|
+
unresolvedCandidates.push(...resolution.unresolved);
|
|
22619
|
+
generation.expandPlans = await resolveExpandContractPlans({
|
|
22620
|
+
previousSnapshot,
|
|
22621
|
+
currentSnapshot,
|
|
22622
|
+
diff: resolution.diff,
|
|
22623
|
+
options,
|
|
22624
|
+
confirmedKeys: expandContractKeysByNamespace.get(namespace) ?? /* @__PURE__ */ new Set()
|
|
22625
|
+
});
|
|
22626
|
+
}
|
|
22627
|
+
if (unresolvedCandidates.length > 0) {
|
|
22628
|
+
const details = unresolvedCandidates.map(({ namespace, label, targets }) => ` - ${label} → ${targets.join(", ")}? (namespace: ${namespace})`).join("\n");
|
|
22629
|
+
throw new Error(`Possible rename(s) detected:\n${details}\nRe-run with --rename "Type.oldField:newField" (field) or --rename "OldType:NewType" (type) to record a rename and scaffold a data copy script, or --drop "Type.field" / --drop "Type" to confirm the removal.`);
|
|
22630
|
+
}
|
|
22631
|
+
for (const { migrationsDir, currentSnapshot, previousSnapshot, diff, expandPlans } of generations) if (!previousSnapshot) await generateInitialSnapshot(currentSnapshot, migrationsDir);
|
|
22632
|
+
else await generateDiffFromSnapshot(previousSnapshot, assertDefined(diff, "Migration diff was not resolved during preflight"), migrationsDir, options, currentSnapshot, expandPlans ?? []);
|
|
22633
|
+
}
|
|
22634
|
+
/**
|
|
22635
|
+
* Decide which unsupported field type changes to carry through a migration
|
|
22636
|
+
* pair, asking about each one that was not already named by a flag.
|
|
22637
|
+
* @param input - Snapshots, diff, command options, and flag-approved fields
|
|
22638
|
+
* @returns Approved plans, empty when nothing was confirmed
|
|
22639
|
+
*/
|
|
22640
|
+
async function resolveExpandContractPlans(input) {
|
|
22641
|
+
const { previousSnapshot, currentSnapshot, diff, options, confirmedKeys } = input;
|
|
22642
|
+
const confirmed = new Set(confirmedKeys);
|
|
22643
|
+
if (!options.yes && canPrompt()) for (const change of diff.changes) {
|
|
22644
|
+
if (change.kind !== "field_type_modified") continue;
|
|
22645
|
+
const key = fieldKey(change.tableName, change.fieldName);
|
|
22646
|
+
if (confirmed.has(key)) continue;
|
|
22647
|
+
if (!canConvertField({
|
|
22648
|
+
previous: previousSnapshot,
|
|
22649
|
+
current: currentSnapshot,
|
|
22650
|
+
tableName: change.tableName,
|
|
22651
|
+
fieldName: change.fieldName
|
|
22652
|
+
})) continue;
|
|
22653
|
+
logger.newline();
|
|
22654
|
+
logger.info(`${change.tableName}.${change.fieldName} changes from ${change.before.type} to ${change.after.type}, which cannot be applied in one step.`);
|
|
22655
|
+
if (await prompt.confirm({
|
|
22656
|
+
message: `Generate two migrations to convert ${change.tableName}.${change.fieldName} through a temporary field?`,
|
|
22657
|
+
default: true
|
|
22658
|
+
})) confirmed.add(key);
|
|
22659
|
+
}
|
|
22660
|
+
return planExpandContract({
|
|
22661
|
+
previous: previousSnapshot,
|
|
22662
|
+
current: currentSnapshot,
|
|
22663
|
+
diff,
|
|
22664
|
+
confirmed
|
|
22665
|
+
}).plans;
|
|
21371
22666
|
}
|
|
21372
22667
|
/**
|
|
21373
22668
|
* Generate the initial schema snapshot
|
|
@@ -21379,19 +22674,174 @@ async function generateInitialSnapshot(snapshot, migrationsDir) {
|
|
|
21379
22674
|
const result = await generateSchemaFile(snapshot, migrationsDir, 0);
|
|
21380
22675
|
logger.success(`Generated initial schema snapshot`);
|
|
21381
22676
|
logger.info(` File: ${result.filePath}`);
|
|
21382
|
-
logger.info(` Types: ${Object.keys(snapshot.
|
|
22677
|
+
logger.info(` Types: ${Object.keys(snapshot.tables).length}`);
|
|
21383
22678
|
logger.log("\nThis is the baseline schema. Future changes will be tracked as diffs.");
|
|
21384
22679
|
}
|
|
21385
22680
|
/**
|
|
21386
|
-
*
|
|
22681
|
+
* Match `--rename` / `--drop` flags against every namespace's snapshots.
|
|
22682
|
+
* Throws when a flag applies to no namespace, so a typo cannot silently fall
|
|
22683
|
+
* back to remove + add.
|
|
22684
|
+
* @param {readonly { raw: string; spec: S }[]} flags - Parsed flags of one form
|
|
22685
|
+
* @param {readonly NamespaceGeneration[]} generations - Snapshots per namespace
|
|
22686
|
+
* @param {(spec: S, previous: SchemaSnapshot, current: SchemaSnapshot) => boolean} applies - Whether a spec matches a namespace's snapshot pair
|
|
22687
|
+
* @param {string} unmatchedError - Error prefix for flags that match no namespace
|
|
22688
|
+
* @returns {Map<string, S[]>} Applicable specs keyed by namespace
|
|
22689
|
+
*/
|
|
22690
|
+
function matchFlagsToNamespaces(flags, generations, applies, unmatchedError) {
|
|
22691
|
+
const specsByNamespace = /* @__PURE__ */ new Map();
|
|
22692
|
+
const matched = /* @__PURE__ */ new Set();
|
|
22693
|
+
for (const { namespace, previousSnapshot, currentSnapshot } of generations) {
|
|
22694
|
+
if (!previousSnapshot) continue;
|
|
22695
|
+
const applicable = flags.filter(({ spec }) => applies(spec, previousSnapshot, currentSnapshot));
|
|
22696
|
+
for (const flag of applicable) matched.add(flag);
|
|
22697
|
+
specsByNamespace.set(namespace, applicable.map((flag) => flag.spec));
|
|
22698
|
+
}
|
|
22699
|
+
const unused = flags.filter((flag) => !matched.has(flag));
|
|
22700
|
+
if (unused.length > 0) throw new Error(`${unmatchedError}: ${unused.map((flag) => flag.raw).join(", ")}`);
|
|
22701
|
+
return specsByNamespace;
|
|
22702
|
+
}
|
|
22703
|
+
function availableRenameTargets(candidate, claimedFields) {
|
|
22704
|
+
return candidate.added.filter((added) => !claimedFields.has(`${candidate.tableName}.${added.fieldName}`)).map((added) => added.fieldName);
|
|
22705
|
+
}
|
|
22706
|
+
/**
|
|
22707
|
+
* Ask the user whether a removed field was renamed to one of the compatible
|
|
22708
|
+
* added fields. Returns the confirmed new field name, or undefined.
|
|
22709
|
+
* @param {FieldRenameCandidate} candidate - Candidate to confirm
|
|
22710
|
+
* @param {string[]} addedFieldNames - Added field names still available as rename targets
|
|
22711
|
+
* @returns {Promise<string | undefined>} Confirmed new field name, if any
|
|
22712
|
+
*/
|
|
22713
|
+
async function promptRenameCandidate(candidate, addedFieldNames) {
|
|
22714
|
+
const oldLabel = `${candidate.tableName}.${candidate.removed.fieldName}`;
|
|
22715
|
+
const [firstFieldName] = addedFieldNames;
|
|
22716
|
+
if (addedFieldNames.length === 1 && firstFieldName) return await prompt.confirm({
|
|
22717
|
+
message: `${oldLabel} was removed and ${firstFieldName} was added with a compatible type. Was it renamed to ${firstFieldName}?`,
|
|
22718
|
+
default: true
|
|
22719
|
+
}) ? firstFieldName : void 0;
|
|
22720
|
+
return await prompt.select({
|
|
22721
|
+
message: `${oldLabel} was removed. Was it renamed to one of these added fields?`,
|
|
22722
|
+
choices: [...addedFieldNames.map((fieldName) => ({
|
|
22723
|
+
name: `Yes, renamed to ${fieldName}`,
|
|
22724
|
+
value: fieldName
|
|
22725
|
+
})), {
|
|
22726
|
+
name: `No, ${candidate.removed.fieldName} was removed`,
|
|
22727
|
+
value: null
|
|
22728
|
+
}]
|
|
22729
|
+
}) ?? void 0;
|
|
22730
|
+
}
|
|
22731
|
+
function availableTypeRenameTargets(candidate, claimedTypes) {
|
|
22732
|
+
return candidate.added.filter((added) => !claimedTypes.has(added.tableName)).map((added) => added.tableName);
|
|
22733
|
+
}
|
|
22734
|
+
/**
|
|
22735
|
+
* Ask the user whether a removed type was renamed to one of the compatible
|
|
22736
|
+
* added types. Returns the confirmed new type name, or undefined.
|
|
22737
|
+
* @param {TypeRenameCandidate} candidate - Candidate to confirm
|
|
22738
|
+
* @param {string[]} addedTypeNames - Added type names still available as rename targets
|
|
22739
|
+
* @returns {Promise<string | undefined>} Confirmed new type name, if any
|
|
22740
|
+
*/
|
|
22741
|
+
async function promptTypeRenameCandidate(candidate, addedTypeNames) {
|
|
22742
|
+
const oldTypeName = candidate.removed.tableName;
|
|
22743
|
+
const [firstTypeName] = addedTypeNames;
|
|
22744
|
+
if (addedTypeNames.length === 1 && firstTypeName) return await prompt.confirm({
|
|
22745
|
+
message: `${oldTypeName} was removed and ${firstTypeName} was added with a compatible schema. Was it renamed to ${firstTypeName}?`,
|
|
22746
|
+
default: true
|
|
22747
|
+
}) ? firstTypeName : void 0;
|
|
22748
|
+
return await prompt.select({
|
|
22749
|
+
message: `${oldTypeName} was removed. Was it renamed to one of these added types?`,
|
|
22750
|
+
choices: [...addedTypeNames.map((tableName) => ({
|
|
22751
|
+
name: `Yes, renamed to ${tableName}`,
|
|
22752
|
+
value: tableName
|
|
22753
|
+
})), {
|
|
22754
|
+
name: `No, ${oldTypeName} was removed`,
|
|
22755
|
+
value: null
|
|
22756
|
+
}]
|
|
22757
|
+
}) ?? void 0;
|
|
22758
|
+
}
|
|
22759
|
+
/**
|
|
22760
|
+
* Resolve field and type renames for a diff: apply `--rename` flags, skip
|
|
22761
|
+
* candidates whose removal is confirmed by `--drop`, confirm the rest
|
|
22762
|
+
* interactively, and recompute the diff with the confirmed renames. When
|
|
22763
|
+
* prompting is unavailable (`--yes` or no TTY), the remaining candidates are
|
|
22764
|
+
* returned as unresolved for the caller to fail on.
|
|
22765
|
+
* @param {NormalizedSchemaSnapshot} previousSnapshot - Previous normalized schema snapshot
|
|
22766
|
+
* @param {NormalizedSchemaSnapshot} currentSnapshot - Current normalized schema snapshot
|
|
22767
|
+
* @param {MigrationDiff} diff - Diff computed without rename knowledge
|
|
22768
|
+
* @param {GenerateOptions} options - Generate options
|
|
22769
|
+
* @param {NamespaceRenameSpecs} specs - This namespace's `--rename` / `--drop` specs
|
|
22770
|
+
* @returns {Promise<RenameResolution>} Diff with confirmed renames and any unresolved candidates
|
|
22771
|
+
*/
|
|
22772
|
+
async function resolveRenames(previousSnapshot, currentSnapshot, diff, options, specs) {
|
|
22773
|
+
const confirmed = [...specs.fieldRenames];
|
|
22774
|
+
const claimedFields = new Set(confirmed.flatMap((spec) => [`${spec.tableName}.${spec.previousFieldName}`, `${spec.tableName}.${spec.fieldName}`]));
|
|
22775
|
+
const droppedFields = new Set(specs.fieldDrops.map((spec) => `${spec.tableName}.${spec.fieldName}`));
|
|
22776
|
+
const confirmedTypes = [...specs.typeRenames];
|
|
22777
|
+
const claimedTypes = new Set(confirmedTypes.flatMap((spec) => [spec.previousTableName, spec.tableName]));
|
|
22778
|
+
const droppedTypes = new Set(specs.typeDrops.map((spec) => spec.tableName));
|
|
22779
|
+
const typeCandidates = findTypeRenameCandidates(diff).filter((candidate) => !droppedTypes.has(candidate.removed.tableName) && !claimedTypes.has(candidate.removed.tableName) && availableTypeRenameTargets(candidate, claimedTypes).length > 0);
|
|
22780
|
+
const candidates = findRenameCandidates(diff).filter((candidate) => !droppedFields.has(`${candidate.tableName}.${candidate.removed.fieldName}`) && !claimedFields.has(`${candidate.tableName}.${candidate.removed.fieldName}`) && availableRenameTargets(candidate, claimedFields).length > 0);
|
|
22781
|
+
const unresolved = [];
|
|
22782
|
+
if (options.yes || !canPrompt()) {
|
|
22783
|
+
for (const candidate of typeCandidates) unresolved.push({
|
|
22784
|
+
namespace: diff.namespace,
|
|
22785
|
+
label: candidate.removed.tableName,
|
|
22786
|
+
targets: availableTypeRenameTargets(candidate, claimedTypes)
|
|
22787
|
+
});
|
|
22788
|
+
for (const candidate of candidates) unresolved.push({
|
|
22789
|
+
namespace: diff.namespace,
|
|
22790
|
+
label: `${candidate.tableName}.${candidate.removed.fieldName}`,
|
|
22791
|
+
targets: availableRenameTargets(candidate, claimedFields)
|
|
22792
|
+
});
|
|
22793
|
+
} else {
|
|
22794
|
+
for (const candidate of typeCandidates) {
|
|
22795
|
+
const addedTypeNames = availableTypeRenameTargets(candidate, claimedTypes);
|
|
22796
|
+
if (addedTypeNames.length === 0) continue;
|
|
22797
|
+
const newTypeName = await promptTypeRenameCandidate(candidate, addedTypeNames);
|
|
22798
|
+
if (newTypeName) {
|
|
22799
|
+
confirmedTypes.push({
|
|
22800
|
+
previousTableName: candidate.removed.tableName,
|
|
22801
|
+
tableName: newTypeName
|
|
22802
|
+
});
|
|
22803
|
+
claimedTypes.add(candidate.removed.tableName);
|
|
22804
|
+
claimedTypes.add(newTypeName);
|
|
22805
|
+
}
|
|
22806
|
+
}
|
|
22807
|
+
for (const candidate of candidates) {
|
|
22808
|
+
const addedFieldNames = availableRenameTargets(candidate, claimedFields);
|
|
22809
|
+
if (addedFieldNames.length === 0) continue;
|
|
22810
|
+
const newFieldName = await promptRenameCandidate(candidate, addedFieldNames);
|
|
22811
|
+
if (newFieldName) {
|
|
22812
|
+
confirmed.push({
|
|
22813
|
+
tableName: candidate.tableName,
|
|
22814
|
+
previousFieldName: candidate.removed.fieldName,
|
|
22815
|
+
fieldName: newFieldName
|
|
22816
|
+
});
|
|
22817
|
+
claimedFields.add(`${candidate.tableName}.${candidate.removed.fieldName}`);
|
|
22818
|
+
claimedFields.add(`${candidate.tableName}.${newFieldName}`);
|
|
22819
|
+
}
|
|
22820
|
+
}
|
|
22821
|
+
}
|
|
22822
|
+
if (confirmed.length === 0 && confirmedTypes.length === 0) return {
|
|
22823
|
+
diff,
|
|
22824
|
+
unresolved
|
|
22825
|
+
};
|
|
22826
|
+
return {
|
|
22827
|
+
diff: compareSnapshots(previousSnapshot, currentSnapshot, {
|
|
22828
|
+
fieldRenames: confirmed,
|
|
22829
|
+
typeRenames: confirmedTypes
|
|
22830
|
+
}),
|
|
22831
|
+
unresolved
|
|
22832
|
+
};
|
|
22833
|
+
}
|
|
22834
|
+
/**
|
|
22835
|
+
* Generate migration files from a diff resolved during preflight
|
|
21387
22836
|
* @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
|
|
21388
|
-
* @param {
|
|
22837
|
+
* @param {MigrationDiff} diff - Diff with confirmed renames recorded
|
|
21389
22838
|
* @param {string} migrationsDir - Migrations directory path
|
|
21390
22839
|
* @param {GenerateOptions} options - Generate options
|
|
22840
|
+
* @param currentSnapshot - Schema the user now declares
|
|
22841
|
+
* @param expandPlans - Field changes confirmed for a migration pair
|
|
21391
22842
|
* @returns {Promise<void>} Promise that resolves when diff is generated
|
|
21392
22843
|
*/
|
|
21393
|
-
async function generateDiffFromSnapshot(previousSnapshot,
|
|
21394
|
-
const diff = compareSnapshots(previousSnapshot, currentSnapshot);
|
|
22844
|
+
async function generateDiffFromSnapshot(previousSnapshot, diff, migrationsDir, options, currentSnapshot, expandPlans = []) {
|
|
21395
22845
|
if (!hasChanges(diff)) {
|
|
21396
22846
|
logger.info("No schema differences detected.");
|
|
21397
22847
|
return;
|
|
@@ -21400,18 +22850,30 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
|
|
|
21400
22850
|
logger.log(formatMigrationDiff(diff));
|
|
21401
22851
|
logger.newline();
|
|
21402
22852
|
logger.info(`Summary: ${formatDiffSummary(diff)}`);
|
|
21403
|
-
const
|
|
22853
|
+
const plannedKeys = new Set(expandPlans.map((plan) => fieldKey(plan.tableName, plan.fieldName)));
|
|
22854
|
+
const unsupportedChanges = diff.breakingChanges.filter((change) => change.unsupported && !(change.fieldName && plannedKeys.has(fieldKey(change.tableName, change.fieldName))));
|
|
21404
22855
|
if (unsupportedChanges.length > 0) {
|
|
21405
22856
|
for (const change of unsupportedChanges) {
|
|
21406
22857
|
logger.newline();
|
|
21407
|
-
logger.error(`Unsupported change: ${change.
|
|
22858
|
+
logger.error(`Unsupported change: ${change.tableName}.${change.fieldName}`);
|
|
21408
22859
|
logger.error(` ${change.reason}`);
|
|
21409
22860
|
}
|
|
22861
|
+
const convertible = unsupportedChanges.filter(({ tableName, fieldName }) => fieldName !== void 0 && canConvertField({
|
|
22862
|
+
previous: previousSnapshot,
|
|
22863
|
+
current: currentSnapshot,
|
|
22864
|
+
tableName,
|
|
22865
|
+
fieldName
|
|
22866
|
+
}));
|
|
22867
|
+
if (convertible.length > 0) {
|
|
22868
|
+
logger.newline();
|
|
22869
|
+
logger.info("Convert these fields through a temporary field with:");
|
|
22870
|
+
for (const { tableName, fieldName } of convertible) logger.info(` --expand-contract "${tableName}.${fieldName}"`);
|
|
22871
|
+
}
|
|
21410
22872
|
if (unsupportedChanges.some((change) => change.showThreeStepHint)) {
|
|
21411
22873
|
logger.newline();
|
|
21412
22874
|
for (const line of getUnsupportedMigrationHintLines()) logger.info(line);
|
|
21413
22875
|
}
|
|
21414
|
-
const details = unsupportedChanges.map((c) => ` - ${c.
|
|
22876
|
+
const details = unsupportedChanges.map((c) => ` - ${c.tableName}.${c.fieldName}: ${c.reason}`).join("\n");
|
|
21415
22877
|
throw new Error(`Unsupported schema changes detected:\n${details}`);
|
|
21416
22878
|
}
|
|
21417
22879
|
if (diff.hasBreakingChanges) {
|
|
@@ -21432,6 +22894,17 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
|
|
|
21432
22894
|
logger.newline();
|
|
21433
22895
|
logger.warn(formatWarnings(diff.warnings));
|
|
21434
22896
|
}
|
|
22897
|
+
if (expandPlans.length > 0) {
|
|
22898
|
+
await generateExpandContractMigrations({
|
|
22899
|
+
previousSnapshot,
|
|
22900
|
+
currentSnapshot,
|
|
22901
|
+
resolvedDiff: diff,
|
|
22902
|
+
plans: expandPlans,
|
|
22903
|
+
migrationsDir,
|
|
22904
|
+
description: options.name
|
|
22905
|
+
});
|
|
22906
|
+
return;
|
|
22907
|
+
}
|
|
21435
22908
|
const result = await generateDiffFiles(diff, migrationsDir, getNextMigrationNumber(migrationsDir), previousSnapshot, options.name);
|
|
21436
22909
|
logger.success(`Generated migration ${styles.bold(formatMigrationNumber(result.migrationNumber))}`);
|
|
21437
22910
|
logger.info(` Diff file: ${result.diffFilePath}`);
|
|
@@ -21464,6 +22937,46 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
|
|
|
21464
22937
|
});
|
|
21465
22938
|
}
|
|
21466
22939
|
/**
|
|
22940
|
+
* Write the two migrations that carry a field type change: one that converts
|
|
22941
|
+
* values into a temporary field, and one that renames it back.
|
|
22942
|
+
* @param input - Snapshots, confirmed plans, and output location
|
|
22943
|
+
* @returns {Promise<void>} Promise that resolves when both migrations are written
|
|
22944
|
+
*/
|
|
22945
|
+
async function generateExpandContractMigrations(input) {
|
|
22946
|
+
const { previousSnapshot, currentSnapshot, resolvedDiff, plans, migrationsDir, description } = input;
|
|
22947
|
+
const intermediateSnapshot = buildIntermediateSnapshot(previousSnapshot, plans);
|
|
22948
|
+
const expandDiff = buildExpandDiff(previousSnapshot, intermediateSnapshot, plans);
|
|
22949
|
+
const confirmedFieldRenames = resolvedDiff.changes.filter((change) => change.kind === "field_renamed").map(({ tableName, previousFieldName, fieldName }) => ({
|
|
22950
|
+
tableName,
|
|
22951
|
+
previousFieldName,
|
|
22952
|
+
fieldName
|
|
22953
|
+
}));
|
|
22954
|
+
const confirmedTypeRenames = resolvedDiff.changes.filter((change) => change.kind === "table_renamed").map(({ previousTableName, tableName }) => ({
|
|
22955
|
+
previousTableName,
|
|
22956
|
+
tableName
|
|
22957
|
+
}));
|
|
22958
|
+
const contractDiff = compareSnapshots(intermediateSnapshot, currentSnapshot, {
|
|
22959
|
+
fieldRenames: [...confirmedFieldRenames, ...plans.map((plan) => ({
|
|
22960
|
+
tableName: plan.tableName,
|
|
22961
|
+
previousFieldName: plan.tempFieldName,
|
|
22962
|
+
fieldName: plan.fieldName
|
|
22963
|
+
}))],
|
|
22964
|
+
typeRenames: confirmedTypeRenames
|
|
22965
|
+
});
|
|
22966
|
+
const expandNumber = getNextMigrationNumber(migrationsDir);
|
|
22967
|
+
if (expandNumber + 1 > 9999) throw new Error(`Converting a field type needs two migration numbers, and ${formatMigrationNumber(MAX_MIGRATION_NUMBER)} is the last one available. Re-baseline the history first.`);
|
|
22968
|
+
const expand = await generateDiffFiles(expandDiff, migrationsDir, expandNumber, previousSnapshot, description, plans);
|
|
22969
|
+
const contract = await generateDiffFiles(contractDiff, migrationsDir, expandNumber + 1, intermediateSnapshot, description);
|
|
22970
|
+
const fields = plans.map((plan) => `${plan.tableName}.${plan.fieldName}`).join(", ");
|
|
22971
|
+
logger.success(`Generated migrations ${styles.bold(formatMigrationNumber(expand.migrationNumber))} and ${styles.bold(formatMigrationNumber(contract.migrationNumber))} to convert ${fields}`);
|
|
22972
|
+
logger.info(` Diff files: ${expand.diffFilePath}, ${contract.diffFilePath}`);
|
|
22973
|
+
if (expand.migrateFilePath) logger.info(` Conversion script: ${expand.migrateFilePath}`);
|
|
22974
|
+
if (contract.migrateFilePath) logger.info(` Copy script: ${contract.migrateFilePath}`);
|
|
22975
|
+
logger.newline();
|
|
22976
|
+
logger.info(`Edit the conversion in ${formatMigrationNumber(expand.migrationNumber)} before deploying. The copy script in ${formatMigrationNumber(contract.migrationNumber)} is complete, though that migration also carries any other change in this run.`, { mode: "plain" });
|
|
22977
|
+
logger.info("Both migrations are applied by 'tailor deploy'.", { mode: "plain" });
|
|
22978
|
+
}
|
|
22979
|
+
/**
|
|
21467
22980
|
* Offer to record a --no-script acknowledgment for a warning-only migration,
|
|
21468
22981
|
* or print the follow-up commands when the session is non-interactive
|
|
21469
22982
|
* @param {AcknowledgeWarningsOptions} options - Target migration and prompt behavior
|
|
@@ -21517,14 +23030,20 @@ const generateCommand = defineAppCommand({
|
|
|
21517
23030
|
alias: "n",
|
|
21518
23031
|
description: "Optional description for the migration"
|
|
21519
23032
|
}),
|
|
21520
|
-
init: arg(z.boolean().default(false), { description: "Delete existing migrations and start fresh" })
|
|
23033
|
+
init: arg(z.boolean().default(false), { description: "Delete existing migrations and start fresh" }),
|
|
23034
|
+
rename: arg(z.array(z.string()).optional(), { description: "Record a field or type rename instead of remove + add (format: \"Type.oldField:newField\" or \"OldType:NewType\"; repeatable). Renames require a migration script that copies the data." }),
|
|
23035
|
+
drop: arg(z.array(z.string()).optional(), { description: "Confirm that a removed field or type is a genuine removal, not a rename (format: \"Type.field\" or \"Type\"; repeatable). Required in non-interactive runs for a removal with rename candidates." }),
|
|
23036
|
+
"expand-contract": arg(z.array(z.string()).optional(), { description: "Convert a field type through a temporary field (format: \"Type.field\"; repeatable). Generates two migrations." })
|
|
21521
23037
|
}),
|
|
21522
23038
|
run: async (args) => {
|
|
21523
23039
|
await generate({
|
|
21524
23040
|
configPath: args.config,
|
|
21525
23041
|
name: args.name,
|
|
21526
23042
|
yes: args.yes,
|
|
21527
|
-
init: args.init
|
|
23043
|
+
init: args.init,
|
|
23044
|
+
renames: args.rename,
|
|
23045
|
+
drops: args.drop,
|
|
23046
|
+
expandContracts: args["expand-contract"]
|
|
21528
23047
|
});
|
|
21529
23048
|
}
|
|
21530
23049
|
});
|
|
@@ -23942,11 +25461,15 @@ function errorToJson(error, options) {
|
|
|
23942
25461
|
...options?.includeStack && error.stack ? { stack: error.stack } : {}
|
|
23943
25462
|
} };
|
|
23944
25463
|
}
|
|
23945
|
-
if (error instanceof Error)
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
|
|
25464
|
+
if (error instanceof Error) {
|
|
25465
|
+
const suggestion = typeOnlyImportHint(error);
|
|
25466
|
+
return { error: {
|
|
25467
|
+
code: error.name === "CIPromptError" ? "INTERACTIVE_PROMPT_REQUIRED" : "UNEXPECTED_ERROR",
|
|
25468
|
+
message: error.message,
|
|
25469
|
+
...suggestion ? { suggestion } : {},
|
|
25470
|
+
...options?.includeStack && error.stack ? { stack: error.stack } : {}
|
|
25471
|
+
} };
|
|
25472
|
+
}
|
|
23950
25473
|
return { error: {
|
|
23951
25474
|
code: "UNKNOWN_ERROR",
|
|
23952
25475
|
message: String(error)
|
|
@@ -24016,5 +25539,5 @@ async function registerTsHook(tsHookUrl) {
|
|
|
24016
25539
|
}
|
|
24017
25540
|
|
|
24018
25541
|
//#endregion
|
|
24019
|
-
export { treeCommand as $,
|
|
24020
|
-
//# sourceMappingURL=register-ts-hook-
|
|
25542
|
+
export { treeCommand as $, sanitizeMigrationLabel as $n, listWorkspaces as $t, truncate as A, MIGRATION_FILE_NAMES as An, readPlatformConfig as Ar, triggerExecutor as At, MIGRATION_REVIEW_REQUIRED_MARKER as B, getLatestMigrationNumber as Bn, deploymentArgs as Br, getCommand$5 as Bt, healthCommand as C, captureFileState as Cn, loadConfigPath as Cr, listCommand$7 as Ct, resumeWorkflow as D, INITIAL_SCHEMA_NUMBER as Dn, loadStoredUserTokens as Dr, listWebhookExecutors as Dt, resumeCommand as E, DIFF_FILE_NAME as En, loadPlatformClientConfig as Er, getFunctionRegistry as Et, chunkSeedData as F, assertValidMigrationFiles as Fn, writePlatformConfig as Fr, jobsCommand as Ft, showCommand as G, loadDiff as Gn, resolveMachineUserInputSource as Gr, functionExecutionStatusToString as Gt, deployCommand as H, getMigrationFilePath as Hn, multiConfigArg as Hr, executionsCommand as Ht, bundleSeedScript as I, compareLocalTypesWithSnapshot as In, defineAppCommand as Ir, listExecutorJobs as It, remove as J, reconstructSnapshotFromMigrations as Jn, deploy as Jt, extractOwnedNamespaces as K, loadSnapshot as Kn, toPageDirection as Kr, getCommand$6 as Kt, generate as L, compareSnapshots as Ln, commonArgs as Lr, watchExecutorJob as Lt, loadSeedContext as M, SCHEMA_FILE_NAME as Mn, resolveConfigUser as Mr, listExecutors as Mt, loadApplicationNamespaces as N, UnsupportedMigrationFileVersionError as Nn, resolveUserTokenKey as Nr, getExecutorJob as Nt, listCommand$2 as O, MIGRATE_FILE_NAME as On, loadWorkspaceId as Or, webhookCommand as Ot, loadTailorDBNamespaces as P, assertMigrationNumberExists as Pn, saveUserTokens as Pr, getExecutorWaitFailureMessage as Pt, organizationTree as Q, handleOptionalToRequiredError as Qn, listCommand$9 as Qt, generateCommand as R, createSnapshotFromLocalTypes as Rn, configArg as Rr, startCommand as Rt, getAppHealth as S, protoGqlPermission as Sn, loadAccessToken as Sr, generate$1 as St, waitWorkflowExecution as T, DB_TYPES_FILE_NAME as Tn, loadMachineUserName as Tr, getCommand$4 as Tt, deployStaticWebsite as U, getMigrationFiles as Un, pagedLogArgs as Ur, getWorkflowExecution as Ut, generateSchemaFile as V, getMigrationDirPath as Vn, isVerbose as Vr, getWorkflow as Vt, show as W, getNextMigrationNumber as Wn, paginationArgs as Wr, listWorkflowExecutions as Wt, updateCommand$1 as X, MIGRATION_LABEL_KEY as Xn, deployMigrationTestBaseline as Xt, removeCommand$1 as Y, MIGRATION_HISTORY_LABEL_KEY as Yn, deployFromCLI as Yt, updateOrganization as Z, createMigrationHistoryId as Zn, deployMigrationTestTarget as Zt, getWorkspace as _, verifyRemoteSchema as _n, loadConfig as _r, getOAuth2Client as _t, queryCommand as a, ensureConfigId as an, resourceTrn as ar, updateFolder as at, listApps as b, compareSnapshotWithRemote as bn, hasAnyUserTokenEntry as br, listCommand$6 as bt, removeCommand as c, executeScript as cn, sdkVersionLabelKey as cr, getCommand$2 as ct, listUsers as d, bundleMigrationScript as dn, generateUserTypes as dr, deleteFolder as dt, createCommand$1 as en, formatDiffSummary as er, listCommand$3 as et, inviteCommand as f, checkMigrationDiffs as fn, formatMigrationNumber as fr, createCommand as ft, getCommand as g, toTailorDBDeployInput as gn, assertWritable as gr, getCommand$3 as gt, restoreWorkspace as h, logRemoteDriftGuidance as hn, apiCall as hr, listOAuth2Clients as ht, query as i, workspaceNameSchema as in, selectTargetNamespace as ir, updateCommand$2 as it, truncateCommand as j, MIGRATION_NUMBER_PATTERN as jn, removeLegacyUserAlias as jr, listCommand$8 as jt, listWorkflows as k, MIGRATE_TEST_FILE_NAME as kn, platformConfigFromProfile as kr, triggerCommand as kt, removeUser as l, waitForExecution as ln, writeMetadataLabels as lr, getFolder as lt, restoreCommand as m, formatRemoteVerificationResults as mn, prompt as mr, listCommand$5 as mt, errorToJson as n, createWorkspace$1 as nn, hasChanges as nr, getCommand$1 as nt, updateCommand as o, getMigrationMachineUser as on, sdkAppIdLabelKey as or, listCommand$4 as ot, inviteUser as p, fetchRemoteSchemaSnapshot as pn, parseMigrationNumberArg as pr, createFolder as pt, logBetaWarning as q, normalizeSchemaSnapshot as qn, workspaceArgs as qr, getExecutor as qt, serializeError as r, formatKeyValueTable as rn, getNamespacesWithMigrations as rr, getOrganization as rt, updateUser as s, updateMigrationLabel as sn, sdkNameLabelKey as sr, listFolders as st, registerTsHook as t, createValidatedWorkspaceWithClient as tn, formatMigrationDiff as tr, listOrganizations as tt, listCommand as u, formatMigrationScriptCommand as un, PluginManager as ur, deleteCommand$1 as ut, deleteCommand as v, fetchRemoteMigrationNumber as vn, deleteUserTokens as vr, getMachineUserToken as vt, waitCommand as w, captureMigrationFileState as wn, loadConsoleBaseUrl as wr, listFunctionRegistries as wt, listCommand$1 as x, generateAllTypeManifestsFromSnapshot as xn, hasUserTokenEntry as xr, listMachineUsers as xt, deleteWorkspace as y, fetchRemoteMigrationState as yn, fetchLatestToken as yr, tokenCommand as yt, scriptCommand as z, formatSchemaDrifts as zn, confirmationArgs as zr, startWorkflow as zt };
|
|
25543
|
+
//# sourceMappingURL=register-ts-hook-ClI226n2.mjs.map
|