@intentius/chant-lexicon-temporal 0.1.5
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/dist/integrity.json +15 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +26 -0
- package/dist/rules/tmp001.ts +53 -0
- package/dist/rules/tmp002.ts +45 -0
- package/dist/rules/tmp010-cron-syntax.ts +64 -0
- package/dist/rules/tmp011-namespace-reference.ts +49 -0
- package/dist/skills/chant-temporal-ops.md +184 -0
- package/dist/skills/chant-temporal.md +201 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +32 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +23 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate.ts +82 -0
- package/src/codegen/package-cli.ts +14 -0
- package/src/codegen/package.ts +55 -0
- package/src/composites/cloud-stack.ts +74 -0
- package/src/composites/composites.test.ts +131 -0
- package/src/composites/dev-stack.ts +81 -0
- package/src/config.ts +150 -0
- package/src/coverage.test.ts +9 -0
- package/src/coverage.ts +37 -0
- package/src/example.test.ts +59 -0
- package/src/generated/lexicon-temporal.json +26 -0
- package/src/index.ts +29 -0
- package/src/lint/post-synth/post-synth.test.ts +152 -0
- package/src/lint/post-synth/tmp010-cron-syntax.ts +64 -0
- package/src/lint/post-synth/tmp011-namespace-reference.ts +49 -0
- package/src/lint/rules/index.ts +2 -0
- package/src/lint/rules/lint-rules.test.ts +150 -0
- package/src/lint/rules/tmp001.ts +53 -0
- package/src/lint/rules/tmp002.ts +45 -0
- package/src/plugin.test.ts +97 -0
- package/src/plugin.ts +286 -0
- package/src/resources.ts +121 -0
- package/src/serializer.test.ts +292 -0
- package/src/serializer.ts +310 -0
- package/src/skills/chant-temporal-ops.md +184 -0
- package/src/skills/chant-temporal.md +201 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate.test.ts +9 -0
- package/src/validate.ts +92 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal serializer tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { temporalSerializer } from "./serializer";
|
|
7
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
8
|
+
|
|
9
|
+
// ── Test helpers ───────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function makeEntity(
|
|
12
|
+
entityType: string,
|
|
13
|
+
props: Record<string, unknown>,
|
|
14
|
+
): Record<string, unknown> {
|
|
15
|
+
return {
|
|
16
|
+
[DECLARABLE_MARKER]: true,
|
|
17
|
+
entityType,
|
|
18
|
+
lexicon: "temporal",
|
|
19
|
+
kind: "resource",
|
|
20
|
+
props,
|
|
21
|
+
attributes: {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeServer(props: Record<string, unknown> = {}): [string, Record<string, unknown>] {
|
|
26
|
+
return ["server", makeEntity("Temporal::Server", props)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeNamespace(name: string, props: Record<string, unknown> = {}): [string, Record<string, unknown>] {
|
|
30
|
+
return [name, makeEntity("Temporal::Namespace", { name, ...props })];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeSearchAttr(name: string, props: Record<string, unknown> = {}): [string, Record<string, unknown>] {
|
|
34
|
+
return [name, makeEntity("Temporal::SearchAttribute", { name, type: "Text", ...props })];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeSchedule(name: string, props: Record<string, unknown> = {}): [string, Record<string, unknown>] {
|
|
38
|
+
return [name, makeEntity("Temporal::Schedule", {
|
|
39
|
+
scheduleId: name,
|
|
40
|
+
spec: { intervals: [{ every: "1d" }] },
|
|
41
|
+
action: { workflowType: "testWorkflow", taskQueue: "test-queue" },
|
|
42
|
+
...props,
|
|
43
|
+
})];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("temporal serializer", () => {
|
|
49
|
+
it("has the correct name", () => {
|
|
50
|
+
expect(temporalSerializer.name).toBe("temporal");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("has rulePrefix TMP", () => {
|
|
54
|
+
expect(temporalSerializer.rulePrefix).toBe("TMP");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("serializes empty map to a string with header comment", () => {
|
|
58
|
+
const result = temporalSerializer.serialize(new Map());
|
|
59
|
+
expect(typeof result).toBe("string");
|
|
60
|
+
expect(result as string).toContain("Generated by chant");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── TemporalServer ─────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
it("serializes TemporalServer dev mode to docker-compose with start-dev command", () => {
|
|
66
|
+
const entities = new Map([makeServer({ mode: "dev" })]);
|
|
67
|
+
const result = temporalSerializer.serialize(entities) as string;
|
|
68
|
+
expect(typeof result).toBe("string");
|
|
69
|
+
expect(result).toContain("temporal server start-dev");
|
|
70
|
+
expect(result).toContain("temporalio/admin-tools");
|
|
71
|
+
expect(result).toContain("7233");
|
|
72
|
+
expect(result).toContain("8080");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("uses default mode 'dev' when mode is not specified", () => {
|
|
76
|
+
const entities = new Map([makeServer()]);
|
|
77
|
+
const result = temporalSerializer.serialize(entities) as string;
|
|
78
|
+
expect(result).toContain("temporal server start-dev");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("serializes TemporalServer full mode with auto-setup + postgresql + UI", () => {
|
|
82
|
+
const entities = new Map([makeServer({ mode: "full" })]);
|
|
83
|
+
const result = temporalSerializer.serialize(entities) as string;
|
|
84
|
+
expect(typeof result).toBe("string");
|
|
85
|
+
expect(result).toContain("temporalio/auto-setup");
|
|
86
|
+
expect(result).toContain("postgresql");
|
|
87
|
+
expect(result).toContain("temporal-ui");
|
|
88
|
+
expect(result).toContain("POSTGRES_USER=temporal");
|
|
89
|
+
expect(result).not.toContain("temporal server start-dev");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("embeds custom version in docker-compose image tag", () => {
|
|
93
|
+
const entities = new Map([makeServer({ version: "1.25.0", mode: "dev" })]);
|
|
94
|
+
const result = temporalSerializer.serialize(entities) as string;
|
|
95
|
+
expect(result).toContain("1.25.0");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns plain string (not SerializerResult) when only TemporalServer present", () => {
|
|
99
|
+
const entities = new Map([makeServer()]);
|
|
100
|
+
const result = temporalSerializer.serialize(entities);
|
|
101
|
+
expect(typeof result).toBe("string");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── TemporalServer + Helm values ──────────────────────────────
|
|
105
|
+
|
|
106
|
+
it("includes helm-values.yaml in files when server + namespace present", () => {
|
|
107
|
+
const entities = new Map([
|
|
108
|
+
makeServer({ version: "1.26.2" }),
|
|
109
|
+
makeNamespace("default"),
|
|
110
|
+
]);
|
|
111
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
112
|
+
expect(typeof result).toBe("object");
|
|
113
|
+
expect(result.files["temporal-helm-values.yaml"]).toBeDefined();
|
|
114
|
+
expect(result.files["temporal-helm-values.yaml"]).toContain("temporalio/server");
|
|
115
|
+
expect(result.files["temporal-helm-values.yaml"]).toContain("1.26.2");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("includes helm chart comment when helmChartVersion is set", () => {
|
|
119
|
+
const entities = new Map([
|
|
120
|
+
makeServer({ helmChartVersion: "0.42.0" }),
|
|
121
|
+
makeNamespace("default"),
|
|
122
|
+
]);
|
|
123
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
124
|
+
expect(result.files["temporal-helm-values.yaml"]).toContain("0.42.0");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── TemporalNamespace ─────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
it("serializes TemporalNamespace to temporal-setup.sh with namespace create command", () => {
|
|
130
|
+
const entities = new Map([makeNamespace("prod", { retention: "30d", description: "Production" })]);
|
|
131
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
132
|
+
expect(typeof result).toBe("object");
|
|
133
|
+
const sh = result.files["temporal-setup.sh"];
|
|
134
|
+
expect(sh).toBeDefined();
|
|
135
|
+
expect(sh).toContain("temporal operator namespace create");
|
|
136
|
+
expect(sh).toContain("--namespace \"prod\"");
|
|
137
|
+
expect(sh).toContain("--retention \"30d\"");
|
|
138
|
+
expect(sh).toContain("--description \"Production\"");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("emits --global-namespace flag when isGlobalNamespace is true", () => {
|
|
142
|
+
const entities = new Map([makeNamespace("global-ns", { isGlobalNamespace: true })]);
|
|
143
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
144
|
+
expect(result.files["temporal-setup.sh"]).toContain("--global-namespace");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("uses default retention of 7d when not specified", () => {
|
|
148
|
+
const entities = new Map([makeNamespace("ns")]);
|
|
149
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
150
|
+
expect(result.files["temporal-setup.sh"]).toContain('"7d"');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── SearchAttribute ────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
it("serializes SearchAttribute to setup.sh with search-attribute create command", () => {
|
|
156
|
+
const entities = new Map([makeSearchAttr("GcpProject", { type: "Text" })]);
|
|
157
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
158
|
+
const sh = result.files["temporal-setup.sh"];
|
|
159
|
+
expect(sh).toContain("temporal operator search-attribute create");
|
|
160
|
+
expect(sh).toContain('--name "GcpProject"');
|
|
161
|
+
expect(sh).toContain("--type Text");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("includes --namespace flag when namespace is specified on SearchAttribute", () => {
|
|
165
|
+
const entities = new Map([makeSearchAttr("Env", { type: "Keyword", namespace: "prod" })]);
|
|
166
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
167
|
+
expect(result.files["temporal-setup.sh"]).toContain('--namespace "prod"');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("omits --namespace flag when namespace is not specified", () => {
|
|
171
|
+
const entities = new Map([makeSearchAttr("Env", { type: "Keyword" })]);
|
|
172
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
173
|
+
expect(result.files["temporal-setup.sh"]).not.toContain("--namespace");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("emits all search attributes in setup.sh", () => {
|
|
177
|
+
const entities = new Map([
|
|
178
|
+
makeSearchAttr("GcpProject", { type: "Text" }),
|
|
179
|
+
makeSearchAttr("CrdbDomain", { type: "Keyword" }),
|
|
180
|
+
makeSearchAttr("DeployPhase", { type: "Int" }),
|
|
181
|
+
]);
|
|
182
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
183
|
+
const sh = result.files["temporal-setup.sh"];
|
|
184
|
+
expect(sh).toContain('"GcpProject"');
|
|
185
|
+
expect(sh).toContain('"CrdbDomain"');
|
|
186
|
+
expect(sh).toContain('"DeployPhase"');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("combines namespaces and search attributes in a single temporal-setup.sh", () => {
|
|
190
|
+
const entities = new Map([
|
|
191
|
+
makeNamespace("deploy-ns"),
|
|
192
|
+
makeSearchAttr("GcpProject", { type: "Text", namespace: "deploy-ns" }),
|
|
193
|
+
]);
|
|
194
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
195
|
+
const sh = result.files["temporal-setup.sh"];
|
|
196
|
+
// Both namespace create and search-attribute create appear in one file
|
|
197
|
+
expect(sh).toContain("temporal operator namespace create");
|
|
198
|
+
expect(sh).toContain("temporal operator search-attribute create");
|
|
199
|
+
// Namespaces before search attributes
|
|
200
|
+
expect(sh.indexOf("namespace create")).toBeLessThan(sh.indexOf("search-attribute create"));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── TemporalSchedule ───────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
it("serializes TemporalSchedule to schedules/<id>.ts with SDK schedule.create call", () => {
|
|
206
|
+
const entities = new Map([makeSchedule("daily-backup")]);
|
|
207
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
208
|
+
const ts = result.files["schedules/daily-backup.ts"];
|
|
209
|
+
expect(ts).toBeDefined();
|
|
210
|
+
expect(ts).toContain("client.schedule.create");
|
|
211
|
+
expect(ts).toContain('"daily-backup"');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("embeds workflowType and taskQueue in schedule code", () => {
|
|
215
|
+
const entities = new Map([makeSchedule("my-sched", {
|
|
216
|
+
action: { workflowType: "myWorkflow", taskQueue: "my-queue" },
|
|
217
|
+
})]);
|
|
218
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
219
|
+
const ts = result.files["schedules/my-sched.ts"];
|
|
220
|
+
expect(ts).toContain('"myWorkflow"');
|
|
221
|
+
expect(ts).toContain('"my-queue"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("generates separate schedule files for each TemporalSchedule entity", () => {
|
|
225
|
+
const entities = new Map([
|
|
226
|
+
makeSchedule("schedule-a"),
|
|
227
|
+
makeSchedule("schedule-b"),
|
|
228
|
+
]);
|
|
229
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
230
|
+
expect(result.files["schedules/schedule-a.ts"]).toBeDefined();
|
|
231
|
+
expect(result.files["schedules/schedule-b.ts"]).toBeDefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses scheduleId from props as the file key", () => {
|
|
235
|
+
const entities = new Map([
|
|
236
|
+
["myEntity", makeEntity("Temporal::Schedule", {
|
|
237
|
+
scheduleId: "custom-id",
|
|
238
|
+
spec: { intervals: [{ every: "1h" }] },
|
|
239
|
+
action: { workflowType: "w", taskQueue: "q" },
|
|
240
|
+
})],
|
|
241
|
+
]);
|
|
242
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
243
|
+
expect(result.files["schedules/custom-id.ts"]).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("includes overlap policy in schedule code when specified", () => {
|
|
247
|
+
const entities = new Map([makeSchedule("with-policy", {
|
|
248
|
+
policies: { overlap: "BufferOne" },
|
|
249
|
+
})]);
|
|
250
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
251
|
+
expect(result.files["schedules/with-policy.ts"]).toContain("BufferOne");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── Mixed entities ─────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
it("returns SerializerResult with all keys for mixed entities", () => {
|
|
257
|
+
const entities = new Map([
|
|
258
|
+
makeServer(),
|
|
259
|
+
makeNamespace("default"),
|
|
260
|
+
makeSearchAttr("Project", { type: "Keyword" }),
|
|
261
|
+
makeSchedule("weekly"),
|
|
262
|
+
]);
|
|
263
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
264
|
+
expect(typeof result).toBe("object");
|
|
265
|
+
expect(result.primary).toContain("temporal server start-dev");
|
|
266
|
+
expect(result.files["temporal-helm-values.yaml"]).toBeDefined();
|
|
267
|
+
expect(result.files["temporal-setup.sh"]).toBeDefined();
|
|
268
|
+
expect(result.files["schedules/weekly.ts"]).toBeDefined();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("primary contains docker-compose content even in mixed mode", () => {
|
|
272
|
+
const entities = new Map([
|
|
273
|
+
makeServer({ mode: "full" }),
|
|
274
|
+
makeNamespace("default"),
|
|
275
|
+
]);
|
|
276
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
277
|
+
expect(result.primary).toContain("temporalio/auto-setup");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("setup.sh uses TEMPORAL_ADDRESS env var", () => {
|
|
281
|
+
const entities = new Map([makeNamespace("ns")]);
|
|
282
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
283
|
+
expect(result.files["temporal-setup.sh"]).toContain("TEMPORAL_ADDRESS");
|
|
284
|
+
expect(result.files["temporal-setup.sh"]).toContain("localhost:7233");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("setup.sh has set -euo pipefail", () => {
|
|
288
|
+
const entities = new Map([makeNamespace("ns")]);
|
|
289
|
+
const result = temporalSerializer.serialize(entities) as { primary: string; files: Record<string, string> };
|
|
290
|
+
expect(result.files["temporal-setup.sh"]).toContain("set -euo pipefail");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal serializer.
|
|
3
|
+
*
|
|
4
|
+
* Routes entities by entityType:
|
|
5
|
+
* Temporal::Server → docker-compose.yml (primary) + temporal-helm-values.yaml (file)
|
|
6
|
+
* Temporal::Namespace → temporal-setup.sh (file)
|
|
7
|
+
* Temporal::SearchAttribute → temporal-setup.sh (file, appended after namespaces)
|
|
8
|
+
* Temporal::Schedule → schedules/<scheduleId>.ts (file per schedule)
|
|
9
|
+
*
|
|
10
|
+
* Returns a plain string when only TemporalServer entities are present.
|
|
11
|
+
* Returns SerializerResult for any other combination.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
15
|
+
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
16
|
+
import type { TemporalServerProps, TemporalNamespaceProps, SearchAttributeProps, TemporalScheduleProps } from "./resources";
|
|
17
|
+
|
|
18
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function getProps(entity: Declarable): Record<string, unknown> {
|
|
21
|
+
if ("props" in entity && typeof entity.props === "object" && entity.props !== null) {
|
|
22
|
+
return entity.props as Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function entityType(entity: Declarable): string {
|
|
28
|
+
return (entity as Record<string, unknown>).entityType as string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Docker Compose ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const HEADER = "# Generated by chant — do not edit directly.\n# Source of truth: the chant TypeScript project.\n";
|
|
34
|
+
|
|
35
|
+
function serializeDockerCompose(servers: Map<string, Declarable>): string {
|
|
36
|
+
const lines: string[] = [HEADER];
|
|
37
|
+
|
|
38
|
+
if (servers.size === 0) {
|
|
39
|
+
lines.push("# No TemporalServer resources defined.");
|
|
40
|
+
return lines.join("\n") + "\n";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use the first server entity (a project should have exactly one)
|
|
44
|
+
const [, serverEntity] = [...servers.entries()][0];
|
|
45
|
+
const props = getProps(serverEntity) as TemporalServerProps;
|
|
46
|
+
const version = props.version ?? "1.26.2";
|
|
47
|
+
const mode = props.mode ?? "dev";
|
|
48
|
+
const port = props.port ?? 7233;
|
|
49
|
+
const uiPort = props.uiPort ?? 8080;
|
|
50
|
+
const postgresVersion = props.postgresVersion ?? "16-alpine";
|
|
51
|
+
|
|
52
|
+
lines.push("services:");
|
|
53
|
+
|
|
54
|
+
if (mode === "dev") {
|
|
55
|
+
lines.push(` temporal:`);
|
|
56
|
+
lines.push(` image: temporalio/admin-tools:${version}`);
|
|
57
|
+
lines.push(` command: temporal server start-dev --namespace default --ui-port ${uiPort}`);
|
|
58
|
+
lines.push(` ports:`);
|
|
59
|
+
lines.push(` - "${port}:${port}"`);
|
|
60
|
+
lines.push(` - "${uiPort}:${uiPort}"`);
|
|
61
|
+
} else {
|
|
62
|
+
// full mode
|
|
63
|
+
lines.push(` temporal:`);
|
|
64
|
+
lines.push(` image: temporalio/auto-setup:${version}`);
|
|
65
|
+
lines.push(` ports:`);
|
|
66
|
+
lines.push(` - "${port}:7233"`);
|
|
67
|
+
lines.push(` environment:`);
|
|
68
|
+
lines.push(` - DB=postgresql`);
|
|
69
|
+
lines.push(` - DB_PORT=5432`);
|
|
70
|
+
lines.push(` - POSTGRES_USER=temporal`);
|
|
71
|
+
lines.push(` - POSTGRES_PWD=temporal`);
|
|
72
|
+
lines.push(` - POSTGRES_SEEDS=postgresql`);
|
|
73
|
+
lines.push(` depends_on:`);
|
|
74
|
+
lines.push(` - postgresql`);
|
|
75
|
+
lines.push(` temporal-ui:`);
|
|
76
|
+
lines.push(` image: temporalio/ui:${version}`);
|
|
77
|
+
lines.push(` environment:`);
|
|
78
|
+
lines.push(` - TEMPORAL_ADDRESS=temporal:7233`);
|
|
79
|
+
lines.push(` - TEMPORAL_CORS_ORIGINS=http://localhost:3000`);
|
|
80
|
+
lines.push(` ports:`);
|
|
81
|
+
lines.push(` - "${uiPort}:8080"`);
|
|
82
|
+
lines.push(` postgresql:`);
|
|
83
|
+
lines.push(` image: postgres:${postgresVersion}`);
|
|
84
|
+
lines.push(` environment:`);
|
|
85
|
+
lines.push(` - POSTGRES_USER=temporal`);
|
|
86
|
+
lines.push(` - POSTGRES_PASSWORD=temporal`);
|
|
87
|
+
lines.push(` volumes:`);
|
|
88
|
+
lines.push(` - temporal-db:/var/lib/postgresql/data`);
|
|
89
|
+
lines.push(`volumes:`);
|
|
90
|
+
lines.push(` temporal-db: {}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lines.join("\n") + "\n";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Helm Values ──────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function serializeHelmValues(servers: Map<string, Declarable>): string {
|
|
99
|
+
if (servers.size === 0) return "";
|
|
100
|
+
|
|
101
|
+
const [, serverEntity] = [...servers.entries()][0];
|
|
102
|
+
const props = getProps(serverEntity) as TemporalServerProps;
|
|
103
|
+
const version = props.version ?? "1.26.2";
|
|
104
|
+
const port = props.port ?? 7233;
|
|
105
|
+
const chartVersion = props.helmChartVersion;
|
|
106
|
+
|
|
107
|
+
const lines: string[] = [
|
|
108
|
+
"# Generated by chant — do not edit directly.",
|
|
109
|
+
"# Apply: helm install temporal temporal/temporal -f temporal-helm-values.yaml",
|
|
110
|
+
"# Helm charts: https://github.com/temporalio/helm-charts",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (chartVersion) {
|
|
114
|
+
lines.push(`# Chart version: ${chartVersion}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lines.push(
|
|
118
|
+
"server:",
|
|
119
|
+
" replicaCount: 1",
|
|
120
|
+
" image:",
|
|
121
|
+
" repository: temporalio/server",
|
|
122
|
+
` tag: "${version}"`,
|
|
123
|
+
"frontend:",
|
|
124
|
+
" replicaCount: 1",
|
|
125
|
+
" service:",
|
|
126
|
+
` port: ${port}`,
|
|
127
|
+
"web:",
|
|
128
|
+
" enabled: true",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return lines.join("\n") + "\n";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Setup Script ─────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function serializeSetupScript(
|
|
137
|
+
namespaces: Map<string, Declarable>,
|
|
138
|
+
searchAttrs: Map<string, Declarable>,
|
|
139
|
+
): string {
|
|
140
|
+
const lines: string[] = [
|
|
141
|
+
"#!/usr/bin/env bash",
|
|
142
|
+
"# Generated by chant — do not edit directly.",
|
|
143
|
+
"# Run once after the Temporal server is ready.",
|
|
144
|
+
"set -euo pipefail",
|
|
145
|
+
"",
|
|
146
|
+
'TEMPORAL_ADDRESS="${TEMPORAL_ADDRESS:-localhost:7233}"',
|
|
147
|
+
"",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (namespaces.size > 0) {
|
|
151
|
+
lines.push("# ── Namespaces ────────────────────────────────────────────────────────");
|
|
152
|
+
lines.push("");
|
|
153
|
+
for (const [, entity] of namespaces) {
|
|
154
|
+
const props = getProps(entity) as TemporalNamespaceProps;
|
|
155
|
+
const parts: string[] = [
|
|
156
|
+
`temporal operator namespace create \\`,
|
|
157
|
+
` --address "\${TEMPORAL_ADDRESS}" \\`,
|
|
158
|
+
` --namespace "${props.name}" \\`,
|
|
159
|
+
` --retention "${props.retention ?? "7d"}"`,
|
|
160
|
+
];
|
|
161
|
+
if (props.description) {
|
|
162
|
+
// replace last line — remove trailing backslash, add it back with description
|
|
163
|
+
parts[parts.length - 1] += " \\";
|
|
164
|
+
parts.push(` --description "${props.description}"`);
|
|
165
|
+
}
|
|
166
|
+
if (props.isGlobalNamespace) {
|
|
167
|
+
parts[parts.length - 1] += " \\";
|
|
168
|
+
parts.push(` --global-namespace`);
|
|
169
|
+
}
|
|
170
|
+
lines.push(...parts);
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (searchAttrs.size > 0) {
|
|
176
|
+
lines.push("# ── Search Attributes ─────────────────────────────────────────────────");
|
|
177
|
+
lines.push("");
|
|
178
|
+
for (const [, entity] of searchAttrs) {
|
|
179
|
+
const props = getProps(entity) as SearchAttributeProps;
|
|
180
|
+
const parts: string[] = [
|
|
181
|
+
`temporal operator search-attribute create \\`,
|
|
182
|
+
` --address "\${TEMPORAL_ADDRESS}" \\`,
|
|
183
|
+
];
|
|
184
|
+
if (props.namespace) {
|
|
185
|
+
parts.push(` --namespace "${props.namespace}" \\`);
|
|
186
|
+
}
|
|
187
|
+
parts.push(
|
|
188
|
+
` --name "${props.name}" \\`,
|
|
189
|
+
` --type ${props.type}`,
|
|
190
|
+
);
|
|
191
|
+
lines.push(...parts);
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return lines.join("\n") + "\n";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Schedule Code ────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function serializeSchedule(scheduleId: string, props: TemporalScheduleProps): string {
|
|
202
|
+
const lines: string[] = [
|
|
203
|
+
`// Generated by chant — do not edit directly.`,
|
|
204
|
+
`// Run: npx tsx schedules/${scheduleId}.ts`,
|
|
205
|
+
`import { Client, Connection } from "@temporalio/client";`,
|
|
206
|
+
``,
|
|
207
|
+
`async function createSchedule(): Promise<void> {`,
|
|
208
|
+
` const connection = await Connection.connect({`,
|
|
209
|
+
` address: process.env.TEMPORAL_ADDRESS ?? "localhost:7233",`,
|
|
210
|
+
` });`,
|
|
211
|
+
` const client = new Client({`,
|
|
212
|
+
` connection,`,
|
|
213
|
+
` namespace: process.env.TEMPORAL_NAMESPACE ?? "${props.namespace ?? "default"}",`,
|
|
214
|
+
` });`,
|
|
215
|
+
``,
|
|
216
|
+
` await client.schedule.create({`,
|
|
217
|
+
` scheduleId: ${JSON.stringify(props.scheduleId)},`,
|
|
218
|
+
` spec: ${JSON.stringify(props.spec, null, 6).replace(/^/gm, " ").trimStart()},`,
|
|
219
|
+
` action: {`,
|
|
220
|
+
` type: "startWorkflow",`,
|
|
221
|
+
` workflowType: ${JSON.stringify(props.action.workflowType)},`,
|
|
222
|
+
` taskQueue: ${JSON.stringify(props.action.taskQueue)},`,
|
|
223
|
+
` args: ${JSON.stringify(props.action.args ?? [])},`,
|
|
224
|
+
` },`,
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
if (props.policies) {
|
|
228
|
+
lines.push(` policies: ${JSON.stringify(props.policies, null, 6).replace(/^/gm, " ").trimStart()},`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (props.state) {
|
|
232
|
+
lines.push(` state: ${JSON.stringify(props.state, null, 6).replace(/^/gm, " ").trimStart()},`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lines.push(
|
|
236
|
+
` });`,
|
|
237
|
+
``,
|
|
238
|
+
` console.log("Schedule created: ${scheduleId}");`,
|
|
239
|
+
` await connection.close();`,
|
|
240
|
+
`}`,
|
|
241
|
+
``,
|
|
242
|
+
`createSchedule().catch((err: unknown) => {`,
|
|
243
|
+
` console.error(err);`,
|
|
244
|
+
` process.exit(1);`,
|
|
245
|
+
`});`,
|
|
246
|
+
``,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return lines.join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Serializer ───────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export const temporalSerializer: Serializer = {
|
|
255
|
+
name: "temporal",
|
|
256
|
+
rulePrefix: "TMP",
|
|
257
|
+
|
|
258
|
+
serialize(
|
|
259
|
+
entities: Map<string, Declarable>,
|
|
260
|
+
_outputs?: unknown,
|
|
261
|
+
): string | SerializerResult {
|
|
262
|
+
const servers = new Map<string, Declarable>();
|
|
263
|
+
const namespaces = new Map<string, Declarable>();
|
|
264
|
+
const searchAttrs = new Map<string, Declarable>();
|
|
265
|
+
const schedules = new Map<string, Declarable>();
|
|
266
|
+
|
|
267
|
+
for (const [name, entity] of entities) {
|
|
268
|
+
const et = entityType(entity);
|
|
269
|
+
if (et === "Temporal::Server") servers.set(name, entity);
|
|
270
|
+
else if (et === "Temporal::Namespace") namespaces.set(name, entity);
|
|
271
|
+
else if (et === "Temporal::SearchAttribute") searchAttrs.set(name, entity);
|
|
272
|
+
else if (et === "Temporal::Schedule") schedules.set(name, entity);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const primary = serializeDockerCompose(servers);
|
|
276
|
+
|
|
277
|
+
const hasExtraFiles =
|
|
278
|
+
servers.size > 0 || // always emit helm-values when a server exists
|
|
279
|
+
namespaces.size > 0 ||
|
|
280
|
+
searchAttrs.size > 0 ||
|
|
281
|
+
schedules.size > 0;
|
|
282
|
+
|
|
283
|
+
// Only-server case: no extra files needed beyond docker-compose → return string
|
|
284
|
+
if (
|
|
285
|
+
namespaces.size === 0 &&
|
|
286
|
+
searchAttrs.size === 0 &&
|
|
287
|
+
schedules.size === 0
|
|
288
|
+
) {
|
|
289
|
+
return primary;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const files: Record<string, string> = {};
|
|
293
|
+
|
|
294
|
+
if (servers.size > 0) {
|
|
295
|
+
files["temporal-helm-values.yaml"] = serializeHelmValues(servers);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (namespaces.size > 0 || searchAttrs.size > 0) {
|
|
299
|
+
files["temporal-setup.sh"] = serializeSetupScript(namespaces, searchAttrs);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const [name, entity] of schedules) {
|
|
303
|
+
const props = getProps(entity) as TemporalScheduleProps;
|
|
304
|
+
const scheduleId = props.scheduleId ?? name;
|
|
305
|
+
files[`schedules/${scheduleId}.ts`] = serializeSchedule(scheduleId, props);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { primary, files };
|
|
309
|
+
},
|
|
310
|
+
};
|