@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,32 @@
|
|
|
1
|
+
export function stableTopologicalSort(nodes) {
|
|
2
|
+
const indexById = new Map(nodes.map((node, index) => [node.id, index]));
|
|
3
|
+
const indegree = new Map(nodes.map((node) => [node.id, node.depends_on.length]));
|
|
4
|
+
const dependents = new Map(nodes.map((node) => [node.id, []]));
|
|
5
|
+
for (const node of nodes) {
|
|
6
|
+
for (const dependency of node.depends_on) {
|
|
7
|
+
dependents.get(dependency)?.push(node.id);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const ready = nodes
|
|
11
|
+
.filter((node) => indegree.get(node.id) === 0)
|
|
12
|
+
.map((node) => node.id);
|
|
13
|
+
const order = [];
|
|
14
|
+
while (ready.length > 0) {
|
|
15
|
+
ready.sort((left, right) => (indexById.get(left) ?? 0) - (indexById.get(right) ?? 0));
|
|
16
|
+
const current = ready.shift();
|
|
17
|
+
if (current === undefined)
|
|
18
|
+
break;
|
|
19
|
+
order.push(current);
|
|
20
|
+
for (const dependent of dependents.get(current) ?? []) {
|
|
21
|
+
const remaining = (indegree.get(dependent) ?? 0) - 1;
|
|
22
|
+
indegree.set(dependent, remaining);
|
|
23
|
+
if (remaining === 0)
|
|
24
|
+
ready.push(dependent);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const cyclicNodeIds = nodes
|
|
28
|
+
.filter((node) => (indegree.get(node.id) ?? 0) > 0)
|
|
29
|
+
.map((node) => node.id);
|
|
30
|
+
return { order, cyclicNodeIds };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=topological-sort.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BusinessKeyPolicy, type ScheduledPeriodPolicy, type ValidationError } from "./network-definition.js";
|
|
2
|
+
export interface BusinessKeyInput {
|
|
3
|
+
readonly networkId: string;
|
|
4
|
+
readonly policy: BusinessKeyPolicy;
|
|
5
|
+
readonly scheduledFor?: string;
|
|
6
|
+
readonly businessKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BusinessKeyResult {
|
|
9
|
+
readonly businessKey?: string;
|
|
10
|
+
readonly errors: readonly ValidationError[];
|
|
11
|
+
}
|
|
12
|
+
export declare function normalizeScheduledFor(value: string): string | undefined;
|
|
13
|
+
export declare function validateScheduledPeriodPolicy(policy: ScheduledPeriodPolicy): ValidationError[];
|
|
14
|
+
export declare function resolveBusinessKey(input: BusinessKeyInput): BusinessKeyResult;
|
|
15
|
+
//# sourceMappingURL=business-key.d.ts.map
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { MAX_BUSINESS_KEY_LENGTH, } from "./network-definition.js";
|
|
2
|
+
const ISO_TIMESTAMP_WITH_OFFSET = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|([+-])(\d{2}):(\d{2}))$/;
|
|
3
|
+
const ALLOWED_PLACEHOLDERS = new Set([
|
|
4
|
+
"{network_id}",
|
|
5
|
+
"{yyyy}",
|
|
6
|
+
"{MM}",
|
|
7
|
+
"{dd}",
|
|
8
|
+
]);
|
|
9
|
+
function hasControlCharacter(value) {
|
|
10
|
+
return [...value].some((character) => {
|
|
11
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
12
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function isLeapYear(year) {
|
|
16
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
17
|
+
}
|
|
18
|
+
function daysInMonth(year, month) {
|
|
19
|
+
const days = [
|
|
20
|
+
31,
|
|
21
|
+
isLeapYear(year) ? 29 : 28,
|
|
22
|
+
31,
|
|
23
|
+
30,
|
|
24
|
+
31,
|
|
25
|
+
30,
|
|
26
|
+
31,
|
|
27
|
+
31,
|
|
28
|
+
30,
|
|
29
|
+
31,
|
|
30
|
+
30,
|
|
31
|
+
31,
|
|
32
|
+
];
|
|
33
|
+
return days[month - 1] ?? 0;
|
|
34
|
+
}
|
|
35
|
+
function parseScheduledFor(value) {
|
|
36
|
+
const match = ISO_TIMESTAMP_WITH_OFFSET.exec(value);
|
|
37
|
+
if (match === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
const year = Number(match[1]);
|
|
40
|
+
const month = Number(match[2]);
|
|
41
|
+
const day = Number(match[3]);
|
|
42
|
+
const hour = Number(match[4]);
|
|
43
|
+
const minute = Number(match[5]);
|
|
44
|
+
const second = match[6] === undefined ? 0 : Number(match[6]);
|
|
45
|
+
const offsetHour = match[10] === undefined ? 0 : Number(match[10]);
|
|
46
|
+
const offsetMinute = match[11] === undefined ? 0 : Number(match[11]);
|
|
47
|
+
// Intl/Date cannot faithfully represent arbitrary precision or extended
|
|
48
|
+
// years. Reject those forms instead of silently normalizing them.
|
|
49
|
+
if (year === 0 ||
|
|
50
|
+
month < 1 ||
|
|
51
|
+
month > 12 ||
|
|
52
|
+
day < 1 ||
|
|
53
|
+
day > daysInMonth(year, month) ||
|
|
54
|
+
hour > 23 ||
|
|
55
|
+
minute > 59 ||
|
|
56
|
+
second > 59 ||
|
|
57
|
+
offsetHour > 14 ||
|
|
58
|
+
offsetMinute > 59 ||
|
|
59
|
+
(offsetHour === 14 && offsetMinute !== 0)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const epochMilliseconds = Date.parse(value);
|
|
63
|
+
return Number.isFinite(epochMilliseconds)
|
|
64
|
+
? new Date(epochMilliseconds)
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
export function normalizeScheduledFor(value) {
|
|
68
|
+
return parseScheduledFor(value)?.toISOString();
|
|
69
|
+
}
|
|
70
|
+
function calendarDateAt(date, timeZone) {
|
|
71
|
+
const formatter = new Intl.DateTimeFormat("en-CA-u-ca-gregory-nu-latn", {
|
|
72
|
+
timeZone,
|
|
73
|
+
year: "numeric",
|
|
74
|
+
month: "2-digit",
|
|
75
|
+
day: "2-digit",
|
|
76
|
+
});
|
|
77
|
+
const parts = new Map(formatter
|
|
78
|
+
.formatToParts(date)
|
|
79
|
+
.filter((part) => part.type !== "literal")
|
|
80
|
+
.map((part) => [part.type, part.value]));
|
|
81
|
+
const yyyy = parts.get("year");
|
|
82
|
+
const MM = parts.get("month");
|
|
83
|
+
const dd = parts.get("day");
|
|
84
|
+
if (yyyy === undefined || MM === undefined || dd === undefined) {
|
|
85
|
+
throw new Error("Intl did not return the requested calendar date parts");
|
|
86
|
+
}
|
|
87
|
+
return { yyyy, MM, dd };
|
|
88
|
+
}
|
|
89
|
+
export function validateScheduledPeriodPolicy(policy) {
|
|
90
|
+
const errors = [];
|
|
91
|
+
try {
|
|
92
|
+
new Intl.DateTimeFormat("en-US", { timeZone: policy.timezone }).format();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
errors.push({
|
|
96
|
+
code: "TIMEZONE_INVALID",
|
|
97
|
+
path: "$.business_key_policy.timezone",
|
|
98
|
+
message: `unknown IANA timezone '${policy.timezone}'`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const placeholders = policy.format.match(/\{[^{}]*\}/g) ?? [];
|
|
102
|
+
for (const placeholder of placeholders) {
|
|
103
|
+
if (!ALLOWED_PLACEHOLDERS.has(placeholder)) {
|
|
104
|
+
errors.push({
|
|
105
|
+
code: "FORMAT_PLACEHOLDER_UNSUPPORTED",
|
|
106
|
+
path: "$.business_key_policy.format",
|
|
107
|
+
message: `unsupported placeholder '${placeholder}'`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const remainder = placeholders.reduce((value, placeholder) => value.replace(placeholder, ""), policy.format);
|
|
112
|
+
if (remainder.includes("{") || remainder.includes("}")) {
|
|
113
|
+
errors.push({
|
|
114
|
+
code: "FORMAT_PLACEHOLDER_INVALID",
|
|
115
|
+
path: "$.business_key_policy.format",
|
|
116
|
+
message: "contains an invalid placeholder expression",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// A period label must retain every coarser calendar component; otherwise
|
|
120
|
+
// different days, months, or years could collapse to the same key.
|
|
121
|
+
const required = policy.period === "day" ? ["{yyyy}", "{MM}", "{dd}"] : ["{yyyy}", "{MM}"];
|
|
122
|
+
for (const placeholder of required) {
|
|
123
|
+
if (!placeholders.includes(placeholder)) {
|
|
124
|
+
errors.push({
|
|
125
|
+
code: "FORMAT_PERIOD_MISMATCH",
|
|
126
|
+
path: "$.business_key_policy.format",
|
|
127
|
+
message: `period '${policy.period}' requires placeholder '${placeholder}'`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Phase 1 month keys represent a normalized month boundary. Including a day
|
|
132
|
+
// would make the same month depend on the scheduled day, so fail closed.
|
|
133
|
+
if (policy.period === "month" && placeholders.includes("{dd}")) {
|
|
134
|
+
errors.push({
|
|
135
|
+
code: "FORMAT_PERIOD_MISMATCH",
|
|
136
|
+
path: "$.business_key_policy.format",
|
|
137
|
+
message: "period 'month' must not use placeholder '{dd}'",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return errors;
|
|
141
|
+
}
|
|
142
|
+
function validateBusinessKey(value, path) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
if (value.length === 0) {
|
|
145
|
+
errors.push({
|
|
146
|
+
code: "BUSINESS_KEY_EMPTY",
|
|
147
|
+
path,
|
|
148
|
+
message: "must not be empty",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (value.length > MAX_BUSINESS_KEY_LENGTH) {
|
|
152
|
+
errors.push({
|
|
153
|
+
code: "BUSINESS_KEY_TOO_LONG",
|
|
154
|
+
path,
|
|
155
|
+
message: `must be at most ${MAX_BUSINESS_KEY_LENGTH} characters`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (hasControlCharacter(value)) {
|
|
159
|
+
errors.push({
|
|
160
|
+
code: "BUSINESS_KEY_CONTROL_CHARACTER",
|
|
161
|
+
path,
|
|
162
|
+
message: "must not contain NUL, newline, or other control characters",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return errors;
|
|
166
|
+
}
|
|
167
|
+
export function resolveBusinessKey(input) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
if (input.policy.type === "explicit") {
|
|
170
|
+
if (input.scheduledFor !== undefined) {
|
|
171
|
+
errors.push({
|
|
172
|
+
code: "SCHEDULED_FOR_NOT_ALLOWED",
|
|
173
|
+
path: "--scheduled-for",
|
|
174
|
+
message: "is not allowed when business_key_policy.type is 'explicit'",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (input.businessKey === undefined) {
|
|
178
|
+
errors.push({
|
|
179
|
+
code: "BUSINESS_KEY_REQUIRED",
|
|
180
|
+
path: "--business-key",
|
|
181
|
+
message: "is required when business_key_policy.type is 'explicit'",
|
|
182
|
+
});
|
|
183
|
+
return { errors };
|
|
184
|
+
}
|
|
185
|
+
errors.push(...validateBusinessKey(input.businessKey, "--business-key"));
|
|
186
|
+
return errors.length === 0
|
|
187
|
+
? { businessKey: input.businessKey, errors }
|
|
188
|
+
: { errors };
|
|
189
|
+
}
|
|
190
|
+
const policyErrors = validateScheduledPeriodPolicy(input.policy);
|
|
191
|
+
errors.push(...policyErrors);
|
|
192
|
+
const { businessKey: providedBusinessKey, scheduledFor: scheduledForInput } = input;
|
|
193
|
+
if (scheduledForInput === undefined) {
|
|
194
|
+
if (providedBusinessKey === undefined) {
|
|
195
|
+
errors.push({
|
|
196
|
+
code: "SCHEDULED_FOR_REQUIRED",
|
|
197
|
+
path: "--scheduled-for/--business-key",
|
|
198
|
+
message: "either --scheduled-for or --business-key is required when business_key_policy.type is 'scheduled_period'",
|
|
199
|
+
});
|
|
200
|
+
return { errors };
|
|
201
|
+
}
|
|
202
|
+
errors.push(...validateBusinessKey(providedBusinessKey, "--business-key"));
|
|
203
|
+
return errors.length === 0
|
|
204
|
+
? { businessKey: providedBusinessKey, errors }
|
|
205
|
+
: { errors };
|
|
206
|
+
}
|
|
207
|
+
const scheduledFor = parseScheduledFor(scheduledForInput);
|
|
208
|
+
if (scheduledFor === undefined) {
|
|
209
|
+
errors.push({
|
|
210
|
+
code: "SCHEDULED_FOR_INVALID",
|
|
211
|
+
path: "--scheduled-for",
|
|
212
|
+
message: "must be a valid ISO 8601 timestamp with an explicit offset",
|
|
213
|
+
});
|
|
214
|
+
return { errors };
|
|
215
|
+
}
|
|
216
|
+
if (policyErrors.length > 0)
|
|
217
|
+
return { errors };
|
|
218
|
+
if (providedBusinessKey !== undefined) {
|
|
219
|
+
errors.push(...validateBusinessKey(providedBusinessKey, "--business-key"));
|
|
220
|
+
return errors.length === 0
|
|
221
|
+
? { businessKey: providedBusinessKey, errors }
|
|
222
|
+
: { errors };
|
|
223
|
+
}
|
|
224
|
+
let calendarDate;
|
|
225
|
+
try {
|
|
226
|
+
calendarDate = calendarDateAt(scheduledFor, input.policy.timezone);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
errors.push({
|
|
230
|
+
code: "TIMEZONE_INVALID",
|
|
231
|
+
path: "$.business_key_policy.timezone",
|
|
232
|
+
message: `unknown IANA timezone '${input.policy.timezone}'`,
|
|
233
|
+
});
|
|
234
|
+
return { errors };
|
|
235
|
+
}
|
|
236
|
+
const replacements = {
|
|
237
|
+
"{network_id}": input.networkId,
|
|
238
|
+
"{yyyy}": calendarDate.yyyy,
|
|
239
|
+
"{MM}": calendarDate.MM,
|
|
240
|
+
// A month policy is normalized to its month boundary. Validation forbids
|
|
241
|
+
// {dd} for month, while using "01" here keeps this function total if an
|
|
242
|
+
// already-validated policy is accidentally bypassed.
|
|
243
|
+
"{dd}": input.policy.period === "month" ? "01" : calendarDate.dd,
|
|
244
|
+
};
|
|
245
|
+
const businessKey = input.policy.format.replace(/\{[^{}]*\}/g, (placeholder) => replacements[placeholder] ?? placeholder);
|
|
246
|
+
errors.push(...validateBusinessKey(businessKey, "$.business_key_policy.format"));
|
|
247
|
+
return errors.length === 0 ? { businessKey, errors } : { errors };
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=business-key.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH = 128;
|
|
2
|
+
export declare const CANONICAL_LOCK_KEY_LENGTH = 46;
|
|
3
|
+
export declare const MAX_KINTONE_UNIQUE_KEY_LENGTH = 64;
|
|
4
|
+
declare const canonicalLockKeyBrand: unique symbol;
|
|
5
|
+
export type CanonicalLockKey<Prefix extends "N1" | "J1"> = `${Prefix}:${string}` & {
|
|
6
|
+
readonly [canonicalLockKeyBrand]: Prefix;
|
|
7
|
+
};
|
|
8
|
+
export type CanonicalLockKeyErrorCode = "LOCK_KEY_IDENTIFIER_NOT_STRING" | "LOCK_KEY_IDENTIFIER_EMPTY" | "LOCK_KEY_IDENTIFIER_NUL" | "LOCK_KEY_IDENTIFIER_SEPARATOR" | "LOCK_KEY_IDENTIFIER_RESERVED" | "LOCK_KEY_IDENTIFIER_TOO_LONG" | "LOCK_KEY_LENGTH_INVARIANT";
|
|
9
|
+
export declare class CanonicalLockKeyError extends Error {
|
|
10
|
+
readonly code: CanonicalLockKeyErrorCode;
|
|
11
|
+
readonly component: "profile" | "networkId" | "jobId" | "generatedKey";
|
|
12
|
+
constructor(code: CanonicalLockKeyErrorCode, component: "profile" | "networkId" | "jobId" | "generatedKey", message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function networkLockKey(profile: string, networkId: string): CanonicalLockKey<"N1">;
|
|
15
|
+
export declare function jobLockKey(profile: string, jobId: string): CanonicalLockKey<"J1">;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=canonical-lock-key.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// Keep these rules aligned with the identifier constraints enforced by
|
|
3
|
+
// schemas/network-definition.schema.json and validate-network.ts.
|
|
4
|
+
export const MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH = 128;
|
|
5
|
+
export const CANONICAL_LOCK_KEY_LENGTH = 46;
|
|
6
|
+
export const MAX_KINTONE_UNIQUE_KEY_LENGTH = 64;
|
|
7
|
+
export class CanonicalLockKeyError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
component;
|
|
10
|
+
constructor(code, component, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "CanonicalLockKeyError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.component = component;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function canonicalIdentifier(value, component) {
|
|
18
|
+
if (typeof value !== "string") {
|
|
19
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_NOT_STRING", component, `${component} must be a string`);
|
|
20
|
+
}
|
|
21
|
+
if (value.length === 0) {
|
|
22
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_EMPTY", component, `${component} must not be empty`);
|
|
23
|
+
}
|
|
24
|
+
if (value.includes("\0")) {
|
|
25
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_NUL", component, `${component} must not contain NUL`);
|
|
26
|
+
}
|
|
27
|
+
if (value.includes(":")) {
|
|
28
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_SEPARATOR", component, `${component} must not contain ':'`);
|
|
29
|
+
}
|
|
30
|
+
if (value === "__net__") {
|
|
31
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_RESERVED", component, `${component} must not use reserved identifier '__net__'`);
|
|
32
|
+
}
|
|
33
|
+
const normalized = value.normalize("NFC");
|
|
34
|
+
const length = Array.from(normalized).length;
|
|
35
|
+
if (length > MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH) {
|
|
36
|
+
throw new CanonicalLockKeyError("LOCK_KEY_IDENTIFIER_TOO_LONG", component, `${component} must be at most ${MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH} Unicode characters after NFC normalization`);
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
function lockKey(prefix, profile, identifier, component) {
|
|
41
|
+
const canonicalProfile = canonicalIdentifier(profile, "profile");
|
|
42
|
+
const canonicalTarget = canonicalIdentifier(identifier, component);
|
|
43
|
+
const canonicalInput = `${prefix}\0${canonicalProfile}\0${canonicalTarget}`;
|
|
44
|
+
const digest = createHash("sha256")
|
|
45
|
+
.update(canonicalInput, "utf8")
|
|
46
|
+
.digest("base64url");
|
|
47
|
+
const key = `${prefix}:${digest}`;
|
|
48
|
+
if (key.length !== CANONICAL_LOCK_KEY_LENGTH ||
|
|
49
|
+
key.length > MAX_KINTONE_UNIQUE_KEY_LENGTH) {
|
|
50
|
+
throw new CanonicalLockKeyError("LOCK_KEY_LENGTH_INVARIANT", "generatedKey", "generated canonical lock key violates its length contract");
|
|
51
|
+
}
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
export function networkLockKey(profile, networkId) {
|
|
55
|
+
return lockKey("N1", profile, networkId, "networkId");
|
|
56
|
+
}
|
|
57
|
+
export function jobLockKey(profile, jobId) {
|
|
58
|
+
return lockKey("J1", profile, jobId, "jobId");
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=canonical-lock-key.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CANONICAL_RECORD_KEY_LENGTH = 46;
|
|
2
|
+
declare const canonicalRecordKeyBrand: unique symbol;
|
|
3
|
+
export type CanonicalRecordKey<Prefix extends "R1" | "S1" | "A1"> = `${Prefix}:${string}` & {
|
|
4
|
+
readonly [canonicalRecordKeyBrand]: Prefix;
|
|
5
|
+
};
|
|
6
|
+
export type CanonicalRecordKeyErrorCode = "RECORD_KEY_IDENTIFIER_NOT_STRING" | "RECORD_KEY_IDENTIFIER_EMPTY" | "RECORD_KEY_IDENTIFIER_NUL" | "RECORD_KEY_IDENTIFIER_SEPARATOR" | "RECORD_KEY_IDENTIFIER_RESERVED" | "RECORD_KEY_IDENTIFIER_TOO_LONG" | "RECORD_KEY_ATTEMPT_NO_INVALID" | "RECORD_KEY_LENGTH_INVARIANT";
|
|
7
|
+
type RecordKeyIdentifierComponent = "profile" | "networkId" | "businessKey" | "runId" | "nodeId";
|
|
8
|
+
type RecordKeyComponent = RecordKeyIdentifierComponent | "attemptNo" | "generatedKey";
|
|
9
|
+
export declare class CanonicalRecordKeyError extends Error {
|
|
10
|
+
readonly code: CanonicalRecordKeyErrorCode;
|
|
11
|
+
readonly component: RecordKeyComponent;
|
|
12
|
+
constructor(code: CanonicalRecordKeyErrorCode, component: RecordKeyComponent, message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function runKey(profile: string, networkId: string, businessKey: string): CanonicalRecordKey<"R1">;
|
|
15
|
+
export declare function nodeStateKey(runId: string, nodeId: string): CanonicalRecordKey<"S1">;
|
|
16
|
+
export declare function attemptKey(runId: string, nodeId: string, attemptNo: number): CanonicalRecordKey<"A1">;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=canonical-record-key.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH, MAX_KINTONE_UNIQUE_KEY_LENGTH, } from "./canonical-lock-key.js";
|
|
3
|
+
export const CANONICAL_RECORD_KEY_LENGTH = 46;
|
|
4
|
+
export class CanonicalRecordKeyError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
component;
|
|
7
|
+
constructor(code, component, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CanonicalRecordKeyError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.component = component;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function canonicalIdentifier(value, component) {
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_NOT_STRING", component, `${component} must be a string`);
|
|
17
|
+
}
|
|
18
|
+
if (value.length === 0) {
|
|
19
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_EMPTY", component, `${component} must not be empty`);
|
|
20
|
+
}
|
|
21
|
+
if (value.includes("\0")) {
|
|
22
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_NUL", component, `${component} must not contain NUL`);
|
|
23
|
+
}
|
|
24
|
+
if (value.includes(":")) {
|
|
25
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_SEPARATOR", component, `${component} must not contain ':'`);
|
|
26
|
+
}
|
|
27
|
+
if (value === "__net__") {
|
|
28
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_RESERVED", component, `${component} must not use reserved identifier '__net__'`);
|
|
29
|
+
}
|
|
30
|
+
const normalized = value.normalize("NFC");
|
|
31
|
+
if (Array.from(normalized).length > MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH) {
|
|
32
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_IDENTIFIER_TOO_LONG", component, `${component} must be at most ${MAX_CANONICAL_LOCK_IDENTIFIER_LENGTH} Unicode characters after NFC normalization`);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
function canonicalKey(prefix, components) {
|
|
37
|
+
const digest = createHash("sha256")
|
|
38
|
+
.update([prefix, ...components].join("\0"), "utf8")
|
|
39
|
+
.digest("base64url");
|
|
40
|
+
const key = `${prefix}:${digest}`;
|
|
41
|
+
if (key.length !== CANONICAL_RECORD_KEY_LENGTH ||
|
|
42
|
+
key.length > MAX_KINTONE_UNIQUE_KEY_LENGTH) {
|
|
43
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_LENGTH_INVARIANT", "generatedKey", "generated canonical record key violates its length contract");
|
|
44
|
+
}
|
|
45
|
+
return key;
|
|
46
|
+
}
|
|
47
|
+
export function runKey(profile, networkId, businessKey) {
|
|
48
|
+
return canonicalKey("R1", [
|
|
49
|
+
canonicalIdentifier(profile, "profile"),
|
|
50
|
+
canonicalIdentifier(networkId, "networkId"),
|
|
51
|
+
canonicalIdentifier(businessKey, "businessKey"),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
export function nodeStateKey(runId, nodeId) {
|
|
55
|
+
return canonicalKey("S1", [
|
|
56
|
+
canonicalIdentifier(runId, "runId"),
|
|
57
|
+
canonicalIdentifier(nodeId, "nodeId"),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
export function attemptKey(runId, nodeId, attemptNo) {
|
|
61
|
+
if (!Number.isSafeInteger(attemptNo) || attemptNo < 1) {
|
|
62
|
+
throw new CanonicalRecordKeyError("RECORD_KEY_ATTEMPT_NO_INVALID", "attemptNo", "attemptNo must be a positive safe integer");
|
|
63
|
+
}
|
|
64
|
+
return canonicalKey("A1", [
|
|
65
|
+
canonicalIdentifier(runId, "runId"),
|
|
66
|
+
canonicalIdentifier(nodeId, "nodeId"),
|
|
67
|
+
String(attemptNo),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=canonical-record-key.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ValidationResult } from "./network-definition.js";
|
|
2
|
+
export declare function loadNetworkDefinition(path: string): ValidationResult;
|
|
3
|
+
export declare function loadNetworkDefinitionSource(source: string): ValidationResult;
|
|
4
|
+
//# sourceMappingURL=load-network.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { validateNetworkDefinition } from "./validate-network.js";
|
|
4
|
+
export function loadNetworkDefinition(path) {
|
|
5
|
+
let source;
|
|
6
|
+
try {
|
|
7
|
+
source = readFileSync(path, "utf8");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
return {
|
|
11
|
+
errors: [
|
|
12
|
+
{
|
|
13
|
+
code: "NETWORK_FILE_UNREADABLE",
|
|
14
|
+
path: "$",
|
|
15
|
+
message: `cannot read network file: ${String(error)}`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return loadNetworkDefinitionSource(source);
|
|
21
|
+
}
|
|
22
|
+
export function loadNetworkDefinitionSource(source) {
|
|
23
|
+
const document = parseDocument(source, { uniqueKeys: true });
|
|
24
|
+
if (document.errors.length > 0) {
|
|
25
|
+
const errors = document.errors.map((error) => ({
|
|
26
|
+
code: "YAML_PARSE_ERROR",
|
|
27
|
+
path: "$",
|
|
28
|
+
message: `invalid YAML: ${error.message}`,
|
|
29
|
+
}));
|
|
30
|
+
return { errors };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return validateNetworkDefinition(document.toJS());
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
errors: [
|
|
38
|
+
{
|
|
39
|
+
code: "YAML_VALUE_INVALID",
|
|
40
|
+
path: "$",
|
|
41
|
+
message: `invalid YAML value: ${String(error)}`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=load-network.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const MAX_IDENTIFIER_LENGTH = 128;
|
|
2
|
+
export declare const MAX_BUSINESS_KEY_LENGTH = 128;
|
|
3
|
+
export type TriggerRule = "all_success" | "none_failed" | "all_done";
|
|
4
|
+
export interface ScheduledPeriodPolicy {
|
|
5
|
+
readonly type: "scheduled_period";
|
|
6
|
+
readonly period: "day" | "month";
|
|
7
|
+
readonly timezone: string;
|
|
8
|
+
readonly format: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExplicitPolicy {
|
|
11
|
+
readonly type: "explicit";
|
|
12
|
+
}
|
|
13
|
+
export type BusinessKeyPolicy = ScheduledPeriodPolicy | ExplicitPolicy;
|
|
14
|
+
export interface NetworkLockDefinition {
|
|
15
|
+
readonly lease_duration_sec: number;
|
|
16
|
+
readonly heartbeat_interval_sec: number;
|
|
17
|
+
}
|
|
18
|
+
export interface NetworkNodeDefinition {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly job_id: string;
|
|
21
|
+
readonly sql: string;
|
|
22
|
+
readonly depends_on: readonly string[];
|
|
23
|
+
readonly trigger_rule: TriggerRule;
|
|
24
|
+
readonly idempotent: boolean;
|
|
25
|
+
readonly inputs?: Readonly<Record<string, string>>;
|
|
26
|
+
readonly outputs?: Readonly<Record<string, string>>;
|
|
27
|
+
}
|
|
28
|
+
export interface NetworkDefinition {
|
|
29
|
+
readonly schema_version: 1;
|
|
30
|
+
readonly network_id: string;
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
readonly business_key_policy: BusinessKeyPolicy;
|
|
33
|
+
readonly max_active_runs: number;
|
|
34
|
+
readonly network_lock: NetworkLockDefinition;
|
|
35
|
+
readonly nodes: readonly NetworkNodeDefinition[];
|
|
36
|
+
}
|
|
37
|
+
export interface ValidationError {
|
|
38
|
+
readonly code: ValidationErrorCode;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}
|
|
42
|
+
export type ValidationErrorCode = "NETWORK_FILE_UNREADABLE" | "YAML_PARSE_ERROR" | "YAML_VALUE_INVALID" | "SCHEMA_INVALID" | "UNKNOWN_PROPERTY" | "TIMEZONE_INVALID" | "FORMAT_PLACEHOLDER_UNSUPPORTED" | "FORMAT_PLACEHOLDER_INVALID" | "FORMAT_PERIOD_MISMATCH" | "NETWORK_LOCK_HEARTBEAT_NOT_LESS_THAN_LEASE" | "NETWORK_LOCK_HEARTBEAT_EXCEEDS_ONE_THIRD" | "DUPLICATE_NODE_ID" | "INPUT_PATTERN_INVALID" | "OUTPUT_PATTERN_INVALID" | "TRIGGER_RULE_UNSUPPORTED" | "SELF_DEPENDENCY" | "DUPLICATE_DEPENDENCY" | "UNKNOWN_DEPENDENCY" | "CYCLE_DETECTED" | "SQL_FILE_UNREADABLE" | "SCHEDULED_FOR_REQUIRED" | "SCHEDULED_FOR_NOT_ALLOWED" | "SCHEDULED_FOR_INVALID" | "BUSINESS_KEY_INPUT_CONFLICT" | "BUSINESS_KEY_REQUIRED" | "BUSINESS_KEY_NOT_ALLOWED" | "BUSINESS_KEY_EMPTY" | "BUSINESS_KEY_TOO_LONG" | "BUSINESS_KEY_CONTROL_CHARACTER";
|
|
43
|
+
export interface ValidationResult {
|
|
44
|
+
readonly definition?: NetworkDefinition;
|
|
45
|
+
readonly errors: readonly ValidationError[];
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=network-definition.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// TODO(D-10): replace this conservative limit when the canonical key and
|
|
2
|
+
// backing-field limits are frozen.
|
|
3
|
+
export const MAX_IDENTIFIER_LENGTH = 128;
|
|
4
|
+
// TODO(D-10): replace this conservative limit when the backing business-key
|
|
5
|
+
// field and canonical lock-key limits are frozen.
|
|
6
|
+
export const MAX_BUSINESS_KEY_LENGTH = 128;
|
|
7
|
+
//# sourceMappingURL=network-definition.js.map
|