@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,175 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
export class RunSubprocess {
|
|
5
|
+
options;
|
|
6
|
+
spawn;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
if (!isAbsolute(options.executionDirectory))
|
|
10
|
+
throw new Error("executionDirectory must be absolute");
|
|
11
|
+
if ((options.timeoutMs !== null &&
|
|
12
|
+
(!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) ||
|
|
13
|
+
!Number.isFinite(options.gracePeriodMs) ||
|
|
14
|
+
options.gracePeriodMs < 0)
|
|
15
|
+
throw new Error("timeoutMs and gracePeriodMs must be non-negative");
|
|
16
|
+
this.spawn = options.spawn ?? spawnRunProcess;
|
|
17
|
+
}
|
|
18
|
+
async run(request) {
|
|
19
|
+
if (!isAbsolute(request.sqlPath))
|
|
20
|
+
throw new Error("sqlPath must be absolute");
|
|
21
|
+
for (const [name, value] of [
|
|
22
|
+
["correlationId", request.correlationId],
|
|
23
|
+
["attemptId", request.attemptId],
|
|
24
|
+
["expectedJobId", request.expectedJobId],
|
|
25
|
+
])
|
|
26
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/.test(value))
|
|
27
|
+
throw new Error(`${name} must match ^[A-Za-z0-9._:-]{1,128}$`);
|
|
28
|
+
const metadataDirectory = join(resolve(this.options.executionDirectory), "metadata");
|
|
29
|
+
await mkdir(metadataDirectory, { recursive: true });
|
|
30
|
+
const resultJsonPath = join(metadataDirectory, `${safeSegment(request.attemptId)}.json`);
|
|
31
|
+
const contractArgs = [
|
|
32
|
+
"run",
|
|
33
|
+
"-f",
|
|
34
|
+
request.sqlPath,
|
|
35
|
+
"--profile",
|
|
36
|
+
request.profile,
|
|
37
|
+
"--config",
|
|
38
|
+
request.configPath,
|
|
39
|
+
"--as-of",
|
|
40
|
+
request.asOf,
|
|
41
|
+
"--result-json",
|
|
42
|
+
resultJsonPath,
|
|
43
|
+
"--correlation-id",
|
|
44
|
+
request.correlationId,
|
|
45
|
+
"--attempt-id",
|
|
46
|
+
request.attemptId,
|
|
47
|
+
"--expected-job-id",
|
|
48
|
+
request.expectedJobId,
|
|
49
|
+
];
|
|
50
|
+
for (const input of [...(request.imports ?? [])].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
51
|
+
if (input.name.length === 0 ||
|
|
52
|
+
input.name.includes(":") ||
|
|
53
|
+
input.name.includes("=") ||
|
|
54
|
+
[...input.name].some((character) => {
|
|
55
|
+
const code = character.codePointAt(0);
|
|
56
|
+
return code <= 0x1f || code === 0x7f;
|
|
57
|
+
}) ||
|
|
58
|
+
!isAbsolute(input.path) ||
|
|
59
|
+
!/^[a-f0-9]{64}$/u.test(input.sha256)) {
|
|
60
|
+
throw new Error("import source must have a safe name, absolute path, and SHA-256");
|
|
61
|
+
}
|
|
62
|
+
contractArgs.push("--import-csv", `${input.name}=${input.path}`, "--expected-import-sha256", `${input.name}=${input.sha256}`);
|
|
63
|
+
}
|
|
64
|
+
for (const output of [...(request.exports ?? [])].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
65
|
+
if (!isSafeIoName(output.name) || !isAbsolute(output.path)) {
|
|
66
|
+
throw new Error("export sink must have a safe name and absolute path");
|
|
67
|
+
}
|
|
68
|
+
contractArgs.push("--export-csv", `${output.name}=${output.path}`);
|
|
69
|
+
}
|
|
70
|
+
const args = [...(this.options.binArgs ?? []), ...contractArgs];
|
|
71
|
+
let stdout = "";
|
|
72
|
+
let stderr = "";
|
|
73
|
+
let handle;
|
|
74
|
+
try {
|
|
75
|
+
handle = this.spawn({
|
|
76
|
+
command: this.options.command,
|
|
77
|
+
args,
|
|
78
|
+
onStdout: (chunk) => (stdout += chunk),
|
|
79
|
+
onStderr: (chunk) => (stderr += chunk),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
exitCode: null,
|
|
85
|
+
stdout,
|
|
86
|
+
stderr: `${stderr}${diagnostic(error)}`,
|
|
87
|
+
resultJsonPath,
|
|
88
|
+
timedOut: false,
|
|
89
|
+
forced: false,
|
|
90
|
+
launchFailureConfirmed: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const first = this.options.timeoutMs === null
|
|
94
|
+
? await handle.completion
|
|
95
|
+
: await raceExit(handle.completion, this.options.timeoutMs);
|
|
96
|
+
if (first)
|
|
97
|
+
return completed(first, stdout, stderr, resultJsonPath, false, false);
|
|
98
|
+
await handle.gracefulStop();
|
|
99
|
+
const graceful = await raceExit(handle.completion, this.options.gracePeriodMs);
|
|
100
|
+
if (graceful)
|
|
101
|
+
return completed(graceful, stdout, stderr, resultJsonPath, true, false);
|
|
102
|
+
await handle.forceStop();
|
|
103
|
+
const forced = await raceExit(handle.completion, this.options.forcedExitWaitMs ?? this.options.gracePeriodMs);
|
|
104
|
+
return completed(forced ?? { exitCode: null }, stdout, stderr, resultJsonPath, true, true);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isSafeIoName(value) {
|
|
108
|
+
return (value.length >= 1 &&
|
|
109
|
+
value.length <= 128 &&
|
|
110
|
+
value !== "__net__" &&
|
|
111
|
+
!value.includes(":") &&
|
|
112
|
+
!value.includes("=") &&
|
|
113
|
+
![...value].some((character) => {
|
|
114
|
+
const code = character.codePointAt(0);
|
|
115
|
+
return code <= 0x1f || code === 0x7f;
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
export const spawnRunProcess = (request) => {
|
|
119
|
+
const child = nodeSpawn(request.command, request.args, {
|
|
120
|
+
shell: false,
|
|
121
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
child.stdout.setEncoding("utf8");
|
|
125
|
+
child.stderr.setEncoding("utf8");
|
|
126
|
+
child.stdout.on("data", request.onStdout);
|
|
127
|
+
child.stderr.on("data", request.onStderr);
|
|
128
|
+
const completion = new Promise((resolveCompletion) => {
|
|
129
|
+
child.once("error", (error) => resolveCompletion({
|
|
130
|
+
exitCode: null,
|
|
131
|
+
launchFailureConfirmed: true,
|
|
132
|
+
launchError: diagnostic(error),
|
|
133
|
+
}));
|
|
134
|
+
child.once("close", (exitCode) => resolveCompletion({ exitCode }));
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
completion,
|
|
138
|
+
gracefulStop: () => {
|
|
139
|
+
child.kill(process.platform === "win32" ? "SIGINT" : "SIGTERM");
|
|
140
|
+
},
|
|
141
|
+
forceStop: () => {
|
|
142
|
+
child.kill("SIGKILL");
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
function completed(exit, stdout, stderr, resultJsonPath, timedOut, forced) {
|
|
147
|
+
return {
|
|
148
|
+
...exit,
|
|
149
|
+
stdout,
|
|
150
|
+
stderr: `${stderr}${exit.launchError ?? ""}`,
|
|
151
|
+
resultJsonPath,
|
|
152
|
+
timedOut,
|
|
153
|
+
forced,
|
|
154
|
+
launchFailureConfirmed: exit.launchFailureConfirmed === true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function raceExit(completion, timeoutMs) {
|
|
158
|
+
return new Promise((resolveRace, reject) => {
|
|
159
|
+
const timer = setTimeout(() => resolveRace(null), timeoutMs);
|
|
160
|
+
completion.then((result) => {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
resolveRace(result);
|
|
163
|
+
}, (error) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(error);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function safeSegment(value) {
|
|
170
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
171
|
+
}
|
|
172
|
+
function diagnostic(error) {
|
|
173
|
+
return error instanceof Error ? error.message : String(error);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=run-subprocess.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InputBaseline } from "./io-path.js";
|
|
2
|
+
import type { ExecutionOutputFile } from "../executor/result-classifier.js";
|
|
3
|
+
export declare const INPUT_AUDIT_SUMMARY_MAX_LENGTH = 10000;
|
|
4
|
+
export interface StoredInputBaseline {
|
|
5
|
+
readonly source: string;
|
|
6
|
+
readonly sha256: string;
|
|
7
|
+
readonly bytes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface InputFileReceipt extends StoredInputBaseline {
|
|
10
|
+
readonly rows: number;
|
|
11
|
+
readonly encoding: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function serializeInputBaseline(inputs: readonly Pick<InputBaseline, "name" | "sha256" | "bytes">[]): string;
|
|
14
|
+
export declare function parseInputBaseline(summary: string | null): StoredInputBaseline[] | null;
|
|
15
|
+
export declare function serializeInputAuditSummary(baseline: readonly Pick<InputBaseline, "name" | "sha256" | "bytes">[], receipts: readonly InputFileReceipt[]): string;
|
|
16
|
+
export declare function serializeOutputAuditSummary(outputs: readonly ExecutionOutputFile[]): string;
|
|
17
|
+
export declare function serializeIoAuditSummary(baseline: readonly Pick<InputBaseline, "name" | "sha256" | "bytes">[], inputReceipts: readonly InputFileReceipt[], outputReceipts: readonly ExecutionOutputFile[]): string;
|
|
18
|
+
export declare function inputBaselinesEqual(expected: readonly StoredInputBaseline[], actual: readonly Pick<InputBaseline, "name" | "sha256" | "bytes">[]): boolean;
|
|
19
|
+
//# sourceMappingURL=input-baseline.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export const INPUT_AUDIT_SUMMARY_MAX_LENGTH = 10_000;
|
|
2
|
+
export function serializeInputBaseline(inputs) {
|
|
3
|
+
const normalized = normalizeBaseline(inputs);
|
|
4
|
+
const summary = checkedJson({
|
|
5
|
+
version: 1,
|
|
6
|
+
kind: "KSQL_FLOWNET_INPUT_BASELINE",
|
|
7
|
+
inputs: normalized,
|
|
8
|
+
});
|
|
9
|
+
// Reserve enough room for the terminal receipt form before accepting the
|
|
10
|
+
// pre-execution baseline into the fixed-size safe summary field.
|
|
11
|
+
checkedJson({
|
|
12
|
+
version: 1,
|
|
13
|
+
kind: "KSQL_FLOWNET_INPUT_AUDIT",
|
|
14
|
+
baseline: normalized,
|
|
15
|
+
input_files: normalized.map((input) => ({
|
|
16
|
+
source: input.source,
|
|
17
|
+
type: "IMPORT",
|
|
18
|
+
sha256: input.sha256.slice(0, 12),
|
|
19
|
+
bytes: input.bytes,
|
|
20
|
+
rows: Number.MAX_SAFE_INTEGER,
|
|
21
|
+
encoding: "x".repeat(32),
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
return summary;
|
|
25
|
+
}
|
|
26
|
+
export function parseInputBaseline(summary) {
|
|
27
|
+
if (summary === null || summary.length > INPUT_AUDIT_SUMMARY_MAX_LENGTH)
|
|
28
|
+
return null;
|
|
29
|
+
let value;
|
|
30
|
+
try {
|
|
31
|
+
value = JSON.parse(summary);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (!isRecord(value))
|
|
37
|
+
return null;
|
|
38
|
+
if (value.kind === "KSQL_FLOWNET_IO_AUDIT") {
|
|
39
|
+
if (!hasOnlyKeys(value, [
|
|
40
|
+
"version",
|
|
41
|
+
"kind",
|
|
42
|
+
"baseline",
|
|
43
|
+
"input_files",
|
|
44
|
+
"output_files",
|
|
45
|
+
]))
|
|
46
|
+
throw new Error("IO audit summary has unknown fields");
|
|
47
|
+
if (value.version !== 1 || !Array.isArray(value.baseline))
|
|
48
|
+
throw new Error("IO audit summary has an invalid version or baseline");
|
|
49
|
+
return strictBaseline(value.baseline);
|
|
50
|
+
}
|
|
51
|
+
if (value.kind === "KSQL_FLOWNET_INPUT_AUDIT") {
|
|
52
|
+
if (!hasOnlyKeys(value, ["version", "kind", "baseline", "input_files"]))
|
|
53
|
+
throw new Error("input audit summary has unknown fields");
|
|
54
|
+
if (value.version !== 1 || !Array.isArray(value.baseline))
|
|
55
|
+
throw new Error("input audit summary has an invalid version or baseline");
|
|
56
|
+
return strictBaseline(value.baseline);
|
|
57
|
+
}
|
|
58
|
+
if (value.kind !== "KSQL_FLOWNET_INPUT_BASELINE")
|
|
59
|
+
return null;
|
|
60
|
+
if (!hasOnlyKeys(value, ["version", "kind", "inputs"]))
|
|
61
|
+
throw new Error("input baseline summary has unknown fields");
|
|
62
|
+
if (value.version !== 1 || !Array.isArray(value.inputs))
|
|
63
|
+
throw new Error("input baseline summary has an invalid version or inputs");
|
|
64
|
+
return strictBaseline(value.inputs);
|
|
65
|
+
}
|
|
66
|
+
export function serializeInputAuditSummary(baseline, receipts) {
|
|
67
|
+
const normalizedBaseline = normalizeBaseline(baseline);
|
|
68
|
+
const normalizedReceipts = [...receipts]
|
|
69
|
+
.sort((left, right) => compareText(left.source, right.source))
|
|
70
|
+
.map((receipt) => {
|
|
71
|
+
assertStoredBaseline(receipt);
|
|
72
|
+
if (!Number.isSafeInteger(receipt.rows) || receipt.rows < 0)
|
|
73
|
+
throw new Error("input receipt rows must be a non-negative integer");
|
|
74
|
+
if (!/^[A-Za-z0-9._-]{1,32}$/u.test(receipt.encoding))
|
|
75
|
+
throw new Error("input receipt encoding is invalid");
|
|
76
|
+
return {
|
|
77
|
+
source: receipt.source,
|
|
78
|
+
type: "IMPORT",
|
|
79
|
+
sha256: receipt.sha256.slice(0, 12),
|
|
80
|
+
bytes: receipt.bytes,
|
|
81
|
+
rows: receipt.rows,
|
|
82
|
+
encoding: receipt.encoding,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return checkedJson({
|
|
86
|
+
version: 1,
|
|
87
|
+
kind: "KSQL_FLOWNET_INPUT_AUDIT",
|
|
88
|
+
baseline: normalizedBaseline,
|
|
89
|
+
input_files: normalizedReceipts,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function serializeOutputAuditSummary(outputs) {
|
|
93
|
+
return checkedJson({
|
|
94
|
+
version: 1,
|
|
95
|
+
kind: "KSQL_FLOWNET_OUTPUT_AUDIT",
|
|
96
|
+
output_files: normalizeOutputReceipts(outputs),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function serializeIoAuditSummary(baseline, inputReceipts, outputReceipts) {
|
|
100
|
+
const inputAudit = JSON.parse(serializeInputAuditSummary(baseline, inputReceipts));
|
|
101
|
+
return checkedJson({
|
|
102
|
+
version: 1,
|
|
103
|
+
kind: "KSQL_FLOWNET_IO_AUDIT",
|
|
104
|
+
baseline: inputAudit.baseline,
|
|
105
|
+
input_files: inputAudit.input_files,
|
|
106
|
+
output_files: normalizeOutputReceipts(outputReceipts),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function normalizeOutputReceipts(outputs) {
|
|
110
|
+
const normalized = outputs.map((output) => {
|
|
111
|
+
if (!isSafeSourceName(output.name) ||
|
|
112
|
+
!/^[a-f0-9]{64}$/u.test(output.sha256) ||
|
|
113
|
+
!Number.isSafeInteger(output.bytes) ||
|
|
114
|
+
output.bytes < 0 ||
|
|
115
|
+
!Number.isSafeInteger(output.rows) ||
|
|
116
|
+
output.rows < 0 ||
|
|
117
|
+
!["utf8", "sjis"].includes(output.encoding))
|
|
118
|
+
throw new Error("output receipt contains invalid audit metadata");
|
|
119
|
+
return {
|
|
120
|
+
sink: output.name,
|
|
121
|
+
type: "EXPORT",
|
|
122
|
+
sha256: output.sha256,
|
|
123
|
+
bytes: output.bytes,
|
|
124
|
+
rows: output.rows,
|
|
125
|
+
encoding: output.encoding,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
if (new Set(normalized.map((output) => output.sink)).size !== normalized.length)
|
|
129
|
+
throw new Error("output receipt sink names must be unique");
|
|
130
|
+
return normalized;
|
|
131
|
+
}
|
|
132
|
+
export function inputBaselinesEqual(expected, actual) {
|
|
133
|
+
return serializeStoredBaseline(expected) === serializeInputBaseline(actual);
|
|
134
|
+
}
|
|
135
|
+
function serializeStoredBaseline(inputs) {
|
|
136
|
+
for (const input of inputs)
|
|
137
|
+
assertStoredBaseline(input);
|
|
138
|
+
return checkedJson({
|
|
139
|
+
version: 1,
|
|
140
|
+
kind: "KSQL_FLOWNET_INPUT_BASELINE",
|
|
141
|
+
inputs: [...inputs].sort((left, right) => compareText(left.source, right.source)),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function normalizeBaseline(inputs) {
|
|
145
|
+
const normalized = inputs
|
|
146
|
+
.map((input) => ({
|
|
147
|
+
source: input.name,
|
|
148
|
+
sha256: input.sha256,
|
|
149
|
+
bytes: input.bytes,
|
|
150
|
+
}))
|
|
151
|
+
.sort((left, right) => compareText(left.source, right.source));
|
|
152
|
+
for (const input of normalized)
|
|
153
|
+
assertStoredBaseline(input);
|
|
154
|
+
if (new Set(normalized.map((input) => input.source)).size !== normalized.length)
|
|
155
|
+
throw new Error("input baseline source names must be unique");
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
function strictBaseline(value) {
|
|
159
|
+
const inputs = value.map((item) => {
|
|
160
|
+
if (!isRecord(item) || !hasOnlyKeys(item, ["source", "sha256", "bytes"]))
|
|
161
|
+
throw new Error("input baseline entry has an invalid shape");
|
|
162
|
+
const baseline = {
|
|
163
|
+
source: item.source,
|
|
164
|
+
sha256: item.sha256,
|
|
165
|
+
bytes: item.bytes,
|
|
166
|
+
};
|
|
167
|
+
assertStoredBaseline(baseline);
|
|
168
|
+
return baseline;
|
|
169
|
+
});
|
|
170
|
+
const canonical = [...inputs].sort((left, right) => compareText(left.source, right.source));
|
|
171
|
+
if (JSON.stringify(inputs) !== JSON.stringify(canonical))
|
|
172
|
+
throw new Error("input baseline entries are not in canonical source order");
|
|
173
|
+
if (new Set(inputs.map((input) => input.source)).size !== inputs.length)
|
|
174
|
+
throw new Error("input baseline source names must be unique");
|
|
175
|
+
return inputs;
|
|
176
|
+
}
|
|
177
|
+
function assertStoredBaseline(value) {
|
|
178
|
+
if (typeof value.source !== "string" ||
|
|
179
|
+
!isSafeSourceName(value.source) ||
|
|
180
|
+
typeof value.sha256 !== "string" ||
|
|
181
|
+
!/^[a-f0-9]{64}$/u.test(value.sha256) ||
|
|
182
|
+
!Number.isSafeInteger(value.bytes) ||
|
|
183
|
+
value.bytes < 0)
|
|
184
|
+
throw new Error("input baseline accepts only source, SHA-256, and byte count");
|
|
185
|
+
}
|
|
186
|
+
function isSafeSourceName(value) {
|
|
187
|
+
return (value.length >= 1 &&
|
|
188
|
+
value.length <= 128 &&
|
|
189
|
+
value !== "__net__" &&
|
|
190
|
+
!value.includes(":") &&
|
|
191
|
+
!value.includes("=") &&
|
|
192
|
+
![...value].some((character) => {
|
|
193
|
+
const code = character.codePointAt(0);
|
|
194
|
+
return code <= 0x1f || code === 0x7f;
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
function checkedJson(value) {
|
|
198
|
+
const summary = JSON.stringify(value);
|
|
199
|
+
if (summary.length > INPUT_AUDIT_SUMMARY_MAX_LENGTH)
|
|
200
|
+
throw new Error(`input audit summary exceeds ${INPUT_AUDIT_SUMMARY_MAX_LENGTH} characters`);
|
|
201
|
+
return summary;
|
|
202
|
+
}
|
|
203
|
+
function isRecord(value) {
|
|
204
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
205
|
+
}
|
|
206
|
+
function hasOnlyKeys(value, keys) {
|
|
207
|
+
const actual = Object.keys(value).sort(compareText);
|
|
208
|
+
const expected = [...keys].sort(compareText);
|
|
209
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
210
|
+
}
|
|
211
|
+
function compareText(left, right) {
|
|
212
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=input-baseline.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
export function loadIoConfig(environment = process.env) {
|
|
4
|
+
const configuredRoot = environment.KSQL_FLOWNET_IO_DIR;
|
|
5
|
+
if (configuredRoot === undefined || configuredRoot.trim() === "") {
|
|
6
|
+
throw new Error("KSQL_FLOWNET_IO_DIR is required for networks with inputs or outputs");
|
|
7
|
+
}
|
|
8
|
+
if (!isAbsolute(configuredRoot)) {
|
|
9
|
+
throw new Error("KSQL_FLOWNET_IO_DIR must be an absolute path");
|
|
10
|
+
}
|
|
11
|
+
const root = resolve(configuredRoot);
|
|
12
|
+
let stat;
|
|
13
|
+
try {
|
|
14
|
+
stat = lstatSync(root);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error("KSQL_FLOWNET_IO_DIR must be an existing directory", {
|
|
18
|
+
cause: error,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (!stat.isDirectory()) {
|
|
22
|
+
throw new Error("KSQL_FLOWNET_IO_DIR must be an existing directory");
|
|
23
|
+
}
|
|
24
|
+
const rawRetention = environment.KSQL_FLOWNET_IO_RETENTION_DAYS;
|
|
25
|
+
const retentionDays = rawRetention === undefined || rawRetention.trim() === ""
|
|
26
|
+
? 90
|
|
27
|
+
: Number(rawRetention);
|
|
28
|
+
if (!Number.isSafeInteger(retentionDays) || retentionDays <= 0) {
|
|
29
|
+
throw new Error("KSQL_FLOWNET_IO_RETENTION_DAYS must be a positive integer");
|
|
30
|
+
}
|
|
31
|
+
return { root, retentionDays };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=io-config.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type InputPathErrorCode = "INPUT_PATH_REJECTED" | "INPUT_FILE_MISSING" | "INPUT_FILE_MUTATED";
|
|
2
|
+
export declare class InputPathError extends Error {
|
|
3
|
+
readonly code: InputPathErrorCode;
|
|
4
|
+
constructor(code: InputPathErrorCode, message: string, options?: ErrorOptions);
|
|
5
|
+
}
|
|
6
|
+
export declare class OutputPathError extends Error {
|
|
7
|
+
readonly code: "OUTPUT_PATH_REJECTED";
|
|
8
|
+
constructor(message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
export interface InputBaseline {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly sha256: string;
|
|
14
|
+
readonly bytes: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveNodeInputsInput {
|
|
17
|
+
readonly ioRoot: string;
|
|
18
|
+
readonly patterns: Readonly<Record<string, string>>;
|
|
19
|
+
readonly businessKey: string;
|
|
20
|
+
readonly profile: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedOutput {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ResolveNodeOutputsInput {
|
|
27
|
+
readonly ioRoot: string;
|
|
28
|
+
readonly patterns: Readonly<Record<string, string>>;
|
|
29
|
+
readonly businessKey: string;
|
|
30
|
+
readonly profile: string;
|
|
31
|
+
readonly runId: string;
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function resolveNodeOutputs(input: ResolveNodeOutputsInput): Promise<readonly ResolvedOutput[]>;
|
|
35
|
+
export declare function resolveNodeInputs(input: ResolveNodeInputsInput): Promise<readonly InputBaseline[]>;
|
|
36
|
+
export declare function percentEncodePathSegment(value: string): string;
|
|
37
|
+
//# sourceMappingURL=io-path.d.ts.map
|