@tailor-platform/sdk 2.0.0-next.10 → 2.0.0-next.11
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 +165 -0
- package/dist/{application-miwiXtB4.mjs → application-CM6hKnSK.mjs} +312 -89
- package/dist/application-CM6hKnSK.mjs.map +1 -0
- package/dist/application-CXNaUNhv.mjs +3 -0
- package/dist/cli/commands/deploy/aigateway.d.mts +1 -1
- package/dist/cli/commands/deploy/application.d.mts +1 -1
- package/dist/cli/commands/deploy/auth.d.mts +1 -1
- package/dist/cli/commands/deploy/deploy.d.mts +1 -0
- package/dist/cli/commands/deploy/executor.d.mts +1 -1
- package/dist/cli/commands/deploy/function-registry.d.mts +1 -2
- package/dist/cli/commands/deploy/idp.d.mts +1 -1
- package/dist/cli/commands/deploy/label.d.mts +2 -0
- package/dist/cli/commands/deploy/resolver.d.mts +1 -1
- package/dist/cli/commands/deploy/staticwebsite.d.mts +1 -1
- package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
- package/dist/cli/commands/deploy/types.d.mts +2 -1
- package/dist/cli/commands/deploy/workflow-execution-policy.d.mts +1 -2
- package/dist/cli/commands/deploy/workflow.d.mts +1 -1
- package/dist/cli/commands/executor/get.d.mts +1 -10
- package/dist/cli/commands/executor/jobs.d.mts +1 -38
- package/dist/cli/commands/executor/trigger.d.mts +1 -11
- package/dist/cli/commands/generate/seed/bundler.d.mts +2 -1
- package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +5 -0
- package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +1 -1
- package/dist/cli/commands/workflow/executions.d.mts +1 -13
- package/dist/cli/commands/workflow/get.d.mts +1 -10
- package/dist/cli/commands/workflow/start.d.mts +2 -14
- package/dist/cli/lib.d.mts +7 -11
- package/dist/cli/lib.mjs +99 -42
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/cli/main.mjs +318 -124
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/services/http-adapter/bundler.d.mts +1 -0
- package/dist/cli/services/workflow/bundler.d.mts +2 -1
- package/dist/cli/shared/client.d.mts +28 -3
- package/dist/cli/shared/config-loader.d.mts +11 -4
- package/dist/cli/shared/tsconfig-paths-plugin.d.mts +2 -0
- package/dist/cli/shared/type-generator.d.mts +4 -3
- package/dist/completion/zsh-worker.zsh +82 -22
- package/dist/configure/config/types.d.mts +26 -3
- package/dist/configure/index.d.mts +2 -2
- package/dist/configure/index.mjs +171 -3
- package/dist/configure/index.mjs.map +1 -1
- package/dist/configure/services/executor/index.d.mts +2 -2
- package/dist/configure/services/executor/trigger/event.d.mts +245 -2
- package/dist/configure/services/executor/trigger/index.d.mts +3 -3
- package/dist/configure/services/idp/index.d.mts +1 -1
- package/dist/configure/services/index.d.mts +2 -2
- package/dist/configure/services/tailordb/index.d.mts +0 -1
- package/dist/configure/services/workflow/execution-policy.d.mts +1 -1
- package/dist/configure/services/workflow/execution-policy.types.d.mts +1 -1
- package/dist/configure/services/workflow/job.d.mts +11 -2
- package/dist/configure/services/workflow/workflow.d.mts +10 -0
- package/dist/{globals-DQotNuwK.mjs → globals-TfAVItuK.mjs} +1 -2
- package/dist/globals-TfAVItuK.mjs.map +1 -0
- package/dist/node-builtins-CmaL2Cbq.mjs +36 -0
- package/dist/node-builtins-CmaL2Cbq.mjs.map +1 -0
- package/dist/plugin/builtin/enum-constants/index.mjs +115 -2
- package/dist/plugin/builtin/enum-constants/index.mjs.map +1 -0
- package/dist/plugin/builtin/file-utils/index.mjs +149 -2
- package/dist/plugin/builtin/file-utils/index.mjs.map +1 -0
- package/dist/plugin/builtin/kysely-type/index.mjs +274 -2
- package/dist/plugin/builtin/kysely-type/index.mjs.map +1 -0
- package/dist/plugin/builtin/seed/index.mjs +1 -1
- package/dist/plugin/builtin/seed/seed-type-processor.d.mts +2 -0
- package/dist/{register-ts-hook-BAHMwxfG.mjs → register-ts-hook-LYV7zH-e.mjs} +2252 -797
- package/dist/register-ts-hook-LYV7zH-e.mjs.map +1 -0
- package/dist/registry-BIGVUrMB.mjs.map +1 -1
- package/dist/runtime/index.mjs +1 -1
- package/dist/runtime/workflow.d.mts +2 -20
- package/dist/runtime/workflow.mjs +1 -1
- package/dist/{seed-LUVr7jHL.mjs → seed-B-yckgQY.mjs} +42 -3
- package/dist/seed-B-yckgQY.mjs.map +1 -0
- package/dist/service-M7OYziQP.mjs +3 -0
- package/dist/{service-BNtPsK9Q.mjs → service-k1xy_A5G.mjs} +29 -2
- package/dist/{service-BNtPsK9Q.mjs.map → service-k1xy_A5G.mjs.map} +1 -1
- package/dist/{service_pb-BwZRiNug.mjs → service_pb-D5--My_M.mjs} +5 -5
- package/dist/service_pb-D5--My_M.mjs.map +1 -0
- package/dist/{service_pb-wRkxUta6.mjs → service_pb-i8CmnQP4.mjs} +1 -1
- package/dist/tailor-proto/src/tailor/v1/application_pb.d.mts +106 -1
- package/dist/tailor-proto/src/tailor/v1/executor_resource_pb.d.mts +24 -1
- package/dist/tailor-proto/src/tailor/v1/function_pb.d.mts +49 -1
- package/dist/tailor-proto/src/tailor/v1/service_pb.d.mts +89 -6
- package/dist/tailor-proto/src/tailor/v1/telemetryrouter_resource_pb.d.mts +26 -1
- package/dist/tailor-proto/src/tailor/v1/workflow_pb.d.mts +101 -1
- package/dist/tailor-proto/src/tailor/v1/workflow_resource_pb.d.mts +22 -1
- package/dist/types/executor.generated.d.mts +23 -3
- package/dist/types/idp.generated.d.mts +1 -1
- package/dist/utils/test/index.d.mts +1 -2
- package/dist/utils/test/index.mjs +1 -173
- package/dist/utils/test/index.mjs.map +1 -1
- package/dist/vitest/environment.mjs +1 -1
- package/dist/vitest/index.d.mts +2 -2
- package/dist/vitest/index.mjs +10 -52
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mocks/workflow.d.mts +0 -9
- package/dist/vitest/setup.mjs +1 -1
- package/dist/{workflow-CsaQ9qK-.mjs → workflow-Bamae_Yc.mjs} +1 -9
- package/dist/workflow-Bamae_Yc.mjs.map +1 -0
- package/dist/{workspace_resource_pb-UGK1SSn_.mjs → workspace_resource_pb-DXnO2AT-.mjs} +7 -7
- package/dist/workspace_resource_pb-DXnO2AT-.mjs.map +1 -0
- package/docs/cli/tailordb.md +39 -7
- package/docs/cli-reference.md +11 -10
- package/docs/configuration.md +50 -0
- package/docs/migration/v2.md +306 -9
- package/docs/runtime.md +1 -1
- package/docs/services/executor.md +83 -3
- package/docs/services/idp.md +8 -6
- package/docs/services/resolver.md +6 -4
- package/docs/services/tailordb-migration.md +17 -6
- package/docs/services/tailordb.md +6 -4
- package/docs/services/workflow.md +33 -3
- package/package.json +19 -17
- package/dist/application-DArFTTJ9.mjs +0 -3
- package/dist/application-miwiXtB4.mjs.map +0 -1
- package/dist/enum-constants-j9QBF0cB.mjs +0 -116
- package/dist/enum-constants-j9QBF0cB.mjs.map +0 -1
- package/dist/file-utils-yEiIS4nJ.mjs +0 -150
- package/dist/file-utils-yEiIS4nJ.mjs.map +0 -1
- package/dist/globals-DQotNuwK.mjs.map +0 -1
- package/dist/kysely-type-DqIRgDYR.mjs +0 -275
- package/dist/kysely-type-DqIRgDYR.mjs.map +0 -1
- package/dist/register-ts-hook-BAHMwxfG.mjs.map +0 -1
- package/dist/seed-LUVr7jHL.mjs.map +0 -1
- package/dist/service-D5jn05ZI.mjs +0 -3
- package/dist/service_pb-BwZRiNug.mjs.map +0 -1
- package/dist/utils/test/mock.d.mts +0 -86
- package/dist/workflow-CsaQ9qK-.mjs.map +0 -1
- package/dist/workspace_resource_pb-UGK1SSn_.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as db } from "./schema--xYWRGfe.mjs";
|
|
2
|
-
import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as Subgraph_ServiceType, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, Ft as CreateApplicationRequestSchema, H as IdPPermissionPermit, Ht as UpdateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, Jt as FilterSchema, K as CreateExecutorExecutorRequestSchema, Kt as ConditionSchema, L as CreateIdPServiceRequestSchema, Lt as UpdateApplicationRequestSchema, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, V as IdPPermissionOperator, Vt as CreateAIGatewayRequestSchema, X as ExecutorTargetType, Y as ExecutorJobStatus, Yt as PageDirection, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as Condition_Operator, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status, zt as ApplicationSchemaUpdateAttemptStatus } from "./workspace_resource_pb-
|
|
2
|
+
import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as Subgraph_ServiceType, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, Ft as CreateApplicationRequestSchema, H as IdPPermissionPermit, Ht as UpdateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, Jt as FilterSchema, K as CreateExecutorExecutorRequestSchema, Kt as ConditionSchema, L as CreateIdPServiceRequestSchema, Lt as UpdateApplicationRequestSchema, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, V as IdPPermissionOperator, Vt as CreateAIGatewayRequestSchema, X as ExecutorTargetType, Y as ExecutorJobStatus, Yt as PageDirection, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as Condition_Operator, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status, zt as ApplicationSchemaUpdateAttemptStatus } from "./workspace_resource_pb-DXnO2AT-.mjs";
|
|
3
3
|
import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
|
|
4
4
|
import { a as parseBoolean, i as symbols, n as logger, o as renderTable, r as styles, t as CIPromptError } from "./logger-CXQq9YIp.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-
|
|
5
|
+
import { $ as LOG_LEVELS, A as hashContent, B as getConsoleBaseUrl, D as hasGenerationHooks, E as getPluginGenerationDependencies, F as fetchAllTolerant, G as initOperatorClient, H as getOrNull, I as fetchMachineUserToken, J as normalizeBaseUrl, K as isDefaultPlatform, L as fetchPaged, N as defaultPlatformBaseUrl, O as createBundleCache, P as fetchAll, Q as createApplyLimiter, T as createBundleLog, U as getPlatformBaseUrl, V as getOAuth2ClientId, W as initOAuth2Client, X as resolveStaticWebsiteUrls, Y as rememberPlatformConfigForToken, Z as byName, _ as stringifyFunction, b as resolveTSConfigWithFallback, f as buildExecutorArgsExpr, g as assertUniqueTailorDBTypeNamesWithExternal, h as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as hashFile, k as getDistDir, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildResolverOperationHookExpr, q as isNotFoundError, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, u as HTTP_METHODS, v as createGeneratedEntryResolverPlugin, x as platformBundleDefinePlugin, y as createTsconfigPathsPlugin, z as fetchUserInfo } from "./application-CM6hKnSK.mjs";
|
|
6
|
+
import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-k1xy_A5G.mjs";
|
|
7
7
|
import { t as multiline } from "./multiline-sfHpTZZK.mjs";
|
|
8
8
|
import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
|
|
9
9
|
import { t as userAgent } from "./user-agent-Bgsszb5I.mjs";
|
|
@@ -315,6 +315,11 @@ const envValueSchema = z.union([
|
|
|
315
315
|
z.number(),
|
|
316
316
|
z.boolean()
|
|
317
317
|
]);
|
|
318
|
+
const allowedSecretValueSchema = z.union([z.string(), z.number()]);
|
|
319
|
+
const envEntrySchema = z.union([envValueSchema, z.strictObject({
|
|
320
|
+
value: allowedSecretValueSchema,
|
|
321
|
+
allowSecretReason: z.string().min(1, { message: "'allowSecretReason' must state why the value is safe to keep in 'env'." })
|
|
322
|
+
})]);
|
|
318
323
|
const LogLevelSchema = z.enum(LOG_LEVELS);
|
|
319
324
|
const logLevelSchema = z.string().refine((value) => LogLevelSchema.safeParse(value.trim().toUpperCase()).success, { message: `'logLevel' must be one of: ${LOG_LEVELS.join(", ")}.` });
|
|
320
325
|
/**
|
|
@@ -331,7 +336,7 @@ const logLevelSchema = z.string().refine((value) => LogLevelSchema.safeParse(val
|
|
|
331
336
|
const AppConfigSchema = z.strictObject({
|
|
332
337
|
id: z.uuid({ message: "'id' must be a UUID." }).optional(),
|
|
333
338
|
name: z.string().min(1, { message: "'name' must be a non-empty string." }),
|
|
334
|
-
env: z.record(z.string(),
|
|
339
|
+
env: z.record(z.string(), envEntrySchema).optional(),
|
|
335
340
|
cors: z.array(z.string()).optional(),
|
|
336
341
|
allowedIpAddresses: z.array(z.string()).optional(),
|
|
337
342
|
disableIntrospection: z.boolean().optional(),
|
|
@@ -1105,6 +1110,213 @@ function loadConfigPath(configPath) {
|
|
|
1105
1110
|
return findUpSync(DEFAULT_CONFIG_FILENAME);
|
|
1106
1111
|
}
|
|
1107
1112
|
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/cli/shared/env-secret-scan.ts
|
|
1115
|
+
const AWS_RULE_ID = "@secretlint/secretlint-rule-aws";
|
|
1116
|
+
const RULE_ID_PREFIX = "@secretlint/secretlint-rule-";
|
|
1117
|
+
/**
|
|
1118
|
+
* Honors `secretlint-disable` comments found in the scanned text. `env` values
|
|
1119
|
+
* are author-controlled text, so leaving this rule in let a value act as a
|
|
1120
|
+
* directive and suppress the findings of the entries after it — defeating the
|
|
1121
|
+
* check without the explicit allowance.
|
|
1122
|
+
*
|
|
1123
|
+
* Matched by id on purpose: the rule declares itself a `scanner`, so it cannot
|
|
1124
|
+
* be told apart by `meta.type`, and the preset's `disabled` option has no
|
|
1125
|
+
* effect on it.
|
|
1126
|
+
*/
|
|
1127
|
+
const COMMENT_DIRECTIVE_RULE_ID = "@secretlint/secretlint-rule-filter-comments";
|
|
1128
|
+
const HIGH_ENTROPY_DETECTOR = "high-entropy";
|
|
1129
|
+
/**
|
|
1130
|
+
* The scanned document is assembled in memory, so the path only labels the
|
|
1131
|
+
* source for the scanner and never reaches the filesystem.
|
|
1132
|
+
*/
|
|
1133
|
+
const VIRTUAL_SOURCE_PATH = "/tailor-config-env";
|
|
1134
|
+
const ENTROPY_MIN_LENGTH = 20;
|
|
1135
|
+
/**
|
|
1136
|
+
* Bits per character. Above 4.0 no hex-only string can reach the threshold,
|
|
1137
|
+
* which keeps commit hashes, UUIDs and slugs out of the heuristic while still
|
|
1138
|
+
* catching base64/base62 credentials.
|
|
1139
|
+
*/
|
|
1140
|
+
const ENTROPY_MIN_BITS_PER_CHAR = 4.2;
|
|
1141
|
+
const TOKEN_LIKE_VALUE = /^[A-Za-z0-9+/=_.-]+$/;
|
|
1142
|
+
/**
|
|
1143
|
+
* A single command can load the same config repeatedly — `setup generate`
|
|
1144
|
+
* derives five separate answers, each through its own `loadConfig` — so results
|
|
1145
|
+
* are memoized on the entries scanned. Without this the scanner would run, and
|
|
1146
|
+
* warnings would print, once per load.
|
|
1147
|
+
*
|
|
1148
|
+
* Keys are digests rather than the serialized entries, so the cache does not
|
|
1149
|
+
* hold the values it was asked to judge for the life of the process.
|
|
1150
|
+
*/
|
|
1151
|
+
const scans = /* @__PURE__ */ new Map();
|
|
1152
|
+
/**
|
|
1153
|
+
* Resolve an `env` entry to the value that gets deployed.
|
|
1154
|
+
* @param entry - Entry as written in the config
|
|
1155
|
+
* @returns The entry's value, unwrapped when it carries an `allowSecretReason`
|
|
1156
|
+
*/
|
|
1157
|
+
function resolveEnvValue(entry) {
|
|
1158
|
+
return isAllowedSecret(entry) ? entry.value : entry;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Scan `env` values for credentials.
|
|
1162
|
+
*
|
|
1163
|
+
* Entries wrapped as `{ value, allowSecretReason }` are skipped.
|
|
1164
|
+
* Provider-specific matches are reported as errors; values that merely look
|
|
1165
|
+
* randomly generated are reported as warnings.
|
|
1166
|
+
* @param input - `env` entries as written in the config
|
|
1167
|
+
* @returns Provider matches first, then entropy warnings; empty when nothing matched
|
|
1168
|
+
*/
|
|
1169
|
+
async function scanEnvForSecrets(input) {
|
|
1170
|
+
const entries = Object.entries(input.env ?? {}).filter(([, entry]) => !isAllowedSecret(entry)).map(([key, entry]) => [key, resolveEnvValue(entry)]);
|
|
1171
|
+
if (entries.length === 0) return [];
|
|
1172
|
+
const findings = await scanWithProviderRules(entries);
|
|
1173
|
+
const alreadyFound = new Set(findings.map((finding) => finding.key));
|
|
1174
|
+
for (const [key, value] of entries) {
|
|
1175
|
+
if (alreadyFound.has(key) || typeof value !== "string") continue;
|
|
1176
|
+
if (looksRandomlyGenerated(value)) findings.push({
|
|
1177
|
+
key,
|
|
1178
|
+
detector: HIGH_ENTROPY_DETECTOR,
|
|
1179
|
+
severity: "warning"
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return findings;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Report secret-looking `env` values, failing when a credential is identified.
|
|
1186
|
+
*
|
|
1187
|
+
* Warnings are logged and do not fail the command. Repeated calls for the same
|
|
1188
|
+
* entries reuse the first result.
|
|
1189
|
+
* @param input - `env` entries as written in the config
|
|
1190
|
+
* @returns Promise that resolves when the entries carry no credential
|
|
1191
|
+
* @throws When a value is identified as a credential
|
|
1192
|
+
*/
|
|
1193
|
+
async function assertEnvHasNoSecrets(input) {
|
|
1194
|
+
const key = createHash("sha256").update(JSON.stringify([input.configPath ?? "", input.env ?? {}])).digest("hex");
|
|
1195
|
+
const pending = scans.get(key);
|
|
1196
|
+
if (pending) return pending;
|
|
1197
|
+
const scan = reportEnvSecrets(input);
|
|
1198
|
+
scans.set(key, scan);
|
|
1199
|
+
return scan;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Log warnings and throw on credentials found in `env`.
|
|
1203
|
+
* @param input - `env` entries as written in the config
|
|
1204
|
+
* @throws When a value is identified as a credential
|
|
1205
|
+
*/
|
|
1206
|
+
async function reportEnvSecrets(input) {
|
|
1207
|
+
const findings = await scanEnvForSecrets(input);
|
|
1208
|
+
for (const finding of findings) {
|
|
1209
|
+
if (finding.severity !== "warning") continue;
|
|
1210
|
+
logger.warn(`env.${finding.key} looks like a randomly generated credential. If it is one, move it to Secret Manager; otherwise allow it with ${styles.bold("allowSecretReason")}.`);
|
|
1211
|
+
}
|
|
1212
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
1213
|
+
if (errors.length === 0) return;
|
|
1214
|
+
const location = input.configPath ? ` in ${input.configPath}` : "";
|
|
1215
|
+
const list = errors.map((error) => {
|
|
1216
|
+
const matched = error.rule ? `${error.detector}: ${error.rule}` : error.detector;
|
|
1217
|
+
const reference = error.docsUrl ? `\n ${error.docsUrl}` : "";
|
|
1218
|
+
return ` - env.${error.key} (matched ${matched})${reference}`;
|
|
1219
|
+
}).join("\n");
|
|
1220
|
+
const example = errors[0]?.key ?? "KEY";
|
|
1221
|
+
throw new Error(`Secret detected in 'env'${location}:\n${list}\n'env' values are deployed as plaintext and are readable by anyone who can read the application's configuration. Define these with defineSecretManager() instead, and read them through Secret Manager at runtime.
|
|
1222
|
+
If a value is genuinely safe to keep in 'env', allow it where it is defined: ${example}: { value: ..., allowSecretReason: "<why this is safe>" }`);
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Run the provider rule set over all entries at once.
|
|
1226
|
+
*
|
|
1227
|
+
* Entries are scanned as one `KEY=value` document because some rules only match
|
|
1228
|
+
* when the key name accompanies the value (`AWS_SECRET_ACCESS_KEY`, database
|
|
1229
|
+
* connection strings). Each finding is mapped back to its key through the
|
|
1230
|
+
* character range the entry occupies, which stays correct for multi-line values.
|
|
1231
|
+
* @param entries - `env` entries to scan
|
|
1232
|
+
* @returns Error-severity findings, deduplicated per key and rule
|
|
1233
|
+
*/
|
|
1234
|
+
async function scanWithProviderRules(entries) {
|
|
1235
|
+
const [{ lintSource }, { rules }] = await Promise.all([import("@secretlint/core"), import("@secretlint/secretlint-rule-preset-recommend")]);
|
|
1236
|
+
const ranges = [];
|
|
1237
|
+
let content = "";
|
|
1238
|
+
for (const [key, value] of entries) {
|
|
1239
|
+
const line = `${key}=${String(value)}`;
|
|
1240
|
+
ranges.push({
|
|
1241
|
+
key,
|
|
1242
|
+
start: content.length,
|
|
1243
|
+
end: content.length + line.length
|
|
1244
|
+
});
|
|
1245
|
+
content += `${line}\n`;
|
|
1246
|
+
}
|
|
1247
|
+
const result = await lintSource({
|
|
1248
|
+
source: {
|
|
1249
|
+
filePath: VIRTUAL_SOURCE_PATH,
|
|
1250
|
+
content,
|
|
1251
|
+
contentType: "text"
|
|
1252
|
+
},
|
|
1253
|
+
options: {
|
|
1254
|
+
maskSecrets: true,
|
|
1255
|
+
noPhysicFilePath: true,
|
|
1256
|
+
config: { rules: rules.filter((rule) => rule.meta.id !== COMMENT_DIRECTIVE_RULE_ID).map((rule) => ({
|
|
1257
|
+
id: rule.meta.id,
|
|
1258
|
+
rule,
|
|
1259
|
+
...rule.meta.id === AWS_RULE_ID ? { options: { enableIDScanRule: true } } : {}
|
|
1260
|
+
})) }
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
const findings = [];
|
|
1264
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1265
|
+
for (const message of result.messages) {
|
|
1266
|
+
const entry = ranges.find((range) => message.range[0] >= range.start && message.range[0] < range.end);
|
|
1267
|
+
if (!entry) continue;
|
|
1268
|
+
const detector = message.ruleId.startsWith(RULE_ID_PREFIX) ? message.ruleId.slice(28) : message.ruleId;
|
|
1269
|
+
const dedupeKey = JSON.stringify([
|
|
1270
|
+
entry.key,
|
|
1271
|
+
detector,
|
|
1272
|
+
message.messageId
|
|
1273
|
+
]);
|
|
1274
|
+
if (seen.has(dedupeKey)) continue;
|
|
1275
|
+
seen.add(dedupeKey);
|
|
1276
|
+
findings.push({
|
|
1277
|
+
key: entry.key,
|
|
1278
|
+
detector,
|
|
1279
|
+
rule: message.messageId,
|
|
1280
|
+
...message.docsUrl ? { docsUrl: message.docsUrl } : {},
|
|
1281
|
+
severity: "error"
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return findings;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Decide whether an entry carries its own allowance.
|
|
1288
|
+
* @param entry - Entry as written in the config
|
|
1289
|
+
* @returns True when the entry is the `{ value, allowSecretReason }` form
|
|
1290
|
+
*/
|
|
1291
|
+
function isAllowedSecret(entry) {
|
|
1292
|
+
return typeof entry === "object";
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Decide whether a value carries enough randomness to look generated rather
|
|
1296
|
+
* than authored.
|
|
1297
|
+
* @param value - `env` value to inspect
|
|
1298
|
+
* @returns True when the value is long, token-shaped and high-entropy
|
|
1299
|
+
*/
|
|
1300
|
+
function looksRandomlyGenerated(value) {
|
|
1301
|
+
if (value.length < ENTROPY_MIN_LENGTH || !TOKEN_LIKE_VALUE.test(value)) return false;
|
|
1302
|
+
return shannonEntropyPerChar(value) >= ENTROPY_MIN_BITS_PER_CHAR;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Compute Shannon entropy of a string in bits per character.
|
|
1306
|
+
* @param value - String to measure
|
|
1307
|
+
* @returns Bits per character
|
|
1308
|
+
*/
|
|
1309
|
+
function shannonEntropyPerChar(value) {
|
|
1310
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const char of value) occurrences.set(char, (occurrences.get(char) ?? 0) + 1);
|
|
1312
|
+
let entropy = 0;
|
|
1313
|
+
for (const count of occurrences.values()) {
|
|
1314
|
+
const probability = count / value.length;
|
|
1315
|
+
entropy -= probability * Math.log2(probability);
|
|
1316
|
+
}
|
|
1317
|
+
return entropy;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1108
1320
|
//#endregion
|
|
1109
1321
|
//#region src/cli/shared/mock.ts
|
|
1110
1322
|
/**
|
|
@@ -1152,6 +1364,12 @@ async function loadConfig(configPath, options = {}) {
|
|
|
1152
1364
|
const issues = validated.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1153
1365
|
throw new Error(`Invalid Tailor config in ${resolvedPath}:\n${issues}`);
|
|
1154
1366
|
}
|
|
1367
|
+
const appConfig = configModule.default;
|
|
1368
|
+
await assertEnvHasNoSecrets({
|
|
1369
|
+
env: appConfig.env,
|
|
1370
|
+
configPath: resolvedPath
|
|
1371
|
+
});
|
|
1372
|
+
const env = appConfig.env ? Object.fromEntries(Object.entries(appConfig.env).map(([key, entry]) => [key, resolveEnvValue(entry)])) : void 0;
|
|
1155
1373
|
const allPlugins = [];
|
|
1156
1374
|
for (const value of Object.values(configModule)) if (Array.isArray(value)) {
|
|
1157
1375
|
const pluginParsed = value.reduce((acc, item) => {
|
|
@@ -1169,6 +1387,7 @@ async function loadConfig(configPath, options = {}) {
|
|
|
1169
1387
|
return {
|
|
1170
1388
|
config: {
|
|
1171
1389
|
...configModule.default,
|
|
1390
|
+
...env ? { env } : {},
|
|
1172
1391
|
path: resolvedPath
|
|
1173
1392
|
},
|
|
1174
1393
|
plugins: allPlugins
|
|
@@ -1456,6 +1675,83 @@ function createCacheManager(options) {
|
|
|
1456
1675
|
};
|
|
1457
1676
|
}
|
|
1458
1677
|
|
|
1678
|
+
//#endregion
|
|
1679
|
+
//#region src/cli/shared/publish-events.ts
|
|
1680
|
+
/**
|
|
1681
|
+
* How each event-publishing resource is named in user-facing messages.
|
|
1682
|
+
*
|
|
1683
|
+
* Every message naming one of these reads from here, so a conflict error and the
|
|
1684
|
+
* confirmation that lists the same resource cannot drift apart.
|
|
1685
|
+
*/
|
|
1686
|
+
const eventSourceLabel = {
|
|
1687
|
+
tailorDBType: (name) => `TailorDB type "${name}"`,
|
|
1688
|
+
resolver: (name) => `Resolver "${name}"`,
|
|
1689
|
+
idpService: (name) => `IdP service "${name}"`,
|
|
1690
|
+
workflow: (name) => `Workflow "${name}"`,
|
|
1691
|
+
workflowJob: (name) => `Job "${name}"`,
|
|
1692
|
+
workflowJobs: (workflowName) => `Jobs of workflow "${workflowName}"`
|
|
1693
|
+
};
|
|
1694
|
+
/** Opt-out conflict details per event-publishing resource. */
|
|
1695
|
+
const publishEventsConflict = {
|
|
1696
|
+
tailorDBType: (name) => ({
|
|
1697
|
+
resource: eventSourceLabel.tailorDBType(name),
|
|
1698
|
+
trigger: "record"
|
|
1699
|
+
}),
|
|
1700
|
+
resolver: (name) => ({
|
|
1701
|
+
resource: eventSourceLabel.resolver(name),
|
|
1702
|
+
trigger: "resolverExecuted"
|
|
1703
|
+
}),
|
|
1704
|
+
idpService: (name) => ({
|
|
1705
|
+
resource: eventSourceLabel.idpService(name),
|
|
1706
|
+
trigger: "idpUser"
|
|
1707
|
+
}),
|
|
1708
|
+
workflow: (name) => ({
|
|
1709
|
+
resource: eventSourceLabel.workflow(name),
|
|
1710
|
+
trigger: "workflowExecution"
|
|
1711
|
+
}),
|
|
1712
|
+
workflowJob: (name) => ({
|
|
1713
|
+
resource: eventSourceLabel.workflowJob(name),
|
|
1714
|
+
trigger: "workflowJobExecution",
|
|
1715
|
+
subscribesTo: "a workflow that runs it"
|
|
1716
|
+
})
|
|
1717
|
+
};
|
|
1718
|
+
/**
|
|
1719
|
+
* Build the error raised when a resource opts out of publishing that a
|
|
1720
|
+
* subscribing executor needs.
|
|
1721
|
+
* @param conflict - Resource, trigger, and subscription target named in the error
|
|
1722
|
+
* @returns Error message
|
|
1723
|
+
*/
|
|
1724
|
+
function publishEventsConflictError(conflict) {
|
|
1725
|
+
const { resource, trigger, subscribesTo = "it" } = conflict;
|
|
1726
|
+
return `${resource} has "publishEvents: false", but executors with ${trigger} triggers subscribe to ${subscribesTo}. Either remove "publishEvents: false" or remove the matching executor triggers.`;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Reject an opt-out that a subscribing executor contradicts.
|
|
1730
|
+
*
|
|
1731
|
+
* Separate from {@link resolvePublishEvents} so a planner can reject the whole
|
|
1732
|
+
* config before issuing any request, rather than partway through.
|
|
1733
|
+
* @param params - Declared value, subscriber presence, and conflict error details
|
|
1734
|
+
*/
|
|
1735
|
+
function assertNoPublishEventsConflict(params) {
|
|
1736
|
+
const { explicit, subscribed, conflict } = params;
|
|
1737
|
+
if (explicit === false && subscribed) throw new Error(publishEventsConflictError(conflict));
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Resolve whether a resource publishes events.
|
|
1741
|
+
*
|
|
1742
|
+
* An unset value is recomputed from the executors taking part in the run, so
|
|
1743
|
+
* removing the last subscribing trigger turns publishing back off. A `deploy`
|
|
1744
|
+
* covering several configs counts a subscriber in any of them, so a resource
|
|
1745
|
+
* shared across configs needs `publishEvents: true` on the resource itself only
|
|
1746
|
+
* to keep publishing when the subscribing config is deployed on its own.
|
|
1747
|
+
* @param params - Declared value, subscriber presence, and conflict error details
|
|
1748
|
+
* @returns Whether the resource publishes events
|
|
1749
|
+
*/
|
|
1750
|
+
function resolvePublishEvents(params) {
|
|
1751
|
+
assertNoPublishEventsConflict(params);
|
|
1752
|
+
return params.explicit ?? params.subscribed;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1459
1755
|
//#endregion
|
|
1460
1756
|
//#region src/cli/shared/type-generator.ts
|
|
1461
1757
|
/**
|
|
@@ -1467,6 +1763,7 @@ function createCacheManager(options) {
|
|
|
1467
1763
|
function extractAttributesFromConfig(config) {
|
|
1468
1764
|
return collectAttributesFromConfig(config);
|
|
1469
1765
|
}
|
|
1766
|
+
const isValidIdentifier = (s) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(s);
|
|
1470
1767
|
/**
|
|
1471
1768
|
* Generate the contents of the user-defined type definition file.
|
|
1472
1769
|
* @param attributes - Attribute configuration
|
|
@@ -1486,13 +1783,10 @@ ${attributeFields}
|
|
|
1486
1783
|
const listBody = `{
|
|
1487
1784
|
__tuple?: ${attributeList ? `[${attributeList.map(() => "string").join(", ")}]` : "[]"};
|
|
1488
1785
|
}`;
|
|
1489
|
-
const envFields = env ? Object.entries(env).map(([key, value]) => {
|
|
1490
|
-
return ` ${key}: ${typeof value === "string" ? `"${value}"` : String(value)};`;
|
|
1491
|
-
}).join("\n") : "";
|
|
1786
|
+
const envFields = env ? Object.entries(env).map(([key, value]) => ` ${isValidIdentifier(key) ? key : JSON.stringify(key)}: ${typeof value};`).join("\n") : "";
|
|
1492
1787
|
const envBody = !env || Object.keys(env).length === 0 ? "{}" : `{
|
|
1493
1788
|
${envFields}
|
|
1494
1789
|
}`;
|
|
1495
|
-
const isValidIdentifier = (s) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(s);
|
|
1496
1790
|
const machineUserFields = machineUserNames?.length ? machineUserNames.map((name) => ` ${isValidIdentifier(name) ? name : JSON.stringify(name)}: true;`).join("\n") : "";
|
|
1497
1791
|
const machineUserBody = !machineUserNames || machineUserNames.length === 0 ? "{}" : `{
|
|
1498
1792
|
${machineUserFields}
|
|
@@ -2132,6 +2426,42 @@ function trnPrefix(workspaceId) {
|
|
|
2132
2426
|
function resourceTrn(workspaceId, kind, name) {
|
|
2133
2427
|
return `${trnPrefix(workspaceId)}:${kind}:${name}`;
|
|
2134
2428
|
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Build the TRN for a resource nested inside another.
|
|
2431
|
+
*
|
|
2432
|
+
* The platform reads everything after the workspace id as alternating key/value
|
|
2433
|
+
* pairs and matches the pair list against one resource type, so a nested
|
|
2434
|
+
* resource is the parent's pair followed by its own. A TailorDB type is
|
|
2435
|
+
* `tailordb:<namespace>:type:<name>`, distinct from the namespace's own
|
|
2436
|
+
* `tailordb:<namespace>`.
|
|
2437
|
+
* @param workspaceId - Workspace ID
|
|
2438
|
+
* @param parent - Parent kind and name, e.g. the namespace holding the resource
|
|
2439
|
+
* @param child - Nested key and name, e.g. `["type", "Order"]`
|
|
2440
|
+
* @returns Fully-qualified TRN string
|
|
2441
|
+
*/
|
|
2442
|
+
function nestedResourceTrn(workspaceId, parent, child) {
|
|
2443
|
+
return `${trnPrefix(workspaceId)}:${parent[0]}:${parent[1]}:${child[0]}:${child[1]}`;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Build the TRN for one TailorDB type.
|
|
2447
|
+
* @param workspaceId - Workspace ID
|
|
2448
|
+
* @param namespace - TailorDB namespace holding the type
|
|
2449
|
+
* @param typeName - Type name
|
|
2450
|
+
* @returns Fully-qualified TRN string
|
|
2451
|
+
*/
|
|
2452
|
+
function tailorDBTypeTrn(workspaceId, namespace, typeName) {
|
|
2453
|
+
return nestedResourceTrn(workspaceId, ["tailordb", namespace], ["type", typeName]);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Build the TRN for one resolver.
|
|
2457
|
+
* @param workspaceId - Workspace ID
|
|
2458
|
+
* @param namespace - Resolver namespace holding the resolver
|
|
2459
|
+
* @param resolverName - Resolver name
|
|
2460
|
+
* @returns Fully-qualified TRN string
|
|
2461
|
+
*/
|
|
2462
|
+
function resolverTrn(workspaceId, namespace, resolverName) {
|
|
2463
|
+
return nestedResourceTrn(workspaceId, ["pipeline", namespace], ["resolver", resolverName]);
|
|
2464
|
+
}
|
|
2135
2465
|
const sdkNameLabelKey = "sdk-name";
|
|
2136
2466
|
const sdkVersionLabelKey = "sdk-version";
|
|
2137
2467
|
const sdkAppIdLabelKey = "sdk-app-id";
|
|
@@ -2165,28 +2495,190 @@ function isOwnedByApp(labels, appName, appId) {
|
|
|
2165
2495
|
if (labelAppId) return appId !== void 0 && labelAppId === toAppIdLabelValue(appId);
|
|
2166
2496
|
return labels[sdkNameLabelKey] === appName;
|
|
2167
2497
|
}
|
|
2498
|
+
const dependedByAppLabelPrefix = "sdk-depended-by-app-";
|
|
2499
|
+
const jobDependedByAppLabelPrefix = "sdk-job-depended-by-app-";
|
|
2500
|
+
function prefixFor(scope) {
|
|
2501
|
+
return scope === "jobs" ? jobDependedByAppLabelPrefix : dependedByAppLabelPrefix;
|
|
2502
|
+
}
|
|
2503
|
+
const RECORDABLE_APP_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
2504
|
+
/**
|
|
2505
|
+
* Build the label key recording that an application depends on this deploy.
|
|
2506
|
+
* @param appId - Stable id of the dependent application
|
|
2507
|
+
* @param scope - Which of the resource's values the record concerns
|
|
2508
|
+
* @returns Label key, or undefined when the id cannot form a valid key
|
|
2509
|
+
*/
|
|
2510
|
+
function dependedByAppLabelKey(appId, scope = "resource") {
|
|
2511
|
+
return RECORDABLE_APP_ID.test(appId) ? `${prefixFor(scope)}${appId}` : void 0;
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Read the dependent applications recorded on a resource.
|
|
2515
|
+
*
|
|
2516
|
+
* Only keys {@link dependedByAppLabelKey} could have written are read back, so a
|
|
2517
|
+
* label the SDK could not have produced cannot raise a confirmation prompt that
|
|
2518
|
+
* naming no config in the run makes unanswerable.
|
|
2519
|
+
* @param labels - Labels currently stored on the remote resource
|
|
2520
|
+
* @param scope - Which of the resource's values to read records for
|
|
2521
|
+
* @returns Recorded dependencies, in label-key order
|
|
2522
|
+
*/
|
|
2523
|
+
function recordedDependencies(labels, scope = "resource") {
|
|
2524
|
+
if (!labels) return [];
|
|
2525
|
+
const prefix = prefixFor(scope);
|
|
2526
|
+
return Object.entries(labels).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).flatMap(([key, reason]) => {
|
|
2527
|
+
if (!key.startsWith(prefix)) return [];
|
|
2528
|
+
const appId = key.slice(prefix.length);
|
|
2529
|
+
return RECORDABLE_APP_ID.test(appId) ? [{
|
|
2530
|
+
appId,
|
|
2531
|
+
reason
|
|
2532
|
+
}] : [];
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Key one event-publishing resource for the dependency records.
|
|
2537
|
+
*
|
|
2538
|
+
* Planners rebuild the same string from the resource they are applying, so the
|
|
2539
|
+
* shape is the TRN's tail rather than anything new to keep in step.
|
|
2540
|
+
*/
|
|
2541
|
+
const eventSourceKey = {
|
|
2542
|
+
tailorDBType: (namespace, typeName) => `tailordb:${namespace}:type:${typeName}`,
|
|
2543
|
+
resolver: (namespace, resolverName) => `pipeline:${namespace}:resolver:${resolverName}`,
|
|
2544
|
+
idp: (name) => `idp:${name}`,
|
|
2545
|
+
workflow: (name) => `workflow:${name}`,
|
|
2546
|
+
workflowJobs: (name) => `workflow:${name}:jobs`
|
|
2547
|
+
};
|
|
2548
|
+
/**
|
|
2549
|
+
* Split the dependency records into the ones to write and the ones to drop.
|
|
2550
|
+
*
|
|
2551
|
+
* A record for an application outside the run appears in neither list, so
|
|
2552
|
+
* {@link writeMetadataLabels} keeps it: it was written when both took part, and
|
|
2553
|
+
* dropping it would lose the only signal that this partial deploy is about to
|
|
2554
|
+
* change how the resource is applied. A record for an application that does take
|
|
2555
|
+
* part is rewritten or dropped, so a dependency that no longer exists disappears
|
|
2556
|
+
* on the next deploy including both.
|
|
2557
|
+
*
|
|
2558
|
+
* A resource that declares `publishEvents` drops every record instead. Nothing
|
|
2559
|
+
* about it depends on which configs the run covers, so a record could only
|
|
2560
|
+
* produce a prompt about a change that cannot happen — and one the owner could
|
|
2561
|
+
* never clear on its own, since clearing needs the dependent to take part.
|
|
2562
|
+
* @param params - Existing labels and the run's dependency inputs
|
|
2563
|
+
* @returns Labels to set and label keys to delete
|
|
2564
|
+
*/
|
|
2565
|
+
function dependencyLabelWrite(params) {
|
|
2566
|
+
const { existingLabels, dependentApps, runAppIds, pinned, scope = "resource" } = params;
|
|
2567
|
+
if (pinned) return {
|
|
2568
|
+
labels: {},
|
|
2569
|
+
remove: recordedDependencies(existingLabels, scope).flatMap(({ appId }) => dependedByAppLabelKey(appId, scope) ?? [])
|
|
2570
|
+
};
|
|
2571
|
+
const dependents = dependentApps ?? /* @__PURE__ */ new Map();
|
|
2572
|
+
const inRun = runAppIds ?? /* @__PURE__ */ new Set();
|
|
2573
|
+
const labels = {};
|
|
2574
|
+
for (const [appId, reason] of dependents) {
|
|
2575
|
+
const key = dependedByAppLabelKey(appId, scope);
|
|
2576
|
+
if (!key) throw new Error(`Application id "${appId}" cannot be recorded as a dependency of this deploy. Ids are written by deploy as lowercase UUIDs; restore the generated value in the config's "id".`);
|
|
2577
|
+
labels[key] = reason;
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
labels,
|
|
2581
|
+
remove: recordedDependencies(existingLabels, scope).flatMap(({ appId }) => {
|
|
2582
|
+
if (!inRun.has(appId) || dependents.has(appId)) return [];
|
|
2583
|
+
const key = dependedByAppLabelKey(appId, scope);
|
|
2584
|
+
return key ? [key] : [];
|
|
2585
|
+
})
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2168
2588
|
/**
|
|
2169
2589
|
* Build metadata request with SDK labels.
|
|
2590
|
+
*
|
|
2591
|
+
* Sets only the SDK's own labels; {@link writeMetadataLabels} keeps the rest
|
|
2592
|
+
* from the labels it reads at write time.
|
|
2593
|
+
*
|
|
2594
|
+
* Without an app id the id label is removed rather than merely left unset,
|
|
2595
|
+
* because {@link isOwnedByApp} decides ownership by that label alone: one left
|
|
2596
|
+
* over from an earlier deploy would keep reading as another app's, and every
|
|
2597
|
+
* later deploy would ask to re-tag the same resources again.
|
|
2170
2598
|
* @param params - Parameters for building the metadata request
|
|
2171
2599
|
* @param params.trn - Target TRN
|
|
2172
2600
|
* @param params.appName - Application name label
|
|
2173
2601
|
* @param params.appId - Stable application id label (when managed by SDK)
|
|
2174
|
-
* @param params.existingLabels - Existing labels to preserve (optional)
|
|
2175
2602
|
* @returns Metadata request
|
|
2176
2603
|
*/
|
|
2177
2604
|
async function buildMetaRequest(params) {
|
|
2178
|
-
const { trn, appName, appId
|
|
2605
|
+
const { trn, appName, appId } = params;
|
|
2179
2606
|
const packageJson = await readPackageJson();
|
|
2180
2607
|
const sdkVersion = packageJson.version ? `v${packageJson.version.replace(/\./g, "-")}` : "unknown";
|
|
2181
2608
|
return {
|
|
2182
2609
|
trn,
|
|
2183
2610
|
labels: {
|
|
2184
|
-
...existingLabels,
|
|
2185
2611
|
[sdkNameLabelKey]: appName,
|
|
2186
2612
|
[sdkVersionLabelKey]: sdkVersion,
|
|
2187
2613
|
...appId ? { [sdkAppIdLabelKey]: toAppIdLabelValue(appId) } : {}
|
|
2188
|
-
}
|
|
2614
|
+
},
|
|
2615
|
+
remove: appId ? void 0 : [sdkAppIdLabelKey]
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Fold a resource's dependency records into the write already planned for it.
|
|
2620
|
+
*
|
|
2621
|
+
* The reconciliation is attached rather than computed, so it runs against the
|
|
2622
|
+
* labels read at write time. Whatever {@link buildMetaRequest} asked for —
|
|
2623
|
+
* dropping a stale `sdk-app-id`, for instance — still happens alongside it.
|
|
2624
|
+
* @param write - The resource's planned metadata write, mutated in place
|
|
2625
|
+
* @param params - The resource's key and the run's inputs
|
|
2626
|
+
* @returns The same write, for use as an expression
|
|
2627
|
+
*/
|
|
2628
|
+
function addDependencyRecords(write, params) {
|
|
2629
|
+
const { key, dependentApps, runAppIds, pinned, scope } = params;
|
|
2630
|
+
write.dependencies = [...write.dependencies ?? [], {
|
|
2631
|
+
dependentApps: dependentApps?.get(key),
|
|
2632
|
+
runAppIds,
|
|
2633
|
+
pinned,
|
|
2634
|
+
scope
|
|
2635
|
+
}];
|
|
2636
|
+
return write;
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Write metadata labels as a change against the resource's current labels.
|
|
2640
|
+
*
|
|
2641
|
+
* `SetMetadata` replaces the whole label map, so a request built from labels
|
|
2642
|
+
* read earlier deletes anything written in between. This re-reads immediately
|
|
2643
|
+
* before writing and applies `labels` and `remove` to what it finds, which is
|
|
2644
|
+
* why every label write in the SDK goes through here rather than calling
|
|
2645
|
+
* `client.setMetadata` directly.
|
|
2646
|
+
*
|
|
2647
|
+
* Concurrent writers are still not safe in the strict sense — that needs
|
|
2648
|
+
* server-side conditional writes — but a write can no longer be built from
|
|
2649
|
+
* state this process read at an unrelated point in time.
|
|
2650
|
+
*
|
|
2651
|
+
* A write that changes nothing does nothing — whether the caller requested no
|
|
2652
|
+
* change or the change turns out to already hold. Writing back what was just
|
|
2653
|
+
* read would still overwrite whatever landed in between, for no gain, and the
|
|
2654
|
+
* labels the SDK sets are unchanged on most deploys.
|
|
2655
|
+
* @param client - Operator client instance
|
|
2656
|
+
* @param write - TRN, labels to set, and label keys to delete
|
|
2657
|
+
* @returns Promise that resolves when the labels are written
|
|
2658
|
+
*/
|
|
2659
|
+
async function writeMetadataLabels(client, write) {
|
|
2660
|
+
const { trn, labels, remove, dependencies } = write;
|
|
2661
|
+
if (!Object.keys(labels ?? {}).length && !(remove ?? []).length && !dependencies?.length) return;
|
|
2662
|
+
const currentLabels = (await getOrNull(() => client.getMetadata({ trn })))?.metadata?.labels ?? {};
|
|
2663
|
+
const resolved = (dependencies ?? []).map((pending) => dependencyLabelWrite({
|
|
2664
|
+
...pending,
|
|
2665
|
+
existingLabels: currentLabels
|
|
2666
|
+
}));
|
|
2667
|
+
const merged = {
|
|
2668
|
+
...currentLabels,
|
|
2669
|
+
...labels,
|
|
2670
|
+
...Object.assign({}, ...resolved.map((records) => records.labels))
|
|
2189
2671
|
};
|
|
2672
|
+
for (const key of [...remove ?? [], ...resolved.flatMap((records) => records.remove)]) delete merged[key];
|
|
2673
|
+
if (areSameLabels(currentLabels, merged)) return;
|
|
2674
|
+
await client.setMetadata({
|
|
2675
|
+
trn,
|
|
2676
|
+
labels: merged
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
function areSameLabels(a, b) {
|
|
2680
|
+
const keys = Object.keys(a);
|
|
2681
|
+
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
2190
2682
|
}
|
|
2191
2683
|
|
|
2192
2684
|
//#endregion
|
|
@@ -2277,14 +2769,14 @@ async function applyStaticWebsite(client, result, phase = "create-update") {
|
|
|
2277
2769
|
if (phase === "create-update") {
|
|
2278
2770
|
await Promise.all([...changeSet.creates.map(async (create) => {
|
|
2279
2771
|
await client.createStaticWebsite(create.request);
|
|
2280
|
-
await client
|
|
2772
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
2281
2773
|
}), ...changeSet.updates.map(async (update) => {
|
|
2282
2774
|
await client.updateStaticWebsite(update.request);
|
|
2283
|
-
await client
|
|
2775
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
2284
2776
|
})]);
|
|
2285
2777
|
await Promise.all([...customDomainChangeSet.creates.map(async (add) => {
|
|
2286
2778
|
await client.addCustomDomain(add.request);
|
|
2287
|
-
await client
|
|
2779
|
+
await writeMetadataLabels(client, add.metaRequest);
|
|
2288
2780
|
}), ...customDomainChangeSet.deletes.map((del) => client.removeCustomDomain(del.request))]);
|
|
2289
2781
|
} else await Promise.all(changeSet.deletes.map((del) => client.deleteStaticWebsite(del.request)));
|
|
2290
2782
|
}
|
|
@@ -2478,11 +2970,11 @@ async function applyAIGateway(client, result, phase = "create-update") {
|
|
|
2478
2970
|
if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
|
|
2479
2971
|
create.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(create.request.workspaceId, "request missing workspaceId"), create.request.cors, "AIGateway CORS");
|
|
2480
2972
|
await client.createAIGateway(create.request);
|
|
2481
|
-
await client
|
|
2973
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
2482
2974
|
}), ...changeSet.updates.map(async (update) => {
|
|
2483
2975
|
update.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(update.request.workspaceId, "request missing workspaceId"), update.request.cors, "AIGateway CORS");
|
|
2484
2976
|
await client.updateAIGateway(update.request);
|
|
2485
|
-
await client
|
|
2977
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
2486
2978
|
})]);
|
|
2487
2979
|
else await Promise.all(changeSet.deletes.map((del) => client.deleteAIGateway(del.request)));
|
|
2488
2980
|
}
|
|
@@ -2609,11 +3101,11 @@ async function applyApplication(client, changeSet, phase = "create-update") {
|
|
|
2609
3101
|
await Promise.all([...changeSet.creates.map(async (create) => {
|
|
2610
3102
|
create.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(create.request.workspaceId, "request missing workspaceId"), create.request.cors, "CORS");
|
|
2611
3103
|
await client.createApplication(create.request);
|
|
2612
|
-
await client
|
|
3104
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
2613
3105
|
}), ...updates.map(async (update) => {
|
|
2614
3106
|
update.request.cors = await resolveStaticWebsiteUrls(client, assertDefined(update.request.workspaceId, "request missing workspaceId"), update.request.cors, "CORS");
|
|
2615
3107
|
await client.updateApplication(update.request);
|
|
2616
|
-
await client
|
|
3108
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
2617
3109
|
})]);
|
|
2618
3110
|
} else await Promise.all(changeSet.deletes.map(async (del) => {
|
|
2619
3111
|
await client.deleteApplication(del.request);
|
|
@@ -2689,6 +3181,9 @@ function areApplicationsEqual(existing, desired) {
|
|
|
2689
3181
|
*/
|
|
2690
3182
|
async function planApplication(context, httpAdapterBuildResult) {
|
|
2691
3183
|
const { client, workspaceId, application, forRemoval } = context;
|
|
3184
|
+
const conflicts = [];
|
|
3185
|
+
const unmanaged = [];
|
|
3186
|
+
const resourceOwners = /* @__PURE__ */ new Set();
|
|
2692
3187
|
const changeSet = createChangeSet("Applications");
|
|
2693
3188
|
const existingApplications = await fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
2694
3189
|
const { applications, nextPageToken } = await client.listApplications({
|
|
@@ -2701,7 +3196,14 @@ async function planApplication(context, httpAdapterBuildResult) {
|
|
|
2701
3196
|
if (forRemoval) {
|
|
2702
3197
|
const candidates = application.id ? existingApplications : existingApplications.filter((app) => app.name === application.name);
|
|
2703
3198
|
const owned = await Promise.all(candidates.map(async (app) => {
|
|
2704
|
-
|
|
3199
|
+
const labels = await fetchAppLabels(client, workspaceId, app.name);
|
|
3200
|
+
return trackRemainingResourceOwner({
|
|
3201
|
+
labels,
|
|
3202
|
+
ownerLabel: labels?.["sdk-name"],
|
|
3203
|
+
appName: application.name,
|
|
3204
|
+
appId: application.id,
|
|
3205
|
+
resourceOwners
|
|
3206
|
+
}) ? app.name : null;
|
|
2705
3207
|
}));
|
|
2706
3208
|
for (const name of owned) if (name) changeSet.deletes.push({
|
|
2707
3209
|
name,
|
|
@@ -2710,9 +3212,9 @@ async function planApplication(context, httpAdapterBuildResult) {
|
|
|
2710
3212
|
applicationName: name
|
|
2711
3213
|
}
|
|
2712
3214
|
});
|
|
2713
|
-
return changeSet;
|
|
3215
|
+
return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
|
|
2714
3216
|
}
|
|
2715
|
-
if (application.subgraphs.length === 0) return changeSet;
|
|
3217
|
+
if (application.subgraphs.length === 0) return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
|
|
2716
3218
|
let authNamespace;
|
|
2717
3219
|
let authIdpConfigName;
|
|
2718
3220
|
if (application.authService) {
|
|
@@ -2743,6 +3245,7 @@ async function planApplication(context, httpAdapterBuildResult) {
|
|
|
2743
3245
|
appName: application.name,
|
|
2744
3246
|
appId: application.id
|
|
2745
3247
|
});
|
|
3248
|
+
const existingLabels = await fetchAppLabels(client, workspaceId, application.name);
|
|
2746
3249
|
const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
|
|
2747
3250
|
const resolvedCors = await resolveStaticWebsiteUrls(client, workspaceId, application.config.cors, "CORS", { expectedLocalNames: expectedLocalWebsites });
|
|
2748
3251
|
const httpAdapters = buildHttpAdapters(application, httpAdapterBuildResult);
|
|
@@ -2773,13 +3276,22 @@ async function planApplication(context, httpAdapterBuildResult) {
|
|
|
2773
3276
|
});
|
|
2774
3277
|
}
|
|
2775
3278
|
if (existing) {
|
|
2776
|
-
const
|
|
3279
|
+
const owned = trackDesiredResourceOwnership({
|
|
3280
|
+
labels: existingLabels,
|
|
3281
|
+
ownerLabel: existingLabels?.[sdkNameLabelKey],
|
|
3282
|
+
appName: application.name,
|
|
3283
|
+
appId: application.id,
|
|
3284
|
+
resourceType: "Application",
|
|
3285
|
+
resourceName: application.name,
|
|
3286
|
+
conflicts,
|
|
3287
|
+
unmanaged
|
|
3288
|
+
});
|
|
2777
3289
|
const update = {
|
|
2778
3290
|
name: application.name,
|
|
2779
3291
|
request,
|
|
2780
3292
|
metaRequest
|
|
2781
3293
|
};
|
|
2782
|
-
if (
|
|
3294
|
+
if (owned && hasMatchingSdkVersion(existingLabels, metaRequest.labels) && areApplicationsEqual(existing, desired)) changeSet.unchanged.push(update);
|
|
2783
3295
|
else {
|
|
2784
3296
|
const details = diffHttpAdapterDisplay(existing.httpAdapters, httpAdapters);
|
|
2785
3297
|
if (details.length > 0) update.details = details;
|
|
@@ -2794,7 +3306,28 @@ async function planApplication(context, httpAdapterBuildResult) {
|
|
|
2794
3306
|
details: details.length > 0 ? details : void 0
|
|
2795
3307
|
});
|
|
2796
3308
|
}
|
|
2797
|
-
return changeSet;
|
|
3309
|
+
return withOwnership(changeSet, conflicts, unmanaged, resourceOwners);
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Attach the ownership-tracking fields to the plan's change set.
|
|
3313
|
+
*
|
|
3314
|
+
* The other resource plans return `{ changeSet, conflicts, ... }`; this one
|
|
3315
|
+
* returns the change set itself, so the fields are attached to it. Without
|
|
3316
|
+
* them the application is the one managed resource that never reaches
|
|
3317
|
+
* `confirmOwnerConflict`, which would let a deploy re-tag — or a `remove`
|
|
3318
|
+
* silently skip — an application this config does not own.
|
|
3319
|
+
* @param changeSet - The application change set
|
|
3320
|
+
* @param conflicts - Resources owned by another application
|
|
3321
|
+
* @param unmanaged - Resources carrying no SDK label
|
|
3322
|
+
* @param resourceOwners - Owners of the resources that were skipped
|
|
3323
|
+
* @returns The change set, with the ownership-tracking fields attached
|
|
3324
|
+
*/
|
|
3325
|
+
function withOwnership(changeSet, conflicts, unmanaged, resourceOwners) {
|
|
3326
|
+
return Object.assign(changeSet, {
|
|
3327
|
+
conflicts,
|
|
3328
|
+
unmanaged,
|
|
3329
|
+
resourceOwners
|
|
3330
|
+
});
|
|
2798
3331
|
}
|
|
2799
3332
|
async function fetchAppLabels(client, workspaceId, appName) {
|
|
2800
3333
|
return (await getOrNull(async () => {
|
|
@@ -3172,7 +3705,7 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
|
|
|
3172
3705
|
appId
|
|
3173
3706
|
});
|
|
3174
3707
|
if (existing) {
|
|
3175
|
-
trackDesiredResourceOwnership({
|
|
3708
|
+
const owned = trackDesiredResourceOwnership({
|
|
3176
3709
|
labels: existing.allLabels,
|
|
3177
3710
|
ownerLabel: existing.label,
|
|
3178
3711
|
appName,
|
|
@@ -3192,7 +3725,7 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
|
|
|
3192
3725
|
},
|
|
3193
3726
|
metaRequest
|
|
3194
3727
|
});
|
|
3195
|
-
else if (!
|
|
3728
|
+
else if (!owned) changeSet.updates.push({
|
|
3196
3729
|
name,
|
|
3197
3730
|
metaRequest
|
|
3198
3731
|
});
|
|
@@ -3240,12 +3773,12 @@ async function applyAuthConnections(client, result, phase) {
|
|
|
3240
3773
|
if (changeSet.creates.length > 0 || changeSet.replaces.length > 0) await withSecretsStateLock(stateScope, async () => {
|
|
3241
3774
|
await Promise.all(changeSet.creates.map(async (create) => {
|
|
3242
3775
|
await client.createAuthConnection(create.request);
|
|
3243
|
-
await client
|
|
3776
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
3244
3777
|
logger.info(`Connection "${create.name}" was created. Authorize it with:\n tailor authconnection authorize --name ${create.name}\nOr via the Console: tailor authconnection open`);
|
|
3245
3778
|
}));
|
|
3246
3779
|
for (const replace of changeSet.replaces) {
|
|
3247
3780
|
if ((await client.updateAuthConnection(replace.updateRequest)).connection?.status === AuthConnection_Status.UNAUTHORIZED) logger.warn(`Connection "${replace.name}" requires re-authorization. Authorize with:\n tailor authconnection authorize --name ${replace.name}\nOr via the Console: tailor authconnection open`);
|
|
3248
|
-
await client
|
|
3781
|
+
await writeMetadataLabels(client, replace.metaRequest);
|
|
3249
3782
|
}
|
|
3250
3783
|
const secretReplaces = changeSet.replaces.filter((replace) => replace.updateRequest.updateMask?.paths?.includes("oauth2.client_secret"));
|
|
3251
3784
|
if (changeSet.creates.length > 0 || secretReplaces.length > 0) {
|
|
@@ -3263,7 +3796,7 @@ async function applyAuthConnections(client, result, phase) {
|
|
|
3263
3796
|
}
|
|
3264
3797
|
});
|
|
3265
3798
|
await Promise.all(changeSet.updates.map(async (update) => {
|
|
3266
|
-
await client
|
|
3799
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
3267
3800
|
}));
|
|
3268
3801
|
} else if (changeSet.deletes.length > 0) await withSecretsStateLock(stateScope, async () => {
|
|
3269
3802
|
await Promise.all(changeSet.deletes.map(async (del) => {
|
|
@@ -3590,10 +4123,10 @@ async function applyFunctionRegistry(client, workspaceId, result, phase = "creat
|
|
|
3590
4123
|
const limitFunction = createApplyLimiter();
|
|
3591
4124
|
await Promise.all([...changeSet.creates.map((create) => limitFunction(async () => {
|
|
3592
4125
|
await uploadFunctionScript(client, workspaceId, create.entry, true);
|
|
3593
|
-
await client
|
|
4126
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
3594
4127
|
})), ...changeSet.updates.map((update) => limitFunction(async () => {
|
|
3595
4128
|
await uploadFunctionScript(client, workspaceId, update.entry, false);
|
|
3596
|
-
await client
|
|
4129
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
3597
4130
|
}))]);
|
|
3598
4131
|
} else await Promise.all(changeSet.deletes.map((del) => client.deleteFunctionRegistry({
|
|
3599
4132
|
workspaceId: del.workspaceId,
|
|
@@ -3976,15 +4509,19 @@ function idpClientSecretName(namespaceName, clientName) {
|
|
|
3976
4509
|
async function applyIdP(client, result, phase = "create-update") {
|
|
3977
4510
|
const { changeSet } = result;
|
|
3978
4511
|
if (phase === "create-update") {
|
|
3979
|
-
await Promise.all([
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4512
|
+
await Promise.all([
|
|
4513
|
+
...changeSet.service.creates.map(async (create) => {
|
|
4514
|
+
await resolveServiceReturnOrigins(client, create.request);
|
|
4515
|
+
await client.createIdPService(create.request);
|
|
4516
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
4517
|
+
}),
|
|
4518
|
+
...changeSet.service.updates.map(async (update) => {
|
|
4519
|
+
await resolveServiceReturnOrigins(client, update.request);
|
|
4520
|
+
await client.updateIdPService(update.request);
|
|
4521
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
4522
|
+
}),
|
|
4523
|
+
...changeSet.service.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
|
|
4524
|
+
]);
|
|
3988
4525
|
await Promise.all([...changeSet.client.creates.map(async (create) => {
|
|
3989
4526
|
const resp = await client.createIdPClient(create.request);
|
|
3990
4527
|
const vaultName = idpClientVaultName(assertDefined(create.request.namespaceName, "request missing namespaceName"), create.request.client?.name || "");
|
|
@@ -4035,10 +4572,13 @@ async function applyIdP(client, result, phase = "create-update") {
|
|
|
4035
4572
|
* @returns Planned changes and metadata
|
|
4036
4573
|
*/
|
|
4037
4574
|
async function planIdP(context) {
|
|
4038
|
-
const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets } = context;
|
|
4575
|
+
const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets, dependentApps, runAppIds } = context;
|
|
4039
4576
|
const idps = forRemoval ? [] : application.idpServices;
|
|
4040
4577
|
const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
|
|
4041
|
-
const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set(), expectedLocalWebsites
|
|
4578
|
+
const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set(), expectedLocalWebsites, {
|
|
4579
|
+
dependentApps,
|
|
4580
|
+
runAppIds
|
|
4581
|
+
});
|
|
4042
4582
|
return {
|
|
4043
4583
|
changeSet: {
|
|
4044
4584
|
service: serviceChangeSet,
|
|
@@ -4091,7 +4631,7 @@ function normalizeComparableIdPService(input) {
|
|
|
4091
4631
|
authorization: input.authorization || void 0,
|
|
4092
4632
|
lang: input.lang === IdPLang.UNSPECIFIED ? IdPLang.EN : input.lang,
|
|
4093
4633
|
userAuthPolicy: input.userAuthPolicy,
|
|
4094
|
-
|
|
4634
|
+
publishEvents: input.publishEvents,
|
|
4095
4635
|
disableGqlOperations: input.disableGqlOperations,
|
|
4096
4636
|
emailConfig: input.emailConfig,
|
|
4097
4637
|
permission: input.permission
|
|
@@ -4123,13 +4663,13 @@ function areIdPServicesEqual(existing, desired) {
|
|
|
4123
4663
|
authorization: existing.authorization,
|
|
4124
4664
|
lang: existing.lang,
|
|
4125
4665
|
userAuthPolicy: normalizeComparableUserAuthPolicy(existing.userAuthPolicy),
|
|
4126
|
-
|
|
4666
|
+
publishEvents: existing.publishUserEvents,
|
|
4127
4667
|
disableGqlOperations: normalizeComparableDisableGqlOperations(existing.disableGqlOperations),
|
|
4128
4668
|
emailConfig: normalizeComparableEmailConfig(existing.emailConfig),
|
|
4129
4669
|
permission: normalizeComparablePermission(existing.permission)
|
|
4130
4670
|
}), desired);
|
|
4131
4671
|
}
|
|
4132
|
-
async function planServices$3(client, workspaceId, appName, appId, idps, idpUserTriggerTargets, expectedLocalWebsites) {
|
|
4672
|
+
async function planServices$3(client, workspaceId, appName, appId, idps, idpUserTriggerTargets, expectedLocalWebsites, records) {
|
|
4133
4673
|
const changeSet = createChangeSet("IdP services");
|
|
4134
4674
|
const conflicts = [];
|
|
4135
4675
|
const unmanaged = [];
|
|
@@ -4150,10 +4690,15 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
|
|
|
4150
4690
|
for (const idp of idps) {
|
|
4151
4691
|
const namespaceName = idp.name;
|
|
4152
4692
|
const existing = existingServices[namespaceName];
|
|
4153
|
-
const metaRequest = await buildMetaRequest({
|
|
4693
|
+
const metaRequest = addDependencyRecords(await buildMetaRequest({
|
|
4154
4694
|
trn: resourceTrn(workspaceId, "idp", namespaceName),
|
|
4155
4695
|
appName,
|
|
4156
4696
|
appId
|
|
4697
|
+
}), {
|
|
4698
|
+
key: eventSourceKey.idp(namespaceName),
|
|
4699
|
+
dependentApps: records.dependentApps,
|
|
4700
|
+
runAppIds: records.runAppIds,
|
|
4701
|
+
pinned: idp.publishEvents !== void 0
|
|
4157
4702
|
});
|
|
4158
4703
|
let authorization;
|
|
4159
4704
|
switch (idp.authorization) {
|
|
@@ -4172,9 +4717,11 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
|
|
|
4172
4717
|
}
|
|
4173
4718
|
const lang = convertLang(idp.lang);
|
|
4174
4719
|
const userAuthPolicy = idp.userAuthPolicy;
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4720
|
+
const publishEvents = resolvePublishEvents({
|
|
4721
|
+
explicit: idp.publishEvents,
|
|
4722
|
+
subscribed: idpUserTriggerTargets.has(namespaceName),
|
|
4723
|
+
conflict: publishEventsConflict.idpService(namespaceName)
|
|
4724
|
+
});
|
|
4178
4725
|
const emailConfig = idp.emailConfig;
|
|
4179
4726
|
if (!idp.permission) logger.warn(`IdP service "${namespaceName}" has no permission configured.`);
|
|
4180
4727
|
const omittedPermitLocations = findOmittedPermitRules(idp.permission);
|
|
@@ -4190,7 +4737,7 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
|
|
|
4190
4737
|
authorization,
|
|
4191
4738
|
lang,
|
|
4192
4739
|
userAuthPolicy: normalizeComparableUserAuthPolicy(userAuthPolicyForCompare),
|
|
4193
|
-
|
|
4740
|
+
publishEvents,
|
|
4194
4741
|
disableGqlOperations: normalizeComparableDisableGqlOperations(convertGqlOperationsToDisable(idp.gqlOperations)),
|
|
4195
4742
|
emailConfig: normalizeComparableEmailConfig(emailConfig),
|
|
4196
4743
|
permission: protoPermission
|
|
@@ -4201,7 +4748,7 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
|
|
|
4201
4748
|
authorization,
|
|
4202
4749
|
lang,
|
|
4203
4750
|
userAuthPolicy,
|
|
4204
|
-
publishUserEvents,
|
|
4751
|
+
publishUserEvents: publishEvents,
|
|
4205
4752
|
disableGqlOperations: convertGqlOperationsToDisable(idp.gqlOperations),
|
|
4206
4753
|
emailConfig,
|
|
4207
4754
|
permission: protoPermission
|
|
@@ -4216,7 +4763,10 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
|
|
|
4216
4763
|
resourceName: idp.name,
|
|
4217
4764
|
conflicts,
|
|
4218
4765
|
unmanaged
|
|
4219
|
-
}) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({
|
|
4766
|
+
}) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({
|
|
4767
|
+
name: namespaceName,
|
|
4768
|
+
metaRequest
|
|
4769
|
+
});
|
|
4220
4770
|
else changeSet.updates.push({
|
|
4221
4771
|
name: namespaceName,
|
|
4222
4772
|
request,
|
|
@@ -4424,10 +4974,10 @@ async function applyAuth(client, result, phase = "create-update") {
|
|
|
4424
4974
|
const applyServices = async () => {
|
|
4425
4975
|
await Promise.all([...changeSet.service.creates.map(async (create) => {
|
|
4426
4976
|
await client.createAuthService(create.request);
|
|
4427
|
-
await client
|
|
4977
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
4428
4978
|
}), ...changeSet.service.updates.map(async (update) => {
|
|
4429
4979
|
await client.updateAuthService(update.request);
|
|
4430
|
-
await client
|
|
4980
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
4431
4981
|
})]);
|
|
4432
4982
|
};
|
|
4433
4983
|
const applyMachineUsers = async () => {
|
|
@@ -5548,10 +6098,10 @@ async function applyExecutor(client, result, phase = "create-update") {
|
|
|
5548
6098
|
const { changeSet } = result;
|
|
5549
6099
|
if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
|
|
5550
6100
|
await client.createExecutorExecutor(create.request);
|
|
5551
|
-
await client
|
|
6101
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
5552
6102
|
}), ...changeSet.updates.map(async (update) => {
|
|
5553
6103
|
await client.updateExecutorExecutor(update.request);
|
|
5554
|
-
await client
|
|
6104
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
5555
6105
|
})]);
|
|
5556
6106
|
else await Promise.all(changeSet.deletes.map((del) => client.deleteExecutorExecutor(del.request)));
|
|
5557
6107
|
}
|
|
@@ -5891,6 +6441,18 @@ function protoExecutor(context, executor) {
|
|
|
5891
6441
|
}
|
|
5892
6442
|
});
|
|
5893
6443
|
break;
|
|
6444
|
+
case "workflowExecution":
|
|
6445
|
+
case "workflowJobExecution":
|
|
6446
|
+
triggerType = ExecutorTriggerType.EVENT;
|
|
6447
|
+
triggerConfig = typedEventTrigger({
|
|
6448
|
+
case: "workflow",
|
|
6449
|
+
value: {
|
|
6450
|
+
eventTypes: trigger.events,
|
|
6451
|
+
workflowName: trigger.workflowName,
|
|
6452
|
+
...trigger.condition ? { condition: { expr: `(${stringifyFunction(trigger.condition)})(${argsExpr})` } } : {}
|
|
6453
|
+
}
|
|
6454
|
+
});
|
|
6455
|
+
break;
|
|
5894
6456
|
default: throw new Error(`Unknown trigger: ${trigger}`);
|
|
5895
6457
|
}
|
|
5896
6458
|
const target = executor.operation;
|
|
@@ -6029,12 +6591,22 @@ async function applyPipeline(client, result, phase = "create-update") {
|
|
|
6029
6591
|
if (phase === "create-update") {
|
|
6030
6592
|
await Promise.all([...changeSet.service.creates.map(async (create) => {
|
|
6031
6593
|
await client.createPipelineService(create.request);
|
|
6032
|
-
await client
|
|
6594
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
6033
6595
|
}), ...changeSet.service.updates.map(async (update) => {
|
|
6034
6596
|
await client.updatePipelineService(update.request);
|
|
6035
|
-
await client
|
|
6597
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
6036
6598
|
})]);
|
|
6037
|
-
await Promise.all([
|
|
6599
|
+
await Promise.all([
|
|
6600
|
+
...changeSet.resolver.creates.map(async (create) => {
|
|
6601
|
+
await client.createPipelineResolver(create.request);
|
|
6602
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
6603
|
+
}),
|
|
6604
|
+
...changeSet.resolver.updates.map(async (update) => {
|
|
6605
|
+
await client.updatePipelineResolver(update.request);
|
|
6606
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
6607
|
+
}),
|
|
6608
|
+
...changeSet.resolver.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
|
|
6609
|
+
]);
|
|
6038
6610
|
} else if (phase === "delete-resources") await Promise.all(changeSet.resolver.deletes.map((del) => client.deletePipelineResolver(del.request)));
|
|
6039
6611
|
else await Promise.all(changeSet.service.deletes.map((del) => client.deletePipelineService(del.request)));
|
|
6040
6612
|
}
|
|
@@ -6053,7 +6625,12 @@ async function planPipeline(context) {
|
|
|
6053
6625
|
const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
|
|
6054
6626
|
const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$1(client, workspaceId, application.name, application.id, pipelines);
|
|
6055
6627
|
const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
|
|
6056
|
-
const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, context.executorUsedResolvers ?? /* @__PURE__ */ new Set(), deletedServices, application.env, getApplicationAuthNamespace(application), forceApplyAll
|
|
6628
|
+
const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, context.executorUsedResolvers ?? /* @__PURE__ */ new Set(), deletedServices, application.env, getApplicationAuthNamespace(application), forceApplyAll, {
|
|
6629
|
+
appName: application.name,
|
|
6630
|
+
appId: application.id,
|
|
6631
|
+
dependentApps: context.dependentApps,
|
|
6632
|
+
runAppIds: context.runAppIds
|
|
6633
|
+
});
|
|
6057
6634
|
return {
|
|
6058
6635
|
changeSet: {
|
|
6059
6636
|
service: serviceChangeSet,
|
|
@@ -6141,8 +6718,28 @@ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
|
|
|
6141
6718
|
resourceOwners
|
|
6142
6719
|
};
|
|
6143
6720
|
}
|
|
6144
|
-
async function planResolvers(client, workspaceId, pipelines, executors, initialExecutorUsedResolvers, deletedServices, env, authNamespace, forceApplyAll = false) {
|
|
6721
|
+
async function planResolvers(client, workspaceId, pipelines, executors, initialExecutorUsedResolvers, deletedServices, env, authNamespace, forceApplyAll = false, records = {}) {
|
|
6145
6722
|
const changeSet = createChangeSet("Pipeline resolvers");
|
|
6723
|
+
const { appName, appId, dependentApps, runAppIds } = records;
|
|
6724
|
+
/**
|
|
6725
|
+
* Build one resolver's metadata write, carrying the dependency records that
|
|
6726
|
+
* belong to it. The resolver is what publishes, so the record lives there.
|
|
6727
|
+
* @param namespace - Namespace holding the resolver
|
|
6728
|
+
* @param resolver - Resolver being planned
|
|
6729
|
+
* @returns The resolver's metadata write
|
|
6730
|
+
*/
|
|
6731
|
+
const resolverMetaRequest = async (namespace, resolver) => {
|
|
6732
|
+
return addDependencyRecords(await buildMetaRequest({
|
|
6733
|
+
trn: resolverTrn(workspaceId, namespace, resolver.name),
|
|
6734
|
+
appName: appName ?? "",
|
|
6735
|
+
appId
|
|
6736
|
+
}), {
|
|
6737
|
+
key: eventSourceKey.resolver(namespace, resolver.name),
|
|
6738
|
+
dependentApps,
|
|
6739
|
+
runAppIds,
|
|
6740
|
+
pinned: resolver.publishEvents !== void 0
|
|
6741
|
+
});
|
|
6742
|
+
};
|
|
6146
6743
|
const fetchResolvers = (namespaceName) => {
|
|
6147
6744
|
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
6148
6745
|
const { pipelineResolvers, nextPageToken } = await client.listPipelineResolvers({
|
|
@@ -6156,26 +6753,36 @@ async function planResolvers(client, workspaceId, pipelines, executors, initialE
|
|
|
6156
6753
|
};
|
|
6157
6754
|
const executorUsedResolvers = new Set(initialExecutorUsedResolvers);
|
|
6158
6755
|
for (const executor of executors) if (executor.trigger.kind === "resolverExecuted") executorUsedResolvers.add(executor.trigger.resolverName);
|
|
6159
|
-
for (const pipeline of pipelines) for (const resolver of Object.values(pipeline.resolvers))
|
|
6756
|
+
for (const pipeline of pipelines) for (const resolver of Object.values(pipeline.resolvers)) assertNoPublishEventsConflict({
|
|
6757
|
+
explicit: resolver.publishEvents,
|
|
6758
|
+
subscribed: executorUsedResolvers.has(resolver.name),
|
|
6759
|
+
conflict: publishEventsConflict.resolver(resolver.name)
|
|
6760
|
+
});
|
|
6160
6761
|
for (const pipeline of pipelines) {
|
|
6161
6762
|
const existingResolvers = await fetchResolvers(pipeline.namespace);
|
|
6162
6763
|
const existingResolversMap = new Map(existingResolvers.map((resolver) => [resolver.name, resolver]));
|
|
6163
6764
|
for (const resolver of Object.values(pipeline.resolvers)) {
|
|
6164
6765
|
const desiredResolver = processResolver(pipeline.namespace, resolver, executorUsedResolvers, env, authNamespace);
|
|
6165
|
-
|
|
6766
|
+
const existingResolver = existingResolversMap.get(resolver.name);
|
|
6767
|
+
const metaRequest = await resolverMetaRequest(pipeline.namespace, resolver);
|
|
6768
|
+
if (existingResolver) {
|
|
6166
6769
|
const { pipelineResolver: existingResolverDetail } = await client.getPipelineResolver({
|
|
6167
6770
|
workspaceId,
|
|
6168
6771
|
namespaceName: pipeline.namespace,
|
|
6169
6772
|
resolverName: resolver.name
|
|
6170
6773
|
});
|
|
6171
|
-
if (!forceApplyAll && existingResolverDetail && areResolversEqual(existingResolverDetail, desiredResolver)) changeSet.unchanged.push({
|
|
6774
|
+
if (!forceApplyAll && existingResolverDetail && areResolversEqual(existingResolverDetail, desiredResolver)) changeSet.unchanged.push({
|
|
6775
|
+
name: resolver.name,
|
|
6776
|
+
metaRequest
|
|
6777
|
+
});
|
|
6172
6778
|
else changeSet.updates.push({
|
|
6173
6779
|
name: resolver.name,
|
|
6174
6780
|
request: {
|
|
6175
6781
|
workspaceId,
|
|
6176
6782
|
namespaceName: pipeline.namespace,
|
|
6177
6783
|
pipelineResolver: desiredResolver
|
|
6178
|
-
}
|
|
6784
|
+
},
|
|
6785
|
+
metaRequest
|
|
6179
6786
|
});
|
|
6180
6787
|
existingResolversMap.delete(resolver.name);
|
|
6181
6788
|
} else changeSet.creates.push({
|
|
@@ -6184,7 +6791,8 @@ async function planResolvers(client, workspaceId, pipelines, executors, initialE
|
|
|
6184
6791
|
workspaceId,
|
|
6185
6792
|
namespaceName: pipeline.namespace,
|
|
6186
6793
|
pipelineResolver: desiredResolver
|
|
6187
|
-
}
|
|
6794
|
+
},
|
|
6795
|
+
metaRequest
|
|
6188
6796
|
});
|
|
6189
6797
|
}
|
|
6190
6798
|
existingResolversMap.forEach((_resolver, name) => {
|
|
@@ -6292,9 +6900,11 @@ function processResolver(namespace, resolver, executorUsedResolvers, env, authNa
|
|
|
6292
6900
|
const resolverDescription = resolver.description || `${resolver.name} resolver`;
|
|
6293
6901
|
const outputDescription = resolver.output.metadata.description;
|
|
6294
6902
|
const combinedDescription = outputDescription ? `${resolverDescription}\n\nReturns:\n${outputDescription}` : resolverDescription;
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6903
|
+
const publishExecutionEvents = resolvePublishEvents({
|
|
6904
|
+
explicit: resolver.publishEvents,
|
|
6905
|
+
subscribed: executorUsedResolvers.has(resolver.name),
|
|
6906
|
+
conflict: publishEventsConflict.resolver(resolver.name)
|
|
6907
|
+
});
|
|
6298
6908
|
return {
|
|
6299
6909
|
authorization: "true==true",
|
|
6300
6910
|
description: combinedDescription,
|
|
@@ -6539,22 +7149,18 @@ async function applySecretManager(client, result, phase = "create-update", appli
|
|
|
6539
7149
|
if (phase === "create-update") {
|
|
6540
7150
|
await Promise.all(vaultChangeSet.creates.map(async (create) => {
|
|
6541
7151
|
await client.createSecretManagerVault(vaultCreateRequest(create));
|
|
6542
|
-
if (application) {
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
});
|
|
6548
|
-
await client.setMetadata(metaRequest);
|
|
6549
|
-
}
|
|
7152
|
+
if (application) await writeMetadataLabels(client, await buildMetaRequest({
|
|
7153
|
+
trn: resourceTrn(create.workspaceId, "vault", create.name),
|
|
7154
|
+
appName: application.name,
|
|
7155
|
+
appId: application.id
|
|
7156
|
+
}));
|
|
6550
7157
|
}));
|
|
6551
7158
|
if (application) await Promise.all(vaultChangeSet.updates.map(async (update) => {
|
|
6552
|
-
|
|
7159
|
+
await writeMetadataLabels(client, await buildMetaRequest({
|
|
6553
7160
|
trn: resourceTrn(update.workspaceId, "vault", update.name),
|
|
6554
7161
|
appName: application.name,
|
|
6555
7162
|
appId: application.id
|
|
6556
|
-
});
|
|
6557
|
-
await client.setMetadata(metaRequest);
|
|
7163
|
+
}));
|
|
6558
7164
|
}));
|
|
6559
7165
|
const secretHashUpdates = [...secretChangeSet.creates, ...secretChangeSet.updates];
|
|
6560
7166
|
if (secretHashUpdates.length > 0) await withSecretsStateLock(stateScope, async () => {
|
|
@@ -6632,197 +7238,46 @@ function getNamespacesWithMigrations(config, configDir) {
|
|
|
6632
7238
|
}
|
|
6633
7239
|
return result;
|
|
6634
7240
|
}
|
|
6635
|
-
|
|
6636
|
-
//#endregion
|
|
6637
|
-
//#region src/cli/commands/tailordb/migrate/diff-calculator.ts
|
|
6638
|
-
/**
|
|
6639
|
-
* Check if a migration diff has any changes
|
|
6640
|
-
* @param {MigrationDiff} diff - Migration diff to check
|
|
6641
|
-
* @returns {boolean} True if diff has changes
|
|
6642
|
-
*/
|
|
6643
|
-
function hasChanges(diff) {
|
|
6644
|
-
return diff.changes.length > 0;
|
|
6645
|
-
}
|
|
6646
7241
|
/**
|
|
6647
|
-
*
|
|
6648
|
-
* @param {
|
|
6649
|
-
* @
|
|
7242
|
+
* Select the single target namespace for a migration command
|
|
7243
|
+
* @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations configured
|
|
7244
|
+
* @param {string | undefined} requested - Namespace requested via --namespace, if any
|
|
7245
|
+
* @returns {NamespaceWithMigrations} The selected namespace
|
|
6650
7246
|
*/
|
|
6651
|
-
function
|
|
6652
|
-
if (
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
existing.push(change);
|
|
6658
|
-
changesByType.set(change.typeName, existing);
|
|
6659
|
-
}
|
|
6660
|
-
for (const [typeName, changes] of changesByType) {
|
|
6661
|
-
lines.push(`${diff.namespace}.${typeName}:`);
|
|
6662
|
-
for (const change of changes) lines.push(formatDiffChange(change));
|
|
7247
|
+
function selectTargetNamespace(namespacesWithMigrations, requested) {
|
|
7248
|
+
if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
|
|
7249
|
+
if (requested) {
|
|
7250
|
+
const found = namespacesWithMigrations.find((ns) => ns.namespace === requested);
|
|
7251
|
+
if (!found) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
|
|
7252
|
+
return found;
|
|
6663
7253
|
}
|
|
6664
|
-
|
|
7254
|
+
if (namespacesWithMigrations.length > 1) throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
|
|
7255
|
+
return assertDefined(namespacesWithMigrations[0], "namespace with migrations missing");
|
|
6665
7256
|
}
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
|
|
6691
|
-
case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
|
|
6692
|
-
case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
|
|
6693
|
-
case "type_scripts_modified": return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`;
|
|
6694
|
-
default: {
|
|
6695
|
-
const unknown = change;
|
|
6696
|
-
return ` ? ${unknown.typeName}.${unknown.fieldName ?? ""}`;
|
|
6697
|
-
}
|
|
6698
|
-
}
|
|
6699
|
-
}
|
|
6700
|
-
/**
|
|
6701
|
-
* Format field type with attributes
|
|
6702
|
-
* @param {SnapshotFieldConfig} field - Field configuration
|
|
6703
|
-
* @returns {string} Formatted field type string
|
|
6704
|
-
*/
|
|
6705
|
-
function formatFieldType(field) {
|
|
6706
|
-
let type = field.type;
|
|
6707
|
-
if (field.array) type += "[]";
|
|
6708
|
-
if (field.required) type += " (required)";
|
|
6709
|
-
else type += " (optional)";
|
|
6710
|
-
return type;
|
|
6711
|
-
}
|
|
6712
|
-
/**
|
|
6713
|
-
* Format field modification details
|
|
6714
|
-
* @param {SnapshotFieldConfig} before - Before field configuration
|
|
6715
|
-
* @param {SnapshotFieldConfig} after - After field configuration
|
|
6716
|
-
* @returns {string} Formatted modification details
|
|
6717
|
-
*/
|
|
6718
|
-
function formatFieldModification(before, after) {
|
|
6719
|
-
const changes = [];
|
|
6720
|
-
if (before.type !== after.type) changes.push(`type: ${before.type} → ${after.type}`);
|
|
6721
|
-
if (before.required !== after.required) changes.push(`required: ${before.required} → ${after.required}`);
|
|
6722
|
-
if (Boolean(before.array) !== Boolean(after.array)) changes.push(`array: ${before.array ?? false} → ${after.array ?? false}`);
|
|
6723
|
-
if (Boolean(before.index) !== Boolean(after.index)) changes.push(`index: ${before.index ?? false} → ${after.index ?? false}`);
|
|
6724
|
-
if (Boolean(before.unique) !== Boolean(after.unique)) changes.push(`unique: ${before.unique ?? false} → ${after.unique ?? false}`);
|
|
6725
|
-
if (Boolean(before.vector) !== Boolean(after.vector)) changes.push(`vector: ${before.vector ?? false} → ${after.vector ?? false}`);
|
|
6726
|
-
const beforeAllowed = before.allowedValues ?? [];
|
|
6727
|
-
const afterAllowed = after.allowedValues ?? [];
|
|
6728
|
-
const afterSet = new Set(afterAllowed.map((v) => v.value));
|
|
6729
|
-
if (beforeAllowed.length !== afterAllowed.length || beforeAllowed.some((v) => !afterSet.has(v.value))) {
|
|
6730
|
-
const beforeValues = beforeAllowed.map((v) => v.value).join(", ");
|
|
6731
|
-
const afterValues = afterAllowed.map((v) => v.value).join(", ");
|
|
6732
|
-
changes.push(`allowedValues: [${beforeValues}] → [${afterValues}]`);
|
|
6733
|
-
}
|
|
6734
|
-
const beforeHooks = before.hooks;
|
|
6735
|
-
const afterHooks = after.hooks;
|
|
6736
|
-
if ((beforeHooks?.create?.expr ?? "") !== (afterHooks?.create?.expr ?? "") || (beforeHooks?.update?.expr ?? "") !== (afterHooks?.update?.expr ?? "")) changes.push("hooks modified");
|
|
6737
|
-
const beforeValidate = before.validate ?? [];
|
|
6738
|
-
const afterValidate = after.validate ?? [];
|
|
6739
|
-
if (beforeValidate.length !== afterValidate.length) changes.push(`validations: ${beforeValidate.length} → ${afterValidate.length}`);
|
|
6740
|
-
if (Boolean(before.serial) !== Boolean(after.serial)) changes.push(`serial: ${before.serial ? "enabled" : "disabled"} → ${after.serial ? "enabled" : "disabled"}`);
|
|
6741
|
-
return changes.join(", ");
|
|
6742
|
-
}
|
|
6743
|
-
/**
|
|
6744
|
-
* Format breaking changes for display
|
|
6745
|
-
* @param {BreakingChangeInfo[]} breakingChanges - Breaking changes to format
|
|
6746
|
-
* @returns {string} Formatted breaking changes string
|
|
6747
|
-
*/
|
|
6748
|
-
function formatBreakingChanges(breakingChanges) {
|
|
6749
|
-
if (breakingChanges.length === 0) return "";
|
|
6750
|
-
const lines = ["Breaking changes detected:", ""];
|
|
6751
|
-
for (const bc of breakingChanges) {
|
|
6752
|
-
const location = bc.fieldName ? `${bc.typeName}.${bc.fieldName}` : bc.typeName;
|
|
6753
|
-
lines.push(` - ${location}: ${bc.reason}`);
|
|
6754
|
-
}
|
|
6755
|
-
return lines.join("\n");
|
|
6756
|
-
}
|
|
6757
|
-
/**
|
|
6758
|
-
* Format warning changes for display
|
|
6759
|
-
* @param {WarningChangeInfo[]} warnings - Warning changes to format
|
|
6760
|
-
* @returns {string} Formatted warning changes string
|
|
6761
|
-
*/
|
|
6762
|
-
function formatWarnings(warnings) {
|
|
6763
|
-
if (warnings.length === 0) return "";
|
|
6764
|
-
const lines = ["Warning: data loss possible:", ""];
|
|
6765
|
-
for (const w of warnings) {
|
|
6766
|
-
const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
|
|
6767
|
-
lines.push(` - ${location}: ${w.reason}`);
|
|
6768
|
-
}
|
|
6769
|
-
return lines.join("\n");
|
|
6770
|
-
}
|
|
6771
|
-
const DIFF_CHANGE_LABELS = {
|
|
6772
|
-
type_added: "type(s) added",
|
|
6773
|
-
type_removed: "type(s) removed",
|
|
6774
|
-
type_modified: "type(s) modified",
|
|
6775
|
-
type_settings_modified: "type setting(s) modified",
|
|
6776
|
-
field_added: "field(s) added",
|
|
6777
|
-
field_removed: "field(s) removed",
|
|
6778
|
-
field_modified: "field(s) modified",
|
|
6779
|
-
index_added: "index(es) added",
|
|
6780
|
-
index_removed: "index(es) removed",
|
|
6781
|
-
index_modified: "index(es) modified",
|
|
6782
|
-
file_added: "file field(s) added",
|
|
6783
|
-
file_removed: "file field(s) removed",
|
|
6784
|
-
file_modified: "file field(s) modified",
|
|
6785
|
-
relationship_added: "relationship(s) added",
|
|
6786
|
-
relationship_removed: "relationship(s) removed",
|
|
6787
|
-
relationship_modified: "relationship(s) modified",
|
|
6788
|
-
permission_modified: "permission(s) modified",
|
|
6789
|
-
type_scripts_modified: "type script(s) modified"
|
|
6790
|
-
};
|
|
6791
|
-
/**
|
|
6792
|
-
* Format a summary of the migration diff
|
|
6793
|
-
* @param {MigrationDiff} diff - Migration diff to summarize
|
|
6794
|
-
* @returns {string} Formatted summary string
|
|
6795
|
-
*/
|
|
6796
|
-
function formatDiffSummary(diff) {
|
|
6797
|
-
const stats = {};
|
|
6798
|
-
for (const change of diff.changes) stats[change.kind] = (stats[change.kind] ?? 0) + 1;
|
|
6799
|
-
const parts = Object.keys(stats).map((kind) => `${stats[kind]} ${DIFF_CHANGE_LABELS[kind]}`);
|
|
6800
|
-
return parts.length > 0 ? parts.join(", ") : "No changes";
|
|
6801
|
-
}
|
|
6802
|
-
|
|
6803
|
-
//#endregion
|
|
6804
|
-
//#region src/parser/service/tailordb/type-script.ts
|
|
6805
|
-
const INPUT = "_input";
|
|
6806
|
-
const NEW_RECORD = "_newRecord";
|
|
6807
|
-
const OLD_RECORD = "_oldRecord";
|
|
6808
|
-
const NOW = "_now";
|
|
6809
|
-
const SOURCE_HASH_PREFIX = "// @sdk-source-hash:";
|
|
6810
|
-
const TIME_TYPES = /* @__PURE__ */ new Set([
|
|
6811
|
-
"datetime",
|
|
6812
|
-
"date",
|
|
6813
|
-
"time"
|
|
6814
|
-
]);
|
|
6815
|
-
const key = (name) => JSON.stringify(name);
|
|
6816
|
-
function collectFieldScriptSources(fields) {
|
|
6817
|
-
const entries = [];
|
|
6818
|
-
for (const [name, config] of Object.entries(fields).toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
6819
|
-
const data = {};
|
|
6820
|
-
if (config.hooks?.create?.expr) data.hc = config.hooks.create.expr;
|
|
6821
|
-
if (config.hooks?.update?.expr) data.hu = config.hooks.update.expr;
|
|
6822
|
-
if (config.validate?.some((v) => v.script?.expr)) data.v = config.validate.filter((v) => v.script?.expr).map((v) => [v.script?.expr, v.errorMessage]);
|
|
6823
|
-
if (config.default !== void 0) {
|
|
6824
|
-
data.d = config.default instanceof Date ? config.default.toISOString() : config.default;
|
|
6825
|
-
data.t = config.type;
|
|
7257
|
+
|
|
7258
|
+
//#endregion
|
|
7259
|
+
//#region src/parser/service/tailordb/type-script.ts
|
|
7260
|
+
const INPUT = "_input";
|
|
7261
|
+
const NEW_RECORD = "_newRecord";
|
|
7262
|
+
const OLD_RECORD = "_oldRecord";
|
|
7263
|
+
const NOW = "_now";
|
|
7264
|
+
const SOURCE_HASH_PREFIX = "// @sdk-source-hash:";
|
|
7265
|
+
const TIME_TYPES = /* @__PURE__ */ new Set([
|
|
7266
|
+
"datetime",
|
|
7267
|
+
"date",
|
|
7268
|
+
"time"
|
|
7269
|
+
]);
|
|
7270
|
+
const key = (name) => JSON.stringify(name);
|
|
7271
|
+
function collectFieldScriptSources(fields) {
|
|
7272
|
+
const entries = [];
|
|
7273
|
+
for (const [name, config] of Object.entries(fields).toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
7274
|
+
const data = {};
|
|
7275
|
+
if (config.hooks?.create?.expr) data.hc = config.hooks.create.expr;
|
|
7276
|
+
if (config.hooks?.update?.expr) data.hu = config.hooks.update.expr;
|
|
7277
|
+
if (config.validate?.some((v) => v.script?.expr)) data.v = config.validate.filter((v) => v.script?.expr).map((v) => [v.script?.expr, v.errorMessage]);
|
|
7278
|
+
if (config.default !== void 0) {
|
|
7279
|
+
data.d = config.default instanceof Date ? config.default.toISOString() : config.default;
|
|
7280
|
+
data.t = config.type;
|
|
6826
7281
|
}
|
|
6827
7282
|
if (config.fields) {
|
|
6828
7283
|
const nested = collectFieldScriptSources(config.fields);
|
|
@@ -6976,77 +7431,244 @@ function buildTypeScripts(fields, options) {
|
|
|
6976
7431
|
}
|
|
6977
7432
|
|
|
6978
7433
|
//#endregion
|
|
6979
|
-
//#region src/cli/commands/tailordb/migrate/
|
|
7434
|
+
//#region src/cli/commands/tailordb/migrate/diff-calculator.ts
|
|
6980
7435
|
/**
|
|
6981
|
-
*
|
|
6982
|
-
* @param
|
|
6983
|
-
* @returns
|
|
7436
|
+
* Check if a migration diff has any changes
|
|
7437
|
+
* @param {MigrationDiff} diff - Migration diff to check
|
|
7438
|
+
* @returns {boolean} True if diff has changes
|
|
6984
7439
|
*/
|
|
6985
|
-
function
|
|
6986
|
-
return
|
|
7440
|
+
function hasChanges(diff) {
|
|
7441
|
+
return diff.changes.length > 0;
|
|
6987
7442
|
}
|
|
6988
7443
|
/**
|
|
6989
|
-
*
|
|
6990
|
-
*
|
|
6991
|
-
*
|
|
6992
|
-
* leading zeros ("0"–"9999"). Commands that disallow the baseline reject
|
|
6993
|
-
* 0 themselves with a context-specific message.
|
|
6994
|
-
* @param numberStr - Raw CLI argument
|
|
6995
|
-
* @returns Parsed migration number
|
|
7444
|
+
* Format a migration diff for display
|
|
7445
|
+
* @param {MigrationDiff} diff - Migration diff to format
|
|
7446
|
+
* @returns {string} Formatted diff string
|
|
6996
7447
|
*/
|
|
6997
|
-
function
|
|
6998
|
-
if (
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7448
|
+
function formatMigrationDiff(diff) {
|
|
7449
|
+
if (diff.changes.length === 0) return "No schema differences detected.";
|
|
7450
|
+
const lines = [];
|
|
7451
|
+
const changesByType = /* @__PURE__ */ new Map();
|
|
7452
|
+
for (const change of diff.changes) {
|
|
7453
|
+
const existing = changesByType.get(change.typeName) ?? [];
|
|
7454
|
+
existing.push(change);
|
|
7455
|
+
changesByType.set(change.typeName, existing);
|
|
7003
7456
|
}
|
|
7004
|
-
|
|
7457
|
+
for (const [typeName, changes] of changesByType) {
|
|
7458
|
+
lines.push(`${diff.namespace}.${typeName}:`);
|
|
7459
|
+
for (const change of changes) lines.push(formatDiffChange(change));
|
|
7460
|
+
}
|
|
7461
|
+
return lines.join("\n");
|
|
7005
7462
|
}
|
|
7006
|
-
|
|
7007
|
-
//#endregion
|
|
7008
|
-
//#region src/cli/commands/tailordb/migrate/snapshot-schema.ts
|
|
7009
7463
|
/**
|
|
7010
|
-
*
|
|
7011
|
-
*
|
|
7012
|
-
*
|
|
7013
|
-
* snapshot-types.ts / diff-calculator.ts. Schemas are cast to
|
|
7014
|
-
* `z.ZodType<T>` to keep them aligned with the interfaces at compile time.
|
|
7015
|
-
*
|
|
7016
|
-
* All object schemas use `z.looseObject` so that unknown keys written
|
|
7017
|
-
* by newer CLI versions survive a load → save round-trip.
|
|
7464
|
+
* Format a single diff change for display
|
|
7465
|
+
* @param {DiffChange} change - Diff change to format
|
|
7466
|
+
* @returns {string} Formatted change string
|
|
7018
7467
|
*/
|
|
7019
|
-
function
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
});
|
|
7029
|
-
continue;
|
|
7030
|
-
}
|
|
7031
|
-
record[key] = result.data;
|
|
7468
|
+
function formatDiffChange(change) {
|
|
7469
|
+
switch (change.kind) {
|
|
7470
|
+
case "type_added": return ` + [Type] ${change.typeName} (new type)`;
|
|
7471
|
+
case "type_removed": return ` - [Type] ${change.typeName} (removed)`;
|
|
7472
|
+
case "type_modified": return ` ~ [Type] ${change.typeName}: ${change.reason}`;
|
|
7473
|
+
case "type_settings_modified": return ` ~ [Type Settings] ${change.typeName}: ${change.reason ?? "settings changed"}`;
|
|
7474
|
+
case "field_added": {
|
|
7475
|
+
const typeStr = formatFieldType(change.after);
|
|
7476
|
+
return ` + ${change.fieldName}: ${typeStr}`;
|
|
7032
7477
|
}
|
|
7033
|
-
return
|
|
7034
|
-
|
|
7478
|
+
case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
|
|
7479
|
+
case "field_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
|
|
7480
|
+
case "index_added": return ` + [Index] ${change.indexName}`;
|
|
7481
|
+
case "index_removed": return ` - [Index] ${change.indexName}`;
|
|
7482
|
+
case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
|
|
7483
|
+
case "file_added": return ` + [File] ${change.fieldName}`;
|
|
7484
|
+
case "file_removed": return ` - [File] ${change.fieldName}`;
|
|
7485
|
+
case "file_modified": return ` ~ [File] ${change.fieldName}: ${change.reason ?? "modified"}`;
|
|
7486
|
+
case "relationship_added": return ` + [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
|
|
7487
|
+
case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
|
|
7488
|
+
case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
|
|
7489
|
+
case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
|
|
7490
|
+
case "type_scripts_modified": return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`;
|
|
7491
|
+
default: {
|
|
7492
|
+
const unknown = change;
|
|
7493
|
+
return ` ? ${unknown.typeName}.${unknown.fieldName ?? ""}`;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7035
7496
|
}
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7497
|
+
/**
|
|
7498
|
+
* Format field type with attributes
|
|
7499
|
+
* @param {SnapshotFieldConfig} field - Field configuration
|
|
7500
|
+
* @returns {string} Formatted field type string
|
|
7501
|
+
*/
|
|
7502
|
+
function formatFieldType(field) {
|
|
7503
|
+
let type = field.type;
|
|
7504
|
+
if (field.array) type += "[]";
|
|
7505
|
+
if (field.required) type += " (required)";
|
|
7506
|
+
else type += " (optional)";
|
|
7507
|
+
return type;
|
|
7508
|
+
}
|
|
7509
|
+
/**
|
|
7510
|
+
* Format field modification details
|
|
7511
|
+
* @param {SnapshotFieldConfig} before - Before field configuration
|
|
7512
|
+
* @param {SnapshotFieldConfig} after - After field configuration
|
|
7513
|
+
* @returns {string} Formatted modification details
|
|
7514
|
+
*/
|
|
7515
|
+
function formatFieldModification(before, after) {
|
|
7516
|
+
const changes = [];
|
|
7517
|
+
if (before.type !== after.type) changes.push(`type: ${before.type} → ${after.type}`);
|
|
7518
|
+
if (before.required !== after.required) changes.push(`required: ${before.required} → ${after.required}`);
|
|
7519
|
+
if (Boolean(before.array) !== Boolean(after.array)) changes.push(`array: ${before.array ?? false} → ${after.array ?? false}`);
|
|
7520
|
+
if (Boolean(before.index) !== Boolean(after.index)) changes.push(`index: ${before.index ?? false} → ${after.index ?? false}`);
|
|
7521
|
+
if (Boolean(before.unique) !== Boolean(after.unique)) changes.push(`unique: ${before.unique ?? false} → ${after.unique ?? false}`);
|
|
7522
|
+
if (Boolean(before.vector) !== Boolean(after.vector)) changes.push(`vector: ${before.vector ?? false} → ${after.vector ?? false}`);
|
|
7523
|
+
const beforeAllowed = before.allowedValues ?? [];
|
|
7524
|
+
const afterAllowed = after.allowedValues ?? [];
|
|
7525
|
+
const afterSet = new Set(afterAllowed.map((v) => v.value));
|
|
7526
|
+
if (beforeAllowed.length !== afterAllowed.length || beforeAllowed.some((v) => !afterSet.has(v.value))) {
|
|
7527
|
+
const beforeValues = beforeAllowed.map((v) => v.value).join(", ");
|
|
7528
|
+
const afterValues = afterAllowed.map((v) => v.value).join(", ");
|
|
7529
|
+
changes.push(`allowedValues: [${beforeValues}] → [${afterValues}]`);
|
|
7530
|
+
}
|
|
7531
|
+
const beforeHooks = before.hooks;
|
|
7532
|
+
const afterHooks = after.hooks;
|
|
7533
|
+
if ((beforeHooks?.create?.expr ?? "") !== (afterHooks?.create?.expr ?? "") || (beforeHooks?.update?.expr ?? "") !== (afterHooks?.update?.expr ?? "")) changes.push("hooks modified");
|
|
7534
|
+
const beforeValidate = before.validate ?? [];
|
|
7535
|
+
const afterValidate = after.validate ?? [];
|
|
7536
|
+
if (beforeValidate.length !== afterValidate.length) changes.push(`validations: ${beforeValidate.length} → ${afterValidate.length}`);
|
|
7537
|
+
if (Boolean(before.serial) !== Boolean(after.serial)) changes.push(`serial: ${before.serial ? "enabled" : "disabled"} → ${after.serial ? "enabled" : "disabled"}`);
|
|
7538
|
+
return changes.join(", ");
|
|
7539
|
+
}
|
|
7540
|
+
/**
|
|
7541
|
+
* Format breaking changes for display
|
|
7542
|
+
* @param {BreakingChangeInfo[]} breakingChanges - Breaking changes to format
|
|
7543
|
+
* @returns {string} Formatted breaking changes string
|
|
7544
|
+
*/
|
|
7545
|
+
function formatBreakingChanges(breakingChanges) {
|
|
7546
|
+
if (breakingChanges.length === 0) return "";
|
|
7547
|
+
const lines = ["Breaking changes detected:", ""];
|
|
7548
|
+
for (const bc of breakingChanges) {
|
|
7549
|
+
const location = bc.fieldName ? `${bc.typeName}.${bc.fieldName}` : bc.typeName;
|
|
7550
|
+
lines.push(` - ${location}: ${bc.reason}`);
|
|
7551
|
+
}
|
|
7552
|
+
return lines.join("\n");
|
|
7553
|
+
}
|
|
7554
|
+
/**
|
|
7555
|
+
* Format warning changes for display
|
|
7556
|
+
* @param {WarningChangeInfo[]} warnings - Warning changes to format
|
|
7557
|
+
* @returns {string} Formatted warning changes string
|
|
7558
|
+
*/
|
|
7559
|
+
function formatWarnings(warnings) {
|
|
7560
|
+
if (warnings.length === 0) return "";
|
|
7561
|
+
const lines = ["Warning: data loss possible:", ""];
|
|
7562
|
+
for (const w of warnings) {
|
|
7563
|
+
const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
|
|
7564
|
+
lines.push(` - ${location}: ${w.reason}`);
|
|
7565
|
+
}
|
|
7566
|
+
return lines.join("\n");
|
|
7567
|
+
}
|
|
7568
|
+
const DIFF_CHANGE_LABELS = {
|
|
7569
|
+
type_added: "type(s) added",
|
|
7570
|
+
type_removed: "type(s) removed",
|
|
7571
|
+
type_modified: "type(s) modified",
|
|
7572
|
+
type_settings_modified: "type setting(s) modified",
|
|
7573
|
+
field_added: "field(s) added",
|
|
7574
|
+
field_removed: "field(s) removed",
|
|
7575
|
+
field_modified: "field(s) modified",
|
|
7576
|
+
index_added: "index(es) added",
|
|
7577
|
+
index_removed: "index(es) removed",
|
|
7578
|
+
index_modified: "index(es) modified",
|
|
7579
|
+
file_added: "file field(s) added",
|
|
7580
|
+
file_removed: "file field(s) removed",
|
|
7581
|
+
file_modified: "file field(s) modified",
|
|
7582
|
+
relationship_added: "relationship(s) added",
|
|
7583
|
+
relationship_removed: "relationship(s) removed",
|
|
7584
|
+
relationship_modified: "relationship(s) modified",
|
|
7585
|
+
permission_modified: "permission(s) modified",
|
|
7586
|
+
type_scripts_modified: "type script(s) modified"
|
|
7587
|
+
};
|
|
7588
|
+
/**
|
|
7589
|
+
* Format a summary of the migration diff
|
|
7590
|
+
* @param {MigrationDiff} diff - Migration diff to summarize
|
|
7591
|
+
* @returns {string} Formatted summary string
|
|
7592
|
+
*/
|
|
7593
|
+
function formatDiffSummary(diff) {
|
|
7594
|
+
const stats = {};
|
|
7595
|
+
for (const change of diff.changes) stats[change.kind] = (stats[change.kind] ?? 0) + 1;
|
|
7596
|
+
const parts = Object.keys(stats).map((kind) => `${stats[kind]} ${DIFF_CHANGE_LABELS[kind]}`);
|
|
7597
|
+
return parts.length > 0 ? parts.join(", ") : "No changes";
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
//#endregion
|
|
7601
|
+
//#region src/cli/commands/tailordb/migrate/migration-number.ts
|
|
7602
|
+
/**
|
|
7603
|
+
* Format migration number as 4-digit string.
|
|
7604
|
+
* @param num - Migration number
|
|
7605
|
+
* @returns 4-digit padded string
|
|
7606
|
+
*/
|
|
7607
|
+
function formatMigrationNumber(num) {
|
|
7608
|
+
return num.toString().padStart(4, "0");
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Parse a migration number CLI argument.
|
|
7612
|
+
*
|
|
7613
|
+
* Accepts the canonical 4-digit form ("0001") or a bare integer without
|
|
7614
|
+
* leading zeros ("0"–"9999"). Commands that disallow the baseline reject
|
|
7615
|
+
* 0 themselves with a context-specific message.
|
|
7616
|
+
* @param numberStr - Raw CLI argument
|
|
7617
|
+
* @returns Parsed migration number
|
|
7618
|
+
*/
|
|
7619
|
+
function parseMigrationNumberArg(numberStr) {
|
|
7620
|
+
if (/^\d{4}$/.test(numberStr)) return parseInt(numberStr, 10);
|
|
7621
|
+
if (/^(0|[1-9]\d*)$/.test(numberStr)) {
|
|
7622
|
+
const parsed = parseInt(numberStr, 10);
|
|
7623
|
+
if (parsed > 9999) throw new Error(`Migration number ${numberStr} is out of range. Expected 0-9999.`);
|
|
7624
|
+
return parsed;
|
|
7625
|
+
}
|
|
7626
|
+
throw new Error(`Invalid migration number format: ${numberStr}. Expected 4-digit format (e.g., 0001) or integer 0-9999 (e.g., 1).`);
|
|
7627
|
+
}
|
|
7628
|
+
|
|
7629
|
+
//#endregion
|
|
7630
|
+
//#region src/cli/commands/tailordb/migrate/snapshot-schema.ts
|
|
7631
|
+
/**
|
|
7632
|
+
* Zod schemas for TailorDB migration snapshot and diff files.
|
|
7633
|
+
*
|
|
7634
|
+
* Each schema mirrors the corresponding hand-written interface from
|
|
7635
|
+
* snapshot-types.ts / diff-calculator.ts. Schemas are cast to
|
|
7636
|
+
* `z.ZodType<T>` to keep them aligned with the interfaces at compile time.
|
|
7637
|
+
*
|
|
7638
|
+
* All object schemas use `z.looseObject` so that unknown keys written
|
|
7639
|
+
* by newer CLI versions survive a load → save round-trip.
|
|
7640
|
+
*/
|
|
7641
|
+
function snapshotRecordSchema(valueSchema) {
|
|
7642
|
+
return z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), { message: "Expected record" }).transform((value, ctx) => {
|
|
7643
|
+
const record = Object.create(null);
|
|
7644
|
+
for (const key of Object.keys(value)) {
|
|
7645
|
+
const result = valueSchema.safeParse(value[key]);
|
|
7646
|
+
if (!result.success) {
|
|
7647
|
+
for (const issue of result.error.issues) ctx.addIssue({
|
|
7648
|
+
...issue,
|
|
7649
|
+
path: [key, ...issue.path]
|
|
7650
|
+
});
|
|
7651
|
+
continue;
|
|
7652
|
+
}
|
|
7653
|
+
record[key] = result.data;
|
|
7654
|
+
}
|
|
7655
|
+
return record;
|
|
7656
|
+
});
|
|
7657
|
+
}
|
|
7658
|
+
const snapshotHookSchema = z.looseObject({ expr: z.string() });
|
|
7659
|
+
const snapshotValidationSchema = z.looseObject({
|
|
7660
|
+
script: z.looseObject({ expr: z.string() }).optional(),
|
|
7661
|
+
errorMessage: z.string()
|
|
7662
|
+
});
|
|
7663
|
+
const snapshotSerialSchema = z.looseObject({
|
|
7664
|
+
start: z.number(),
|
|
7665
|
+
maxValue: z.number().optional(),
|
|
7666
|
+
format: z.string().optional()
|
|
7667
|
+
});
|
|
7668
|
+
const snapshotEnumValueSchema = z.looseObject({
|
|
7669
|
+
value: z.string(),
|
|
7670
|
+
description: z.string().optional()
|
|
7671
|
+
});
|
|
7050
7672
|
const snapshotFieldConfigSchema = z.looseObject({
|
|
7051
7673
|
type: z.string(),
|
|
7052
7674
|
required: z.boolean().default(true),
|
|
@@ -7960,11 +8582,29 @@ function reconstructSnapshotFromMigrations(migrationsDir, maxVersion) {
|
|
|
7960
8582
|
* @returns {number} Latest migration number or 0 if no migrations exist
|
|
7961
8583
|
*/
|
|
7962
8584
|
function getLatestMigrationNumber(migrationsDir) {
|
|
7963
|
-
|
|
8585
|
+
return latestMigrationNumber(getMigrationFiles(migrationsDir));
|
|
8586
|
+
}
|
|
8587
|
+
function latestMigrationNumber(files) {
|
|
7964
8588
|
if (files.length === 0) return 0;
|
|
7965
8589
|
return Math.max(...files.map((f) => f.number));
|
|
7966
8590
|
}
|
|
7967
8591
|
/**
|
|
8592
|
+
* Assert that a migration number exists in the local migration history.
|
|
8593
|
+
* 0 is always accepted as the baseline snapshot.
|
|
8594
|
+
*
|
|
8595
|
+
* Returns the latest migration number so callers that need it (e.g. sync's
|
|
8596
|
+
* post-sync hint) can reuse this function's directory scan instead of
|
|
8597
|
+
* scanning the migrations directory a second time.
|
|
8598
|
+
* @param {string} migrationsDir - Migrations directory path
|
|
8599
|
+
* @param {number} migrationNumber - Migration number to check
|
|
8600
|
+
* @returns {number} The latest migration number in the history
|
|
8601
|
+
*/
|
|
8602
|
+
function assertMigrationNumberExists(migrationsDir, migrationNumber) {
|
|
8603
|
+
const files = getMigrationFiles(migrationsDir);
|
|
8604
|
+
if (migrationNumber !== 0 && !files.some((f) => f.number === migrationNumber)) throw new Error(`Migration ${formatMigrationNumber(migrationNumber)} does not exist in working tree (latest is ${formatMigrationNumber(latestMigrationNumber(files))}).`);
|
|
8605
|
+
return latestMigrationNumber(files);
|
|
8606
|
+
}
|
|
8607
|
+
/**
|
|
7968
8608
|
* Compare two field configs and determine if they are different
|
|
7969
8609
|
* @param {SnapshotFieldConfig} oldField - Old field configuration
|
|
7970
8610
|
* @param {SnapshotFieldConfig} newField - New field configuration
|
|
@@ -9206,7 +9846,11 @@ function generateTailorDBTypeManifestFromSnapshot(snapshotType, options = {}) {
|
|
|
9206
9846
|
defaultQueryLimitSize: 100n,
|
|
9207
9847
|
maxBulkUpsertSize: 1000n,
|
|
9208
9848
|
pluralForm,
|
|
9209
|
-
publishRecordEvents:
|
|
9849
|
+
publishRecordEvents: resolvePublishEvents({
|
|
9850
|
+
explicit: snapshotType.settings?.publishEvents,
|
|
9851
|
+
subscribed: options.subscribed ?? false,
|
|
9852
|
+
conflict: publishEventsConflict.tailorDBType(snapshotType.name)
|
|
9853
|
+
})
|
|
9210
9854
|
};
|
|
9211
9855
|
const ops = snapshotType.settings?.gqlOperations ?? options.namespaceGqlOperations;
|
|
9212
9856
|
if (ops) defaultSettings.disableGqlOperations = {
|
|
@@ -9473,14 +10117,9 @@ function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
|
|
|
9473
10117
|
const manifests = /* @__PURE__ */ new Map();
|
|
9474
10118
|
const { executorUsedTypes, ...baseOptions } = options;
|
|
9475
10119
|
for (const [typeName, snapshotType] of Object.entries(snapshot.types)) {
|
|
9476
|
-
if (executorUsedTypes?.has(typeName) && snapshotType.settings?.publishEvents === false) throw new Error(`Type "${typeName}" has publishEvents set to false, but it is used by an executor with a record trigger. Either remove the publishEvents: false setting or remove the executor trigger for this type.`);
|
|
9477
|
-
let publishRecordEvents;
|
|
9478
|
-
if (snapshotType.settings?.publishEvents !== void 0) publishRecordEvents = snapshotType.settings.publishEvents;
|
|
9479
|
-
else if (executorUsedTypes?.has(typeName)) publishRecordEvents = true;
|
|
9480
|
-
else publishRecordEvents = baseOptions.publishRecordEvents ?? false;
|
|
9481
10120
|
const typeOptions = {
|
|
9482
10121
|
...baseOptions,
|
|
9483
|
-
|
|
10122
|
+
subscribed: executorUsedTypes?.has(typeName) ?? false
|
|
9484
10123
|
};
|
|
9485
10124
|
manifests.set(typeName, generateTailorDBTypeManifestFromSnapshot(snapshotType, typeOptions));
|
|
9486
10125
|
}
|
|
@@ -9751,6 +10390,11 @@ function applyPreMigrationIndexAdjustments(indexes, typeIndexChanges) {
|
|
|
9751
10390
|
* Types for TailorDB migration execution
|
|
9752
10391
|
*/
|
|
9753
10392
|
/**
|
|
10393
|
+
* Maximum length for Kubernetes label values
|
|
10394
|
+
* Labels must match pattern: ^[a-z][a-z0-9_-]{0,62}
|
|
10395
|
+
*/
|
|
10396
|
+
const MAX_LABEL_LENGTH = 63;
|
|
10397
|
+
/**
|
|
9754
10398
|
* Prefix added to migration numbers in labels (required because migration names start with numbers)
|
|
9755
10399
|
*/
|
|
9756
10400
|
const MIGRATION_LABEL_PREFIX = "m";
|
|
@@ -9759,12 +10403,23 @@ const MIGRATION_LABEL_PREFIX = "m";
|
|
|
9759
10403
|
*/
|
|
9760
10404
|
const MIGRATION_LABEL_KEY = "sdk-migration";
|
|
9761
10405
|
/**
|
|
10406
|
+
* Sanitize migration number for use as label value
|
|
10407
|
+
* Label pattern: ^[a-z][a-z0-9_-]{0,62}
|
|
10408
|
+
* - Must start with lowercase letter (add prefix since migration numbers start with digits)
|
|
10409
|
+
* - Max 63 characters
|
|
10410
|
+
* @param {number} migrationNumber - Migration number to sanitize
|
|
10411
|
+
* @returns {string} Sanitized label value
|
|
10412
|
+
*/
|
|
10413
|
+
function sanitizeMigrationLabel(migrationNumber) {
|
|
10414
|
+
return (MIGRATION_LABEL_PREFIX + formatMigrationNumber(migrationNumber)).slice(0, 63);
|
|
10415
|
+
}
|
|
10416
|
+
/**
|
|
9762
10417
|
* Parse migration number from label value
|
|
9763
10418
|
* @param {string} label - Label value (e.g., "m0001")
|
|
9764
10419
|
* @returns {number | null} Parsed number or null if invalid
|
|
9765
10420
|
*/
|
|
9766
10421
|
function parseMigrationLabelNumber(label) {
|
|
9767
|
-
if (!label.startsWith(
|
|
10422
|
+
if (!label.startsWith(MIGRATION_LABEL_PREFIX)) return null;
|
|
9768
10423
|
const numStr = label.slice(1);
|
|
9769
10424
|
if (!/^\d+$/.test(numStr)) return null;
|
|
9770
10425
|
const num = parseInt(numStr, 10);
|
|
@@ -9784,6 +10439,283 @@ function handleOptionalToRequiredError(error, messages) {
|
|
|
9784
10439
|
throw error;
|
|
9785
10440
|
}
|
|
9786
10441
|
|
|
10442
|
+
//#endregion
|
|
10443
|
+
//#region src/cli/commands/tailordb/migrate/schema-checks.ts
|
|
10444
|
+
/**
|
|
10445
|
+
* Schema validation checks shared between `deploy` and
|
|
10446
|
+
* `tailordb migration validate`.
|
|
10447
|
+
*
|
|
10448
|
+
* These checks are read-only: they compare local type definitions, the
|
|
10449
|
+
* migration snapshot history, and the remote schema without mutating any
|
|
10450
|
+
* state.
|
|
10451
|
+
*/
|
|
10452
|
+
/**
|
|
10453
|
+
* Convert a runtime TailorDBService to the snapshot-shaped deploy input.
|
|
10454
|
+
* @param service - Loaded TailorDB service (after `loadTypes()`)
|
|
10455
|
+
* @returns The canonical snapshot-shaped deploy input for downstream plan/apply phases.
|
|
10456
|
+
*/
|
|
10457
|
+
function toTailorDBDeployInput(service) {
|
|
10458
|
+
const types = {};
|
|
10459
|
+
for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
|
|
10460
|
+
return {
|
|
10461
|
+
namespace: service.namespace,
|
|
10462
|
+
config: service.config,
|
|
10463
|
+
types
|
|
10464
|
+
};
|
|
10465
|
+
}
|
|
10466
|
+
/**
|
|
10467
|
+
* Fetch all TailorDB types from remote for a namespace
|
|
10468
|
+
* @param {OperatorClient} client - Operator client instance
|
|
10469
|
+
* @param {string} workspaceId - Workspace ID
|
|
10470
|
+
* @param {string} namespace - TailorDB namespace
|
|
10471
|
+
* @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
|
|
10472
|
+
*/
|
|
10473
|
+
async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
10474
|
+
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
10475
|
+
const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
|
|
10476
|
+
workspaceId,
|
|
10477
|
+
namespaceName: namespace,
|
|
10478
|
+
pageToken,
|
|
10479
|
+
pageSize: maxPageSize
|
|
10480
|
+
});
|
|
10481
|
+
return [tailordbTypes, nextPageToken];
|
|
10482
|
+
});
|
|
10483
|
+
}
|
|
10484
|
+
async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
|
|
10485
|
+
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
10486
|
+
const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
|
|
10487
|
+
workspaceId,
|
|
10488
|
+
namespaceName: namespace,
|
|
10489
|
+
pageToken,
|
|
10490
|
+
pageSize: maxPageSize
|
|
10491
|
+
});
|
|
10492
|
+
return [permissions, nextPageToken];
|
|
10493
|
+
});
|
|
10494
|
+
}
|
|
10495
|
+
function definedWhenNotEmpty(value) {
|
|
10496
|
+
return Object.keys(value).length > 0 ? value : void 0;
|
|
10497
|
+
}
|
|
10498
|
+
function namespaceConfig(config, tailorDBInputs, namespace) {
|
|
10499
|
+
return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
|
|
10500
|
+
}
|
|
10501
|
+
function namespaceGqlOperations(config, tailorDBInputs, namespace) {
|
|
10502
|
+
return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
|
|
10503
|
+
}
|
|
10504
|
+
const GQL_OPERATION_KEYS = [
|
|
10505
|
+
"create",
|
|
10506
|
+
"update",
|
|
10507
|
+
"delete",
|
|
10508
|
+
"read"
|
|
10509
|
+
];
|
|
10510
|
+
function configuredDisabledGqlOperations(operations) {
|
|
10511
|
+
if (!operations) return void 0;
|
|
10512
|
+
if (operations === "query") return {
|
|
10513
|
+
create: false,
|
|
10514
|
+
update: false,
|
|
10515
|
+
delete: false
|
|
10516
|
+
};
|
|
10517
|
+
const disabled = {};
|
|
10518
|
+
for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
|
|
10519
|
+
return definedWhenNotEmpty(disabled);
|
|
10520
|
+
}
|
|
10521
|
+
function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
|
|
10522
|
+
if (!remoteDisabled || !configuredDisabled) return void 0;
|
|
10523
|
+
const disabled = {};
|
|
10524
|
+
for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
|
|
10525
|
+
return definedWhenNotEmpty(disabled);
|
|
10526
|
+
}
|
|
10527
|
+
function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
|
|
10528
|
+
const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
|
|
10529
|
+
const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
|
|
10530
|
+
const types = {};
|
|
10531
|
+
for (const [typeName, type] of Object.entries(snapshot.types)) {
|
|
10532
|
+
const settings = { ...type.settings };
|
|
10533
|
+
const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
|
|
10534
|
+
if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
|
|
10535
|
+
if (type.settings?.gqlOperations === void 0) {
|
|
10536
|
+
const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
|
|
10537
|
+
if (disabled) settings.gqlOperations = disabled;
|
|
10538
|
+
}
|
|
10539
|
+
const comparableType = { ...type };
|
|
10540
|
+
const comparableSettings = definedWhenNotEmpty(settings);
|
|
10541
|
+
if (comparableSettings) comparableType.settings = comparableSettings;
|
|
10542
|
+
else delete comparableType.settings;
|
|
10543
|
+
types[typeName] = comparableType;
|
|
10544
|
+
}
|
|
10545
|
+
return {
|
|
10546
|
+
...snapshot,
|
|
10547
|
+
types
|
|
10548
|
+
};
|
|
10549
|
+
}
|
|
10550
|
+
async function getRemoteMigrationNumberState(client, workspaceId, namespace) {
|
|
10551
|
+
const trn = resourceTrn(workspaceId, "tailordb", namespace);
|
|
10552
|
+
const metadata = await getOrNull(async () => {
|
|
10553
|
+
const { metadata } = await client.getMetadata({ trn });
|
|
10554
|
+
return metadata;
|
|
10555
|
+
});
|
|
10556
|
+
if (!metadata) return {
|
|
10557
|
+
metadataExists: false,
|
|
10558
|
+
number: null
|
|
10559
|
+
};
|
|
10560
|
+
const label = metadata.labels[MIGRATION_LABEL_KEY];
|
|
10561
|
+
if (!label) return {
|
|
10562
|
+
metadataExists: true,
|
|
10563
|
+
number: null
|
|
10564
|
+
};
|
|
10565
|
+
return {
|
|
10566
|
+
metadataExists: true,
|
|
10567
|
+
number: parseMigrationLabelNumber(label)
|
|
10568
|
+
};
|
|
10569
|
+
}
|
|
10570
|
+
/**
|
|
10571
|
+
* Get the current migration number from remote metadata
|
|
10572
|
+
* @param {OperatorClient} client - Operator client instance
|
|
10573
|
+
* @param {string} workspaceId - Workspace ID
|
|
10574
|
+
* @param {string} namespace - TailorDB namespace
|
|
10575
|
+
* @returns {Promise<number | null>} Current migration number, or null if no migration label exists
|
|
10576
|
+
*/
|
|
10577
|
+
async function getRemoteMigrationNumber(client, workspaceId, namespace) {
|
|
10578
|
+
return (await getRemoteMigrationNumberState(client, workspaceId, namespace)).number;
|
|
10579
|
+
}
|
|
10580
|
+
/**
|
|
10581
|
+
* Verify remote schema matches the expected snapshot state
|
|
10582
|
+
* @param {OperatorClient} client - Operator client instance
|
|
10583
|
+
* @param {string} workspaceId - Workspace ID
|
|
10584
|
+
* @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
|
|
10585
|
+
* @param {LoadedConfig} config - Loaded application config
|
|
10586
|
+
* @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
|
|
10587
|
+
* @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
|
|
10588
|
+
*/
|
|
10589
|
+
async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
|
|
10590
|
+
const results = [];
|
|
10591
|
+
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
10592
|
+
const { metadataExists, number: remoteMigrationNumber } = await getRemoteMigrationNumberState(client, workspaceId, namespace);
|
|
10593
|
+
if (remoteMigrationNumber === null) {
|
|
10594
|
+
results.push({
|
|
10595
|
+
namespace,
|
|
10596
|
+
remoteMigrationNumber: 0,
|
|
10597
|
+
drifts: [],
|
|
10598
|
+
hasDrift: false,
|
|
10599
|
+
skipped: metadataExists ? "no_migration_label" : "not_deployed"
|
|
10600
|
+
});
|
|
10601
|
+
continue;
|
|
10602
|
+
}
|
|
10603
|
+
if (remoteMigrationNumber > getLatestMigrationNumber(migrationsDir)) {
|
|
10604
|
+
results.push({
|
|
10605
|
+
namespace,
|
|
10606
|
+
remoteMigrationNumber,
|
|
10607
|
+
drifts: [],
|
|
10608
|
+
hasDrift: false,
|
|
10609
|
+
checkpointMissingLocal: true
|
|
10610
|
+
});
|
|
10611
|
+
continue;
|
|
10612
|
+
}
|
|
10613
|
+
const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
|
|
10614
|
+
if (!expectedSnapshot) {
|
|
10615
|
+
results.push({
|
|
10616
|
+
namespace,
|
|
10617
|
+
remoteMigrationNumber,
|
|
10618
|
+
drifts: [],
|
|
10619
|
+
hasDrift: false,
|
|
10620
|
+
skipped: "no_snapshot"
|
|
10621
|
+
});
|
|
10622
|
+
continue;
|
|
10623
|
+
}
|
|
10624
|
+
const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
|
|
10625
|
+
const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
|
|
10626
|
+
results.push({
|
|
10627
|
+
namespace,
|
|
10628
|
+
remoteMigrationNumber,
|
|
10629
|
+
drifts,
|
|
10630
|
+
hasDrift: drifts.length > 0
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
10633
|
+
return results;
|
|
10634
|
+
}
|
|
10635
|
+
/**
|
|
10636
|
+
* Log common causes of remote schema drift and how to resolve them
|
|
10637
|
+
*/
|
|
10638
|
+
function logRemoteDriftGuidance() {
|
|
10639
|
+
logger.info("This may indicate:");
|
|
10640
|
+
logger.info(" - Another developer applied different migrations", { mode: "plain" });
|
|
10641
|
+
logger.info(" - Manual schema changes were made directly", { mode: "plain" });
|
|
10642
|
+
logger.info(" - Migration history is out of sync", { mode: "plain" });
|
|
10643
|
+
logger.newline();
|
|
10644
|
+
logger.info("To resolve:");
|
|
10645
|
+
logger.info(" - Run 'tailor tailordb migration status' to compare local vs remote.", { mode: "plain" });
|
|
10646
|
+
logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
|
|
10647
|
+
logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
|
|
10648
|
+
logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
|
|
10649
|
+
}
|
|
10650
|
+
/**
|
|
10651
|
+
* Format remote schema verification results for display
|
|
10652
|
+
* @param {RemoteSchemaVerificationResult[]} results - Verification results
|
|
10653
|
+
* @returns {string} Formatted results string
|
|
10654
|
+
*/
|
|
10655
|
+
function formatRemoteVerificationResults(results) {
|
|
10656
|
+
const lines = [];
|
|
10657
|
+
for (const result of results) {
|
|
10658
|
+
if (!result.hasDrift) continue;
|
|
10659
|
+
lines.push(`Namespace: ${result.namespace}`);
|
|
10660
|
+
lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
|
|
10661
|
+
lines.push(` Differences:`);
|
|
10662
|
+
lines.push(formatSchemaDrifts(result.drifts));
|
|
10663
|
+
lines.push("");
|
|
10664
|
+
}
|
|
10665
|
+
return lines.join("\n");
|
|
10666
|
+
}
|
|
10667
|
+
/**
|
|
10668
|
+
* Check if there are schema differences between migration snapshots and local definitions
|
|
10669
|
+
* @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
|
|
10670
|
+
* @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
|
|
10671
|
+
* @returns {Promise<MigrationCheckResult[]>} Results for each namespace
|
|
10672
|
+
*/
|
|
10673
|
+
async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
|
|
10674
|
+
const results = [];
|
|
10675
|
+
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
10676
|
+
const localTypes = typesByNamespace.get(namespace);
|
|
10677
|
+
if (!localTypes) continue;
|
|
10678
|
+
const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
|
|
10679
|
+
if (!previousSnapshot) {
|
|
10680
|
+
results.push({
|
|
10681
|
+
namespace,
|
|
10682
|
+
migrationsDir,
|
|
10683
|
+
hasDiff: true,
|
|
10684
|
+
diff: void 0
|
|
10685
|
+
});
|
|
10686
|
+
continue;
|
|
10687
|
+
}
|
|
10688
|
+
const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
|
|
10689
|
+
results.push({
|
|
10690
|
+
namespace,
|
|
10691
|
+
migrationsDir,
|
|
10692
|
+
hasDiff: hasChanges(diff),
|
|
10693
|
+
diff: hasChanges(diff) ? diff : void 0
|
|
10694
|
+
});
|
|
10695
|
+
}
|
|
10696
|
+
return results;
|
|
10697
|
+
}
|
|
10698
|
+
/**
|
|
10699
|
+
* Format migration check results for display
|
|
10700
|
+
* @param {MigrationCheckResult[]} results - Migration check results
|
|
10701
|
+
* @returns {string} Formatted results string
|
|
10702
|
+
*/
|
|
10703
|
+
function formatMigrationCheckResults(results) {
|
|
10704
|
+
const lines = [];
|
|
10705
|
+
for (const result of results) {
|
|
10706
|
+
if (!result.hasDiff) continue;
|
|
10707
|
+
lines.push(`Namespace: ${result.namespace}`);
|
|
10708
|
+
if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor tailordb migration generate' first.");
|
|
10709
|
+
else {
|
|
10710
|
+
lines.push(` ${formatDiffSummary(result.diff)}`);
|
|
10711
|
+
lines.push("");
|
|
10712
|
+
lines.push(formatMigrationDiff(result.diff));
|
|
10713
|
+
}
|
|
10714
|
+
lines.push("");
|
|
10715
|
+
}
|
|
10716
|
+
return lines.join("\n");
|
|
10717
|
+
}
|
|
10718
|
+
|
|
9787
10719
|
//#endregion
|
|
9788
10720
|
//#region src/cli/commands/tailordb/migrate/bundler.ts
|
|
9789
10721
|
/**
|
|
@@ -9832,31 +10764,39 @@ async function bundleMigrationScript(sourceFile, namespace, migrationNumber, env
|
|
|
9832
10764
|
}
|
|
9833
10765
|
`;
|
|
9834
10766
|
fs$2.writeFileSync(entryPath, entryContent);
|
|
9835
|
-
const
|
|
10767
|
+
const projectDir = baseDir ?? path.dirname(absoluteSourcePath);
|
|
10768
|
+
const tsconfig = await resolveTSConfigWithFallback(projectDir);
|
|
10769
|
+
const bundleLog = createBundleLog({ tsconfig });
|
|
10770
|
+
const result = await rolldown.build({
|
|
10771
|
+
plugins: [
|
|
10772
|
+
createGeneratedEntryResolverPlugin(entryPath, projectDir),
|
|
10773
|
+
createTsconfigPathsPlugin(),
|
|
10774
|
+
platformBundleDefinePlugin
|
|
10775
|
+
],
|
|
10776
|
+
input: entryPath,
|
|
10777
|
+
write: false,
|
|
10778
|
+
output: {
|
|
10779
|
+
format: "esm",
|
|
10780
|
+
sourcemap: false,
|
|
10781
|
+
minify: false,
|
|
10782
|
+
codeSplitting: false,
|
|
10783
|
+
globals: { tailordb: "tailordb" }
|
|
10784
|
+
},
|
|
10785
|
+
external: ["tailordb"],
|
|
10786
|
+
resolve: { conditionNames: ["node", "import"] },
|
|
10787
|
+
tsconfig,
|
|
10788
|
+
treeshake: {
|
|
10789
|
+
moduleSideEffects: false,
|
|
10790
|
+
annotations: true,
|
|
10791
|
+
unknownGlobalSideEffects: false
|
|
10792
|
+
},
|
|
10793
|
+
...bundleLog.options
|
|
10794
|
+
});
|
|
10795
|
+
bundleLog.assertAllResolved();
|
|
9836
10796
|
return {
|
|
9837
10797
|
namespace,
|
|
9838
10798
|
migrationNumber,
|
|
9839
|
-
bundledCode:
|
|
9840
|
-
plugins: [platformBundleDefinePlugin],
|
|
9841
|
-
input: entryPath,
|
|
9842
|
-
write: false,
|
|
9843
|
-
output: {
|
|
9844
|
-
format: "esm",
|
|
9845
|
-
sourcemap: false,
|
|
9846
|
-
minify: false,
|
|
9847
|
-
codeSplitting: false,
|
|
9848
|
-
globals: { tailordb: "tailordb" }
|
|
9849
|
-
},
|
|
9850
|
-
external: ["tailordb"],
|
|
9851
|
-
resolve: { conditionNames: ["node", "import"] },
|
|
9852
|
-
tsconfig,
|
|
9853
|
-
treeshake: {
|
|
9854
|
-
moduleSideEffects: false,
|
|
9855
|
-
annotations: true,
|
|
9856
|
-
unknownGlobalSideEffects: false
|
|
9857
|
-
},
|
|
9858
|
-
logLevel: "silent"
|
|
9859
|
-
})).output[0].code
|
|
10799
|
+
bundledCode: result.output[0].code
|
|
9860
10800
|
};
|
|
9861
10801
|
}
|
|
9862
10802
|
|
|
@@ -10201,16 +11141,9 @@ async function executeSingleMigration(options, migration) {
|
|
|
10201
11141
|
* @returns {Promise<void>}
|
|
10202
11142
|
*/
|
|
10203
11143
|
async function updateMigrationLabel(client, workspaceId, namespace, migrationNumber) {
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
const newLabel = `m${formatMigrationNumber(migrationNumber)}`;
|
|
10208
|
-
await client.setMetadata({
|
|
10209
|
-
trn,
|
|
10210
|
-
labels: {
|
|
10211
|
-
...existingLabels,
|
|
10212
|
-
[MIGRATION_LABEL_KEY]: newLabel
|
|
10213
|
-
}
|
|
11144
|
+
await writeMetadataLabels(client, {
|
|
11145
|
+
trn: resourceTrn(workspaceId, "tailordb", namespace),
|
|
11146
|
+
labels: { [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber) }
|
|
10214
11147
|
});
|
|
10215
11148
|
}
|
|
10216
11149
|
/**
|
|
@@ -10285,169 +11218,6 @@ function groupMigrationsByNamespace(migrations) {
|
|
|
10285
11218
|
|
|
10286
11219
|
//#endregion
|
|
10287
11220
|
//#region src/cli/commands/deploy/tailordb/index.ts
|
|
10288
|
-
/**
|
|
10289
|
-
* Fetch all TailorDB types from remote for a namespace
|
|
10290
|
-
* @param {OperatorClient} client - Operator client instance
|
|
10291
|
-
* @param {string} workspaceId - Workspace ID
|
|
10292
|
-
* @param {string} namespace - TailorDB namespace
|
|
10293
|
-
* @returns {Promise<ProtoTailorDBType[]>} Remote TailorDB types
|
|
10294
|
-
*/
|
|
10295
|
-
async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
10296
|
-
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
10297
|
-
const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
|
|
10298
|
-
workspaceId,
|
|
10299
|
-
namespaceName: namespace,
|
|
10300
|
-
pageToken,
|
|
10301
|
-
pageSize: maxPageSize
|
|
10302
|
-
});
|
|
10303
|
-
return [tailordbTypes, nextPageToken];
|
|
10304
|
-
});
|
|
10305
|
-
}
|
|
10306
|
-
async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
|
|
10307
|
-
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
10308
|
-
const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
|
|
10309
|
-
workspaceId,
|
|
10310
|
-
namespaceName: namespace,
|
|
10311
|
-
pageToken,
|
|
10312
|
-
pageSize: maxPageSize
|
|
10313
|
-
});
|
|
10314
|
-
return [permissions, nextPageToken];
|
|
10315
|
-
});
|
|
10316
|
-
}
|
|
10317
|
-
function definedWhenNotEmpty(value) {
|
|
10318
|
-
return Object.keys(value).length > 0 ? value : void 0;
|
|
10319
|
-
}
|
|
10320
|
-
function namespaceConfig(config, tailorDBInputs, namespace) {
|
|
10321
|
-
return tailorDBInputs.find((input) => input.namespace === namespace)?.config ?? config.db?.[namespace];
|
|
10322
|
-
}
|
|
10323
|
-
function namespaceGqlOperations(config, tailorDBInputs, namespace) {
|
|
10324
|
-
return namespaceConfig(config, tailorDBInputs, namespace)?.gqlOperations;
|
|
10325
|
-
}
|
|
10326
|
-
const GQL_OPERATION_KEYS = [
|
|
10327
|
-
"create",
|
|
10328
|
-
"update",
|
|
10329
|
-
"delete",
|
|
10330
|
-
"read"
|
|
10331
|
-
];
|
|
10332
|
-
function configuredDisabledGqlOperations(operations) {
|
|
10333
|
-
if (!operations) return void 0;
|
|
10334
|
-
if (operations === "query") return {
|
|
10335
|
-
create: false,
|
|
10336
|
-
update: false,
|
|
10337
|
-
delete: false
|
|
10338
|
-
};
|
|
10339
|
-
const disabled = {};
|
|
10340
|
-
for (const key of GQL_OPERATION_KEYS) if (operations[key] === false) disabled[key] = false;
|
|
10341
|
-
return definedWhenNotEmpty(disabled);
|
|
10342
|
-
}
|
|
10343
|
-
function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabled) {
|
|
10344
|
-
if (!remoteDisabled || !configuredDisabled) return void 0;
|
|
10345
|
-
const disabled = {};
|
|
10346
|
-
for (const key of GQL_OPERATION_KEYS) if (configuredDisabled[key] === false && remoteDisabled[key]) disabled[key] = false;
|
|
10347
|
-
return definedWhenNotEmpty(disabled);
|
|
10348
|
-
}
|
|
10349
|
-
function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
|
|
10350
|
-
const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
|
|
10351
|
-
const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
|
|
10352
|
-
const types = {};
|
|
10353
|
-
for (const [typeName, type] of Object.entries(snapshot.types)) {
|
|
10354
|
-
const settings = { ...type.settings };
|
|
10355
|
-
const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
|
|
10356
|
-
if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
|
|
10357
|
-
if (type.settings?.gqlOperations === void 0) {
|
|
10358
|
-
const disabled = appliedConfiguredDisabledGqlOperations(remoteSettings?.disableGqlOperations, configuredDisabled);
|
|
10359
|
-
if (disabled) settings.gqlOperations = disabled;
|
|
10360
|
-
}
|
|
10361
|
-
const comparableType = { ...type };
|
|
10362
|
-
const comparableSettings = definedWhenNotEmpty(settings);
|
|
10363
|
-
if (comparableSettings) comparableType.settings = comparableSettings;
|
|
10364
|
-
else delete comparableType.settings;
|
|
10365
|
-
types[typeName] = comparableType;
|
|
10366
|
-
}
|
|
10367
|
-
return {
|
|
10368
|
-
...snapshot,
|
|
10369
|
-
types
|
|
10370
|
-
};
|
|
10371
|
-
}
|
|
10372
|
-
/**
|
|
10373
|
-
* Get the current migration number from remote metadata
|
|
10374
|
-
* @param {OperatorClient} client - Operator client instance
|
|
10375
|
-
* @param {string} workspaceId - Workspace ID
|
|
10376
|
-
* @param {string} namespace - TailorDB namespace
|
|
10377
|
-
* @returns {Promise<number | null>} Current migration number, or null if no migration label exists
|
|
10378
|
-
*/
|
|
10379
|
-
async function getRemoteMigrationNumber(client, workspaceId, namespace) {
|
|
10380
|
-
try {
|
|
10381
|
-
const trn = resourceTrn(workspaceId, "tailordb", namespace);
|
|
10382
|
-
const { metadata } = await client.getMetadata({ trn });
|
|
10383
|
-
const label = metadata?.labels["sdk-migration"];
|
|
10384
|
-
if (!label) return null;
|
|
10385
|
-
const match = label.match(/^m(\d+)$/);
|
|
10386
|
-
return match ? parseInt(assertDefined(match[1], "migration label capture group missing"), 10) : null;
|
|
10387
|
-
} catch {
|
|
10388
|
-
return null;
|
|
10389
|
-
}
|
|
10390
|
-
}
|
|
10391
|
-
/**
|
|
10392
|
-
* Verify remote schema matches the expected snapshot state
|
|
10393
|
-
* @param {OperatorClient} client - Operator client instance
|
|
10394
|
-
* @param {string} workspaceId - Workspace ID
|
|
10395
|
-
* @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migration config
|
|
10396
|
-
* @param {LoadedConfig} config - Loaded application config
|
|
10397
|
-
* @param {ReadonlyArray<TailorDBDeployInput>} tailorDBInputs - Deploy inputs for namespace defaults
|
|
10398
|
-
* @returns {Promise<RemoteSchemaVerificationResult[]>} Verification results per namespace
|
|
10399
|
-
*/
|
|
10400
|
-
async function verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs) {
|
|
10401
|
-
const results = [];
|
|
10402
|
-
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
10403
|
-
const remoteMigrationNumber = await getRemoteMigrationNumber(client, workspaceId, namespace);
|
|
10404
|
-
if (remoteMigrationNumber === null) {
|
|
10405
|
-
results.push({
|
|
10406
|
-
namespace,
|
|
10407
|
-
remoteMigrationNumber: 0,
|
|
10408
|
-
drifts: [],
|
|
10409
|
-
hasDrift: false
|
|
10410
|
-
});
|
|
10411
|
-
continue;
|
|
10412
|
-
}
|
|
10413
|
-
const expectedSnapshot = reconstructSnapshotFromMigrations(migrationsDir, remoteMigrationNumber);
|
|
10414
|
-
if (!expectedSnapshot) {
|
|
10415
|
-
results.push({
|
|
10416
|
-
namespace,
|
|
10417
|
-
remoteMigrationNumber,
|
|
10418
|
-
drifts: [],
|
|
10419
|
-
hasDrift: false
|
|
10420
|
-
});
|
|
10421
|
-
continue;
|
|
10422
|
-
}
|
|
10423
|
-
const [remoteTypes, remoteGqlPermissions] = await Promise.all([fetchRemoteTypes(client, workspaceId, namespace), fetchRemoteGqlPermissions(client, workspaceId, namespace)]);
|
|
10424
|
-
const drifts = compareRemoteWithSnapshot(remoteTypes, deployComparableSnapshot(expectedSnapshot, remoteTypes, namespaceGqlOperations(config, tailorDBInputs, namespace)), remoteGqlPermissions);
|
|
10425
|
-
results.push({
|
|
10426
|
-
namespace,
|
|
10427
|
-
remoteMigrationNumber,
|
|
10428
|
-
drifts,
|
|
10429
|
-
hasDrift: drifts.length > 0
|
|
10430
|
-
});
|
|
10431
|
-
}
|
|
10432
|
-
return results;
|
|
10433
|
-
}
|
|
10434
|
-
/**
|
|
10435
|
-
* Format remote schema verification results for display
|
|
10436
|
-
* @param {RemoteSchemaVerificationResult[]} results - Verification results
|
|
10437
|
-
* @returns {string} Formatted results string
|
|
10438
|
-
*/
|
|
10439
|
-
function formatRemoteVerificationResults(results) {
|
|
10440
|
-
const lines = [];
|
|
10441
|
-
for (const result of results) {
|
|
10442
|
-
if (!result.hasDrift) continue;
|
|
10443
|
-
lines.push(`Namespace: ${result.namespace}`);
|
|
10444
|
-
lines.push(` Remote migration: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
|
|
10445
|
-
lines.push(` Differences:`);
|
|
10446
|
-
lines.push(formatSchemaDrifts(result.drifts));
|
|
10447
|
-
lines.push("");
|
|
10448
|
-
}
|
|
10449
|
-
return lines.join("\n");
|
|
10450
|
-
}
|
|
10451
11221
|
const MIGRATION_FILE_KINDS = [
|
|
10452
11222
|
"schema",
|
|
10453
11223
|
"diff",
|
|
@@ -10506,20 +11276,19 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
|
|
|
10506
11276
|
throw new Error("Schema migration check failed");
|
|
10507
11277
|
}
|
|
10508
11278
|
const remoteVerificationResults = await verifyRemoteSchema(client, workspaceId, namespacesWithMigrations, config, tailorDBInputs);
|
|
11279
|
+
const missingCheckpointResults = remoteVerificationResults.filter((result) => result.checkpointMissingLocal);
|
|
11280
|
+
if (missingCheckpointResults.length > 0) {
|
|
11281
|
+
logger.error("Remote migration checkpoint is not in the local migration history:");
|
|
11282
|
+
for (const result of missingCheckpointResults) logger.log(` ${result.namespace}: ${formatMigrationNumber(result.remoteMigrationNumber)}`);
|
|
11283
|
+
logger.newline();
|
|
11284
|
+
logger.info("Pull the latest migration files, or run 'tailor tailordb migration status' to compare.");
|
|
11285
|
+
throw new Error("Remote migration checkpoint verification failed");
|
|
11286
|
+
}
|
|
10509
11287
|
if (remoteVerificationResults.some((r) => r.hasDrift)) {
|
|
10510
11288
|
logger.error("Remote schema drift detected:");
|
|
10511
11289
|
logger.log(formatRemoteVerificationResults(remoteVerificationResults));
|
|
10512
11290
|
logger.newline();
|
|
10513
|
-
|
|
10514
|
-
logger.info(" - Another developer applied different migrations", { mode: "plain" });
|
|
10515
|
-
logger.info(" - Manual schema changes were made directly", { mode: "plain" });
|
|
10516
|
-
logger.info(" - Migration history is out of sync", { mode: "plain" });
|
|
10517
|
-
logger.newline();
|
|
10518
|
-
logger.info("To resolve:");
|
|
10519
|
-
logger.info(" - Run 'tailor tailordb migration status' to compare local vs remote.", { mode: "plain" });
|
|
10520
|
-
logger.info(" - If remote is correct, update local types and run 'migration generate'.", { mode: "plain" });
|
|
10521
|
-
logger.info(" - If local migration history is correct, run 'migration sync <N>' to overwrite remote.", { mode: "plain" });
|
|
10522
|
-
logger.info(" - If only bookkeeping is stale, run 'migration set <N>'.", { mode: "plain" });
|
|
11291
|
+
logRemoteDriftGuidance();
|
|
10523
11292
|
logger.newline();
|
|
10524
11293
|
logger.info("Use '--no-schema-check' to skip this check (not recommended).");
|
|
10525
11294
|
throw new Error("Remote schema verification failed");
|
|
@@ -10557,7 +11326,7 @@ async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigra
|
|
|
10557
11326
|
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
10558
11327
|
if (getMigrationFiles(migrationsDir).length === 0) continue;
|
|
10559
11328
|
const targetVersion = getLatestMigrationNumber(migrationsDir);
|
|
10560
|
-
const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace);
|
|
11329
|
+
const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace).catch(() => null);
|
|
10561
11330
|
if (currentVersion === targetVersion) continue;
|
|
10562
11331
|
await updateMigrationLabel(client, workspaceId, namespace, targetVersion);
|
|
10563
11332
|
const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
|
|
@@ -10663,14 +11432,26 @@ async function applyTailorDB(client, result, phase = "create-update") {
|
|
|
10663
11432
|
});
|
|
10664
11433
|
if (remainingGqlPermissionDeletes.length > 0) await Promise.all(remainingGqlPermissionDeletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
|
|
10665
11434
|
await Promise.all(changeSet.type.deletes.filter((del) => isOutsideMigrations(del.request.namespaceName) && !deletedResources.types.has(del.name)).map((del) => client.deleteTailorDBType(del.request)));
|
|
11435
|
+
await Promise.all([
|
|
11436
|
+
...changeSet.type.creates,
|
|
11437
|
+
...changeSet.type.updates,
|
|
11438
|
+
...changeSet.type.unchanged
|
|
11439
|
+
].filter((entry) => entry.metaRequest && !deletedResources.types.has(entry.name)).flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : []));
|
|
10666
11440
|
} else {
|
|
10667
11441
|
await Promise.all([...changeSet.service.creates.map(async (create) => {
|
|
10668
11442
|
await client.createTailorDBService(create.request);
|
|
10669
|
-
await client
|
|
10670
|
-
}), ...changeSet.service.updates.map((update) => client
|
|
11443
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
11444
|
+
}), ...changeSet.service.updates.map((update) => writeMetadataLabels(client, update.metaRequest))]);
|
|
10671
11445
|
try {
|
|
10672
|
-
for (const create of changeSet.type.creates)
|
|
10673
|
-
|
|
11446
|
+
for (const create of changeSet.type.creates) {
|
|
11447
|
+
await client.createTailorDBType(create.request);
|
|
11448
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
11449
|
+
}
|
|
11450
|
+
for (const update of changeSet.type.updates) {
|
|
11451
|
+
await client.updateTailorDBType(update.request);
|
|
11452
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
11453
|
+
}
|
|
11454
|
+
await Promise.all(changeSet.type.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : []));
|
|
10674
11455
|
} catch (error) {
|
|
10675
11456
|
handleOptionalToRequiredError(error, ["Run 'tailor tailordb migration generate' to create migration files.", "Migration scripts allow you to handle existing data before applying the schema change."]);
|
|
10676
11457
|
}
|
|
@@ -10713,8 +11494,8 @@ function getDeletedTypeNames(migration) {
|
|
|
10713
11494
|
async function executeServicesCreation(client, changeSet) {
|
|
10714
11495
|
await Promise.all([...changeSet.service.creates.map(async (create) => {
|
|
10715
11496
|
await client.createTailorDBService(create.request);
|
|
10716
|
-
await client
|
|
10717
|
-
}), ...changeSet.service.updates.map((update) => client
|
|
11497
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
11498
|
+
}), ...changeSet.service.updates.map((update) => writeMetadataLabels(client, update.metaRequest))]);
|
|
10718
11499
|
}
|
|
10719
11500
|
/**
|
|
10720
11501
|
* Track which types have been created/updated across migrations
|
|
@@ -10759,7 +11540,7 @@ function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executor
|
|
|
10759
11540
|
if (!snapshotType) return void 0;
|
|
10760
11541
|
const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
|
|
10761
11542
|
return generateTailorDBTypeManifestFromSnapshot(snapshotType, {
|
|
10762
|
-
|
|
11543
|
+
subscribed: executorUsedTypes.has(snapshotType.name),
|
|
10763
11544
|
namespaceGqlOperations: input?.config.gqlOperations
|
|
10764
11545
|
});
|
|
10765
11546
|
}
|
|
@@ -10962,7 +11743,7 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
|
|
|
10962
11743
|
try {
|
|
10963
11744
|
if (priorType) {
|
|
10964
11745
|
const manifest = generateTailorDBTypeManifestFromSnapshot(priorType, {
|
|
10965
|
-
|
|
11746
|
+
subscribed: executorUsedTypes.has(priorType.name),
|
|
10966
11747
|
namespaceGqlOperations: input?.config.gqlOperations
|
|
10967
11748
|
});
|
|
10968
11749
|
await client.updateTailorDBType({
|
|
@@ -10988,19 +11769,10 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
|
|
|
10988
11769
|
}
|
|
10989
11770
|
}
|
|
10990
11771
|
/**
|
|
10991
|
-
*
|
|
10992
|
-
* @param
|
|
10993
|
-
* @returns
|
|
11772
|
+
* Plan TailorDB-related changes based on current and desired state.
|
|
11773
|
+
* @param context - Planning context
|
|
11774
|
+
* @returns Planned changes
|
|
10994
11775
|
*/
|
|
10995
|
-
function toTailorDBDeployInput(service) {
|
|
10996
|
-
const types = {};
|
|
10997
|
-
for (const [typeName, type] of Object.entries(service.types)) types[typeName] = createSnapshotType(type);
|
|
10998
|
-
return {
|
|
10999
|
-
namespace: service.namespace,
|
|
11000
|
-
config: service.config,
|
|
11001
|
-
types
|
|
11002
|
-
};
|
|
11003
|
-
}
|
|
11004
11776
|
async function planTailorDB(context) {
|
|
11005
11777
|
const { client, workspaceId, application, forRemoval, config, noSchemaCheck, forceApplyAll = false } = context;
|
|
11006
11778
|
const tailordbs = [];
|
|
@@ -11019,7 +11791,12 @@ async function planTailorDB(context) {
|
|
|
11019
11791
|
} : await validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck ?? false, tailordbs);
|
|
11020
11792
|
const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
|
|
11021
11793
|
const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
|
|
11022
|
-
const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll
|
|
11794
|
+
const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, void 0, forceApplyAll, {
|
|
11795
|
+
appName: application.name,
|
|
11796
|
+
appId: application.id,
|
|
11797
|
+
dependentApps: context.dependentApps,
|
|
11798
|
+
runAppIds: context.runAppIds
|
|
11799
|
+
}), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
|
|
11023
11800
|
typeChangeSet.creates.sort(byName);
|
|
11024
11801
|
typeChangeSet.updates.sort(byName);
|
|
11025
11802
|
typeChangeSet.deletes.sort(byName);
|
|
@@ -11118,8 +11895,7 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
|
|
|
11118
11895
|
const metaRequest = await buildMetaRequest({
|
|
11119
11896
|
trn: resourceTrn(workspaceId, "tailordb", tailordb.namespace),
|
|
11120
11897
|
appName,
|
|
11121
|
-
appId
|
|
11122
|
-
existingLabels: existing?.allLabels
|
|
11898
|
+
appId
|
|
11123
11899
|
});
|
|
11124
11900
|
if (existing) {
|
|
11125
11901
|
if (trackDesiredResourceOwnership({
|
|
@@ -11170,8 +11946,29 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
|
|
|
11170
11946
|
resourceOwners
|
|
11171
11947
|
};
|
|
11172
11948
|
}
|
|
11173
|
-
async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, filteredTypesByNamespace, forceApplyAll = false) {
|
|
11949
|
+
async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, deletedServices, filteredTypesByNamespace, forceApplyAll = false, records = {}) {
|
|
11174
11950
|
const changeSet = createChangeSet("TailorDB types");
|
|
11951
|
+
const { appName, appId, dependentApps, runAppIds } = records;
|
|
11952
|
+
/**
|
|
11953
|
+
* Build one type's metadata write, carrying the dependency records that belong
|
|
11954
|
+
* to it. The type is what publishes record events, so the record lives there.
|
|
11955
|
+
* @param namespace - Namespace holding the type
|
|
11956
|
+
* @param typeName - Type name
|
|
11957
|
+
* @param explicitPublishEvents - `publishEvents` declared on the type, if any
|
|
11958
|
+
* @returns The type's metadata write
|
|
11959
|
+
*/
|
|
11960
|
+
const typeMetaRequest = async (namespace, typeName, explicitPublishEvents) => {
|
|
11961
|
+
return addDependencyRecords(await buildMetaRequest({
|
|
11962
|
+
trn: tailorDBTypeTrn(workspaceId, namespace, typeName),
|
|
11963
|
+
appName: appName ?? "",
|
|
11964
|
+
appId
|
|
11965
|
+
}), {
|
|
11966
|
+
key: eventSourceKey.tailorDBType(namespace, typeName),
|
|
11967
|
+
dependentApps,
|
|
11968
|
+
runAppIds,
|
|
11969
|
+
pinned: explicitPublishEvents !== void 0
|
|
11970
|
+
});
|
|
11971
|
+
};
|
|
11175
11972
|
const fetchTypes = (namespaceName) => {
|
|
11176
11973
|
return fetchAllTolerant(async (pageToken, maxPageSize) => {
|
|
11177
11974
|
const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
|
|
@@ -11185,27 +11982,36 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
|
|
|
11185
11982
|
};
|
|
11186
11983
|
for (const tailordb of tailordbs) {
|
|
11187
11984
|
const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
|
|
11188
|
-
for (const [typeName, type] of Object.entries(types))
|
|
11985
|
+
for (const [typeName, type] of Object.entries(types)) assertNoPublishEventsConflict({
|
|
11986
|
+
explicit: type.settings?.publishEvents,
|
|
11987
|
+
subscribed: executorUsedTypes.has(typeName),
|
|
11988
|
+
conflict: publishEventsConflict.tailorDBType(typeName)
|
|
11989
|
+
});
|
|
11189
11990
|
}
|
|
11190
11991
|
for (const tailordb of tailordbs) {
|
|
11191
11992
|
const existingTypes = await fetchTypes(tailordb.namespace);
|
|
11192
11993
|
const existingTypesMap = new Map(existingTypes.map((type) => [type.name, type]));
|
|
11193
11994
|
const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
|
|
11995
|
+
const typeMeta = (typeName) => typeMetaRequest(tailordb.namespace, typeName, types[typeName]?.settings?.publishEvents);
|
|
11194
11996
|
for (const [typeName, tailordbTypeSnapshot] of Object.entries(types)) {
|
|
11195
11997
|
const tailordbType = generateTailorDBTypeManifestFromSnapshot(tailordbTypeSnapshot, {
|
|
11196
|
-
|
|
11998
|
+
subscribed: executorUsedTypes.has(typeName),
|
|
11197
11999
|
namespaceGqlOperations: tailordb.config.gqlOperations
|
|
11198
12000
|
});
|
|
11199
12001
|
const existingType = existingTypesMap.get(typeName);
|
|
11200
12002
|
if (existingType) {
|
|
11201
|
-
if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({
|
|
12003
|
+
if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({
|
|
12004
|
+
name: typeName,
|
|
12005
|
+
metaRequest: await typeMeta(typeName)
|
|
12006
|
+
});
|
|
11202
12007
|
else changeSet.updates.push({
|
|
11203
12008
|
name: typeName,
|
|
11204
12009
|
request: {
|
|
11205
12010
|
workspaceId,
|
|
11206
12011
|
namespaceName: tailordb.namespace,
|
|
11207
12012
|
tailordbType
|
|
11208
|
-
}
|
|
12013
|
+
},
|
|
12014
|
+
metaRequest: await typeMeta(typeName)
|
|
11209
12015
|
});
|
|
11210
12016
|
existingTypesMap.delete(typeName);
|
|
11211
12017
|
} else changeSet.creates.push({
|
|
@@ -11214,7 +12020,8 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
|
|
|
11214
12020
|
workspaceId,
|
|
11215
12021
|
namespaceName: tailordb.namespace,
|
|
11216
12022
|
tailordbType
|
|
11217
|
-
}
|
|
12023
|
+
},
|
|
12024
|
+
metaRequest: await typeMeta(typeName)
|
|
11218
12025
|
});
|
|
11219
12026
|
}
|
|
11220
12027
|
existingTypesMap.forEach((_type, name) => {
|
|
@@ -11375,83 +12182,32 @@ async function planGqlPermissions(client, workspaceId, tailordbs, deletedService
|
|
|
11375
12182
|
}
|
|
11376
12183
|
existingNameSet.forEach((name) => {
|
|
11377
12184
|
changeSet.deletes.push({
|
|
11378
|
-
name,
|
|
11379
|
-
request: {
|
|
11380
|
-
workspaceId,
|
|
11381
|
-
namespaceName: tailordb.namespace,
|
|
11382
|
-
typeName: name
|
|
11383
|
-
}
|
|
11384
|
-
});
|
|
11385
|
-
});
|
|
11386
|
-
}
|
|
11387
|
-
for (const namespaceName of deletedServices) (await fetchGqlPermissions(namespaceName)).forEach((gqlPermission) => {
|
|
11388
|
-
changeSet.deletes.push({
|
|
11389
|
-
name: gqlPermission.typeName,
|
|
11390
|
-
request: {
|
|
11391
|
-
workspaceId,
|
|
11392
|
-
namespaceName,
|
|
11393
|
-
typeName: gqlPermission.typeName
|
|
11394
|
-
}
|
|
11395
|
-
});
|
|
11396
|
-
});
|
|
11397
|
-
return changeSet;
|
|
11398
|
-
}
|
|
11399
|
-
function normalizeComparableGqlPermission(permission) {
|
|
11400
|
-
return { policies: (normalizeProtoConfig(permission)?.policies ?? []).map((policy) => ({
|
|
11401
|
-
...policy,
|
|
11402
|
-
actions: (policy.actions ?? []).toSorted((left, right) => left - right)
|
|
11403
|
-
})) };
|
|
11404
|
-
}
|
|
11405
|
-
/**
|
|
11406
|
-
* Check if there are schema differences between migration snapshots and local definitions
|
|
11407
|
-
* @param {ReadonlyMap<string, Record<string, TailorDBSnapshotType>>} typesByNamespace - Snapshot-shaped local types by namespace
|
|
11408
|
-
* @param {NamespaceWithMigrations[]} namespacesWithMigrations - Namespaces with migrations config
|
|
11409
|
-
* @returns {Promise<MigrationCheckResult[]>} Results for each namespace
|
|
11410
|
-
*/
|
|
11411
|
-
async function checkMigrationDiffs(typesByNamespace, namespacesWithMigrations) {
|
|
11412
|
-
const results = [];
|
|
11413
|
-
for (const { namespace, migrationsDir } of namespacesWithMigrations) {
|
|
11414
|
-
const localTypes = typesByNamespace.get(namespace);
|
|
11415
|
-
if (!localTypes) continue;
|
|
11416
|
-
const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
|
|
11417
|
-
if (!previousSnapshot) {
|
|
11418
|
-
results.push({
|
|
11419
|
-
namespace,
|
|
11420
|
-
migrationsDir,
|
|
11421
|
-
hasDiff: true,
|
|
11422
|
-
diff: void 0
|
|
11423
|
-
});
|
|
11424
|
-
continue;
|
|
11425
|
-
}
|
|
11426
|
-
const diff = compareLocalTypesWithSnapshot(previousSnapshot, localTypes, namespace);
|
|
11427
|
-
results.push({
|
|
11428
|
-
namespace,
|
|
11429
|
-
migrationsDir,
|
|
11430
|
-
hasDiff: hasChanges(diff),
|
|
11431
|
-
diff: hasChanges(diff) ? diff : void 0
|
|
12185
|
+
name,
|
|
12186
|
+
request: {
|
|
12187
|
+
workspaceId,
|
|
12188
|
+
namespaceName: tailordb.namespace,
|
|
12189
|
+
typeName: name
|
|
12190
|
+
}
|
|
12191
|
+
});
|
|
11432
12192
|
});
|
|
11433
12193
|
}
|
|
11434
|
-
|
|
12194
|
+
for (const namespaceName of deletedServices) (await fetchGqlPermissions(namespaceName)).forEach((gqlPermission) => {
|
|
12195
|
+
changeSet.deletes.push({
|
|
12196
|
+
name: gqlPermission.typeName,
|
|
12197
|
+
request: {
|
|
12198
|
+
workspaceId,
|
|
12199
|
+
namespaceName,
|
|
12200
|
+
typeName: gqlPermission.typeName
|
|
12201
|
+
}
|
|
12202
|
+
});
|
|
12203
|
+
});
|
|
12204
|
+
return changeSet;
|
|
11435
12205
|
}
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
function formatMigrationCheckResults(results) {
|
|
11442
|
-
const lines = [];
|
|
11443
|
-
for (const result of results) {
|
|
11444
|
-
if (!result.hasDiff) continue;
|
|
11445
|
-
lines.push(`Namespace: ${result.namespace}`);
|
|
11446
|
-
if (!result.diff) lines.push(" No migration snapshot found. Run 'tailor tailordb migration generate' first.");
|
|
11447
|
-
else {
|
|
11448
|
-
lines.push(` ${formatDiffSummary(result.diff)}`);
|
|
11449
|
-
lines.push("");
|
|
11450
|
-
lines.push(formatMigrationDiff(result.diff));
|
|
11451
|
-
}
|
|
11452
|
-
lines.push("");
|
|
11453
|
-
}
|
|
11454
|
-
return lines.join("\n");
|
|
12206
|
+
function normalizeComparableGqlPermission(permission) {
|
|
12207
|
+
return { policies: (normalizeProtoConfig(permission)?.policies ?? []).map((policy) => ({
|
|
12208
|
+
...policy,
|
|
12209
|
+
actions: (policy.actions ?? []).toSorted((left, right) => left - right)
|
|
12210
|
+
})) };
|
|
11455
12211
|
}
|
|
11456
12212
|
|
|
11457
12213
|
//#endregion
|
|
@@ -11466,32 +12222,38 @@ function formatMigrationCheckResults(results) {
|
|
|
11466
12222
|
async function applyWorkflow(client, result, phase = "create-update") {
|
|
11467
12223
|
const { changeSet, appName, appId } = result;
|
|
11468
12224
|
if (phase === "create-update") {
|
|
11469
|
-
const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames);
|
|
11470
|
-
await Promise.all([
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
12225
|
+
const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames, result.jobFunctionPublishEvents);
|
|
12226
|
+
await Promise.all([
|
|
12227
|
+
...changeSet.creates.map(async (create) => {
|
|
12228
|
+
const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
|
|
12229
|
+
const shape = buildWorkflowValidationShape(create.workspaceId, create.workflow);
|
|
12230
|
+
await client.createWorkflow({
|
|
12231
|
+
workspaceId: shape.workspaceId,
|
|
12232
|
+
workflowName: shape.workflowName,
|
|
12233
|
+
mainJobFunctionName: shape.mainJobFunctionName,
|
|
12234
|
+
retryPolicy: shape.retryPolicy,
|
|
12235
|
+
concurrencyPolicy: shape.concurrencyPolicy,
|
|
12236
|
+
jobFunctions: filteredVersions,
|
|
12237
|
+
publishExecutionEvents: shape.publishExecutionEvents
|
|
12238
|
+
});
|
|
12239
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
12240
|
+
}),
|
|
12241
|
+
...changeSet.updates.map(async (update) => {
|
|
12242
|
+
const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, update.usedJobNames);
|
|
12243
|
+
const shape = buildWorkflowValidationShape(update.workspaceId, update.workflow);
|
|
12244
|
+
await client.updateWorkflow({
|
|
12245
|
+
workspaceId: shape.workspaceId,
|
|
12246
|
+
workflowName: shape.workflowName,
|
|
12247
|
+
mainJobFunctionName: shape.mainJobFunctionName,
|
|
12248
|
+
retryPolicy: shape.retryPolicy,
|
|
12249
|
+
concurrencyPolicy: shape.concurrencyPolicy,
|
|
12250
|
+
jobFunctions: filteredVersions,
|
|
12251
|
+
publishExecutionEvents: shape.publishExecutionEvents
|
|
12252
|
+
});
|
|
12253
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
12254
|
+
}),
|
|
12255
|
+
...changeSet.unchanged.flatMap((entry) => entry.metaRequest ? [writeMetadataLabels(client, entry.metaRequest)] : [])
|
|
12256
|
+
]);
|
|
11495
12257
|
} else {
|
|
11496
12258
|
await deleteAllSettled(changeSet.deletes.map((del) => ({
|
|
11497
12259
|
resourceType: "workflow",
|
|
@@ -11549,9 +12311,10 @@ function filterJobFunctionVersions(allVersions, usedJobNames) {
|
|
|
11549
12311
|
* @param appName - Application name
|
|
11550
12312
|
* @param appId - Application ID used for job function metadata when available
|
|
11551
12313
|
* @param unchangedWorkflowJobNames - Job function names used by unchanged workflows
|
|
12314
|
+
* @param jobFunctionPublishEvents - Resolved `publishExecutionEvents` keyed by job function name
|
|
11552
12315
|
* @returns Map of job function names to versions
|
|
11553
12316
|
*/
|
|
11554
|
-
async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
|
|
12317
|
+
async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set(), jobFunctionPublishEvents = /* @__PURE__ */ new Map()) {
|
|
11555
12318
|
const jobFunctionVersions = {};
|
|
11556
12319
|
const firstWorkflow = changeSet.creates[0] || changeSet.updates[0] || changeSet.deletes[0];
|
|
11557
12320
|
if (!firstWorkflow) return jobFunctionVersions;
|
|
@@ -11570,16 +12333,15 @@ async function registerJobFunctions(client, changeSet, appName, appId, unchanged
|
|
|
11570
12333
|
const existingJobNamesSet = new Set(existingJobFunctions);
|
|
11571
12334
|
if (changeSet.creates.length > 0 || changeSet.updates.length > 0) {
|
|
11572
12335
|
const results = await Promise.all(Array.from(allUsedJobNames).map(async (jobName) => {
|
|
11573
|
-
const
|
|
11574
|
-
|
|
11575
|
-
jobFunctionName: jobName,
|
|
11576
|
-
scriptRef: workflowJobFunctionName(jobName)
|
|
11577
|
-
}) : await client.createWorkflowJobFunction({
|
|
12336
|
+
const isExisting = existingJobNamesSet.has(jobName);
|
|
12337
|
+
const request = {
|
|
11578
12338
|
workspaceId,
|
|
11579
12339
|
jobFunctionName: jobName,
|
|
11580
|
-
scriptRef: workflowJobFunctionName(jobName)
|
|
11581
|
-
|
|
11582
|
-
|
|
12340
|
+
scriptRef: workflowJobFunctionName(jobName),
|
|
12341
|
+
publishExecutionEvents: jobFunctionPublishEvents.get(jobName) ?? false
|
|
12342
|
+
};
|
|
12343
|
+
const response = isExisting ? await client.updateWorkflowJobFunction(request) : await client.createWorkflowJobFunction(request);
|
|
12344
|
+
await writeMetadataLabels(client, await buildMetaRequest({
|
|
11583
12345
|
trn: resourceTrn(workspaceId, "workflow_job_function", jobName),
|
|
11584
12346
|
appName,
|
|
11585
12347
|
appId
|
|
@@ -11625,9 +12387,77 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
|
|
|
11625
12387
|
workflowName: workflow.name,
|
|
11626
12388
|
mainJobFunctionName: workflow.mainJob.name,
|
|
11627
12389
|
...workflow.retryPolicy && { retryPolicy: toRetryPolicy(workflow.retryPolicy) },
|
|
11628
|
-
...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) }
|
|
12390
|
+
...workflow.concurrencyPolicy && { concurrencyPolicy: toConcurrencyPolicy(workflow.concurrencyPolicy) },
|
|
12391
|
+
publishExecutionEvents: workflow.publishEvents ?? false
|
|
11629
12392
|
};
|
|
11630
12393
|
}
|
|
12394
|
+
const NO_EVENT_SUBSCRIBERS = { workflowNames: /* @__PURE__ */ new Set() };
|
|
12395
|
+
function isSubscribed(subscribers, workflowName) {
|
|
12396
|
+
return subscribers.workflowNames.has(workflowName);
|
|
12397
|
+
}
|
|
12398
|
+
/**
|
|
12399
|
+
* Job names the given workflows' subscriptions enable.
|
|
12400
|
+
*
|
|
12401
|
+
* A job execution trigger names a workflow rather than a job, so the value is
|
|
12402
|
+
* resolved per job name over the union of the workflows that run it: a job two
|
|
12403
|
+
* workflows share stays on while either one is subscribed. Whoever asks whether a
|
|
12404
|
+
* job still publishes has to apply that same union, which is why this is shared
|
|
12405
|
+
* rather than restated — the two answering it differently is what let a shared
|
|
12406
|
+
* job be reported as turning off while a peer subscription kept it on.
|
|
12407
|
+
* @param params - Workflows, the jobs each runs, and which are subscribed
|
|
12408
|
+
* @param params.workflows - Workflows whose job sets are considered
|
|
12409
|
+
* @param params.mainJobDeps - Job names each workflow runs, keyed by its main job
|
|
12410
|
+
* @param params.isSubscribed - Whether a workflow's jobs are subscribed in this run
|
|
12411
|
+
* @returns Job names those subscriptions enable
|
|
12412
|
+
*/
|
|
12413
|
+
function subscribedWorkflowJobNames(params) {
|
|
12414
|
+
const { workflows, mainJobDeps, isSubscribed } = params;
|
|
12415
|
+
const jobNames = /* @__PURE__ */ new Set();
|
|
12416
|
+
for (const workflow of workflows) {
|
|
12417
|
+
if (!isSubscribed(workflow.name)) continue;
|
|
12418
|
+
for (const jobName of mainJobDeps[workflow.mainJob.name] ?? []) jobNames.add(jobName);
|
|
12419
|
+
}
|
|
12420
|
+
return jobNames;
|
|
12421
|
+
}
|
|
12422
|
+
/**
|
|
12423
|
+
* Resolve `publishExecutionEvents` for every job function used by a workflow.
|
|
12424
|
+
*
|
|
12425
|
+
* A job execution trigger names a workflow rather than a job, so a subscription
|
|
12426
|
+
* opts in every job that workflow runs.
|
|
12427
|
+
* @param params - Workflows, their job dependencies, subscribers, and explicit job flags
|
|
12428
|
+
* @returns Resolved flags keyed by job function name
|
|
12429
|
+
*/
|
|
12430
|
+
function resolveJobPublishEvents(params) {
|
|
12431
|
+
const { workflows, mainJobDeps, subscribers, explicit } = params;
|
|
12432
|
+
const usedJobNames = /* @__PURE__ */ new Set();
|
|
12433
|
+
for (const workflow of Object.values(workflows)) for (const jobName of mainJobDeps[workflow.mainJob.name] ?? []) usedJobNames.add(jobName);
|
|
12434
|
+
const subscribedJobNames = subscribedWorkflowJobNames({
|
|
12435
|
+
workflows: Object.values(workflows),
|
|
12436
|
+
mainJobDeps,
|
|
12437
|
+
isSubscribed: (workflowName) => isSubscribed(subscribers, workflowName)
|
|
12438
|
+
});
|
|
12439
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
12440
|
+
for (const jobName of usedJobNames) resolved.set(jobName, resolvePublishEvents({
|
|
12441
|
+
explicit: explicit.get(jobName),
|
|
12442
|
+
subscribed: subscribedJobNames.has(jobName),
|
|
12443
|
+
conflict: publishEventsConflict.workflowJob(jobName)
|
|
12444
|
+
}));
|
|
12445
|
+
return resolved;
|
|
12446
|
+
}
|
|
12447
|
+
/**
|
|
12448
|
+
* Collect job functions whose remote publishing flag no longer matches the plan.
|
|
12449
|
+
* @param existing - Existing job functions keyed by name
|
|
12450
|
+
* @param resolved - Resolved `publishExecutionEvents` keyed by job function name
|
|
12451
|
+
* @returns Job function names needing re-registration
|
|
12452
|
+
*/
|
|
12453
|
+
function collectStaleJobFunctionNames(existing, resolved) {
|
|
12454
|
+
const stale = /* @__PURE__ */ new Set();
|
|
12455
|
+
for (const [jobName, publishEvents] of resolved) {
|
|
12456
|
+
const remote = existing.get(jobName);
|
|
12457
|
+
if (remote && remote.publishExecutionEvents !== publishEvents) stale.add(jobName);
|
|
12458
|
+
}
|
|
12459
|
+
return stale;
|
|
12460
|
+
}
|
|
11631
12461
|
/**
|
|
11632
12462
|
* Plan workflow changes and job functions based on current and desired state.
|
|
11633
12463
|
* @param client - Operator client instance
|
|
@@ -11637,15 +12467,31 @@ function buildWorkflowValidationShape(workspaceId, workflow) {
|
|
|
11637
12467
|
* @param workflows - Parsed workflows
|
|
11638
12468
|
* @param mainJobDeps - Main job dependencies by workflow
|
|
11639
12469
|
* @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan
|
|
12470
|
+
* @param eventPublishing - Executor subscriptions and explicit job flags driving execution event publishing
|
|
11640
12471
|
* @returns Planned workflow changes
|
|
11641
12472
|
*/
|
|
11642
|
-
async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
|
|
12473
|
+
async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set(), eventPublishing = {}) {
|
|
11643
12474
|
const changeSet = createChangeSet("Workflows");
|
|
11644
12475
|
const conflicts = [];
|
|
11645
12476
|
const unmanaged = [];
|
|
11646
12477
|
const resourceOwners = /* @__PURE__ */ new Set();
|
|
11647
12478
|
const unchangedWorkflowJobNames = /* @__PURE__ */ new Set();
|
|
11648
12479
|
const retainedWorkflowJobNames = /* @__PURE__ */ new Set();
|
|
12480
|
+
const executionSubscribers = eventPublishing.execution ?? NO_EVENT_SUBSCRIBERS;
|
|
12481
|
+
const { dependentApps, runAppIds } = eventPublishing;
|
|
12482
|
+
const everyJobDeclaresPublishEvents = (workflowName) => {
|
|
12483
|
+
const jobNames = mainJobDeps[workflows[workflowName]?.mainJob.name ?? ""] ?? [];
|
|
12484
|
+
const explicit = eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map();
|
|
12485
|
+
return jobNames.length > 0 && jobNames.every((jobName) => explicit.has(jobName));
|
|
12486
|
+
};
|
|
12487
|
+
const existingJobFunctions = await fetchExistingJobFunctions(client, workspaceId);
|
|
12488
|
+
const jobFunctionPublishEvents = resolveJobPublishEvents({
|
|
12489
|
+
workflows,
|
|
12490
|
+
mainJobDeps,
|
|
12491
|
+
subscribers: eventPublishing.jobExecution ?? NO_EVENT_SUBSCRIBERS,
|
|
12492
|
+
explicit: eventPublishing.jobPublishEvents ?? /* @__PURE__ */ new Map()
|
|
12493
|
+
});
|
|
12494
|
+
const staleJobFunctionNames = collectStaleJobFunctionNames(existingJobFunctions, jobFunctionPublishEvents);
|
|
11649
12495
|
const existingWorkflows = await fetchExistingResourcesWithLabels({
|
|
11650
12496
|
client,
|
|
11651
12497
|
fetchPage: async (pageToken, pageSize) => {
|
|
@@ -11661,14 +12507,34 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
|
|
|
11661
12507
|
});
|
|
11662
12508
|
for (const workflow of Object.values(workflows)) {
|
|
11663
12509
|
const existing = existingWorkflows[workflow.name];
|
|
11664
|
-
const metaRequest = await buildMetaRequest({
|
|
12510
|
+
const metaRequest = addDependencyRecords(await buildMetaRequest({
|
|
11665
12511
|
trn: resourceTrn(workspaceId, "workflow", workflow.name),
|
|
11666
12512
|
appName,
|
|
11667
12513
|
appId
|
|
12514
|
+
}), {
|
|
12515
|
+
key: eventSourceKey.workflow(workflow.name),
|
|
12516
|
+
dependentApps,
|
|
12517
|
+
runAppIds,
|
|
12518
|
+
pinned: workflow.publishEvents !== void 0
|
|
12519
|
+
});
|
|
12520
|
+
addDependencyRecords(metaRequest, {
|
|
12521
|
+
key: eventSourceKey.workflowJobs(workflow.name),
|
|
12522
|
+
dependentApps,
|
|
12523
|
+
runAppIds,
|
|
12524
|
+
pinned: everyJobDeclaresPublishEvents(workflow.name),
|
|
12525
|
+
scope: "jobs"
|
|
11668
12526
|
});
|
|
11669
12527
|
const usedJobNames = mainJobDeps[workflow.mainJob.name];
|
|
11670
12528
|
if (!usedJobNames) throw new Error(`Job "${workflow.mainJob.name}" (mainJob of workflow "${workflow.name}") was not found.\n\nPossible causes:\n - The job is not exported as a named export\n - The file containing the job is not included in workflow.files glob pattern\n\nSolution:\n export const ${workflow.mainJob.name} = createWorkflowJob({ name: "${workflow.mainJob.name}", ... })`);
|
|
11671
12529
|
usedJobNames.forEach((jobName) => retainedWorkflowJobNames.add(jobName));
|
|
12530
|
+
const desiredWorkflow = {
|
|
12531
|
+
...workflow,
|
|
12532
|
+
publishEvents: resolvePublishEvents({
|
|
12533
|
+
explicit: workflow.publishEvents,
|
|
12534
|
+
subscribed: isSubscribed(executionSubscribers, workflow.name),
|
|
12535
|
+
conflict: publishEventsConflict.workflow(workflow.name)
|
|
12536
|
+
})
|
|
12537
|
+
};
|
|
11672
12538
|
if (existing) {
|
|
11673
12539
|
if (trackDesiredResourceOwnership({
|
|
11674
12540
|
labels: existing.allLabels,
|
|
@@ -11679,13 +12545,22 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
|
|
|
11679
12545
|
resourceName: workflow.name,
|
|
11680
12546
|
conflicts,
|
|
11681
12547
|
unmanaged
|
|
11682
|
-
}) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(
|
|
11683
|
-
|
|
12548
|
+
}) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged({
|
|
12549
|
+
existing: existing.resource,
|
|
12550
|
+
workflow: desiredWorkflow,
|
|
12551
|
+
usedJobNames,
|
|
12552
|
+
unchangedJobFunctions,
|
|
12553
|
+
staleJobFunctionNames
|
|
12554
|
+
})) {
|
|
12555
|
+
changeSet.unchanged.push({
|
|
12556
|
+
name: workflow.name,
|
|
12557
|
+
metaRequest
|
|
12558
|
+
});
|
|
11684
12559
|
for (const jobName of usedJobNames) unchangedWorkflowJobNames.add(jobName);
|
|
11685
12560
|
} else changeSet.updates.push({
|
|
11686
12561
|
name: workflow.name,
|
|
11687
12562
|
workspaceId,
|
|
11688
|
-
workflow,
|
|
12563
|
+
workflow: desiredWorkflow,
|
|
11689
12564
|
usedJobNames,
|
|
11690
12565
|
metaRequest
|
|
11691
12566
|
});
|
|
@@ -11693,7 +12568,7 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
|
|
|
11693
12568
|
} else changeSet.creates.push({
|
|
11694
12569
|
name: workflow.name,
|
|
11695
12570
|
workspaceId,
|
|
11696
|
-
workflow,
|
|
12571
|
+
workflow: desiredWorkflow,
|
|
11697
12572
|
usedJobNames,
|
|
11698
12573
|
metaRequest
|
|
11699
12574
|
});
|
|
@@ -11723,7 +12598,9 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
|
|
|
11723
12598
|
workspaceId,
|
|
11724
12599
|
appName,
|
|
11725
12600
|
appId,
|
|
11726
|
-
|
|
12601
|
+
existingJobFunctionNames: [...existingJobFunctions.keys()],
|
|
12602
|
+
retainedWorkflowJobNames,
|
|
12603
|
+
resourceOwners
|
|
11727
12604
|
});
|
|
11728
12605
|
const deletableJobNames = new Set(jobFunctionDeletes.map((del) => del.jobFunctionName));
|
|
11729
12606
|
for (const del of deleteWorkflows) changeSet.deletes.push({
|
|
@@ -11738,23 +12615,33 @@ async function planWorkflow(client, workspaceId, appName, appId, workflows, main
|
|
|
11738
12615
|
appName,
|
|
11739
12616
|
appId,
|
|
11740
12617
|
unchangedWorkflowJobNames,
|
|
11741
|
-
jobFunctionDeletes
|
|
12618
|
+
jobFunctionDeletes,
|
|
12619
|
+
jobFunctionPublishEvents
|
|
11742
12620
|
};
|
|
11743
12621
|
}
|
|
11744
|
-
async function
|
|
11745
|
-
const
|
|
11746
|
-
const existingJobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
|
|
12622
|
+
async function fetchExistingJobFunctions(client, workspaceId) {
|
|
12623
|
+
const jobFunctions = await fetchAll(async (pageToken, maxPageSize) => {
|
|
11747
12624
|
const response = await client.listWorkflowJobFunctions({
|
|
11748
12625
|
workspaceId,
|
|
11749
12626
|
pageToken,
|
|
11750
12627
|
pageSize: maxPageSize
|
|
11751
12628
|
});
|
|
11752
|
-
return [response.jobFunctions
|
|
12629
|
+
return [response.jobFunctions, response.nextPageToken];
|
|
11753
12630
|
});
|
|
11754
|
-
|
|
12631
|
+
return new Map(jobFunctions.map((jobFunction) => [jobFunction.name, jobFunction]));
|
|
12632
|
+
}
|
|
12633
|
+
async function planWorkflowJobFunctionDeletes(params) {
|
|
12634
|
+
const { client, workspaceId, appName, appId, existingJobFunctionNames, retainedWorkflowJobNames, resourceOwners } = params;
|
|
12635
|
+
const candidates = existingJobFunctionNames.filter((jobName) => !retainedWorkflowJobNames.has(jobName));
|
|
11755
12636
|
return (await Promise.all(candidates.map(async (jobFunctionName) => {
|
|
11756
12637
|
const { metadata } = await client.getMetadata({ trn: resourceTrn(workspaceId, "workflow_job_function", jobFunctionName) });
|
|
11757
|
-
return
|
|
12638
|
+
return trackRemainingResourceOwner({
|
|
12639
|
+
labels: metadata?.labels,
|
|
12640
|
+
ownerLabel: metadata?.labels["sdk-name"],
|
|
12641
|
+
appName,
|
|
12642
|
+
appId,
|
|
12643
|
+
resourceOwners
|
|
12644
|
+
}) ? {
|
|
11758
12645
|
workspaceId,
|
|
11759
12646
|
jobFunctionName
|
|
11760
12647
|
} : void 0;
|
|
@@ -11769,12 +12656,14 @@ async function planWorkflowJobFunctionDeletes(params) {
|
|
|
11769
12656
|
function formatWorkflowChangeEntries(changeSet, workflowJobFunctionChanges) {
|
|
11770
12657
|
return formatChangeEntriesWithFunctionRegistry("workflow", changeSet, workflowJobFunctionChanges, (item) => "usedJobNames" in item ? item.usedJobNames.map((jobName) => workflowJobFunctionName(jobName)) : []);
|
|
11771
12658
|
}
|
|
11772
|
-
function canTreatWorkflowAsUnchanged(
|
|
12659
|
+
function canTreatWorkflowAsUnchanged(params) {
|
|
12660
|
+
const { existing, workflow, usedJobNames, unchangedJobFunctions, staleJobFunctionNames } = params;
|
|
11773
12661
|
if (!usedJobNames.every((jobName) => unchangedJobFunctions.has(jobName))) return false;
|
|
12662
|
+
if (usedJobNames.some((jobName) => staleJobFunctionNames.has(jobName))) return false;
|
|
11774
12663
|
return areWorkflowsEqual(existing, workflow, usedJobNames);
|
|
11775
12664
|
}
|
|
11776
12665
|
function areWorkflowsEqual(existing, workflow, usedJobNames) {
|
|
11777
|
-
return existing.mainJobFunctionName === workflow.mainJob.name && areNormalizedEqual(normalizeComparableExistingWorkflowRetryPolicy(existing.retryPolicy), normalizeComparableWorkflowRetryPolicy(workflow.retryPolicy)) && areNormalizedEqual(normalizeComparableConcurrencyPolicy(existing.concurrencyPolicy), normalizeComparableConcurrencyPolicy(workflow.concurrencyPolicy)) && areNormalizedEqual(normalizeComparableWorkflowJobNames(existing.jobFunctions), normalizeComparableWorkflowJobNames(usedJobNames));
|
|
12666
|
+
return existing.mainJobFunctionName === workflow.mainJob.name && (existing.publishExecutionEvents ?? false) === (workflow.publishEvents ?? false) && areNormalizedEqual(normalizeComparableExistingWorkflowRetryPolicy(existing.retryPolicy), normalizeComparableWorkflowRetryPolicy(workflow.retryPolicy)) && areNormalizedEqual(normalizeComparableConcurrencyPolicy(existing.concurrencyPolicy), normalizeComparableConcurrencyPolicy(workflow.concurrencyPolicy)) && areNormalizedEqual(normalizeComparableWorkflowJobNames(existing.jobFunctions), normalizeComparableWorkflowJobNames(usedJobNames));
|
|
11778
12667
|
}
|
|
11779
12668
|
function normalizeComparableExistingWorkflowRetryPolicy(policy) {
|
|
11780
12669
|
if (!policy) return;
|
|
@@ -11993,7 +12882,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
|
|
|
11993
12882
|
executionPolicyKey: toPlatformExecutionPolicyKey(replace.policy),
|
|
11994
12883
|
concurrencyPolicy: toConcurrencyPolicyInit(replace.policy.concurrencyPolicy)
|
|
11995
12884
|
});
|
|
11996
|
-
await client
|
|
12885
|
+
await writeMetadataLabels(client, replace.metaRequest);
|
|
11997
12886
|
}),
|
|
11998
12887
|
...changeSet.creates.map(async (create) => {
|
|
11999
12888
|
await client.createWorkflowJobFunctionExecutionPolicy({
|
|
@@ -12002,7 +12891,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
|
|
|
12002
12891
|
executionPolicyKey: toPlatformExecutionPolicyKey(create.policy),
|
|
12003
12892
|
concurrencyPolicy: toConcurrencyPolicyInit(create.policy.concurrencyPolicy)
|
|
12004
12893
|
});
|
|
12005
|
-
await client
|
|
12894
|
+
await writeMetadataLabels(client, create.metaRequest);
|
|
12006
12895
|
}),
|
|
12007
12896
|
...changeSet.updates.map(async (update) => {
|
|
12008
12897
|
await client.updateWorkflowJobFunctionExecutionPolicy({
|
|
@@ -12010,7 +12899,7 @@ async function applyWorkflowJobFunctionExecutionPolicy(client, plan, phase = "cr
|
|
|
12010
12899
|
executionPolicyName: update.policy.name,
|
|
12011
12900
|
concurrencyPolicy: toConcurrencyPolicyInit(update.policy.concurrencyPolicy)
|
|
12012
12901
|
});
|
|
12013
|
-
await client
|
|
12902
|
+
await writeMetadataLabels(client, update.metaRequest);
|
|
12014
12903
|
})
|
|
12015
12904
|
]);
|
|
12016
12905
|
else await Promise.all(changeSet.deletes.map((del) => client.deleteWorkflowJobFunctionExecutionPolicy({
|
|
@@ -12082,6 +12971,21 @@ async function applyDeploymentPlans(client, workspaceId, deployments) {
|
|
|
12082
12971
|
|
|
12083
12972
|
//#endregion
|
|
12084
12973
|
//#region src/cli/commands/deploy/config-id-injector.ts
|
|
12974
|
+
/**
|
|
12975
|
+
* Warn when a command that decides resource ownership resolved a config
|
|
12976
|
+
* without an app id.
|
|
12977
|
+
*
|
|
12978
|
+
* Injection only reaches an inline `defineConfig({...})` call, so a config
|
|
12979
|
+
* that re-exports one from another file resolves without an id and nothing
|
|
12980
|
+
* says so. Ownership then falls back to the application name, and resources
|
|
12981
|
+
* tagged with an id from an earlier deploy read as another application's.
|
|
12982
|
+
* @param appId - Application id from the resolved config, when it has one
|
|
12983
|
+
*/
|
|
12984
|
+
function warnMissingAppId(appId) {
|
|
12985
|
+
if (appId) return;
|
|
12986
|
+
logger.warn("The config resolved without an 'id'.");
|
|
12987
|
+
logger.log(" Resources tagged with an id from an earlier deploy read as another application's:\n deploy asks before taking them over, and remove leaves them in place. Only resources\n carrying no id are matched by application name.\n Add an 'id' to the object passed to defineConfig() — a config that re-exports it from\n another file cannot have one injected automatically. 'tailor setup' can add it for you.");
|
|
12988
|
+
}
|
|
12085
12989
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12086
12990
|
function findDefineConfigCalls(node, results) {
|
|
12087
12991
|
if (!node || typeof node !== "object") return;
|
|
@@ -12237,27 +13141,26 @@ function insertIdProperty(source, configObj, id) {
|
|
|
12237
13141
|
//#region src/cli/commands/deploy/confirm.ts
|
|
12238
13142
|
/**
|
|
12239
13143
|
* Confirm reassignment of resources when owner conflicts are detected.
|
|
12240
|
-
* Splits into
|
|
12241
|
-
*
|
|
12242
|
-
*
|
|
13144
|
+
* Splits into three scenarios, each with its own prompt because the
|
|
13145
|
+
* user-facing meaning is different: the resource carries the same sdk-name and
|
|
13146
|
+
* an sdk-app-id the config does not match, either because the config now holds
|
|
13147
|
+
* a different id (regeneration) or because it holds none at all; or the
|
|
13148
|
+
* resource carries a different sdk-name (name mismatch).
|
|
12243
13149
|
* @param conflicts - Detected owner conflicts
|
|
12244
13150
|
* @param appName - Target application name
|
|
12245
13151
|
* @param yes - Whether to auto-confirm without prompting
|
|
13152
|
+
* @param appId - Target application id, when the config resolves to one
|
|
12246
13153
|
* @returns Promise that resolves when confirmation completes
|
|
12247
13154
|
*/
|
|
12248
|
-
async function confirmOwnerConflict(conflicts, appName, yes) {
|
|
13155
|
+
async function confirmOwnerConflict(conflicts, appName, yes, appId) {
|
|
12249
13156
|
if (conflicts.length === 0) return;
|
|
12250
|
-
const
|
|
13157
|
+
const idMismatches = conflicts.filter((c) => c.currentOwner === appName);
|
|
12251
13158
|
const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
|
|
12252
|
-
if (
|
|
13159
|
+
if (idMismatches.length > 0) await (appId ? confirmIdRegeneration(idMismatches, appName, yes) : confirmMissingConfigId(idMismatches, appName, yes));
|
|
12253
13160
|
if (nameMismatches.length > 0) await confirmNameMismatch(nameMismatches, appName, yes);
|
|
12254
13161
|
}
|
|
12255
13162
|
async function confirmIdRegeneration(conflicts, appName, yes) {
|
|
12256
|
-
|
|
12257
|
-
logger.log(" These resources still carry the previous id.");
|
|
12258
|
-
logger.newline();
|
|
12259
|
-
logger.log(` ${styles.info("Resources")}:`);
|
|
12260
|
-
for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
|
|
13163
|
+
logIdMismatch(`Application id was regenerated for "${appName}":`, conflicts);
|
|
12261
13164
|
if (yes) {
|
|
12262
13165
|
logger.success("Re-tagging resources with the new id (--yes flag specified)...", { mode: "plain" });
|
|
12263
13166
|
return;
|
|
@@ -12270,6 +13173,27 @@ async function confirmIdRegeneration(conflicts, appName, yes) {
|
|
|
12270
13173
|
To override, run again and confirm, or use --yes flag.
|
|
12271
13174
|
`);
|
|
12272
13175
|
}
|
|
13176
|
+
function logIdMismatch(heading, conflicts) {
|
|
13177
|
+
logger.warn(heading);
|
|
13178
|
+
logger.log(" These resources are tagged with an id from an earlier deploy.");
|
|
13179
|
+
logger.newline();
|
|
13180
|
+
logger.log(` ${styles.info("Resources")}:`);
|
|
13181
|
+
for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
|
|
13182
|
+
}
|
|
13183
|
+
async function confirmMissingConfigId(conflicts, appName, yes) {
|
|
13184
|
+
logIdMismatch(`No application id resolved for "${appName}":`, conflicts);
|
|
13185
|
+
if (yes) {
|
|
13186
|
+
logger.success("Managing these resources by name (--yes flag specified)...", { mode: "plain" });
|
|
13187
|
+
return;
|
|
13188
|
+
}
|
|
13189
|
+
if (!await prompt.confirm({
|
|
13190
|
+
message: `Drop that id and manage these resources by name for "${appName}"?\n${styles.dim("(the config resolves without an 'id', so ownership falls back to the application name)")}`,
|
|
13191
|
+
default: false
|
|
13192
|
+
})) throw new Error(multiline`
|
|
13193
|
+
Apply cancelled. Resources remain tagged with their current id.
|
|
13194
|
+
Restore the 'id' in your config to keep owning them by id, or run again and confirm to own them by name.
|
|
13195
|
+
`);
|
|
13196
|
+
}
|
|
12273
13197
|
async function confirmNameMismatch(conflicts, appName, yes) {
|
|
12274
13198
|
const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
|
|
12275
13199
|
logger.warn("Application name mismatch detected:");
|
|
@@ -12344,6 +13268,125 @@ async function confirmImportantResourceDeletion(resources, yes) {
|
|
|
12344
13268
|
To override, run again and confirm, or use --yes flag.
|
|
12345
13269
|
`);
|
|
12346
13270
|
}
|
|
13271
|
+
/**
|
|
13272
|
+
* Confirm continuing without an application recorded as a dependency.
|
|
13273
|
+
*
|
|
13274
|
+
* A previous deploy recorded that another config's executors make this config's
|
|
13275
|
+
* resources publish events. Applying this config alone resolves those flags from
|
|
13276
|
+
* a smaller set of executors, which turns publishing off.
|
|
13277
|
+
* @param missing - Recorded dependencies absent from this deploy
|
|
13278
|
+
* @param yes - Whether `--yes` was passed
|
|
13279
|
+
* @returns Promise that resolves when the deploy may continue
|
|
13280
|
+
*/
|
|
13281
|
+
async function confirmMissingDependentApps(missing, yes) {
|
|
13282
|
+
if (missing.length === 0) return;
|
|
13283
|
+
logger.warn("Applications recorded as depending on this deploy are missing:");
|
|
13284
|
+
for (const entry of missing) logger.log(` • application id ${styles.info(entry.appId)} depends on ${styles.bold(entry.resource)} (${entry.reason})`);
|
|
13285
|
+
logger.newline();
|
|
13286
|
+
logger.log(" Applying without them turns off event publishing on the resources above:");
|
|
13287
|
+
logger.log(" nothing in this deploy subscribes to them, so the value resolves to false.");
|
|
13288
|
+
logger.log(" To keep it, add their configs to --config, or set publishEvents on");
|
|
13289
|
+
logger.log(" the resources above.");
|
|
13290
|
+
if (yes) {
|
|
13291
|
+
logger.warn("Continuing without them (--yes flag specified); applying turns publishing off.");
|
|
13292
|
+
return;
|
|
13293
|
+
}
|
|
13294
|
+
if (!await prompt.confirm({
|
|
13295
|
+
message: "Continue without them?",
|
|
13296
|
+
default: false
|
|
13297
|
+
})) throw new Error(multiline`
|
|
13298
|
+
Apply cancelled. Add the missing configs to --config, or set publishEvents
|
|
13299
|
+
explicitly on the resources that should keep publishing.
|
|
13300
|
+
`);
|
|
13301
|
+
}
|
|
13302
|
+
|
|
13303
|
+
//#endregion
|
|
13304
|
+
//#region src/cli/commands/deploy/dependency-records.ts
|
|
13305
|
+
/**
|
|
13306
|
+
* List the resources whose `publishEvents` this run recomputes.
|
|
13307
|
+
*
|
|
13308
|
+
* A resource that declares the value keeps it whatever the run covers, so no
|
|
13309
|
+
* absent config can change it and its records cannot matter. Skipping those is
|
|
13310
|
+
* what keeps a declared value from prompting forever.
|
|
13311
|
+
* @param workspaceId - Workspace being deployed to
|
|
13312
|
+
* @param application - Application whose resources are listed
|
|
13313
|
+
* @param jobsByWorkflow - Job names each workflow runs, keyed by its main job
|
|
13314
|
+
* @param subscribedKeys - Resources this run subscribes to, by resource key
|
|
13315
|
+
* @returns One entry per value that leaves `publishEvents` unset
|
|
13316
|
+
*/
|
|
13317
|
+
function recomputedResources(workspaceId, application, jobsByWorkflow, subscribedKeys) {
|
|
13318
|
+
const resources = [];
|
|
13319
|
+
for (const service of application.tailorDBServices) for (const [typeName, type] of Object.entries(service.types)) if (type.settings.publishEvents === void 0) resources.push({
|
|
13320
|
+
trn: tailorDBTypeTrn(workspaceId, service.namespace, typeName),
|
|
13321
|
+
key: eventSourceKey.tailorDBType(service.namespace, typeName),
|
|
13322
|
+
scope: "resource",
|
|
13323
|
+
label: eventSourceLabel.tailorDBType(typeName)
|
|
13324
|
+
});
|
|
13325
|
+
for (const service of application.resolverServices) for (const resolver of Object.values(service.resolvers)) if (resolver.publishEvents === void 0) resources.push({
|
|
13326
|
+
trn: resolverTrn(workspaceId, service.namespace, resolver.name),
|
|
13327
|
+
key: eventSourceKey.resolver(service.namespace, resolver.name),
|
|
13328
|
+
scope: "resource",
|
|
13329
|
+
label: eventSourceLabel.resolver(resolver.name)
|
|
13330
|
+
});
|
|
13331
|
+
for (const idp of application.idpServices) if (idp.publishEvents === void 0) resources.push({
|
|
13332
|
+
trn: resourceTrn(workspaceId, "idp", idp.name),
|
|
13333
|
+
key: eventSourceKey.idp(idp.name),
|
|
13334
|
+
scope: "resource",
|
|
13335
|
+
label: eventSourceLabel.idpService(idp.name)
|
|
13336
|
+
});
|
|
13337
|
+
const explicitByJob = new Map((application.workflowService?.jobs ?? []).map((job) => [job.name, job.publishEvents]));
|
|
13338
|
+
const subscribedJobNames = subscribedWorkflowJobNames({
|
|
13339
|
+
workflows: Object.values(application.workflowService?.workflows ?? {}),
|
|
13340
|
+
mainJobDeps: jobsByWorkflow,
|
|
13341
|
+
isSubscribed: (workflowName) => subscribedKeys.has(eventSourceKey.workflowJobs(workflowName))
|
|
13342
|
+
});
|
|
13343
|
+
for (const workflow of Object.values(application.workflowService?.workflows ?? {})) {
|
|
13344
|
+
const trn = resourceTrn(workspaceId, "workflow", workflow.name);
|
|
13345
|
+
if (workflow.publishEvents === void 0) resources.push({
|
|
13346
|
+
trn,
|
|
13347
|
+
key: eventSourceKey.workflow(workflow.name),
|
|
13348
|
+
scope: "resource",
|
|
13349
|
+
label: eventSourceLabel.workflow(workflow.name)
|
|
13350
|
+
});
|
|
13351
|
+
if ((jobsByWorkflow[workflow.mainJob.name] ?? []).some((jobName) => explicitByJob.get(jobName) === void 0 && !subscribedJobNames.has(jobName))) resources.push({
|
|
13352
|
+
trn,
|
|
13353
|
+
key: eventSourceKey.workflowJobs(workflow.name),
|
|
13354
|
+
scope: "jobs",
|
|
13355
|
+
label: eventSourceLabel.workflowJobs(workflow.name)
|
|
13356
|
+
});
|
|
13357
|
+
}
|
|
13358
|
+
return resources;
|
|
13359
|
+
}
|
|
13360
|
+
/**
|
|
13361
|
+
* Read the applications recorded as depending on this config's resources but
|
|
13362
|
+
* absent from the current deploy.
|
|
13363
|
+
*
|
|
13364
|
+
* Records live on the resources rather than on the application, so this survives
|
|
13365
|
+
* the application being renamed and stops reporting a resource that is gone.
|
|
13366
|
+
*
|
|
13367
|
+
* A resource this run still subscribes to is skipped: its value resolves to `true`
|
|
13368
|
+
* from the run's own executors, so the absent config changes nothing about it and
|
|
13369
|
+
* asking would be asking about something that cannot happen. What is left over is
|
|
13370
|
+
* a resource that really does turn off.
|
|
13371
|
+
* @param params - Client, workspace, application, and the run's inputs
|
|
13372
|
+
* @param params.client - Operator client instance
|
|
13373
|
+
* @param params.workspaceId - Workspace being deployed to
|
|
13374
|
+
* @param params.application - Application being planned
|
|
13375
|
+
* @param params.runAppIds - Stable ids of every application in the run
|
|
13376
|
+
* @param params.subscribedKeys - Resources this run subscribes to, by resource key
|
|
13377
|
+
* @param params.jobsByWorkflow - Job names each workflow runs, keyed by its main job
|
|
13378
|
+
* @returns Recorded dependencies missing from the run
|
|
13379
|
+
*/
|
|
13380
|
+
async function fetchMissingDependentApps(params) {
|
|
13381
|
+
const { client, workspaceId, application, runAppIds, subscribedKeys, jobsByWorkflow } = params;
|
|
13382
|
+
return (await Promise.all(recomputedResources(workspaceId, application, jobsByWorkflow, subscribedKeys).filter(({ key }) => !subscribedKeys.has(key)).map(async ({ trn, scope, label }) => {
|
|
13383
|
+
return recordedDependencies((await getOrNull(() => client.getMetadata({ trn })))?.metadata?.labels, scope).filter((dependency) => !runAppIds.has(dependency.appId)).map((dependency) => ({
|
|
13384
|
+
resource: label,
|
|
13385
|
+
appId: dependency.appId,
|
|
13386
|
+
reason: dependency.reason
|
|
13387
|
+
}));
|
|
13388
|
+
}))).flat();
|
|
13389
|
+
}
|
|
12347
13390
|
|
|
12348
13391
|
//#endregion
|
|
12349
13392
|
//#region src/cli/commands/deploy/validate-plan.ts
|
|
@@ -13421,57 +14464,187 @@ async function resolveDeployWorkspace(options = {}) {
|
|
|
13421
14464
|
//#endregion
|
|
13422
14465
|
//#region src/cli/commands/deploy/deploy.ts
|
|
13423
14466
|
/**
|
|
13424
|
-
* Resolve
|
|
13425
|
-
*
|
|
13426
|
-
* and
|
|
13427
|
-
*
|
|
13428
|
-
*
|
|
13429
|
-
* @
|
|
13430
|
-
* @returns Set of IdP names targeted by idpUser triggers
|
|
14467
|
+
* Resolve which IdP an `idpUser` trigger subscribes to. A trigger that omits the
|
|
14468
|
+
* `idp` option means the only configured IdP; when several exist the target is
|
|
14469
|
+
* ambiguous and the apply pipeline throws a clearer error for it later.
|
|
14470
|
+
* @param application - Application declaring the executor
|
|
14471
|
+
* @param trigger - The executor's `idpUser` trigger
|
|
14472
|
+
* @returns The subscribed IdP name, or undefined when ambiguous
|
|
13431
14473
|
*/
|
|
13432
|
-
function
|
|
13433
|
-
|
|
14474
|
+
function subscribedIdpName(application, trigger) {
|
|
14475
|
+
if (trigger.idp != null) return trigger.idp;
|
|
13434
14476
|
const idps = collectApplicationIdpNames(application);
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
else if (idps.size === 1) {
|
|
13439
|
-
const [idp] = idps;
|
|
13440
|
-
if (idp) targets.add(idp);
|
|
13441
|
-
}
|
|
13442
|
-
}
|
|
13443
|
-
return targets;
|
|
13444
|
-
}
|
|
13445
|
-
function collectDeployIdpUserTriggerTargets(targets) {
|
|
13446
|
-
const triggerTargets = /* @__PURE__ */ new Set();
|
|
13447
|
-
for (const target of targets) for (const idpName of collectIdpUserTriggerTargets(target.application)) triggerTargets.add(idpName);
|
|
13448
|
-
return triggerTargets;
|
|
14477
|
+
if (idps.size !== 1) return;
|
|
14478
|
+
const [only] = idps;
|
|
14479
|
+
return only;
|
|
13449
14480
|
}
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
14481
|
+
/**
|
|
14482
|
+
* Build the error for a subscription whose resource no config in the run declares.
|
|
14483
|
+
*
|
|
14484
|
+
* The advice differs by cause: a config that declares the resource as owned
|
|
14485
|
+
* elsewhere is missing its peer from `--config`, while one that declares nothing
|
|
14486
|
+
* external is naming a resource that does not exist.
|
|
14487
|
+
* @param executorName - Name of the subscribing executor
|
|
14488
|
+
* @param lookup - How the subscribed resource is found
|
|
14489
|
+
* @param subscriber - Deployment target declaring the executor
|
|
14490
|
+
* @returns Error message
|
|
14491
|
+
*/
|
|
14492
|
+
function missingOwnerMessage(executorName, lookup, subscriber) {
|
|
14493
|
+
const subject = `Executor "${executorName}" subscribes to ${lookup.resource}, which no config in this deploy declares.`;
|
|
14494
|
+
const external = lookup.externalHint(subscriber);
|
|
14495
|
+
if (external) return `${subject} This config declares ${external}, so add the config that owns it to --config.`;
|
|
14496
|
+
if (lookup.trigger.kind === "workflowExecution" || lookup.trigger.kind === "workflowJobExecution") return `${subject} A workflow has no "external" declaration, unlike a TailorDB namespace or resolver, so nothing in this config points at the one that owns it. Check the name, or add the config that declares the workflow to --config.`;
|
|
14497
|
+
return `${subject} This config declares nothing external that could hold it, so check the name.`;
|
|
14498
|
+
}
|
|
14499
|
+
/**
|
|
14500
|
+
* Resolve every event subscription in the run to the config that declares the
|
|
14501
|
+
* subscribed resource.
|
|
14502
|
+
*
|
|
14503
|
+
* Doing this once keeps the flags the planners resolve and the cross-config
|
|
14504
|
+
* dependencies recorded on the application in agreement — both are read off the
|
|
14505
|
+
* same list.
|
|
14506
|
+
* @param targets - Built deployment targets in the run
|
|
14507
|
+
* @returns One entry per executor whose trigger names a publishing resource
|
|
14508
|
+
*/
|
|
14509
|
+
function collectEventSubscriptions(targets) {
|
|
14510
|
+
const subscriptions = [];
|
|
13453
14511
|
const applications = targets.map((target) => target.application);
|
|
13454
|
-
for (const
|
|
13455
|
-
const
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
14512
|
+
for (const subscriber of targets) {
|
|
14513
|
+
const executors = subscriber.application.executorService?.executors ?? {};
|
|
14514
|
+
const visibility = collectSubscriberVisibility(subscriber, applications);
|
|
14515
|
+
for (const executor of Object.values(executors)) {
|
|
14516
|
+
const lookup = eventSourceLookup(executor, subscriber, visibility);
|
|
14517
|
+
if (!lookup) continue;
|
|
14518
|
+
const entry = {
|
|
14519
|
+
subscriber,
|
|
14520
|
+
executorName: executor.name,
|
|
14521
|
+
resource: lookup.resource,
|
|
14522
|
+
trigger: lookup.trigger
|
|
14523
|
+
};
|
|
14524
|
+
if (lookup.declaredBy(subscriber)) {
|
|
14525
|
+
subscriptions.push({
|
|
14526
|
+
...entry,
|
|
14527
|
+
owner: subscriber,
|
|
14528
|
+
pinned: lookup.pinned(subscriber),
|
|
14529
|
+
key: lookup.keyIn(subscriber)
|
|
14530
|
+
});
|
|
14531
|
+
continue;
|
|
14532
|
+
}
|
|
14533
|
+
const peers = targets.filter((target) => target.config.path !== subscriber.config.path && lookup.declaredBy(target));
|
|
14534
|
+
const owners = lookup.narrowOwners(peers);
|
|
14535
|
+
if (owners === "ambiguous") continue;
|
|
14536
|
+
const [owner] = owners;
|
|
14537
|
+
if (!owner) throw new Error(missingOwnerMessage(executor.name, lookup, subscriber));
|
|
14538
|
+
subscriptions.push({
|
|
14539
|
+
...entry,
|
|
14540
|
+
owner,
|
|
14541
|
+
pinned: lookup.pinned(owner),
|
|
14542
|
+
key: lookup.keyIn(owner)
|
|
14543
|
+
});
|
|
13459
14544
|
}
|
|
13460
14545
|
}
|
|
13461
|
-
return
|
|
14546
|
+
return subscriptions;
|
|
13462
14547
|
}
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
14548
|
+
/**
|
|
14549
|
+
* Key the resources this run subscribes to among the target's own.
|
|
14550
|
+
*
|
|
14551
|
+
* A resource with a subscriber in the run resolves to `true` from that subscriber
|
|
14552
|
+
* alone, so no absent config can change it.
|
|
14553
|
+
* @param subscriptions - Subscriptions resolved across the run
|
|
14554
|
+
* @param owner - Deployment target being planned
|
|
14555
|
+
* @returns Resource keys the run subscribes to
|
|
14556
|
+
*/
|
|
14557
|
+
function subscribedResourceKeys(subscriptions, owner) {
|
|
14558
|
+
return new Set(ownedSubscriptions(subscriptions, owner).flatMap((subscription) => subscription.key ?? []));
|
|
14559
|
+
}
|
|
14560
|
+
/**
|
|
14561
|
+
* Select the subscriptions pointing at resources the given target declares.
|
|
14562
|
+
* @param subscriptions - Subscriptions resolved across the run
|
|
14563
|
+
* @param owner - Deployment target being planned
|
|
14564
|
+
* @returns Subscriptions whose subscribed resource belongs to `owner`
|
|
14565
|
+
*/
|
|
14566
|
+
function ownedSubscriptions(subscriptions, owner) {
|
|
14567
|
+
return subscriptions.filter((subscription) => subscription.owner.config.path === owner.config.path);
|
|
14568
|
+
}
|
|
14569
|
+
/**
|
|
14570
|
+
* Collect the TailorDB types subscribed to among the given subscriptions.
|
|
14571
|
+
* @param subscriptions - Subscriptions owned by the target being planned
|
|
14572
|
+
* @returns Subscribed type names
|
|
14573
|
+
*/
|
|
14574
|
+
function subscribedTailorDBTypes(subscriptions) {
|
|
14575
|
+
return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "tailordb" ? [subscription.trigger.typeName] : []));
|
|
14576
|
+
}
|
|
14577
|
+
/**
|
|
14578
|
+
* Collect the resolvers subscribed to among the given subscriptions.
|
|
14579
|
+
* @param subscriptions - Subscriptions owned by the target being planned
|
|
14580
|
+
* @returns Subscribed resolver names
|
|
14581
|
+
*/
|
|
14582
|
+
function subscribedResolvers(subscriptions) {
|
|
14583
|
+
return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "resolverExecuted" ? [subscription.trigger.resolverName] : []));
|
|
14584
|
+
}
|
|
14585
|
+
/**
|
|
14586
|
+
* Collect the IdPs subscribed to among the given subscriptions.
|
|
14587
|
+
* @param subscriptions - Subscriptions owned by the target being planned
|
|
14588
|
+
* @returns Subscribed IdP names
|
|
14589
|
+
*/
|
|
14590
|
+
function subscribedIdps(subscriptions) {
|
|
14591
|
+
return new Set(subscriptions.flatMap((subscription) => subscription.trigger.kind === "idpUser" ? [subscribedIdpName(subscription.subscriber.application, subscription.trigger) ?? []].flat() : []));
|
|
14592
|
+
}
|
|
14593
|
+
/**
|
|
14594
|
+
* Collect the workflows subscribed to, per event granularity level.
|
|
14595
|
+
* @param subscriptions - Subscriptions owned by the target being planned
|
|
14596
|
+
* @returns Subscribers keyed by event granularity level
|
|
14597
|
+
*/
|
|
14598
|
+
function subscribedWorkflows(subscriptions) {
|
|
14599
|
+
const execution = { workflowNames: /* @__PURE__ */ new Set() };
|
|
14600
|
+
const jobExecution = { workflowNames: /* @__PURE__ */ new Set() };
|
|
14601
|
+
for (const { trigger } of subscriptions) if (trigger.kind === "workflowExecution") execution.workflowNames.add(trigger.workflowName);
|
|
14602
|
+
else if (trigger.kind === "workflowJobExecution") jobExecution.workflowNames.add(trigger.workflowName);
|
|
14603
|
+
return {
|
|
14604
|
+
execution,
|
|
14605
|
+
jobExecution
|
|
14606
|
+
};
|
|
14607
|
+
}
|
|
14608
|
+
/**
|
|
14609
|
+
* Collect the applications that have to take part in the same deploy for this
|
|
14610
|
+
* target's resources to be applied the same way.
|
|
14611
|
+
*
|
|
14612
|
+
* An executor in another config makes the resource it subscribes to publish
|
|
14613
|
+
* events, so deploying this config without that one would resolve the flag from
|
|
14614
|
+
* a smaller set of executors and turn publishing off.
|
|
14615
|
+
*
|
|
14616
|
+
* A resource that declares `publishEvents` keeps its value either way, so
|
|
14617
|
+
* recording a dependency for it would ask about a partial deploy that changes
|
|
14618
|
+
* nothing — and `prompt.confirm` rejects outright where it cannot ask, failing a
|
|
14619
|
+
* deploy that was never at risk.
|
|
14620
|
+
*
|
|
14621
|
+
* Records are keyed by resource rather than by application: the resource is what
|
|
14622
|
+
* carries them, so a record survives the owner being renamed and disappears with
|
|
14623
|
+
* the resource itself.
|
|
14624
|
+
* @param subscriptions - Subscriptions owned by the target being planned
|
|
14625
|
+
* @returns Dependent application ids and reasons, keyed by resource
|
|
14626
|
+
*/
|
|
14627
|
+
function collectDependentApps(subscriptions) {
|
|
14628
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
14629
|
+
for (const { subscriber, owner, pinned, key } of subscriptions) {
|
|
14630
|
+
if (subscriber.config.path === owner.config.path || pinned || key === void 0) continue;
|
|
14631
|
+
const appId = subscriber.application.id;
|
|
14632
|
+
if (appId === void 0) continue;
|
|
14633
|
+
const dependents = byResource.get(key) ?? /* @__PURE__ */ new Map();
|
|
14634
|
+
dependents.set(appId, "publish-events");
|
|
14635
|
+
byResource.set(key, dependents);
|
|
14636
|
+
}
|
|
14637
|
+
return byResource;
|
|
14638
|
+
}
|
|
14639
|
+
/**
|
|
14640
|
+
* Collect explicit `publishEvents` values declared on this target's workflow jobs.
|
|
14641
|
+
* @param target - Deployment target being planned
|
|
14642
|
+
* @returns Explicit flags keyed by job name
|
|
14643
|
+
*/
|
|
14644
|
+
function collectWorkflowJobPublishEvents(target) {
|
|
14645
|
+
const jobPublishEvents = /* @__PURE__ */ new Map();
|
|
14646
|
+
for (const job of target.application.workflowService?.jobs ?? []) if (job.publishEvents !== void 0) jobPublishEvents.set(job.name, job.publishEvents);
|
|
14647
|
+
return jobPublishEvents;
|
|
13475
14648
|
}
|
|
13476
14649
|
function collectExpectedLocalStaticWebsiteNames(targets) {
|
|
13477
14650
|
const websiteNames = /* @__PURE__ */ new Set();
|
|
@@ -13929,7 +15102,9 @@ async function loadDeployConfig(params) {
|
|
|
13929
15102
|
buildOnly
|
|
13930
15103
|
});
|
|
13931
15104
|
}
|
|
13932
|
-
|
|
15105
|
+
const loaded = await loadConfig(configPath);
|
|
15106
|
+
if (!buildOnly) warnMissingAppId(loaded.config.id);
|
|
15107
|
+
return loaded;
|
|
13933
15108
|
});
|
|
13934
15109
|
}
|
|
13935
15110
|
async function buildDeploymentTargets(params) {
|
|
@@ -14005,6 +15180,161 @@ function assertUniqueGlobalResourceNames(targets) {
|
|
|
14005
15180
|
}
|
|
14006
15181
|
}
|
|
14007
15182
|
}
|
|
15183
|
+
function collectSubscriberVisibility(subscriber, applications) {
|
|
15184
|
+
return {
|
|
15185
|
+
tailorDBTypes: collectVisibleTailorDBTypeNamespaces(subscriber.application, applications),
|
|
15186
|
+
resolvers: collectVisibleResolverNamespaces(subscriber.application, applications),
|
|
15187
|
+
idps: collectVisibleIdpNames(subscriber.application, applications)
|
|
15188
|
+
};
|
|
15189
|
+
}
|
|
15190
|
+
/**
|
|
15191
|
+
* TailorDB namespaces the config declares as owned elsewhere.
|
|
15192
|
+
* @param target - Deployment target declaring the executor
|
|
15193
|
+
* @returns Namespace names declared with `external: true`
|
|
15194
|
+
*/
|
|
15195
|
+
function externalTailorDBNamespaces(target) {
|
|
15196
|
+
return target.application.externalTailorDBNamespaces;
|
|
15197
|
+
}
|
|
15198
|
+
function externalSubgraphNames(target, subgraphType, localNames) {
|
|
15199
|
+
return target.application.subgraphs.filter((subgraph) => subgraph.Type === subgraphType && !localNames.has(subgraph.Name)).map((subgraph) => subgraph.Name);
|
|
15200
|
+
}
|
|
15201
|
+
function externalResolverNamespaces(target) {
|
|
15202
|
+
return externalSubgraphNames(target, "pipeline", new Set(target.application.resolverServices.map((service) => service.namespace)));
|
|
15203
|
+
}
|
|
15204
|
+
function externalIdpNames(target) {
|
|
15205
|
+
return externalSubgraphNames(target, "idp", new Set(target.application.idpServices.map((idp) => idp.name)));
|
|
15206
|
+
}
|
|
15207
|
+
/**
|
|
15208
|
+
* Describe the external declarations that could hold a missing resource.
|
|
15209
|
+
* @param kind - Resource kind named in the message, e.g. `TailorDB namespace`
|
|
15210
|
+
* @param names - External names the subscribing config declares
|
|
15211
|
+
* @returns A phrase for the error message, or undefined when it declares none
|
|
15212
|
+
*/
|
|
15213
|
+
function describeExternal(kind, names) {
|
|
15214
|
+
if (names.length === 0) return;
|
|
15215
|
+
return `external ${names.length === 1 ? kind : `${kind}s`} ${names.map((name) => `"${name}"`).join(", ")}`;
|
|
15216
|
+
}
|
|
15217
|
+
function declaresTailorDBType(target, typeName) {
|
|
15218
|
+
return target.application.tailorDBServices.some((service) => service.types[typeName]) || void 0;
|
|
15219
|
+
}
|
|
15220
|
+
function declaresResolver(target, resolverName) {
|
|
15221
|
+
return target.application.resolverServices.some((service) => Object.values(service.resolvers).some((resolver) => resolver.name === resolverName)) || void 0;
|
|
15222
|
+
}
|
|
15223
|
+
function declaresIdp(target, idpName) {
|
|
15224
|
+
return target.application.idpServices.some((entry) => entry.name === idpName) || void 0;
|
|
15225
|
+
}
|
|
15226
|
+
function declaresWorkflow(target, workflowName) {
|
|
15227
|
+
return Object.values(target.application.workflowService?.workflows ?? {}).some((entry) => entry.name === workflowName) || void 0;
|
|
15228
|
+
}
|
|
15229
|
+
function pinsTailorDBType(target, typeName) {
|
|
15230
|
+
for (const service of target.application.tailorDBServices) {
|
|
15231
|
+
const type = service.types[typeName];
|
|
15232
|
+
if (type) return type.settings.publishEvents !== void 0;
|
|
15233
|
+
}
|
|
15234
|
+
return false;
|
|
15235
|
+
}
|
|
15236
|
+
function pinsResolverIn(target, namespace, resolverName) {
|
|
15237
|
+
return target.application.resolverServices.some((service) => service.namespace === namespace && Object.values(service.resolvers).some((resolver) => resolver.name === resolverName && resolver.publishEvents !== void 0));
|
|
15238
|
+
}
|
|
15239
|
+
function pinsIdp(target, idpName) {
|
|
15240
|
+
return target.application.idpServices.some((entry) => entry.name === idpName && entry.publishEvents !== void 0);
|
|
15241
|
+
}
|
|
15242
|
+
function pinsWorkflow(target, workflowName) {
|
|
15243
|
+
return Object.values(target.application.workflowService?.workflows ?? {}).some((entry) => entry.name === workflowName && entry.publishEvents !== void 0);
|
|
15244
|
+
}
|
|
15245
|
+
function pinsEveryJobOfWorkflow(target, workflowName) {
|
|
15246
|
+
const workflow = Object.values(target.application.workflowService?.workflows ?? {}).find((entry) => entry.name === workflowName);
|
|
15247
|
+
if (workflow === void 0) return false;
|
|
15248
|
+
const jobNames = target.workflowBuildResult?.mainJobDeps[workflow.mainJob.name] ?? [];
|
|
15249
|
+
const declared = collectWorkflowJobPublishEvents(target);
|
|
15250
|
+
return jobNames.length > 0 && jobNames.every((jobName) => declared.has(jobName));
|
|
15251
|
+
}
|
|
15252
|
+
function tailorDBTypeNamespaceIn(target, typeName) {
|
|
15253
|
+
return target.application.tailorDBServices.find((service) => service.types[typeName])?.namespace;
|
|
15254
|
+
}
|
|
15255
|
+
function narrowByVisibleNamespace(params) {
|
|
15256
|
+
const { candidates, visible, resourceKey, declaresIn } = params;
|
|
15257
|
+
if (!visible.has(resourceKey)) return [];
|
|
15258
|
+
const namespace = visible.get(resourceKey);
|
|
15259
|
+
if (namespace === void 0) return "ambiguous";
|
|
15260
|
+
return candidates.filter((target) => declaresIn(target, namespace));
|
|
15261
|
+
}
|
|
15262
|
+
function declaresTailorDBTypeIn(target, namespace, typeName) {
|
|
15263
|
+
return target.application.tailorDBServices.some((service) => service.namespace === namespace && Boolean(service.types[typeName]));
|
|
15264
|
+
}
|
|
15265
|
+
function declaresResolverIn(target, namespace, resolverName) {
|
|
15266
|
+
return target.application.resolverServices.some((service) => service.namespace === namespace && Object.values(service.resolvers).some((resolver) => resolver.name === resolverName));
|
|
15267
|
+
}
|
|
15268
|
+
/**
|
|
15269
|
+
* Resolve how to find the resource an executor's event trigger subscribes to.
|
|
15270
|
+
* @param executor - Executor declared by the subscribing config
|
|
15271
|
+
* @param subscriber - Deployment target declaring the executor
|
|
15272
|
+
* @param visibility - What the subscribing config can see, by resource kind
|
|
15273
|
+
* @returns The lookup, or undefined for triggers with no publishing resource
|
|
15274
|
+
*/
|
|
15275
|
+
function eventSourceLookup(executor, subscriber, visibility) {
|
|
15276
|
+
const { trigger } = executor;
|
|
15277
|
+
switch (trigger.kind) {
|
|
15278
|
+
case "tailordb": return {
|
|
15279
|
+
resource: publishEventsConflict.tailorDBType(trigger.typeName).resource,
|
|
15280
|
+
trigger,
|
|
15281
|
+
declaredBy: (target) => declaresTailorDBType(target, trigger.typeName),
|
|
15282
|
+
pinned: (target) => pinsTailorDBType(target, trigger.typeName),
|
|
15283
|
+
keyIn: (owner) => {
|
|
15284
|
+
const namespace = tailorDBTypeNamespaceIn(owner, trigger.typeName);
|
|
15285
|
+
return namespace && eventSourceKey.tailorDBType(namespace, trigger.typeName);
|
|
15286
|
+
},
|
|
15287
|
+
narrowOwners: (candidates) => narrowByVisibleNamespace({
|
|
15288
|
+
candidates,
|
|
15289
|
+
visible: visibility.tailorDBTypes,
|
|
15290
|
+
resourceKey: trigger.typeName,
|
|
15291
|
+
declaresIn: (target, namespace) => declaresTailorDBTypeIn(target, namespace, trigger.typeName)
|
|
15292
|
+
}),
|
|
15293
|
+
externalHint: (target) => describeExternal("TailorDB namespace", externalTailorDBNamespaces(target))
|
|
15294
|
+
};
|
|
15295
|
+
case "resolverExecuted": {
|
|
15296
|
+
const namespace = findResolverNamespace(subscriber.application, trigger.resolverName) ?? visibility.resolvers.get(trigger.resolverName);
|
|
15297
|
+
return {
|
|
15298
|
+
resource: publishEventsConflict.resolver(trigger.resolverName).resource,
|
|
15299
|
+
trigger,
|
|
15300
|
+
declaredBy: (target) => declaresResolver(target, trigger.resolverName),
|
|
15301
|
+
pinned: (target) => namespace !== void 0 && pinsResolverIn(target, namespace, trigger.resolverName),
|
|
15302
|
+
keyIn: () => namespace && eventSourceKey.resolver(namespace, trigger.resolverName),
|
|
15303
|
+
narrowOwners: (candidates) => narrowByVisibleNamespace({
|
|
15304
|
+
candidates,
|
|
15305
|
+
visible: visibility.resolvers,
|
|
15306
|
+
resourceKey: trigger.resolverName,
|
|
15307
|
+
declaresIn: (target, namespace) => declaresResolverIn(target, namespace, trigger.resolverName)
|
|
15308
|
+
}),
|
|
15309
|
+
externalHint: (target) => describeExternal("resolver namespace", externalResolverNamespaces(target))
|
|
15310
|
+
};
|
|
15311
|
+
}
|
|
15312
|
+
case "idpUser": {
|
|
15313
|
+
const idpName = subscribedIdpName(subscriber.application, trigger);
|
|
15314
|
+
if (idpName === void 0) return;
|
|
15315
|
+
return {
|
|
15316
|
+
resource: publishEventsConflict.idpService(idpName).resource,
|
|
15317
|
+
trigger,
|
|
15318
|
+
declaredBy: (target) => declaresIdp(target, idpName),
|
|
15319
|
+
pinned: (target) => pinsIdp(target, idpName),
|
|
15320
|
+
keyIn: () => eventSourceKey.idp(idpName),
|
|
15321
|
+
narrowOwners: (candidates) => visibility.idps.has(idpName) ? candidates.filter((target) => declaresIdp(target, idpName)) : [],
|
|
15322
|
+
externalHint: (target) => describeExternal("IdP", externalIdpNames(target).filter((name) => name === idpName))
|
|
15323
|
+
};
|
|
15324
|
+
}
|
|
15325
|
+
case "workflowExecution":
|
|
15326
|
+
case "workflowJobExecution": return {
|
|
15327
|
+
resource: trigger.kind === "workflowExecution" ? eventSourceLabel.workflow(trigger.workflowName) : eventSourceLabel.workflowJobs(trigger.workflowName),
|
|
15328
|
+
trigger,
|
|
15329
|
+
declaredBy: (target) => declaresWorkflow(target, trigger.workflowName),
|
|
15330
|
+
pinned: (target) => trigger.kind === "workflowExecution" ? pinsWorkflow(target, trigger.workflowName) : pinsEveryJobOfWorkflow(target, trigger.workflowName),
|
|
15331
|
+
keyIn: () => trigger.kind === "workflowExecution" ? eventSourceKey.workflow(trigger.workflowName) : eventSourceKey.workflowJobs(trigger.workflowName),
|
|
15332
|
+
narrowOwners: (candidates) => candidates.filter((target) => declaresWorkflow(target, trigger.workflowName)),
|
|
15333
|
+
externalHint: () => void 0
|
|
15334
|
+
};
|
|
15335
|
+
default: return;
|
|
15336
|
+
}
|
|
15337
|
+
}
|
|
14008
15338
|
function collectPlannedExternalTailorDBServices(target, targets) {
|
|
14009
15339
|
const externalNamespaces = new Set(target.application.externalTailorDBNamespaces);
|
|
14010
15340
|
if (externalNamespaces.size === 0) return [];
|
|
@@ -14022,9 +15352,37 @@ function collectExternalAuthIdpConfigNames(targets) {
|
|
|
14022
15352
|
}
|
|
14023
15353
|
return idpConfigNames;
|
|
14024
15354
|
}
|
|
14025
|
-
|
|
15355
|
+
/**
|
|
15356
|
+
* Reject a cross-config subscription whose dependency cannot be recorded.
|
|
15357
|
+
*
|
|
15358
|
+
* Records live on the subscribed resource, so there is always somewhere to put
|
|
15359
|
+
* one. What can be missing is the dependent's name: the record identifies it by
|
|
15360
|
+
* application id, and a subscriber that resolves without one cannot be recorded,
|
|
15361
|
+
* leaving the next deploy of the owner alone to turn publishing off unannounced.
|
|
15362
|
+
*
|
|
15363
|
+
* The check applies only to a run that writes: `--dry-run` leaves every id
|
|
15364
|
+
* uninjected, so demanding one there would reject configs a real deploy gives one.
|
|
15365
|
+
* @param subscriptions - Every event subscription resolved for the run
|
|
15366
|
+
* @param writes - Whether this run applies changes rather than only reporting them
|
|
15367
|
+
*/
|
|
15368
|
+
function assertRecordableDependencies(subscriptions, writes) {
|
|
15369
|
+
for (const { subscriber, owner, executorName, resource, pinned } of subscriptions) {
|
|
15370
|
+
if (subscriber.config.path === owner.config.path) continue;
|
|
15371
|
+
if (pinned) continue;
|
|
15372
|
+
if (!writes) continue;
|
|
15373
|
+
const appId = subscriber.application.id;
|
|
15374
|
+
if (appId !== void 0 && dependedByAppLabelKey(appId) !== void 0) continue;
|
|
15375
|
+
const cause = appId === void 0 ? `${subscriber.config.path} resolves without an "id" — a config that re-exports defineConfig() from another file never gets one` : `${subscriber.config.path} resolves to the id "${appId}", which is not the lowercase UUID deploy writes`;
|
|
15376
|
+
const fix = appId === void 0 ? `Call defineConfig() inline in ${subscriber.config.path} so deploy can manage its "id".` : `Restore the generated value in ${subscriber.config.path}'s "id".`;
|
|
15377
|
+
throw new Error(`Executor "${executorName}" in ${subscriber.config.path} subscribes to ${resource} in ${owner.config.path}, which would enable event publishing on it for this deploy only. ${cause} — so deploy cannot record which config the dependency belongs to, and deploying ${owner.config.path} alone later would turn publishing back off without asking.\n\n${fix}`);
|
|
15378
|
+
}
|
|
15379
|
+
}
|
|
15380
|
+
function collectDeployRunPlanInputs(targets, writes) {
|
|
15381
|
+
const eventSubscriptions = collectEventSubscriptions(targets);
|
|
15382
|
+
assertRecordableDependencies(eventSubscriptions, writes);
|
|
14026
15383
|
return {
|
|
14027
|
-
|
|
15384
|
+
eventSubscriptions,
|
|
15385
|
+
runAppIds: new Set(targets.map((target) => target.application.id).filter((id) => id !== void 0)),
|
|
14028
15386
|
expectedLocalStaticWebsiteNames: collectExpectedLocalStaticWebsiteNames(targets),
|
|
14029
15387
|
externalAuthIdpConfigNames: collectExternalAuthIdpConfigNames(targets)
|
|
14030
15388
|
};
|
|
@@ -14032,6 +15390,7 @@ function collectDeployRunPlanInputs(targets) {
|
|
|
14032
15390
|
async function planDeploymentTarget(params) {
|
|
14033
15391
|
const { target, targets, runInputs, client, workspaceId, noSchemaCheck } = params;
|
|
14034
15392
|
const { config, application, workflowBuildResult, httpAdapterBuildResult, bundledScripts } = target;
|
|
15393
|
+
const owned = ownedSubscriptions(runInputs.eventSubscriptions, target);
|
|
14035
15394
|
await withSpan("plan.validateTailorDBTypeNames", () => assertUniqueTailorDBTypeNamesWithExternal({
|
|
14036
15395
|
client,
|
|
14037
15396
|
workspaceId,
|
|
@@ -14056,8 +15415,10 @@ async function planDeploymentTarget(params) {
|
|
|
14056
15415
|
noSchemaCheck,
|
|
14057
15416
|
forceApplyAll,
|
|
14058
15417
|
...runInputs,
|
|
14059
|
-
|
|
14060
|
-
|
|
15418
|
+
idpUserTriggerTargets: subscribedIdps(owned),
|
|
15419
|
+
executorUsedTailorDBTypes: subscribedTailorDBTypes(owned),
|
|
15420
|
+
executorUsedResolvers: subscribedResolvers(owned),
|
|
15421
|
+
dependentApps: collectDependentApps(owned),
|
|
14061
15422
|
tailorDBTypeNamespaces,
|
|
14062
15423
|
resolverNamespaces,
|
|
14063
15424
|
idpNames
|
|
@@ -14073,7 +15434,12 @@ async function planDeploymentTarget(params) {
|
|
|
14073
15434
|
withSpan("plan.pipeline", () => planPipeline(ctx)),
|
|
14074
15435
|
withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
|
|
14075
15436
|
withSpan("plan.executor", () => planExecutor(ctx)),
|
|
14076
|
-
withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs
|
|
15437
|
+
withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs, {
|
|
15438
|
+
...subscribedWorkflows(owned),
|
|
15439
|
+
jobPublishEvents: collectWorkflowJobPublishEvents(target),
|
|
15440
|
+
dependentApps: ctx.dependentApps,
|
|
15441
|
+
runAppIds: ctx.runAppIds
|
|
15442
|
+
})),
|
|
14077
15443
|
withSpan("plan.workflowExecutionPolicy", () => planWorkflowJobFunctionExecutionPolicy(client, workspaceId, application.name, application.id, config.workflow?.executionPolicies ?? {})),
|
|
14078
15444
|
withSpan("plan.secretManager", () => planSecretManager(ctx))
|
|
14079
15445
|
]);
|
|
@@ -14107,15 +15473,14 @@ function deploymentPlanResults(deployment) {
|
|
|
14107
15473
|
return results;
|
|
14108
15474
|
}
|
|
14109
15475
|
/**
|
|
14110
|
-
* Every `PlanResults` entry
|
|
14111
|
-
*
|
|
14112
|
-
*
|
|
15476
|
+
* Every `PlanResults` entry carries ownership-tracking fields; deriving the
|
|
15477
|
+
* list from `results` itself (instead of naming each key) keeps these
|
|
15478
|
+
* collectors in sync with `PlannedDeployment` as resource types are added.
|
|
14113
15479
|
* @param results - Plan results for a single deployment
|
|
14114
|
-
* @returns The ownership-tracking plan entries
|
|
15480
|
+
* @returns The ownership-tracking plan entries
|
|
14115
15481
|
*/
|
|
14116
15482
|
function ownershipTrackedPlans(results) {
|
|
14117
|
-
|
|
14118
|
-
return Object.values(rest);
|
|
15483
|
+
return Object.values(results);
|
|
14119
15484
|
}
|
|
14120
15485
|
function collectOwnerConflicts(results) {
|
|
14121
15486
|
return ownershipTrackedPlans(results).flatMap((plan) => plan.conflicts);
|
|
@@ -14454,7 +15819,8 @@ function collectDeploymentResourceOwners(deployments) {
|
|
|
14454
15819
|
return owners;
|
|
14455
15820
|
}
|
|
14456
15821
|
async function confirmDeploymentPlans(params) {
|
|
14457
|
-
const { deployments, yes } = params;
|
|
15822
|
+
const { deployments, yes, missingDependentApps = [] } = params;
|
|
15823
|
+
await confirmMissingDependentApps(missingDependentApps, yes);
|
|
14458
15824
|
const targetAppNames = new Set(deployments.map((deployment) => deployment.application.name));
|
|
14459
15825
|
const resourceOwners = collectDeploymentResourceOwners(deployments);
|
|
14460
15826
|
const scheduledRenamedAppDeletes = /* @__PURE__ */ new Set();
|
|
@@ -14462,7 +15828,7 @@ async function confirmDeploymentPlans(params) {
|
|
|
14462
15828
|
for (const deployment of deployments) {
|
|
14463
15829
|
const results = deploymentPlanResults(deployment);
|
|
14464
15830
|
const conflicts = collectOwnerConflicts(results);
|
|
14465
|
-
await confirmOwnerConflict(conflicts, deployment.application.name, yes);
|
|
15831
|
+
await confirmOwnerConflict(conflicts, deployment.application.name, yes, deployment.application.id);
|
|
14466
15832
|
await confirmUnmanagedResources(collectUnmanagedResources(results), deployment.application.name, yes);
|
|
14467
15833
|
importantDeletions.push(...collectImportantResourceDeletions(results));
|
|
14468
15834
|
const emptyApps = computeRenamedAppDeletions({
|
|
@@ -14577,19 +15943,29 @@ async function deployInternal(options, cliContext) {
|
|
|
14577
15943
|
const { client, workspaceId } = workspace;
|
|
14578
15944
|
rootSpan.setAttribute("app.name", targets.map((target) => target.application.name).join(","));
|
|
14579
15945
|
rootSpan.setAttribute("workspace.id", workspaceId);
|
|
15946
|
+
const runInputs = collectDeployRunPlanInputs(targets, !options?.dryRun);
|
|
14580
15947
|
const deployments = await planDeploymentTargets({
|
|
14581
15948
|
targets,
|
|
14582
|
-
runInputs
|
|
15949
|
+
runInputs,
|
|
14583
15950
|
client,
|
|
14584
15951
|
workspaceId,
|
|
14585
15952
|
noSchemaCheck: options?.noSchemaCheck
|
|
14586
15953
|
});
|
|
14587
15954
|
const yes = options?.yes ?? false;
|
|
14588
15955
|
dropCrossDeploymentManagedDeletes(deployments);
|
|
15956
|
+
const missingDependentApps = (await Promise.all(targets.map((target) => fetchMissingDependentApps({
|
|
15957
|
+
client,
|
|
15958
|
+
workspaceId,
|
|
15959
|
+
application: target.application,
|
|
15960
|
+
runAppIds: runInputs.runAppIds ?? /* @__PURE__ */ new Set(),
|
|
15961
|
+
subscribedKeys: subscribedResourceKeys(runInputs.eventSubscriptions, target),
|
|
15962
|
+
jobsByWorkflow: target.workflowBuildResult?.mainJobDeps ?? {}
|
|
15963
|
+
})))).flat();
|
|
14589
15964
|
await withSpan("confirm", async () => {
|
|
14590
15965
|
await confirmDeploymentPlans({
|
|
14591
15966
|
deployments,
|
|
14592
|
-
yes
|
|
15967
|
+
yes,
|
|
15968
|
+
missingDependentApps
|
|
14593
15969
|
});
|
|
14594
15970
|
});
|
|
14595
15971
|
const planSummary = printDeploymentPlans(deployments, { dryRun: options?.dryRun });
|
|
@@ -14888,6 +16264,12 @@ function formatEventTriggerConfig(config) {
|
|
|
14888
16264
|
eventType: config.eventType,
|
|
14889
16265
|
condition: config.condition?.expr || ""
|
|
14890
16266
|
};
|
|
16267
|
+
if (typedConfig.case === "workflow") return {
|
|
16268
|
+
kind: typedConfig.case,
|
|
16269
|
+
eventTypes: typedConfig.value.eventTypes,
|
|
16270
|
+
condition: typedConfig.value.condition?.expr || "",
|
|
16271
|
+
...typedConfig.value.workflowName && { workflowName: typedConfig.value.workflowName }
|
|
16272
|
+
};
|
|
14891
16273
|
const base = {
|
|
14892
16274
|
kind: typedConfig.case,
|
|
14893
16275
|
eventTypes: typedConfig.value.eventTypes,
|
|
@@ -14979,8 +16361,13 @@ async function resolveExecutor(client, workspaceId, name) {
|
|
|
14979
16361
|
if (!executor) throw new Error(`Executor '${name}' not found.`);
|
|
14980
16362
|
return executor;
|
|
14981
16363
|
}
|
|
16364
|
+
/**
|
|
16365
|
+
* Get an executor by name and return CLI-friendly info.
|
|
16366
|
+
* @param options - Executor lookup options
|
|
16367
|
+
* @returns Executor information
|
|
16368
|
+
*/
|
|
14982
16369
|
async function getExecutor(options) {
|
|
14983
|
-
const name =
|
|
16370
|
+
const name = options.executor.name;
|
|
14984
16371
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
14985
16372
|
const workspaceId = await loadWorkspaceId({
|
|
14986
16373
|
workspaceId: options.workspaceId,
|
|
@@ -15002,7 +16389,7 @@ const getCommand$6 = defineAppCommand({
|
|
|
15002
16389
|
}),
|
|
15003
16390
|
run: async (args) => {
|
|
15004
16391
|
const executor = await getExecutor({
|
|
15005
|
-
name: args.name,
|
|
16392
|
+
executor: { name: args.name },
|
|
15006
16393
|
workspaceId: args["workspace-id"],
|
|
15007
16394
|
profile: args.profile
|
|
15008
16395
|
});
|
|
@@ -15486,8 +16873,18 @@ function parseStatus(status) {
|
|
|
15486
16873
|
default: throw new Error(`Invalid status: ${status}. Valid values: UNSPECIFIED, PENDING, PENDING_RESUME, RUNNING, SUCCESS, FAILED, PENDING_RETRY, WAITING`);
|
|
15487
16874
|
}
|
|
15488
16875
|
}
|
|
16876
|
+
/**
|
|
16877
|
+
* List workflow executions with optional filters.
|
|
16878
|
+
*
|
|
16879
|
+
* Returns at most `options.limit` items. When `limit` is omitted or 0 the
|
|
16880
|
+
* function pages through every execution. The CLI caps this at 50 by
|
|
16881
|
+
* default via `pagedLogArgs`; programmatic callers that want the same
|
|
16882
|
+
* cap should pass `limit: 50` explicitly.
|
|
16883
|
+
* @param options - Workflow execution listing options
|
|
16884
|
+
* @returns List of workflow executions
|
|
16885
|
+
*/
|
|
15489
16886
|
async function listWorkflowExecutions(options) {
|
|
15490
|
-
const workflowName = options
|
|
16887
|
+
const workflowName = options?.workflow?.name;
|
|
15491
16888
|
const client = await initOperatorClient(await loadAccessToken({ profile: options?.profile }));
|
|
15492
16889
|
const workspaceId = await loadWorkspaceId({
|
|
15493
16890
|
workspaceId: options?.workspaceId,
|
|
@@ -15708,7 +17105,7 @@ const executionsCommand = defineAppCommand({
|
|
|
15708
17105
|
const executions = await listWorkflowExecutions({
|
|
15709
17106
|
workspaceId: args["workspace-id"],
|
|
15710
17107
|
profile: args.profile,
|
|
15711
|
-
|
|
17108
|
+
workflow: args["workflow-name"] === void 0 ? void 0 : { name: args["workflow-name"] },
|
|
15712
17109
|
status: args.status,
|
|
15713
17110
|
order: args.order,
|
|
15714
17111
|
limit: args.limit
|
|
@@ -15735,8 +17132,13 @@ async function resolveWorkflow(client, workspaceId, name) {
|
|
|
15735
17132
|
if (!workflow) throw new Error(`Workflow '${name}' not found.`);
|
|
15736
17133
|
return workflow;
|
|
15737
17134
|
}
|
|
17135
|
+
/**
|
|
17136
|
+
* Get a workflow by name and return CLI-friendly info.
|
|
17137
|
+
* @param options - Workflow lookup options
|
|
17138
|
+
* @returns Workflow information
|
|
17139
|
+
*/
|
|
15738
17140
|
async function getWorkflow(options) {
|
|
15739
|
-
const name =
|
|
17141
|
+
const name = options.workflow.name;
|
|
15740
17142
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
15741
17143
|
const workspaceId = await loadWorkspaceId({
|
|
15742
17144
|
workspaceId: options.workspaceId,
|
|
@@ -15758,7 +17160,7 @@ const getCommand$5 = defineAppCommand({
|
|
|
15758
17160
|
}),
|
|
15759
17161
|
run: async (args) => {
|
|
15760
17162
|
const workflow = await getWorkflow({
|
|
15761
|
-
name: args.name,
|
|
17163
|
+
workflow: { name: args.name },
|
|
15762
17164
|
workspaceId: args["workspace-id"],
|
|
15763
17165
|
profile: args.profile
|
|
15764
17166
|
});
|
|
@@ -15808,6 +17210,13 @@ async function resolveApplicationAuthNamespace(options) {
|
|
|
15808
17210
|
if (!authNamespace) throw new Error(`Application ${config.name} does not have an auth configuration.`);
|
|
15809
17211
|
return authNamespace;
|
|
15810
17212
|
}
|
|
17213
|
+
/**
|
|
17214
|
+
* Start a workflow looked up by name, resolving the machine user from the
|
|
17215
|
+
* flag, environment, or profile default. Backs the `workflow start` command;
|
|
17216
|
+
* programmatic callers use {@link startWorkflow} with a workflow definition.
|
|
17217
|
+
* @param options - Start options keyed by workflow name
|
|
17218
|
+
* @returns Start result with wait helper
|
|
17219
|
+
*/
|
|
15811
17220
|
async function startWorkflowByName(options) {
|
|
15812
17221
|
const machineUser = await loadMachineUserName({
|
|
15813
17222
|
machineUser: options.machineUser,
|
|
@@ -15837,8 +17246,12 @@ async function startWorkflowByName(options) {
|
|
|
15837
17246
|
interval: options.interval
|
|
15838
17247
|
});
|
|
15839
17248
|
}
|
|
17249
|
+
/**
|
|
17250
|
+
* Start a workflow and return a handle to wait for completion.
|
|
17251
|
+
* @param options - Start options
|
|
17252
|
+
* @returns Start result with wait helper
|
|
17253
|
+
*/
|
|
15840
17254
|
async function startWorkflow(options) {
|
|
15841
|
-
if ("name" in options) return await startWorkflowByName(options);
|
|
15842
17255
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
15843
17256
|
const workspaceId = await loadWorkspaceId({
|
|
15844
17257
|
workspaceId: options.workspaceId,
|
|
@@ -15938,8 +17351,18 @@ function createUnknownExecutorJob(executorName, jobId) {
|
|
|
15938
17351
|
updatedAt: "N/A"
|
|
15939
17352
|
};
|
|
15940
17353
|
}
|
|
17354
|
+
/**
|
|
17355
|
+
* List executor jobs for a given executor.
|
|
17356
|
+
*
|
|
17357
|
+
* Returns at most `options.limit` items. When `limit` is omitted or 0 the
|
|
17358
|
+
* function pages through every job. The CLI caps this at 50 by default
|
|
17359
|
+
* via `pagedLogArgs`; programmatic callers that want the same cap should
|
|
17360
|
+
* pass `limit: 50` explicitly.
|
|
17361
|
+
* @param options - Options for listing executor jobs
|
|
17362
|
+
* @returns List of executor job information
|
|
17363
|
+
*/
|
|
15941
17364
|
async function listExecutorJobs(options) {
|
|
15942
|
-
const executorName =
|
|
17365
|
+
const executorName = options.executor.name;
|
|
15943
17366
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
15944
17367
|
const workspaceId = await loadWorkspaceId({
|
|
15945
17368
|
workspaceId: options.workspaceId,
|
|
@@ -15976,8 +17399,13 @@ async function listExecutorJobs(options) {
|
|
|
15976
17399
|
throw error;
|
|
15977
17400
|
}
|
|
15978
17401
|
}
|
|
17402
|
+
/**
|
|
17403
|
+
* Get details of a specific executor job.
|
|
17404
|
+
* @param options - Options for getting executor job details
|
|
17405
|
+
* @returns Executor job detail information
|
|
17406
|
+
*/
|
|
15979
17407
|
async function getExecutorJob(options) {
|
|
15980
|
-
const executorName =
|
|
17408
|
+
const executorName = options.executor.name;
|
|
15981
17409
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
15982
17410
|
const workspaceId = await loadWorkspaceId({
|
|
15983
17411
|
workspaceId: options.workspaceId,
|
|
@@ -16013,8 +17441,13 @@ async function getExecutorJob(options) {
|
|
|
16013
17441
|
throw error;
|
|
16014
17442
|
}
|
|
16015
17443
|
}
|
|
17444
|
+
/**
|
|
17445
|
+
* Watch an executor job until completion, including downstream executions.
|
|
17446
|
+
* @param options - Options for watching executor job
|
|
17447
|
+
* @returns Result including job details and downstream execution info
|
|
17448
|
+
*/
|
|
16016
17449
|
async function watchExecutorJob(options) {
|
|
16017
|
-
const executorName =
|
|
17450
|
+
const executorName = options.executor.name;
|
|
16018
17451
|
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
16019
17452
|
const workspaceId = await loadWorkspaceId({
|
|
16020
17453
|
workspaceId: options.workspaceId,
|
|
@@ -16333,7 +17766,7 @@ const jobsCommand = defineAppCommand({
|
|
|
16333
17766
|
if (args.jobId) {
|
|
16334
17767
|
if (args.wait) {
|
|
16335
17768
|
const result = await watchExecutorJob({
|
|
16336
|
-
|
|
17769
|
+
executor: { name: args.executorName },
|
|
16337
17770
|
jobId: args.jobId,
|
|
16338
17771
|
workspaceId: args["workspace-id"],
|
|
16339
17772
|
profile: args.profile,
|
|
@@ -16382,7 +17815,7 @@ const jobsCommand = defineAppCommand({
|
|
|
16382
17815
|
return;
|
|
16383
17816
|
}
|
|
16384
17817
|
const job = await getExecutorJob({
|
|
16385
|
-
|
|
17818
|
+
executor: { name: args.executorName },
|
|
16386
17819
|
jobId: args.jobId,
|
|
16387
17820
|
attempts: args.attempts,
|
|
16388
17821
|
workspaceId: args["workspace-id"],
|
|
@@ -16393,7 +17826,7 @@ const jobsCommand = defineAppCommand({
|
|
|
16393
17826
|
} else {
|
|
16394
17827
|
if (args.wait) logger.warn("--wait flag is ignored in list mode. Specify a job ID to wait.");
|
|
16395
17828
|
const jobs = await listExecutorJobs({
|
|
16396
|
-
|
|
17829
|
+
executor: { name: args.executorName },
|
|
16397
17830
|
status: args.status,
|
|
16398
17831
|
order: args.order,
|
|
16399
17832
|
limit: args.limit,
|
|
@@ -16503,8 +17936,12 @@ async function triggerExecutorByName(options) {
|
|
|
16503
17936
|
throw error;
|
|
16504
17937
|
}
|
|
16505
17938
|
}
|
|
17939
|
+
/**
|
|
17940
|
+
* Trigger an executor and return the job ID.
|
|
17941
|
+
* @param options - Options for triggering executor
|
|
17942
|
+
* @returns Result containing the job ID if available
|
|
17943
|
+
*/
|
|
16506
17944
|
async function triggerExecutor(options) {
|
|
16507
|
-
if ("executorName" in options) return await triggerExecutorByName(options);
|
|
16508
17945
|
if (options.executor.trigger.kind !== "incomingWebhook" && options.payload !== void 0) throw new Error(`Executor '${options.executor.name}' has '${options.executor.trigger.kind}' trigger type. The payload is only available for 'incomingWebhook' trigger type.`);
|
|
16509
17946
|
return await triggerExecutorByName({
|
|
16510
17947
|
executorName: options.executor.name,
|
|
@@ -16621,7 +18058,7 @@ The \`--logs\` option displays logs from the downstream execution when available
|
|
|
16621
18058
|
logger.success(`Executor '${args.executorName}' triggered successfully. Job ID: ${result.jobId}`);
|
|
16622
18059
|
if (args.wait) {
|
|
16623
18060
|
const watchResult = await watchExecutorJob({
|
|
16624
|
-
|
|
18061
|
+
executor: { name: args.executorName },
|
|
16625
18062
|
jobId: result.jobId,
|
|
16626
18063
|
workspaceId: args["workspace-id"],
|
|
16627
18064
|
profile: args.profile,
|
|
@@ -17958,10 +19395,12 @@ async function loadOptions$10(options) {
|
|
|
17958
19395
|
profile: options?.profile
|
|
17959
19396
|
});
|
|
17960
19397
|
const { config } = await loadConfig(options?.configPath);
|
|
19398
|
+
const application = defineApplication({ config });
|
|
19399
|
+
warnMissingAppId(application.id);
|
|
17961
19400
|
return {
|
|
17962
19401
|
client,
|
|
17963
19402
|
workspaceId,
|
|
17964
|
-
application
|
|
19403
|
+
application,
|
|
17965
19404
|
config
|
|
17966
19405
|
};
|
|
17967
19406
|
}
|
|
@@ -17987,6 +19426,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
|
|
|
17987
19426
|
functionRegistry: await planFunctionRegistry(client, workspaceId, application.name, application.id, []),
|
|
17988
19427
|
secretManager: await planSecretManager(ctx)
|
|
17989
19428
|
};
|
|
19429
|
+
const leftBehind = Object.values(plans).some((plan) => "resourceOwners" in plan && plan.resourceOwners.has(application.name));
|
|
17990
19430
|
const removeLines = [
|
|
17991
19431
|
...plans.functionRegistry.changeSet.lines(),
|
|
17992
19432
|
...plans.staticWebsite.changeSet.lines(),
|
|
@@ -18016,7 +19456,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
|
|
|
18016
19456
|
...plans.secretManager.secretChangeSet.lines()
|
|
18017
19457
|
];
|
|
18018
19458
|
if (removeLines.length > 0) logger.log(removeLines.join("\n"));
|
|
18019
|
-
if (plans.tailorDB.changeSet.service.deletes.length === 0 && plans.staticWebsite.changeSet.deletes.length === 0 && plans.aiGateway.changeSet.deletes.length === 0 && plans.idp.changeSet.service.deletes.length === 0 && plans.auth.changeSet.service.deletes.length === 0 && plans.pipeline.changeSet.service.deletes.length === 0 && plans.app.deletes.length === 0 && plans.executor.changeSet.deletes.length === 0 && plans.workflow.changeSet.deletes.length === 0 && plans.workflowExecutionPolicy.changeSet.deletes.length === 0 && plans.functionRegistry.changeSet.deletes.length === 0 && plans.secretManager.vaultChangeSet.deletes.length === 0 && plans.secretManager.secretChangeSet.deletes.length === 0) return;
|
|
19459
|
+
if (plans.tailorDB.changeSet.service.deletes.length === 0 && plans.staticWebsite.changeSet.deletes.length === 0 && plans.aiGateway.changeSet.deletes.length === 0 && plans.idp.changeSet.service.deletes.length === 0 && plans.auth.changeSet.service.deletes.length === 0 && plans.pipeline.changeSet.service.deletes.length === 0 && plans.app.deletes.length === 0 && plans.executor.changeSet.deletes.length === 0 && plans.workflow.changeSet.deletes.length === 0 && plans.workflowExecutionPolicy.changeSet.deletes.length === 0 && plans.functionRegistry.changeSet.deletes.length === 0 && plans.secretManager.vaultChangeSet.deletes.length === 0 && plans.secretManager.secretChangeSet.deletes.length === 0) return { leftBehind };
|
|
18020
19460
|
if (confirm) await confirm();
|
|
18021
19461
|
await applyWorkflow(client, plans.workflow, "delete");
|
|
18022
19462
|
await applyWorkflowJobFunctionExecutionPolicy(client, plans.workflowExecutionPolicy, "delete");
|
|
@@ -18034,6 +19474,7 @@ async function execRemove(client, workspaceId, application, config, confirm) {
|
|
|
18034
19474
|
await applyTailorDB(client, plans.tailorDB, "delete-services");
|
|
18035
19475
|
await applyFunctionRegistry(client, workspaceId, plans.functionRegistry, "delete");
|
|
18036
19476
|
await applySecretManager(client, plans.secretManager, "delete");
|
|
19477
|
+
return { leftBehind };
|
|
18037
19478
|
}
|
|
18038
19479
|
/**
|
|
18039
19480
|
* Remove all resources managed by the current application.
|
|
@@ -18060,7 +19501,7 @@ const removeCommand$1 = defineAppCommand({
|
|
|
18060
19501
|
});
|
|
18061
19502
|
logger.info(`Planning removal of resources managed by "${application.name}"...`);
|
|
18062
19503
|
logger.newline();
|
|
18063
|
-
await execRemove(client, workspaceId, application, config, async () => {
|
|
19504
|
+
const { leftBehind } = await execRemove(client, workspaceId, application, config, async () => {
|
|
18064
19505
|
if (!args.yes) {
|
|
18065
19506
|
if (!await prompt.confirm({
|
|
18066
19507
|
message: "Are you sure you want to remove all resources?",
|
|
@@ -18071,6 +19512,13 @@ const removeCommand$1 = defineAppCommand({
|
|
|
18071
19512
|
`);
|
|
18072
19513
|
} else logger.success("Removing all resources (--yes flag specified)...");
|
|
18073
19514
|
});
|
|
19515
|
+
if (leftBehind) {
|
|
19516
|
+
logger.warn(multiline`
|
|
19517
|
+
Resources tagged with "${application.name}" were left in place: they carry an application id this config does not match.
|
|
19518
|
+
Put that id in your config, or run deploy to take them over first, then remove again.
|
|
19519
|
+
`);
|
|
19520
|
+
return;
|
|
19521
|
+
}
|
|
18074
19522
|
logger.success(`Successfully removed all resources managed by "${application.name}".`);
|
|
18075
19523
|
}
|
|
18076
19524
|
});
|
|
@@ -19094,7 +20542,7 @@ async function generate(options) {
|
|
|
19094
20542
|
if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
|
|
19095
20543
|
let pluginManager;
|
|
19096
20544
|
if (plugins.length > 0) pluginManager = new PluginManager(plugins);
|
|
19097
|
-
const { defineApplication } = await import("./application-
|
|
20545
|
+
const { defineApplication } = await import("./application-CXNaUNhv.mjs");
|
|
19098
20546
|
const application = defineApplication({
|
|
19099
20547
|
config,
|
|
19100
20548
|
pluginManager
|
|
@@ -20308,8 +21756,13 @@ async function bundleQueryScript(engine, baseDir) {
|
|
|
20308
21756
|
const entryContent = engine === "sql" ? createSqlEntry() : createGqlEntry();
|
|
20309
21757
|
fs$2.writeFileSync(entryPath, entryContent);
|
|
20310
21758
|
const tsconfig = await resolveTSConfigWithFallback(baseDir);
|
|
20311
|
-
|
|
20312
|
-
|
|
21759
|
+
const bundleLog = createBundleLog({ tsconfig });
|
|
21760
|
+
const result = await rolldown.build({
|
|
21761
|
+
plugins: [
|
|
21762
|
+
createGeneratedEntryResolverPlugin(entryPath, baseDir),
|
|
21763
|
+
createTsconfigPathsPlugin(),
|
|
21764
|
+
platformBundleDefinePlugin
|
|
21765
|
+
],
|
|
20313
21766
|
input: entryPath,
|
|
20314
21767
|
write: false,
|
|
20315
21768
|
output: {
|
|
@@ -20327,8 +21780,10 @@ async function bundleQueryScript(engine, baseDir) {
|
|
|
20327
21780
|
annotations: true,
|
|
20328
21781
|
unknownGlobalSideEffects: false
|
|
20329
21782
|
},
|
|
20330
|
-
|
|
20331
|
-
})
|
|
21783
|
+
...bundleLog.options
|
|
21784
|
+
});
|
|
21785
|
+
bundleLog.assertAllResolved();
|
|
21786
|
+
return result.output[0].code;
|
|
20332
21787
|
}
|
|
20333
21788
|
|
|
20334
21789
|
//#endregion
|
|
@@ -21246,5 +22701,5 @@ async function registerTsHook(tsHookUrl) {
|
|
|
21246
22701
|
}
|
|
21247
22702
|
|
|
21248
22703
|
//#endregion
|
|
21249
|
-
export { getOrganization as $,
|
|
21250
|
-
//# sourceMappingURL=register-ts-hook-
|
|
22704
|
+
export { getOrganization as $, hasUserTokenEntry as $n, ensureConfigId as $t, truncate as A, loadDiff as An, getExecutorJob as At, show as B, sdkAppIdLabelKey as Bn, getWorkflowExecution as Bt, healthCommand as C, createSnapshotFromLocalTypes as Cn, resolveMachineUserInputSource as Cr, getFunctionRegistry as Ct, resumeWorkflow as D, getMigrationFilePath as Dn, triggerExecutor as Dt, resumeCommand as E, getMigrationDirPath as En, triggerCommand as Et, writeDbTypesFile as F, formatMigrationDiff as Fn, startCommand as Ft, removeCommand$1 as G, generateUserTypes as Gn, deploy as Gt, extractOwnedNamespaces as H, sdkVersionLabelKey as Hn, functionExecutionStatusToString as Ht, getConfiguredEditorCommand as I, hasChanges as In, startWorkflow as It, organizationTree as J, assertWritable as Jn, listWorkspaces as Jt, updateCommand$1 as K, prompt as Kn, deployFromCLI as Kt, openInConfiguredEditor as L, getNamespacesWithMigrations as Ln, getCommand$5 as Lt, generate as M, formatMigrationNumber as Mn, jobsCommand as Mt, generateCommand as N, parseMigrationNumberArg as Nn, listExecutorJobs as Nt, listCommand$2 as O, getMigrationFiles as On, listCommand$8 as Ot, generateMigrationScript as P, formatDiffSummary as Pn, watchExecutorJob as Pt, getCommand$1 as Q, hasAnyUserTokenEntry as Qn, workspaceNameSchema as Qt, deployCommand as R, selectTargetNamespace as Rn, getWorkflow as Rt, getAppHealth as S, compareSnapshots as Sn, paginationArgs as Sr, getCommand$4 as St, waitWorkflowExecution as T, getLatestMigrationNumber as Tn, workspaceArgs as Tr, webhookCommand as Tt, logBetaWarning as U, writeMetadataLabels as Un, getCommand$6 as Ut, showCommand as V, sdkNameLabelKey as Vn, listWorkflowExecutions as Vt, remove as W, PluginManager as Wn, getExecutor as Wt, listCommand$3 as X, deleteUserTokens as Xn, createWorkspace$1 as Xt, treeCommand as Y, loadConfig as Yn, createCommand$1 as Yt, listOrganizations as Z, fetchLatestToken as Zn, formatKeyValueTable as Zt, getWorkspace as _, MIGRATE_FILE_NAME as _n, confirmationArgs as _r, listMachineUsers as _t, queryCommand as a, toTailorDBDeployInput as an, loadStoredUserTokens as ar, getFolder as at, listApps as b, assertValidMigrationFiles as bn, multiConfigArg as br, listCommand$7 as bt, removeCommand as c, handleOptionalToRequiredError as cn, readPlatformConfig as cr, createCommand as ct, listUsers as d, compareSnapshotWithRemote as dn, resolveUserTokenKey as dr, listOAuth2Clients as dt, executeScript as en, loadAccessToken as er, updateCommand$2 as et, inviteCommand as f, generateAllTypeManifestsFromSnapshot as fn, saveUserTokens as fr, getCommand$3 as ft, getCommand as g, INITIAL_SCHEMA_NUMBER as gn, configArg as gr, listCommand$6 as gt, restoreWorkspace as h, DIFF_FILE_NAME as hn, commonArgs as hr, tokenCommand as ht, query as i, logRemoteDriftGuidance as in, loadPlatformClientConfig as ir, getCommand$2 as it, truncateCommand as j, reconstructSnapshotFromMigrations as jn, getExecutorWaitFailureMessage as jt, listWorkflows as k, getNextMigrationNumber as kn, listExecutors as kt, removeUser as l, parseMigrationLabelNumber as ln, removeLegacyUserAlias as lr, createFolder as lt, restoreCommand as m, DB_TYPES_FILE_NAME as mn, defineAppCommand as mr, getMachineUserToken as mt, errorToJson as n, bundleMigrationScript as nn, loadConsoleBaseUrl as nr, listCommand$4 as nt, updateCommand as o, verifyRemoteSchema as on, loadWorkspaceId as or, deleteCommand$1 as ot, inviteUser as p, protoGqlPermission as pn, writePlatformConfig as pr, getOAuth2Client as pt, updateOrganization as q, apiCall as qn, listCommand$9 as qt, serializeError as r, checkMigrationDiffs as rn, loadMachineUserName as rr, listFolders as rt, updateUser as s, MIGRATION_LABEL_KEY as sn, platformConfigFromProfile as sr, deleteFolder as st, registerTsHook as t, waitForExecution as tn, loadConfigPath as tr, updateFolder as tt, listCommand as u, sanitizeMigrationLabel as un, resolveConfigUser as ur, listCommand$5 as ut, deleteCommand as v, SCHEMA_FILE_NAME as vn, deploymentArgs as vr, generate$1 as vt, waitCommand as w, formatSchemaDrifts as wn, toPageDirection as wr, listWebhookExecutors as wt, listCommand$1 as x, compareLocalTypesWithSnapshot as xn, pagedLogArgs as xr, listFunctionRegistries as xt, deleteWorkspace as y, assertMigrationNumberExists as yn, isVerbose as yr, getAuthInput as yt, deployStaticWebsite as z, resourceTrn as zn, executionsCommand as zt };
|
|
22705
|
+
//# sourceMappingURL=register-ts-hook-LYV7zH-e.mjs.map
|