@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.
- package/LICENSE +21 -0
- package/README.ja.md +81 -0
- package/README.md +117 -0
- package/dist/bundle/bundle-builder.d.ts +49 -0
- package/dist/bundle/bundle-builder.js +208 -0
- package/dist/bundle/index.d.ts +4 -0
- package/dist/bundle/index.js +4 -0
- package/dist/bundle/upload.d.ts +28 -0
- package/dist/bundle/upload.js +72 -0
- package/dist/bundle/zip-store.d.ts +17 -0
- package/dist/bundle/zip-store.js +175 -0
- package/dist/cli/archive-run-command.d.ts +12 -0
- package/dist/cli/archive-run-command.js +131 -0
- package/dist/cli/cancel-run-command.d.ts +9 -0
- package/dist/cli/cancel-run-command.js +77 -0
- package/dist/cli/force-unlock-network-command.d.ts +8 -0
- package/dist/cli/force-unlock-network-command.js +129 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +132 -0
- package/dist/cli/plan-command.d.ts +2 -0
- package/dist/cli/plan-command.js +100 -0
- package/dist/cli/poll-requests-command.d.ts +3 -0
- package/dist/cli/poll-requests-command.js +74 -0
- package/dist/cli/record-job-unlock-command.d.ts +11 -0
- package/dist/cli/record-job-unlock-command.js +78 -0
- package/dist/cli/resolve-node-command.d.ts +13 -0
- package/dist/cli/resolve-node-command.js +174 -0
- package/dist/cli/run-network-command.d.ts +14 -0
- package/dist/cli/run-network-command.js +418 -0
- package/dist/cli/status-command.d.ts +5 -0
- package/dist/cli/status-command.js +137 -0
- package/dist/cli/validate-command.d.ts +6 -0
- package/dist/cli/validate-command.js +17 -0
- package/dist/dag/descendants.d.ts +9 -0
- package/dist/dag/descendants.js +67 -0
- package/dist/dag/topological-sort.d.ts +7 -0
- package/dist/dag/topological-sort.js +32 -0
- package/dist/domain/business-key.d.ts +15 -0
- package/dist/domain/business-key.js +249 -0
- package/dist/domain/canonical-lock-key.d.ts +17 -0
- package/dist/domain/canonical-lock-key.js +60 -0
- package/dist/domain/canonical-record-key.d.ts +18 -0
- package/dist/domain/canonical-record-key.js +70 -0
- package/dist/domain/load-network.d.ts +4 -0
- package/dist/domain/load-network.js +47 -0
- package/dist/domain/network-definition.d.ts +47 -0
- package/dist/domain/network-definition.js +7 -0
- package/dist/domain/persistence-model.d.ts +198 -0
- package/dist/domain/persistence-model.js +2 -0
- package/dist/domain/run-aggregate.d.ts +3 -0
- package/dist/domain/run-aggregate.js +15 -0
- package/dist/domain/validate-network-path.d.ts +11 -0
- package/dist/domain/validate-network-path.js +33 -0
- package/dist/domain/validate-network.d.ts +3 -0
- package/dist/domain/validate-network.js +180 -0
- package/dist/executor/attempt-executor.d.ts +43 -0
- package/dist/executor/attempt-executor.js +236 -0
- package/dist/executor/job-log-reader.d.ts +37 -0
- package/dist/executor/job-log-reader.js +91 -0
- package/dist/executor/ksql-flow-cli.d.ts +86 -0
- package/dist/executor/ksql-flow-cli.js +161 -0
- package/dist/executor/preflight.d.ts +41 -0
- package/dist/executor/preflight.js +150 -0
- package/dist/executor/result-classifier.d.ts +69 -0
- package/dist/executor/result-classifier.js +237 -0
- package/dist/executor/run-subprocess.d.ts +64 -0
- package/dist/executor/run-subprocess.js +175 -0
- package/dist/io/input-baseline.d.ts +19 -0
- package/dist/io/input-baseline.js +214 -0
- package/dist/io/io-config.d.ts +6 -0
- package/dist/io/io-config.js +33 -0
- package/dist/io/io-path.d.ts +37 -0
- package/dist/io/io-path.js +279 -0
- package/dist/orchestration/archive-run.d.ts +53 -0
- package/dist/orchestration/archive-run.js +323 -0
- package/dist/orchestration/cancel-request.d.ts +18 -0
- package/dist/orchestration/cancel-request.js +67 -0
- package/dist/orchestration/ensure-run.d.ts +83 -0
- package/dist/orchestration/ensure-run.js +632 -0
- package/dist/orchestration/job-lock-audit.d.ts +24 -0
- package/dist/orchestration/job-lock-audit.js +123 -0
- package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
- package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
- package/dist/orchestration/reconciliation.d.ts +41 -0
- package/dist/orchestration/reconciliation.js +526 -0
- package/dist/orchestration/resolve-node.d.ts +29 -0
- package/dist/orchestration/resolve-node.js +105 -0
- package/dist/orchestration/run-activity.d.ts +13 -0
- package/dist/orchestration/run-activity.js +14 -0
- package/dist/orchestration/sequential-scheduler.d.ts +79 -0
- package/dist/orchestration/sequential-scheduler.js +706 -0
- package/dist/orchestration/status.d.ts +91 -0
- package/dist/orchestration/status.js +171 -0
- package/dist/persistence/in-memory-repository.d.ts +41 -0
- package/dist/persistence/in-memory-repository.js +333 -0
- package/dist/persistence/kintone/client.d.ts +36 -0
- package/dist/persistence/kintone/client.js +93 -0
- package/dist/persistence/kintone/design-notes.d.ts +38 -0
- package/dist/persistence/kintone/design-notes.js +38 -0
- package/dist/persistence/kintone/repository.d.ts +44 -0
- package/dist/persistence/kintone/repository.js +836 -0
- package/dist/persistence/kintone/schema.d.ts +116 -0
- package/dist/persistence/kintone/schema.js +115 -0
- package/dist/persistence/network-lock-reader.d.ts +24 -0
- package/dist/persistence/network-lock-reader.js +43 -0
- package/dist/persistence/network-lock-recovery.d.ts +66 -0
- package/dist/persistence/network-lock-recovery.js +392 -0
- package/dist/persistence/network-lock.d.ts +97 -0
- package/dist/persistence/network-lock.js +408 -0
- package/dist/persistence/repository.d.ts +101 -0
- package/dist/persistence/repository.js +11 -0
- package/dist/persistence/state-transition.d.ts +4 -0
- package/dist/persistence/state-transition.js +25 -0
- package/dist/requests/flownet-child-client.d.ts +78 -0
- package/dist/requests/flownet-child-client.js +243 -0
- package/dist/requests/kintone-request-store.d.ts +44 -0
- package/dist/requests/kintone-request-store.js +202 -0
- package/dist/requests/poll-requests-config.d.ts +26 -0
- package/dist/requests/poll-requests-config.js +105 -0
- package/dist/requests/request-model.d.ts +57 -0
- package/dist/requests/request-model.js +305 -0
- package/dist/requests/request-poller.d.ts +33 -0
- package/dist/requests/request-poller.js +353 -0
- package/dist/requests/request-result.d.ts +18 -0
- package/dist/requests/request-result.js +215 -0
- package/dist/requests/start-request.d.ts +26 -0
- package/dist/requests/start-request.js +83 -0
- package/package.json +63 -0
- package/schemas/.gitkeep +1 -0
- package/schemas/network-definition.schema.json +121 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const EXECUTION_CONTRACT = "ksql-flow.execution/v1";
|
|
3
|
+
export const REQUIRED_CAPABILITY_FEATURES = [
|
|
4
|
+
"resultJson",
|
|
5
|
+
"correlationIds",
|
|
6
|
+
"describeProfile",
|
|
7
|
+
"inspectJob",
|
|
8
|
+
"durableExecutionStarted",
|
|
9
|
+
];
|
|
10
|
+
export const NONDETERMINISTIC_CODES = ["KSQL1306"];
|
|
11
|
+
export class PreflightError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
details;
|
|
14
|
+
constructor(code, message, details = []) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.details = details;
|
|
18
|
+
this.name = "PreflightError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function validateCapabilities(capabilities, additionalFeatures = []) {
|
|
22
|
+
if (!capabilities.executionContracts.includes(EXECUTION_CONTRACT)) {
|
|
23
|
+
throw new PreflightError("CAPABILITY_CONTRACT_MISSING", `required execution contract '${EXECUTION_CONTRACT}' is not supported`);
|
|
24
|
+
}
|
|
25
|
+
const required = [...REQUIRED_CAPABILITY_FEATURES, ...additionalFeatures];
|
|
26
|
+
const missing = required.filter((feature) => capabilities.features[feature] !== true);
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
throw new PreflightError("CAPABILITY_FEATURE_MISSING", `required capability features are not true: ${missing.join(", ")}`, missing);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** All object keys are sorted recursively; array order remains significant. */
|
|
32
|
+
export function canonicalJson(value) {
|
|
33
|
+
const serialized = JSON.stringify(sortJsonKeys(value));
|
|
34
|
+
if (serialized === undefined) {
|
|
35
|
+
throw new TypeError("value cannot be represented as canonical JSON");
|
|
36
|
+
}
|
|
37
|
+
return serialized;
|
|
38
|
+
}
|
|
39
|
+
export function sha256Hex(value) {
|
|
40
|
+
return createHash("sha256").update(value).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
export function canonicalJsonSha256(value) {
|
|
43
|
+
return sha256Hex(Buffer.from(canonicalJson(value), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
export function profileSnapshot(description) {
|
|
46
|
+
return {
|
|
47
|
+
canonicalJsonSha256: canonicalJsonSha256(description),
|
|
48
|
+
baseUrl: description.baseUrl,
|
|
49
|
+
guestSpaceId: description.guestSpaceId,
|
|
50
|
+
apps: description.apps,
|
|
51
|
+
timezone: description.timezone,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function assertProfileSnapshot(description, expected) {
|
|
55
|
+
const actual = profileSnapshot(description);
|
|
56
|
+
const mismatches = [];
|
|
57
|
+
if (actual.canonicalJsonSha256 !== expected.canonicalJsonSha256) {
|
|
58
|
+
mismatches.push("canonicalJsonSha256");
|
|
59
|
+
}
|
|
60
|
+
if (actual.baseUrl !== expected.baseUrl)
|
|
61
|
+
mismatches.push("baseUrl");
|
|
62
|
+
if (actual.guestSpaceId !== expected.guestSpaceId) {
|
|
63
|
+
mismatches.push("guestSpaceId");
|
|
64
|
+
}
|
|
65
|
+
if (actual.timezone !== expected.timezone)
|
|
66
|
+
mismatches.push("timezone");
|
|
67
|
+
if (canonicalJson(actual.apps) !== canonicalJson(expected.apps)) {
|
|
68
|
+
mismatches.push("apps");
|
|
69
|
+
}
|
|
70
|
+
if (mismatches.length > 0) {
|
|
71
|
+
throw new PreflightError("PROFILE_SNAPSHOT_MISMATCH", `resolved profile differs from snapshot: ${mismatches.join(", ")}`, mismatches);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function validateJobInspections(nodes, inspections, exceptions = []) {
|
|
75
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
76
|
+
validateExceptionFields(exceptions, nodesById);
|
|
77
|
+
const duplicateKeys = new Set();
|
|
78
|
+
const seenKeys = new Set();
|
|
79
|
+
for (const exception of exceptions) {
|
|
80
|
+
const key = `${exception.node_id}\0${exception.code}`;
|
|
81
|
+
if (seenKeys.has(key))
|
|
82
|
+
duplicateKeys.add(key);
|
|
83
|
+
seenKeys.add(key);
|
|
84
|
+
}
|
|
85
|
+
if (duplicateKeys.size > 0) {
|
|
86
|
+
throw new PreflightError("INSPECTION_EXCEPTION_INVALID", "duplicate approved inspection exception");
|
|
87
|
+
}
|
|
88
|
+
const result = [];
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
const inspection = inspections.get(node.id);
|
|
91
|
+
if (inspection === undefined) {
|
|
92
|
+
throw new PreflightError("JOB_ID_MISMATCH", `inspection is missing for node '${node.id}'`, [node.id]);
|
|
93
|
+
}
|
|
94
|
+
if (inspection.jobId !== node.job_id) {
|
|
95
|
+
throw new PreflightError("JOB_ID_MISMATCH", `node '${node.id}' expects job_id '${node.job_id}', inspected '${inspection.jobId}'`, [node.id]);
|
|
96
|
+
}
|
|
97
|
+
// D-23 deliberately recognizes only the engine's public KSQL1306 code.
|
|
98
|
+
// KSQL1305 remains a warning and static inspection does not prove idempotency.
|
|
99
|
+
const detected = uniqueSorted(inspection.nondeterministicElements
|
|
100
|
+
.filter((diagnostic) => NONDETERMINISTIC_CODES.includes(diagnostic.code))
|
|
101
|
+
.map((diagnostic) => diagnostic.code));
|
|
102
|
+
const approved = exceptions.filter((exception) => exception.node_id === node.id);
|
|
103
|
+
for (const exception of approved) {
|
|
104
|
+
if (!detected.includes(exception.code)) {
|
|
105
|
+
throw new PreflightError("INSPECTION_EXCEPTION_NOT_DETECTED", `exception ${exception.code} for node '${node.id}' has no matching detected diagnostic`, [node.id, exception.code]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const unapproved = detected.filter((code) => !approved.some((exception) => exception.code === code));
|
|
109
|
+
if (node.idempotent && unapproved.length > 0) {
|
|
110
|
+
throw new PreflightError("NONDETERMINISTIC_IDEMPOTENT_JOB", `idempotent node '${node.id}' has unapproved nondeterministic elements: ${unapproved.join(", ")}`, [node.id, ...unapproved]);
|
|
111
|
+
}
|
|
112
|
+
result.push({
|
|
113
|
+
nodeId: node.id,
|
|
114
|
+
jobId: inspection.jobId,
|
|
115
|
+
inspection,
|
|
116
|
+
nondeterministicCodes: detected,
|
|
117
|
+
approvedExceptions: approved,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function sortJsonKeys(value) {
|
|
123
|
+
if (Array.isArray(value))
|
|
124
|
+
return value.map(sortJsonKeys);
|
|
125
|
+
if (value === null || typeof value !== "object")
|
|
126
|
+
return value;
|
|
127
|
+
const source = value;
|
|
128
|
+
const sorted = {};
|
|
129
|
+
for (const key of Object.keys(source).sort()) {
|
|
130
|
+
sorted[key] = sortJsonKeys(source[key]);
|
|
131
|
+
}
|
|
132
|
+
return sorted;
|
|
133
|
+
}
|
|
134
|
+
function uniqueSorted(values) {
|
|
135
|
+
return [...new Set(values)].sort();
|
|
136
|
+
}
|
|
137
|
+
function validateExceptionFields(exceptions, nodesById) {
|
|
138
|
+
for (const exception of exceptions) {
|
|
139
|
+
const validTimestamp = typeof exception.approved_at === "string" &&
|
|
140
|
+
Number.isFinite(Date.parse(exception.approved_at));
|
|
141
|
+
if (!nodesById.has(exception.node_id) ||
|
|
142
|
+
!NONDETERMINISTIC_CODES.includes(exception.code) ||
|
|
143
|
+
exception.approved_by.trim() === "" ||
|
|
144
|
+
exception.reason.trim() === "" ||
|
|
145
|
+
!validTimestamp) {
|
|
146
|
+
throw new PreflightError("INSPECTION_EXCEPTION_INVALID", `approved inspection exception is invalid for node '${exception.node_id}'`, [exception.node_id, exception.code]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare const NO_EXECUTION_RESULT: "NO_EXECUTION_RESULT";
|
|
2
|
+
export type AttemptOutcome = "SUCCESS" | "FAILED" | "CANCELLED" | "UNKNOWN" | "LOCK_CONFLICT";
|
|
3
|
+
export interface ExecutionError {
|
|
4
|
+
readonly category: string;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly retryable: boolean;
|
|
8
|
+
readonly detailsTruncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecutionInputFile {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly sha256: string;
|
|
13
|
+
readonly bytes: number;
|
|
14
|
+
readonly rows: number;
|
|
15
|
+
readonly encoding: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExecutionOutputFile {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly sha256: string;
|
|
20
|
+
readonly bytes: number;
|
|
21
|
+
readonly rows: number;
|
|
22
|
+
readonly encoding: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ExecutionResult {
|
|
25
|
+
readonly formatVersion: 1;
|
|
26
|
+
readonly kind: "EXECUTION_RESULT";
|
|
27
|
+
readonly contract: "ksql-flow.execution/v1";
|
|
28
|
+
readonly correlationId: string;
|
|
29
|
+
readonly attemptId: string;
|
|
30
|
+
readonly executionId: string;
|
|
31
|
+
readonly jobId: string;
|
|
32
|
+
readonly profile: string;
|
|
33
|
+
readonly status: "SUCCESS" | "FAILED" | "CANCELLED";
|
|
34
|
+
readonly resultCode: string;
|
|
35
|
+
readonly executionStarted: boolean;
|
|
36
|
+
readonly exitCode: number;
|
|
37
|
+
readonly asOf: string | null;
|
|
38
|
+
readonly startedAt: string | null;
|
|
39
|
+
readonly finishedAt: string;
|
|
40
|
+
readonly durationMs: number;
|
|
41
|
+
readonly readCount: number;
|
|
42
|
+
readonly writtenCount: number;
|
|
43
|
+
readonly deletedCount: number;
|
|
44
|
+
readonly apiCalls: number;
|
|
45
|
+
readonly lastSuccessfulChunkNo: number | null;
|
|
46
|
+
readonly lastWrittenKey: string | null;
|
|
47
|
+
readonly ksqlFlowVersion: string;
|
|
48
|
+
readonly engineVersion: string;
|
|
49
|
+
readonly error: ExecutionError | null;
|
|
50
|
+
readonly input_files?: readonly ExecutionInputFile[];
|
|
51
|
+
readonly output_files?: readonly ExecutionOutputFile[];
|
|
52
|
+
}
|
|
53
|
+
export interface ClassificationContext {
|
|
54
|
+
readonly correlationId: string;
|
|
55
|
+
readonly attemptId: string;
|
|
56
|
+
readonly processExitCode: number | null;
|
|
57
|
+
}
|
|
58
|
+
export interface ResultClassification {
|
|
59
|
+
readonly kind: "VALID_RESULT" | "INVALID_RESULT" | "NO_RESULT";
|
|
60
|
+
readonly attemptOutcome: AttemptOutcome;
|
|
61
|
+
readonly resultCode: string | null;
|
|
62
|
+
readonly details: readonly string[];
|
|
63
|
+
readonly result: ExecutionResult | null;
|
|
64
|
+
readonly invocationResultCode: string | null;
|
|
65
|
+
}
|
|
66
|
+
export type ResultFileReader = (path: string) => Promise<string>;
|
|
67
|
+
export declare function readAndClassifyResult(path: string, context: ClassificationContext, reader?: ResultFileReader): Promise<ResultClassification>;
|
|
68
|
+
export declare function classifyResult(value: unknown, context: ClassificationContext): ResultClassification;
|
|
69
|
+
//# sourceMappingURL=result-classifier.d.ts.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export const NO_EXECUTION_RESULT = "NO_EXECUTION_RESULT";
|
|
3
|
+
const KNOWN_RESULTS = {
|
|
4
|
+
OK: { status: "SUCCESS", exitCode: 0 },
|
|
5
|
+
NO_DATA: { status: "SUCCESS", exitCode: 0 },
|
|
6
|
+
VALIDATION_ERROR: { status: "FAILED", exitCode: 1 },
|
|
7
|
+
ASSERT_FAILED: { status: "FAILED", exitCode: 2 },
|
|
8
|
+
SQL_ERROR: { status: "FAILED", exitCode: 1 },
|
|
9
|
+
API_ERROR: { status: "FAILED", exitCode: 3 },
|
|
10
|
+
AUTH_ERROR: { status: "FAILED", exitCode: 3 },
|
|
11
|
+
EXECUTION_TIMEOUT: { status: "FAILED", exitCode: 3 },
|
|
12
|
+
LOCK_UNAVAILABLE: { status: "FAILED", exitCode: 3 },
|
|
13
|
+
INTERNAL_ERROR: { status: "FAILED", exitCode: 3 },
|
|
14
|
+
CANCELLED: { status: "CANCELLED", exitCode: 3 },
|
|
15
|
+
LOCK_CONFLICT: { status: "FAILED", exitCode: 5 },
|
|
16
|
+
};
|
|
17
|
+
export async function readAndClassifyResult(path, context, reader = (filePath) => readFile(filePath, "utf8")) {
|
|
18
|
+
let source;
|
|
19
|
+
try {
|
|
20
|
+
source = await reader(path);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return noResult("result file does not exist or could not be read");
|
|
24
|
+
}
|
|
25
|
+
let value;
|
|
26
|
+
try {
|
|
27
|
+
value = JSON.parse(source);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return noResult("result file is not complete JSON");
|
|
31
|
+
}
|
|
32
|
+
return classifyResult(value, context);
|
|
33
|
+
}
|
|
34
|
+
export function classifyResult(value, context) {
|
|
35
|
+
const errors = validateShape(value);
|
|
36
|
+
if (errors.length > 0 || !isRecord(value))
|
|
37
|
+
return invalid(errors);
|
|
38
|
+
if (value.correlationId !== context.correlationId)
|
|
39
|
+
errors.push("correlationId does not echo the invocation value");
|
|
40
|
+
if (value.attemptId !== context.attemptId)
|
|
41
|
+
errors.push("attemptId does not echo the invocation value");
|
|
42
|
+
if (value.exitCode !== context.processExitCode)
|
|
43
|
+
errors.push("result exitCode does not match process exit code");
|
|
44
|
+
const status = value.status;
|
|
45
|
+
const resultCode = value.resultCode;
|
|
46
|
+
const exitCode = value.exitCode;
|
|
47
|
+
const known = KNOWN_RESULTS[resultCode];
|
|
48
|
+
if (known) {
|
|
49
|
+
if (known.status !== status || known.exitCode !== exitCode)
|
|
50
|
+
errors.push("status/resultCode/exitCode combination is inconsistent");
|
|
51
|
+
}
|
|
52
|
+
else if (!isStatusExitCompatible(status, exitCode)) {
|
|
53
|
+
errors.push("unknown resultCode has an inconsistent status/exitCode");
|
|
54
|
+
}
|
|
55
|
+
if ((resultCode === "VALIDATION_ERROR" || resultCode === "LOCK_CONFLICT") &&
|
|
56
|
+
value.executionStarted !== false)
|
|
57
|
+
errors.push(`${resultCode} requires executionStarted=false`);
|
|
58
|
+
if (status === "SUCCESS" && value.executionStarted !== true)
|
|
59
|
+
errors.push("SUCCESS requires executionStarted=true");
|
|
60
|
+
if (errors.length > 0)
|
|
61
|
+
return invalid(errors);
|
|
62
|
+
const result = value;
|
|
63
|
+
const attemptOutcome = resultCode === "LOCK_CONFLICT"
|
|
64
|
+
? "LOCK_CONFLICT"
|
|
65
|
+
: status === "SUCCESS"
|
|
66
|
+
? "SUCCESS"
|
|
67
|
+
: status === "CANCELLED"
|
|
68
|
+
? "CANCELLED"
|
|
69
|
+
: "FAILED";
|
|
70
|
+
return {
|
|
71
|
+
kind: "VALID_RESULT",
|
|
72
|
+
attemptOutcome,
|
|
73
|
+
resultCode,
|
|
74
|
+
details: [],
|
|
75
|
+
result,
|
|
76
|
+
invocationResultCode: resultCode === "LOCK_CONFLICT" ? "LOCK_CONFLICT" : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function validateShape(value) {
|
|
80
|
+
if (!isRecord(value))
|
|
81
|
+
return ["result must be a JSON object"];
|
|
82
|
+
const errors = [];
|
|
83
|
+
literal(value, "formatVersion", 1, errors);
|
|
84
|
+
literal(value, "kind", "EXECUTION_RESULT", errors);
|
|
85
|
+
literal(value, "contract", "ksql-flow.execution/v1", errors);
|
|
86
|
+
for (const field of [
|
|
87
|
+
"correlationId",
|
|
88
|
+
"attemptId",
|
|
89
|
+
"executionId",
|
|
90
|
+
"jobId",
|
|
91
|
+
"profile",
|
|
92
|
+
"resultCode",
|
|
93
|
+
"finishedAt",
|
|
94
|
+
"ksqlFlowVersion",
|
|
95
|
+
"engineVersion",
|
|
96
|
+
])
|
|
97
|
+
requireString(value, field, errors);
|
|
98
|
+
if (!["SUCCESS", "FAILED", "CANCELLED"].includes(String(value.status)))
|
|
99
|
+
errors.push("status must be SUCCESS, FAILED, or CANCELLED");
|
|
100
|
+
if (typeof value.executionStarted !== "boolean")
|
|
101
|
+
errors.push("executionStarted must be boolean");
|
|
102
|
+
if (!Number.isSafeInteger(value.exitCode))
|
|
103
|
+
errors.push("exitCode must be an integer");
|
|
104
|
+
if (!(value.asOf === null || isOffsetIso(value.asOf)))
|
|
105
|
+
errors.push("asOf must be an ISO-8601 timestamp or null");
|
|
106
|
+
if (!(value.startedAt === null || isUtcIso(value.startedAt)))
|
|
107
|
+
errors.push("startedAt must be a UTC ISO-8601 timestamp or null");
|
|
108
|
+
if (!isUtcIso(value.finishedAt))
|
|
109
|
+
errors.push("finishedAt must be a UTC ISO-8601 timestamp");
|
|
110
|
+
if (typeof value.durationMs !== "number" ||
|
|
111
|
+
!Number.isFinite(value.durationMs) ||
|
|
112
|
+
value.durationMs < 0)
|
|
113
|
+
errors.push("durationMs must be non-negative");
|
|
114
|
+
for (const field of ["readCount", "writtenCount", "deletedCount", "apiCalls"])
|
|
115
|
+
requireNonNegativeInteger(value, field, errors);
|
|
116
|
+
if (!(value.lastSuccessfulChunkNo === null ||
|
|
117
|
+
(Number.isSafeInteger(value.lastSuccessfulChunkNo) &&
|
|
118
|
+
value.lastSuccessfulChunkNo >= 0)))
|
|
119
|
+
errors.push("lastSuccessfulChunkNo must be a non-negative integer or null");
|
|
120
|
+
if (!(value.lastWrittenKey === null || typeof value.lastWrittenKey === "string"))
|
|
121
|
+
errors.push("lastWrittenKey must be string or null");
|
|
122
|
+
if (!(value.error === null || isExecutionError(value.error)))
|
|
123
|
+
errors.push("error must be a contract error object or null");
|
|
124
|
+
if (value.input_files !== undefined &&
|
|
125
|
+
!isExecutionFileReceipts(value.input_files, false))
|
|
126
|
+
errors.push("input_files must contain safe input receipt entries");
|
|
127
|
+
if (value.output_files !== undefined &&
|
|
128
|
+
!isExecutionFileReceipts(value.output_files, true))
|
|
129
|
+
errors.push("output_files must contain safe output receipt entries");
|
|
130
|
+
if (value.status === "SUCCESS" && value.error !== null)
|
|
131
|
+
errors.push("SUCCESS requires error=null");
|
|
132
|
+
if (value.status !== "SUCCESS" && value.error === null)
|
|
133
|
+
errors.push("non-success result requires an error object");
|
|
134
|
+
return errors;
|
|
135
|
+
}
|
|
136
|
+
function isExecutionFileReceipts(value, output) {
|
|
137
|
+
if (!Array.isArray(value))
|
|
138
|
+
return false;
|
|
139
|
+
const names = new Set();
|
|
140
|
+
for (const item of value) {
|
|
141
|
+
if (!isRecord(item) ||
|
|
142
|
+
(output &&
|
|
143
|
+
!hasOnlyKeys(item, ["name", "sha256", "bytes", "rows", "encoding"])) ||
|
|
144
|
+
typeof item.name !== "string" ||
|
|
145
|
+
!isSafeSourceName(item.name) ||
|
|
146
|
+
names.has(item.name) ||
|
|
147
|
+
typeof item.sha256 !== "string" ||
|
|
148
|
+
!/^[a-f0-9]{64}$/u.test(item.sha256) ||
|
|
149
|
+
!Number.isSafeInteger(item.bytes) ||
|
|
150
|
+
item.bytes < 0 ||
|
|
151
|
+
!Number.isSafeInteger(item.rows) ||
|
|
152
|
+
item.rows < 0 ||
|
|
153
|
+
typeof item.encoding !== "string" ||
|
|
154
|
+
(output
|
|
155
|
+
? !["utf8", "sjis"].includes(item.encoding)
|
|
156
|
+
: !/^[A-Za-z0-9._-]{1,32}$/u.test(item.encoding)))
|
|
157
|
+
return false;
|
|
158
|
+
names.add(item.name);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function hasOnlyKeys(value, expected) {
|
|
163
|
+
return (JSON.stringify(Object.keys(value).sort()) ===
|
|
164
|
+
JSON.stringify([...expected].sort()));
|
|
165
|
+
}
|
|
166
|
+
function isSafeSourceName(value) {
|
|
167
|
+
return (value.length >= 1 &&
|
|
168
|
+
value.length <= 128 &&
|
|
169
|
+
value !== "__net__" &&
|
|
170
|
+
!value.includes(":") &&
|
|
171
|
+
!value.includes("=") &&
|
|
172
|
+
![...value].some((character) => {
|
|
173
|
+
const code = character.codePointAt(0);
|
|
174
|
+
return code <= 0x1f || code === 0x7f;
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
function isStatusExitCompatible(status, exitCode) {
|
|
178
|
+
if (status === "SUCCESS")
|
|
179
|
+
return exitCode === 0;
|
|
180
|
+
if (status === "CANCELLED")
|
|
181
|
+
return exitCode === 3;
|
|
182
|
+
return [1, 2, 3, 5].includes(exitCode);
|
|
183
|
+
}
|
|
184
|
+
function isExecutionError(value) {
|
|
185
|
+
return (isRecord(value) &&
|
|
186
|
+
typeof value.category === "string" &&
|
|
187
|
+
typeof value.code === "string" &&
|
|
188
|
+
typeof value.message === "string" &&
|
|
189
|
+
typeof value.retryable === "boolean" &&
|
|
190
|
+
typeof value.detailsTruncated === "boolean");
|
|
191
|
+
}
|
|
192
|
+
function isRecord(value) {
|
|
193
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
function isUtcIso(value) {
|
|
196
|
+
return (typeof value === "string" &&
|
|
197
|
+
/T.*Z$/.test(value) &&
|
|
198
|
+
!Number.isNaN(Date.parse(value)));
|
|
199
|
+
}
|
|
200
|
+
function isOffsetIso(value) {
|
|
201
|
+
return (typeof value === "string" &&
|
|
202
|
+
/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) &&
|
|
203
|
+
!Number.isNaN(Date.parse(value)));
|
|
204
|
+
}
|
|
205
|
+
function literal(value, field, expected, errors) {
|
|
206
|
+
if (value[field] !== expected)
|
|
207
|
+
errors.push(`${field} must be ${String(expected)}`);
|
|
208
|
+
}
|
|
209
|
+
function requireString(value, field, errors) {
|
|
210
|
+
if (typeof value[field] !== "string" || value[field].length === 0)
|
|
211
|
+
errors.push(`${field} must be a non-empty string`);
|
|
212
|
+
}
|
|
213
|
+
function requireNonNegativeInteger(value, field, errors) {
|
|
214
|
+
if (!Number.isSafeInteger(value[field]) || value[field] < 0)
|
|
215
|
+
errors.push(`${field} must be a non-negative integer`);
|
|
216
|
+
}
|
|
217
|
+
function invalid(details) {
|
|
218
|
+
return {
|
|
219
|
+
kind: "INVALID_RESULT",
|
|
220
|
+
attemptOutcome: "UNKNOWN",
|
|
221
|
+
resultCode: null,
|
|
222
|
+
details,
|
|
223
|
+
result: null,
|
|
224
|
+
invocationResultCode: "INVALID_EXECUTION_RESULT",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function noResult(detail) {
|
|
228
|
+
return {
|
|
229
|
+
kind: "NO_RESULT",
|
|
230
|
+
attemptOutcome: "UNKNOWN",
|
|
231
|
+
resultCode: null,
|
|
232
|
+
details: [detail],
|
|
233
|
+
result: null,
|
|
234
|
+
invocationResultCode: NO_EXECUTION_RESULT,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=result-classifier.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface RunRequest {
|
|
2
|
+
readonly sqlPath: string;
|
|
3
|
+
readonly profile: string;
|
|
4
|
+
readonly configPath: string;
|
|
5
|
+
readonly asOf: string;
|
|
6
|
+
readonly correlationId: string;
|
|
7
|
+
readonly attemptId: string;
|
|
8
|
+
readonly expectedJobId: string;
|
|
9
|
+
readonly imports?: readonly RunImport[];
|
|
10
|
+
readonly exports?: readonly RunExport[];
|
|
11
|
+
}
|
|
12
|
+
export interface RunImport {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly sha256: string;
|
|
16
|
+
readonly bytes: number;
|
|
17
|
+
}
|
|
18
|
+
export interface RunExport {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ProcessExit {
|
|
23
|
+
readonly exitCode: number | null;
|
|
24
|
+
readonly launchFailureConfirmed?: boolean;
|
|
25
|
+
readonly launchError?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SpawnHandle {
|
|
28
|
+
readonly completion: Promise<ProcessExit>;
|
|
29
|
+
gracefulStop(): void | Promise<void>;
|
|
30
|
+
forceStop(): void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface SpawnRunRequest {
|
|
33
|
+
readonly command: string;
|
|
34
|
+
readonly args: readonly string[];
|
|
35
|
+
onStdout(chunk: string): void;
|
|
36
|
+
onStderr(chunk: string): void;
|
|
37
|
+
}
|
|
38
|
+
export type RunSpawn = (request: SpawnRunRequest) => SpawnHandle;
|
|
39
|
+
export interface RunSubprocessOptions {
|
|
40
|
+
readonly command: string;
|
|
41
|
+
readonly binArgs?: readonly string[];
|
|
42
|
+
readonly executionDirectory: string;
|
|
43
|
+
readonly timeoutMs: number | null;
|
|
44
|
+
readonly gracePeriodMs: number;
|
|
45
|
+
readonly forcedExitWaitMs?: number;
|
|
46
|
+
readonly spawn?: RunSpawn;
|
|
47
|
+
}
|
|
48
|
+
export interface SubprocessRunResult {
|
|
49
|
+
readonly exitCode: number | null;
|
|
50
|
+
readonly stdout: string;
|
|
51
|
+
readonly stderr: string;
|
|
52
|
+
readonly resultJsonPath: string;
|
|
53
|
+
readonly timedOut: boolean;
|
|
54
|
+
readonly forced: boolean;
|
|
55
|
+
readonly launchFailureConfirmed: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare class RunSubprocess {
|
|
58
|
+
private readonly options;
|
|
59
|
+
private readonly spawn;
|
|
60
|
+
constructor(options: RunSubprocessOptions);
|
|
61
|
+
run(request: RunRequest): Promise<SubprocessRunResult>;
|
|
62
|
+
}
|
|
63
|
+
export declare const spawnRunProcess: RunSpawn;
|
|
64
|
+
//# sourceMappingURL=run-subprocess.d.ts.map
|