@forzalabs/remora 2.4.0 → 2.5.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/CHANGELOG.md +47 -0
- package/docs/Remora - Code Sets.md +268 -0
- package/docs/Remora - Consumers.md +2224 -0
- package/docs/Remora - Producers.md +1658 -0
- package/docs/Remora - Sources.md +1150 -0
- package/documentation/README.md +262 -130
- package/documentation/default_resources/mock_data.csv +2 -2
- package/index.js +2151 -1597
- package/json_schemas/consumer-schema.json +146 -1
- package/package.json +1 -1
- package/workers/ExecutorWorker.js +572 -47
|
@@ -8538,7 +8538,7 @@ var require_node2 = __commonJS({
|
|
|
8538
8538
|
var require_tail_file = __commonJS({
|
|
8539
8539
|
"../../node_modules/winston/lib/winston/tail-file.js"(exports2, module2) {
|
|
8540
8540
|
"use strict";
|
|
8541
|
-
var
|
|
8541
|
+
var fs33 = require("fs");
|
|
8542
8542
|
var { StringDecoder } = require("string_decoder");
|
|
8543
8543
|
var { Stream } = require_readable();
|
|
8544
8544
|
function noop() {
|
|
@@ -8559,7 +8559,7 @@ var require_tail_file = __commonJS({
|
|
|
8559
8559
|
stream.emit("end");
|
|
8560
8560
|
stream.emit("close");
|
|
8561
8561
|
};
|
|
8562
|
-
|
|
8562
|
+
fs33.open(options2.file, "a+", "0644", (err2, fd) => {
|
|
8563
8563
|
if (err2) {
|
|
8564
8564
|
if (!iter) {
|
|
8565
8565
|
stream.emit("error", err2);
|
|
@@ -8571,10 +8571,10 @@ var require_tail_file = __commonJS({
|
|
|
8571
8571
|
}
|
|
8572
8572
|
(function read() {
|
|
8573
8573
|
if (stream.destroyed) {
|
|
8574
|
-
|
|
8574
|
+
fs33.close(fd, noop);
|
|
8575
8575
|
return;
|
|
8576
8576
|
}
|
|
8577
|
-
return
|
|
8577
|
+
return fs33.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
|
|
8578
8578
|
if (error) {
|
|
8579
8579
|
if (!iter) {
|
|
8580
8580
|
stream.emit("error", error);
|
|
@@ -8633,7 +8633,7 @@ var require_tail_file = __commonJS({
|
|
|
8633
8633
|
var require_file = __commonJS({
|
|
8634
8634
|
"../../node_modules/winston/lib/winston/transports/file.js"(exports2, module2) {
|
|
8635
8635
|
"use strict";
|
|
8636
|
-
var
|
|
8636
|
+
var fs33 = require("fs");
|
|
8637
8637
|
var path22 = require("path");
|
|
8638
8638
|
var asyncSeries = require_series();
|
|
8639
8639
|
var zlib3 = require("zlib");
|
|
@@ -8838,7 +8838,7 @@ var require_file = __commonJS({
|
|
|
8838
8838
|
let buff = "";
|
|
8839
8839
|
let results = [];
|
|
8840
8840
|
let row = 0;
|
|
8841
|
-
const stream =
|
|
8841
|
+
const stream = fs33.createReadStream(file, {
|
|
8842
8842
|
encoding: "utf8"
|
|
8843
8843
|
});
|
|
8844
8844
|
stream.on("error", (err2) => {
|
|
@@ -8990,7 +8990,7 @@ var require_file = __commonJS({
|
|
|
8990
8990
|
stat(callback) {
|
|
8991
8991
|
const target = this._getFile();
|
|
8992
8992
|
const fullpath = path22.join(this.dirname, target);
|
|
8993
|
-
|
|
8993
|
+
fs33.stat(fullpath, (err2, stat) => {
|
|
8994
8994
|
if (err2 && err2.code === "ENOENT") {
|
|
8995
8995
|
debug("ENOENT\xA0ok", fullpath);
|
|
8996
8996
|
this.filename = target;
|
|
@@ -9095,7 +9095,7 @@ var require_file = __commonJS({
|
|
|
9095
9095
|
_createStream(source) {
|
|
9096
9096
|
const fullpath = path22.join(this.dirname, this.filename);
|
|
9097
9097
|
debug("create stream start", fullpath, this.options);
|
|
9098
|
-
const dest =
|
|
9098
|
+
const dest = fs33.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
|
|
9099
9099
|
debug("file open ok", fullpath);
|
|
9100
9100
|
this.emit("open", fullpath);
|
|
9101
9101
|
source.pipe(dest);
|
|
@@ -9174,7 +9174,7 @@ var require_file = __commonJS({
|
|
|
9174
9174
|
const isZipped = this.zippedArchive ? ".gz" : "";
|
|
9175
9175
|
const filePath = `${basename}${isOldest}${ext}${isZipped}`;
|
|
9176
9176
|
const target = path22.join(this.dirname, filePath);
|
|
9177
|
-
|
|
9177
|
+
fs33.unlink(target, callback);
|
|
9178
9178
|
}
|
|
9179
9179
|
/**
|
|
9180
9180
|
* Roll files forward based on integer, up to maxFiles. e.g. if base if
|
|
@@ -9197,17 +9197,17 @@ var require_file = __commonJS({
|
|
|
9197
9197
|
tasks.push(function(i6, cb) {
|
|
9198
9198
|
let fileName = `${basename}${i6 - 1}${ext}${isZipped}`;
|
|
9199
9199
|
const tmppath = path22.join(this.dirname, fileName);
|
|
9200
|
-
|
|
9200
|
+
fs33.exists(tmppath, (exists) => {
|
|
9201
9201
|
if (!exists) {
|
|
9202
9202
|
return cb(null);
|
|
9203
9203
|
}
|
|
9204
9204
|
fileName = `${basename}${i6}${ext}${isZipped}`;
|
|
9205
|
-
|
|
9205
|
+
fs33.rename(tmppath, path22.join(this.dirname, fileName), cb);
|
|
9206
9206
|
});
|
|
9207
9207
|
}.bind(this, x2));
|
|
9208
9208
|
}
|
|
9209
9209
|
asyncSeries(tasks, () => {
|
|
9210
|
-
|
|
9210
|
+
fs33.rename(
|
|
9211
9211
|
path22.join(this.dirname, `${basename}${ext}${isZipped}`),
|
|
9212
9212
|
path22.join(this.dirname, `${basename}1${ext}${isZipped}`),
|
|
9213
9213
|
callback
|
|
@@ -9223,22 +9223,22 @@ var require_file = __commonJS({
|
|
|
9223
9223
|
* @private
|
|
9224
9224
|
*/
|
|
9225
9225
|
_compressFile(src, dest, callback) {
|
|
9226
|
-
|
|
9226
|
+
fs33.access(src, fs33.F_OK, (err2) => {
|
|
9227
9227
|
if (err2) {
|
|
9228
9228
|
return callback();
|
|
9229
9229
|
}
|
|
9230
9230
|
var gzip = zlib3.createGzip();
|
|
9231
|
-
var inp =
|
|
9232
|
-
var out =
|
|
9231
|
+
var inp = fs33.createReadStream(src);
|
|
9232
|
+
var out = fs33.createWriteStream(dest);
|
|
9233
9233
|
out.on("finish", () => {
|
|
9234
|
-
|
|
9234
|
+
fs33.unlink(src, callback);
|
|
9235
9235
|
});
|
|
9236
9236
|
inp.pipe(gzip).pipe(out);
|
|
9237
9237
|
});
|
|
9238
9238
|
}
|
|
9239
9239
|
_createLogDirIfNotExist(dirPath) {
|
|
9240
|
-
if (!
|
|
9241
|
-
|
|
9240
|
+
if (!fs33.existsSync(dirPath)) {
|
|
9241
|
+
fs33.mkdirSync(dirPath, { recursive: true });
|
|
9242
9242
|
}
|
|
9243
9243
|
}
|
|
9244
9244
|
};
|
|
@@ -11809,7 +11809,7 @@ var CONSTANTS, Constants_default;
|
|
|
11809
11809
|
var init_Constants = __esm({
|
|
11810
11810
|
"../../packages/constants/src/Constants.ts"() {
|
|
11811
11811
|
CONSTANTS = {
|
|
11812
|
-
cliVersion: "2.
|
|
11812
|
+
cliVersion: "2.5.0",
|
|
11813
11813
|
backendVersion: 1,
|
|
11814
11814
|
backendPort: 5088,
|
|
11815
11815
|
workerVersion: 2,
|
|
@@ -11934,6 +11934,13 @@ var init_Constants = __esm({
|
|
|
11934
11934
|
* mode when its next line arrives — its byte counter lives in the bucket, not the stream.
|
|
11935
11935
|
*/
|
|
11936
11936
|
MAX_OPEN_SPLIT_STREAMS: 64,
|
|
11937
|
+
/**
|
|
11938
|
+
* Record layouts one FIXED output may write into a single file, which is also how many write
|
|
11939
|
+
* streams its block-ordering pass holds open. A copybook of this size is already unusual —
|
|
11940
|
+
* the NASCO POD feed writes five transaction types — and the bound stops a generated config
|
|
11941
|
+
* from opening a descriptor per distinct value of the wrong field.
|
|
11942
|
+
*/
|
|
11943
|
+
MAX_FIXED_LAYOUTS: 64,
|
|
11937
11944
|
/**
|
|
11938
11945
|
* Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
|
|
11939
11946
|
* export of it — `remora sample -s` and the preview's Data tab are both held to this.
|
|
@@ -13176,7 +13183,7 @@ var init_Validator = __esm({
|
|
|
13176
13183
|
errors.push(`Consumer "${consumer.name}" declares ${consumer.outputs.length} outputs (${consumer.outputs.map((x2) => x2.format).join(", ")}). A consumer writes exactly one output \u2014 only the first was ever written \u2014 so split it into one consumer per output.`);
|
|
13177
13184
|
for (const output of consumer.outputs) {
|
|
13178
13185
|
const format4 = output.format.toUpperCase();
|
|
13179
|
-
if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
13186
|
+
if (format4 === "CSV" || format4 === "FIXED" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
13180
13187
|
if (!output.exportDestination)
|
|
13181
13188
|
errors.push(`A static file output must have an export destination set (${format4})`);
|
|
13182
13189
|
else if (!Environment_default.getSource(output.exportDestination))
|
|
@@ -13186,6 +13193,7 @@ var init_Validator = __esm({
|
|
|
13186
13193
|
errors.push(...this.validateOmitNull(consumer));
|
|
13187
13194
|
errors.push(...this.validateHeader(consumer));
|
|
13188
13195
|
errors.push(...this.validateSplit(consumer));
|
|
13196
|
+
errors.push(...this.validateFixedOutput(consumer));
|
|
13189
13197
|
const dimensionFields = consumer.fields.filter((x2) => x2.key !== "*" && !x2.fixed && !x2.copyFrom && !x2.lineCount);
|
|
13190
13198
|
const dimensionKeys = dimensionFields.map((x2) => `${x2.from ?? "_default_"}::${x2.key}`);
|
|
13191
13199
|
const duplicateDimensionKeys = dimensionKeys.filter((k7, i6) => dimensionKeys.indexOf(k7) !== i6);
|
|
@@ -13254,7 +13262,7 @@ var init_Validator = __esm({
|
|
|
13254
13262
|
this.validateSplit = (consumer) => {
|
|
13255
13263
|
const errors = [];
|
|
13256
13264
|
const { split: split2 } = consumer;
|
|
13257
|
-
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13265
|
+
const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
|
|
13258
13266
|
if (!Algo_default.hasVal(split2)) return errors;
|
|
13259
13267
|
if (Algo_default.hasVal(consumer.maximumFileSize))
|
|
13260
13268
|
errors.push(`Consumer "${consumer.name}" sets both "maximumFileSize" and "split". They are the same cap: move the value to "split": { "maxFileSizeGb": ${consumer.maximumFileSize} } and remove "maximumFileSize".`);
|
|
@@ -13285,7 +13293,7 @@ var init_Validator = __esm({
|
|
|
13285
13293
|
*/
|
|
13286
13294
|
this.validateOmitNull = (consumer) => {
|
|
13287
13295
|
const errors = [];
|
|
13288
|
-
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13296
|
+
const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
|
|
13289
13297
|
for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
|
|
13290
13298
|
const format4 = output.format.toUpperCase();
|
|
13291
13299
|
if (positionalFormats.includes(format4))
|
|
@@ -13305,10 +13313,49 @@ var init_Validator = __esm({
|
|
|
13305
13313
|
for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
|
|
13306
13314
|
const format4 = output.format.toUpperCase();
|
|
13307
13315
|
if (!headerFormats.includes(format4))
|
|
13308
|
-
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output:
|
|
13316
|
+
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: ${this.headerlessReason(format4)}.`);
|
|
13309
13317
|
}
|
|
13310
13318
|
return errors;
|
|
13311
13319
|
};
|
|
13320
|
+
/** Why the format in hand has no header line, as the clause that finishes the sentence above. */
|
|
13321
|
+
this.headerlessReason = (format4) => {
|
|
13322
|
+
if (format4 === "FIXED") return "a positional line has no room for one, and a field name would not sit on the columns it describes";
|
|
13323
|
+
if (format4 === "PARQUET") return `a ${format4} file carries its own schema`;
|
|
13324
|
+
return `a ${format4} record already carries its own field names`;
|
|
13325
|
+
};
|
|
13326
|
+
/**
|
|
13327
|
+
* Checks a consumer's FIXED output from its configuration alone: the properties only a
|
|
13328
|
+
* positional file has, the ones it cannot have, and whether anything downstream tries to read
|
|
13329
|
+
* one back. The layout itself — widths, columns and record lengths — is resolved against the
|
|
13330
|
+
* dataset's columns, which the producers define, so it is checked by `compile` and again before
|
|
13331
|
+
* a run starts rather than here.
|
|
13332
|
+
*
|
|
13333
|
+
* A property declared on the wrong format is rejected rather than ignored, the way a `position`
|
|
13334
|
+
* on a non-FIXED producer is: silently dropping a rule the author wrote is how a file ships
|
|
13335
|
+
* looking almost right.
|
|
13336
|
+
*/
|
|
13337
|
+
this.validateFixedOutput = (consumer) => {
|
|
13338
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
13339
|
+
const errors = [];
|
|
13340
|
+
const output = consumer.outputs?.[0];
|
|
13341
|
+
if (!output) return errors;
|
|
13342
|
+
const format4 = output.format?.toUpperCase();
|
|
13343
|
+
const positioned = (consumer.fields ?? []).filter((x2) => x2.position).map((x2) => `"${x2.alias ?? x2.key}"`);
|
|
13344
|
+
if (format4 !== "FIXED") {
|
|
13345
|
+
const fixedOnly = ["recordLength", "defaults", "lineEnding", "layouts"];
|
|
13346
|
+
const declared = fixedOnly.filter((name) => Algo_default.hasVal(output[name]));
|
|
13347
|
+
if (declared.length > 0)
|
|
13348
|
+
errors.push(`The ${format4} output of consumer "${consumer.name}" declares ${declared.map((x2) => `"${x2}"`).join(", ")}, which only ${declared.length > 1 ? "apply" : "applies"} to a FIXED output, where ${declared.length > 1 ? "they are" : "it is"} ignored.`);
|
|
13349
|
+
if (positioned.length > 0)
|
|
13350
|
+
errors.push(`Consumer "${consumer.name}" declares "position" on field(s) ${positioned.join(", ")} but its output format is not "FIXED", where positions are ignored.`);
|
|
13351
|
+
return errors;
|
|
13352
|
+
}
|
|
13353
|
+
if (Algo_default.hasVal(output.delimiter))
|
|
13354
|
+
errors.push(`The FIXED output of consumer "${consumer.name}" declares a "delimiter". A positional line has none: its fields are found by the columns they occupy.`);
|
|
13355
|
+
if ((consumer.fields ?? []).some((x2) => x2.key === "*"))
|
|
13356
|
+
errors.push(`Consumer "${consumer.name}" selects fields with the wildcard key "*", which a FIXED output can't write: every field of a positional line declares its own "width", so the field list has to be explicit.`);
|
|
13357
|
+
return errors;
|
|
13358
|
+
};
|
|
13312
13359
|
/**
|
|
13313
13360
|
* Checks a field whose key selects columns by pattern ("addr*Street"). A pattern that matches
|
|
13314
13361
|
* nothing is always a mistake — a renamed dimension, or a producer that never had those columns —
|
|
@@ -14889,7 +14936,7 @@ A consumer takes the data from one or more producers and changes it's shape to t
|
|
|
14889
14936
|
# RULES
|
|
14890
14937
|
- If a field is not needed, do not add it e.g.
|
|
14891
14938
|
- Only import a producer once
|
|
14892
|
-
- Awlays include this exact property as the first -> $schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
|
|
14939
|
+
- Awlays include this exact property as the first -> "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
|
|
14893
14940
|
- Use "API" as the only valid output format.
|
|
14894
14941
|
- The "from" must contain only the name of the producer
|
|
14895
14942
|
|
|
@@ -33941,6 +33988,307 @@ var init_FixedWidthParser = __esm({
|
|
|
33941
33988
|
}
|
|
33942
33989
|
});
|
|
33943
33990
|
|
|
33991
|
+
// ../../packages/parsing/src/FixedWidthWriter.ts
|
|
33992
|
+
var BASE_FILL, TYPE_FILL, FixedWidthWriterClass, FixedWidthWriter, FixedWidthWriter_default;
|
|
33993
|
+
var init_FixedWidthWriter = __esm({
|
|
33994
|
+
"../../packages/parsing/src/FixedWidthWriter.ts"() {
|
|
33995
|
+
init_src();
|
|
33996
|
+
init_src3();
|
|
33997
|
+
BASE_FILL = { align: "left", pad: " ", nullFill: " ", overflow: "fail" };
|
|
33998
|
+
TYPE_FILL = {
|
|
33999
|
+
string: BASE_FILL,
|
|
34000
|
+
number: { ...BASE_FILL, align: "right", pad: "0" },
|
|
34001
|
+
datetime: BASE_FILL,
|
|
34002
|
+
boolean: BASE_FILL
|
|
34003
|
+
};
|
|
34004
|
+
FixedWidthWriterClass = class {
|
|
34005
|
+
constructor() {
|
|
34006
|
+
/**
|
|
34007
|
+
* Resolves one record layout for writing: every column's fill rules decided, its columns
|
|
34008
|
+
* computed, and the record length agreed with what the output declares.
|
|
34009
|
+
*
|
|
34010
|
+
* Called once per layout when an export starts, never per record. `columns` is the line as the
|
|
34011
|
+
* dataset schema has it — line order, visible fields only — and `positions` carries each one's
|
|
34012
|
+
* `position` block, keyed by the same name the column has.
|
|
34013
|
+
*
|
|
34014
|
+
* `context` is the phrase that names this layout in an error, e.g. `the FIXED output of consumer
|
|
34015
|
+
* "x"`, so a message reads as one sentence wherever the layout came from.
|
|
34016
|
+
*/
|
|
34017
|
+
this.compilePlan = (columns, positions, output, context = "the FIXED output") => {
|
|
34018
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34019
|
+
const selected = columns?.map((column, index) => ({ column, index }));
|
|
34020
|
+
return this.plan(selected, positions, output.defaults, output.recordLength, context);
|
|
34021
|
+
};
|
|
34022
|
+
/**
|
|
34023
|
+
* A FIXED output resolved for INSPECTION rather than for writing: every problem with it, and the
|
|
34024
|
+
* layouts it would write. What `compile` reports and prints.
|
|
34025
|
+
*
|
|
34026
|
+
* The rules are not restated here. The layouts are resolved by the same code the export runs,
|
|
34027
|
+
* and what that throws is reported rather than raised — so a layout `compile` accepts is a
|
|
34028
|
+
* layout the export can write. What review adds is that every field's own declaration is
|
|
34029
|
+
* checked first, across the whole output: an author with six fields missing a width sees six
|
|
34030
|
+
* lines from one `compile`, not one line from each of six.
|
|
34031
|
+
*
|
|
34032
|
+
* A field named by a layout that the dataset has no column for is left to the resolution below,
|
|
34033
|
+
* which names it as the layout's error rather than as a field with no width.
|
|
34034
|
+
*/
|
|
34035
|
+
this.review = (dsSchema, positions, output, consumerName) => {
|
|
34036
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
34037
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34038
|
+
const context = `the FIXED output${consumerName ? ` of consumer "${consumerName}"` : ""}`;
|
|
34039
|
+
const declared = this.declarationErrors(dsSchema, positions, output, context);
|
|
34040
|
+
if (declared.length > 0) return { errors: declared, layouts: [] };
|
|
34041
|
+
try {
|
|
34042
|
+
if (!output.layouts)
|
|
34043
|
+
return { errors: [], layouts: [{ value: null, plan: this.compilePlan(dsSchema.columns, positions, output, context) }] };
|
|
34044
|
+
const layouts = this.compileLayouts(dsSchema, positions, output, consumerName);
|
|
34045
|
+
return { errors: [], layouts: layouts.order.map((value) => ({ value, plan: layouts.plans.get(value) })) };
|
|
34046
|
+
} catch (e7) {
|
|
34047
|
+
return { errors: [this.reason(e7)], layouts: [] };
|
|
34048
|
+
}
|
|
34049
|
+
};
|
|
34050
|
+
/** Every field's own declaration, over every field the output writes, before any offset is computed. */
|
|
34051
|
+
this.declarationErrors = (dsSchema, positions, output, context) => {
|
|
34052
|
+
const written = output.layouts ? [...new Set((output.layouts.order ?? []).flatMap((value) => output.layouts.values?.[value]?.fields ?? []))] : dsSchema.columns.map((column) => column.name);
|
|
34053
|
+
const errors = [];
|
|
34054
|
+
for (const name of written) {
|
|
34055
|
+
const column = dsSchema.columns.find((x2) => x2.name === name);
|
|
34056
|
+
if (column) errors.push(...this.fieldErrors(column, positions?.[name], output.defaults, context));
|
|
34057
|
+
}
|
|
34058
|
+
return errors;
|
|
34059
|
+
};
|
|
34060
|
+
/**
|
|
34061
|
+
* What an affirm said, as a sentence a report can start a line with: without the prefix it
|
|
34062
|
+
* carries for a log, and with its first letter upper-cased — the messages are written to be
|
|
34063
|
+
* embedded ("... of the FIXED output of consumer x ..."), so some begin with that context
|
|
34064
|
+
* rather than with a capital.
|
|
34065
|
+
*/
|
|
34066
|
+
this.reason = (error) => {
|
|
34067
|
+
const prefix2 = "Affirm failed: ";
|
|
34068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34069
|
+
const sentence = message.startsWith(prefix2) ? message.slice(prefix2.length) : message;
|
|
34070
|
+
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
34071
|
+
};
|
|
34072
|
+
/**
|
|
34073
|
+
* The record layouts of a multi-layout FIXED output, compiled in `order` so the export writes
|
|
34074
|
+
* its blocks in the order the author declared.
|
|
34075
|
+
*
|
|
34076
|
+
* Each layout names a subset of the dataset's columns, in its own column order, so a field's
|
|
34077
|
+
* place on the written line has nothing to do with its place on the staged one — which is what
|
|
34078
|
+
* `IFixedWriteField.index` carries. The output's `defaults` stand under every layout's own.
|
|
34079
|
+
*/
|
|
34080
|
+
this.compileLayouts = (dsSchema, positions, output, consumerName) => {
|
|
34081
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
34082
|
+
Affirm_default(output?.layouts, "Invalid fixed-width layouts");
|
|
34083
|
+
const { discriminator, order, values } = output.layouts;
|
|
34084
|
+
const where = consumerName ? ` of consumer "${consumerName}"` : "";
|
|
34085
|
+
Affirm_default(discriminator, `The FIXED layouts${where} name no discriminator field`);
|
|
34086
|
+
Affirm_default.hasItems(order, `The FIXED layouts${where} declare no "order", which is the order their record blocks are written in`);
|
|
34087
|
+
Affirm_default.noDupes(order, `The FIXED layouts${where} name a discriminator value twice in "order"`);
|
|
34088
|
+
Affirm_default(
|
|
34089
|
+
order.length <= Constants_default.defaults.MAX_FIXED_LAYOUTS,
|
|
34090
|
+
`The FIXED layouts${where} declare ${order.length} record layouts, more than the ${Constants_default.defaults.MAX_FIXED_LAYOUTS} one output may write`
|
|
34091
|
+
);
|
|
34092
|
+
Affirm_default(
|
|
34093
|
+
output.recordLength === void 0 || output.recordLength === null,
|
|
34094
|
+
`The FIXED output${where} declares both "layouts" and a "recordLength". Each layout declares its own; the output has no single one.`
|
|
34095
|
+
);
|
|
34096
|
+
const index = dsSchema.columns.findIndex((column) => column.name === discriminator);
|
|
34097
|
+
Affirm_default(index >= 0, `The FIXED discriminator "${discriminator}"${where} is not one of the consumer's visible fields`);
|
|
34098
|
+
const plans = /* @__PURE__ */ new Map();
|
|
34099
|
+
for (const value of order)
|
|
34100
|
+
plans.set(value, this.layoutPlan(values?.[value], dsSchema, positions, output, value, consumerName));
|
|
34101
|
+
return { discriminator, index, order, plans };
|
|
34102
|
+
};
|
|
34103
|
+
/** One entry of the `layouts` map as a plan: its named fields, found on the staged line, in its order. */
|
|
34104
|
+
this.layoutPlan = (layout, dsSchema, positions, output, value, consumerName) => {
|
|
34105
|
+
const of = consumerName ? ` of consumer "${consumerName}"` : "";
|
|
34106
|
+
const where = `the FIXED layout "${value}"${of}`;
|
|
34107
|
+
Affirm_default(layout, `The FIXED "order"${of} names the discriminator value "${value}", which has no layout under "values"`);
|
|
34108
|
+
Affirm_default.hasItems(layout.fields, `${where} writes no field`);
|
|
34109
|
+
Affirm_default.noDupes(layout.fields, `${where} writes a field twice`);
|
|
34110
|
+
const selected = layout.fields.map((name) => {
|
|
34111
|
+
const index = dsSchema.columns.findIndex((column) => column.name === name);
|
|
34112
|
+
Affirm_default(index >= 0, `The field "${name}" of ${where} is not one of the consumer's visible fields`);
|
|
34113
|
+
return { column: dsSchema.columns[index], index };
|
|
34114
|
+
});
|
|
34115
|
+
return this.plan(selected, positions, this.mergeDefaults(output.defaults, layout.defaults), layout.recordLength, where);
|
|
34116
|
+
};
|
|
34117
|
+
/**
|
|
34118
|
+
* One record layout ready to write: every column's fill rules decided, its columns computed from
|
|
34119
|
+
* the widths before it, and the record length agreed with what the layout declares.
|
|
34120
|
+
*/
|
|
34121
|
+
this.plan = (selected, positions, defaults, recordLength, context) => {
|
|
34122
|
+
Affirm_default.hasItems(selected, `${context} has no field to write`);
|
|
34123
|
+
Affirm_default(positions, "Invalid fixed-width field positions");
|
|
34124
|
+
const fields = [];
|
|
34125
|
+
let start = 1;
|
|
34126
|
+
for (const { column, index } of selected) {
|
|
34127
|
+
const field = this.resolveField(column, positions[column.name], defaults, start, context);
|
|
34128
|
+
fields.push({ ...field, index });
|
|
34129
|
+
start = field.end + 1;
|
|
34130
|
+
}
|
|
34131
|
+
const composed = start - 1;
|
|
34132
|
+
Affirm_default(
|
|
34133
|
+
recordLength === void 0 || recordLength === null || recordLength === composed,
|
|
34134
|
+
`${context} declares a recordLength of ${recordLength}, but its ${fields.length} field widths sum to ${composed}. A field, a filler or a width is missing.`
|
|
34135
|
+
);
|
|
34136
|
+
return { fields, recordLength: composed };
|
|
34137
|
+
};
|
|
34138
|
+
/**
|
|
34139
|
+
* The layout's per-type fill rules over the output's, type by type — so an output states a rule
|
|
34140
|
+
* once for the whole file and a layout only names where it differs.
|
|
34141
|
+
*/
|
|
34142
|
+
this.mergeDefaults = (output, layout) => {
|
|
34143
|
+
if (!layout) return output;
|
|
34144
|
+
if (!output) return layout;
|
|
34145
|
+
const merged = { ...output };
|
|
34146
|
+
for (const type of Object.keys(layout))
|
|
34147
|
+
merged[type] = this.override(output[type] ?? {}, layout[type]);
|
|
34148
|
+
return merged;
|
|
34149
|
+
};
|
|
34150
|
+
/**
|
|
34151
|
+
* The layout a record is written with, by the value of the discriminator field on its staged
|
|
34152
|
+
* line. A value with no layout stops the run: it is a record the file has no block for, and
|
|
34153
|
+
* writing it under another layout would put its fields at the wrong columns.
|
|
34154
|
+
*/
|
|
34155
|
+
this.planFor = (values, layouts) => {
|
|
34156
|
+
const value = values?.[layouts.index];
|
|
34157
|
+
const plan = value === null || value === void 0 ? null : layouts.plans.get(String(value));
|
|
34158
|
+
Affirm_default(plan, `No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`);
|
|
34159
|
+
return plan;
|
|
34160
|
+
};
|
|
34161
|
+
/**
|
|
34162
|
+
* Everything wrong with the way ONE field is declared, as messages rather than as a throw: no
|
|
34163
|
+
* `position` at all, a width that is not a whole number of columns, a fill character that is not
|
|
34164
|
+
* one character.
|
|
34165
|
+
*
|
|
34166
|
+
* Not the field's place on the line — that is only meaningful once every field before it stands
|
|
34167
|
+
* on its own, so `resolveField` checks it where it computes it. This is the one statement of the
|
|
34168
|
+
* rules: the export affirms them, and `review` collects them for every field of an output so
|
|
34169
|
+
* `compile` reports six missing widths in one run rather than one per run.
|
|
34170
|
+
*/
|
|
34171
|
+
this.fieldErrors = (column, position, defaults, context) => {
|
|
34172
|
+
Affirm_default(column, "Invalid dataset column");
|
|
34173
|
+
const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
|
|
34174
|
+
if (!position)
|
|
34175
|
+
return [`Missing "position" on ${where}: every visible field of a FIXED output declares the width it occupies`];
|
|
34176
|
+
const errors = [];
|
|
34177
|
+
if (!Number.isInteger(position.width) || position.width < 1)
|
|
34178
|
+
errors.push(`"position.width" of ${where} must be a whole number of 1 or greater`);
|
|
34179
|
+
const fill2 = this.fillOf(column.type, position, defaults);
|
|
34180
|
+
if (fill2.pad.length !== 1)
|
|
34181
|
+
errors.push(`"position.pad" of ${where} must be a single character, not "${fill2.pad}"`);
|
|
34182
|
+
if (fill2.nullFill.length !== 1)
|
|
34183
|
+
errors.push(`"position.nullFill" of ${where} must be a single character, not "${fill2.nullFill}"`);
|
|
34184
|
+
return errors;
|
|
34185
|
+
};
|
|
34186
|
+
/**
|
|
34187
|
+
* One column with its fill rules resolved and its place on the line computed. The field's own
|
|
34188
|
+
* `position` wins over the output's `defaults` for its type, which wins over the type's own rule,
|
|
34189
|
+
* so a wide layout states a rule once and names only its exceptions.
|
|
34190
|
+
*/
|
|
34191
|
+
this.resolveField = (column, position, defaults, start, context) => {
|
|
34192
|
+
const declaration = this.fieldErrors(column, position, defaults, context);
|
|
34193
|
+
Affirm_default(declaration.length === 0, declaration.join(" "));
|
|
34194
|
+
const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
|
|
34195
|
+
const fill2 = this.fillOf(column.type, position, defaults);
|
|
34196
|
+
const end = start + position.width - 1;
|
|
34197
|
+
Affirm_default(
|
|
34198
|
+
position.start === void 0 || position.start === null || position.start === start,
|
|
34199
|
+
`"position.start" of ${where} says column ${position.start}, but the widths before it place the field at column ${start}. A field or a filler is missing, or two are out of order.`
|
|
34200
|
+
);
|
|
34201
|
+
return {
|
|
34202
|
+
name: column.name,
|
|
34203
|
+
width: position.width,
|
|
34204
|
+
start,
|
|
34205
|
+
end,
|
|
34206
|
+
align: fill2.align,
|
|
34207
|
+
pad: fill2.pad,
|
|
34208
|
+
nullText: fill2.nullFill.repeat(position.width),
|
|
34209
|
+
overflow: fill2.overflow
|
|
34210
|
+
};
|
|
34211
|
+
};
|
|
34212
|
+
/** The three fill sources folded into one, nearest declaration first. */
|
|
34213
|
+
this.fillOf = (type, position, defaults) => {
|
|
34214
|
+
const base = TYPE_FILL[type] ?? TYPE_FILL.string;
|
|
34215
|
+
return this.override(this.override(base, defaults?.[type]), position);
|
|
34216
|
+
};
|
|
34217
|
+
/** `fill` with whatever `override` actually declares. An absent rule leaves the one below it standing. */
|
|
34218
|
+
this.override = (fill2, override) => {
|
|
34219
|
+
if (!override) return fill2;
|
|
34220
|
+
const merged = { ...fill2 };
|
|
34221
|
+
if (override.align) merged.align = override.align;
|
|
34222
|
+
if (override.pad) merged.pad = override.pad;
|
|
34223
|
+
if (override.nullFill) merged.nullFill = override.nullFill;
|
|
34224
|
+
if (override.overflow) merged.overflow = override.overflow;
|
|
34225
|
+
return merged;
|
|
34226
|
+
};
|
|
34227
|
+
/**
|
|
34228
|
+
* One record as its positional line, with no delimiter, no quoting and no terminator. The values
|
|
34229
|
+
* are the record's fields as the STAGED line holds them, each plan field reading the one it was
|
|
34230
|
+
* compiled against; a value the array does not reach is written as a null, which is what a
|
|
34231
|
+
* staged line does when its last fields carry nothing.
|
|
34232
|
+
*/
|
|
34233
|
+
this.composeLine = (values, plan) => {
|
|
34234
|
+
Affirm_default(values, "Invalid record values");
|
|
34235
|
+
Affirm_default(plan, "Invalid fixed-width write plan");
|
|
34236
|
+
let line = "";
|
|
34237
|
+
for (const field of plan.fields)
|
|
34238
|
+
line += this.writeField(values[field.index], field);
|
|
34239
|
+
Affirm_default(line.length === plan.recordLength, `A fixed-width record composed to ${line.length} characters, not the declared recordLength of ${plan.recordLength}`);
|
|
34240
|
+
return line;
|
|
34241
|
+
};
|
|
34242
|
+
/**
|
|
34243
|
+
* One value as one field: its text padded out to the field's width on the side `align` names.
|
|
34244
|
+
*
|
|
34245
|
+
* A null is the full width in `nullFill`, and an EMPTY STRING is a null — the staged encoding
|
|
34246
|
+
* keeps the two apart and a positional line cannot. A value too long for its field stops the
|
|
34247
|
+
* run; see `overflowed`.
|
|
34248
|
+
*/
|
|
34249
|
+
this.writeField = (value, field) => {
|
|
34250
|
+
if (value === null || value === void 0) return field.nullText;
|
|
34251
|
+
const text = typeof value === "string" ? value : String(value);
|
|
34252
|
+
if (text.length === 0) return field.nullText;
|
|
34253
|
+
if (text.length > field.width) return this.overflowed(text, field);
|
|
34254
|
+
return field.align === "right" ? text.padStart(field.width, field.pad) : text.padEnd(field.width, field.pad);
|
|
34255
|
+
};
|
|
34256
|
+
/**
|
|
34257
|
+
* A value longer than its field. It fails the run by default: a truncated account number is a
|
|
34258
|
+
* plausible-looking account number, and silent corruption in a feed nobody re-reads is the one
|
|
34259
|
+
* failure mode a positional writer must not have.
|
|
34260
|
+
*
|
|
34261
|
+
* Where the layout genuinely specifies a cut, the end that survives follows `align`, which is
|
|
34262
|
+
* what a COBOL MOVE does: a left-aligned value keeps its head, a right-aligned one keeps its
|
|
34263
|
+
* low-order tail.
|
|
34264
|
+
*/
|
|
34265
|
+
this.overflowed = (text, field) => {
|
|
34266
|
+
Affirm_default(
|
|
34267
|
+
field.overflow === "truncate",
|
|
34268
|
+
`Value "${text}" is ${text.length} characters, longer than the ${field.width} of field "${field.name}" at columns ${field.start}-${field.end}. Widen the field, or set its "overflow" to "truncate" if the layout really specifies a cut.`
|
|
34269
|
+
);
|
|
34270
|
+
return field.align === "right" ? text.slice(text.length - field.width) : text.slice(0, field.width);
|
|
34271
|
+
};
|
|
34272
|
+
/**
|
|
34273
|
+
* What a record carries after its last field, BEFORE the newline the destination writes. Every
|
|
34274
|
+
* destination driver ends each transformed line with one `\n`, the last line included, so an
|
|
34275
|
+
* `lf` output needs nothing here and a `crlf` one needs the `\r` that turns that newline into a
|
|
34276
|
+
* CRLF — which is exactly what a CRLF file looks like to anything that splits on `\n`.
|
|
34277
|
+
*
|
|
34278
|
+
* A real positional feed picks one terminator and holds it: the NASCO POD files are CRLF on
|
|
34279
|
+
* every line including the last, and `lf` is the default everywhere else.
|
|
34280
|
+
*/
|
|
34281
|
+
this.lineSuffix = (output) => {
|
|
34282
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34283
|
+
return output.lineEnding === "crlf" ? "\r" : "";
|
|
34284
|
+
};
|
|
34285
|
+
}
|
|
34286
|
+
};
|
|
34287
|
+
FixedWidthWriter = new FixedWidthWriterClass();
|
|
34288
|
+
FixedWidthWriter_default = FixedWidthWriter;
|
|
34289
|
+
}
|
|
34290
|
+
});
|
|
34291
|
+
|
|
33944
34292
|
// ../../node_modules/pend/index.js
|
|
33945
34293
|
var require_pend = __commonJS({
|
|
33946
34294
|
"../../node_modules/pend/index.js"(exports2, module2) {
|
|
@@ -33999,7 +34347,7 @@ var require_pend = __commonJS({
|
|
|
33999
34347
|
// ../../node_modules/yauzl/fd-slicer.js
|
|
34000
34348
|
var require_fd_slicer = __commonJS({
|
|
34001
34349
|
"../../node_modules/yauzl/fd-slicer.js"(exports2) {
|
|
34002
|
-
var
|
|
34350
|
+
var fs33 = require("fs");
|
|
34003
34351
|
var util = require("util");
|
|
34004
34352
|
var stream = require("stream");
|
|
34005
34353
|
var Readable10 = stream.Readable;
|
|
@@ -34019,7 +34367,7 @@ var require_fd_slicer = __commonJS({
|
|
|
34019
34367
|
FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
|
|
34020
34368
|
var self2 = this;
|
|
34021
34369
|
self2.pend.go(function(cb) {
|
|
34022
|
-
|
|
34370
|
+
fs33.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
|
|
34023
34371
|
cb();
|
|
34024
34372
|
callback(err2, bytesRead, buffer2);
|
|
34025
34373
|
});
|
|
@@ -34036,7 +34384,7 @@ var require_fd_slicer = __commonJS({
|
|
|
34036
34384
|
self2.refCount -= 1;
|
|
34037
34385
|
if (self2.refCount < 0) throw new Error("invalid unref");
|
|
34038
34386
|
if (self2.refCount > 0) return;
|
|
34039
|
-
|
|
34387
|
+
fs33.close(self2.fd, onCloseDone);
|
|
34040
34388
|
function onCloseDone(err2) {
|
|
34041
34389
|
if (err2) {
|
|
34042
34390
|
self2.emit("error", err2);
|
|
@@ -34068,7 +34416,7 @@ var require_fd_slicer = __commonJS({
|
|
|
34068
34416
|
}
|
|
34069
34417
|
self2.context.pend.go(function(cb) {
|
|
34070
34418
|
var buffer = Buffer.allocUnsafe(toRead);
|
|
34071
|
-
|
|
34419
|
+
fs33.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
|
|
34072
34420
|
if (err2) {
|
|
34073
34421
|
self2.destroy(err2);
|
|
34074
34422
|
} else if (bytesRead === 0) {
|
|
@@ -34428,7 +34776,7 @@ var require_crc32 = __commonJS({
|
|
|
34428
34776
|
// ../../node_modules/yauzl/index.js
|
|
34429
34777
|
var require_yauzl = __commonJS({
|
|
34430
34778
|
"../../node_modules/yauzl/index.js"(exports2) {
|
|
34431
|
-
var
|
|
34779
|
+
var fs33 = require("fs");
|
|
34432
34780
|
var zlib3 = require("zlib");
|
|
34433
34781
|
var fd_slicer = require_fd_slicer();
|
|
34434
34782
|
var util = require("util");
|
|
@@ -34497,10 +34845,10 @@ var require_yauzl = __commonJS({
|
|
|
34497
34845
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34498
34846
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34499
34847
|
if (callback == null) callback = defaultCallback;
|
|
34500
|
-
|
|
34848
|
+
fs33.open(path22, "r", function(err2, fd) {
|
|
34501
34849
|
if (err2) return callback(err2);
|
|
34502
34850
|
fromFd(fd, options2, function(err3, zipfile) {
|
|
34503
|
-
if (err3)
|
|
34851
|
+
if (err3) fs33.close(fd, defaultCallback);
|
|
34504
34852
|
callback(err3, zipfile);
|
|
34505
34853
|
});
|
|
34506
34854
|
});
|
|
@@ -34517,7 +34865,7 @@ var require_yauzl = __commonJS({
|
|
|
34517
34865
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34518
34866
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34519
34867
|
if (callback == null) callback = defaultCallback;
|
|
34520
|
-
|
|
34868
|
+
fs33.fstat(fd, function(err2, stats) {
|
|
34521
34869
|
if (err2) return callback(err2);
|
|
34522
34870
|
var reader = new fd_slicer.FdSlicer(fd);
|
|
34523
34871
|
fromRandomAccessReader(reader, stats.size, options2, callback);
|
|
@@ -38598,6 +38946,7 @@ var init_src5 = __esm({
|
|
|
38598
38946
|
init_CSVParser();
|
|
38599
38947
|
init_DatasetCodec();
|
|
38600
38948
|
init_FixedWidthParser();
|
|
38949
|
+
init_FixedWidthWriter();
|
|
38601
38950
|
init_ParseCompression();
|
|
38602
38951
|
init_ParseHelper();
|
|
38603
38952
|
init_XLSParser();
|
|
@@ -46233,8 +46582,8 @@ var init_LocalSourceDriver = __esm({
|
|
|
46233
46582
|
Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
|
|
46234
46583
|
const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
|
|
46235
46584
|
const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
|
|
46236
|
-
const
|
|
46237
|
-
const results = await Promise.all(
|
|
46585
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
|
|
46586
|
+
const results = await Promise.all(promises3);
|
|
46238
46587
|
return results.flat();
|
|
46239
46588
|
} else {
|
|
46240
46589
|
return await this._get(request, "");
|
|
@@ -46254,8 +46603,8 @@ var init_LocalSourceDriver = __esm({
|
|
|
46254
46603
|
Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
|
|
46255
46604
|
const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
|
|
46256
46605
|
const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
|
|
46257
|
-
const
|
|
46258
|
-
const results = await Promise.all(
|
|
46606
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
|
|
46607
|
+
const results = await Promise.all(promises3);
|
|
46259
46608
|
return results.flat();
|
|
46260
46609
|
} else {
|
|
46261
46610
|
return await this._get(request, "");
|
|
@@ -46683,8 +47032,8 @@ var init_S3SourceDriver = __esm({
|
|
|
46683
47032
|
if (fileKey.includes("%")) {
|
|
46684
47033
|
const allFileKeys = await this.listFiles(fileKey);
|
|
46685
47034
|
Affirm_default(allFileKeys.length < 50, `Pattern ${fileKey} of producer requested to S3 matches more than 50 files (${allFileKeys.length}), this is more than the S3 allowed limit. Please refine your pattern, remove some files or use a separate bucket.`);
|
|
46686
|
-
const
|
|
46687
|
-
const results = await Promise.all(
|
|
47035
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
|
|
47036
|
+
const results = await Promise.all(promises3);
|
|
46688
47037
|
return results.flat();
|
|
46689
47038
|
} else {
|
|
46690
47039
|
return await this._get(request);
|
|
@@ -46697,8 +47046,8 @@ var init_S3SourceDriver = __esm({
|
|
|
46697
47046
|
const { fileKey } = request;
|
|
46698
47047
|
if (fileKey.includes("%")) {
|
|
46699
47048
|
const allFileKeys = await this.listFiles(fileKey);
|
|
46700
|
-
const
|
|
46701
|
-
const results = await Promise.all(
|
|
47049
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
|
|
47050
|
+
const results = await Promise.all(promises3);
|
|
46702
47051
|
return results.flat();
|
|
46703
47052
|
} else {
|
|
46704
47053
|
return await this._get(request);
|
|
@@ -56102,6 +56451,8 @@ var init_DatasetSchema = __esm({
|
|
|
56102
56451
|
return "JSON";
|
|
56103
56452
|
case "TXT":
|
|
56104
56453
|
return "CSV";
|
|
56454
|
+
case "FIXED":
|
|
56455
|
+
return "CSV";
|
|
56105
56456
|
case "CSV":
|
|
56106
56457
|
return "CSV";
|
|
56107
56458
|
default:
|
|
@@ -56181,6 +56532,112 @@ var init_DatasetSchema = __esm({
|
|
|
56181
56532
|
}
|
|
56182
56533
|
});
|
|
56183
56534
|
|
|
56535
|
+
// ../../packages/executors/src/FixedBlockOrder.ts
|
|
56536
|
+
var fs25, FixedBlockOrderClass, FixedBlockOrder, FixedBlockOrder_default;
|
|
56537
|
+
var init_FixedBlockOrder = __esm({
|
|
56538
|
+
"../../packages/executors/src/FixedBlockOrder.ts"() {
|
|
56539
|
+
fs25 = __toESM(require("fs"));
|
|
56540
|
+
init_src();
|
|
56541
|
+
init_src5();
|
|
56542
|
+
init_src2();
|
|
56543
|
+
FixedBlockOrderClass = class {
|
|
56544
|
+
constructor() {
|
|
56545
|
+
/** The file one block's records are collected in. Named by the value's place in `order`, not by
|
|
56546
|
+
* the value itself: a discriminator value is data and may hold anything a path may not. */
|
|
56547
|
+
this.blockPath = (sourcePath, position) => `${sourcePath}.block_${position}`;
|
|
56548
|
+
/**
|
|
56549
|
+
* Routes the staged dataset into one file per layout and concatenates them in `layouts.order`,
|
|
56550
|
+
* returning the path of the block-ordered dataset the export then transforms.
|
|
56551
|
+
*/
|
|
56552
|
+
this.reorder = async (request) => {
|
|
56553
|
+
Affirm_default(request, "Invalid block order request");
|
|
56554
|
+
Affirm_default(request.sourcePath, "Missing dataset to order into blocks");
|
|
56555
|
+
Affirm_default(request.layouts, "Invalid fixed-width layouts");
|
|
56556
|
+
Affirm_default.hasItems(request.layouts.order, "The fixed-width layouts declare no block order");
|
|
56557
|
+
const { sourcePath, layouts, consumerName } = request;
|
|
56558
|
+
const orderedPath = `${sourcePath}.blocks`;
|
|
56559
|
+
const counts = await this._route(request);
|
|
56560
|
+
await fs25.promises.unlink(sourcePath);
|
|
56561
|
+
await this._concat(sourcePath, layouts, orderedPath);
|
|
56562
|
+
Logger_default.info(`Consumer "${consumerName}" writes its FIXED record blocks as ${layouts.order.map((value, position) => `${value} x${counts[position]}`).join(", ")}`);
|
|
56563
|
+
return orderedPath;
|
|
56564
|
+
};
|
|
56565
|
+
/**
|
|
56566
|
+
* The routing pass: every line read once, appended to the file of the block its discriminator
|
|
56567
|
+
* names. One stream per layout, all open at once — `MAX_FIXED_LAYOUTS` is what bounds them, so
|
|
56568
|
+
* there is no eviction to do and a block's file is only ever written by its own stream.
|
|
56569
|
+
*
|
|
56570
|
+
* Returns the records each block took, in `order`, which is what the log line reports.
|
|
56571
|
+
*/
|
|
56572
|
+
this._route = async (request) => {
|
|
56573
|
+
const { sourcePath, layouts } = request;
|
|
56574
|
+
const streams = layouts.order.map((_3, position) => fs25.createWriteStream(this.blockPath(sourcePath, position)));
|
|
56575
|
+
const positions = new Map(layouts.order.map((value, position) => [value, position]));
|
|
56576
|
+
const counts = layouts.order.map(() => 0);
|
|
56577
|
+
const readStream = fs25.createReadStream(sourcePath);
|
|
56578
|
+
try {
|
|
56579
|
+
for await (const line of readLines(readStream)) {
|
|
56580
|
+
const position = this._positionOf(line, layouts, positions);
|
|
56581
|
+
counts[position]++;
|
|
56582
|
+
const stream = streams[position];
|
|
56583
|
+
if (!stream.write(line + "\n"))
|
|
56584
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
56585
|
+
}
|
|
56586
|
+
} finally {
|
|
56587
|
+
for (const stream of streams)
|
|
56588
|
+
await this._closeStream(stream);
|
|
56589
|
+
readStream.destroy();
|
|
56590
|
+
}
|
|
56591
|
+
return counts;
|
|
56592
|
+
};
|
|
56593
|
+
/** The block a staged line belongs to. A record no layout covers stops the run; see `planFor`. */
|
|
56594
|
+
this._positionOf = (line, layouts, positions) => {
|
|
56595
|
+
const value = DatasetCodec_default.decodeRow(line)[layouts.index];
|
|
56596
|
+
const position = value === null || value === void 0 ? void 0 : positions.get(String(value));
|
|
56597
|
+
Affirm_default(
|
|
56598
|
+
position !== void 0,
|
|
56599
|
+
`No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`
|
|
56600
|
+
);
|
|
56601
|
+
return position;
|
|
56602
|
+
};
|
|
56603
|
+
/** The blocks appended into one dataset, in `order`, each file removed as it is consumed. */
|
|
56604
|
+
this._concat = async (sourcePath, layouts, orderedPath) => {
|
|
56605
|
+
const writer = fs25.createWriteStream(orderedPath);
|
|
56606
|
+
try {
|
|
56607
|
+
for (let position = 0; position < layouts.order.length; position++) {
|
|
56608
|
+
const blockPath = this.blockPath(sourcePath, position);
|
|
56609
|
+
await this._append(blockPath, writer);
|
|
56610
|
+
await fs25.promises.unlink(blockPath);
|
|
56611
|
+
}
|
|
56612
|
+
} finally {
|
|
56613
|
+
await this._closeStream(writer);
|
|
56614
|
+
}
|
|
56615
|
+
};
|
|
56616
|
+
/** One block's bytes onto the end of the ordered dataset. Piped, so a block is never held in memory. */
|
|
56617
|
+
this._append = (blockPath, writer) => {
|
|
56618
|
+
return new Promise((resolve, reject) => {
|
|
56619
|
+
const reader = fs25.createReadStream(blockPath);
|
|
56620
|
+
reader.on("error", reject);
|
|
56621
|
+
reader.on("end", resolve);
|
|
56622
|
+
reader.pipe(writer, { end: false });
|
|
56623
|
+
});
|
|
56624
|
+
};
|
|
56625
|
+
/** Flushes and releases a stream's descriptor. Awaited: the read that follows must not race the flush. */
|
|
56626
|
+
this._closeStream = async (stream) => {
|
|
56627
|
+
if (!stream) return;
|
|
56628
|
+
stream.end();
|
|
56629
|
+
await new Promise((resolve, reject) => {
|
|
56630
|
+
stream.on("finish", resolve);
|
|
56631
|
+
stream.on("error", reject);
|
|
56632
|
+
});
|
|
56633
|
+
};
|
|
56634
|
+
}
|
|
56635
|
+
};
|
|
56636
|
+
FixedBlockOrder = new FixedBlockOrderClass();
|
|
56637
|
+
FixedBlockOrder_default = FixedBlockOrder;
|
|
56638
|
+
}
|
|
56639
|
+
});
|
|
56640
|
+
|
|
56184
56641
|
// ../../packages/executors/src/OutputExecutor.ts
|
|
56185
56642
|
var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
|
|
56186
56643
|
var init_OutputExecutor = __esm({
|
|
@@ -56193,6 +56650,7 @@ var init_OutputExecutor = __esm({
|
|
|
56193
56650
|
init_src5();
|
|
56194
56651
|
init_src2();
|
|
56195
56652
|
init_DatasetSchema();
|
|
56653
|
+
init_FixedBlockOrder();
|
|
56196
56654
|
OutputExecutorClass = class {
|
|
56197
56655
|
constructor() {
|
|
56198
56656
|
/**
|
|
@@ -56283,6 +56741,8 @@ var init_OutputExecutor = __esm({
|
|
|
56283
56741
|
const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
|
|
56284
56742
|
Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
|
|
56285
56743
|
const header = this._composeHeader(output, dsSchema);
|
|
56744
|
+
const fixedLayouts = output.format === "FIXED" && output.layouts ? FixedWidthWriter_default.compileLayouts(dsSchema, this._fixedPositions(consumer), output, consumer.name) : null;
|
|
56745
|
+
const fixedLine = output.format === "FIXED" ? this._fixedLineWriter(consumer, dsSchema, output, fixedLayouts) : null;
|
|
56286
56746
|
const results = [];
|
|
56287
56747
|
for (const { path: startingPath, label } of splitFiles) {
|
|
56288
56748
|
const destinationPath = this.getCompletedPath(destinationName, label);
|
|
@@ -56303,6 +56763,11 @@ var init_OutputExecutor = __esm({
|
|
|
56303
56763
|
);
|
|
56304
56764
|
break;
|
|
56305
56765
|
}
|
|
56766
|
+
case "FIXED": {
|
|
56767
|
+
const fixedPath = fixedLayouts ? await FixedBlockOrder_default.reorder({ sourcePath: startingPath, layouts: fixedLayouts, consumerName: consumer.name }) : startingPath;
|
|
56768
|
+
results.push(await driver.transformAndMove(fixedPath, fixedLine, destinationPath));
|
|
56769
|
+
break;
|
|
56770
|
+
}
|
|
56306
56771
|
case "PARQUET":
|
|
56307
56772
|
results.push(await this._exportParquet({
|
|
56308
56773
|
sourcePath: startingPath,
|
|
@@ -56318,6 +56783,60 @@ var init_OutputExecutor = __esm({
|
|
|
56318
56783
|
}
|
|
56319
56784
|
return this._unifingResult(results);
|
|
56320
56785
|
};
|
|
56786
|
+
/**
|
|
56787
|
+
* A consumer's FIXED output resolved without running it: the layouts the export would compile,
|
|
56788
|
+
* and every problem with them. `compile` prints what it resolves and fails on what it reports,
|
|
56789
|
+
* and a run resolves it before reading a producer — so a width that does not add up is caught
|
|
56790
|
+
* where it was written rather than after the whole dataset has been built.
|
|
56791
|
+
*
|
|
56792
|
+
* Null for a consumer that writes anything else: there is no layout to resolve.
|
|
56793
|
+
*/
|
|
56794
|
+
this.reviewFixedOutput = (consumer, dsSchema) => {
|
|
56795
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56796
|
+
const output = consumer.outputs?.[0];
|
|
56797
|
+
if (output?.format !== "FIXED") return null;
|
|
56798
|
+
const schema = dsSchema ?? DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
|
|
56799
|
+
return FixedWidthWriter_default.review(schema, this._fixedPositions(consumer), output, consumer.name);
|
|
56800
|
+
};
|
|
56801
|
+
/**
|
|
56802
|
+
* The line function a FIXED export hands its destination driver: one staged record in, the
|
|
56803
|
+
* positional line it becomes out. The layout is resolved here, once, and closed over — so the
|
|
56804
|
+
* per-record cost is a decode of the staged line and a pad per field, with no config lookup.
|
|
56805
|
+
*
|
|
56806
|
+
* With `layouts` declared the record's own discriminator picks its plan, which is the only
|
|
56807
|
+
* per-record branch on the path; the records are already grouped into their blocks by then.
|
|
56808
|
+
*
|
|
56809
|
+
* No header is passed alongside it: a positional file has no room for one, and the field names
|
|
56810
|
+
* would not sit on the columns they describe.
|
|
56811
|
+
*/
|
|
56812
|
+
this._fixedLineWriter = (consumer, dsSchema, output, layouts) => {
|
|
56813
|
+
const suffix = FixedWidthWriter_default.lineSuffix(output);
|
|
56814
|
+
if (layouts) {
|
|
56815
|
+
for (const value of layouts.order)
|
|
56816
|
+
Logger_default.info(`Consumer "${consumer.name}" writes its FIXED "${value}" record as ${layouts.plans.get(value).recordLength} characters over ${layouts.plans.get(value).fields.length} field(s)`);
|
|
56817
|
+
return (line) => {
|
|
56818
|
+
const values = DatasetCodec_default.decodeRow(line);
|
|
56819
|
+
return FixedWidthWriter_default.composeLine(values, FixedWidthWriter_default.planFor(values, layouts)) + suffix;
|
|
56820
|
+
};
|
|
56821
|
+
}
|
|
56822
|
+
const plan = FixedWidthWriter_default.compilePlan(dsSchema.columns, this._fixedPositions(consumer), output, `the FIXED output of consumer "${consumer.name}"`);
|
|
56823
|
+
Logger_default.info(`Consumer "${consumer.name}" writes a FIXED record of ${plan.recordLength} characters over ${plan.fields.length} field(s)`);
|
|
56824
|
+
return (line) => FixedWidthWriter_default.composeLine(DatasetCodec_default.decodeRow(line), plan) + suffix;
|
|
56825
|
+
};
|
|
56826
|
+
/**
|
|
56827
|
+
* Every visible field's `position` block, keyed by the name its column carries on the staged
|
|
56828
|
+
* line. Read from the EXPANDED fields rather than the consumer's own, so a glob key brings the
|
|
56829
|
+
* `position` it was declared with to each of the columns it stands for, and a field the dataset
|
|
56830
|
+
* has no `position` for is left absent for `compilePlan` to name.
|
|
56831
|
+
*/
|
|
56832
|
+
this._fixedPositions = (consumer) => {
|
|
56833
|
+
const positions = {};
|
|
56834
|
+
for (const field of ConsumerManager_default.getExpandedFields(consumer)) {
|
|
56835
|
+
if (field.cField.hidden) continue;
|
|
56836
|
+
positions[field.finalKey] = field.cField.position;
|
|
56837
|
+
}
|
|
56838
|
+
return positions;
|
|
56839
|
+
};
|
|
56321
56840
|
/**
|
|
56322
56841
|
* Ships one split file as parquet. A parquet file is a columnar container whose footer can only
|
|
56323
56842
|
* be written once the last row group is encoded, so there is no per-line mapping to hand a
|
|
@@ -56355,8 +56874,9 @@ var init_OutputExecutor = __esm({
|
|
|
56355
56874
|
/**
|
|
56356
56875
|
* The header line of a positional export: the visible field names, in column order, quoted and
|
|
56357
56876
|
* delimited exactly as the rows under them — so the header parses with the same reader the data
|
|
56358
|
-
* does. Null for a JSON/API output, where a record already carries its own field names,
|
|
56359
|
-
*
|
|
56877
|
+
* does. Null for a JSON/API output, where a record already carries its own field names, for a
|
|
56878
|
+
* FIXED one, where a name does not fit the column it describes and the receiver reads by
|
|
56879
|
+
* position anyway, and for an output that turns the header off.
|
|
56360
56880
|
*
|
|
56361
56881
|
* The names come from the dataset's own schema, so a `pivot` is described by the header.
|
|
56362
56882
|
*/
|
|
@@ -57010,10 +57530,10 @@ var init_ExecutorPerformance = __esm({
|
|
|
57010
57530
|
});
|
|
57011
57531
|
|
|
57012
57532
|
// ../../packages/executors/src/ExecutorWriter.ts
|
|
57013
|
-
var
|
|
57533
|
+
var fs28, ExecutorWriter, ExecutorWriter_default;
|
|
57014
57534
|
var init_ExecutorWriter = __esm({
|
|
57015
57535
|
"../../packages/executors/src/ExecutorWriter.ts"() {
|
|
57016
|
-
|
|
57536
|
+
fs28 = __toESM(require("fs"));
|
|
57017
57537
|
init_src();
|
|
57018
57538
|
init_src3();
|
|
57019
57539
|
init_src10();
|
|
@@ -57065,7 +57585,7 @@ var init_ExecutorWriter = __esm({
|
|
|
57065
57585
|
await this._route(pass, dsSchema);
|
|
57066
57586
|
if (pass.buckets.size === 0)
|
|
57067
57587
|
return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
|
|
57068
|
-
await
|
|
57588
|
+
await fs28.promises.unlink(sourcePath);
|
|
57069
57589
|
return this._listFiles(pass);
|
|
57070
57590
|
};
|
|
57071
57591
|
/**
|
|
@@ -57073,7 +57593,7 @@ var init_ExecutorWriter = __esm({
|
|
|
57073
57593
|
*/
|
|
57074
57594
|
this._route = async (pass, dsSchema) => {
|
|
57075
57595
|
const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
|
|
57076
|
-
const readStream =
|
|
57596
|
+
const readStream = fs28.createReadStream(pass.sourcePath);
|
|
57077
57597
|
try {
|
|
57078
57598
|
for await (const line of readLines(readStream)) {
|
|
57079
57599
|
const bucket = this._bucketFor(line, pass, parseLine);
|
|
@@ -57177,7 +57697,7 @@ var init_ExecutorWriter = __esm({
|
|
|
57177
57697
|
}
|
|
57178
57698
|
if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
|
|
57179
57699
|
await this._closeStream(pass.openStreams.shift());
|
|
57180
|
-
bucket.stream =
|
|
57700
|
+
bucket.stream = fs28.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
|
|
57181
57701
|
pass.openStreams.push(bucket);
|
|
57182
57702
|
return bucket.stream;
|
|
57183
57703
|
};
|
|
@@ -57762,6 +58282,11 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57762
58282
|
const start = performance.now();
|
|
57763
58283
|
const executorResults = [];
|
|
57764
58284
|
Logger_default.info(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length}${OrchestratorHelper_default.formatRequestOptions(options2)}) | ${OrchestratorHelper_default.formatMemoryUsage()}`);
|
|
58285
|
+
const fixedReview = request.skipExport ? null : OutputExecutor_default.reviewFixedOutput(consumer, dsSchema);
|
|
58286
|
+
Affirm_default(
|
|
58287
|
+
!fixedReview || fixedReview.errors.length === 0,
|
|
58288
|
+
`Consumer "${consumer.name}" cannot write its FIXED output: ${fixedReview?.errors.join(" ")}`
|
|
58289
|
+
);
|
|
57765
58290
|
const lookups = await LookupResolver_default.materialize(consumer, expandedFields, scope, request);
|
|
57766
58291
|
const consumerInputs = await ConsumerInputResolver_default.materialize(consumer, scope, request);
|
|
57767
58292
|
let counter = performance.now();
|