@forzalabs/remora 1.1.11 → 1.1.13

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.
@@ -1,53 +1,17 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
3
- "name": "<consumer name>",
4
- "description": "<consumer description>",
5
- "producers": [
6
- {
7
- "name": "<primary producer name>"
8
- },
9
- {
10
- "name": "<secondary producer name>",
11
- "joins": [
12
- {
13
- "otherName": "<primary producer name>",
14
- "relationship": "one-to-many",
15
- "sql": "${P.id} = ${<primary producer name>.fk_id}"
16
- }
17
- ]
18
- }
19
- ],
20
- "fields": [
21
- { "key": "<producer field name>", "from": "<producer name>" },
22
- { "key": "<original field name>", "from": "<producer name>", "alias": "<new field name>" },
23
- { "key": "<secondary producer field name>", "from": "<secondary producer name>" },
24
- { "key": "<another field name>", "from": "<producer name>" }
25
- ],
26
- "filters": [
27
- {
28
- "sql": "<filter condition>"
29
- }
30
- ],
31
- "outputs": [
32
- { "format": "API" },
33
- {
34
- "format": "JSON",
35
- "exportDestination": "<export destination>"
36
- },
37
- {
38
- "format": "CSV",
39
- "exportDestination": "<export destination>",
40
- "trigger": {
41
- "type": "CRON",
42
- "value": "0 0 * * *"
43
- }
44
- }
45
- ],
46
- "metadata": {
47
- "<metadata tag key>": "<metadata tag value>",
48
- "owner_email": "<owner email>"
49
- },
50
- "project": "<project name>",
51
- "schema": "<schema name>",
52
- "_version": 1
53
- }
2
+ "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
3
+ "name": "c_default",
4
+ "producers": [
5
+ { "name": "p_default" }
6
+ ],
7
+ "fields": [
8
+ { "key": "id" },
9
+ { "key": "name" },
10
+ { "key": "age" },
11
+ { "key": "country" },
12
+ { "key": "spend" }
13
+ ],
14
+ "outputs": [
15
+ { "format": "JSON", "exportDestination": "s_default" }
16
+ ]
17
+ }
@@ -0,0 +1,6 @@
1
+ id,name,age,country,spend
2
+ USR-00001,Alice,28,Italy,0,48
3
+ USR-00002,Bob,35,Portugal,191.75
4
+ USR-00003,Carla,36,Netherlands,23.5
5
+ USR-00004,Diego,54,France,35.25
6
+ USR-00005,Elisa,62,Germany,47,52
@@ -1,33 +1,16 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/producer-schema.json",
3
- "name": "<producer name>",
4
- "description": "<producer description>",
5
- "source": "<source name>",
6
- "dimensions": [
7
- {
8
- "name": "<primary key field name>",
9
- "type": "<number | string | datetime>",
10
- "pk": true,
11
- "description": "<field description>"
12
- },
13
- {
14
- "name": "<sensitive field name>",
15
- "type": "<number | string | datetime>",
16
- "classification": ["<'PHI' | 'PII' | 'GDPR'>"],
17
- "mask": "<hash | mask | crypt>",
18
- "description": "<field description>"
19
- }
20
- ],
21
- "measures": [
22
- {
23
- "name": "<calculated measure name>",
24
- "description": "<measure description>",
25
- "sql": "<sql expression>"
26
- }
27
- ],
28
- "settings": {
29
- "sqlTable": "<source table name>",
30
- "direct": true
31
- },
32
- "_version": 1
33
- }
2
+ "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/producer-schema.json",
3
+ "name": "p_default",
4
+ "source": "s_default",
5
+ "dimensions": [
6
+ { "name": "id", "type": "string" },
7
+ { "name": "name", "type": "string" },
8
+ { "name": "age", "type": "number" },
9
+ { "name": "country", "type": "string" },
10
+ { "name": "spend", "type": "number" }
11
+ ],
12
+ "settings": {
13
+ "fileType": "CSV",
14
+ "fileKey": "mock_data.csv"
15
+ }
16
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/project-schema.json",
3
- "name": "PROJECT_NAME_EXAMPLE",
3
+ "name": "Default Remora Project",
4
4
  "version": "1.0.0",
5
- "description": "DESCRIPTION_OF_THIS_PROJECT",
5
+ "description": "This is the standard Remora project created when you run init.",
6
6
  "consumers": ["/consumers"],
7
7
  "producers": ["/producers"],
8
8
  "sources": ["/sources"],
@@ -1,16 +1,9 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/source-schema.json",
3
- "name": "<source name>",
4
- "description": "<source description>",
5
- "_version": 1,
6
- "authentication": {
7
- "method": "<iam | username-password | access-secret-key | arn | implicit>",
8
- "schema": "<database_schema_name - db schema>",
9
- "accessKey": "<access key var from env>",
10
- "secretKey": "<secret key var from env>",
11
- "region": "<aws region>",
12
- "database": "<db name>",
13
- "workgroup": "<db workgroup>"
14
- },
15
- "engine": "<aws-redshift | aws-dynamodb | aws-s3 | postgres | local>"
2
+ "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/source-schema.json",
3
+ "name": "s_default",
4
+ "authentication": {
5
+ "method": "implicit",
6
+ "path": "./default_remora_data"
7
+ },
8
+ "engine": "local"
16
9
  }
package/index.js CHANGED
@@ -13228,6 +13228,8 @@ var FileLogServiceClass = class {
13228
13228
  constructor() {
13229
13229
  this._enabled = false;
13230
13230
  this.enable = (folder = "logs", file = "remora.log") => {
13231
+ this._folder = folder;
13232
+ this._file = file;
13231
13233
  this._logger = import_winston.default.createLogger({
13232
13234
  level: "debug",
13233
13235
  format: import_winston.default.format.combine(
@@ -13260,6 +13262,23 @@ ${stack}` : base;
13260
13262
  });
13261
13263
  };
13262
13264
  this.flush = () => {
13265
+ if (!this._enabled || !this._logger) return Promise.resolve();
13266
+ return new Promise((resolve) => {
13267
+ let pending = this._logger.transports.length;
13268
+ if (pending === 0) return resolve();
13269
+ for (const transport of this._logger.transports) {
13270
+ if (typeof transport.on === "function") {
13271
+ transport.once("logged", () => {
13272
+ if (--pending === 0) resolve();
13273
+ });
13274
+ this._logger.info("");
13275
+ } else {
13276
+ if (--pending === 0) resolve();
13277
+ }
13278
+ }
13279
+ });
13280
+ };
13281
+ this.close = () => {
13263
13282
  if (!this._enabled || !this._logger) return Promise.resolve();
13264
13283
  return new Promise((resolve) => {
13265
13284
  this._logger.on("finish", resolve);
@@ -13339,6 +13358,7 @@ var Logger = class {
13339
13358
  FileLogService_default.write("INFO", String(message));
13340
13359
  };
13341
13360
  this.flush = () => FileLogService_default.flush();
13361
+ this.close = () => FileLogService_default.close();
13342
13362
  this.error = (error) => {
13343
13363
  let message;
13344
13364
  let stack;
@@ -13480,7 +13500,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
13480
13500
 
13481
13501
  // ../../packages/constants/src/Constants.ts
13482
13502
  var CONSTANTS = {
13483
- cliVersion: "1.1.11",
13503
+ cliVersion: "1.1.13",
13484
13504
  backendVersion: 1,
13485
13505
  backendPort: 5088,
13486
13506
  workerVersion: 2,
@@ -13652,6 +13672,42 @@ var SchemaValidatorClass = class {
13652
13672
  var SchemaValidator = new SchemaValidatorClass();
13653
13673
  var SchemaValidator_default = SchemaValidator;
13654
13674
 
13675
+ // ../../packages/common/src/ResourcesUtils.ts
13676
+ var ResourcesUtilsClass = class {
13677
+ constructor() {
13678
+ this.getAvailableColumns = (consumer) => {
13679
+ return consumer.producers.flatMap((cProd) => {
13680
+ const producer = Environment_default.getProducer(cProd.name);
13681
+ if (!producer) {
13682
+ const subConsumer = Environment_default.getConsumer(cProd.name);
13683
+ Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
13684
+ return this.getAvailableColumns(subConsumer);
13685
+ } else {
13686
+ const dims = producer.dimensions.map((x) => ({
13687
+ consumerAlias: null,
13688
+ consumerKey: null,
13689
+ nameInProducer: x.name,
13690
+ aliasInProducer: x.alias,
13691
+ dimension: x,
13692
+ owner: cProd.name
13693
+ }));
13694
+ const meas = producer.measures?.map((x) => ({
13695
+ consumerAlias: null,
13696
+ consumerKey: null,
13697
+ nameInProducer: x.name,
13698
+ aliasInProducer: x.name,
13699
+ measure: x,
13700
+ owner: cProd.name
13701
+ })) ?? [];
13702
+ return [...dims, ...meas];
13703
+ }
13704
+ });
13705
+ };
13706
+ }
13707
+ };
13708
+ var ResourcesUtils = new ResourcesUtilsClass();
13709
+ var ResourcesUtils_default = ResourcesUtils;
13710
+
13655
13711
  // ../../packages/common/src/validation/Validator.ts
13656
13712
  var ValidatorClass = class {
13657
13713
  constructor() {
@@ -13762,6 +13818,38 @@ var ValidatorClass = class {
13762
13818
  const uniqNames = Algo_default.uniqBy(sources, "name");
13763
13819
  if (uniqNames.length !== 1)
13764
13820
  errors.push(`Producers with different sources are used in the consumer "${consumer.name}" (${uniqNames.join(", ")})`);
13821
+ const availableColumns = ResourcesUtils_default.getAvailableColumns(consumer);
13822
+ const availableFieldsByProducer = /* @__PURE__ */ new Map();
13823
+ for (const col of availableColumns) {
13824
+ const existing = availableFieldsByProducer.get(col.owner) ?? [];
13825
+ if (!existing.includes(col.nameInProducer))
13826
+ existing.push(col.nameInProducer);
13827
+ availableFieldsByProducer.set(col.owner, existing);
13828
+ }
13829
+ const allAvailableFields = [...availableFieldsByProducer.values()].flat();
13830
+ for (const field of consumer.fields) {
13831
+ if (field.key === "*" || field.fixed || field.copyFrom) continue;
13832
+ if (field.from) {
13833
+ const producerFields = availableFieldsByProducer.get(field.from);
13834
+ if (producerFields && !producerFields.includes(field.key))
13835
+ errors.push(`Field "${field.key}" (from: "${field.from}") is not found in producer "${field.from}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
13836
+ } else if (consumer.producers.length === 1) {
13837
+ const producerFields = availableFieldsByProducer.get(consumer.producers[0].name);
13838
+ if (producerFields && !producerFields.includes(field.key))
13839
+ errors.push(`Field "${field.key}" is not found in producer "${consumer.producers[0].name}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
13840
+ } else {
13841
+ if (allAvailableFields.length > 0 && !allAvailableFields.includes(field.key))
13842
+ errors.push(`Field "${field.key}" is not found in any of the producers of consumer "${consumer.name}".`);
13843
+ }
13844
+ }
13845
+ for (let i = 0; i < consumer.fields.length; i++) {
13846
+ const field = consumer.fields[i];
13847
+ if (!field.copyFrom) continue;
13848
+ const precedingFields = consumer.fields.slice(0, i);
13849
+ const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
13850
+ if (!found)
13851
+ errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
13852
+ }
13765
13853
  if (consumer.filters && consumer.filters.length > 0) {
13766
13854
  if (consumer.filters.some((x) => x.sql && x.rule))
13767
13855
  errors.push(`A single consumer can't have both filters based on SQL and filters based on rules.`);
@@ -13834,6 +13922,20 @@ var ValidatorClass = class {
13834
13922
  if (consumer.options) {
13835
13923
  if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
13836
13924
  errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
13925
+ if (Algo_default.hasVal(consumer.options.distinctOn)) {
13926
+ const { distinctOn } = consumer.options;
13927
+ const hasWildcard = consumer.fields.some((x) => x.key === "*");
13928
+ const consumerFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
13929
+ if (!hasWildcard) {
13930
+ const missingKeys = distinctOn.keys.filter((k) => !consumerFieldKeys.includes(k));
13931
+ if (missingKeys.length > 0)
13932
+ errors.push(`distinctOn references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13933
+ }
13934
+ if (distinctOn.resolution?.orderBy && !hasWildcard) {
13935
+ if (!consumerFieldKeys.includes(distinctOn.resolution.orderBy))
13936
+ errors.push(`distinctOn resolution orderBy field "${distinctOn.resolution.orderBy}" is not present in the consumer "${consumer.name}".`);
13937
+ }
13938
+ }
13837
13939
  if (Algo_default.hasVal(consumer.options.pivot)) {
13838
13940
  if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
13839
13941
  errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
@@ -16071,7 +16173,7 @@ var DOCUMENTATION_DIR = resolveAssetDir("documentation");
16071
16173
 
16072
16174
  // src/actions/init.ts
16073
16175
  var init = async () => {
16074
- console.log(import_chalk5.default.blue.bold("\u{1F4E6} Initializing your application..."));
16176
+ console.log(import_chalk5.default.blue.bold("\u{1F4E6} Initializing your Remora app (make sure you have set your REMORA_LICENCE_KEY in the environment variables)..."));
16075
16177
  try {
16076
16178
  const spinner = (0, import_ora3.default)("Creating configuration files...").start();
16077
16179
  const directories = [
@@ -16079,23 +16181,22 @@ var init = async () => {
16079
16181
  "remora/consumers",
16080
16182
  "remora/producers",
16081
16183
  "remora/schemas",
16082
- "remora/sources"
16184
+ "remora/sources",
16185
+ "default_remora_data"
16083
16186
  ];
16084
16187
  const defaultSource = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/source.json"), "utf-8");
16085
16188
  const defaultConsumer = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/consumer.json"), "utf-8");
16086
16189
  const defaultProducer = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/producer.json"), "utf-8");
16087
16190
  const defaultRemoraProject = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/project.json"), "utf-8");
16191
+ const defaultMockData = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "default_resources/mock_data.csv"), "utf-8");
16088
16192
  const readme = import_fs_extra2.default.readFileSync(import_path13.default.join(DOCUMENTATION_DIR, "README.md"), "utf-8");
16089
16193
  const files = [
16090
- { path: "remora/sources/.gitkeep", content: "" },
16091
- { path: "remora/sources/default-source.json", content: defaultSource },
16092
- { path: "remora/consumers/.gitkeep", content: "" },
16093
- { path: "remora/consumers/default-consumer.json", content: defaultConsumer },
16094
- { path: "remora/producers/.gitkeep", content: "" },
16095
- { path: "remora/producers/default-producer.json", content: defaultProducer },
16096
- { path: "remora/schemas/.gitkeep", content: "" },
16194
+ { path: "remora/sources/s_default.json", content: defaultSource },
16195
+ { path: "remora/consumers/c_default.json", content: defaultConsumer },
16196
+ { path: "remora/producers/p_default.json", content: defaultProducer },
16097
16197
  { path: "remora/project.json", content: defaultRemoraProject },
16098
- { path: "remora/README.md", content: readme }
16198
+ { path: "remora/README.md", content: readme },
16199
+ { path: "default_remora_data/mock_data.csv", content: defaultMockData }
16099
16200
  ];
16100
16201
  for (let i = 0; i < directories.length; i++) {
16101
16202
  const dir = directories[i];
@@ -16136,8 +16237,15 @@ var DatabaseEngineClass = class {
16136
16237
  this.MAX_TRY_CONNECTION = 3;
16137
16238
  this.db = () => this._db;
16138
16239
  this.connect = async () => {
16139
- this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI") ?? Helper_default.isDev() ? "mongodb://mongo:27017/remora" : "mongodb://localhost:27017/remora";
16140
- this._client = new import_mongodb.MongoClient(this._uri);
16240
+ if (!ProcessENVManager_default.getEnvVariable("MONGO_URI")) {
16241
+ if (Helper_default.isDev()) {
16242
+ this._uri = "mongodb://mongo:27017/remora";
16243
+ } else {
16244
+ this._uri = "mongodb://localhost:27017/remora";
16245
+ }
16246
+ } else {
16247
+ this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI");
16248
+ }
16141
16249
  const errors = [];
16142
16250
  this._client = new import_mongodb.MongoClient(this._uri);
16143
16251
  for (let i = 0; i < this.MAX_TRY_CONNECTION; i++) {
@@ -17715,33 +17823,7 @@ var ConsumerManagerClass = class {
17715
17823
  return expandedFields;
17716
17824
  };
17717
17825
  this.getAvailableColumns = (consumer) => {
17718
- const availableColumns = consumer.producers.flatMap((cProd) => {
17719
- const producer = Environment_default.getProducer(cProd.name);
17720
- if (!producer) {
17721
- const subConsumer = Environment_default.getConsumer(cProd.name);
17722
- Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
17723
- return this.getAvailableColumns(subConsumer);
17724
- } else {
17725
- const dims = producer.dimensions.map((x) => ({
17726
- consumerAlias: null,
17727
- consumerKey: null,
17728
- nameInProducer: x.name,
17729
- aliasInProducer: x.alias,
17730
- dimension: x,
17731
- owner: cProd.name
17732
- }));
17733
- const meas = producer.measures?.map((x) => ({
17734
- consumerAlias: null,
17735
- consumerKey: null,
17736
- nameInProducer: x.name,
17737
- aliasInProducer: x.name,
17738
- measure: x,
17739
- owner: cProd.name
17740
- })) ?? [];
17741
- return [...dims, ...meas];
17742
- }
17743
- });
17744
- return availableColumns;
17826
+ return ResourcesUtils_default.getAvailableColumns(consumer);
17745
17827
  };
17746
17828
  this.expandField = (consumer, field, availableColumns) => {
17747
17829
  Affirm_default(consumer, "Invalid consumer");
@@ -19326,19 +19408,17 @@ var ExecutorWriter_default = ExecutorWriter;
19326
19408
  var import_promises10 = require("stream/promises");
19327
19409
  var ExecutorOrchestratorClass = class {
19328
19410
  constructor() {
19329
- this.init = () => {
19330
- if (!this._executorPool) {
19331
- const options = {
19332
- workerThreadOpts: {
19333
- resourceLimits: {
19334
- maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
19335
- }
19411
+ this.createPool = () => {
19412
+ const options = {
19413
+ workerThreadOpts: {
19414
+ resourceLimits: {
19415
+ maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
19336
19416
  }
19337
- };
19338
- const workerPath = this._getWorkerPath();
19339
- Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
19340
- this._executorPool = import_workerpool.default.pool(import_path19.default.join(workerPath, "ExecutorWorker.js"), options);
19341
- }
19417
+ }
19418
+ };
19419
+ const workerPath = this._getWorkerPath();
19420
+ Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
19421
+ return import_workerpool.default.pool(import_path19.default.join(workerPath, "ExecutorWorker.js"), options);
19342
19422
  };
19343
19423
  this.launch = async (request) => {
19344
19424
  Affirm_default(request, "Invalid options");
@@ -19349,9 +19429,9 @@ var ExecutorOrchestratorClass = class {
19349
19429
  const _progress = new ExecutorProgress_default(logProgress);
19350
19430
  const { usageId } = UsageManager_default.startUsage(consumer, details);
19351
19431
  const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.MaximumFileSize };
19432
+ const pool = this.createPool();
19352
19433
  try {
19353
19434
  const start = performance.now();
19354
- this.init();
19355
19435
  const executorResults = [];
19356
19436
  Logger_default.log(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length})`);
19357
19437
  let counter = performance.now();
@@ -19397,16 +19477,16 @@ var ExecutorOrchestratorClass = class {
19397
19477
  _progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
19398
19478
  scope.workersId.push(workerId);
19399
19479
  Logger_default.log(`[${usageId}] Spawning worker ${workerId} for producer "${prod.name}" \u2014 chunk ${chunk.start}-${chunk.end} (${Math.round((chunk.end - chunk.start) / 1024)}KB)`);
19400
- workerThreads.push(this._executorPool.exec("executor", [workerData], {
19480
+ workerThreads.push(pool.exec("executor", [workerData], {
19401
19481
  on: (payload) => this.onWorkAdvanced(payload, currentWorkerIndex, _progress)
19402
19482
  }));
19403
19483
  }
19404
19484
  Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete`);
19405
19485
  executorResults.push(...await Promise.all(workerThreads));
19406
19486
  Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles}`);
19407
- await this._executorPool.terminate();
19408
19487
  }
19409
19488
  }
19489
+ await pool.terminate();
19410
19490
  _progress.complete();
19411
19491
  if (executorResults.some((x) => !Algo_default.hasVal(x)))
19412
19492
  throw new Error(`${executorResults.filter((x) => !Algo_default.hasVal(x)).length} worker(s) failed to produce valid results`);
@@ -19465,6 +19545,7 @@ var ExecutorOrchestratorClass = class {
19465
19545
  } catch (error) {
19466
19546
  Logger_default.log(`[${usageId}] Consumer "${consumer.name}" failed: ${Helper_default.asError(error).message}`);
19467
19547
  Logger_default.error(Helper_default.asError(error));
19548
+ await pool.terminate();
19468
19549
  await ConsumerOnFinishManager_default.onConsumerError(consumer, usageId);
19469
19550
  Logger_default.log(`[${usageId}] Running cleanup after failure`);
19470
19551
  await this.performCleanupOperations(scope, tracker);
@@ -20070,6 +20151,8 @@ var mock = async (producerName, records) => {
20070
20151
 
20071
20152
  // src/index.ts
20072
20153
  import_dotenv.default.configDotenv();
20154
+ if (!process.env.NODE_ENV)
20155
+ process.env.NODE_ENV = "production";
20073
20156
  if (process.env.NODE_ENV !== "development" && process.env.REMORA_DEBUG_MODE === "true") {
20074
20157
  Logger_default.enableFileLogging("./remora/logs");
20075
20158
  console.log(`Enabled file logger.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forzalabs/remora",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "A powerful CLI tool for seamless data translation.",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -13222,6 +13222,8 @@ var FileLogServiceClass = class {
13222
13222
  constructor() {
13223
13223
  this._enabled = false;
13224
13224
  this.enable = (folder = "logs", file = "remora.log") => {
13225
+ this._folder = folder;
13226
+ this._file = file;
13225
13227
  this._logger = import_winston.default.createLogger({
13226
13228
  level: "debug",
13227
13229
  format: import_winston.default.format.combine(
@@ -13254,6 +13256,23 @@ ${stack}` : base;
13254
13256
  });
13255
13257
  };
13256
13258
  this.flush = () => {
13259
+ if (!this._enabled || !this._logger) return Promise.resolve();
13260
+ return new Promise((resolve) => {
13261
+ let pending = this._logger.transports.length;
13262
+ if (pending === 0) return resolve();
13263
+ for (const transport of this._logger.transports) {
13264
+ if (typeof transport.on === "function") {
13265
+ transport.once("logged", () => {
13266
+ if (--pending === 0) resolve();
13267
+ });
13268
+ this._logger.info("");
13269
+ } else {
13270
+ if (--pending === 0) resolve();
13271
+ }
13272
+ }
13273
+ });
13274
+ };
13275
+ this.close = () => {
13257
13276
  if (!this._enabled || !this._logger) return Promise.resolve();
13258
13277
  return new Promise((resolve) => {
13259
13278
  this._logger.on("finish", resolve);
@@ -13333,6 +13352,7 @@ var Logger = class {
13333
13352
  FileLogService_default.write("INFO", String(message));
13334
13353
  };
13335
13354
  this.flush = () => FileLogService_default.flush();
13355
+ this.close = () => FileLogService_default.close();
13336
13356
  this.error = (error) => {
13337
13357
  let message;
13338
13358
  let stack;
@@ -13474,7 +13494,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
13474
13494
 
13475
13495
  // ../../packages/constants/src/Constants.ts
13476
13496
  var CONSTANTS = {
13477
- cliVersion: "1.1.11",
13497
+ cliVersion: "1.1.13",
13478
13498
  backendVersion: 1,
13479
13499
  backendPort: 5088,
13480
13500
  workerVersion: 2,
@@ -13646,6 +13666,42 @@ var SchemaValidatorClass = class {
13646
13666
  var SchemaValidator = new SchemaValidatorClass();
13647
13667
  var SchemaValidator_default = SchemaValidator;
13648
13668
 
13669
+ // ../../packages/common/src/ResourcesUtils.ts
13670
+ var ResourcesUtilsClass = class {
13671
+ constructor() {
13672
+ this.getAvailableColumns = (consumer) => {
13673
+ return consumer.producers.flatMap((cProd) => {
13674
+ const producer = Environment_default.getProducer(cProd.name);
13675
+ if (!producer) {
13676
+ const subConsumer = Environment_default.getConsumer(cProd.name);
13677
+ Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
13678
+ return this.getAvailableColumns(subConsumer);
13679
+ } else {
13680
+ const dims = producer.dimensions.map((x) => ({
13681
+ consumerAlias: null,
13682
+ consumerKey: null,
13683
+ nameInProducer: x.name,
13684
+ aliasInProducer: x.alias,
13685
+ dimension: x,
13686
+ owner: cProd.name
13687
+ }));
13688
+ const meas = producer.measures?.map((x) => ({
13689
+ consumerAlias: null,
13690
+ consumerKey: null,
13691
+ nameInProducer: x.name,
13692
+ aliasInProducer: x.name,
13693
+ measure: x,
13694
+ owner: cProd.name
13695
+ })) ?? [];
13696
+ return [...dims, ...meas];
13697
+ }
13698
+ });
13699
+ };
13700
+ }
13701
+ };
13702
+ var ResourcesUtils = new ResourcesUtilsClass();
13703
+ var ResourcesUtils_default = ResourcesUtils;
13704
+
13649
13705
  // ../../packages/common/src/validation/Validator.ts
13650
13706
  var ValidatorClass = class {
13651
13707
  constructor() {
@@ -13756,6 +13812,38 @@ var ValidatorClass = class {
13756
13812
  const uniqNames = Algo_default.uniqBy(sources, "name");
13757
13813
  if (uniqNames.length !== 1)
13758
13814
  errors.push(`Producers with different sources are used in the consumer "${consumer.name}" (${uniqNames.join(", ")})`);
13815
+ const availableColumns = ResourcesUtils_default.getAvailableColumns(consumer);
13816
+ const availableFieldsByProducer = /* @__PURE__ */ new Map();
13817
+ for (const col of availableColumns) {
13818
+ const existing = availableFieldsByProducer.get(col.owner) ?? [];
13819
+ if (!existing.includes(col.nameInProducer))
13820
+ existing.push(col.nameInProducer);
13821
+ availableFieldsByProducer.set(col.owner, existing);
13822
+ }
13823
+ const allAvailableFields = [...availableFieldsByProducer.values()].flat();
13824
+ for (const field of consumer.fields) {
13825
+ if (field.key === "*" || field.fixed || field.copyFrom) continue;
13826
+ if (field.from) {
13827
+ const producerFields = availableFieldsByProducer.get(field.from);
13828
+ if (producerFields && !producerFields.includes(field.key))
13829
+ errors.push(`Field "${field.key}" (from: "${field.from}") is not found in producer "${field.from}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
13830
+ } else if (consumer.producers.length === 1) {
13831
+ const producerFields = availableFieldsByProducer.get(consumer.producers[0].name);
13832
+ if (producerFields && !producerFields.includes(field.key))
13833
+ errors.push(`Field "${field.key}" is not found in producer "${consumer.producers[0].name}" in consumer "${consumer.name}". Available fields: ${producerFields.join(", ")}`);
13834
+ } else {
13835
+ if (allAvailableFields.length > 0 && !allAvailableFields.includes(field.key))
13836
+ errors.push(`Field "${field.key}" is not found in any of the producers of consumer "${consumer.name}".`);
13837
+ }
13838
+ }
13839
+ for (let i = 0; i < consumer.fields.length; i++) {
13840
+ const field = consumer.fields[i];
13841
+ if (!field.copyFrom) continue;
13842
+ const precedingFields = consumer.fields.slice(0, i);
13843
+ const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
13844
+ if (!found)
13845
+ errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
13846
+ }
13759
13847
  if (consumer.filters && consumer.filters.length > 0) {
13760
13848
  if (consumer.filters.some((x) => x.sql && x.rule))
13761
13849
  errors.push(`A single consumer can't have both filters based on SQL and filters based on rules.`);
@@ -13828,6 +13916,20 @@ var ValidatorClass = class {
13828
13916
  if (consumer.options) {
13829
13917
  if (Algo_default.hasVal(consumer.options.distinct) && Algo_default.hasVal(consumer.options.distinctOn))
13830
13918
  errors.push(`Can't specify a "distinct" and a "distinctOn" clause on the same consumer (${consumer.name}); use one or the other.`);
13919
+ if (Algo_default.hasVal(consumer.options.distinctOn)) {
13920
+ const { distinctOn } = consumer.options;
13921
+ const hasWildcard = consumer.fields.some((x) => x.key === "*");
13922
+ const consumerFieldKeys = consumer.fields.map((x) => x.alias ?? x.key);
13923
+ if (!hasWildcard) {
13924
+ const missingKeys = distinctOn.keys.filter((k) => !consumerFieldKeys.includes(k));
13925
+ if (missingKeys.length > 0)
13926
+ errors.push(`distinctOn references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13927
+ }
13928
+ if (distinctOn.resolution?.orderBy && !hasWildcard) {
13929
+ if (!consumerFieldKeys.includes(distinctOn.resolution.orderBy))
13930
+ errors.push(`distinctOn resolution orderBy field "${distinctOn.resolution.orderBy}" is not present in the consumer "${consumer.name}".`);
13931
+ }
13932
+ }
13831
13933
  if (Algo_default.hasVal(consumer.options.pivot)) {
13832
13934
  if (Algo_default.hasVal(consumer.options.distinct) || Algo_default.hasVal(consumer.options.distinctOn))
13833
13935
  errors.push(`Can't specify "pivot" together with "distinct" or "distinctOn" on the same consumer (${consumer.name}).`);
@@ -17051,33 +17153,7 @@ var ConsumerManagerClass = class {
17051
17153
  return expandedFields;
17052
17154
  };
17053
17155
  this.getAvailableColumns = (consumer) => {
17054
- const availableColumns = consumer.producers.flatMap((cProd) => {
17055
- const producer = Environment_default.getProducer(cProd.name);
17056
- if (!producer) {
17057
- const subConsumer = Environment_default.getConsumer(cProd.name);
17058
- Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
17059
- return this.getAvailableColumns(subConsumer);
17060
- } else {
17061
- const dims = producer.dimensions.map((x) => ({
17062
- consumerAlias: null,
17063
- consumerKey: null,
17064
- nameInProducer: x.name,
17065
- aliasInProducer: x.alias,
17066
- dimension: x,
17067
- owner: cProd.name
17068
- }));
17069
- const meas = producer.measures?.map((x) => ({
17070
- consumerAlias: null,
17071
- consumerKey: null,
17072
- nameInProducer: x.name,
17073
- aliasInProducer: x.name,
17074
- measure: x,
17075
- owner: cProd.name
17076
- })) ?? [];
17077
- return [...dims, ...meas];
17078
- }
17079
- });
17080
- return availableColumns;
17156
+ return ResourcesUtils_default.getAvailableColumns(consumer);
17081
17157
  };
17082
17158
  this.expandField = (consumer, field, availableColumns) => {
17083
17159
  Affirm_default(consumer, "Invalid consumer");
@@ -17789,8 +17865,15 @@ var DatabaseEngineClass = class {
17789
17865
  this.MAX_TRY_CONNECTION = 3;
17790
17866
  this.db = () => this._db;
17791
17867
  this.connect = async () => {
17792
- this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI") ?? Helper_default.isDev() ? "mongodb://mongo:27017/remora" : "mongodb://localhost:27017/remora";
17793
- this._client = new import_mongodb.MongoClient(this._uri);
17868
+ if (!ProcessENVManager_default.getEnvVariable("MONGO_URI")) {
17869
+ if (Helper_default.isDev()) {
17870
+ this._uri = "mongodb://mongo:27017/remora";
17871
+ } else {
17872
+ this._uri = "mongodb://localhost:27017/remora";
17873
+ }
17874
+ } else {
17875
+ this._uri = ProcessENVManager_default.getEnvVariable("MONGO_URI");
17876
+ }
17794
17877
  const errors = [];
17795
17878
  this._client = new import_mongodb.MongoClient(this._uri);
17796
17879
  for (let i = 0; i < this.MAX_TRY_CONNECTION; i++) {
@@ -19084,19 +19167,17 @@ var ExecutorWriter_default = ExecutorWriter;
19084
19167
  var import_promises10 = require("stream/promises");
19085
19168
  var ExecutorOrchestratorClass = class {
19086
19169
  constructor() {
19087
- this.init = () => {
19088
- if (!this._executorPool) {
19089
- const options = {
19090
- workerThreadOpts: {
19091
- resourceLimits: {
19092
- maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
19093
- }
19170
+ this.createPool = () => {
19171
+ const options = {
19172
+ workerThreadOpts: {
19173
+ resourceLimits: {
19174
+ maxOldGenerationSizeMb: Constants_default.defaults.MIN_RUNTIME_HEAP_MB
19094
19175
  }
19095
- };
19096
- const workerPath = this._getWorkerPath();
19097
- Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
19098
- this._executorPool = import_workerpool.default.pool(import_path16.default.join(workerPath, "ExecutorWorker.js"), options);
19099
- }
19176
+ }
19177
+ };
19178
+ const workerPath = this._getWorkerPath();
19179
+ Logger_default.log(`Initializing worker pool from ${workerPath} (heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB)`);
19180
+ return import_workerpool.default.pool(import_path16.default.join(workerPath, "ExecutorWorker.js"), options);
19100
19181
  };
19101
19182
  this.launch = async (request) => {
19102
19183
  Affirm_default(request, "Invalid options");
@@ -19107,9 +19188,9 @@ var ExecutorOrchestratorClass = class {
19107
19188
  const _progress = new ExecutorProgress_default(logProgress);
19108
19189
  const { usageId } = UsageManager_default.startUsage(consumer, details);
19109
19190
  const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.MaximumFileSize };
19191
+ const pool = this.createPool();
19110
19192
  try {
19111
19193
  const start = performance.now();
19112
- this.init();
19113
19194
  const executorResults = [];
19114
19195
  Logger_default.log(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length})`);
19115
19196
  let counter = performance.now();
@@ -19155,16 +19236,16 @@ var ExecutorOrchestratorClass = class {
19155
19236
  _progress.register((currentWorkerIndex + 1).toString(), prod.name, fileIndex, totalFiles);
19156
19237
  scope.workersId.push(workerId);
19157
19238
  Logger_default.log(`[${usageId}] Spawning worker ${workerId} for producer "${prod.name}" \u2014 chunk ${chunk.start}-${chunk.end} (${Math.round((chunk.end - chunk.start) / 1024)}KB)`);
19158
- workerThreads.push(this._executorPool.exec("executor", [workerData], {
19239
+ workerThreads.push(pool.exec("executor", [workerData], {
19159
19240
  on: (payload) => this.onWorkAdvanced(payload, currentWorkerIndex, _progress)
19160
19241
  }));
19161
19242
  }
19162
19243
  Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete`);
19163
19244
  executorResults.push(...await Promise.all(workerThreads));
19164
19245
  Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles}`);
19165
- await this._executorPool.terminate();
19166
19246
  }
19167
19247
  }
19248
+ await pool.terminate();
19168
19249
  _progress.complete();
19169
19250
  if (executorResults.some((x) => !Algo_default.hasVal(x)))
19170
19251
  throw new Error(`${executorResults.filter((x) => !Algo_default.hasVal(x)).length} worker(s) failed to produce valid results`);
@@ -19223,6 +19304,7 @@ var ExecutorOrchestratorClass = class {
19223
19304
  } catch (error) {
19224
19305
  Logger_default.log(`[${usageId}] Consumer "${consumer.name}" failed: ${Helper_default.asError(error).message}`);
19225
19306
  Logger_default.error(Helper_default.asError(error));
19307
+ await pool.terminate();
19226
19308
  await ConsumerOnFinishManager_default.onConsumerError(consumer, usageId);
19227
19309
  Logger_default.log(`[${usageId}] Running cleanup after failure`);
19228
19310
  await this.performCleanupOperations(scope, tracker);