@dragonmastery/tamer 0.1.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +569 -18
- package/dist/CFApiClient-DhbyyV71.mjs +868 -0
- package/dist/CFApiClient-DhbyyV71.mjs.map +1 -0
- package/dist/StateManager-DTqtLLVX.mjs +760 -0
- package/dist/StateManager-DTqtLLVX.mjs.map +1 -0
- package/dist/apply-B0b_jjGv.mjs +423 -0
- package/dist/apply-B0b_jjGv.mjs.map +1 -0
- package/dist/applyTarget-BetDYdeS.mjs +152 -0
- package/dist/applyTarget-BetDYdeS.mjs.map +1 -0
- package/dist/bootstrap-CBzPilB1.mjs +33 -0
- package/dist/bootstrap-CBzPilB1.mjs.map +1 -0
- package/dist/buildDispatchUploadForm-BoUB93b3.mjs +38 -0
- package/dist/buildDispatchUploadForm-BoUB93b3.mjs.map +1 -0
- package/dist/cloudflareSnapshot-B4FOaNr0.mjs +163 -0
- package/dist/cloudflareSnapshot-B4FOaNr0.mjs.map +1 -0
- package/dist/deploy-gHEQxhmx.mjs +119 -0
- package/dist/deploy-gHEQxhmx.mjs.map +1 -0
- package/dist/destroy-B21f3wgq.mjs +215 -0
- package/dist/destroy-B21f3wgq.mjs.map +1 -0
- package/dist/destroy-tenant-BW2nasnK.mjs +103 -0
- package/dist/destroy-tenant-BW2nasnK.mjs.map +1 -0
- package/dist/dev-Dt26nzMJ.mjs +103 -0
- package/dist/dev-Dt26nzMJ.mjs.map +1 -0
- package/dist/dns-records.resolve-C2T0m4NG.mjs +3 -0
- package/dist/dns-records.resolve-DwBR_1WI.mjs +47 -0
- package/dist/dns-records.resolve-DwBR_1WI.mjs.map +1 -0
- package/dist/dns-records.sync-Bpzz9H0s.mjs +75 -0
- package/dist/dns-records.sync-Bpzz9H0s.mjs.map +1 -0
- package/dist/doctor-C_hs7k2D.mjs +34 -0
- package/dist/doctor-C_hs7k2D.mjs.map +1 -0
- package/dist/drift-D5qzCTft.mjs +10 -0
- package/dist/drift-D8ZrSgTn.mjs +323 -0
- package/dist/drift-D8ZrSgTn.mjs.map +1 -0
- package/dist/events-BSwGdkGj.mjs +68 -0
- package/dist/events-BSwGdkGj.mjs.map +1 -0
- package/dist/fetchStackImports-B4ZJahOt.mjs +3817 -0
- package/dist/fetchStackImports-B4ZJahOt.mjs.map +1 -0
- package/dist/generator-CIMbcPzv.mjs +77 -0
- package/dist/generator-CIMbcPzv.mjs.map +1 -0
- package/dist/import-BrduwA9Z.mjs +164 -0
- package/dist/import-BrduwA9Z.mjs.map +1 -0
- package/dist/index.d.mts +5568 -1297
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +18 -1
- package/dist/index.mjs.map +1 -0
- package/dist/loader-DP7yXqT6.mjs +518 -0
- package/dist/loader-DP7yXqT6.mjs.map +1 -0
- package/dist/logpush-job-xS7270FZ.mjs +1106 -0
- package/dist/logpush-job-xS7270FZ.mjs.map +1 -0
- package/dist/migrate-CahG6BYV.mjs +87 -0
- package/dist/migrate-CahG6BYV.mjs.map +1 -0
- package/dist/normalize-Bx0bpFop.mjs +236 -0
- package/dist/normalize-Bx0bpFop.mjs.map +1 -0
- package/dist/plan-DWvsvy1U.mjs +453 -0
- package/dist/plan-DWvsvy1U.mjs.map +1 -0
- package/dist/planFormat-CJw8Kq2s.mjs +119 -0
- package/dist/planFormat-CJw8Kq2s.mjs.map +1 -0
- package/dist/provision-tenant-WTKo93Y0.mjs +192 -0
- package/dist/provision-tenant-WTKo93Y0.mjs.map +1 -0
- package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs +92 -0
- package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs.map +1 -0
- package/dist/stackOutputs-W9mnnJuj.mjs +69 -0
- package/dist/stackOutputs-W9mnnJuj.mjs.map +1 -0
- package/dist/status-DLwREPjb.mjs +198 -0
- package/dist/status-DLwREPjb.mjs.map +1 -0
- package/dist/sync-f2K2blwm.mjs +90 -0
- package/dist/sync-f2K2blwm.mjs.map +1 -0
- package/dist/tamer.d.mts +1 -0
- package/dist/tamer.mjs +4553 -0
- package/dist/tamer.mjs.map +1 -0
- package/dist/tamerArtifactsR2-Ccgplu2Q.mjs +52 -0
- package/dist/tamerArtifactsR2-Ccgplu2Q.mjs.map +1 -0
- package/dist/types-CqxqYnrT.mjs +44 -0
- package/dist/types-CqxqYnrT.mjs.map +1 -0
- package/dist/verifyPlanFile-c16z1AMH.mjs +33 -0
- package/dist/verifyPlanFile-c16z1AMH.mjs.map +1 -0
- package/dist/wfp-delete-DysvX1u7.mjs +36 -0
- package/dist/wfp-delete-DysvX1u7.mjs.map +1 -0
- package/dist/wfp-put-jaVd_LjO.mjs +52 -0
- package/dist/wfp-put-jaVd_LjO.mjs.map +1 -0
- package/dist/worker-route-Be2IvOdr.mjs +263 -0
- package/dist/worker-route-Be2IvOdr.mjs.map +1 -0
- package/dist/workers-aGILs77X.mjs +87 -0
- package/dist/workers-aGILs77X.mjs.map +1 -0
- package/dist/wranglerSpawn-DmEz0ldT.mjs +24 -0
- package/dist/wranglerSpawn-DmEz0ldT.mjs.map +1 -0
- package/dist/zoneResolver-VoxLHM4N.mjs +32 -0
- package/dist/zoneResolver-VoxLHM4N.mjs.map +1 -0
- package/package.json +38 -3
|
@@ -0,0 +1,3817 @@
|
|
|
1
|
+
import { f as getDispatchNamespaces, n as materializeTamerResolvable, r as materializeVars } from "./normalize-Bx0bpFop.mjs";
|
|
2
|
+
import { t as getWorkers } from "./loader-DP7yXqT6.mjs";
|
|
3
|
+
import { f as stackNameForConfig, o as effectiveDispatchNamespaceName, s as isEphemeralEnv, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
|
|
4
|
+
import { n as r2S3CredentialsFromEnv, t as emptyR2BucketViaS3 } from "./r2S3EmptyBucket-DD81ZWQ7.mjs";
|
|
5
|
+
import { n as logApplyChange } from "./planFormat-CJw8Kq2s.mjs";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
//#region src/core/naming/NamingEngine.ts
|
|
9
|
+
function reLiteral(s) {
|
|
10
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
var NamingEngine = class {
|
|
13
|
+
constructor(tenant, conventions) {
|
|
14
|
+
this.tenant = tenant;
|
|
15
|
+
this.conventions = conventions;
|
|
16
|
+
}
|
|
17
|
+
d1SingleName(logicalName, env) {
|
|
18
|
+
if (this.conventions?.d1Single) return this.conventions.d1Single(logicalName, this.tenant.id, env);
|
|
19
|
+
return `db_${logicalName}_t_${this.tenant.id}_${env}`;
|
|
20
|
+
}
|
|
21
|
+
d1ShardName(logicalName, shardDate, env) {
|
|
22
|
+
if (this.conventions?.d1Shard) return this.conventions.d1Shard(logicalName, shardDate, this.tenant.id, env);
|
|
23
|
+
const dateNoDashes = shardDate.replace(/-/g, "");
|
|
24
|
+
if (logicalName === "default" || logicalName === "") return `db_${dateNoDashes}_t_${this.tenant.id}_${env}`;
|
|
25
|
+
return `db_${logicalName}_${dateNoDashes}_t_${this.tenant.id}_${env}`;
|
|
26
|
+
}
|
|
27
|
+
d1SingleBindingKey(logicalName) {
|
|
28
|
+
return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
29
|
+
}
|
|
30
|
+
d1ShardBindingKey(logicalName, shardDate) {
|
|
31
|
+
const dateNoDashes = shardDate.replace(/-/g, "");
|
|
32
|
+
return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_${dateNoDashes}_T_${this.tenant.id.toUpperCase()}`;
|
|
33
|
+
}
|
|
34
|
+
r2BucketName(logicalName, env) {
|
|
35
|
+
if (this.conventions?.r2Bucket) {
|
|
36
|
+
const dateNoDashes = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
37
|
+
return this.conventions.r2Bucket(logicalName, dateNoDashes, this.tenant.id, env);
|
|
38
|
+
}
|
|
39
|
+
return `r2-${logicalName}-t-${this.tenant.id}-${env}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Wrangler `r2_buckets[].binding`: logical + tenant only (same idea as {@link kvBindingKey}).
|
|
43
|
+
* Stable across envs; default {@link r2BucketName} is `r2-{logical}-t-{tenant}-{env}`.
|
|
44
|
+
*/
|
|
45
|
+
r2BindingKey(logicalName) {
|
|
46
|
+
return `R2_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
47
|
+
}
|
|
48
|
+
kvNamespaceName(logicalName, env) {
|
|
49
|
+
return `kv_${logicalName}_t_${this.tenant.id}_${env}`;
|
|
50
|
+
}
|
|
51
|
+
kvBindingKey(logicalName) {
|
|
52
|
+
return `KV_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
53
|
+
}
|
|
54
|
+
queueName(logicalName, env) {
|
|
55
|
+
return `q-${logicalName}-t-${this.tenant.id}-${env}`;
|
|
56
|
+
}
|
|
57
|
+
queueBindingKey(logicalName) {
|
|
58
|
+
return `Q_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
59
|
+
}
|
|
60
|
+
hyperdriveName(logicalName, env) {
|
|
61
|
+
return `hd-${logicalName}-t-${this.tenant.id}-${env}`;
|
|
62
|
+
}
|
|
63
|
+
hyperdriveBindingKey(logicalName) {
|
|
64
|
+
return `HD_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
65
|
+
}
|
|
66
|
+
vectorizeName(logicalName, env) {
|
|
67
|
+
return `vec-${logicalName}-t-${this.tenant.id}-${env}`;
|
|
68
|
+
}
|
|
69
|
+
vectorizeBindingKey(logicalName) {
|
|
70
|
+
return `VEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* AI Gateway slug (== Cloudflare gateway id). Per the Cloudflare API,
|
|
74
|
+
* gateway ids are case-sensitive lowercase ascii with `-`/`_`. Format:
|
|
75
|
+
* `aigw-{logical}-t-{tenantId}-{env}`.
|
|
76
|
+
*/
|
|
77
|
+
aiGatewayId(logicalName, env) {
|
|
78
|
+
return `aigw-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stable cross-reference binding key for AI Gateway. AI Gateway has no
|
|
82
|
+
* Wrangler binding kind today, so this is only consumed by
|
|
83
|
+
* `${tamer:ai_gateway:<logical>.binding}` interpolations.
|
|
84
|
+
*/
|
|
85
|
+
aiGatewayBindingKey(logicalName) {
|
|
86
|
+
return `AI_GW_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cloudflare-side pipeline name. Pipelines V1 names must be lowercase
|
|
90
|
+
* alphanumerics + hyphens (no underscores), so we mirror the R2 scheme.
|
|
91
|
+
* Pattern: `pipe-{logical}-t-{tenantId}-{env}`.
|
|
92
|
+
*/
|
|
93
|
+
pipelineName(logicalName, env) {
|
|
94
|
+
return `pipe-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrangler binding key emitted in `pipelines[]`. Uppercased logical with
|
|
98
|
+
* the tenant id appended so two tenants can share a worker namespace
|
|
99
|
+
* without colliding bindings.
|
|
100
|
+
*/
|
|
101
|
+
pipelineBindingKey(logicalName) {
|
|
102
|
+
return `PIPE_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
103
|
+
}
|
|
104
|
+
pipelineMatchPattern(logicalName, env) {
|
|
105
|
+
const exact = this.pipelineName(logicalName, env);
|
|
106
|
+
return (name) => name === exact;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Cloudflare-side workflow name. Workflow names accept lowercase
|
|
110
|
+
* alphanumerics + hyphens; we mirror the pipelines/AI-Gateway hyphen
|
|
111
|
+
* scheme. Pattern: `wf-{logical}-t-{tenantId}-{env}`.
|
|
112
|
+
*/
|
|
113
|
+
workflowName(logicalName, env) {
|
|
114
|
+
return `wf-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Wrangler binding key emitted in `workflows[]`. Uppercased logical with
|
|
118
|
+
* the tenant id appended so two tenants sharing a script can't collide.
|
|
119
|
+
*/
|
|
120
|
+
workflowBindingKey(logicalName) {
|
|
121
|
+
return `WF_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
122
|
+
}
|
|
123
|
+
workflowMatchPattern(logicalName, env) {
|
|
124
|
+
const exact = this.workflowName(logicalName, env);
|
|
125
|
+
return (name) => name === exact;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Cloudflare Secrets Store name. Account-scoped — the API allows free-form
|
|
129
|
+
* names (the dashboard examples use both `service_x_keys` and dashed
|
|
130
|
+
* variants), so we mirror our hyphen convention for consistency with
|
|
131
|
+
* pipelines / workflows / AI Gateway. Pattern:
|
|
132
|
+
* `sec-{logical}-t-{tenantId}-{env}`.
|
|
133
|
+
*/
|
|
134
|
+
secretsStoreName(logicalName, env) {
|
|
135
|
+
return `sec-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Stable cross-reference key for the store. Secrets Store has no Wrangler
|
|
139
|
+
* binding kind directly — `secrets_store_secrets[]` references the
|
|
140
|
+
* resolved `store_id`, not the store name — so this only powers
|
|
141
|
+
* `${tamer:secret_store:<n>.binding}` interpolations.
|
|
142
|
+
*/
|
|
143
|
+
secretsStoreBindingKey(logicalName) {
|
|
144
|
+
return `SEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
|
|
145
|
+
}
|
|
146
|
+
secretsStoreMatchPattern(logicalName, env) {
|
|
147
|
+
const exact = this.secretsStoreName(logicalName, env);
|
|
148
|
+
return (name) => name === exact;
|
|
149
|
+
}
|
|
150
|
+
workerName(workerKey, env) {
|
|
151
|
+
if (this.conventions?.workerName) return this.conventions.workerName(this.tenant.slug, workerKey, env, this.tenant.id);
|
|
152
|
+
if (env === "local") return `${this.tenant.slug}-${workerKey}-${this.tenant.id}`;
|
|
153
|
+
return `${this.tenant.slug}-${workerKey}-${env}-${this.tenant.id}`;
|
|
154
|
+
}
|
|
155
|
+
d1MatchPattern(logicalName, env) {
|
|
156
|
+
const suffix = `_t_${this.tenant.id}_${env}`;
|
|
157
|
+
if (logicalName === "default" || logicalName === "") return (name) => /^db_\d{8}_t_/.test(name) && name.endsWith(suffix);
|
|
158
|
+
const prefix = `db_${logicalName}_`;
|
|
159
|
+
return (name) => name.startsWith(prefix) && name.endsWith(suffix);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Default: exact {@link r2BucketName}, or legacy dated buckets
|
|
163
|
+
* `r2-{logical}-{YYYYMMDD}-t-{tenant}-{env}` from older Tamer versions.
|
|
164
|
+
* Custom {@link NamingConventions.r2Bucket}: exact name only (uses today's date
|
|
165
|
+
* when calling the hook, same as apply).
|
|
166
|
+
*/
|
|
167
|
+
r2MatchPattern(logicalName, env) {
|
|
168
|
+
if (this.conventions?.r2Bucket) {
|
|
169
|
+
const expected = this.r2BucketName(logicalName, env);
|
|
170
|
+
return (name) => name === expected;
|
|
171
|
+
}
|
|
172
|
+
const exactNew = `r2-${logicalName}-t-${this.tenant.id}-${env}`;
|
|
173
|
+
const legacyDated = /* @__PURE__ */ new RegExp(`^r2-${reLiteral(logicalName)}-\\d{8}-t-${reLiteral(this.tenant.id)}-${reLiteral(env)}$`);
|
|
174
|
+
return (name) => name === exactNew || legacyDated.test(name);
|
|
175
|
+
}
|
|
176
|
+
extractD1ShardDate(name) {
|
|
177
|
+
const match = name.match(/_(\d{8})_t_/);
|
|
178
|
+
if (match) {
|
|
179
|
+
const d = match[1];
|
|
180
|
+
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
extractR2Date(name) {
|
|
185
|
+
const match = name.match(/r2-\w+-(\d{8})-t-/);
|
|
186
|
+
if (match) {
|
|
187
|
+
const d = match[1];
|
|
188
|
+
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/core/config/namingFromConfig.ts
|
|
196
|
+
function namingFromConfig(config) {
|
|
197
|
+
const conventions = "naming" in config && config.naming ? config.naming : void 0;
|
|
198
|
+
return new NamingEngine(config.tenant, conventions);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/core/routes/routes.resolve.ts
|
|
203
|
+
const DEFAULT_PROD_ENVS = ["prod", "production"];
|
|
204
|
+
const DEFAULT_SKIP_ENVS = ["local"];
|
|
205
|
+
/**
|
|
206
|
+
* Per `docs/handoff.md` §6:
|
|
207
|
+
* - prod / production → bare apex (`todo.com`)
|
|
208
|
+
* - any other env (including ephemeral `pr-*`) → `{env}.{apex}`
|
|
209
|
+
* - `local` (or anything in `skipEnvs`) → no route
|
|
210
|
+
*
|
|
211
|
+
* The resource-name `-{env}` suffix on workers/D1/R2/KV is decoupled from
|
|
212
|
+
* URLs; this function only computes the URL.
|
|
213
|
+
*/
|
|
214
|
+
function effectiveHostForEnv(route, env) {
|
|
215
|
+
if ((route.skipEnvs ?? DEFAULT_SKIP_ENVS).includes(env)) return void 0;
|
|
216
|
+
if ((route.prodEnvs ?? DEFAULT_PROD_ENVS).includes(env)) return route.host;
|
|
217
|
+
return `${env}.${route.host}`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Expand one Tamer route into a wrangler `Route` (or `undefined` if the env
|
|
221
|
+
* should not receive the route at all).
|
|
222
|
+
*/
|
|
223
|
+
function expandRouteForEnv(route, env) {
|
|
224
|
+
const host = effectiveHostForEnv(route, env);
|
|
225
|
+
if (!host) return void 0;
|
|
226
|
+
const zone = route.zone ?? route.host;
|
|
227
|
+
if (route.customDomain) return {
|
|
228
|
+
pattern: host,
|
|
229
|
+
custom_domain: true
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
pattern: `${host}${route.path ?? "/*"}`,
|
|
233
|
+
zone_name: zone
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Expand `tamerRoutes` for the given env, dropping any that resolve to
|
|
238
|
+
* `undefined` (`local`, `skipEnvs`).
|
|
239
|
+
*
|
|
240
|
+
* Note: ephemeral envs (matching `tenant.ephemeralEnvPattern`) follow the
|
|
241
|
+
* same `{env}.{apex}` prefix rule as any non-prod env — e.g. an env named
|
|
242
|
+
* `pr-1234` resolves to `pr-1234.todo.com`. Callers that want a different
|
|
243
|
+
* URL scheme for ephemeral envs should special-case before calling.
|
|
244
|
+
*/
|
|
245
|
+
function effectiveRoutesForEnv(tamerRoutes, env) {
|
|
246
|
+
if (!tamerRoutes || tamerRoutes.length === 0) return [];
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const r of tamerRoutes) {
|
|
249
|
+
const expanded = expandRouteForEnv(r, env);
|
|
250
|
+
if (expanded) out.push(expanded);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Zone-name routes are attached via the Cloudflare Workers Routes HTTP API
|
|
256
|
+
* (`/zones/{id}/workers/routes`), not via `wrangler.json`, so deploys stay
|
|
257
|
+
* consistent with `tamer drift` / destroy.
|
|
258
|
+
*/
|
|
259
|
+
function isApiManagedZoneRoute(r) {
|
|
260
|
+
return typeof r === "object" && r !== null && "zone_name" in r && typeof r.zone_name === "string" && "pattern" in r && typeof r.pattern === "string" && !("custom_domain" in r && r.custom_domain === true);
|
|
261
|
+
}
|
|
262
|
+
/** Custom-domain `tamerRoutes` stay in wrangler until a dedicated API path exists. */
|
|
263
|
+
function isWranglerOnlyTamerRoute(r) {
|
|
264
|
+
return typeof r === "object" && r !== null && "custom_domain" in r && r.custom_domain === true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/core/wrangler/wranglerOutFile.ts
|
|
269
|
+
/** Reject path segments in generated Wrangler config filename. */
|
|
270
|
+
function assertSafeWranglerOutFile(name) {
|
|
271
|
+
const trimmed = name.trim();
|
|
272
|
+
if (!trimmed) throw new Error("wranglerOutFile cannot be empty");
|
|
273
|
+
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("..")) throw new Error(`Invalid wranglerOutFile "${name}": use a basename only (e.g. wrangler.json)`);
|
|
274
|
+
return trimmed;
|
|
275
|
+
}
|
|
276
|
+
/** Extra CLI args so Wrangler uses a non-default config file. */
|
|
277
|
+
function wranglerConfigCliArgs(outFile) {
|
|
278
|
+
if (outFile === "wrangler.json") return [];
|
|
279
|
+
return ["--config", outFile];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/core/references/references.ts
|
|
284
|
+
var TamerReferenceError = class extends Error {
|
|
285
|
+
constructor(message, fieldPath) {
|
|
286
|
+
super(`${message} (at ${fieldPath})`);
|
|
287
|
+
this.fieldPath = fieldPath;
|
|
288
|
+
this.name = "TamerReferenceError";
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const REF_RE = /\$\{tamer:([a-z0-9_]+):([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
|
|
292
|
+
/**
|
|
293
|
+
* Scan a string for any `${tamer:...}` references. Returns true when at
|
|
294
|
+
* least one reference is present (used for cheap pre-checks).
|
|
295
|
+
*/
|
|
296
|
+
function stringHasReference(s) {
|
|
297
|
+
REF_RE.lastIndex = 0;
|
|
298
|
+
return REF_RE.test(s);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Resolve every `${tamer:...}` reference in `value`. Replacement preserves
|
|
302
|
+
* surrounding text for partial-string interpolation. `fieldPath` is included
|
|
303
|
+
* in any thrown {@link TamerReferenceError} for actionable diagnostics.
|
|
304
|
+
*/
|
|
305
|
+
function resolveReferencesInString(value, ctx, fieldPath) {
|
|
306
|
+
if (!stringHasReference(value)) return value;
|
|
307
|
+
return value.replace(REF_RE, (match, kind, logicalName, field) => {
|
|
308
|
+
try {
|
|
309
|
+
return lookupReference(kind, logicalName, field, ctx, fieldPath);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (ctx.tolerant && err instanceof TamerReferenceError) return match;
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Walk a `vars` record (or any flat string→string map) replacing references
|
|
318
|
+
* in every value. Returns a new object; the input is not mutated.
|
|
319
|
+
*/
|
|
320
|
+
function resolveReferencesInVars(vars, ctx, fieldPathPrefix) {
|
|
321
|
+
if (!vars) return vars;
|
|
322
|
+
const out = {};
|
|
323
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
324
|
+
if (typeof value !== "string") {
|
|
325
|
+
out[key] = value;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
out[key] = resolveReferencesInString(value, ctx, `${fieldPathPrefix}.${key}`);
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
function lookupReference(kind, logicalName, field, ctx, fieldPath) {
|
|
333
|
+
switch (kind) {
|
|
334
|
+
case "d1": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "d1_database" && entry.logicalName === logicalName);
|
|
335
|
+
case "r2": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "r2_bucket" && entry.logicalName === logicalName);
|
|
336
|
+
case "kv": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "kv_namespace" && entry.logicalName === logicalName);
|
|
337
|
+
case "queue": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "queue" && entry.logicalName === logicalName);
|
|
338
|
+
case "hyperdrive": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "hyperdrive" && entry.logicalName === logicalName);
|
|
339
|
+
case "vectorize": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "vectorize" && entry.logicalName === logicalName);
|
|
340
|
+
case "ai_gateway": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "ai_gateway" && entry.logicalName === logicalName);
|
|
341
|
+
case "pipeline": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "pipeline" && entry.logicalName === logicalName);
|
|
342
|
+
case "workflow": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "workflow" && entry.logicalName === logicalName);
|
|
343
|
+
case "secret_store": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "secrets_store" && entry.logicalName === logicalName);
|
|
344
|
+
case "dispatch_namespace": return lookupDispatchNamespace(ctx, logicalName, field, fieldPath);
|
|
345
|
+
case "worker": return lookupWorker(ctx, logicalName, field, fieldPath);
|
|
346
|
+
case "import": return lookupImport(ctx, logicalName, field, fieldPath);
|
|
347
|
+
case "logpush_pipelines": return lookupLogpushPipelines(ctx, logicalName, field, fieldPath);
|
|
348
|
+
case "config": return lookupConfigField(ctx, logicalName, field, fieldPath);
|
|
349
|
+
default: throw new TamerReferenceError(`Unknown reference kind "${kind}" — expected one of d1 | r2 | kv | queue | hyperdrive | vectorize | ai_gateway | pipeline | workflow | secret_store | dispatch_namespace | worker | import | logpush_pipelines | config`, fieldPath);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function lookupConfigField(ctx, _logicalName, field, fieldPath) {
|
|
353
|
+
if (field === "account_id") {
|
|
354
|
+
const id = (ctx.accountId ?? ctx.config.account_id ?? "").trim();
|
|
355
|
+
if (!id) throw new TamerReferenceError(`Reference \${tamer:config:stack.account_id} needs the stack Cloudflare account id — set top-level account_id in tamer/project.config.ts or pass CLOUDFLARE_ACCOUNT_ID when running tamer.`, fieldPath);
|
|
356
|
+
return id;
|
|
357
|
+
}
|
|
358
|
+
throw new TamerReferenceError(`Unknown field "${field}" on config reference — expected account_id`, fieldPath);
|
|
359
|
+
}
|
|
360
|
+
function getLogpushPipelinesEntry(ctx, logicalName, fieldPath) {
|
|
361
|
+
const all = ctx.state.getAll();
|
|
362
|
+
const entry = Object.values(all).find((e) => e.type === "logpush_pipelines" && e.logicalName === logicalName);
|
|
363
|
+
if (!entry) throw new TamerReferenceError(`Reference \${tamer:logpush_pipelines:${logicalName}.…} cannot be resolved — no pipelines graph in state for logpush job "${logicalName}". Run a full \`tamer apply\` (including the Logpush / pipelinesAuto step) for env "${ctx.env}" first.`, fieldPath);
|
|
364
|
+
return entry;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Exposes fields from the `logpush_pipelines:*` D1 state row (after
|
|
368
|
+
* `ensurePipelinesLogpushProvision` has run) for `outputs` and `vars`, e.g.
|
|
369
|
+
* `${tamer:logpush_pipelines:workers-trace.r2_data_catalog_table_name}`.
|
|
370
|
+
*/
|
|
371
|
+
function lookupLogpushPipelines(ctx, logicalName, field, fieldPath) {
|
|
372
|
+
const entry = getLogpushPipelinesEntry(ctx, logicalName, fieldPath);
|
|
373
|
+
switch (field) {
|
|
374
|
+
case "r2_data_catalog_table_name":
|
|
375
|
+
case "iceberg_table": {
|
|
376
|
+
const v = entry.r2DataCatalogTableName?.trim();
|
|
377
|
+
if (!v) throw new TamerReferenceError(`logpush_pipelines state for "${logicalName}" has no r2DataCatalogTableName (sink not yet created, or pre-upgrade state). Re-run a full \`tamer apply\` for env "${ctx.env}" after pipelinesAuto provisions the sink, or set table manually in consuming stack.`, fieldPath);
|
|
378
|
+
return v;
|
|
379
|
+
}
|
|
380
|
+
case "r2_data_catalog_table_name_pipelines":
|
|
381
|
+
case "iceberg_table_pipelines": {
|
|
382
|
+
const v = entry.r2DataCatalogTableNamePipelines?.trim();
|
|
383
|
+
if (v) return v;
|
|
384
|
+
const fallback = entry.r2DataCatalogTableName?.trim();
|
|
385
|
+
if (!fallback) throw new TamerReferenceError(`logpush_pipelines state for "${logicalName}" has no table name (sink not yet created). Re-run \`tamer apply\` for env "${ctx.env}".`, fieldPath);
|
|
386
|
+
return fallback;
|
|
387
|
+
}
|
|
388
|
+
case "r2_data_catalog_namespace":
|
|
389
|
+
case "iceberg_namespace": return (entry.r2DataCatalogNamespace ?? "default").trim() || "default";
|
|
390
|
+
case "name": return entry.pipelineName;
|
|
391
|
+
case "id": return entry.pipelineId;
|
|
392
|
+
default: throw new TamerReferenceError(`Unknown field "${field}" on logpush_pipelines reference — expected r2_data_catalog_table_name | r2_data_catalog_table_name_pipelines | r2_data_catalog_namespace | name | id | iceberg_table | iceberg_table_pipelines | iceberg_namespace`, fieldPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Resolve a `${tamer:import:<stack>.<output>}` against pre-fetched sibling
|
|
397
|
+
* stack outputs. The pre-fetch (`fetchStackImports`) loads every imported
|
|
398
|
+
* stack's `cfi_state:{stack}` row before resolution begins; this lookup is
|
|
399
|
+
* pure map access. Throws if the stack isn't in `ctx.imports` (config
|
|
400
|
+
* never declared it / pre-fetch wasn't wired in for this command) or if
|
|
401
|
+
* the named output hasn't been published yet (sibling stack hasn't run
|
|
402
|
+
* `apply` since the output was declared, or has been destroyed).
|
|
403
|
+
*/
|
|
404
|
+
function lookupImport(ctx, stackName, outputName, fieldPath) {
|
|
405
|
+
const imports = ctx.imports;
|
|
406
|
+
if (!imports || !(stackName in imports)) throw new TamerReferenceError(`Reference \${tamer:import:${stackName}.${outputName}} cannot be resolved — no imported stack "${stackName}" available. Ensure stack "${stackName}" exists in env "${ctx.env}" (run 'tamer apply' there first) and that the current command pre-fetches sibling stacks.`, fieldPath);
|
|
407
|
+
const stackOutputs = imports[stackName];
|
|
408
|
+
if (!(outputName in stackOutputs)) {
|
|
409
|
+
const available = Object.keys(stackOutputs).sort();
|
|
410
|
+
throw new TamerReferenceError(`Reference \${tamer:import:${stackName}.${outputName}} cannot be resolved — output "${outputName}" not found on imported stack "${stackName}".${available.length > 0 ? ` Available outputs on stack "${stackName}": ${available.join(", ")}.` : ` Stack "${stackName}" has no published outputs — run 'tamer apply' there with an \`outputs:\` block.`}`, fieldPath);
|
|
411
|
+
}
|
|
412
|
+
return stackOutputs[outputName];
|
|
413
|
+
}
|
|
414
|
+
function lookupResource(ctx, kind, logicalName, field, fieldPath, predicate) {
|
|
415
|
+
const all = ctx.state.getAll();
|
|
416
|
+
const entry = Object.values(all).find(predicate);
|
|
417
|
+
if (!entry) throw new TamerReferenceError(`Reference \${tamer:${kind}:${logicalName}.${field}} cannot be resolved — no ${kind} resource named "${logicalName}" in state. Run 'tamer apply --env ${ctx.env}' first.`, fieldPath);
|
|
418
|
+
switch (field) {
|
|
419
|
+
case "name": return resourceName(entry);
|
|
420
|
+
case "id": return resourceId(entry, kind, logicalName, fieldPath);
|
|
421
|
+
case "binding": return resourceBinding(entry);
|
|
422
|
+
default: throw new TamerReferenceError(`Unknown field "${field}" on ${kind} reference — expected name | id | binding`, fieldPath);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function resourceName(entry) {
|
|
426
|
+
switch (entry.type) {
|
|
427
|
+
case "d1_database":
|
|
428
|
+
case "kv_namespace":
|
|
429
|
+
case "queue":
|
|
430
|
+
case "hyperdrive":
|
|
431
|
+
case "vectorize":
|
|
432
|
+
case "ai_gateway":
|
|
433
|
+
case "pipeline":
|
|
434
|
+
case "workflow":
|
|
435
|
+
case "secrets_store":
|
|
436
|
+
case "dispatch_namespace":
|
|
437
|
+
case "r2_bucket": return entry.derivedName;
|
|
438
|
+
case "dns_record": return entry.name;
|
|
439
|
+
case "logpush_job": return entry.derivedName;
|
|
440
|
+
case "logpush_pipelines": return entry.pipelineName;
|
|
441
|
+
case "worker_route": return entry.pattern;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function resourceId(entry, kind, logicalName, fieldPath) {
|
|
445
|
+
switch (entry.type) {
|
|
446
|
+
case "d1_database":
|
|
447
|
+
case "kv_namespace":
|
|
448
|
+
case "queue":
|
|
449
|
+
case "hyperdrive":
|
|
450
|
+
case "vectorize":
|
|
451
|
+
case "ai_gateway":
|
|
452
|
+
case "pipeline":
|
|
453
|
+
case "workflow":
|
|
454
|
+
case "secrets_store": return entry.cfId;
|
|
455
|
+
case "r2_bucket": throw new TamerReferenceError(`R2 bucket "${logicalName}" has no .id (R2 buckets are addressed by name); use \${tamer:${kind}:${logicalName}.name}`, fieldPath);
|
|
456
|
+
case "dispatch_namespace": return entry.derivedName;
|
|
457
|
+
case "dns_record": return entry.recordId;
|
|
458
|
+
case "logpush_job": return String(entry.cfJobId);
|
|
459
|
+
case "logpush_pipelines": return entry.pipelineId;
|
|
460
|
+
case "worker_route": return entry.routeId;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function resourceBinding(entry) {
|
|
464
|
+
switch (entry.type) {
|
|
465
|
+
case "d1_database":
|
|
466
|
+
case "r2_bucket":
|
|
467
|
+
case "kv_namespace":
|
|
468
|
+
case "queue":
|
|
469
|
+
case "hyperdrive":
|
|
470
|
+
case "vectorize":
|
|
471
|
+
case "ai_gateway":
|
|
472
|
+
case "pipeline":
|
|
473
|
+
case "workflow":
|
|
474
|
+
case "secrets_store": return entry.bindingKey;
|
|
475
|
+
case "dispatch_namespace": return entry.derivedName;
|
|
476
|
+
case "dns_record": throw new Error("internal: dns_record has no .binding — use .name or .id in config references");
|
|
477
|
+
case "logpush_job": throw new Error("internal: logpush_job has no .binding — use .name or .id in config references");
|
|
478
|
+
case "logpush_pipelines": throw new Error("internal: logpush_pipelines has no .binding — use .name or .id in config references");
|
|
479
|
+
case "worker_route": return entry.routeId;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function lookupDispatchNamespace(ctx, logicalName, field, fieldPath) {
|
|
483
|
+
if (!getDispatchNamespaces(ctx.config).find((d) => d.logicalName === logicalName)) throw new TamerReferenceError(`Reference \${tamer:dispatch_namespace:${logicalName}.${field}} cannot be resolved — no dispatchNamespaces entry named "${logicalName}" in the Tamer project config`, fieldPath);
|
|
484
|
+
if (field !== "name" && field !== "id") throw new TamerReferenceError(`Unknown field "${field}" on dispatch_namespace reference — expected name | id`, fieldPath);
|
|
485
|
+
const all = ctx.state.getAll();
|
|
486
|
+
const stateEntry = Object.values(all).find((e) => e.type === "dispatch_namespace" && e.logicalName === logicalName);
|
|
487
|
+
if (!stateEntry || stateEntry.type !== "dispatch_namespace") throw new TamerReferenceError(`Reference \${tamer:dispatch_namespace:${logicalName}.${field}} unresolved — no state entry. Run 'tamer apply --env ${ctx.env}' first.`, fieldPath);
|
|
488
|
+
return stateEntry.derivedName;
|
|
489
|
+
}
|
|
490
|
+
function lookupWorker(ctx, workerKey, field, fieldPath) {
|
|
491
|
+
let target;
|
|
492
|
+
if (ctx.config.workers && ctx.config.workers[workerKey]) target = ctx.config.workers[workerKey];
|
|
493
|
+
else if (ctx.config.worker && workerKey === "default") target = ctx.config.worker;
|
|
494
|
+
if (!target) throw new TamerReferenceError(`Reference \${tamer:worker:${workerKey}.${field}} cannot be resolved — no worker key "${workerKey}" in the Tamer project config`, fieldPath);
|
|
495
|
+
if (field !== "name") throw new TamerReferenceError(`Unknown field "${field}" on worker reference — only "name" is supported (the env-suffixed deployed script name)`, fieldPath);
|
|
496
|
+
return resolveDeployedWorkerName(ctx.config, workerKey, target, ctx.env, ctx.naming);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/core/config/resolver.ts
|
|
501
|
+
/** Wrangler script name after env suffix rules (matches `tamer deploy`). */
|
|
502
|
+
function resolveDeployedWorkerName(_config, workerKey, workerConfig, env, naming) {
|
|
503
|
+
const sn = workerConfig.scriptName?.trim();
|
|
504
|
+
if (sn) {
|
|
505
|
+
if (env === "local") return sn;
|
|
506
|
+
return `${sn}-${env}`;
|
|
507
|
+
}
|
|
508
|
+
return naming.workerName(workerKey, env);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Map from base service-binding target name (`scriptName` for envs other than
|
|
512
|
+
* `local`, or the local-deployed name) → env-suffixed deployed name for every
|
|
513
|
+
* worker in `config`. Used to auto-rewrite intra-stack `services[].service`
|
|
514
|
+
* fields so fixtures don't have to repeat env overrides for every env.
|
|
515
|
+
*/
|
|
516
|
+
function buildIntraStackScriptNameMap(config, env, naming) {
|
|
517
|
+
const map = /* @__PURE__ */ new Map();
|
|
518
|
+
if (config.workers) for (const [key, wc] of Object.entries(config.workers)) {
|
|
519
|
+
const baseName = wc.scriptName?.trim() ?? naming.workerName(key, "local");
|
|
520
|
+
const deployed = resolveDeployedWorkerName(config, key, wc, env, naming);
|
|
521
|
+
map.set(baseName, deployed);
|
|
522
|
+
}
|
|
523
|
+
if (config.worker) {
|
|
524
|
+
const w = config.worker;
|
|
525
|
+
const baseName = w.scriptName?.trim() ?? naming.workerName("default", "local");
|
|
526
|
+
const deployed = resolveDeployedWorkerName(config, "default", w, env, naming);
|
|
527
|
+
map.set(baseName, deployed);
|
|
528
|
+
}
|
|
529
|
+
return map;
|
|
530
|
+
}
|
|
531
|
+
/** Returns a worker config copy whose `services[].service` is rewritten to env-suffixed deployed names. */
|
|
532
|
+
function rewriteIntraStackServiceTargets(workerConfig, baseToDeployed) {
|
|
533
|
+
const services = workerConfig.services;
|
|
534
|
+
if (!services || services.length === 0) return workerConfig;
|
|
535
|
+
let rewroteAny = false;
|
|
536
|
+
const rewritten = services.map((s) => {
|
|
537
|
+
const target = baseToDeployed.get(s.service);
|
|
538
|
+
if (!target || target === s.service) return s;
|
|
539
|
+
rewroteAny = true;
|
|
540
|
+
return {
|
|
541
|
+
...s,
|
|
542
|
+
service: target
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
if (!rewroteAny) return workerConfig;
|
|
546
|
+
return {
|
|
547
|
+
...workerConfig,
|
|
548
|
+
services: rewritten
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function mergeVars(base = {}, override = {}) {
|
|
552
|
+
return {
|
|
553
|
+
...base,
|
|
554
|
+
...override
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** Same env merge as `resolveWorkerConfig` (for deploy topo-sort service edges). */
|
|
558
|
+
function mergedWorkerConfigForEnv(workerConfig, env, tenant) {
|
|
559
|
+
let merged = { ...workerConfig };
|
|
560
|
+
if (env === "local" && workerConfig.local) merged = {
|
|
561
|
+
...merged,
|
|
562
|
+
...workerConfig.local,
|
|
563
|
+
vars: mergeVars(workerConfig.vars, workerConfig.local.vars)
|
|
564
|
+
};
|
|
565
|
+
else if (workerConfig.env?.[env]) {
|
|
566
|
+
const envOverride = workerConfig.env[env];
|
|
567
|
+
merged = {
|
|
568
|
+
...merged,
|
|
569
|
+
...envOverride,
|
|
570
|
+
vars: mergeVars(workerConfig.vars, envOverride.vars)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const mv = merged.vars;
|
|
574
|
+
if (mv && Object.prototype.hasOwnProperty.call(mv, "BRANCH_SUFFIX")) merged = {
|
|
575
|
+
...merged,
|
|
576
|
+
vars: {
|
|
577
|
+
...mv,
|
|
578
|
+
BRANCH_SUFFIX: isEphemeralEnv(env, tenant) ? env : ""
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
return merged;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Align `dispatch_namespaces[].namespace` and `vars.WFP_NAMESPACE` with
|
|
585
|
+
* {@link effectiveDispatchNamespaceName} for envs that have no explicit
|
|
586
|
+
* `worker.env[env]` block (e.g. `pr-*` shared namespace).
|
|
587
|
+
*/
|
|
588
|
+
function applyDispatchNamespaceEnvOverrides(config, merged, env) {
|
|
589
|
+
const dns = getDispatchNamespaces(config);
|
|
590
|
+
if (dns.length === 0) return merged;
|
|
591
|
+
const resolved = effectiveDispatchNamespaceName(dns[0], env, config.tenant);
|
|
592
|
+
const m = merged;
|
|
593
|
+
let next = merged;
|
|
594
|
+
if (m.dispatch_namespaces?.length) next = {
|
|
595
|
+
...next,
|
|
596
|
+
dispatch_namespaces: m.dispatch_namespaces.map((d) => ({
|
|
597
|
+
...d,
|
|
598
|
+
namespace: resolved
|
|
599
|
+
}))
|
|
600
|
+
};
|
|
601
|
+
if (m.vars && typeof m.vars.WFP_NAMESPACE === "string" && m.vars.WFP_NAMESPACE === dns[0].namespace) {
|
|
602
|
+
const v = next.vars;
|
|
603
|
+
next = {
|
|
604
|
+
...next,
|
|
605
|
+
vars: {
|
|
606
|
+
...v,
|
|
607
|
+
WFP_NAMESPACE: resolved
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return next;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Walk the merged worker config and replace `${tamer:<kind>:<logical>.<field>}`
|
|
615
|
+
* references in `vars` and `tamerRoutes[].host` / `.zone` against the current
|
|
616
|
+
* state snapshot. Also resolves `r2_buckets[].bucket_name`,
|
|
617
|
+
* **`services[].service`**, **`dispatch_namespaces[].namespace`**, and
|
|
618
|
+
* `resources.d1[].databaseName` when `ownership` is `external`. Throws
|
|
619
|
+
* `TamerReferenceError` (with field path) if any
|
|
620
|
+
* reference is unresolved — bubbles up to the caller as a fatal.
|
|
621
|
+
*/
|
|
622
|
+
function resolveCrossResourceReferences(merged, ctx) {
|
|
623
|
+
const refCtx = {
|
|
624
|
+
config: ctx.config,
|
|
625
|
+
env: ctx.env,
|
|
626
|
+
state: ctx.state,
|
|
627
|
+
naming: ctx.naming,
|
|
628
|
+
tolerant: ctx.tolerant,
|
|
629
|
+
imports: ctx.imports,
|
|
630
|
+
accountId: ctx.accountId
|
|
631
|
+
};
|
|
632
|
+
const m = merged;
|
|
633
|
+
let next = merged;
|
|
634
|
+
if (m.vars) {
|
|
635
|
+
const resolvedVars = resolveReferencesInVars(materializeVars(m.vars), refCtx, `worker[${ctx.workerKey}].vars`);
|
|
636
|
+
if (resolvedVars && resolvedVars !== m.vars) next = {
|
|
637
|
+
...next,
|
|
638
|
+
vars: resolvedVars
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const tamerRoutes = next.tamerRoutes;
|
|
642
|
+
if (tamerRoutes && tamerRoutes.length > 0) {
|
|
643
|
+
let mutated = false;
|
|
644
|
+
const resolvedRoutes = tamerRoutes.map((r, idx) => {
|
|
645
|
+
const fieldBase = `worker[${ctx.workerKey}].tamerRoutes[${idx}]`;
|
|
646
|
+
const host = resolveReferencesInString(r.host, refCtx, `${fieldBase}.host`);
|
|
647
|
+
const zone = r.zone ? resolveReferencesInString(r.zone, refCtx, `${fieldBase}.zone`) : r.zone;
|
|
648
|
+
if (host !== r.host || zone !== r.zone) mutated = true;
|
|
649
|
+
return {
|
|
650
|
+
...r,
|
|
651
|
+
host,
|
|
652
|
+
...zone !== void 0 ? { zone } : {}
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
if (mutated) next = {
|
|
656
|
+
...next,
|
|
657
|
+
tamerRoutes: resolvedRoutes
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const r2Buckets = next.r2_buckets;
|
|
661
|
+
if (r2Buckets && r2Buckets.length > 0) {
|
|
662
|
+
let mutated = false;
|
|
663
|
+
const resolvedBuckets = r2Buckets.map((b, idx) => {
|
|
664
|
+
const raw = b.bucket_name;
|
|
665
|
+
if (raw === void 0) return b;
|
|
666
|
+
const fieldBase = `worker[${ctx.workerKey}].r2_buckets[${idx}].bucket_name`;
|
|
667
|
+
const bucket_name = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
|
|
668
|
+
if (bucket_name !== raw) mutated = true;
|
|
669
|
+
return {
|
|
670
|
+
...b,
|
|
671
|
+
bucket_name
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
if (mutated) next = {
|
|
675
|
+
...next,
|
|
676
|
+
r2_buckets: resolvedBuckets
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const svc = next.services;
|
|
680
|
+
if (svc && svc.length > 0) {
|
|
681
|
+
let mutated = false;
|
|
682
|
+
const resolvedSvc = svc.map((s, idx) => {
|
|
683
|
+
const raw = s.service;
|
|
684
|
+
const fieldBase = `worker[${ctx.workerKey}].services[${idx}].service`;
|
|
685
|
+
const service = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
|
|
686
|
+
if (service !== raw) mutated = true;
|
|
687
|
+
return {
|
|
688
|
+
...s,
|
|
689
|
+
service
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
if (mutated) next = {
|
|
693
|
+
...next,
|
|
694
|
+
services: resolvedSvc
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const dispatchNsMerged = next.dispatch_namespaces;
|
|
698
|
+
if (dispatchNsMerged && dispatchNsMerged.length > 0) {
|
|
699
|
+
let mutated = false;
|
|
700
|
+
const resolvedDn = dispatchNsMerged.map((d, idx) => {
|
|
701
|
+
const raw = d.namespace;
|
|
702
|
+
const fieldBase = `worker[${ctx.workerKey}].dispatch_namespaces[${idx}].namespace`;
|
|
703
|
+
const namespace = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
|
|
704
|
+
if (namespace !== raw) mutated = true;
|
|
705
|
+
return {
|
|
706
|
+
...d,
|
|
707
|
+
namespace
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
if (mutated) next = {
|
|
711
|
+
...next,
|
|
712
|
+
dispatch_namespaces: resolvedDn
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const resBlock = next.resources;
|
|
716
|
+
if (resBlock?.d1 && resBlock.d1.length > 0) {
|
|
717
|
+
let mutated = false;
|
|
718
|
+
const d1Resolved = resBlock.d1.map((d1c, idx) => {
|
|
719
|
+
if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
|
|
720
|
+
const fieldBase = `worker[${ctx.workerKey}].resources.d1[${idx}].databaseName`;
|
|
721
|
+
const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
|
|
722
|
+
if (databaseName !== d1c.databaseName) mutated = true;
|
|
723
|
+
return {
|
|
724
|
+
...d1c,
|
|
725
|
+
databaseName
|
|
726
|
+
};
|
|
727
|
+
});
|
|
728
|
+
if (mutated) next = {
|
|
729
|
+
...next,
|
|
730
|
+
resources: {
|
|
731
|
+
...resBlock,
|
|
732
|
+
d1: d1Resolved
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return next;
|
|
737
|
+
}
|
|
738
|
+
function stripTamerFields(config) {
|
|
739
|
+
const { path, config: configPath, resources, local, env, scriptName: _scriptName, wranglerOutFile: _out, dispatchNamespace: _dispatchNs, tamerRoutes: _tamerRoutes, tamerStaleRouteSweepZones: _tamerStaleRouteSweepZones, ...rest } = config;
|
|
740
|
+
return rest;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Env merge + intra-stack `services` rewrite + sibling-stack resolution for
|
|
744
|
+
* **`resources.d1[].databaseName` only** when `ownership: "external"`.
|
|
745
|
+
*
|
|
746
|
+
* Use before {@link ResourceModule.pickResources} during `apply` / `sync` /
|
|
747
|
+
* `drift` so imported D1 names are known **without** resolving worker `vars`
|
|
748
|
+
* (those references often need resources created earlier in the same `apply`).
|
|
749
|
+
*/
|
|
750
|
+
function mergeWorkerConfigForResourcePick(config, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
|
|
751
|
+
let merged = mergedWorkerConfigForEnv(workerConfig, env, config.tenant);
|
|
752
|
+
merged = applyDispatchNamespaceEnvOverrides(config, merged, env);
|
|
753
|
+
const intraMap = buildIntraStackScriptNameMap(config, env, naming);
|
|
754
|
+
merged = rewriteIntraStackServiceTargets(merged, intraMap);
|
|
755
|
+
const refCtx = {
|
|
756
|
+
config,
|
|
757
|
+
env,
|
|
758
|
+
state,
|
|
759
|
+
naming,
|
|
760
|
+
tolerant: opts.referencesMode === "tolerant",
|
|
761
|
+
imports: opts.imports,
|
|
762
|
+
accountId
|
|
763
|
+
};
|
|
764
|
+
const resBlock = merged.resources;
|
|
765
|
+
if (!resBlock?.d1?.length) return merged;
|
|
766
|
+
const d1Resolved = resBlock.d1.map((d1c, idx) => {
|
|
767
|
+
if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
|
|
768
|
+
const fieldBase = `worker[${workerKey}].resources.d1[${idx}].databaseName`;
|
|
769
|
+
const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
|
|
770
|
+
return {
|
|
771
|
+
...d1c,
|
|
772
|
+
databaseName
|
|
773
|
+
};
|
|
774
|
+
});
|
|
775
|
+
return {
|
|
776
|
+
...merged,
|
|
777
|
+
resources: {
|
|
778
|
+
...resBlock,
|
|
779
|
+
d1: d1Resolved
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Env-merged worker config with every `${tamer:…}` site resolved for wrangler
|
|
785
|
+
* (`vars`, `tamerRoutes`, `r2_buckets[].bucket_name`, external D1 names).
|
|
786
|
+
* Used by {@link resolveWorkerConfig} after resource state is up to date.
|
|
787
|
+
*/
|
|
788
|
+
function mergeWorkerConfigWithResolvedRefs(config, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
|
|
789
|
+
let merged = mergedWorkerConfigForEnv(workerConfig, env, config.tenant);
|
|
790
|
+
merged = applyDispatchNamespaceEnvOverrides(config, merged, env);
|
|
791
|
+
const intraMap = buildIntraStackScriptNameMap(config, env, naming);
|
|
792
|
+
merged = rewriteIntraStackServiceTargets(merged, intraMap);
|
|
793
|
+
return resolveCrossResourceReferences(merged, {
|
|
794
|
+
config,
|
|
795
|
+
env,
|
|
796
|
+
state,
|
|
797
|
+
naming,
|
|
798
|
+
workerKey,
|
|
799
|
+
tolerant: opts.referencesMode === "tolerant",
|
|
800
|
+
imports: opts.imports,
|
|
801
|
+
accountId
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async function resolveWorkerConfig(config, workerKey, workerConfig, env, baseDir, accountId, naming, state, opts = {}) {
|
|
805
|
+
const merged = mergeWorkerConfigWithResolvedRefs(config, workerKey, workerConfig, env, accountId, naming, state, opts);
|
|
806
|
+
const workerDir = workerConfig.path ? resolve(baseDir, workerConfig.path) : baseDir;
|
|
807
|
+
const m = merged;
|
|
808
|
+
const workerName = resolveDeployedWorkerName(config, workerKey, merged, env, naming);
|
|
809
|
+
const wranglerOutFile = assertSafeWranglerOutFile(m.wranglerOutFile?.trim() || "wrangler.json");
|
|
810
|
+
const dispatchNamespace = m.dispatchNamespace?.trim() || void 0;
|
|
811
|
+
const stripped = stripTamerFields(merged);
|
|
812
|
+
const tamerRoutes = merged.tamerRoutes;
|
|
813
|
+
const expandedRoutes = effectiveRoutesForEnv(tamerRoutes, env);
|
|
814
|
+
const apiManagedRoutes = expandedRoutes.filter(isApiManagedZoneRoute);
|
|
815
|
+
const wranglerTamerRoutes = expandedRoutes.filter(isWranglerOnlyTamerRoute);
|
|
816
|
+
const mergedRoutes = [...stripped.routes ?? [], ...wranglerTamerRoutes];
|
|
817
|
+
/** Non-local deploys emit `routes: []` when there are none — omitting `routes` can leave stale Wrangler-published custom domains attached from a prior config. */
|
|
818
|
+
const wranglerRoutes = mergedRoutes.length > 0 ? mergedRoutes : env === "local" ? void 0 : [];
|
|
819
|
+
return {
|
|
820
|
+
workerKey,
|
|
821
|
+
workerName,
|
|
822
|
+
workerDir,
|
|
823
|
+
env,
|
|
824
|
+
wranglerOutFile,
|
|
825
|
+
dispatchNamespace,
|
|
826
|
+
wranglerConfig: {
|
|
827
|
+
...stripped,
|
|
828
|
+
...wranglerRoutes !== void 0 ? { routes: wranglerRoutes } : {},
|
|
829
|
+
name: workerName,
|
|
830
|
+
account_id: accountId,
|
|
831
|
+
compatibility_date: stripped.compatibility_date ?? config.compatibility_date
|
|
832
|
+
},
|
|
833
|
+
resources: merged.resources ?? workerConfig.resources ?? {},
|
|
834
|
+
apiManagedRoutes
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/features/d1/d1.ownership.ts
|
|
840
|
+
function d1IsExternal(config) {
|
|
841
|
+
return config.ownership === "external";
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Skip provision / migrate / Cloudflare delete for this binding.
|
|
845
|
+
* `preserveOnDestroy` alone keeps legacy "same derived name as owner" behaviour.
|
|
846
|
+
*/
|
|
847
|
+
function d1SkipsProvisionAndMigrate(config) {
|
|
848
|
+
return d1IsExternal(config) || config.preserveOnDestroy === true;
|
|
849
|
+
}
|
|
850
|
+
/** Cloudflare D1 database name for state keys, sync, and wrangler `database_name`. */
|
|
851
|
+
function d1CloudflareDatabaseName(config, env, naming) {
|
|
852
|
+
if (config.type !== "single") throw new Error(`D1 "${config.logicalName}": expected type "single" for derived database name`);
|
|
853
|
+
if (d1IsExternal(config)) {
|
|
854
|
+
const n = config.databaseName;
|
|
855
|
+
if (typeof n !== "string" || !n.trim()) throw new Error(`D1 "${config.logicalName}" has ownership "external" but databaseName is empty — ensure imports are pre-fetched and config is merged with resolveWorkerConfig / mergeWorkerConfigWithResolvedRefs.`);
|
|
856
|
+
return n.trim();
|
|
857
|
+
}
|
|
858
|
+
return naming.d1SingleName(config.logicalName, env);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/features/d1/d1.apply.ts
|
|
863
|
+
function todayNoDashes() {
|
|
864
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
865
|
+
}
|
|
866
|
+
function todayIso$1() {
|
|
867
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
868
|
+
}
|
|
869
|
+
async function d1Apply(resources, tenant, env, api, state, naming, addShard) {
|
|
870
|
+
for (const config of resources) {
|
|
871
|
+
if (config.type === "single") {
|
|
872
|
+
const derivedName$1 = d1CloudflareDatabaseName(config, env, naming);
|
|
873
|
+
if (state.get(derivedName$1)) continue;
|
|
874
|
+
if (d1SkipsProvisionAndMigrate(config)) {
|
|
875
|
+
console.warn(`[apply] Skipping D1 create for "${config.logicalName}" (external / preserveOnDestroy). Run \`tamer sync\` after the owning stack has created ${derivedName$1}.`);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const { uuid: uuid$1 } = await api.d1Create(derivedName$1);
|
|
879
|
+
state.set(derivedName$1, {
|
|
880
|
+
type: "d1_database",
|
|
881
|
+
logicalName: config.logicalName,
|
|
882
|
+
derivedName: derivedName$1,
|
|
883
|
+
bindingKey: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
|
|
884
|
+
cfId: uuid$1,
|
|
885
|
+
migrationsDir: config.migrationsDir,
|
|
886
|
+
preserveOnDestroy: false,
|
|
887
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
888
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
889
|
+
});
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (addShard && addShard !== config.logicalName) continue;
|
|
893
|
+
if (addShard && addShard === config.logicalName) {
|
|
894
|
+
const shardDate$1 = todayNoDashes();
|
|
895
|
+
const derivedName$1 = naming.d1ShardName(config.logicalName, shardDate$1, env);
|
|
896
|
+
if (state.get(derivedName$1)) continue;
|
|
897
|
+
const { uuid: uuid$1 } = await api.d1Create(derivedName$1);
|
|
898
|
+
state.set(derivedName$1, {
|
|
899
|
+
type: "d1_database",
|
|
900
|
+
logicalName: config.logicalName,
|
|
901
|
+
shardDate: todayIso$1(),
|
|
902
|
+
derivedName: derivedName$1,
|
|
903
|
+
bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate$1),
|
|
904
|
+
cfId: uuid$1,
|
|
905
|
+
migrationsDir: config.migrationsDir,
|
|
906
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
907
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
908
|
+
});
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const allResources = state.getAll();
|
|
912
|
+
if (Object.values(allResources).filter((e) => e.type === "d1_database" && "logicalName" in e && e.logicalName === config.logicalName).length > 0) continue;
|
|
913
|
+
const shardDate = todayNoDashes();
|
|
914
|
+
const derivedName = naming.d1ShardName(config.logicalName, shardDate, env);
|
|
915
|
+
const { uuid } = await api.d1Create(derivedName);
|
|
916
|
+
state.set(derivedName, {
|
|
917
|
+
type: "d1_database",
|
|
918
|
+
logicalName: config.logicalName,
|
|
919
|
+
shardDate: todayIso$1(),
|
|
920
|
+
derivedName,
|
|
921
|
+
bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate),
|
|
922
|
+
cfId: uuid,
|
|
923
|
+
migrationsDir: config.migrationsDir,
|
|
924
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
925
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/features/d1/d1.naming.ts
|
|
932
|
+
function d1MatchPattern(config, env, naming) {
|
|
933
|
+
if (config.type === "single") {
|
|
934
|
+
const exactName = naming.d1SingleName(config.logicalName, env);
|
|
935
|
+
return (name) => name === exactName;
|
|
936
|
+
}
|
|
937
|
+
return naming.d1MatchPattern(config.logicalName, env);
|
|
938
|
+
}
|
|
939
|
+
function d1ExtractShardDate(name, naming) {
|
|
940
|
+
return naming.extractD1ShardDate(name);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
//#endregion
|
|
944
|
+
//#region src/features/d1/d1.sync.ts
|
|
945
|
+
function d1Sync(allD1, resources, tenant, env, state, naming) {
|
|
946
|
+
for (const config of resources) {
|
|
947
|
+
const pattern = d1MatchPattern(config, env, naming);
|
|
948
|
+
if (config.type === "single") {
|
|
949
|
+
const derivedKey = d1CloudflareDatabaseName(config, env, naming);
|
|
950
|
+
const match = allD1.find((db) => db.name === derivedKey);
|
|
951
|
+
if (match) state.set(match.name, {
|
|
952
|
+
type: "d1_database",
|
|
953
|
+
logicalName: config.logicalName,
|
|
954
|
+
derivedName: match.name,
|
|
955
|
+
bindingKey: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
|
|
956
|
+
cfId: match.uuid,
|
|
957
|
+
migrationsDir: config.migrationsDir,
|
|
958
|
+
preserveOnDestroy: d1SkipsProvisionAndMigrate(config),
|
|
959
|
+
createdAt: match.created_at,
|
|
960
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
961
|
+
});
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const shards = allD1.filter((db) => pattern(db.name));
|
|
965
|
+
for (const shard of shards) {
|
|
966
|
+
const shardDate = d1ExtractShardDate(shard.name, naming);
|
|
967
|
+
if (!shardDate) continue;
|
|
968
|
+
state.set(shard.name, {
|
|
969
|
+
type: "d1_database",
|
|
970
|
+
logicalName: config.logicalName,
|
|
971
|
+
shardDate,
|
|
972
|
+
derivedName: shard.name,
|
|
973
|
+
bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate),
|
|
974
|
+
cfId: shard.uuid,
|
|
975
|
+
migrationsDir: config.migrationsDir,
|
|
976
|
+
createdAt: shard.created_at,
|
|
977
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/features/d1/d1.drift.ts
|
|
985
|
+
/**
|
|
986
|
+
* Diff D1 state against the Cloudflare list and the current config.
|
|
987
|
+
*
|
|
988
|
+
* - **single**: one expected derived name per resource.
|
|
989
|
+
* - **sharded**: every CF database matching the pattern is "expected"; state
|
|
990
|
+
* should contain an entry for each.
|
|
991
|
+
*/
|
|
992
|
+
function d1Drift(allD1, resources, env, state, naming) {
|
|
993
|
+
const drift = {
|
|
994
|
+
kind: "d1",
|
|
995
|
+
missingFromCloudflare: [],
|
|
996
|
+
unrecordedInState: [],
|
|
997
|
+
undeployed: []
|
|
998
|
+
};
|
|
999
|
+
const cfByName = new Map(allD1.map((db) => [db.name, db.uuid]));
|
|
1000
|
+
const allState = state.getAll();
|
|
1001
|
+
const d1State = Object.values(allState).filter((e) => e.type === "d1_database");
|
|
1002
|
+
for (const config of resources) {
|
|
1003
|
+
if (config.type === "single") {
|
|
1004
|
+
const derivedName = d1CloudflareDatabaseName(config, env, naming);
|
|
1005
|
+
const cfId = cfByName.get(derivedName);
|
|
1006
|
+
const stateEntry = d1State.find((e) => e.logicalName === config.logicalName && !e.shardDate);
|
|
1007
|
+
if (stateEntry && !cfId) drift.missingFromCloudflare.push(toEntry(stateEntry));
|
|
1008
|
+
else if (cfId && !stateEntry) drift.unrecordedInState.push({
|
|
1009
|
+
logicalName: config.logicalName,
|
|
1010
|
+
derivedName,
|
|
1011
|
+
cfId
|
|
1012
|
+
});
|
|
1013
|
+
else if (!cfId && !stateEntry) drift.undeployed.push({
|
|
1014
|
+
logicalName: config.logicalName,
|
|
1015
|
+
derivedName
|
|
1016
|
+
});
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const pattern = naming.d1MatchPattern(config.logicalName, env);
|
|
1020
|
+
const cfShards = allD1.filter((db) => pattern(db.name));
|
|
1021
|
+
const cfShardNames = new Set(cfShards.map((s) => s.name));
|
|
1022
|
+
const stateShards = d1State.filter((e) => e.logicalName === config.logicalName && !!e.shardDate);
|
|
1023
|
+
const stateShardNames = new Set(stateShards.map((s) => s.derivedName));
|
|
1024
|
+
for (const shard of stateShards) if (!cfShardNames.has(shard.derivedName)) drift.missingFromCloudflare.push(toEntry(shard));
|
|
1025
|
+
for (const shard of cfShards) if (!stateShardNames.has(shard.name)) drift.unrecordedInState.push({
|
|
1026
|
+
logicalName: config.logicalName,
|
|
1027
|
+
derivedName: shard.name,
|
|
1028
|
+
cfId: shard.uuid,
|
|
1029
|
+
detail: d1ExtractShardDate(shard.name, naming) ?? void 0
|
|
1030
|
+
});
|
|
1031
|
+
if (cfShards.length === 0 && stateShards.length === 0) drift.undeployed.push({
|
|
1032
|
+
logicalName: config.logicalName,
|
|
1033
|
+
derivedName: `(no shards for ${config.logicalName})`
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return drift;
|
|
1037
|
+
}
|
|
1038
|
+
function toEntry(e) {
|
|
1039
|
+
return {
|
|
1040
|
+
logicalName: e.logicalName,
|
|
1041
|
+
derivedName: e.derivedName,
|
|
1042
|
+
cfId: e.cfId,
|
|
1043
|
+
detail: e.shardDate
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/core/config/resourcesFromConfig.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* Logical resource names declared on workers in this config (for
|
|
1051
|
+
* stack-scoped destroy / drift). Iterates the resource registry — adding a
|
|
1052
|
+
* new kind doesn't require editing this file.
|
|
1053
|
+
*/
|
|
1054
|
+
async function logicalNamesForResourceKind(config, baseDir, kind) {
|
|
1055
|
+
const mod = getResourceModule(kind);
|
|
1056
|
+
if (!mod) return /* @__PURE__ */ new Set();
|
|
1057
|
+
const workers = await getWorkers(config, baseDir);
|
|
1058
|
+
const set = /* @__PURE__ */ new Set();
|
|
1059
|
+
for (const [, wc] of workers) for (const r of mod.pickResources(wc)) {
|
|
1060
|
+
const logical = r.logicalName;
|
|
1061
|
+
if (logical) set.add(logical);
|
|
1062
|
+
}
|
|
1063
|
+
return set;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/features/d1/d1.destroy.ts
|
|
1068
|
+
async function d1Destroy(_env, state, api, config, baseDir, _force) {
|
|
1069
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "d1");
|
|
1070
|
+
const resources = state.getAll();
|
|
1071
|
+
const d1Entries = Object.values(resources).filter((e) => e.type === "d1_database");
|
|
1072
|
+
for (const entry of d1Entries) {
|
|
1073
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
1074
|
+
if (entry.preserveOnDestroy) {
|
|
1075
|
+
console.log(`Skipping D1 destroy for ${entry.derivedName} (preserveOnDestroy / cross-stack binding).`);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
await api.d1Delete(entry.cfId);
|
|
1080
|
+
state.delete(entry.derivedName);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
console.warn(`Failed to delete D1 ${entry.derivedName}:`, err);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/features/d1/d1.generate.ts
|
|
1089
|
+
function d1Generate(resources, env, state, naming) {
|
|
1090
|
+
const bindings = [];
|
|
1091
|
+
for (const config of resources) {
|
|
1092
|
+
if (config.type === "single") {
|
|
1093
|
+
const derivedName = d1CloudflareDatabaseName(config, env, naming);
|
|
1094
|
+
const entry = state.get(derivedName);
|
|
1095
|
+
if (!entry || entry.type !== "d1_database") {
|
|
1096
|
+
if (d1SkipsProvisionAndMigrate(config)) throw new Error(`Cross-stack D1 "${config.logicalName}" (${derivedName}) not in state. Deploy the owning stack first, then re-run from this stack (apply now syncs automatically for env "${env}").`);
|
|
1097
|
+
throw new Error(`D1 "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
1098
|
+
}
|
|
1099
|
+
bindings.push({
|
|
1100
|
+
binding: entry.bindingKey,
|
|
1101
|
+
database_id: entry.cfId,
|
|
1102
|
+
database_name: derivedName,
|
|
1103
|
+
migrations_dir: config.migrationsDir,
|
|
1104
|
+
migrations_table: config.migrationsTable
|
|
1105
|
+
});
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const allResources = state.getAll();
|
|
1109
|
+
const shards = Object.values(allResources).filter((e) => e.type === "d1_database" && e.logicalName === config.logicalName);
|
|
1110
|
+
for (const shard of shards) bindings.push({
|
|
1111
|
+
binding: shard.bindingKey,
|
|
1112
|
+
database_id: shard.cfId,
|
|
1113
|
+
database_name: shard.derivedName,
|
|
1114
|
+
migrations_dir: config.migrationsDir,
|
|
1115
|
+
migrations_table: config.migrationsTable
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
return bindings;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/features/d1/d1.status.ts
|
|
1123
|
+
function d1Status(resources, env, state, naming) {
|
|
1124
|
+
const results = [];
|
|
1125
|
+
for (const config of resources) {
|
|
1126
|
+
if (config.type === "single") {
|
|
1127
|
+
const derivedName = d1CloudflareDatabaseName(config, env, naming);
|
|
1128
|
+
const entry = state.get(derivedName);
|
|
1129
|
+
results.push({
|
|
1130
|
+
binding: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
|
|
1131
|
+
name: derivedName,
|
|
1132
|
+
cfId: entry?.cfId ?? "",
|
|
1133
|
+
status: entry ? "ok" : "missing"
|
|
1134
|
+
});
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const allResources = state.getAll();
|
|
1138
|
+
const shards = Object.values(allResources).filter((e) => e.type === "d1_database" && e.logicalName === config.logicalName);
|
|
1139
|
+
for (const shard of shards) results.push({
|
|
1140
|
+
binding: shard.bindingKey,
|
|
1141
|
+
name: shard.derivedName,
|
|
1142
|
+
cfId: shard.cfId,
|
|
1143
|
+
status: "ok"
|
|
1144
|
+
});
|
|
1145
|
+
if (shards.length === 0) results.push({
|
|
1146
|
+
binding: naming.d1ShardBindingKey(config.logicalName, "00000000"),
|
|
1147
|
+
name: `(no shards for ${config.logicalName})`,
|
|
1148
|
+
cfId: "",
|
|
1149
|
+
status: "missing"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return results;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/core/registry/pickResources.ts
|
|
1157
|
+
/**
|
|
1158
|
+
* Helper used by every {@link ResourceModule.pickResources} so module files
|
|
1159
|
+
* can accept either a full `WorkerConfig` (the common path from `apply`,
|
|
1160
|
+
* `sync`, `drift`, `status`) or a bare `WorkerResources` (the wrangler
|
|
1161
|
+
* generator passes `resolved.resources` directly). Returns `undefined`
|
|
1162
|
+
* when no resource map is present.
|
|
1163
|
+
*/
|
|
1164
|
+
function resourcesFrom(source) {
|
|
1165
|
+
if (!source) return void 0;
|
|
1166
|
+
if ("resources" in source) return source.resources;
|
|
1167
|
+
return source;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/features/d1/d1.module.ts
|
|
1172
|
+
const d1Module = {
|
|
1173
|
+
kind: "d1",
|
|
1174
|
+
label: "D1",
|
|
1175
|
+
configKey: "d1",
|
|
1176
|
+
stateEntryType: "d1_database",
|
|
1177
|
+
async fetchAll(api) {
|
|
1178
|
+
return api.d1ListAll();
|
|
1179
|
+
},
|
|
1180
|
+
async apply(ctx) {
|
|
1181
|
+
await d1Apply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming, ctx.addShard);
|
|
1182
|
+
},
|
|
1183
|
+
sync(ctx) {
|
|
1184
|
+
d1Sync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
1185
|
+
},
|
|
1186
|
+
drift(ctx) {
|
|
1187
|
+
return d1Drift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1188
|
+
},
|
|
1189
|
+
async destroy(ctx) {
|
|
1190
|
+
await d1Destroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
1191
|
+
},
|
|
1192
|
+
status(ctx) {
|
|
1193
|
+
return d1Status(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1194
|
+
},
|
|
1195
|
+
generate(ctx) {
|
|
1196
|
+
if (ctx.resources.length === 0) return {};
|
|
1197
|
+
const generated = d1Generate(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1198
|
+
return { d1_databases: [...ctx.passthrough?.d1_databases ?? [], ...generated] };
|
|
1199
|
+
},
|
|
1200
|
+
pickResources(source) {
|
|
1201
|
+
return resourcesFrom(source)?.d1 ?? [];
|
|
1202
|
+
},
|
|
1203
|
+
async destroyOne({ api, state, key, entry }) {
|
|
1204
|
+
if (entry.type !== "d1_database") return;
|
|
1205
|
+
try {
|
|
1206
|
+
await api.d1Delete(entry.cfId);
|
|
1207
|
+
state.delete(key);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
console.warn(`Rollback: failed to delete D1 ${entry.derivedName}:`, err);
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
1213
|
+
if (!options.cfId) throw new Error("import d1: --cf-id <uuid> is required");
|
|
1214
|
+
const hit = (await api.d1ListAll()).find((d) => d.uuid === options.cfId);
|
|
1215
|
+
if (!hit) throw new Error(`import d1: D1 database with uuid "${options.cfId}" not found in account`);
|
|
1216
|
+
const derivedName = options.shardDate ? naming.d1ShardName(options.logical, options.shardDate, env) : naming.d1SingleName(options.logical, env);
|
|
1217
|
+
if (hit.name !== derivedName) throw new Error(`import d1: cf name "${hit.name}" does not match derived "${derivedName}" — wrong --logical, missing --shard-date, or naming convention drift?`);
|
|
1218
|
+
const bindingKey = options.shardDate ? naming.d1ShardBindingKey(options.logical, options.shardDate) : naming.d1SingleBindingKey(options.logical);
|
|
1219
|
+
const existing = state.get(derivedName);
|
|
1220
|
+
if (existing && existing.type === "d1_database" && existing.cfId !== options.cfId) throw new Error(`import d1: state already tracks "${derivedName}" with a different cfId`);
|
|
1221
|
+
const entry = {
|
|
1222
|
+
type: "d1_database",
|
|
1223
|
+
logicalName: options.logical,
|
|
1224
|
+
shardDate: options.shardDate,
|
|
1225
|
+
derivedName,
|
|
1226
|
+
bindingKey,
|
|
1227
|
+
cfId: options.cfId,
|
|
1228
|
+
createdAt: existing?.type === "d1_database" ? existing.createdAt : ts,
|
|
1229
|
+
updatedAt: ts
|
|
1230
|
+
};
|
|
1231
|
+
state.set(derivedName, entry);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/features/r2/r2.apply.ts
|
|
1237
|
+
function existingR2ForLogical(state, logicalName) {
|
|
1238
|
+
for (const e of Object.values(state.getAll())) if (e.type === "r2_bucket" && e.logicalName === logicalName) return e;
|
|
1239
|
+
}
|
|
1240
|
+
function todayIso() {
|
|
1241
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1242
|
+
}
|
|
1243
|
+
function assertValidR2BucketName(name, logicalName) {
|
|
1244
|
+
const errors = [];
|
|
1245
|
+
if (name.length < 3 || name.length > 63) errors.push(`length must be 3-63 chars (got ${name.length})`);
|
|
1246
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name)) errors.push("must contain only lowercase letters, numbers, and hyphens, and start/end with a letter or number");
|
|
1247
|
+
if (/--/.test(name)) errors.push("must not contain consecutive hyphens");
|
|
1248
|
+
if (errors.length > 0) throw new Error(`Invalid R2 bucket name "${name}" (derived for logical resource "${logicalName}"): ${errors.join("; ")}. Adjust your naming.r2Bucket hook (e.g., replace underscores with hyphens and lowercase the result).`);
|
|
1249
|
+
}
|
|
1250
|
+
async function r2Apply(resources, tenant, env, api, state, naming) {
|
|
1251
|
+
for (const config of resources) {
|
|
1252
|
+
const derivedName = naming.r2BucketName(config.logicalName, env);
|
|
1253
|
+
if (state.get(derivedName)) {
|
|
1254
|
+
console.log(`R2: skip "${derivedName}" (already in state; logical "${config.logicalName}")`);
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const prior = existingR2ForLogical(state, config.logicalName);
|
|
1258
|
+
if (prior) {
|
|
1259
|
+
console.log(`R2: skip "${derivedName}" (logical "${config.logicalName}" already in state as "${prior.derivedName}")`);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
assertValidR2BucketName(derivedName, config.logicalName);
|
|
1263
|
+
await api.r2Create(derivedName);
|
|
1264
|
+
console.log(`R2: created "${derivedName}" (logical "${config.logicalName}")`);
|
|
1265
|
+
state.set(derivedName, {
|
|
1266
|
+
type: "r2_bucket",
|
|
1267
|
+
logicalName: config.logicalName,
|
|
1268
|
+
createdDate: todayIso(),
|
|
1269
|
+
derivedName,
|
|
1270
|
+
bindingKey: config.binding?.trim() || naming.r2BindingKey(config.logicalName),
|
|
1271
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1272
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/features/r2/r2.naming.ts
|
|
1279
|
+
function r2ExtractDate(name, naming) {
|
|
1280
|
+
return naming.extractR2Date(name);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
//#endregion
|
|
1284
|
+
//#region src/features/r2/r2.sync.ts
|
|
1285
|
+
function r2Sync(allR2, resources, tenant, env, state, naming) {
|
|
1286
|
+
for (const config of resources) {
|
|
1287
|
+
const pattern = naming.r2MatchPattern(config.logicalName, env);
|
|
1288
|
+
const match = allR2.find((b) => pattern(b.name));
|
|
1289
|
+
if (match) {
|
|
1290
|
+
const createdDate = r2ExtractDate(match.name, naming) ?? match.creation_date.slice(0, 10);
|
|
1291
|
+
const prev = state.get(match.name);
|
|
1292
|
+
state.set(match.name, {
|
|
1293
|
+
type: "r2_bucket",
|
|
1294
|
+
logicalName: config.logicalName,
|
|
1295
|
+
createdDate,
|
|
1296
|
+
derivedName: match.name,
|
|
1297
|
+
bindingKey: config.binding?.trim() || (prev?.type === "r2_bucket" && prev.bindingKey ? prev.bindingKey : naming.r2BindingKey(config.logicalName)),
|
|
1298
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1299
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region src/features/r2/r2.drift.ts
|
|
1307
|
+
/**
|
|
1308
|
+
* Diff R2 state against the Cloudflare list and the current config.
|
|
1309
|
+
*
|
|
1310
|
+
* R2 buckets carry a date stamp, so each declared resource matches at most one
|
|
1311
|
+
* bucket (the one whose name matches the logical-name pattern).
|
|
1312
|
+
*/
|
|
1313
|
+
function r2Drift(allR2, resources, env, state, naming) {
|
|
1314
|
+
const drift = {
|
|
1315
|
+
kind: "r2",
|
|
1316
|
+
missingFromCloudflare: [],
|
|
1317
|
+
unrecordedInState: [],
|
|
1318
|
+
undeployed: []
|
|
1319
|
+
};
|
|
1320
|
+
const cfNames = new Set(allR2.map((b) => b.name));
|
|
1321
|
+
const allState = state.getAll();
|
|
1322
|
+
const r2State = Object.values(allState).filter((e) => e.type === "r2_bucket");
|
|
1323
|
+
for (const config of resources) {
|
|
1324
|
+
const pattern = naming.r2MatchPattern(config.logicalName, env);
|
|
1325
|
+
const cfMatch = allR2.find((b) => pattern(b.name));
|
|
1326
|
+
const stateEntry = r2State.find((e) => e.logicalName === config.logicalName);
|
|
1327
|
+
if (stateEntry && !cfNames.has(stateEntry.derivedName)) {
|
|
1328
|
+
drift.missingFromCloudflare.push({
|
|
1329
|
+
logicalName: stateEntry.logicalName,
|
|
1330
|
+
derivedName: stateEntry.derivedName
|
|
1331
|
+
});
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (cfMatch && !stateEntry) {
|
|
1335
|
+
drift.unrecordedInState.push({
|
|
1336
|
+
logicalName: config.logicalName,
|
|
1337
|
+
derivedName: cfMatch.name
|
|
1338
|
+
});
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (!cfMatch && !stateEntry) drift.undeployed.push({
|
|
1342
|
+
logicalName: config.logicalName,
|
|
1343
|
+
derivedName: `(no bucket for ${config.logicalName})`
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
return drift;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/features/r2/r2.destroy.ts
|
|
1351
|
+
async function r2Destroy(_env, state, api, config, baseDir, _force) {
|
|
1352
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "r2");
|
|
1353
|
+
const resources = state.getAll();
|
|
1354
|
+
const r2Entries = Object.values(resources).filter((e) => e.type === "r2_bucket");
|
|
1355
|
+
const accountId = api.getAccountId();
|
|
1356
|
+
const s3creds = r2S3CredentialsFromEnv();
|
|
1357
|
+
for (const entry of r2Entries) {
|
|
1358
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
1359
|
+
const name = entry.derivedName;
|
|
1360
|
+
if (s3creds) try {
|
|
1361
|
+
console.log(`R2: emptying bucket "${name}" via S3 API (incomplete multipart uploads, then objects)…`);
|
|
1362
|
+
const { uploadsAborted, objectsDeleted } = await emptyR2BucketViaS3(accountId, name, s3creds);
|
|
1363
|
+
console.log(`R2: bucket "${name}" — aborted ${uploadsAborted} multipart upload(s), deleted ${objectsDeleted} object(s).`);
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
console.warn(`R2: S3 empty failed for "${name}" (fix credentials or use the dashboard; still attempting API bucket delete):`, err instanceof Error ? err.message : err);
|
|
1366
|
+
}
|
|
1367
|
+
else console.warn(`R2: skipping S3 empty for "${name}" (set R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY to remove all objects and incomplete multipart uploads before bucket delete). Create an R2 API token in the dashboard (R2 → Manage R2 API Tokens) with object read+write; those keys are not the same as CLOUDFLARE_API_TOKEN.`);
|
|
1368
|
+
try {
|
|
1369
|
+
await api.r2Delete(name);
|
|
1370
|
+
state.delete(name);
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
console.warn(`Failed to delete R2 ${name}:`, err);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/features/r2/r2.generate.ts
|
|
1379
|
+
function r2Generate(resources, env, state, naming) {
|
|
1380
|
+
const bindings = [];
|
|
1381
|
+
for (const config of resources) {
|
|
1382
|
+
const allResources = state.getAll();
|
|
1383
|
+
const entry = Object.values(allResources).find((e) => e.type === "r2_bucket" && e.logicalName === config.logicalName);
|
|
1384
|
+
if (!entry) throw new Error(`R2 "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
1385
|
+
bindings.push({
|
|
1386
|
+
binding: entry.bindingKey,
|
|
1387
|
+
bucket_name: entry.derivedName
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
return bindings;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
//#endregion
|
|
1394
|
+
//#region src/features/r2/r2.status.ts
|
|
1395
|
+
function r2Status(resources, env, state, naming) {
|
|
1396
|
+
const results = [];
|
|
1397
|
+
for (const config of resources) {
|
|
1398
|
+
const allResources = state.getAll();
|
|
1399
|
+
const entry = Object.values(allResources).find((e) => e.type === "r2_bucket" && e.logicalName === config.logicalName);
|
|
1400
|
+
results.push({
|
|
1401
|
+
binding: entry?.bindingKey ?? naming.r2BindingKey(config.logicalName),
|
|
1402
|
+
name: entry?.derivedName ?? naming.r2BucketName(config.logicalName, env),
|
|
1403
|
+
status: entry ? "ok" : "missing"
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
return results;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/features/r2/r2.module.ts
|
|
1411
|
+
const r2Module = {
|
|
1412
|
+
kind: "r2",
|
|
1413
|
+
label: "R2",
|
|
1414
|
+
configKey: "r2",
|
|
1415
|
+
stateEntryType: "r2_bucket",
|
|
1416
|
+
async fetchAll(api) {
|
|
1417
|
+
return api.r2ListAll();
|
|
1418
|
+
},
|
|
1419
|
+
async apply(ctx) {
|
|
1420
|
+
await r2Apply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
1421
|
+
},
|
|
1422
|
+
sync(ctx) {
|
|
1423
|
+
r2Sync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
1424
|
+
},
|
|
1425
|
+
drift(ctx) {
|
|
1426
|
+
return r2Drift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1427
|
+
},
|
|
1428
|
+
async destroy(ctx) {
|
|
1429
|
+
await r2Destroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
1430
|
+
},
|
|
1431
|
+
status(ctx) {
|
|
1432
|
+
return r2Status(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1433
|
+
},
|
|
1434
|
+
generate(ctx) {
|
|
1435
|
+
if (ctx.resources.length === 0) return {};
|
|
1436
|
+
return { r2_buckets: r2Generate(ctx.resources, ctx.env, ctx.state, ctx.naming) };
|
|
1437
|
+
},
|
|
1438
|
+
pickResources(source) {
|
|
1439
|
+
return resourcesFrom(source)?.r2 ?? [];
|
|
1440
|
+
},
|
|
1441
|
+
async destroyOne({ api, state, key, entry }) {
|
|
1442
|
+
if (entry.type !== "r2_bucket") return;
|
|
1443
|
+
try {
|
|
1444
|
+
await api.r2Delete(entry.derivedName);
|
|
1445
|
+
state.delete(key);
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
console.warn(`Rollback: failed to delete R2 ${entry.derivedName}:`, err);
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
1451
|
+
const bucketName = options.cfId;
|
|
1452
|
+
if (!bucketName) throw new Error("import r2: --cf-id <bucket-name> is required");
|
|
1453
|
+
const hit = (await api.r2ListAll()).find((b) => b.name === bucketName);
|
|
1454
|
+
if (!hit) throw new Error(`import r2: bucket "${bucketName}" not found in account`);
|
|
1455
|
+
if (!naming.r2MatchPattern(options.logical, env)(hit.name)) throw new Error(`import r2: bucket name "${hit.name}" does not match expected pattern for logical "${options.logical}" and env "${env}"`);
|
|
1456
|
+
const derivedName = hit.name;
|
|
1457
|
+
const createdDate = options.createdDate ?? naming.extractR2Date(hit.name) ?? hit.creation_date.slice(0, 10);
|
|
1458
|
+
const bindingKey = naming.r2BindingKey(options.logical);
|
|
1459
|
+
const existing = state.get(derivedName);
|
|
1460
|
+
const entry = {
|
|
1461
|
+
type: "r2_bucket",
|
|
1462
|
+
logicalName: options.logical,
|
|
1463
|
+
derivedName,
|
|
1464
|
+
bindingKey,
|
|
1465
|
+
createdDate,
|
|
1466
|
+
createdAt: existing?.type === "r2_bucket" ? existing.createdAt : ts,
|
|
1467
|
+
updatedAt: ts
|
|
1468
|
+
};
|
|
1469
|
+
state.set(derivedName, entry);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/features/kv/kv.apply.ts
|
|
1475
|
+
async function kvApply(resources, tenant, env, api, state, naming) {
|
|
1476
|
+
for (const config of resources) {
|
|
1477
|
+
const derivedName = naming.kvNamespaceName(config.logicalName, env);
|
|
1478
|
+
if (state.get(derivedName)) continue;
|
|
1479
|
+
const { id } = await api.kvCreate(derivedName);
|
|
1480
|
+
state.set(derivedName, {
|
|
1481
|
+
type: "kv_namespace",
|
|
1482
|
+
logicalName: config.logicalName,
|
|
1483
|
+
derivedName,
|
|
1484
|
+
bindingKey: config.binding?.trim() || naming.kvBindingKey(config.logicalName),
|
|
1485
|
+
cfId: id,
|
|
1486
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1487
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region src/features/kv/kv.sync.ts
|
|
1494
|
+
function kvSync(allKV, resources, tenant, env, state, naming) {
|
|
1495
|
+
for (const config of resources) {
|
|
1496
|
+
const derivedName = naming.kvNamespaceName(config.logicalName, env);
|
|
1497
|
+
const match = allKV.find((ns) => ns.title === derivedName);
|
|
1498
|
+
if (match) state.set(derivedName, {
|
|
1499
|
+
type: "kv_namespace",
|
|
1500
|
+
logicalName: config.logicalName,
|
|
1501
|
+
derivedName,
|
|
1502
|
+
bindingKey: config.binding?.trim() || naming.kvBindingKey(config.logicalName),
|
|
1503
|
+
cfId: match.id,
|
|
1504
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1505
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
//#endregion
|
|
1511
|
+
//#region src/features/kv/kv.drift.ts
|
|
1512
|
+
function kvDrift(allKV, resources, env, state, naming) {
|
|
1513
|
+
const drift = {
|
|
1514
|
+
kind: "kv",
|
|
1515
|
+
missingFromCloudflare: [],
|
|
1516
|
+
unrecordedInState: [],
|
|
1517
|
+
undeployed: []
|
|
1518
|
+
};
|
|
1519
|
+
const cfByTitle = new Map(allKV.map((ns) => [ns.title, ns.id]));
|
|
1520
|
+
const allState = state.getAll();
|
|
1521
|
+
const kvState = Object.values(allState).filter((e) => e.type === "kv_namespace");
|
|
1522
|
+
for (const config of resources) {
|
|
1523
|
+
const derivedName = naming.kvNamespaceName(config.logicalName, env);
|
|
1524
|
+
const cfId = cfByTitle.get(derivedName);
|
|
1525
|
+
const stateEntry = kvState.find((e) => e.logicalName === config.logicalName);
|
|
1526
|
+
if (stateEntry && !cfId) drift.missingFromCloudflare.push({
|
|
1527
|
+
logicalName: stateEntry.logicalName,
|
|
1528
|
+
derivedName: stateEntry.derivedName,
|
|
1529
|
+
cfId: stateEntry.cfId
|
|
1530
|
+
});
|
|
1531
|
+
else if (cfId && !stateEntry) drift.unrecordedInState.push({
|
|
1532
|
+
logicalName: config.logicalName,
|
|
1533
|
+
derivedName,
|
|
1534
|
+
cfId
|
|
1535
|
+
});
|
|
1536
|
+
else if (!cfId && !stateEntry) drift.undeployed.push({
|
|
1537
|
+
logicalName: config.logicalName,
|
|
1538
|
+
derivedName
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return drift;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
//#endregion
|
|
1545
|
+
//#region src/features/kv/kv.destroy.ts
|
|
1546
|
+
async function kvDestroy(_env, state, api, config, baseDir, _force) {
|
|
1547
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "kv");
|
|
1548
|
+
const resources = state.getAll();
|
|
1549
|
+
const kvEntries = Object.values(resources).filter((e) => e.type === "kv_namespace");
|
|
1550
|
+
for (const entry of kvEntries) {
|
|
1551
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
1552
|
+
try {
|
|
1553
|
+
await api.kvDelete(entry.cfId);
|
|
1554
|
+
state.delete(entry.derivedName);
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
console.warn(`Failed to delete KV ${entry.derivedName}:`, err);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/features/kv/kv.generate.ts
|
|
1563
|
+
function kvGenerate(resources, env, state, naming) {
|
|
1564
|
+
const bindings = [];
|
|
1565
|
+
for (const config of resources) {
|
|
1566
|
+
const derivedName = naming.kvNamespaceName(config.logicalName, env);
|
|
1567
|
+
const entry = state.get(derivedName);
|
|
1568
|
+
if (!entry || entry.type !== "kv_namespace") throw new Error(`KV "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
1569
|
+
bindings.push({
|
|
1570
|
+
binding: entry.bindingKey,
|
|
1571
|
+
id: entry.cfId
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
return bindings;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/features/kv/kv.status.ts
|
|
1579
|
+
function kvStatus(resources, env, state, naming) {
|
|
1580
|
+
const results = [];
|
|
1581
|
+
for (const config of resources) {
|
|
1582
|
+
const derivedName = naming.kvNamespaceName(config.logicalName, env);
|
|
1583
|
+
const entry = state.get(derivedName);
|
|
1584
|
+
results.push({
|
|
1585
|
+
binding: naming.kvBindingKey(config.logicalName),
|
|
1586
|
+
name: derivedName,
|
|
1587
|
+
status: entry ? "ok" : "missing"
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
return results;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
//#endregion
|
|
1594
|
+
//#region src/features/kv/kv.module.ts
|
|
1595
|
+
const kvModule = {
|
|
1596
|
+
kind: "kv",
|
|
1597
|
+
label: "KV",
|
|
1598
|
+
configKey: "kv",
|
|
1599
|
+
stateEntryType: "kv_namespace",
|
|
1600
|
+
async fetchAll(api) {
|
|
1601
|
+
return (await api.kvListAll()).map((k) => ({
|
|
1602
|
+
id: k.id,
|
|
1603
|
+
title: k.title
|
|
1604
|
+
}));
|
|
1605
|
+
},
|
|
1606
|
+
async apply(ctx) {
|
|
1607
|
+
await kvApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
1608
|
+
},
|
|
1609
|
+
sync(ctx) {
|
|
1610
|
+
kvSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
1611
|
+
},
|
|
1612
|
+
drift(ctx) {
|
|
1613
|
+
return kvDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1614
|
+
},
|
|
1615
|
+
async destroy(ctx) {
|
|
1616
|
+
await kvDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
1617
|
+
},
|
|
1618
|
+
status(ctx) {
|
|
1619
|
+
return kvStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1620
|
+
},
|
|
1621
|
+
generate(ctx) {
|
|
1622
|
+
if (ctx.resources.length === 0) return {};
|
|
1623
|
+
return { kv_namespaces: kvGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) };
|
|
1624
|
+
},
|
|
1625
|
+
pickResources(source) {
|
|
1626
|
+
return resourcesFrom(source)?.kv ?? [];
|
|
1627
|
+
},
|
|
1628
|
+
async destroyOne({ api, state, key, entry }) {
|
|
1629
|
+
if (entry.type !== "kv_namespace") return;
|
|
1630
|
+
try {
|
|
1631
|
+
await api.kvDelete(entry.cfId);
|
|
1632
|
+
state.delete(key);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
console.warn(`Rollback: failed to delete KV ${entry.derivedName}:`, err);
|
|
1635
|
+
}
|
|
1636
|
+
},
|
|
1637
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
1638
|
+
if (!options.cfId) throw new Error("import kv: --cf-id <namespace-id> is required");
|
|
1639
|
+
const hit = (await api.kvListAll()).find((k) => k.id === options.cfId);
|
|
1640
|
+
if (!hit) throw new Error(`import kv: KV namespace id "${options.cfId}" not found in account`);
|
|
1641
|
+
const derivedName = naming.kvNamespaceName(options.logical, env);
|
|
1642
|
+
if (hit.title !== derivedName) throw new Error(`import kv: cf title "${hit.title}" does not match derived "${derivedName}"`);
|
|
1643
|
+
const bindingKey = naming.kvBindingKey(options.logical);
|
|
1644
|
+
const existing = state.get(derivedName);
|
|
1645
|
+
const entry = {
|
|
1646
|
+
type: "kv_namespace",
|
|
1647
|
+
logicalName: options.logical,
|
|
1648
|
+
derivedName,
|
|
1649
|
+
bindingKey,
|
|
1650
|
+
cfId: options.cfId,
|
|
1651
|
+
createdAt: existing?.type === "kv_namespace" ? existing.createdAt : ts,
|
|
1652
|
+
updatedAt: ts
|
|
1653
|
+
};
|
|
1654
|
+
state.set(derivedName, entry);
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
//#endregion
|
|
1659
|
+
//#region src/features/queues/queues.apply.ts
|
|
1660
|
+
const STATE_KEY_PREFIX$6 = "queue:";
|
|
1661
|
+
function stateKey$6(derivedName) {
|
|
1662
|
+
return `${STATE_KEY_PREFIX$6}${derivedName}`;
|
|
1663
|
+
}
|
|
1664
|
+
async function queuesApply(resources, _tenant, env, api, state, naming) {
|
|
1665
|
+
for (const config of resources) {
|
|
1666
|
+
const derivedName = naming.queueName(config.logicalName, env);
|
|
1667
|
+
const key = stateKey$6(derivedName);
|
|
1668
|
+
if (state.get(key)) continue;
|
|
1669
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1670
|
+
const { queue_id } = await api.queueCreate(derivedName);
|
|
1671
|
+
state.set(key, {
|
|
1672
|
+
type: "queue",
|
|
1673
|
+
logicalName: config.logicalName,
|
|
1674
|
+
derivedName,
|
|
1675
|
+
bindingKey: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
|
|
1676
|
+
cfId: queue_id,
|
|
1677
|
+
producerBinding: !config.consumerOnly,
|
|
1678
|
+
createdAt: ts,
|
|
1679
|
+
updatedAt: ts
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
//#endregion
|
|
1685
|
+
//#region src/features/queues/queues.sync.ts
|
|
1686
|
+
function queuesSync(allQueues, resources, _tenant, env, state, naming) {
|
|
1687
|
+
for (const config of resources) {
|
|
1688
|
+
const derivedName = naming.queueName(config.logicalName, env);
|
|
1689
|
+
const match = allQueues.find((q) => q.queue_name === derivedName);
|
|
1690
|
+
if (!match) continue;
|
|
1691
|
+
const key = `queue:${derivedName}`;
|
|
1692
|
+
const existing = state.get(key);
|
|
1693
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1694
|
+
state.set(key, {
|
|
1695
|
+
type: "queue",
|
|
1696
|
+
logicalName: config.logicalName,
|
|
1697
|
+
derivedName,
|
|
1698
|
+
bindingKey: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
|
|
1699
|
+
cfId: match.queue_id,
|
|
1700
|
+
producerBinding: !config.consumerOnly,
|
|
1701
|
+
createdAt: existing?.type === "queue" ? existing.createdAt : ts,
|
|
1702
|
+
updatedAt: ts
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/features/queues/queues.drift.ts
|
|
1709
|
+
function queuesDrift(allQueues, resources, env, state, naming) {
|
|
1710
|
+
const drift = {
|
|
1711
|
+
kind: "queue",
|
|
1712
|
+
missingFromCloudflare: [],
|
|
1713
|
+
unrecordedInState: [],
|
|
1714
|
+
undeployed: []
|
|
1715
|
+
};
|
|
1716
|
+
const cfByName = new Map(allQueues.map((q) => [q.queue_name, q.queue_id]));
|
|
1717
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "queue");
|
|
1718
|
+
for (const config of resources) {
|
|
1719
|
+
const derivedName = naming.queueName(config.logicalName, env);
|
|
1720
|
+
const cfId = cfByName.get(derivedName);
|
|
1721
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
1722
|
+
if (stateEntry && !cfId) drift.missingFromCloudflare.push({
|
|
1723
|
+
logicalName: stateEntry.logicalName,
|
|
1724
|
+
derivedName: stateEntry.derivedName,
|
|
1725
|
+
cfId: stateEntry.cfId
|
|
1726
|
+
});
|
|
1727
|
+
else if (cfId && !stateEntry) drift.unrecordedInState.push({
|
|
1728
|
+
logicalName: config.logicalName,
|
|
1729
|
+
derivedName,
|
|
1730
|
+
cfId
|
|
1731
|
+
});
|
|
1732
|
+
else if (!cfId && !stateEntry) drift.undeployed.push({
|
|
1733
|
+
logicalName: config.logicalName,
|
|
1734
|
+
derivedName
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
return drift;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
//#endregion
|
|
1741
|
+
//#region src/features/queues/queues.destroy.ts
|
|
1742
|
+
async function queuesDestroy(_env, state, api, config, baseDir, _force) {
|
|
1743
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "queue");
|
|
1744
|
+
const resources = state.getAll();
|
|
1745
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "queue");
|
|
1746
|
+
for (const [key, entry] of entries) {
|
|
1747
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
1748
|
+
try {
|
|
1749
|
+
await api.queueDelete(entry.cfId);
|
|
1750
|
+
state.delete(key);
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
console.warn(`Failed to delete queue ${entry.derivedName}:`, err);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
//#endregion
|
|
1758
|
+
//#region src/features/queues/queues.generate.ts
|
|
1759
|
+
/**
|
|
1760
|
+
* Emit `queues.producers[]` for every declared queue (skips entries marked
|
|
1761
|
+
* `consumerOnly`). Consumers stay wrangler-side and can be set on the worker
|
|
1762
|
+
* config under `queues.consumers` (passed through unchanged).
|
|
1763
|
+
*/
|
|
1764
|
+
function queuesGenerate(resources, env, state, naming) {
|
|
1765
|
+
const producers = [];
|
|
1766
|
+
for (const config of resources) {
|
|
1767
|
+
if (config.consumerOnly) continue;
|
|
1768
|
+
const derivedName = naming.queueName(config.logicalName, env);
|
|
1769
|
+
const entry = state.get(`queue:${derivedName}`);
|
|
1770
|
+
if (!entry || entry.type !== "queue") throw new Error(`Queue "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
1771
|
+
producers.push({
|
|
1772
|
+
binding: entry.bindingKey,
|
|
1773
|
+
queue: entry.derivedName
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
if (producers.length === 0) return void 0;
|
|
1777
|
+
return { producers };
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/features/queues/queues.status.ts
|
|
1782
|
+
function queuesStatus(resources, env, state, naming) {
|
|
1783
|
+
const out = [];
|
|
1784
|
+
for (const config of resources) {
|
|
1785
|
+
const derivedName = naming.queueName(config.logicalName, env);
|
|
1786
|
+
const entry = state.get(`queue:${derivedName}`);
|
|
1787
|
+
out.push({
|
|
1788
|
+
binding: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
|
|
1789
|
+
name: derivedName,
|
|
1790
|
+
status: entry ? "ok" : "missing"
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
return out;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
//#endregion
|
|
1797
|
+
//#region src/features/queues/queues.module.ts
|
|
1798
|
+
const queuesModule = {
|
|
1799
|
+
kind: "queue",
|
|
1800
|
+
label: "Queues",
|
|
1801
|
+
configKey: "queues",
|
|
1802
|
+
stateEntryType: "queue",
|
|
1803
|
+
async fetchAll(api) {
|
|
1804
|
+
return (await api.queuesListAll()).map((q) => ({
|
|
1805
|
+
queue_id: q.queue_id,
|
|
1806
|
+
queue_name: q.queue_name
|
|
1807
|
+
}));
|
|
1808
|
+
},
|
|
1809
|
+
async apply(ctx) {
|
|
1810
|
+
await queuesApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
1811
|
+
},
|
|
1812
|
+
sync(ctx) {
|
|
1813
|
+
queuesSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
1814
|
+
},
|
|
1815
|
+
drift(ctx) {
|
|
1816
|
+
return queuesDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1817
|
+
},
|
|
1818
|
+
async destroy(ctx) {
|
|
1819
|
+
await queuesDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
1820
|
+
},
|
|
1821
|
+
status(ctx) {
|
|
1822
|
+
return queuesStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
1823
|
+
},
|
|
1824
|
+
generate(ctx) {
|
|
1825
|
+
const generated = ctx.resources.length > 0 ? queuesGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : void 0;
|
|
1826
|
+
const passthrough = ctx.passthrough?.queues;
|
|
1827
|
+
if (!generated && !passthrough) return {};
|
|
1828
|
+
const producers = [...passthrough?.producers ?? [], ...generated?.producers ?? []];
|
|
1829
|
+
const consumers = passthrough?.consumers ?? [];
|
|
1830
|
+
if (producers.length === 0 && consumers.length === 0) return {};
|
|
1831
|
+
const out = {};
|
|
1832
|
+
if (producers.length > 0) out.producers = producers;
|
|
1833
|
+
if (consumers.length > 0) out.consumers = consumers;
|
|
1834
|
+
return { queues: out };
|
|
1835
|
+
},
|
|
1836
|
+
pickResources(source) {
|
|
1837
|
+
return resourcesFrom(source)?.queues ?? [];
|
|
1838
|
+
},
|
|
1839
|
+
async destroyOne({ api, state, key, entry }) {
|
|
1840
|
+
if (entry.type !== "queue") return;
|
|
1841
|
+
try {
|
|
1842
|
+
await api.queueDelete(entry.cfId);
|
|
1843
|
+
state.delete(key);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
console.warn(`Rollback: failed to delete queue ${entry.derivedName}:`, err);
|
|
1846
|
+
}
|
|
1847
|
+
},
|
|
1848
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
1849
|
+
if (!options.cfId) throw new Error("import queue: --cf-id <queue-id> is required");
|
|
1850
|
+
const hit = (await api.queuesListAll()).find((q) => q.queue_id === options.cfId);
|
|
1851
|
+
if (!hit) throw new Error(`import queue: queue id "${options.cfId}" not found in account`);
|
|
1852
|
+
const derivedName = naming.queueName(options.logical, env);
|
|
1853
|
+
if (hit.queue_name !== derivedName) throw new Error(`import queue: cf name "${hit.queue_name}" does not match derived "${derivedName}"`);
|
|
1854
|
+
const bindingKey = naming.queueBindingKey(options.logical);
|
|
1855
|
+
const key = `queue:${derivedName}`;
|
|
1856
|
+
const existing = state.get(key);
|
|
1857
|
+
const entry = {
|
|
1858
|
+
type: "queue",
|
|
1859
|
+
logicalName: options.logical,
|
|
1860
|
+
derivedName,
|
|
1861
|
+
bindingKey,
|
|
1862
|
+
cfId: options.cfId,
|
|
1863
|
+
producerBinding: existing?.type === "queue" ? existing.producerBinding : true,
|
|
1864
|
+
createdAt: existing?.type === "queue" ? existing.createdAt : ts,
|
|
1865
|
+
updatedAt: ts
|
|
1866
|
+
};
|
|
1867
|
+
state.set(key, entry);
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
//#endregion
|
|
1872
|
+
//#region src/features/hyperdrive/hyperdrive.secrets.ts
|
|
1873
|
+
/**
|
|
1874
|
+
* Resolve a value that is either a literal string or `{ fromEnv: "VAR" }`
|
|
1875
|
+
* lookup against `process.env`. Throws a clear error when the env var is
|
|
1876
|
+
* missing — these values are sent to Cloudflare on create and never
|
|
1877
|
+
* persisted in Tamer state.
|
|
1878
|
+
*/
|
|
1879
|
+
function resolveSecret(value, label) {
|
|
1880
|
+
if (typeof value === "string") return value;
|
|
1881
|
+
const envName = value.fromEnv;
|
|
1882
|
+
const v = process.env[envName];
|
|
1883
|
+
if (!v) throw new Error(`hyperdrive: ${label} requires env var ${envName} (set it before running 'tamer apply')`);
|
|
1884
|
+
return v;
|
|
1885
|
+
}
|
|
1886
|
+
function resolveHyperdriveOrigin(config) {
|
|
1887
|
+
const o = config.origin;
|
|
1888
|
+
return {
|
|
1889
|
+
scheme: o.scheme,
|
|
1890
|
+
host: o.host,
|
|
1891
|
+
port: o.port,
|
|
1892
|
+
database: o.database,
|
|
1893
|
+
user: o.user,
|
|
1894
|
+
password: resolveSecret(o.password, `${config.logicalName}.origin.password`),
|
|
1895
|
+
access_client_id: o.access_client_id,
|
|
1896
|
+
access_client_secret: o.access_client_secret ? resolveSecret(o.access_client_secret, `${config.logicalName}.origin.access_client_secret`) : void 0
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
//#endregion
|
|
1901
|
+
//#region src/features/hyperdrive/hyperdrive.diff.ts
|
|
1902
|
+
function hyperdriveDiffPlanItems(args) {
|
|
1903
|
+
const { resources, env, state, naming } = args;
|
|
1904
|
+
const items = [];
|
|
1905
|
+
for (const config of resources) {
|
|
1906
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
1907
|
+
const entry = state.get(`hyperdrive:${derivedName}`);
|
|
1908
|
+
if (!entry || entry.type !== "hyperdrive") continue;
|
|
1909
|
+
const changes = computeChanges$1(entry, config);
|
|
1910
|
+
if (changes.length === 0) continue;
|
|
1911
|
+
items.push({
|
|
1912
|
+
kind: "hyperdrive",
|
|
1913
|
+
action: "update",
|
|
1914
|
+
logicalName: config.logicalName,
|
|
1915
|
+
derivedName,
|
|
1916
|
+
detail: changes.map((c) => `${c.field}: ${formatVal$2(c.from)} -> ${formatVal$2(c.to)}`).join(", "),
|
|
1917
|
+
changes
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
return items;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Pure comparison shared with `hyperdriveApply` so plan and apply agree
|
|
1924
|
+
* on what counts as drift on the origin fields Tamer persists.
|
|
1925
|
+
*/
|
|
1926
|
+
function computeHyperdriveChanges(state, config) {
|
|
1927
|
+
return computeChanges$1(state, config);
|
|
1928
|
+
}
|
|
1929
|
+
function computeChanges$1(state, config) {
|
|
1930
|
+
const changes = [];
|
|
1931
|
+
if (state.scheme !== config.origin.scheme) changes.push({
|
|
1932
|
+
field: "origin.scheme",
|
|
1933
|
+
from: state.scheme,
|
|
1934
|
+
to: config.origin.scheme,
|
|
1935
|
+
kind: "mutable"
|
|
1936
|
+
});
|
|
1937
|
+
if (state.originHost !== config.origin.host) changes.push({
|
|
1938
|
+
field: "origin.host",
|
|
1939
|
+
from: state.originHost,
|
|
1940
|
+
to: config.origin.host,
|
|
1941
|
+
kind: "mutable"
|
|
1942
|
+
});
|
|
1943
|
+
if (state.originDatabase !== config.origin.database) changes.push({
|
|
1944
|
+
field: "origin.database",
|
|
1945
|
+
from: state.originDatabase,
|
|
1946
|
+
to: config.origin.database,
|
|
1947
|
+
kind: "mutable"
|
|
1948
|
+
});
|
|
1949
|
+
return changes;
|
|
1950
|
+
}
|
|
1951
|
+
function formatVal$2(v) {
|
|
1952
|
+
if (v === void 0) return "(unset)";
|
|
1953
|
+
if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
|
|
1954
|
+
return String(v);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
//#endregion
|
|
1958
|
+
//#region src/features/hyperdrive/hyperdrive.apply.ts
|
|
1959
|
+
const STATE_KEY_PREFIX$5 = "hyperdrive:";
|
|
1960
|
+
function stateKey$5(derivedName) {
|
|
1961
|
+
return `${STATE_KEY_PREFIX$5}${derivedName}`;
|
|
1962
|
+
}
|
|
1963
|
+
async function hyperdriveApply(resources, _tenant, env, api, state, naming) {
|
|
1964
|
+
for (const config of resources) {
|
|
1965
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
1966
|
+
const key = stateKey$5(derivedName);
|
|
1967
|
+
const existing = state.get(key);
|
|
1968
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1969
|
+
if (existing && existing.type === "hyperdrive") {
|
|
1970
|
+
const stateEntry = existing;
|
|
1971
|
+
const changes = computeHyperdriveChanges(stateEntry, config);
|
|
1972
|
+
if (changes.length === 0) continue;
|
|
1973
|
+
logApplyChange({
|
|
1974
|
+
kind: "hyperdrive",
|
|
1975
|
+
action: "update",
|
|
1976
|
+
logical: config.logicalName,
|
|
1977
|
+
derived: derivedName,
|
|
1978
|
+
changes
|
|
1979
|
+
});
|
|
1980
|
+
const origin$1 = resolveHyperdriveOrigin(config);
|
|
1981
|
+
await api.hyperdrivePatch(stateEntry.cfId, { origin: origin$1 });
|
|
1982
|
+
state.set(key, {
|
|
1983
|
+
...stateEntry,
|
|
1984
|
+
scheme: config.origin.scheme,
|
|
1985
|
+
originHost: config.origin.host,
|
|
1986
|
+
originDatabase: config.origin.database,
|
|
1987
|
+
updatedAt: ts
|
|
1988
|
+
});
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
logApplyChange({
|
|
1992
|
+
kind: "hyperdrive",
|
|
1993
|
+
action: "create",
|
|
1994
|
+
logical: config.logicalName,
|
|
1995
|
+
derived: derivedName
|
|
1996
|
+
});
|
|
1997
|
+
const origin = resolveHyperdriveOrigin(config);
|
|
1998
|
+
const { id } = await api.hyperdriveCreate({
|
|
1999
|
+
name: derivedName,
|
|
2000
|
+
origin,
|
|
2001
|
+
caching: config.caching,
|
|
2002
|
+
mtls: config.mtls
|
|
2003
|
+
});
|
|
2004
|
+
state.set(key, {
|
|
2005
|
+
type: "hyperdrive",
|
|
2006
|
+
logicalName: config.logicalName,
|
|
2007
|
+
derivedName,
|
|
2008
|
+
bindingKey: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
|
|
2009
|
+
cfId: id,
|
|
2010
|
+
scheme: config.origin.scheme,
|
|
2011
|
+
originHost: config.origin.host,
|
|
2012
|
+
originDatabase: config.origin.database,
|
|
2013
|
+
createdAt: ts,
|
|
2014
|
+
updatedAt: ts
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
//#endregion
|
|
2020
|
+
//#region src/features/hyperdrive/hyperdrive.sync.ts
|
|
2021
|
+
function hyperdriveSync(allHyperdrive, resources, _tenant, env, state, naming) {
|
|
2022
|
+
for (const config of resources) {
|
|
2023
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
2024
|
+
const match = allHyperdrive.find((h) => h.name === derivedName);
|
|
2025
|
+
if (!match) continue;
|
|
2026
|
+
const key = `hyperdrive:${derivedName}`;
|
|
2027
|
+
const existing = state.get(key);
|
|
2028
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2029
|
+
state.set(key, {
|
|
2030
|
+
type: "hyperdrive",
|
|
2031
|
+
logicalName: config.logicalName,
|
|
2032
|
+
derivedName,
|
|
2033
|
+
bindingKey: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
|
|
2034
|
+
cfId: match.id,
|
|
2035
|
+
scheme: config.origin.scheme,
|
|
2036
|
+
originHost: match.origin?.host ?? config.origin.host,
|
|
2037
|
+
originDatabase: match.origin?.database ?? config.origin.database,
|
|
2038
|
+
createdAt: existing?.type === "hyperdrive" ? existing.createdAt : ts,
|
|
2039
|
+
updatedAt: ts
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
//#endregion
|
|
2045
|
+
//#region src/features/hyperdrive/hyperdrive.drift.ts
|
|
2046
|
+
function hyperdriveDrift(allHyperdrive, resources, env, state, naming) {
|
|
2047
|
+
const drift = {
|
|
2048
|
+
kind: "hyperdrive",
|
|
2049
|
+
missingFromCloudflare: [],
|
|
2050
|
+
unrecordedInState: [],
|
|
2051
|
+
undeployed: []
|
|
2052
|
+
};
|
|
2053
|
+
const cfByName = new Map(allHyperdrive.map((h) => [h.name, h.id]));
|
|
2054
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "hyperdrive");
|
|
2055
|
+
for (const config of resources) {
|
|
2056
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
2057
|
+
const cfId = cfByName.get(derivedName);
|
|
2058
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
2059
|
+
if (stateEntry && !cfId) drift.missingFromCloudflare.push({
|
|
2060
|
+
logicalName: stateEntry.logicalName,
|
|
2061
|
+
derivedName: stateEntry.derivedName,
|
|
2062
|
+
cfId: stateEntry.cfId
|
|
2063
|
+
});
|
|
2064
|
+
else if (cfId && !stateEntry) drift.unrecordedInState.push({
|
|
2065
|
+
logicalName: config.logicalName,
|
|
2066
|
+
derivedName,
|
|
2067
|
+
cfId
|
|
2068
|
+
});
|
|
2069
|
+
else if (!cfId && !stateEntry) drift.undeployed.push({
|
|
2070
|
+
logicalName: config.logicalName,
|
|
2071
|
+
derivedName
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
return drift;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
//#endregion
|
|
2078
|
+
//#region src/features/hyperdrive/hyperdrive.destroy.ts
|
|
2079
|
+
async function hyperdriveDestroy(_env, state, api, config, baseDir, _force) {
|
|
2080
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "hyperdrive");
|
|
2081
|
+
const resources = state.getAll();
|
|
2082
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "hyperdrive");
|
|
2083
|
+
for (const [key, entry] of entries) {
|
|
2084
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
2085
|
+
try {
|
|
2086
|
+
await api.hyperdriveDelete(entry.cfId);
|
|
2087
|
+
state.delete(key);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
console.warn(`Failed to delete hyperdrive ${entry.derivedName}:`, err);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region src/features/hyperdrive/hyperdrive.generate.ts
|
|
2096
|
+
function hyperdriveGenerate(resources, env, state, naming) {
|
|
2097
|
+
const out = [];
|
|
2098
|
+
for (const config of resources) {
|
|
2099
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
2100
|
+
const entry = state.get(`hyperdrive:${derivedName}`);
|
|
2101
|
+
if (!entry || entry.type !== "hyperdrive") throw new Error(`Hyperdrive "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
2102
|
+
out.push({
|
|
2103
|
+
binding: entry.bindingKey,
|
|
2104
|
+
id: entry.cfId,
|
|
2105
|
+
localConnectionString: config.localConnectionString
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
return out;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
//#endregion
|
|
2112
|
+
//#region src/features/hyperdrive/hyperdrive.status.ts
|
|
2113
|
+
function hyperdriveStatus(resources, env, state, naming) {
|
|
2114
|
+
const out = [];
|
|
2115
|
+
for (const config of resources) {
|
|
2116
|
+
const derivedName = naming.hyperdriveName(config.logicalName, env);
|
|
2117
|
+
const entry = state.get(`hyperdrive:${derivedName}`);
|
|
2118
|
+
out.push({
|
|
2119
|
+
binding: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
|
|
2120
|
+
name: derivedName,
|
|
2121
|
+
status: entry ? "ok" : "missing"
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
return out;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
//#endregion
|
|
2128
|
+
//#region src/features/hyperdrive/hyperdrive.module.ts
|
|
2129
|
+
const hyperdriveModule = {
|
|
2130
|
+
kind: "hyperdrive",
|
|
2131
|
+
label: "Hyperdrive",
|
|
2132
|
+
configKey: "hyperdrive",
|
|
2133
|
+
stateEntryType: "hyperdrive",
|
|
2134
|
+
async fetchAll(api) {
|
|
2135
|
+
return (await api.hyperdriveListAll()).map((h) => ({
|
|
2136
|
+
id: h.id,
|
|
2137
|
+
name: h.name,
|
|
2138
|
+
origin: h.origin
|
|
2139
|
+
}));
|
|
2140
|
+
},
|
|
2141
|
+
async apply(ctx) {
|
|
2142
|
+
await hyperdriveApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
2143
|
+
},
|
|
2144
|
+
sync(ctx) {
|
|
2145
|
+
hyperdriveSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
2146
|
+
},
|
|
2147
|
+
drift(ctx) {
|
|
2148
|
+
return hyperdriveDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2149
|
+
},
|
|
2150
|
+
diff(ctx) {
|
|
2151
|
+
return hyperdriveDiffPlanItems({
|
|
2152
|
+
resources: ctx.resources,
|
|
2153
|
+
env: ctx.env,
|
|
2154
|
+
state: ctx.state,
|
|
2155
|
+
naming: ctx.naming
|
|
2156
|
+
});
|
|
2157
|
+
},
|
|
2158
|
+
async destroy(ctx) {
|
|
2159
|
+
await hyperdriveDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
2160
|
+
},
|
|
2161
|
+
status(ctx) {
|
|
2162
|
+
return hyperdriveStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2163
|
+
},
|
|
2164
|
+
generate(ctx) {
|
|
2165
|
+
const generated = ctx.resources.length > 0 ? hyperdriveGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
|
|
2166
|
+
const passthrough = ctx.passthrough?.hyperdrive ?? [];
|
|
2167
|
+
if (generated.length === 0 && passthrough.length === 0) return {};
|
|
2168
|
+
return { hyperdrive: [...passthrough, ...generated] };
|
|
2169
|
+
},
|
|
2170
|
+
pickResources(source) {
|
|
2171
|
+
return resourcesFrom(source)?.hyperdrive ?? [];
|
|
2172
|
+
},
|
|
2173
|
+
async destroyOne({ api, state, key, entry }) {
|
|
2174
|
+
if (entry.type !== "hyperdrive") return;
|
|
2175
|
+
try {
|
|
2176
|
+
await api.hyperdriveDelete(entry.cfId);
|
|
2177
|
+
state.delete(key);
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
console.warn(`Rollback: failed to delete Hyperdrive ${entry.derivedName}:`, err);
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
2183
|
+
if (!options.cfId) throw new Error("import hyperdrive: --cf-id <config-id> is required");
|
|
2184
|
+
const hit = (await api.hyperdriveListAll()).find((h) => h.id === options.cfId);
|
|
2185
|
+
if (!hit) throw new Error(`import hyperdrive: config id "${options.cfId}" not found in account`);
|
|
2186
|
+
const derivedName = naming.hyperdriveName(options.logical, env);
|
|
2187
|
+
if (hit.name !== derivedName) throw new Error(`import hyperdrive: cf name "${hit.name}" does not match derived "${derivedName}"`);
|
|
2188
|
+
const bindingKey = naming.hyperdriveBindingKey(options.logical);
|
|
2189
|
+
const key = `hyperdrive:${derivedName}`;
|
|
2190
|
+
const existing = state.get(key);
|
|
2191
|
+
const scheme = hit.origin?.scheme === "mysql" ? "mysql" : hit.origin?.scheme === "postgresql" ? "postgresql" : "postgres";
|
|
2192
|
+
const entry = {
|
|
2193
|
+
type: "hyperdrive",
|
|
2194
|
+
logicalName: options.logical,
|
|
2195
|
+
derivedName,
|
|
2196
|
+
bindingKey,
|
|
2197
|
+
cfId: options.cfId,
|
|
2198
|
+
scheme,
|
|
2199
|
+
originHost: hit.origin?.host ?? "",
|
|
2200
|
+
originDatabase: hit.origin?.database ?? "",
|
|
2201
|
+
createdAt: existing?.type === "hyperdrive" ? existing.createdAt : ts,
|
|
2202
|
+
updatedAt: ts
|
|
2203
|
+
};
|
|
2204
|
+
state.set(key, entry);
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
//#endregion
|
|
2209
|
+
//#region src/features/vectorize/vectorize.apply.ts
|
|
2210
|
+
const STATE_KEY_PREFIX$4 = "vectorize:";
|
|
2211
|
+
function stateKey$4(derivedName) {
|
|
2212
|
+
return `${STATE_KEY_PREFIX$4}${derivedName}`;
|
|
2213
|
+
}
|
|
2214
|
+
async function vectorizeApply(resources, _tenant, env, api, state, naming) {
|
|
2215
|
+
for (const config of resources) {
|
|
2216
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2217
|
+
const key = stateKey$4(derivedName);
|
|
2218
|
+
const existing = state.get(key);
|
|
2219
|
+
if (existing) {
|
|
2220
|
+
if (existing.type === "vectorize") {
|
|
2221
|
+
if (existing.dimensions !== config.dimensions || existing.metric !== config.metric) throw new Error(`Vectorize index "${config.logicalName}" config changed (dimensions/metric are immutable). Run 'tamer destroy --resource ${config.logicalName}' first, then re-apply.`);
|
|
2222
|
+
}
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2226
|
+
const created = await api.vectorizeCreate({
|
|
2227
|
+
name: derivedName,
|
|
2228
|
+
description: config.description,
|
|
2229
|
+
config: {
|
|
2230
|
+
dimensions: config.dimensions,
|
|
2231
|
+
metric: config.metric
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
state.set(key, {
|
|
2235
|
+
type: "vectorize",
|
|
2236
|
+
logicalName: config.logicalName,
|
|
2237
|
+
derivedName,
|
|
2238
|
+
bindingKey: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
|
|
2239
|
+
cfId: created.id ?? derivedName,
|
|
2240
|
+
dimensions: config.dimensions,
|
|
2241
|
+
metric: config.metric,
|
|
2242
|
+
createdAt: ts,
|
|
2243
|
+
updatedAt: ts
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
//#endregion
|
|
2249
|
+
//#region src/features/vectorize/vectorize.sync.ts
|
|
2250
|
+
function vectorizeSync(allIndexes, resources, _tenant, env, state, naming) {
|
|
2251
|
+
for (const config of resources) {
|
|
2252
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2253
|
+
const match = allIndexes.find((i) => i.name === derivedName);
|
|
2254
|
+
if (!match) continue;
|
|
2255
|
+
const key = `vectorize:${derivedName}`;
|
|
2256
|
+
const existing = state.get(key);
|
|
2257
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2258
|
+
state.set(key, {
|
|
2259
|
+
type: "vectorize",
|
|
2260
|
+
logicalName: config.logicalName,
|
|
2261
|
+
derivedName,
|
|
2262
|
+
bindingKey: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
|
|
2263
|
+
cfId: match.id ?? derivedName,
|
|
2264
|
+
dimensions: match.config?.dimensions ?? config.dimensions,
|
|
2265
|
+
metric: (match.config?.metric ?? null) || config.metric,
|
|
2266
|
+
createdAt: existing?.type === "vectorize" ? existing.createdAt : ts,
|
|
2267
|
+
updatedAt: ts
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
//#endregion
|
|
2273
|
+
//#region src/features/vectorize/vectorize.drift.ts
|
|
2274
|
+
function vectorizeDrift(allIndexes, resources, env, state, naming) {
|
|
2275
|
+
const drift = {
|
|
2276
|
+
kind: "vectorize",
|
|
2277
|
+
missingFromCloudflare: [],
|
|
2278
|
+
unrecordedInState: [],
|
|
2279
|
+
undeployed: []
|
|
2280
|
+
};
|
|
2281
|
+
const cfByName = new Map(allIndexes.map((i) => [i.name, i.id ?? i.name]));
|
|
2282
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "vectorize");
|
|
2283
|
+
for (const config of resources) {
|
|
2284
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2285
|
+
const cfId = cfByName.get(derivedName);
|
|
2286
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
2287
|
+
if (stateEntry && !cfId) drift.missingFromCloudflare.push({
|
|
2288
|
+
logicalName: stateEntry.logicalName,
|
|
2289
|
+
derivedName: stateEntry.derivedName,
|
|
2290
|
+
cfId: stateEntry.cfId
|
|
2291
|
+
});
|
|
2292
|
+
else if (cfId && !stateEntry) drift.unrecordedInState.push({
|
|
2293
|
+
logicalName: config.logicalName,
|
|
2294
|
+
derivedName,
|
|
2295
|
+
cfId
|
|
2296
|
+
});
|
|
2297
|
+
else if (!cfId && !stateEntry) drift.undeployed.push({
|
|
2298
|
+
logicalName: config.logicalName,
|
|
2299
|
+
derivedName
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
return drift;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
//#endregion
|
|
2306
|
+
//#region src/features/vectorize/vectorize.destroy.ts
|
|
2307
|
+
async function vectorizeDestroy(_env, state, api, config, baseDir, _force) {
|
|
2308
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "vectorize");
|
|
2309
|
+
const resources = state.getAll();
|
|
2310
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "vectorize");
|
|
2311
|
+
for (const [key, entry] of entries) {
|
|
2312
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
2313
|
+
try {
|
|
2314
|
+
await api.vectorizeDelete(entry.derivedName);
|
|
2315
|
+
state.delete(key);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
console.warn(`Failed to delete vectorize index ${entry.derivedName}:`, err);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
//#endregion
|
|
2323
|
+
//#region src/features/vectorize/vectorize.generate.ts
|
|
2324
|
+
/**
|
|
2325
|
+
* Emit `vectorize[]` bindings for declared indexes. Each binding requires
|
|
2326
|
+
* the index to exist in state (i.e. `tamer apply` has run). Throws otherwise
|
|
2327
|
+
* so the operator gets an actionable error before `wrangler deploy`.
|
|
2328
|
+
*/
|
|
2329
|
+
function vectorizeGenerate(resources, env, state, naming) {
|
|
2330
|
+
const out = [];
|
|
2331
|
+
for (const config of resources) {
|
|
2332
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2333
|
+
const entry = state.get(`vectorize:${derivedName}`);
|
|
2334
|
+
if (!entry || entry.type !== "vectorize") throw new Error(`Vectorize index "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
2335
|
+
out.push({
|
|
2336
|
+
binding: entry.bindingKey,
|
|
2337
|
+
index_name: entry.derivedName
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
return out;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
//#endregion
|
|
2344
|
+
//#region src/features/vectorize/vectorize.status.ts
|
|
2345
|
+
function vectorizeStatus(resources, env, state, naming) {
|
|
2346
|
+
const out = [];
|
|
2347
|
+
for (const config of resources) {
|
|
2348
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2349
|
+
const entry = state.get(`vectorize:${derivedName}`);
|
|
2350
|
+
out.push({
|
|
2351
|
+
binding: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
|
|
2352
|
+
name: derivedName,
|
|
2353
|
+
status: entry ? "ok" : "missing"
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
return out;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
//#endregion
|
|
2360
|
+
//#region src/features/vectorize/vectorize.diff.ts
|
|
2361
|
+
function vectorizeDiffPlanItems(args) {
|
|
2362
|
+
const { resources, env, state, naming } = args;
|
|
2363
|
+
const items = [];
|
|
2364
|
+
for (const config of resources) {
|
|
2365
|
+
const derivedName = naming.vectorizeName(config.logicalName, env);
|
|
2366
|
+
const key = `vectorize:${derivedName}`;
|
|
2367
|
+
const entry = state.get(key);
|
|
2368
|
+
if (!entry || entry.type !== "vectorize") continue;
|
|
2369
|
+
const stateEntry = entry;
|
|
2370
|
+
const changes = [];
|
|
2371
|
+
if (stateEntry.dimensions !== config.dimensions) changes.push({
|
|
2372
|
+
field: "dimensions",
|
|
2373
|
+
from: stateEntry.dimensions,
|
|
2374
|
+
to: config.dimensions,
|
|
2375
|
+
kind: "immutable"
|
|
2376
|
+
});
|
|
2377
|
+
if (stateEntry.metric !== config.metric) changes.push({
|
|
2378
|
+
field: "metric",
|
|
2379
|
+
from: stateEntry.metric,
|
|
2380
|
+
to: config.metric,
|
|
2381
|
+
kind: "immutable"
|
|
2382
|
+
});
|
|
2383
|
+
if (changes.length === 0) continue;
|
|
2384
|
+
items.push({
|
|
2385
|
+
kind: "vectorize",
|
|
2386
|
+
action: "replace",
|
|
2387
|
+
logicalName: config.logicalName,
|
|
2388
|
+
derivedName,
|
|
2389
|
+
detail: changes.map((c) => `${c.field}: ${c.from} -> ${c.to}`).join(", "),
|
|
2390
|
+
changes
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
return items;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region src/features/vectorize/vectorize.module.ts
|
|
2398
|
+
const vectorizeModule = {
|
|
2399
|
+
kind: "vectorize",
|
|
2400
|
+
label: "Vectorize",
|
|
2401
|
+
configKey: "vectorize",
|
|
2402
|
+
stateEntryType: "vectorize",
|
|
2403
|
+
async fetchAll(api) {
|
|
2404
|
+
return (await api.vectorizeListAll()).map((v) => ({
|
|
2405
|
+
id: v.id,
|
|
2406
|
+
name: v.name,
|
|
2407
|
+
config: v.config
|
|
2408
|
+
}));
|
|
2409
|
+
},
|
|
2410
|
+
async apply(ctx) {
|
|
2411
|
+
await vectorizeApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
2412
|
+
},
|
|
2413
|
+
sync(ctx) {
|
|
2414
|
+
vectorizeSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
2415
|
+
},
|
|
2416
|
+
drift(ctx) {
|
|
2417
|
+
return vectorizeDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2418
|
+
},
|
|
2419
|
+
diff(ctx) {
|
|
2420
|
+
return vectorizeDiffPlanItems({
|
|
2421
|
+
resources: ctx.resources,
|
|
2422
|
+
env: ctx.env,
|
|
2423
|
+
state: ctx.state,
|
|
2424
|
+
naming: ctx.naming
|
|
2425
|
+
});
|
|
2426
|
+
},
|
|
2427
|
+
async destroy(ctx) {
|
|
2428
|
+
await vectorizeDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
2429
|
+
},
|
|
2430
|
+
status(ctx) {
|
|
2431
|
+
return vectorizeStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2432
|
+
},
|
|
2433
|
+
generate(ctx) {
|
|
2434
|
+
const generated = ctx.resources.length > 0 ? vectorizeGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
|
|
2435
|
+
const passthrough = ctx.passthrough?.vectorize ?? [];
|
|
2436
|
+
if (generated.length === 0 && passthrough.length === 0) return {};
|
|
2437
|
+
return { vectorize: [...passthrough, ...generated] };
|
|
2438
|
+
},
|
|
2439
|
+
pickResources(source) {
|
|
2440
|
+
return resourcesFrom(source)?.vectorize ?? [];
|
|
2441
|
+
},
|
|
2442
|
+
async destroyOne({ api, state, key, entry }) {
|
|
2443
|
+
if (entry.type !== "vectorize") return;
|
|
2444
|
+
try {
|
|
2445
|
+
await api.vectorizeDelete(entry.derivedName);
|
|
2446
|
+
state.delete(key);
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
console.warn(`Rollback: failed to delete Vectorize ${entry.derivedName}:`, err);
|
|
2449
|
+
}
|
|
2450
|
+
},
|
|
2451
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
2452
|
+
const indexName = options.cfId;
|
|
2453
|
+
if (!indexName) throw new Error("import vectorize: --cf-id <index-name> is required");
|
|
2454
|
+
const hit = (await api.vectorizeListAll()).find((v) => v.name === indexName);
|
|
2455
|
+
if (!hit) throw new Error(`import vectorize: index "${indexName}" not found in account`);
|
|
2456
|
+
const derivedName = naming.vectorizeName(options.logical, env);
|
|
2457
|
+
if (hit.name !== derivedName) throw new Error(`import vectorize: cf name "${hit.name}" does not match derived "${derivedName}"`);
|
|
2458
|
+
if (!hit.config) throw new Error(`import vectorize: cf index "${hit.name}" returned no config (dimensions/metric)`);
|
|
2459
|
+
const metric = hit.config.metric;
|
|
2460
|
+
const bindingKey = naming.vectorizeBindingKey(options.logical);
|
|
2461
|
+
const key = `vectorize:${derivedName}`;
|
|
2462
|
+
const existing = state.get(key);
|
|
2463
|
+
const entry = {
|
|
2464
|
+
type: "vectorize",
|
|
2465
|
+
logicalName: options.logical,
|
|
2466
|
+
derivedName,
|
|
2467
|
+
bindingKey,
|
|
2468
|
+
cfId: hit.id ?? derivedName,
|
|
2469
|
+
dimensions: hit.config.dimensions,
|
|
2470
|
+
metric,
|
|
2471
|
+
createdAt: existing?.type === "vectorize" ? existing.createdAt : ts,
|
|
2472
|
+
updatedAt: ts
|
|
2473
|
+
};
|
|
2474
|
+
state.set(key, entry);
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
|
|
2478
|
+
//#endregion
|
|
2479
|
+
//#region src/features/ai-gateway/ai-gateway.types.ts
|
|
2480
|
+
/**
|
|
2481
|
+
* Default values applied to optional `AIGatewayResourceConfig` fields. Kept
|
|
2482
|
+
* here so apply / sync / drift agree on what "no override" actually means
|
|
2483
|
+
* when comparing config to Cloudflare state.
|
|
2484
|
+
*/
|
|
2485
|
+
const AI_GATEWAY_DEFAULTS = {
|
|
2486
|
+
cacheTtl: 0,
|
|
2487
|
+
cacheInvalidateOnUpdate: false,
|
|
2488
|
+
collectLogs: true,
|
|
2489
|
+
authentication: false,
|
|
2490
|
+
rateLimitingInterval: 0,
|
|
2491
|
+
rateLimitingLimit: 0,
|
|
2492
|
+
rateLimitingTechnique: "fixed"
|
|
2493
|
+
};
|
|
2494
|
+
function resolveAIGatewayConfig(config) {
|
|
2495
|
+
return {
|
|
2496
|
+
cacheTtl: config.cacheTtl ?? AI_GATEWAY_DEFAULTS.cacheTtl,
|
|
2497
|
+
cacheInvalidateOnUpdate: config.cacheInvalidateOnUpdate ?? AI_GATEWAY_DEFAULTS.cacheInvalidateOnUpdate,
|
|
2498
|
+
collectLogs: config.collectLogs ?? AI_GATEWAY_DEFAULTS.collectLogs,
|
|
2499
|
+
authentication: config.authentication ?? AI_GATEWAY_DEFAULTS.authentication,
|
|
2500
|
+
rateLimitingInterval: config.rateLimitingInterval ?? AI_GATEWAY_DEFAULTS.rateLimitingInterval,
|
|
2501
|
+
rateLimitingLimit: config.rateLimitingLimit ?? AI_GATEWAY_DEFAULTS.rateLimitingLimit,
|
|
2502
|
+
rateLimitingTechnique: config.rateLimitingTechnique ?? AI_GATEWAY_DEFAULTS.rateLimitingTechnique
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
//#endregion
|
|
2507
|
+
//#region src/features/ai-gateway/ai-gateway.diff.ts
|
|
2508
|
+
function aiGatewayDiffPlanItems(args) {
|
|
2509
|
+
const { resources, env, state, naming } = args;
|
|
2510
|
+
const items = [];
|
|
2511
|
+
for (const config of resources) {
|
|
2512
|
+
const derivedName = naming.aiGatewayId(config.logicalName, env);
|
|
2513
|
+
const entry = state.get(`ai_gateway:${derivedName}`);
|
|
2514
|
+
if (!entry || entry.type !== "ai_gateway") continue;
|
|
2515
|
+
const changes = computeChanges(entry, config);
|
|
2516
|
+
if (changes.length === 0) continue;
|
|
2517
|
+
items.push({
|
|
2518
|
+
kind: "ai_gateway",
|
|
2519
|
+
action: "update",
|
|
2520
|
+
logicalName: config.logicalName,
|
|
2521
|
+
derivedName,
|
|
2522
|
+
detail: changes.map((c) => `${c.field}: ${formatVal$1(c.from)} -> ${formatVal$1(c.to)}`).join(", "),
|
|
2523
|
+
changes
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
return items;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Pure comparison shared with `aiGatewayApply` — keeps the plan and the
|
|
2530
|
+
* apply path in lockstep about which fields count as drift.
|
|
2531
|
+
*/
|
|
2532
|
+
function computeAIGatewayChanges(state, config) {
|
|
2533
|
+
return computeChanges(state, config);
|
|
2534
|
+
}
|
|
2535
|
+
function computeChanges(state, config) {
|
|
2536
|
+
const resolved = resolveAIGatewayConfig(config);
|
|
2537
|
+
const changes = [];
|
|
2538
|
+
if (state.cacheTtl !== resolved.cacheTtl) changes.push({
|
|
2539
|
+
field: "cacheTtl",
|
|
2540
|
+
from: state.cacheTtl,
|
|
2541
|
+
to: resolved.cacheTtl,
|
|
2542
|
+
kind: "mutable"
|
|
2543
|
+
});
|
|
2544
|
+
if (state.cacheInvalidateOnUpdate !== resolved.cacheInvalidateOnUpdate) changes.push({
|
|
2545
|
+
field: "cacheInvalidateOnUpdate",
|
|
2546
|
+
from: state.cacheInvalidateOnUpdate,
|
|
2547
|
+
to: resolved.cacheInvalidateOnUpdate,
|
|
2548
|
+
kind: "mutable"
|
|
2549
|
+
});
|
|
2550
|
+
if (state.collectLogs !== resolved.collectLogs) changes.push({
|
|
2551
|
+
field: "collectLogs",
|
|
2552
|
+
from: state.collectLogs,
|
|
2553
|
+
to: resolved.collectLogs,
|
|
2554
|
+
kind: "mutable"
|
|
2555
|
+
});
|
|
2556
|
+
if (state.authentication !== resolved.authentication) changes.push({
|
|
2557
|
+
field: "authentication",
|
|
2558
|
+
from: state.authentication,
|
|
2559
|
+
to: resolved.authentication,
|
|
2560
|
+
kind: "mutable"
|
|
2561
|
+
});
|
|
2562
|
+
if (state.rateLimitingInterval !== resolved.rateLimitingInterval) changes.push({
|
|
2563
|
+
field: "rateLimitingInterval",
|
|
2564
|
+
from: state.rateLimitingInterval,
|
|
2565
|
+
to: resolved.rateLimitingInterval,
|
|
2566
|
+
kind: "mutable"
|
|
2567
|
+
});
|
|
2568
|
+
if (state.rateLimitingLimit !== resolved.rateLimitingLimit) changes.push({
|
|
2569
|
+
field: "rateLimitingLimit",
|
|
2570
|
+
from: state.rateLimitingLimit,
|
|
2571
|
+
to: resolved.rateLimitingLimit,
|
|
2572
|
+
kind: "mutable"
|
|
2573
|
+
});
|
|
2574
|
+
if (state.rateLimitingTechnique !== resolved.rateLimitingTechnique) changes.push({
|
|
2575
|
+
field: "rateLimitingTechnique",
|
|
2576
|
+
from: state.rateLimitingTechnique,
|
|
2577
|
+
to: resolved.rateLimitingTechnique,
|
|
2578
|
+
kind: "mutable"
|
|
2579
|
+
});
|
|
2580
|
+
return changes;
|
|
2581
|
+
}
|
|
2582
|
+
function formatVal$1(v) {
|
|
2583
|
+
if (v === void 0) return "(unset)";
|
|
2584
|
+
if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
|
|
2585
|
+
return String(v);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
//#endregion
|
|
2589
|
+
//#region src/features/ai-gateway/ai-gateway.apply.ts
|
|
2590
|
+
const STATE_KEY_PREFIX$3 = "ai_gateway:";
|
|
2591
|
+
function stateKey$3(derivedName) {
|
|
2592
|
+
return `${STATE_KEY_PREFIX$3}${derivedName}`;
|
|
2593
|
+
}
|
|
2594
|
+
async function aiGatewayApply(resources, _tenant, env, api, state, naming) {
|
|
2595
|
+
for (const config of resources) {
|
|
2596
|
+
const derivedName = naming.aiGatewayId(config.logicalName, env);
|
|
2597
|
+
const key = stateKey$3(derivedName);
|
|
2598
|
+
const existing = state.get(key);
|
|
2599
|
+
const resolved = resolveAIGatewayConfig(config);
|
|
2600
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2601
|
+
if (existing && existing.type === "ai_gateway") {
|
|
2602
|
+
const changes = computeAIGatewayChanges(existing, config);
|
|
2603
|
+
if (changes.length === 0) continue;
|
|
2604
|
+
logApplyChange({
|
|
2605
|
+
kind: "ai_gateway",
|
|
2606
|
+
action: "update",
|
|
2607
|
+
logical: config.logicalName,
|
|
2608
|
+
derived: derivedName,
|
|
2609
|
+
changes
|
|
2610
|
+
});
|
|
2611
|
+
await api.aiGatewayUpdate(derivedName, {
|
|
2612
|
+
cache_ttl: resolved.cacheTtl,
|
|
2613
|
+
cache_invalidate_on_update: resolved.cacheInvalidateOnUpdate,
|
|
2614
|
+
collect_logs: resolved.collectLogs,
|
|
2615
|
+
authentication: resolved.authentication,
|
|
2616
|
+
rate_limiting_interval: resolved.rateLimitingInterval,
|
|
2617
|
+
rate_limiting_limit: resolved.rateLimitingLimit,
|
|
2618
|
+
rate_limiting_technique: resolved.rateLimitingTechnique
|
|
2619
|
+
});
|
|
2620
|
+
state.set(key, {
|
|
2621
|
+
...existing,
|
|
2622
|
+
cacheTtl: resolved.cacheTtl,
|
|
2623
|
+
cacheInvalidateOnUpdate: resolved.cacheInvalidateOnUpdate,
|
|
2624
|
+
collectLogs: resolved.collectLogs,
|
|
2625
|
+
authentication: resolved.authentication,
|
|
2626
|
+
rateLimitingInterval: resolved.rateLimitingInterval,
|
|
2627
|
+
rateLimitingLimit: resolved.rateLimitingLimit,
|
|
2628
|
+
rateLimitingTechnique: resolved.rateLimitingTechnique,
|
|
2629
|
+
updatedAt: ts
|
|
2630
|
+
});
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
logApplyChange({
|
|
2634
|
+
kind: "ai_gateway",
|
|
2635
|
+
action: "create",
|
|
2636
|
+
logical: config.logicalName,
|
|
2637
|
+
derived: derivedName
|
|
2638
|
+
});
|
|
2639
|
+
await api.aiGatewayCreate({
|
|
2640
|
+
id: derivedName,
|
|
2641
|
+
cache_ttl: resolved.cacheTtl,
|
|
2642
|
+
cache_invalidate_on_update: resolved.cacheInvalidateOnUpdate,
|
|
2643
|
+
collect_logs: resolved.collectLogs,
|
|
2644
|
+
authentication: resolved.authentication,
|
|
2645
|
+
rate_limiting_interval: resolved.rateLimitingInterval,
|
|
2646
|
+
rate_limiting_limit: resolved.rateLimitingLimit,
|
|
2647
|
+
rate_limiting_technique: resolved.rateLimitingTechnique
|
|
2648
|
+
});
|
|
2649
|
+
state.set(key, {
|
|
2650
|
+
type: "ai_gateway",
|
|
2651
|
+
logicalName: config.logicalName,
|
|
2652
|
+
derivedName,
|
|
2653
|
+
bindingKey: naming.aiGatewayBindingKey(config.logicalName),
|
|
2654
|
+
cfId: derivedName,
|
|
2655
|
+
cacheTtl: resolved.cacheTtl,
|
|
2656
|
+
cacheInvalidateOnUpdate: resolved.cacheInvalidateOnUpdate,
|
|
2657
|
+
collectLogs: resolved.collectLogs,
|
|
2658
|
+
authentication: resolved.authentication,
|
|
2659
|
+
rateLimitingInterval: resolved.rateLimitingInterval,
|
|
2660
|
+
rateLimitingLimit: resolved.rateLimitingLimit,
|
|
2661
|
+
rateLimitingTechnique: resolved.rateLimitingTechnique,
|
|
2662
|
+
createdAt: ts,
|
|
2663
|
+
updatedAt: ts
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/features/ai-gateway/ai-gateway.sync.ts
|
|
2670
|
+
function aiGatewaySync(allGateways, resources, _tenant, env, state, naming) {
|
|
2671
|
+
for (const config of resources) {
|
|
2672
|
+
const derivedName = naming.aiGatewayId(config.logicalName, env);
|
|
2673
|
+
const match = allGateways.find((g) => g.id === derivedName);
|
|
2674
|
+
if (!match) continue;
|
|
2675
|
+
const key = `ai_gateway:${derivedName}`;
|
|
2676
|
+
const existing = state.get(key);
|
|
2677
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2678
|
+
const resolved = resolveAIGatewayConfig(config);
|
|
2679
|
+
state.set(key, {
|
|
2680
|
+
type: "ai_gateway",
|
|
2681
|
+
logicalName: config.logicalName,
|
|
2682
|
+
derivedName,
|
|
2683
|
+
bindingKey: naming.aiGatewayBindingKey(config.logicalName),
|
|
2684
|
+
cfId: derivedName,
|
|
2685
|
+
cacheTtl: match.cache_ttl ?? resolved.cacheTtl,
|
|
2686
|
+
cacheInvalidateOnUpdate: match.cache_invalidate_on_update ?? resolved.cacheInvalidateOnUpdate,
|
|
2687
|
+
collectLogs: match.collect_logs ?? resolved.collectLogs,
|
|
2688
|
+
authentication: match.authentication ?? resolved.authentication,
|
|
2689
|
+
rateLimitingInterval: match.rate_limiting_interval ?? resolved.rateLimitingInterval,
|
|
2690
|
+
rateLimitingLimit: match.rate_limiting_limit ?? resolved.rateLimitingLimit,
|
|
2691
|
+
rateLimitingTechnique: match.rate_limiting_technique ?? resolved.rateLimitingTechnique,
|
|
2692
|
+
createdAt: existing?.type === "ai_gateway" ? existing.createdAt : ts,
|
|
2693
|
+
updatedAt: ts
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
//#endregion
|
|
2699
|
+
//#region src/features/ai-gateway/ai-gateway.drift.ts
|
|
2700
|
+
function aiGatewayDrift(allGateways, resources, env, state, naming) {
|
|
2701
|
+
const drift = {
|
|
2702
|
+
kind: "ai_gateway",
|
|
2703
|
+
missingFromCloudflare: [],
|
|
2704
|
+
unrecordedInState: [],
|
|
2705
|
+
undeployed: []
|
|
2706
|
+
};
|
|
2707
|
+
const cfIds = new Set(allGateways.map((g) => g.id));
|
|
2708
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "ai_gateway");
|
|
2709
|
+
for (const config of resources) {
|
|
2710
|
+
const derivedName = naming.aiGatewayId(config.logicalName, env);
|
|
2711
|
+
const inCloudflare = cfIds.has(derivedName);
|
|
2712
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
2713
|
+
if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
|
|
2714
|
+
logicalName: stateEntry.logicalName,
|
|
2715
|
+
derivedName: stateEntry.derivedName,
|
|
2716
|
+
cfId: stateEntry.cfId
|
|
2717
|
+
});
|
|
2718
|
+
else if (inCloudflare && !stateEntry) drift.unrecordedInState.push({
|
|
2719
|
+
logicalName: config.logicalName,
|
|
2720
|
+
derivedName,
|
|
2721
|
+
cfId: derivedName
|
|
2722
|
+
});
|
|
2723
|
+
else if (!inCloudflare && !stateEntry) drift.undeployed.push({
|
|
2724
|
+
logicalName: config.logicalName,
|
|
2725
|
+
derivedName
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
return drift;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/features/ai-gateway/ai-gateway.destroy.ts
|
|
2733
|
+
async function aiGatewayDestroy(_env, state, api, config, baseDir, _force) {
|
|
2734
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "ai_gateway");
|
|
2735
|
+
const resources = state.getAll();
|
|
2736
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "ai_gateway");
|
|
2737
|
+
for (const [key, entry] of entries) {
|
|
2738
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
2739
|
+
try {
|
|
2740
|
+
await api.aiGatewayDelete(entry.derivedName);
|
|
2741
|
+
state.delete(key);
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
console.warn(`Failed to delete AI Gateway ${entry.derivedName}:`, err);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
//#endregion
|
|
2749
|
+
//#region src/features/ai-gateway/ai-gateway.status.ts
|
|
2750
|
+
function aiGatewayStatus(resources, env, state, naming) {
|
|
2751
|
+
const out = [];
|
|
2752
|
+
for (const config of resources) {
|
|
2753
|
+
const derivedName = naming.aiGatewayId(config.logicalName, env);
|
|
2754
|
+
const entry = state.get(`ai_gateway:${derivedName}`);
|
|
2755
|
+
out.push({
|
|
2756
|
+
binding: naming.aiGatewayBindingKey(config.logicalName),
|
|
2757
|
+
name: derivedName,
|
|
2758
|
+
status: entry ? "ok" : "missing"
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
return out;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
//#endregion
|
|
2765
|
+
//#region src/features/ai-gateway/ai-gateway.module.ts
|
|
2766
|
+
const aiGatewayModule = {
|
|
2767
|
+
kind: "ai_gateway",
|
|
2768
|
+
label: "AI Gateway",
|
|
2769
|
+
configKey: "aiGateway",
|
|
2770
|
+
stateEntryType: "ai_gateway",
|
|
2771
|
+
async fetchAll(api) {
|
|
2772
|
+
return await api.aiGatewayListAll();
|
|
2773
|
+
},
|
|
2774
|
+
async apply(ctx) {
|
|
2775
|
+
await aiGatewayApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
2776
|
+
},
|
|
2777
|
+
sync(ctx) {
|
|
2778
|
+
aiGatewaySync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
2779
|
+
},
|
|
2780
|
+
drift(ctx) {
|
|
2781
|
+
return aiGatewayDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2782
|
+
},
|
|
2783
|
+
diff(ctx) {
|
|
2784
|
+
return aiGatewayDiffPlanItems({
|
|
2785
|
+
resources: ctx.resources,
|
|
2786
|
+
env: ctx.env,
|
|
2787
|
+
state: ctx.state,
|
|
2788
|
+
naming: ctx.naming
|
|
2789
|
+
});
|
|
2790
|
+
},
|
|
2791
|
+
async destroy(ctx) {
|
|
2792
|
+
await aiGatewayDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
2793
|
+
},
|
|
2794
|
+
status(ctx) {
|
|
2795
|
+
return aiGatewayStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
2796
|
+
},
|
|
2797
|
+
generate() {
|
|
2798
|
+
return {};
|
|
2799
|
+
},
|
|
2800
|
+
pickResources(source) {
|
|
2801
|
+
return resourcesFrom(source)?.aiGateway ?? [];
|
|
2802
|
+
},
|
|
2803
|
+
async destroyOne({ api, state, key, entry }) {
|
|
2804
|
+
if (entry.type !== "ai_gateway") return;
|
|
2805
|
+
try {
|
|
2806
|
+
await api.aiGatewayDelete(entry.derivedName);
|
|
2807
|
+
state.delete(key);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
console.warn(`Rollback: failed to delete AI Gateway ${entry.derivedName}:`, err);
|
|
2810
|
+
}
|
|
2811
|
+
},
|
|
2812
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
2813
|
+
const gatewayId = options.cfId;
|
|
2814
|
+
if (!gatewayId) throw new Error("import ai_gateway: --cf-id <gateway-id> is required");
|
|
2815
|
+
const hit = (await api.aiGatewayListAll()).find((g) => g.id === gatewayId);
|
|
2816
|
+
if (!hit) throw new Error(`import ai_gateway: gateway "${gatewayId}" not found in account`);
|
|
2817
|
+
const derivedName = naming.aiGatewayId(options.logical, env);
|
|
2818
|
+
if (hit.id !== derivedName) throw new Error(`import ai_gateway: cf id "${hit.id}" does not match derived "${derivedName}"`);
|
|
2819
|
+
const key = `ai_gateway:${derivedName}`;
|
|
2820
|
+
const existing = state.get(key);
|
|
2821
|
+
const defaults = resolveAIGatewayConfig({ logicalName: options.logical });
|
|
2822
|
+
const entry = {
|
|
2823
|
+
type: "ai_gateway",
|
|
2824
|
+
logicalName: options.logical,
|
|
2825
|
+
derivedName,
|
|
2826
|
+
bindingKey: naming.aiGatewayBindingKey(options.logical),
|
|
2827
|
+
cfId: hit.id,
|
|
2828
|
+
cacheTtl: hit.cache_ttl ?? defaults.cacheTtl,
|
|
2829
|
+
cacheInvalidateOnUpdate: hit.cache_invalidate_on_update ?? defaults.cacheInvalidateOnUpdate,
|
|
2830
|
+
collectLogs: hit.collect_logs ?? defaults.collectLogs,
|
|
2831
|
+
authentication: hit.authentication ?? defaults.authentication,
|
|
2832
|
+
rateLimitingInterval: hit.rate_limiting_interval ?? defaults.rateLimitingInterval,
|
|
2833
|
+
rateLimitingLimit: hit.rate_limiting_limit ?? defaults.rateLimitingLimit,
|
|
2834
|
+
rateLimitingTechnique: hit.rate_limiting_technique ?? defaults.rateLimitingTechnique,
|
|
2835
|
+
createdAt: existing?.type === "ai_gateway" ? existing.createdAt : ts,
|
|
2836
|
+
updatedAt: ts
|
|
2837
|
+
};
|
|
2838
|
+
state.set(key, entry);
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
|
|
2842
|
+
//#endregion
|
|
2843
|
+
//#region src/features/pipelines/pipelines.apply.ts
|
|
2844
|
+
const STATE_KEY_PREFIX$2 = "pipeline:";
|
|
2845
|
+
function stateKey$2(derivedName) {
|
|
2846
|
+
return `${STATE_KEY_PREFIX$2}${derivedName}`;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Idempotent create — skips when a state entry already exists. Pipeline SQL
|
|
2850
|
+
* updates on existing pipelines are deliberately not handled here (Cloudflare's
|
|
2851
|
+
* V1 endpoint has no PATCH today; SQL changes require a destroy + recreate).
|
|
2852
|
+
* Drift surfaces SQL mismatches so operators can recover explicitly.
|
|
2853
|
+
*/
|
|
2854
|
+
async function pipelinesApply(resources, _tenant, env, api, state, naming) {
|
|
2855
|
+
for (const config of resources) {
|
|
2856
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
2857
|
+
const key = stateKey$2(derivedName);
|
|
2858
|
+
if (state.get(key)) continue;
|
|
2859
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2860
|
+
const created = await api.pipelineCreate({
|
|
2861
|
+
name: derivedName,
|
|
2862
|
+
sql: config.sql
|
|
2863
|
+
});
|
|
2864
|
+
state.set(key, {
|
|
2865
|
+
type: "pipeline",
|
|
2866
|
+
logicalName: config.logicalName,
|
|
2867
|
+
derivedName,
|
|
2868
|
+
bindingKey: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
|
|
2869
|
+
cfId: created.id,
|
|
2870
|
+
sql: created.sql ?? config.sql,
|
|
2871
|
+
status: created.status,
|
|
2872
|
+
createdAt: ts,
|
|
2873
|
+
updatedAt: ts
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
//#endregion
|
|
2879
|
+
//#region src/features/pipelines/pipelines.sync.ts
|
|
2880
|
+
function pipelinesSync(allPipelines, resources, _tenant, env, state, naming) {
|
|
2881
|
+
for (const config of resources) {
|
|
2882
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
2883
|
+
const match = allPipelines.find((p) => p.name === derivedName);
|
|
2884
|
+
if (!match) continue;
|
|
2885
|
+
const key = `pipeline:${derivedName}`;
|
|
2886
|
+
const existing = state.get(key);
|
|
2887
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2888
|
+
state.set(key, {
|
|
2889
|
+
type: "pipeline",
|
|
2890
|
+
logicalName: config.logicalName,
|
|
2891
|
+
derivedName,
|
|
2892
|
+
bindingKey: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
|
|
2893
|
+
cfId: match.id,
|
|
2894
|
+
sql: match.sql,
|
|
2895
|
+
status: match.status,
|
|
2896
|
+
createdAt: existing?.type === "pipeline" ? existing.createdAt : ts,
|
|
2897
|
+
updatedAt: ts
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
//#endregion
|
|
2903
|
+
//#region src/features/pipelines/pipelines.drift.ts
|
|
2904
|
+
function pipelinesDrift(allPipelines, resources, env, state, naming) {
|
|
2905
|
+
const drift = {
|
|
2906
|
+
kind: "pipeline",
|
|
2907
|
+
missingFromCloudflare: [],
|
|
2908
|
+
unrecordedInState: [],
|
|
2909
|
+
undeployed: []
|
|
2910
|
+
};
|
|
2911
|
+
const cfByName = new Map(allPipelines.map((p) => [p.name, p]));
|
|
2912
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "pipeline");
|
|
2913
|
+
for (const config of resources) {
|
|
2914
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
2915
|
+
const inCloudflare = cfByName.has(derivedName);
|
|
2916
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
2917
|
+
if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
|
|
2918
|
+
logicalName: stateEntry.logicalName,
|
|
2919
|
+
derivedName: stateEntry.derivedName,
|
|
2920
|
+
cfId: stateEntry.cfId
|
|
2921
|
+
});
|
|
2922
|
+
else if (inCloudflare && !stateEntry) {
|
|
2923
|
+
const cf = cfByName.get(derivedName);
|
|
2924
|
+
drift.unrecordedInState.push({
|
|
2925
|
+
logicalName: config.logicalName,
|
|
2926
|
+
derivedName,
|
|
2927
|
+
cfId: cf.id
|
|
2928
|
+
});
|
|
2929
|
+
} else if (!inCloudflare && !stateEntry) drift.undeployed.push({
|
|
2930
|
+
logicalName: config.logicalName,
|
|
2931
|
+
derivedName
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
return drift;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/features/pipelines/pipelines.destroy.ts
|
|
2939
|
+
async function pipelinesDestroy(_env, state, api, config, baseDir, _force) {
|
|
2940
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "pipeline");
|
|
2941
|
+
const resources = state.getAll();
|
|
2942
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "pipeline");
|
|
2943
|
+
for (const [key, entry] of entries) {
|
|
2944
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
2945
|
+
try {
|
|
2946
|
+
await api.pipelineDelete(entry.cfId);
|
|
2947
|
+
state.delete(key);
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
console.warn(`Failed to delete Pipeline ${entry.derivedName}:`, err);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
//#endregion
|
|
2955
|
+
//#region src/features/pipelines/pipelines.generate.ts
|
|
2956
|
+
/**
|
|
2957
|
+
* Emit `pipelines[]` bindings for declared pipelines. Each binding requires
|
|
2958
|
+
* the pipeline to exist in state (i.e. `tamer apply` has run). Throws
|
|
2959
|
+
* otherwise so the operator gets an actionable error before `wrangler deploy`.
|
|
2960
|
+
*
|
|
2961
|
+
* Note: Wrangler's `pipeline` field accepts the pipeline **name**, not the
|
|
2962
|
+
* server-assigned id, so we emit the derived name here.
|
|
2963
|
+
*/
|
|
2964
|
+
function pipelinesGenerate(resources, env, state, naming) {
|
|
2965
|
+
const out = [];
|
|
2966
|
+
for (const config of resources) {
|
|
2967
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
2968
|
+
const entry = state.get(`pipeline:${derivedName}`);
|
|
2969
|
+
if (!entry || entry.type !== "pipeline") throw new Error(`Pipeline "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
2970
|
+
out.push({
|
|
2971
|
+
binding: entry.bindingKey,
|
|
2972
|
+
pipeline: entry.derivedName
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
return out;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
//#endregion
|
|
2979
|
+
//#region src/features/pipelines/pipelines.status.ts
|
|
2980
|
+
function pipelinesStatus(resources, env, state, naming) {
|
|
2981
|
+
const out = [];
|
|
2982
|
+
for (const config of resources) {
|
|
2983
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
2984
|
+
const entry = state.get(`pipeline:${derivedName}`);
|
|
2985
|
+
out.push({
|
|
2986
|
+
binding: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
|
|
2987
|
+
name: derivedName,
|
|
2988
|
+
status: entry ? "ok" : "missing"
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
return out;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
//#endregion
|
|
2995
|
+
//#region src/features/pipelines/pipelines.diff.ts
|
|
2996
|
+
function pipelinesDiffPlanItems(args) {
|
|
2997
|
+
const { resources, env, state, naming } = args;
|
|
2998
|
+
const items = [];
|
|
2999
|
+
for (const config of resources) {
|
|
3000
|
+
const derivedName = naming.pipelineName(config.logicalName, env);
|
|
3001
|
+
const key = `pipeline:${derivedName}`;
|
|
3002
|
+
const entry = state.get(key);
|
|
3003
|
+
if (!entry || entry.type !== "pipeline") continue;
|
|
3004
|
+
const stateEntry = entry;
|
|
3005
|
+
const expectedBinding = config.binding?.trim() || naming.pipelineBindingKey(config.logicalName);
|
|
3006
|
+
const changes = [];
|
|
3007
|
+
if (stateEntry.sql !== config.sql) changes.push({
|
|
3008
|
+
field: "sql",
|
|
3009
|
+
from: truncate(stateEntry.sql),
|
|
3010
|
+
to: truncate(config.sql),
|
|
3011
|
+
kind: "immutable"
|
|
3012
|
+
});
|
|
3013
|
+
if (stateEntry.bindingKey !== expectedBinding) changes.push({
|
|
3014
|
+
field: "binding",
|
|
3015
|
+
from: stateEntry.bindingKey,
|
|
3016
|
+
to: expectedBinding,
|
|
3017
|
+
kind: "mutable"
|
|
3018
|
+
});
|
|
3019
|
+
if (changes.length === 0) continue;
|
|
3020
|
+
const hasImmutable = changes.some((c) => c.kind === "immutable");
|
|
3021
|
+
items.push({
|
|
3022
|
+
kind: "pipeline",
|
|
3023
|
+
action: hasImmutable ? "replace" : "update",
|
|
3024
|
+
logicalName: config.logicalName,
|
|
3025
|
+
derivedName,
|
|
3026
|
+
detail: changes.map((c) => c.field).join(", "),
|
|
3027
|
+
changes
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
return items;
|
|
3031
|
+
}
|
|
3032
|
+
function truncate(v) {
|
|
3033
|
+
return v.length > 60 ? `${v.slice(0, 57)}...` : v;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/features/pipelines/pipelines.module.ts
|
|
3038
|
+
const pipelinesModule = {
|
|
3039
|
+
kind: "pipeline",
|
|
3040
|
+
label: "Pipeline",
|
|
3041
|
+
configKey: "pipelines",
|
|
3042
|
+
stateEntryType: "pipeline",
|
|
3043
|
+
async fetchAll(api) {
|
|
3044
|
+
return (await api.pipelineListAll()).map((p) => ({
|
|
3045
|
+
id: p.id,
|
|
3046
|
+
name: p.name,
|
|
3047
|
+
sql: p.sql,
|
|
3048
|
+
status: p.status
|
|
3049
|
+
}));
|
|
3050
|
+
},
|
|
3051
|
+
async apply(ctx) {
|
|
3052
|
+
await pipelinesApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
3053
|
+
},
|
|
3054
|
+
sync(ctx) {
|
|
3055
|
+
pipelinesSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
3056
|
+
},
|
|
3057
|
+
drift(ctx) {
|
|
3058
|
+
return pipelinesDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3059
|
+
},
|
|
3060
|
+
diff(ctx) {
|
|
3061
|
+
return pipelinesDiffPlanItems({
|
|
3062
|
+
resources: ctx.resources,
|
|
3063
|
+
env: ctx.env,
|
|
3064
|
+
state: ctx.state,
|
|
3065
|
+
naming: ctx.naming
|
|
3066
|
+
});
|
|
3067
|
+
},
|
|
3068
|
+
async destroy(ctx) {
|
|
3069
|
+
await pipelinesDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
3070
|
+
},
|
|
3071
|
+
status(ctx) {
|
|
3072
|
+
return pipelinesStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3073
|
+
},
|
|
3074
|
+
generate(ctx) {
|
|
3075
|
+
const generated = ctx.resources.length > 0 ? pipelinesGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
|
|
3076
|
+
const passthrough = ctx.passthrough?.pipelines ?? [];
|
|
3077
|
+
if (generated.length === 0 && passthrough.length === 0) return {};
|
|
3078
|
+
return { pipelines: [...passthrough, ...generated] };
|
|
3079
|
+
},
|
|
3080
|
+
pickResources(source) {
|
|
3081
|
+
return resourcesFrom(source)?.pipelines ?? [];
|
|
3082
|
+
},
|
|
3083
|
+
async destroyOne({ api, state, key, entry }) {
|
|
3084
|
+
if (entry.type !== "pipeline") return;
|
|
3085
|
+
try {
|
|
3086
|
+
await api.pipelineDelete(entry.cfId);
|
|
3087
|
+
state.delete(key);
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
console.warn(`Rollback: failed to delete Pipeline ${entry.derivedName}:`, err);
|
|
3090
|
+
}
|
|
3091
|
+
},
|
|
3092
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
3093
|
+
const pipelineId = options.cfId;
|
|
3094
|
+
if (!pipelineId) throw new Error("import pipeline: --cf-id <pipeline-id> is required");
|
|
3095
|
+
const hit = (await api.pipelineListAll()).find((p) => p.id === pipelineId);
|
|
3096
|
+
if (!hit) throw new Error(`import pipeline: pipeline "${pipelineId}" not found in account`);
|
|
3097
|
+
const derivedName = naming.pipelineName(options.logical, env);
|
|
3098
|
+
if (hit.name !== derivedName) throw new Error(`import pipeline: cf name "${hit.name}" does not match derived "${derivedName}"`);
|
|
3099
|
+
const key = `pipeline:${derivedName}`;
|
|
3100
|
+
const existing = state.get(key);
|
|
3101
|
+
const entry = {
|
|
3102
|
+
type: "pipeline",
|
|
3103
|
+
logicalName: options.logical,
|
|
3104
|
+
derivedName,
|
|
3105
|
+
bindingKey: naming.pipelineBindingKey(options.logical),
|
|
3106
|
+
cfId: hit.id,
|
|
3107
|
+
sql: hit.sql,
|
|
3108
|
+
status: hit.status,
|
|
3109
|
+
createdAt: existing?.type === "pipeline" ? existing.createdAt : ts,
|
|
3110
|
+
updatedAt: ts
|
|
3111
|
+
};
|
|
3112
|
+
state.set(key, entry);
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
|
|
3116
|
+
//#endregion
|
|
3117
|
+
//#region src/features/workflows/workflows.diff.ts
|
|
3118
|
+
function workflowsDiffPlanItems(args) {
|
|
3119
|
+
const { resources, env, state, naming, worker } = args;
|
|
3120
|
+
const items = [];
|
|
3121
|
+
for (const config of resources) {
|
|
3122
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3123
|
+
const key = `workflow:${derivedName}`;
|
|
3124
|
+
const entry = state.get(key);
|
|
3125
|
+
if (!entry || entry.type !== "workflow") continue;
|
|
3126
|
+
const changes = computeWorkflowChanges(entry, config, naming, worker);
|
|
3127
|
+
if (changes.length === 0) continue;
|
|
3128
|
+
items.push({
|
|
3129
|
+
kind: "workflow",
|
|
3130
|
+
action: "update",
|
|
3131
|
+
logicalName: config.logicalName,
|
|
3132
|
+
derivedName,
|
|
3133
|
+
detail: changes.map((c) => `${c.field}: ${formatVal(c.from)} -> ${formatVal(c.to)}`).join(", "),
|
|
3134
|
+
changes
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
return items;
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Pure comparison shared with `workflowsApply` so plan and apply agree
|
|
3141
|
+
* on what counts as drift on a registered workflow. Drift in any of
|
|
3142
|
+
* `className`, `scriptName` (defaulting to the owning worker's
|
|
3143
|
+
* deployed name), `limits.steps`, or the binding key triggers an
|
|
3144
|
+
* in-place PUT — Cloudflare treats `PUT /accounts/{id}/workflows/{name}`
|
|
3145
|
+
* as create-or-update.
|
|
3146
|
+
*/
|
|
3147
|
+
function computeWorkflowChanges(state, config, naming, worker) {
|
|
3148
|
+
const expectedScriptName = config.scriptName?.trim() ?? worker?.deployedName;
|
|
3149
|
+
const expectedBindingKey = config.binding?.trim() || naming.workflowBindingKey(config.logicalName);
|
|
3150
|
+
const changes = [];
|
|
3151
|
+
if (state.className !== config.className) changes.push({
|
|
3152
|
+
field: "className",
|
|
3153
|
+
from: state.className,
|
|
3154
|
+
to: config.className,
|
|
3155
|
+
kind: "mutable"
|
|
3156
|
+
});
|
|
3157
|
+
if (expectedScriptName !== void 0 && state.scriptName !== expectedScriptName) changes.push({
|
|
3158
|
+
field: "scriptName",
|
|
3159
|
+
from: state.scriptName,
|
|
3160
|
+
to: expectedScriptName,
|
|
3161
|
+
kind: "mutable"
|
|
3162
|
+
});
|
|
3163
|
+
const stateSteps = state.limits?.steps;
|
|
3164
|
+
const configSteps = config.limits?.steps;
|
|
3165
|
+
if ((stateSteps ?? null) !== (configSteps ?? null)) changes.push({
|
|
3166
|
+
field: "limits.steps",
|
|
3167
|
+
from: stateSteps,
|
|
3168
|
+
to: configSteps,
|
|
3169
|
+
kind: "mutable"
|
|
3170
|
+
});
|
|
3171
|
+
if (state.bindingKey !== expectedBindingKey) changes.push({
|
|
3172
|
+
field: "binding",
|
|
3173
|
+
from: state.bindingKey,
|
|
3174
|
+
to: expectedBindingKey,
|
|
3175
|
+
kind: "mutable"
|
|
3176
|
+
});
|
|
3177
|
+
return changes;
|
|
3178
|
+
}
|
|
3179
|
+
function formatVal(v) {
|
|
3180
|
+
if (v === void 0 || v === null) return "(unset)";
|
|
3181
|
+
if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
|
|
3182
|
+
return String(v);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
//#endregion
|
|
3186
|
+
//#region src/features/workflows/workflows.apply.ts
|
|
3187
|
+
const STATE_KEY_PREFIX$1 = "workflow:";
|
|
3188
|
+
function stateKey$1(derivedName) {
|
|
3189
|
+
return `${STATE_KEY_PREFIX$1}${derivedName}`;
|
|
3190
|
+
}
|
|
3191
|
+
function resolveScriptName(config, worker) {
|
|
3192
|
+
const explicit = config.scriptName?.trim();
|
|
3193
|
+
if (explicit) return explicit;
|
|
3194
|
+
if (!worker) throw new Error(`workflow "${config.logicalName}": no scriptName given and module ran without a worker scope — set scriptName on the resource or invoke apply per-worker.`);
|
|
3195
|
+
return worker.deployedName;
|
|
3196
|
+
}
|
|
3197
|
+
function limitsEqual(a, b) {
|
|
3198
|
+
return (a?.steps ?? null) === (b?.steps ?? null);
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Idempotent upsert via `PUT /accounts/{id}/workflows/{name}`. Cloudflare
|
|
3202
|
+
* treats PUT as create-or-update, so we issue it whenever the recorded
|
|
3203
|
+
* `(class_name, script_name, limits)` triple has drifted from config (or
|
|
3204
|
+
* the workflow has never been registered at all). Drift between recorded
|
|
3205
|
+
* state and *live* Cloudflare is surfaced separately by `workflowsDrift`.
|
|
3206
|
+
*/
|
|
3207
|
+
async function workflowsApply(resources, _tenant, env, api, state, naming, worker) {
|
|
3208
|
+
for (const config of resources) {
|
|
3209
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3210
|
+
const scriptName = resolveScriptName(config, worker);
|
|
3211
|
+
const key = stateKey$1(derivedName);
|
|
3212
|
+
const existing = state.get(key);
|
|
3213
|
+
const wantsBindingKey = config.binding?.trim() || naming.workflowBindingKey(config.logicalName);
|
|
3214
|
+
if (existing && existing.type === "workflow" && existing.className === config.className && existing.scriptName === scriptName && limitsEqual(existing.limits, config.limits) && existing.bindingKey === wantsBindingKey) continue;
|
|
3215
|
+
if (existing && existing.type === "workflow") {
|
|
3216
|
+
const changes = computeWorkflowChanges(existing, config, naming, worker);
|
|
3217
|
+
logApplyChange({
|
|
3218
|
+
kind: "workflow",
|
|
3219
|
+
action: "update",
|
|
3220
|
+
logical: config.logicalName,
|
|
3221
|
+
derived: derivedName,
|
|
3222
|
+
changes
|
|
3223
|
+
});
|
|
3224
|
+
} else logApplyChange({
|
|
3225
|
+
kind: "workflow",
|
|
3226
|
+
action: "create",
|
|
3227
|
+
logical: config.logicalName,
|
|
3228
|
+
derived: derivedName
|
|
3229
|
+
});
|
|
3230
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3231
|
+
const upserted = await api.workflowUpsert(derivedName, {
|
|
3232
|
+
class_name: config.className,
|
|
3233
|
+
script_name: scriptName,
|
|
3234
|
+
...config.limits ? { limits: config.limits } : {}
|
|
3235
|
+
});
|
|
3236
|
+
const entry = {
|
|
3237
|
+
type: "workflow",
|
|
3238
|
+
logicalName: config.logicalName,
|
|
3239
|
+
derivedName,
|
|
3240
|
+
bindingKey: wantsBindingKey,
|
|
3241
|
+
cfId: upserted.id,
|
|
3242
|
+
className: upserted.class_name,
|
|
3243
|
+
scriptName: upserted.script_name,
|
|
3244
|
+
limits: config.limits,
|
|
3245
|
+
createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
|
|
3246
|
+
updatedAt: ts
|
|
3247
|
+
};
|
|
3248
|
+
state.set(key, entry);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
//#endregion
|
|
3253
|
+
//#region src/features/workflows/workflows.sync.ts
|
|
3254
|
+
/**
|
|
3255
|
+
* Match each declared workflow against `GET /accounts/{id}/workflows` and
|
|
3256
|
+
* upsert into state. Stack-scoped: `resources` is already filtered to the
|
|
3257
|
+
* current config, so we never persist a workflow owned by another stack
|
|
3258
|
+
* sharing this state row.
|
|
3259
|
+
*/
|
|
3260
|
+
function workflowsSync(allWorkflows, resources, _tenant, env, state, naming) {
|
|
3261
|
+
for (const config of resources) {
|
|
3262
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3263
|
+
const match = allWorkflows.find((w) => w.name === derivedName);
|
|
3264
|
+
if (!match) continue;
|
|
3265
|
+
const key = `workflow:${derivedName}`;
|
|
3266
|
+
const existing = state.get(key);
|
|
3267
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3268
|
+
const entry = {
|
|
3269
|
+
type: "workflow",
|
|
3270
|
+
logicalName: config.logicalName,
|
|
3271
|
+
derivedName,
|
|
3272
|
+
bindingKey: config.binding?.trim() || naming.workflowBindingKey(config.logicalName),
|
|
3273
|
+
cfId: match.id,
|
|
3274
|
+
className: match.class_name,
|
|
3275
|
+
scriptName: match.script_name,
|
|
3276
|
+
limits: existing?.type === "workflow" ? existing.limits : config.limits,
|
|
3277
|
+
createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
|
|
3278
|
+
updatedAt: ts
|
|
3279
|
+
};
|
|
3280
|
+
state.set(key, entry);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
//#endregion
|
|
3285
|
+
//#region src/features/workflows/workflows.drift.ts
|
|
3286
|
+
function workflowsDrift(allWorkflows, resources, env, state, naming) {
|
|
3287
|
+
const drift = {
|
|
3288
|
+
kind: "workflow",
|
|
3289
|
+
missingFromCloudflare: [],
|
|
3290
|
+
unrecordedInState: [],
|
|
3291
|
+
undeployed: []
|
|
3292
|
+
};
|
|
3293
|
+
const cfByName = new Map(allWorkflows.map((w) => [w.name, w]));
|
|
3294
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "workflow");
|
|
3295
|
+
for (const config of resources) {
|
|
3296
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3297
|
+
const inCloudflare = cfByName.has(derivedName);
|
|
3298
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
3299
|
+
if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
|
|
3300
|
+
logicalName: stateEntry.logicalName,
|
|
3301
|
+
derivedName: stateEntry.derivedName,
|
|
3302
|
+
cfId: stateEntry.cfId
|
|
3303
|
+
});
|
|
3304
|
+
else if (inCloudflare && !stateEntry) {
|
|
3305
|
+
const cf = cfByName.get(derivedName);
|
|
3306
|
+
drift.unrecordedInState.push({
|
|
3307
|
+
logicalName: config.logicalName,
|
|
3308
|
+
derivedName,
|
|
3309
|
+
cfId: cf.id
|
|
3310
|
+
});
|
|
3311
|
+
} else if (!inCloudflare && !stateEntry) drift.undeployed.push({
|
|
3312
|
+
logicalName: config.logicalName,
|
|
3313
|
+
derivedName
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
return drift;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
//#endregion
|
|
3320
|
+
//#region src/features/workflows/workflows.destroy.ts
|
|
3321
|
+
async function workflowsDestroy(_env, state, api, config, baseDir, _force) {
|
|
3322
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "workflow");
|
|
3323
|
+
const resources = state.getAll();
|
|
3324
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "workflow");
|
|
3325
|
+
for (const [key, entry] of entries) {
|
|
3326
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
3327
|
+
try {
|
|
3328
|
+
await api.workflowDelete(entry.derivedName);
|
|
3329
|
+
state.delete(key);
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
console.warn(`Failed to delete Workflow ${entry.derivedName}:`, err);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
//#endregion
|
|
3337
|
+
//#region src/features/workflows/workflows.generate.ts
|
|
3338
|
+
/**
|
|
3339
|
+
* Emit `workflows[]` bindings for declared workflows. Requires `tamer apply`
|
|
3340
|
+
* to have run (state row present) so we can fail fast with a clear message
|
|
3341
|
+
* before `wrangler deploy` would surface a less actionable error.
|
|
3342
|
+
*
|
|
3343
|
+
* `script_name` is only emitted when the user pinned an explicit override
|
|
3344
|
+
* — without it, Wrangler binds to the current worker's own script, which
|
|
3345
|
+
* matches our default-to-owning-worker semantics in apply.
|
|
3346
|
+
*/
|
|
3347
|
+
function workflowsGenerate(resources, env, state, naming, worker) {
|
|
3348
|
+
const out = [];
|
|
3349
|
+
for (const config of resources) {
|
|
3350
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3351
|
+
const entry = state.get(`workflow:${derivedName}`);
|
|
3352
|
+
if (!entry || entry.type !== "workflow") throw new Error(`Workflow "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
|
|
3353
|
+
const explicitScript = config.scriptName?.trim();
|
|
3354
|
+
const ownerScript = worker?.deployedName;
|
|
3355
|
+
const binding = {
|
|
3356
|
+
binding: entry.bindingKey,
|
|
3357
|
+
name: entry.derivedName,
|
|
3358
|
+
class_name: entry.className
|
|
3359
|
+
};
|
|
3360
|
+
if (explicitScript) binding.script_name = explicitScript;
|
|
3361
|
+
else if (ownerScript && ownerScript !== entry.scriptName) binding.script_name = entry.scriptName;
|
|
3362
|
+
out.push(binding);
|
|
3363
|
+
}
|
|
3364
|
+
return out;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
//#endregion
|
|
3368
|
+
//#region src/features/workflows/workflows.status.ts
|
|
3369
|
+
function workflowsStatus(resources, env, state, naming) {
|
|
3370
|
+
const out = [];
|
|
3371
|
+
for (const config of resources) {
|
|
3372
|
+
const derivedName = naming.workflowName(config.logicalName, env);
|
|
3373
|
+
const entry = state.get(`workflow:${derivedName}`);
|
|
3374
|
+
out.push({
|
|
3375
|
+
binding: config.binding?.trim() || naming.workflowBindingKey(config.logicalName),
|
|
3376
|
+
name: derivedName,
|
|
3377
|
+
status: entry ? "ok" : "missing"
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
return out;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
//#endregion
|
|
3384
|
+
//#region src/features/workflows/workflows.module.ts
|
|
3385
|
+
const workflowsModule = {
|
|
3386
|
+
kind: "workflow",
|
|
3387
|
+
label: "Workflow",
|
|
3388
|
+
configKey: "workflows",
|
|
3389
|
+
stateEntryType: "workflow",
|
|
3390
|
+
async fetchAll(api) {
|
|
3391
|
+
return (await api.workflowListAll()).map((w) => ({
|
|
3392
|
+
id: w.id,
|
|
3393
|
+
name: w.name,
|
|
3394
|
+
class_name: w.class_name,
|
|
3395
|
+
script_name: w.script_name
|
|
3396
|
+
}));
|
|
3397
|
+
},
|
|
3398
|
+
async apply(ctx) {
|
|
3399
|
+
await workflowsApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming, ctx.worker);
|
|
3400
|
+
},
|
|
3401
|
+
sync(ctx) {
|
|
3402
|
+
workflowsSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
3403
|
+
},
|
|
3404
|
+
drift(ctx) {
|
|
3405
|
+
return workflowsDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3406
|
+
},
|
|
3407
|
+
diff(ctx) {
|
|
3408
|
+
return workflowsDiffPlanItems({
|
|
3409
|
+
resources: ctx.resources,
|
|
3410
|
+
env: ctx.env,
|
|
3411
|
+
state: ctx.state,
|
|
3412
|
+
naming: ctx.naming,
|
|
3413
|
+
worker: ctx.worker
|
|
3414
|
+
});
|
|
3415
|
+
},
|
|
3416
|
+
async destroy(ctx) {
|
|
3417
|
+
await workflowsDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
3418
|
+
},
|
|
3419
|
+
status(ctx) {
|
|
3420
|
+
return workflowsStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3421
|
+
},
|
|
3422
|
+
generate(ctx) {
|
|
3423
|
+
const generated = ctx.resources.length > 0 ? workflowsGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming, ctx.worker) : [];
|
|
3424
|
+
const passthrough = ctx.passthrough?.workflows ?? [];
|
|
3425
|
+
if (generated.length === 0 && passthrough.length === 0) return {};
|
|
3426
|
+
return { workflows: [...passthrough, ...generated] };
|
|
3427
|
+
},
|
|
3428
|
+
pickResources(source) {
|
|
3429
|
+
return resourcesFrom(source)?.workflows ?? [];
|
|
3430
|
+
},
|
|
3431
|
+
async destroyOne({ api, state, key, entry }) {
|
|
3432
|
+
if (entry.type !== "workflow") return;
|
|
3433
|
+
try {
|
|
3434
|
+
await api.workflowDelete(entry.derivedName);
|
|
3435
|
+
state.delete(key);
|
|
3436
|
+
} catch (err) {
|
|
3437
|
+
console.warn(`Rollback: failed to delete Workflow ${entry.derivedName}:`, err);
|
|
3438
|
+
}
|
|
3439
|
+
},
|
|
3440
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
3441
|
+
const all = await api.workflowListAll();
|
|
3442
|
+
const derivedName = naming.workflowName(options.logical, env);
|
|
3443
|
+
const hit = all.find((w) => w.name === derivedName);
|
|
3444
|
+
if (!hit) throw new Error(`import workflow: no workflow named "${derivedName}" found in account`);
|
|
3445
|
+
if (options.cfId && hit.id !== options.cfId) throw new Error(`import workflow: cf id "${hit.id}" does not match --cf-id "${options.cfId}"`);
|
|
3446
|
+
const key = `workflow:${derivedName}`;
|
|
3447
|
+
const existing = state.get(key);
|
|
3448
|
+
const entry = {
|
|
3449
|
+
type: "workflow",
|
|
3450
|
+
logicalName: options.logical,
|
|
3451
|
+
derivedName,
|
|
3452
|
+
bindingKey: naming.workflowBindingKey(options.logical),
|
|
3453
|
+
cfId: hit.id,
|
|
3454
|
+
className: hit.class_name,
|
|
3455
|
+
scriptName: hit.script_name,
|
|
3456
|
+
limits: existing?.type === "workflow" ? existing.limits : void 0,
|
|
3457
|
+
createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
|
|
3458
|
+
updatedAt: ts
|
|
3459
|
+
};
|
|
3460
|
+
state.set(key, entry);
|
|
3461
|
+
}
|
|
3462
|
+
};
|
|
3463
|
+
|
|
3464
|
+
//#endregion
|
|
3465
|
+
//#region src/features/secrets-store/secrets-store.apply.ts
|
|
3466
|
+
const STATE_KEY_PREFIX = "secrets_store:";
|
|
3467
|
+
function stateKey(derivedName) {
|
|
3468
|
+
return `${STATE_KEY_PREFIX}${derivedName}`;
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Idempotent provisioning for Secrets Store stores. The Cloudflare create
|
|
3472
|
+
* endpoint is non-idempotent (a duplicate name returns 409), so we list
|
|
3473
|
+
* first and short-circuit when a matching name already exists. Secret
|
|
3474
|
+
* **values** are deliberately out of scope — Tamer only manages the
|
|
3475
|
+
* account-scoped container. See `SecretsStoreSecretBinding` for how
|
|
3476
|
+
* worker bindings reference the resolved `store_id`.
|
|
3477
|
+
*/
|
|
3478
|
+
async function secretsStoreApply(resources, _tenant, env, api, state, naming) {
|
|
3479
|
+
if (resources.length === 0) return;
|
|
3480
|
+
let allStores = null;
|
|
3481
|
+
for (const config of resources) {
|
|
3482
|
+
const derivedName = naming.secretsStoreName(config.logicalName, env);
|
|
3483
|
+
const key = stateKey(derivedName);
|
|
3484
|
+
const existing = state.get(key);
|
|
3485
|
+
const wantsBindingKey = naming.secretsStoreBindingKey(config.logicalName);
|
|
3486
|
+
if (existing && existing.type === "secrets_store" && existing.bindingKey === wantsBindingKey) continue;
|
|
3487
|
+
if (allStores === null) allStores = await api.secretsStoreListAll();
|
|
3488
|
+
const match = allStores.find((s) => s.name === derivedName);
|
|
3489
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3490
|
+
let cfId;
|
|
3491
|
+
if (match) cfId = match.id;
|
|
3492
|
+
else cfId = (await api.secretsStoreCreate(derivedName)).id;
|
|
3493
|
+
const entry = {
|
|
3494
|
+
type: "secrets_store",
|
|
3495
|
+
logicalName: config.logicalName,
|
|
3496
|
+
derivedName,
|
|
3497
|
+
bindingKey: wantsBindingKey,
|
|
3498
|
+
cfId,
|
|
3499
|
+
createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
|
|
3500
|
+
updatedAt: ts
|
|
3501
|
+
};
|
|
3502
|
+
state.set(key, entry);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
//#endregion
|
|
3507
|
+
//#region src/features/secrets-store/secrets-store.sync.ts
|
|
3508
|
+
/**
|
|
3509
|
+
* Match each declared store against `GET /accounts/{id}/secrets_store/stores`
|
|
3510
|
+
* and upsert into state. Stack-scoped: `resources` is already filtered to
|
|
3511
|
+
* the current config so we never persist a store owned by another stack
|
|
3512
|
+
* sharing this state row.
|
|
3513
|
+
*/
|
|
3514
|
+
function secretsStoreSync(allStores, resources, _tenant, env, state, naming) {
|
|
3515
|
+
for (const config of resources) {
|
|
3516
|
+
const derivedName = naming.secretsStoreName(config.logicalName, env);
|
|
3517
|
+
const match = allStores.find((s) => s.name === derivedName);
|
|
3518
|
+
if (!match) continue;
|
|
3519
|
+
const key = `secrets_store:${derivedName}`;
|
|
3520
|
+
const existing = state.get(key);
|
|
3521
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3522
|
+
const entry = {
|
|
3523
|
+
type: "secrets_store",
|
|
3524
|
+
logicalName: config.logicalName,
|
|
3525
|
+
derivedName,
|
|
3526
|
+
bindingKey: naming.secretsStoreBindingKey(config.logicalName),
|
|
3527
|
+
cfId: match.id,
|
|
3528
|
+
createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
|
|
3529
|
+
updatedAt: ts
|
|
3530
|
+
};
|
|
3531
|
+
state.set(key, entry);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
//#endregion
|
|
3536
|
+
//#region src/features/secrets-store/secrets-store.drift.ts
|
|
3537
|
+
function secretsStoreDrift(allStores, resources, env, state, naming) {
|
|
3538
|
+
const drift = {
|
|
3539
|
+
kind: "secret_store",
|
|
3540
|
+
missingFromCloudflare: [],
|
|
3541
|
+
unrecordedInState: [],
|
|
3542
|
+
undeployed: []
|
|
3543
|
+
};
|
|
3544
|
+
const cfByName = new Map(allStores.map((s) => [s.name, s]));
|
|
3545
|
+
const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "secrets_store");
|
|
3546
|
+
for (const config of resources) {
|
|
3547
|
+
const derivedName = naming.secretsStoreName(config.logicalName, env);
|
|
3548
|
+
const inCloudflare = cfByName.has(derivedName);
|
|
3549
|
+
const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
|
|
3550
|
+
if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
|
|
3551
|
+
logicalName: stateEntry.logicalName,
|
|
3552
|
+
derivedName: stateEntry.derivedName,
|
|
3553
|
+
cfId: stateEntry.cfId
|
|
3554
|
+
});
|
|
3555
|
+
else if (inCloudflare && !stateEntry) {
|
|
3556
|
+
const cf = cfByName.get(derivedName);
|
|
3557
|
+
drift.unrecordedInState.push({
|
|
3558
|
+
logicalName: config.logicalName,
|
|
3559
|
+
derivedName,
|
|
3560
|
+
cfId: cf.id
|
|
3561
|
+
});
|
|
3562
|
+
} else if (!inCloudflare && !stateEntry) drift.undeployed.push({
|
|
3563
|
+
logicalName: config.logicalName,
|
|
3564
|
+
derivedName
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
return drift;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
//#endregion
|
|
3571
|
+
//#region src/features/secrets-store/secrets-store.destroy.ts
|
|
3572
|
+
async function secretsStoreDestroy(_env, state, api, config, baseDir, _force) {
|
|
3573
|
+
const owned = await logicalNamesForResourceKind(config, baseDir, "secret_store");
|
|
3574
|
+
const resources = state.getAll();
|
|
3575
|
+
const entries = Object.entries(resources).filter((kv) => kv[1].type === "secrets_store");
|
|
3576
|
+
for (const [key, entry] of entries) {
|
|
3577
|
+
if (!owned.has(entry.logicalName)) continue;
|
|
3578
|
+
try {
|
|
3579
|
+
await api.secretsStoreDelete(entry.cfId);
|
|
3580
|
+
state.delete(key);
|
|
3581
|
+
} catch (err) {
|
|
3582
|
+
console.warn(`Failed to delete Secrets Store ${entry.derivedName}:`, err);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
//#endregion
|
|
3588
|
+
//#region src/features/secrets-store/secrets-store.status.ts
|
|
3589
|
+
function secretsStoreStatus(resources, env, state, naming) {
|
|
3590
|
+
const out = [];
|
|
3591
|
+
for (const config of resources) {
|
|
3592
|
+
const derivedName = naming.secretsStoreName(config.logicalName, env);
|
|
3593
|
+
const entry = state.get(`secrets_store:${derivedName}`);
|
|
3594
|
+
out.push({
|
|
3595
|
+
binding: naming.secretsStoreBindingKey(config.logicalName),
|
|
3596
|
+
name: derivedName,
|
|
3597
|
+
cfId: entry?.cfId,
|
|
3598
|
+
status: entry ? "ok" : "missing"
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
return out;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
//#endregion
|
|
3605
|
+
//#region src/features/secrets-store/secrets-store.module.ts
|
|
3606
|
+
const secretsStoreModule = {
|
|
3607
|
+
kind: "secret_store",
|
|
3608
|
+
label: "Secrets Store",
|
|
3609
|
+
configKey: "secretsStores",
|
|
3610
|
+
stateEntryType: "secrets_store",
|
|
3611
|
+
async fetchAll(api) {
|
|
3612
|
+
return (await api.secretsStoreListAll()).map((s) => ({
|
|
3613
|
+
id: s.id,
|
|
3614
|
+
name: s.name
|
|
3615
|
+
}));
|
|
3616
|
+
},
|
|
3617
|
+
async apply(ctx) {
|
|
3618
|
+
await secretsStoreApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
|
|
3619
|
+
},
|
|
3620
|
+
sync(ctx) {
|
|
3621
|
+
secretsStoreSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
|
|
3622
|
+
},
|
|
3623
|
+
drift(ctx) {
|
|
3624
|
+
return secretsStoreDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3625
|
+
},
|
|
3626
|
+
async destroy(ctx) {
|
|
3627
|
+
await secretsStoreDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
|
|
3628
|
+
},
|
|
3629
|
+
status(ctx) {
|
|
3630
|
+
return secretsStoreStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
|
|
3631
|
+
},
|
|
3632
|
+
generate() {
|
|
3633
|
+
return {};
|
|
3634
|
+
},
|
|
3635
|
+
pickResources(source) {
|
|
3636
|
+
return resourcesFrom(source)?.secretsStores ?? [];
|
|
3637
|
+
},
|
|
3638
|
+
async destroyOne({ api, state, key, entry }) {
|
|
3639
|
+
if (entry.type !== "secrets_store") return;
|
|
3640
|
+
try {
|
|
3641
|
+
await api.secretsStoreDelete(entry.cfId);
|
|
3642
|
+
state.delete(key);
|
|
3643
|
+
} catch (err) {
|
|
3644
|
+
console.warn(`Rollback: failed to delete Secrets Store ${entry.derivedName}:`, err);
|
|
3645
|
+
}
|
|
3646
|
+
},
|
|
3647
|
+
async importOne({ options, env, api, state, naming, ts }) {
|
|
3648
|
+
const all = await api.secretsStoreListAll();
|
|
3649
|
+
const derivedName = naming.secretsStoreName(options.logical, env);
|
|
3650
|
+
const hit = all.find((s) => s.name === derivedName);
|
|
3651
|
+
if (!hit) throw new Error(`import secret_store: no store named "${derivedName}" found in account`);
|
|
3652
|
+
if (options.cfId && hit.id !== options.cfId) throw new Error(`import secret_store: cf id "${hit.id}" does not match --cf-id "${options.cfId}"`);
|
|
3653
|
+
const key = `secrets_store:${derivedName}`;
|
|
3654
|
+
const existing = state.get(key);
|
|
3655
|
+
const entry = {
|
|
3656
|
+
type: "secrets_store",
|
|
3657
|
+
logicalName: options.logical,
|
|
3658
|
+
derivedName,
|
|
3659
|
+
bindingKey: naming.secretsStoreBindingKey(options.logical),
|
|
3660
|
+
cfId: hit.id,
|
|
3661
|
+
createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
|
|
3662
|
+
updatedAt: ts
|
|
3663
|
+
};
|
|
3664
|
+
state.set(key, entry);
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
|
|
3668
|
+
//#endregion
|
|
3669
|
+
//#region src/core/registry/registry.ts
|
|
3670
|
+
const resourceModules = [
|
|
3671
|
+
d1Module,
|
|
3672
|
+
r2Module,
|
|
3673
|
+
kvModule,
|
|
3674
|
+
queuesModule,
|
|
3675
|
+
hyperdriveModule,
|
|
3676
|
+
vectorizeModule,
|
|
3677
|
+
aiGatewayModule,
|
|
3678
|
+
pipelinesModule,
|
|
3679
|
+
workflowsModule,
|
|
3680
|
+
secretsStoreModule
|
|
3681
|
+
];
|
|
3682
|
+
const moduleByKind = new Map(resourceModules.map((m) => [m.kind, m]));
|
|
3683
|
+
/** Lookup by stable kind. Returns `undefined` for unknown kinds. */
|
|
3684
|
+
function getResourceModule(kind) {
|
|
3685
|
+
return moduleByKind.get(kind);
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
//#endregion
|
|
3689
|
+
//#region src/core/imports/fetchStackImports.ts
|
|
3690
|
+
const IMPORT_RE = /\$\{tamer:import:([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
|
|
3691
|
+
/**
|
|
3692
|
+
* Walk the merged `CfiConfig` and collect every `${tamer:import:…}` ref
|
|
3693
|
+
* site along with where it was found. Used both to drive the pre-fetch
|
|
3694
|
+
* (which sibling stacks to load) and by `tamer status` to render an
|
|
3695
|
+
* "inbound imports" panel even before any `apply` has run.
|
|
3696
|
+
*
|
|
3697
|
+
* Self-imports (current stack importing from its own name) are filtered
|
|
3698
|
+
* out — they are almost always a config typo and would otherwise
|
|
3699
|
+
* silently resolve via the same row this command is about to write.
|
|
3700
|
+
*/
|
|
3701
|
+
function scanConfigForImports(config) {
|
|
3702
|
+
const selfStack = stackNameForConfig(config);
|
|
3703
|
+
const refs = [];
|
|
3704
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3705
|
+
const push = (raw, fieldPath) => {
|
|
3706
|
+
if (!raw) return;
|
|
3707
|
+
IMPORT_RE.lastIndex = 0;
|
|
3708
|
+
let m;
|
|
3709
|
+
while ((m = IMPORT_RE.exec(raw)) !== null) {
|
|
3710
|
+
const stack = m[1];
|
|
3711
|
+
const output = m[2];
|
|
3712
|
+
if (stack === selfStack) continue;
|
|
3713
|
+
const key = `${fieldPath}::${stack}.${output}`;
|
|
3714
|
+
if (seen.has(key)) continue;
|
|
3715
|
+
seen.add(key);
|
|
3716
|
+
refs.push({
|
|
3717
|
+
stack,
|
|
3718
|
+
output,
|
|
3719
|
+
fieldPath
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
const walkVars = (vars, pathPrefix) => {
|
|
3724
|
+
if (!vars) return;
|
|
3725
|
+
for (const [k, v] of Object.entries(vars)) push(materializeTamerResolvable(v), `${pathPrefix}.${k}`);
|
|
3726
|
+
};
|
|
3727
|
+
const walkR2BucketNames = (w, pathPrefix) => {
|
|
3728
|
+
if (!w.r2_buckets) return;
|
|
3729
|
+
w.r2_buckets.forEach((b, i) => {
|
|
3730
|
+
if (b.bucket_name === void 0) return;
|
|
3731
|
+
push(materializeTamerResolvable(b.bucket_name), `${pathPrefix}.r2_buckets[${i}].bucket_name`);
|
|
3732
|
+
});
|
|
3733
|
+
};
|
|
3734
|
+
const walkD1DatabaseNames = (w, pathPrefix) => {
|
|
3735
|
+
const d1 = w.resources?.d1;
|
|
3736
|
+
if (!d1) return;
|
|
3737
|
+
d1.forEach((d, i) => {
|
|
3738
|
+
if (d.databaseName === void 0) return;
|
|
3739
|
+
push(materializeTamerResolvable(d.databaseName), `${pathPrefix}.resources.d1[${i}].databaseName`);
|
|
3740
|
+
});
|
|
3741
|
+
};
|
|
3742
|
+
/** Service bindings / WfP namespace strings may carry `${tamer:import:…}`. */
|
|
3743
|
+
const walkBindingsWithRefs = (w, pathPrefix) => {
|
|
3744
|
+
w.services?.forEach((s, i) => {
|
|
3745
|
+
if (s.service === void 0) return;
|
|
3746
|
+
push(materializeTamerResolvable(s.service), `${pathPrefix}.services[${i}].service`);
|
|
3747
|
+
});
|
|
3748
|
+
w.dispatch_namespaces?.forEach((d, i) => {
|
|
3749
|
+
if (d.namespace === void 0) return;
|
|
3750
|
+
push(materializeTamerResolvable(d.namespace), `${pathPrefix}.dispatch_namespaces[${i}].namespace`);
|
|
3751
|
+
});
|
|
3752
|
+
};
|
|
3753
|
+
if (config.worker) {
|
|
3754
|
+
walkVars(config.worker.vars, "worker.vars");
|
|
3755
|
+
walkR2BucketNames(config.worker, "worker");
|
|
3756
|
+
walkD1DatabaseNames(config.worker, "worker");
|
|
3757
|
+
walkBindingsWithRefs(config.worker, "worker");
|
|
3758
|
+
if (config.worker.tamerRoutes) config.worker.tamerRoutes.forEach((r, i) => {
|
|
3759
|
+
push(r.host, `worker.tamerRoutes[${i}].host`);
|
|
3760
|
+
push(r.zone, `worker.tamerRoutes[${i}].zone`);
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
if (config.workers) for (const [key, w] of Object.entries(config.workers)) {
|
|
3764
|
+
walkVars(w.vars, `worker[${key}].vars`);
|
|
3765
|
+
walkR2BucketNames(w, `worker[${key}]`);
|
|
3766
|
+
walkD1DatabaseNames(w, `worker[${key}]`);
|
|
3767
|
+
walkBindingsWithRefs(w, `worker[${key}]`);
|
|
3768
|
+
if (w.tamerRoutes) w.tamerRoutes.forEach((r, i) => {
|
|
3769
|
+
push(r.host, `worker[${key}].tamerRoutes[${i}].host`);
|
|
3770
|
+
push(r.zone, `worker[${key}].tamerRoutes[${i}].zone`);
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
if (config.outputs) for (const [name, source] of Object.entries(config.outputs)) push(materializeTamerResolvable(source), `outputs.${name}`);
|
|
3774
|
+
return refs;
|
|
3775
|
+
}
|
|
3776
|
+
/** Distinct sibling stack names referenced anywhere in `config`. */
|
|
3777
|
+
function importedStackNames(config) {
|
|
3778
|
+
const refs = scanConfigForImports(config);
|
|
3779
|
+
return [...new Set(refs.map((r) => r.stack))].sort();
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Hydrate every imported sibling stack's persisted outputs and return
|
|
3783
|
+
* them shaped for {@link ReferenceContext.imports}.
|
|
3784
|
+
*
|
|
3785
|
+
* - `local` env: returns `{}` immediately. Local mode never persists
|
|
3786
|
+
* state, so cross-stack imports are inherently unresolvable. Callers
|
|
3787
|
+
* running in tolerant mode (`plan`, `status`) will see the placeholder
|
|
3788
|
+
* verbatim; strict callers (`apply`, `deploy`) will fail at resolution
|
|
3789
|
+
* with a clear "no imported stack" message.
|
|
3790
|
+
* - Missing sibling state row: recorded as an empty outputs map, so
|
|
3791
|
+
* `lookupImport` can produce a "no published outputs" diagnostic
|
|
3792
|
+
* (vs. the generic "stack not pre-fetched" error).
|
|
3793
|
+
*
|
|
3794
|
+
* The {@link CFApiClient} is shared with the caller for socket reuse.
|
|
3795
|
+
*/
|
|
3796
|
+
async function fetchStackImports(api, config, env) {
|
|
3797
|
+
const stacks = importedStackNames(config);
|
|
3798
|
+
if (stacks.length === 0 || env === "local") return {};
|
|
3799
|
+
const out = {};
|
|
3800
|
+
for (const stack of stacks) {
|
|
3801
|
+
const sibling = new StateManager(config.tenant.id, env, stack);
|
|
3802
|
+
try {
|
|
3803
|
+
await sibling.hydrate(api);
|
|
3804
|
+
} catch (err) {
|
|
3805
|
+
throw new Error(`Failed to hydrate imported stack "${stack}" from env "${env}": ${err instanceof Error ? err.message : String(err)}`);
|
|
3806
|
+
}
|
|
3807
|
+
const persisted = sibling.getStackOutputs();
|
|
3808
|
+
const flat = {};
|
|
3809
|
+
for (const [name, v] of Object.entries(persisted)) flat[name] = v.value;
|
|
3810
|
+
out[stack] = flat;
|
|
3811
|
+
}
|
|
3812
|
+
return out;
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
//#endregion
|
|
3816
|
+
export { namingFromConfig as _, resourceModules as a, d1SkipsProvisionAndMigrate as c, mergedWorkerConfigForEnv as d, resolveDeployedWorkerName as f, wranglerConfigCliArgs as g, resolveReferencesInString as h, getResourceModule as i, buildIntraStackScriptNameMap as l, rewriteIntraStackServiceTargets as m, importedStackNames as n, logicalNamesForResourceKind as o, resolveWorkerConfig as p, scanConfigForImports as r, d1CloudflareDatabaseName as s, fetchStackImports as t, mergeWorkerConfigForResourcePick as u };
|
|
3817
|
+
//# sourceMappingURL=fetchStackImports-B4ZJahOt.mjs.map
|