@rex0220/ksql-flownet 1.0.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +81 -0
  3. package/README.md +117 -0
  4. package/dist/bundle/bundle-builder.d.ts +49 -0
  5. package/dist/bundle/bundle-builder.js +208 -0
  6. package/dist/bundle/index.d.ts +4 -0
  7. package/dist/bundle/index.js +4 -0
  8. package/dist/bundle/upload.d.ts +28 -0
  9. package/dist/bundle/upload.js +72 -0
  10. package/dist/bundle/zip-store.d.ts +17 -0
  11. package/dist/bundle/zip-store.js +175 -0
  12. package/dist/cli/archive-run-command.d.ts +12 -0
  13. package/dist/cli/archive-run-command.js +131 -0
  14. package/dist/cli/cancel-run-command.d.ts +9 -0
  15. package/dist/cli/cancel-run-command.js +77 -0
  16. package/dist/cli/force-unlock-network-command.d.ts +8 -0
  17. package/dist/cli/force-unlock-network-command.js +129 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.js +132 -0
  20. package/dist/cli/plan-command.d.ts +2 -0
  21. package/dist/cli/plan-command.js +100 -0
  22. package/dist/cli/poll-requests-command.d.ts +3 -0
  23. package/dist/cli/poll-requests-command.js +74 -0
  24. package/dist/cli/record-job-unlock-command.d.ts +11 -0
  25. package/dist/cli/record-job-unlock-command.js +78 -0
  26. package/dist/cli/resolve-node-command.d.ts +13 -0
  27. package/dist/cli/resolve-node-command.js +174 -0
  28. package/dist/cli/run-network-command.d.ts +14 -0
  29. package/dist/cli/run-network-command.js +418 -0
  30. package/dist/cli/status-command.d.ts +5 -0
  31. package/dist/cli/status-command.js +137 -0
  32. package/dist/cli/validate-command.d.ts +6 -0
  33. package/dist/cli/validate-command.js +17 -0
  34. package/dist/dag/descendants.d.ts +9 -0
  35. package/dist/dag/descendants.js +67 -0
  36. package/dist/dag/topological-sort.d.ts +7 -0
  37. package/dist/dag/topological-sort.js +32 -0
  38. package/dist/domain/business-key.d.ts +15 -0
  39. package/dist/domain/business-key.js +249 -0
  40. package/dist/domain/canonical-lock-key.d.ts +17 -0
  41. package/dist/domain/canonical-lock-key.js +60 -0
  42. package/dist/domain/canonical-record-key.d.ts +18 -0
  43. package/dist/domain/canonical-record-key.js +70 -0
  44. package/dist/domain/load-network.d.ts +4 -0
  45. package/dist/domain/load-network.js +47 -0
  46. package/dist/domain/network-definition.d.ts +47 -0
  47. package/dist/domain/network-definition.js +7 -0
  48. package/dist/domain/persistence-model.d.ts +198 -0
  49. package/dist/domain/persistence-model.js +2 -0
  50. package/dist/domain/run-aggregate.d.ts +3 -0
  51. package/dist/domain/run-aggregate.js +15 -0
  52. package/dist/domain/validate-network-path.d.ts +11 -0
  53. package/dist/domain/validate-network-path.js +33 -0
  54. package/dist/domain/validate-network.d.ts +3 -0
  55. package/dist/domain/validate-network.js +180 -0
  56. package/dist/executor/attempt-executor.d.ts +43 -0
  57. package/dist/executor/attempt-executor.js +236 -0
  58. package/dist/executor/job-log-reader.d.ts +37 -0
  59. package/dist/executor/job-log-reader.js +91 -0
  60. package/dist/executor/ksql-flow-cli.d.ts +86 -0
  61. package/dist/executor/ksql-flow-cli.js +161 -0
  62. package/dist/executor/preflight.d.ts +41 -0
  63. package/dist/executor/preflight.js +150 -0
  64. package/dist/executor/result-classifier.d.ts +69 -0
  65. package/dist/executor/result-classifier.js +237 -0
  66. package/dist/executor/run-subprocess.d.ts +64 -0
  67. package/dist/executor/run-subprocess.js +175 -0
  68. package/dist/io/input-baseline.d.ts +19 -0
  69. package/dist/io/input-baseline.js +214 -0
  70. package/dist/io/io-config.d.ts +6 -0
  71. package/dist/io/io-config.js +33 -0
  72. package/dist/io/io-path.d.ts +37 -0
  73. package/dist/io/io-path.js +279 -0
  74. package/dist/orchestration/archive-run.d.ts +53 -0
  75. package/dist/orchestration/archive-run.js +323 -0
  76. package/dist/orchestration/cancel-request.d.ts +18 -0
  77. package/dist/orchestration/cancel-request.js +67 -0
  78. package/dist/orchestration/ensure-run.d.ts +83 -0
  79. package/dist/orchestration/ensure-run.js +632 -0
  80. package/dist/orchestration/job-lock-audit.d.ts +24 -0
  81. package/dist/orchestration/job-lock-audit.js +123 -0
  82. package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
  83. package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
  84. package/dist/orchestration/reconciliation.d.ts +41 -0
  85. package/dist/orchestration/reconciliation.js +526 -0
  86. package/dist/orchestration/resolve-node.d.ts +29 -0
  87. package/dist/orchestration/resolve-node.js +105 -0
  88. package/dist/orchestration/run-activity.d.ts +13 -0
  89. package/dist/orchestration/run-activity.js +14 -0
  90. package/dist/orchestration/sequential-scheduler.d.ts +79 -0
  91. package/dist/orchestration/sequential-scheduler.js +706 -0
  92. package/dist/orchestration/status.d.ts +91 -0
  93. package/dist/orchestration/status.js +171 -0
  94. package/dist/persistence/in-memory-repository.d.ts +41 -0
  95. package/dist/persistence/in-memory-repository.js +333 -0
  96. package/dist/persistence/kintone/client.d.ts +36 -0
  97. package/dist/persistence/kintone/client.js +93 -0
  98. package/dist/persistence/kintone/design-notes.d.ts +38 -0
  99. package/dist/persistence/kintone/design-notes.js +38 -0
  100. package/dist/persistence/kintone/repository.d.ts +44 -0
  101. package/dist/persistence/kintone/repository.js +836 -0
  102. package/dist/persistence/kintone/schema.d.ts +116 -0
  103. package/dist/persistence/kintone/schema.js +115 -0
  104. package/dist/persistence/network-lock-reader.d.ts +24 -0
  105. package/dist/persistence/network-lock-reader.js +43 -0
  106. package/dist/persistence/network-lock-recovery.d.ts +66 -0
  107. package/dist/persistence/network-lock-recovery.js +392 -0
  108. package/dist/persistence/network-lock.d.ts +97 -0
  109. package/dist/persistence/network-lock.js +408 -0
  110. package/dist/persistence/repository.d.ts +101 -0
  111. package/dist/persistence/repository.js +11 -0
  112. package/dist/persistence/state-transition.d.ts +4 -0
  113. package/dist/persistence/state-transition.js +25 -0
  114. package/dist/requests/flownet-child-client.d.ts +78 -0
  115. package/dist/requests/flownet-child-client.js +243 -0
  116. package/dist/requests/kintone-request-store.d.ts +44 -0
  117. package/dist/requests/kintone-request-store.js +202 -0
  118. package/dist/requests/poll-requests-config.d.ts +26 -0
  119. package/dist/requests/poll-requests-config.js +105 -0
  120. package/dist/requests/request-model.d.ts +57 -0
  121. package/dist/requests/request-model.js +305 -0
  122. package/dist/requests/request-poller.d.ts +33 -0
  123. package/dist/requests/request-poller.js +353 -0
  124. package/dist/requests/request-result.d.ts +18 -0
  125. package/dist/requests/request-result.js +215 -0
  126. package/dist/requests/start-request.d.ts +26 -0
  127. package/dist/requests/start-request.js +83 -0
  128. package/package.json +63 -0
  129. package/schemas/.gitkeep +1 -0
  130. package/schemas/network-definition.schema.json +121 -0
@@ -0,0 +1,836 @@
1
+ import { createHash } from "node:crypto";
2
+ import { attemptKey, runKey } from "../../domain/canonical-record-key.js";
3
+ import { MAX_KINTONE_UNIQUE_KEY_LENGTH } from "../../domain/canonical-lock-key.js";
4
+ import { RepositoryError } from "../repository.js";
5
+ import { isAllowedNodeStateTransition, isAllowedResolutionTransition, } from "../state-transition.js";
6
+ import { KintoneApiError, KintoneClient, KintoneTransportError, } from "./client.js";
7
+ const field = (value) => ({ value });
8
+ const text = (record, code) => String(record[code]?.value ?? "");
9
+ const nullableText = (record, code) => {
10
+ const value = text(record, code);
11
+ return value === "" ? null : value;
12
+ };
13
+ const numberValue = (record, code) => Number(record[code]?.value);
14
+ const nullableNumber = (record, code) => {
15
+ const value = record[code]?.value;
16
+ return value === "" || value === null || value === undefined
17
+ ? null
18
+ : Number(value);
19
+ };
20
+ const jsonArray = (record, code) => JSON.parse(text(record, code));
21
+ const revisionOf = (record) => Number(record.$revision?.value);
22
+ function quote(value) {
23
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
24
+ }
25
+ function inQuery(fieldCode, value) {
26
+ return `${fieldCode} in (${quote(value)})`;
27
+ }
28
+ async function pagedRecords(client, filter) {
29
+ const result = [];
30
+ let lastId = 0;
31
+ while (true) {
32
+ const page = await client.getRecords(`${filter} and $id > ${lastId} order by $id asc limit 100`);
33
+ result.push(...page);
34
+ if (page.length < 100)
35
+ return result;
36
+ const nextId = Number(page.at(-1)?.$id?.value);
37
+ if (!Number.isSafeInteger(nextId) || nextId <= lastId) {
38
+ throw new RepositoryError("REMOTE_ERROR", "kintone pagination returned an invalid $id");
39
+ }
40
+ lastId = nextId;
41
+ }
42
+ }
43
+ function resolutionDetails(record) {
44
+ const raw = text(record, "reason");
45
+ try {
46
+ const value = JSON.parse(raw);
47
+ return {
48
+ resolution_type: value.resolution_type,
49
+ reason: String(value.reason ?? ""),
50
+ stop_confirmed_by: String(value.stop_confirmed_by ?? ""),
51
+ stop_evidence_ref: String(value.stop_evidence_ref ?? ""),
52
+ resolved_at: typeof value.resolved_at === "string" ? value.resolved_at : undefined,
53
+ };
54
+ }
55
+ catch {
56
+ return {
57
+ resolution_type: "OUTCOME_CONFIRMED",
58
+ reason: raw,
59
+ stop_confirmed_by: "",
60
+ stop_evidence_ref: "",
61
+ };
62
+ }
63
+ }
64
+ function uniqueKey(value) {
65
+ if (value.length > MAX_KINTONE_UNIQUE_KEY_LENGTH) {
66
+ throw new RepositoryError("UNIQUE_KEY_TOO_LONG", `unique key exceeds ${MAX_KINTONE_UNIQUE_KEY_LENGTH} characters`);
67
+ }
68
+ return value;
69
+ }
70
+ function resolutionRecordKey(attemptId, resolvedAt) {
71
+ const digest = createHash("sha256")
72
+ .update(`${attemptId}\0${resolvedAt}`)
73
+ .digest("hex");
74
+ return `RES:${digest.slice(0, MAX_KINTONE_UNIQUE_KEY_LENGTH - 4)}`;
75
+ }
76
+ function decodeResolution(value) {
77
+ const details = resolutionDetails(value);
78
+ return {
79
+ event_type: text(value, "event_type"),
80
+ ...details,
81
+ attempt_id: text(value, "attempt_id"),
82
+ resolved_outcome: text(value, "resolved_outcome"),
83
+ evidence_ref: text(value, "evidence_ref"),
84
+ service_principal: text(value, "service_principal"),
85
+ requested_by: text(value, "requested_by"),
86
+ approved_by: text(value, "approved_by"),
87
+ resolved_at: details.resolved_at ?? text(value, "resolved_at"),
88
+ };
89
+ }
90
+ function mapError(error) {
91
+ if (error instanceof KintoneApiError) {
92
+ if (error.status === 409 && error.apiCode === "GAIA_CO02") {
93
+ throw new RepositoryError("REVISION_CONFLICT", error.message, error);
94
+ }
95
+ if (error.status === 400 && error.apiCode === "CB_VA01") {
96
+ throw new RepositoryError("DUPLICATE_RECORD", error.message, error);
97
+ }
98
+ }
99
+ if (error instanceof KintoneTransportError) {
100
+ throw new RepositoryError("AMBIGUOUS_WRITE", error.message, error);
101
+ }
102
+ throw new RepositoryError("REMOTE_ERROR", "kintone operation failed", error);
103
+ }
104
+ function versioned(record, decode) {
105
+ return { value: decode(record), revision: revisionOf(record) };
106
+ }
107
+ function runRecord(run) {
108
+ return {
109
+ record_key: field(uniqueKey(runRecordKey(run))),
110
+ record_type: field("NETWORK_RUN"),
111
+ run_id: field(run.run_id),
112
+ network_id: field(run.network_id),
113
+ business_key: field(run.business_key),
114
+ max_active_runs: field(run.max_active_runs),
115
+ status: field(run.status),
116
+ lifecycle_status: field(run.lifecycle_status),
117
+ resume_allowed: field(String(run.resume_allowed)),
118
+ as_of: field(run.as_of ?? ""),
119
+ definition_schema_version: field(run.definition_schema_version),
120
+ definition_sha256: field(run.definition_sha256),
121
+ source_bundle_sha256: field(run.source_bundle_sha256),
122
+ // The schema stores an attachment, whose upload fileKey is supplied here.
123
+ // Bundle upload/filename reconciliation belongs to FN-07/M4.
124
+ source_bundle_attachment: field([
125
+ { fileKey: run.source_bundle_attachment },
126
+ ]),
127
+ resolved_profile_snapshot: field(JSON.stringify(run.resolved_profile_snapshot)),
128
+ resolved_profile_sha256: field(run.resolved_profile_sha256),
129
+ ksql_flow_version: field(run.ksql_flow_version),
130
+ engine_version: field(run.engine_version),
131
+ dialect: field(run.dialect),
132
+ created_at: field(run.created_at),
133
+ started_at: field(run.started_at ?? ""),
134
+ finished_at: field(run.finished_at ?? ""),
135
+ updated_at: field(run.updated_at),
136
+ };
137
+ }
138
+ function runRecordKey(run) {
139
+ return runKey(run.resolved_profile_snapshot.profile, run.network_id, run.business_key);
140
+ }
141
+ function decodeRun(record) {
142
+ const attachments = record.source_bundle_attachment?.value;
143
+ return {
144
+ run_id: text(record, "run_id"),
145
+ network_id: text(record, "network_id"),
146
+ business_key: text(record, "business_key"),
147
+ max_active_runs: numberValue(record, "max_active_runs"),
148
+ status: text(record, "status"),
149
+ lifecycle_status: text(record, "lifecycle_status"),
150
+ resume_allowed: text(record, "resume_allowed") === "true",
151
+ as_of: nullableText(record, "as_of"),
152
+ definition_schema_version: numberValue(record, "definition_schema_version"),
153
+ definition_sha256: text(record, "definition_sha256"),
154
+ source_bundle_sha256: text(record, "source_bundle_sha256"),
155
+ source_bundle_attachment: attachments?.[0]?.fileKey ?? attachments?.[0]?.name ?? "",
156
+ resolved_profile_snapshot: JSON.parse(text(record, "resolved_profile_snapshot")),
157
+ resolved_profile_sha256: text(record, "resolved_profile_sha256"),
158
+ ksql_flow_version: text(record, "ksql_flow_version"),
159
+ engine_version: text(record, "engine_version"),
160
+ dialect: numberValue(record, "dialect"),
161
+ created_at: text(record, "created_at"),
162
+ started_at: nullableText(record, "started_at"),
163
+ finished_at: nullableText(record, "finished_at"),
164
+ updated_at: text(record, "updated_at"),
165
+ };
166
+ }
167
+ function cancelRequestRecord(value) {
168
+ const details = {
169
+ state: value.state,
170
+ requested_by: value.requested_by,
171
+ reason: value.reason,
172
+ requested_at: value.requested_at,
173
+ accepted_at: value.accepted_at,
174
+ released_at: value.released_at,
175
+ release_reason: value.release_reason,
176
+ release_requested_by: value.release_requested_by,
177
+ };
178
+ return {
179
+ record_key: field(uniqueKey(`CANCEL:${value.run_id}`)),
180
+ record_type: field("CANCEL_REQUEST"),
181
+ run_id: field(value.run_id),
182
+ status_reason: field(JSON.stringify(details)),
183
+ };
184
+ }
185
+ function decodeCancelRequest(record) {
186
+ let details;
187
+ try {
188
+ details = JSON.parse(text(record, "status_reason"));
189
+ }
190
+ catch (error) {
191
+ throw new RepositoryError("REMOTE_ERROR", "CANCEL_REQUEST status_reason is not valid JSON", error);
192
+ }
193
+ if (details === null ||
194
+ typeof details !== "object" ||
195
+ !("state" in details) ||
196
+ !["REQUESTED", "ACCEPTED", "RELEASED"].includes(String(details.state))) {
197
+ throw new RepositoryError("REMOTE_ERROR", "CANCEL_REQUEST status_reason has an invalid state");
198
+ }
199
+ const value = details;
200
+ return { run_id: text(record, "run_id"), ...value };
201
+ }
202
+ function allowedCancelTransition(from, to) {
203
+ return ((from === "REQUESTED" && (to === "ACCEPTED" || to === "RELEASED")) ||
204
+ (from === "ACCEPTED" && to === "RELEASED") ||
205
+ (from === "RELEASED" && to === "REQUESTED"));
206
+ }
207
+ function invocationRecord(value) {
208
+ return {
209
+ record_key: field(uniqueKey(`INV:${value.invocation_id}`)),
210
+ record_type: field("RUN_INVOCATION"),
211
+ run_id: field(value.run_id),
212
+ started_at: field(value.started_at),
213
+ finished_at: field(value.finished_at ?? ""),
214
+ status: field(value.status),
215
+ result_code: field(value.result_code),
216
+ invocation_id: field(value.invocation_id),
217
+ mode: field(value.mode),
218
+ requested_by: field(value.requested_by),
219
+ host: field(value.host),
220
+ selected_node_ids: field(JSON.stringify(value.selected_node_ids)),
221
+ preserved_node_ids: field(JSON.stringify(value.preserved_node_ids)),
222
+ blocked_node_ids: field(JSON.stringify(value.blocked_node_ids)),
223
+ reason: field(value.reason),
224
+ };
225
+ }
226
+ function decodeInvocation(record) {
227
+ return {
228
+ invocation_id: text(record, "invocation_id"),
229
+ run_id: text(record, "run_id"),
230
+ mode: text(record, "mode"),
231
+ requested_by: text(record, "requested_by"),
232
+ host: text(record, "host"),
233
+ started_at: text(record, "started_at"),
234
+ finished_at: nullableText(record, "finished_at"),
235
+ status: text(record, "status"),
236
+ result_code: text(record, "result_code"),
237
+ selected_node_ids: jsonArray(record, "selected_node_ids"),
238
+ preserved_node_ids: jsonArray(record, "preserved_node_ids"),
239
+ blocked_node_ids: jsonArray(record, "blocked_node_ids"),
240
+ reason: text(record, "reason"),
241
+ };
242
+ }
243
+ function stateRecord(value) {
244
+ return {
245
+ record_key: field(uniqueKey(`STATE:${value.node_state_key}`)),
246
+ record_type: field("NODE_STATE"),
247
+ run_id: field(value.run_id),
248
+ revision: field(value.revision),
249
+ status: field(value.status),
250
+ started_at: field(value.started_at ?? ""),
251
+ finished_at: field(value.finished_at ?? ""),
252
+ updated_at: field(value.updated_at),
253
+ node_state_id: field(value.node_state_id),
254
+ node_state_key: field(uniqueKey(value.node_state_key)),
255
+ node_id: field(value.node_id),
256
+ job_id: field(value.job_id),
257
+ latest_attempt_no: field(value.latest_attempt_no),
258
+ active_attempt_id: field(value.active_attempt_id ?? ""),
259
+ idempotent: field(String(value.idempotent)),
260
+ trigger_rule: field(value.trigger_rule),
261
+ blocked_by: field(JSON.stringify(value.blocked_by)),
262
+ status_reason: field(value.status_reason ?? ""),
263
+ };
264
+ }
265
+ function decodeState(record) {
266
+ return {
267
+ node_state_id: text(record, "node_state_id"),
268
+ node_state_key: text(record, "node_state_key"),
269
+ run_id: text(record, "run_id"),
270
+ node_id: text(record, "node_id"),
271
+ job_id: text(record, "job_id"),
272
+ status: text(record, "status"),
273
+ latest_attempt_no: numberValue(record, "latest_attempt_no"),
274
+ active_attempt_id: nullableText(record, "active_attempt_id"),
275
+ revision: numberValue(record, "revision"),
276
+ idempotent: text(record, "idempotent") === "true",
277
+ trigger_rule: text(record, "trigger_rule"),
278
+ blocked_by: jsonArray(record, "blocked_by"),
279
+ status_reason: nullableText(record, "status_reason"),
280
+ started_at: nullableText(record, "started_at"),
281
+ finished_at: nullableText(record, "finished_at"),
282
+ updated_at: text(record, "updated_at"),
283
+ };
284
+ }
285
+ function attemptRecord(value) {
286
+ return {
287
+ record_key: field(uniqueKey(`ATT:${value.node_attempt_id}`)),
288
+ record_type: field("NODE_ATTEMPT"),
289
+ run_id: field(value.run_id),
290
+ finished_at: field(value.finished_at ?? ""),
291
+ status: field(value.status),
292
+ result_code: field(value.result_code),
293
+ node_attempt_id: field(uniqueKey(value.node_attempt_id)),
294
+ attempt_key: field(uniqueKey(value.attempt_key)),
295
+ node_id: field(value.node_id),
296
+ job_id: field(value.job_id),
297
+ invocation_id: field(value.invocation_id),
298
+ attempt_no: field(value.attempt_no),
299
+ execution_started_at: field(value.execution_started_at ?? ""),
300
+ runner_execution_started_at: field(value.runner_execution_started_at ?? ""),
301
+ execution_id: field(value.execution_id ?? ""),
302
+ duration_sec: field(value.duration_sec ?? ""),
303
+ error_message: field(value.error_message ?? ""),
304
+ read_count: field(value.read_count),
305
+ written_count: field(value.written_count),
306
+ last_successful_chunk_no: field(value.last_successful_chunk_no ?? ""),
307
+ last_written_key: field(value.last_written_key ?? ""),
308
+ state_revision_before: field(value.state_revision_before ?? ""),
309
+ };
310
+ }
311
+ function decodeAttempt(record) {
312
+ return {
313
+ node_attempt_id: text(record, "node_attempt_id"),
314
+ attempt_key: text(record, "attempt_key"),
315
+ run_id: text(record, "run_id"),
316
+ node_id: text(record, "node_id"),
317
+ job_id: text(record, "job_id"),
318
+ invocation_id: text(record, "invocation_id"),
319
+ attempt_no: numberValue(record, "attempt_no"),
320
+ status: text(record, "status"),
321
+ result_code: text(record, "result_code"),
322
+ execution_started_at: nullableText(record, "execution_started_at"),
323
+ runner_execution_started_at: nullableText(record, "runner_execution_started_at"),
324
+ execution_id: nullableText(record, "execution_id"),
325
+ finished_at: nullableText(record, "finished_at"),
326
+ duration_sec: nullableNumber(record, "duration_sec"),
327
+ error_message: nullableText(record, "error_message"),
328
+ read_count: numberValue(record, "read_count"),
329
+ written_count: numberValue(record, "written_count"),
330
+ last_successful_chunk_no: nullableNumber(record, "last_successful_chunk_no"),
331
+ last_written_key: nullableText(record, "last_written_key"),
332
+ state_revision_before: nullableNumber(record, "state_revision_before"),
333
+ };
334
+ }
335
+ export class KintonePersistenceRepository {
336
+ state;
337
+ audit;
338
+ constructor(config) {
339
+ this.state = new KintoneClient({
340
+ baseUrl: config.baseUrl,
341
+ appId: config.stateAppId,
342
+ apiToken: config.stateApiToken,
343
+ ...(config.fetch ? { fetch: config.fetch } : {}),
344
+ });
345
+ this.audit = new KintoneClient({
346
+ baseUrl: config.baseUrl,
347
+ appId: config.auditAppId,
348
+ apiToken: config.auditApiToken,
349
+ ...(config.fetch ? { fetch: config.fetch } : {}),
350
+ });
351
+ }
352
+ async createRun(run) {
353
+ const recordKey = runRecordKey(run);
354
+ return this.createWithAdjudication(this.state, recordKey, runRecord(run), decodeRun, (found) => found.run_id === run.run_id);
355
+ }
356
+ async getCancelRequest(runId) {
357
+ try {
358
+ const records = await this.state.getRecords(inQuery("record_key", `CANCEL:${runId}`));
359
+ if (records.length === 0)
360
+ return null;
361
+ if (records.length !== 1)
362
+ throw new RepositoryError("MULTIPLE_RECORDS", `cancel request for run_id ${runId} is not unique`);
363
+ return versioned(records[0], decodeCancelRequest);
364
+ }
365
+ catch (error) {
366
+ if (error instanceof RepositoryError)
367
+ throw error;
368
+ mapError(error);
369
+ }
370
+ }
371
+ async createCancelRequest(request) {
372
+ if (request.state !== "REQUESTED")
373
+ throw new RepositoryError("INVALID_STATE_TRANSITION", "a new cancel request must be REQUESTED");
374
+ return this.createWithAdjudication(this.state, `CANCEL:${request.run_id}`, cancelRequestRecord(request), decodeCancelRequest, (found) => found.run_id === request.run_id);
375
+ }
376
+ async updateCancelRequest(runId, expectedRevision, request) {
377
+ const key = `CANCEL:${runId}`;
378
+ const current = await this.requiredByRecordKey(this.state, key, decodeCancelRequest);
379
+ if (request.run_id !== runId ||
380
+ !allowedCancelTransition(current.value.state, request.state))
381
+ throw new RepositoryError("INVALID_STATE_TRANSITION", `${current.value.state} -> ${request.state} is not allowed`);
382
+ await this.put(this.state, key, expectedRevision, {
383
+ status_reason: cancelRequestRecord(request).status_reason,
384
+ });
385
+ return { value: request, revision: expectedRevision + 1 };
386
+ }
387
+ async getRunByBusinessKey(profile, networkId, businessKey) {
388
+ const recordKey = runKey(profile, networkId, businessKey);
389
+ let records;
390
+ try {
391
+ records = await this.state.getRecords(inQuery("record_key", recordKey));
392
+ }
393
+ catch (error) {
394
+ mapError(error);
395
+ }
396
+ if (records.length === 0)
397
+ return null;
398
+ if (records.length !== 1) {
399
+ throw new RepositoryError("MULTIPLE_RECORDS", "multiple runs matched");
400
+ }
401
+ const found = versioned(records[0], decodeRun);
402
+ if (runRecordKey(found.value) !== recordKey) {
403
+ throw new RepositoryError("REMOTE_ERROR", "run fields do not match canonical record key");
404
+ }
405
+ return found;
406
+ }
407
+ async getRun(runId) {
408
+ return this.requiredRunById(runId);
409
+ }
410
+ async listRuns(profile, networkId) {
411
+ let records;
412
+ try {
413
+ records = await pagedRecords(this.state, `${inQuery("record_type", "NETWORK_RUN")} and ${inQuery("network_id", networkId)}`);
414
+ }
415
+ catch (error) {
416
+ mapError(error);
417
+ }
418
+ return records
419
+ .map((record) => versioned(record, decodeRun))
420
+ .filter(({ value }) => value.resolved_profile_snapshot.profile === profile)
421
+ .sort((left, right) => left.value.run_id.localeCompare(right.value.run_id));
422
+ }
423
+ async updateRunAggregate(runId, expectedRevision, update) {
424
+ const current = await this.requiredRunById(runId);
425
+ const recordKey = runRecordKey(current.value);
426
+ const value = { ...current.value, ...update };
427
+ await this.put(this.state, recordKey, expectedRevision, {
428
+ status: field(update.status),
429
+ started_at: field(update.started_at ?? ""),
430
+ finished_at: field(update.finished_at ?? ""),
431
+ updated_at: field(update.updated_at),
432
+ });
433
+ return { value, revision: expectedRevision + 1 };
434
+ }
435
+ async archiveRun(runId, expectedRevision, at) {
436
+ const current = await this.requiredRunById(runId);
437
+ const recordKey = runRecordKey(current.value);
438
+ await this.put(this.state, recordKey, expectedRevision, {
439
+ lifecycle_status: field("ARCHIVED"),
440
+ updated_at: field(at),
441
+ });
442
+ return {
443
+ value: { ...current.value, lifecycle_status: "ARCHIVED", updated_at: at },
444
+ revision: expectedRevision + 1,
445
+ };
446
+ }
447
+ async requiredRunById(runId) {
448
+ let records;
449
+ try {
450
+ records = await this.state.getRecords([inQuery("record_type", "NETWORK_RUN"), inQuery("run_id", runId)].join(" and "));
451
+ }
452
+ catch (error) {
453
+ mapError(error);
454
+ }
455
+ if (records.length === 0)
456
+ throw new RepositoryError("RECORD_NOT_FOUND", `run ${runId} not found`);
457
+ if (records.length !== 1)
458
+ throw new RepositoryError("MULTIPLE_RECORDS", `run_id ${runId} is not unique`);
459
+ return versioned(records[0], decodeRun);
460
+ }
461
+ async createInvocation(value) {
462
+ return this.createWithAdjudication(this.audit, `INV:${value.invocation_id}`, invocationRecord(value), decodeInvocation, (found) => found.invocation_id === value.invocation_id);
463
+ }
464
+ async getInvocations(runId) {
465
+ let records;
466
+ try {
467
+ records = await pagedRecords(this.audit, `${inQuery("record_type", "RUN_INVOCATION")} and ${inQuery("run_id", runId)}`);
468
+ }
469
+ catch (error) {
470
+ mapError(error);
471
+ }
472
+ // kintone DATETIMEは分精度のため、順序は$id(pagedRecordsの取得順)で保持する
473
+ return records.map((record) => versioned(record, decodeInvocation));
474
+ }
475
+ async finalizeInvocation(invocationId, expectedRevision, finalization) {
476
+ const key = `INV:${invocationId}`;
477
+ const current = await this.requiredByRecordKey(this.audit, key, decodeInvocation);
478
+ const currentStatus = current.value.status;
479
+ if (currentStatus !== "RUNNING" && currentStatus !== "CREATED") {
480
+ throw new RepositoryError("INVALID_STATE_TRANSITION", "only an unterminated invocation can be finalized");
481
+ }
482
+ await this.put(this.audit, key, expectedRevision, {
483
+ status: field(finalization.status),
484
+ result_code: field(finalization.result_code),
485
+ finished_at: field(finalization.finished_at),
486
+ ...(finalization.selected_node_ids === undefined
487
+ ? {}
488
+ : {
489
+ selected_node_ids: field(JSON.stringify(finalization.selected_node_ids)),
490
+ }),
491
+ ...(finalization.preserved_node_ids === undefined
492
+ ? {}
493
+ : {
494
+ preserved_node_ids: field(JSON.stringify(finalization.preserved_node_ids)),
495
+ }),
496
+ ...(finalization.blocked_node_ids === undefined
497
+ ? {}
498
+ : {
499
+ blocked_node_ids: field(JSON.stringify(finalization.blocked_node_ids)),
500
+ }),
501
+ ...(finalization.reason === undefined
502
+ ? {}
503
+ : { reason: field(finalization.reason) }),
504
+ });
505
+ return {
506
+ value: {
507
+ ...current.value,
508
+ status: finalization.status,
509
+ result_code: finalization.result_code,
510
+ finished_at: finalization.finished_at,
511
+ selected_node_ids: finalization.selected_node_ids === undefined
512
+ ? current.value.selected_node_ids
513
+ : [...finalization.selected_node_ids],
514
+ preserved_node_ids: finalization.preserved_node_ids === undefined
515
+ ? current.value.preserved_node_ids
516
+ : [...finalization.preserved_node_ids],
517
+ blocked_node_ids: finalization.blocked_node_ids === undefined
518
+ ? current.value.blocked_node_ids
519
+ : [...finalization.blocked_node_ids],
520
+ reason: finalization.reason ?? current.value.reason,
521
+ },
522
+ revision: expectedRevision + 1,
523
+ };
524
+ }
525
+ async getNodeStates(runId) {
526
+ const records = await this.state.getRecords(`${inQuery("record_type", "NODE_STATE")} and ${inQuery("run_id", runId)}`);
527
+ return records.map((record) => versioned(record, decodeState));
528
+ }
529
+ async getAttempts(runId) {
530
+ const records = await this.audit.getRecords(`${inQuery("record_type", "NODE_ATTEMPT")} and ${inQuery("run_id", runId)}`);
531
+ return records.map((record) => versioned(record, decodeAttempt));
532
+ }
533
+ async getResolutions(runId) {
534
+ const attemptIds = new Set((await this.getAttempts(runId)).map(({ value }) => value.node_attempt_id));
535
+ const records = await this.audit.getRecords(inQuery("record_type", "ATTEMPT_RESOLUTION"));
536
+ return records
537
+ .filter((record) => attemptIds.has(text(record, "attempt_id")))
538
+ .map((record) => versioned(record, decodeResolution));
539
+ }
540
+ async upsertNodeState(write) {
541
+ const key = `STATE:${write.value.node_state_key}`;
542
+ if (write.expected_revision === null) {
543
+ if (write.value.status !== "WAITING") {
544
+ throw new RepositoryError("INVALID_STATE_TRANSITION", "a new node state must be WAITING");
545
+ }
546
+ return this.createWithAdjudication(this.state, key, stateRecord(write.value), decodeState, (found) => found.node_state_key === write.value.node_state_key);
547
+ }
548
+ const current = await this.requiredByRecordKey(this.state, key, decodeState);
549
+ if (write.resolution_event !== undefined) {
550
+ const resolution = await this.requiredByRecordKey(this.audit, resolutionRecordKey(write.resolution_event.attempt_id, write.resolution_event.resolved_at), decodeResolution);
551
+ if (resolution.value.event_type !== write.resolution_event.event_type ||
552
+ resolution.value.attempt_id !== write.resolution_event.attempt_id ||
553
+ resolution.value.resolved_outcome !==
554
+ write.resolution_event.resolved_outcome ||
555
+ resolution.value.resolved_at !== write.resolution_event.resolved_at) {
556
+ throw new RepositoryError("RECORD_NOT_FOUND", "the correlated Attempt Resolution does not match");
557
+ }
558
+ }
559
+ const resolutionTransition = write.resolution_event !== undefined &&
560
+ write.resolution_event.resolved_outcome === write.value.status &&
561
+ isAllowedResolutionTransition(current.value.status, write.value.status);
562
+ if (!isAllowedNodeStateTransition(current.value.status, write.value.status) &&
563
+ !resolutionTransition) {
564
+ throw new RepositoryError("INVALID_STATE_TRANSITION", `${current.value.status} -> ${write.value.status} is not allowed`);
565
+ }
566
+ if ((current.value.status === "FAILED" ||
567
+ current.value.status === "CANCELLED") &&
568
+ write.value.status === "WAITING" &&
569
+ !current.value.idempotent) {
570
+ throw new RepositoryError("INVALID_STATE_TRANSITION", "a non-idempotent node cannot resume automatically");
571
+ }
572
+ const newRevision = write.expected_revision + 1;
573
+ const value = { ...write.value, revision: newRevision };
574
+ await this.put(this.state, key, write.expected_revision, stateRecord(value));
575
+ return { value, revision: newRevision };
576
+ }
577
+ async createAttempt(input) {
578
+ const state = input.node_state.value;
579
+ const attemptNo = state.latest_attempt_no + 1;
580
+ const value = {
581
+ node_attempt_id: input.node_attempt_id,
582
+ attempt_key: attemptKey(state.run_id, state.node_id, attemptNo),
583
+ run_id: state.run_id,
584
+ node_id: state.node_id,
585
+ job_id: state.job_id,
586
+ invocation_id: input.invocation_id,
587
+ attempt_no: attemptNo,
588
+ status: "RUNNING",
589
+ result_code: "PENDING",
590
+ execution_started_at: null,
591
+ runner_execution_started_at: null,
592
+ execution_id: null,
593
+ finished_at: null,
594
+ duration_sec: null,
595
+ error_message: null,
596
+ read_count: 0,
597
+ written_count: 0,
598
+ last_successful_chunk_no: null,
599
+ last_written_key: null,
600
+ state_revision_before: input.node_state.revision,
601
+ };
602
+ try {
603
+ return await this.createWithAdjudication(this.audit, `ATT:${value.node_attempt_id}`, attemptRecord(value), decodeAttempt, (found) => found.node_attempt_id === value.node_attempt_id);
604
+ }
605
+ catch (error) {
606
+ if (error instanceof RepositoryError &&
607
+ (error.code === "DUPLICATE_RECORD" || error.code === "AMBIGUOUS_WRITE")) {
608
+ const matches = await this.audit.getRecords(`${inQuery("record_type", "NODE_ATTEMPT")} and ${inQuery("attempt_key", value.attempt_key)}`);
609
+ if (matches.length === 1 &&
610
+ text(matches[0], "node_attempt_id") === value.node_attempt_id) {
611
+ return versioned(matches[0], decodeAttempt);
612
+ }
613
+ throw new RepositoryError("ATTEMPT_NUMBER_CONFLICT", `attempt number ${attemptNo} cannot be uniquely confirmed`, error);
614
+ }
615
+ throw error;
616
+ }
617
+ }
618
+ async setAttemptExecutionStarted(attemptId, expectedRevision, start) {
619
+ const key = `ATT:${attemptId}`;
620
+ const current = await this.requiredByRecordKey(this.audit, key, decodeAttempt);
621
+ if (current.value.status !== "RUNNING")
622
+ throw new RepositoryError("ATTEMPT_TERMINAL", "attempt is terminal");
623
+ if (current.value.execution_started_at !== null)
624
+ throw new RepositoryError("ATTEMPT_LIFECYCLE_VIOLATION", "execution_started_at is already set");
625
+ await this.put(this.audit, key, expectedRevision, {
626
+ execution_started_at: field(start.execution_started_at),
627
+ });
628
+ return {
629
+ value: { ...current.value, ...start },
630
+ revision: expectedRevision + 1,
631
+ };
632
+ }
633
+ async setAttemptInputBaseline(attemptId, expectedRevision, write) {
634
+ const key = `ATT:${attemptId}`;
635
+ const current = await this.requiredByRecordKey(this.audit, key, decodeAttempt);
636
+ if (current.value.status !== "RUNNING" ||
637
+ current.value.execution_started_at !== null)
638
+ throw new RepositoryError("ATTEMPT_LIFECYCLE_VIOLATION", "input baseline must be recorded before execution starts");
639
+ await this.put(this.audit, key, expectedRevision, {
640
+ error_message: field(write.error_message),
641
+ });
642
+ return {
643
+ value: { ...current.value, error_message: write.error_message },
644
+ revision: expectedRevision + 1,
645
+ };
646
+ }
647
+ async finalizeAttempt(attemptId, expectedRevision, finalization) {
648
+ const key = `ATT:${attemptId}`;
649
+ const current = await this.requiredByRecordKey(this.audit, key, decodeAttempt);
650
+ if (current.value.status !== "RUNNING")
651
+ throw new RepositoryError("ATTEMPT_TERMINAL", "attempt is terminal");
652
+ const value = { ...current.value, ...finalization };
653
+ await this.put(this.audit, key, expectedRevision, attemptRecord(value));
654
+ return { value, revision: expectedRevision + 1 };
655
+ }
656
+ async appendResolution(value) {
657
+ const attempt = await this.requiredByRecordKey(this.audit, `ATT:${value.attempt_id}`, decodeAttempt);
658
+ if (attempt.value.status !== "UNKNOWN" && attempt.value.status !== "FAILED")
659
+ throw new RepositoryError("ATTEMPT_LIFECYCLE_VIOLATION", "only UNKNOWN or FAILED attempts can be resolved");
660
+ const key = resolutionRecordKey(value.attempt_id, value.resolved_at);
661
+ const record = {
662
+ record_key: field(key),
663
+ record_type: field("ATTEMPT_RESOLUTION"),
664
+ event_type: field(value.event_type),
665
+ attempt_id: field(value.attempt_id),
666
+ resolved_outcome: field(value.resolved_outcome),
667
+ evidence_ref: field(value.evidence_ref),
668
+ service_principal: field(value.service_principal),
669
+ requested_by: field(value.requested_by),
670
+ approved_by: field(value.approved_by),
671
+ reason: field(JSON.stringify({
672
+ resolution_type: value.resolution_type,
673
+ reason: value.reason,
674
+ stop_confirmed_by: value.stop_confirmed_by,
675
+ stop_evidence_ref: value.stop_evidence_ref,
676
+ resolved_at: value.resolved_at,
677
+ })),
678
+ resolved_at: field(value.resolved_at),
679
+ };
680
+ return this.createWithAdjudication(this.audit, key, record, () => value, () => true);
681
+ }
682
+ async appendOperationAudit(value) {
683
+ const key = uniqueKey(`OP:${value.event_id}`);
684
+ const networkForceRelease = value.event_type === "NETWORK_LOCK_FORCE_RELEASED";
685
+ const archived = value.event_type === "RUN_ARCHIVED";
686
+ const record = {
687
+ record_key: field(key),
688
+ record_type: field("OPERATION_AUDIT"),
689
+ run_id: field(networkForceRelease ? "" : value.run_id),
690
+ result_code: field(value.event_type === "RECONCILIATION_REPAIR"
691
+ ? value.repair_type
692
+ : value.event_type === "JOB_LOCK_FORCE_UNLOCK_RECORDED"
693
+ ? value.lock_recovery_result.outcome
694
+ : value.event_type),
695
+ reason: field(JSON.stringify(value)),
696
+ resolved_at: field(value.event_type === "RECONCILIATION_REPAIR"
697
+ ? value.occurred_at
698
+ : value.event_type === "JOB_LOCK_FORCE_UNLOCK_RECORDED"
699
+ ? value.recorded_at
700
+ : archived
701
+ ? value.archived_at
702
+ : value.released_at),
703
+ };
704
+ return this.createWithAdjudication(this.audit, key, record, (found) => JSON.parse(text(found, "reason")), (found) => found.event_id === value.event_id &&
705
+ (!archived ||
706
+ (found.event_type === "RUN_ARCHIVED" &&
707
+ found.run_id === value.run_id &&
708
+ found.previous_status === value.previous_status &&
709
+ found.run_revision_before === value.run_revision_before)), archived ? "AUDIT_CONFLICT" : undefined);
710
+ }
711
+ async getOperationAuditByEventId(eventId) {
712
+ const key = uniqueKey(`OP:${eventId}`);
713
+ let records;
714
+ try {
715
+ records = await this.audit.getRecords(inQuery("record_key", key));
716
+ }
717
+ catch (error) {
718
+ mapError(error);
719
+ }
720
+ if (records.length === 0)
721
+ return null;
722
+ if (records.length !== 1)
723
+ throw new RepositoryError("MULTIPLE_RECORDS", "multiple operation audits matched");
724
+ return versioned(records[0], (record) => JSON.parse(text(record, "reason")));
725
+ }
726
+ async listInconsistencies(runId) {
727
+ const states = await this.getNodeStates(runId);
728
+ const attemptRecords = await this.audit.getRecords(`${inQuery("record_type", "NODE_ATTEMPT")} and ${inQuery("run_id", runId)}`);
729
+ const attempts = attemptRecords.map((record) => versioned(record, decodeAttempt));
730
+ const result = [];
731
+ for (const state of states) {
732
+ const nodeAttempts = attempts.filter(({ value }) => value.node_id === state.value.node_id);
733
+ const running = nodeAttempts.filter(({ value }) => value.status === "RUNNING");
734
+ if (running.length > 1)
735
+ result.push({
736
+ code: "MULTIPLE_RUNNING_ATTEMPTS",
737
+ node_state: state,
738
+ attempts: running,
739
+ });
740
+ if (state.value.active_attempt_id) {
741
+ const active = nodeAttempts.find(({ value }) => value.node_attempt_id === state.value.active_attempt_id);
742
+ if (!active)
743
+ result.push({ code: "ACTIVE_ATTEMPT_MISSING", node_state: state });
744
+ else if (active.value.status !== state.value.status)
745
+ result.push({
746
+ code: "STATE_ATTEMPT_STATUS_MISMATCH",
747
+ node_state: state,
748
+ attempt: active,
749
+ });
750
+ }
751
+ }
752
+ return result;
753
+ }
754
+ async createWithAdjudication(client, recordKey, record, decode, same, mismatchCode) {
755
+ try {
756
+ const created = await client.postRecord(record);
757
+ return {
758
+ value: decode({
759
+ ...record,
760
+ $revision: field(String(created.revision)),
761
+ }),
762
+ revision: created.revision,
763
+ };
764
+ }
765
+ catch (error) {
766
+ if (!(error instanceof KintoneTransportError) &&
767
+ !(error instanceof KintoneApiError &&
768
+ error.status === 400 &&
769
+ error.apiCode === "CB_VA01"))
770
+ mapError(error);
771
+ let records;
772
+ try {
773
+ records = await client.getRecords(inQuery("record_key", recordKey));
774
+ }
775
+ catch (rereadError) {
776
+ mapError(rereadError);
777
+ }
778
+ if (records.length === 1) {
779
+ const found = versioned(records[0], decode);
780
+ if (same(found.value))
781
+ return found;
782
+ if (mismatchCode !== undefined)
783
+ throw new RepositoryError(mismatchCode, "operation audit event_id belongs to different archive facts", error);
784
+ }
785
+ if (error instanceof KintoneTransportError)
786
+ throw new RepositoryError("AMBIGUOUS_WRITE", "insert cannot be uniquely confirmed", error);
787
+ throw new RepositoryError("DUPLICATE_RECORD", "insert unique conflict belongs to another record", error);
788
+ }
789
+ }
790
+ async requiredByRecordKey(client, recordKey, decode) {
791
+ let records;
792
+ try {
793
+ records = await client.getRecords(inQuery("record_key", recordKey));
794
+ }
795
+ catch (error) {
796
+ mapError(error);
797
+ }
798
+ if (records.length === 0)
799
+ throw new RepositoryError("RECORD_NOT_FOUND", `${recordKey} not found`);
800
+ if (records.length !== 1)
801
+ throw new RepositoryError("MULTIPLE_RECORDS", `${recordKey} is not unique`);
802
+ return versioned(records[0], decode);
803
+ }
804
+ async put(client, recordKey, revision, record) {
805
+ try {
806
+ // TODO(FDR): define a retry policy. Until then, deliberately do not retry;
807
+ // callers must re-GET and reconcile ambiguous outcomes before continuing.
808
+ const update = { ...record };
809
+ delete update.record_key;
810
+ await client.putRecord(recordKey, revision, update);
811
+ }
812
+ catch (error) {
813
+ if (error instanceof KintoneApiError &&
814
+ error.status === 400 &&
815
+ error.apiCode === "GAIA_DA02") {
816
+ // A concurrent updateKey PUT loser can be reported as GAIA_DA02 rather
817
+ // than GAIA_CO02. Only reclassify it when a re-GET proves that the
818
+ // expected revision lost its target; otherwise retain fail-closed
819
+ // REMOTE_ERROR behavior for unrelated GAIA_DA02 responses.
820
+ let records;
821
+ try {
822
+ records = await client.getRecords(inQuery("record_key", recordKey));
823
+ }
824
+ catch {
825
+ mapError(error);
826
+ }
827
+ if (records.length === 0 ||
828
+ (records.length === 1 && revisionOf(records[0]) > revision)) {
829
+ throw new RepositoryError("REVISION_CONFLICT", error.message, error);
830
+ }
831
+ }
832
+ mapError(error);
833
+ }
834
+ }
835
+ }
836
+ //# sourceMappingURL=repository.js.map