@sasjs/cli 6.8.2 → 6.8.3

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,3 +1,41 @@
1
1
  import { AuthConfig, Target } from '@sasjs/utils';
2
2
  import SASjs from '@sasjs/adapter/node';
3
+ import { FlowWave } from '../../types';
4
+ /**
5
+ * A flow that finished without every job succeeding.
6
+ *
7
+ * It carries the jobs that did not, so the command can say which ones rather
8
+ * than reporting a JavaScript fault two frames later - which is what happened
9
+ * when `execute()` resolved `false` instead and the command called `.replace`
10
+ * on it.
11
+ *
12
+ * The marker flag rather than `instanceof`: this package compiles to es5, where
13
+ * a subclass of a built-in loses its prototype chain and `instanceof` answers
14
+ * false in the built artefact.
15
+ */
16
+ export declare class FlowExecutionError extends Error {
17
+ readonly flowFailure = true;
18
+ readonly csvFilePath?: string;
19
+ /**
20
+ * @param message what did not succeed.
21
+ * @param csvFilePath the flow results file, which records every job and its
22
+ * outcome. It is the diagnostic for a failed run, so it belongs in the
23
+ * message: without it the failure path says less than the success path,
24
+ * which prints the same path.
25
+ */
26
+ constructor(message: string, csvFilePath?: string);
27
+ }
28
+ export declare const isFlowFailure: (error: unknown) => error is FlowExecutionError;
29
+ /**
30
+ * Names the jobs that did not succeed, so the message a pipeline sees is about
31
+ * the flow rather than about the code that reported it.
32
+ *
33
+ * A job with no status never ran - its flow was failed by a predecessor - and is
34
+ * left out. The test is against `undefined` rather than truthiness: a status
35
+ * constant added later that happens to be falsy would otherwise be dropped from
36
+ * the message, and the message is the whole diagnosis.
37
+ */
38
+ export declare const describeFailedJobs: (flows: {
39
+ [key: string]: FlowWave;
40
+ }) => string;
3
41
  export declare function execute(target: Target, sasjs: SASjs, authConfig: AuthConfig, source: string, logFolder?: string, csvFile?: string, streamLog?: boolean): Promise<unknown>;
@@ -1,4 +1,19 @@
1
1
  "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
2
17
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
18
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
19
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -50,10 +65,71 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
50
65
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
66
  };
52
67
  Object.defineProperty(exports, "__esModule", { value: true });
68
+ exports.describeFailedJobs = exports.isFlowFailure = exports.FlowExecutionError = void 0;
53
69
  exports.execute = execute;
54
70
  var displayResult_1 = require("../../utils/displayResult");
55
71
  var examples_1 = __importDefault(require("./internal/examples"));
56
72
  var internal_1 = require("./internal");
73
+ var flow_1 = require("../../types/flow");
74
+ /**
75
+ * A flow that finished without every job succeeding.
76
+ *
77
+ * It carries the jobs that did not, so the command can say which ones rather
78
+ * than reporting a JavaScript fault two frames later - which is what happened
79
+ * when `execute()` resolved `false` instead and the command called `.replace`
80
+ * on it.
81
+ *
82
+ * The marker flag rather than `instanceof`: this package compiles to es5, where
83
+ * a subclass of a built-in loses its prototype chain and `instanceof` answers
84
+ * false in the built artefact.
85
+ */
86
+ var FlowExecutionError = /** @class */ (function (_super) {
87
+ __extends(FlowExecutionError, _super);
88
+ /**
89
+ * @param message what did not succeed.
90
+ * @param csvFilePath the flow results file, which records every job and its
91
+ * outcome. It is the diagnostic for a failed run, so it belongs in the
92
+ * message: without it the failure path says less than the success path,
93
+ * which prints the same path.
94
+ */
95
+ function FlowExecutionError(message, csvFilePath) {
96
+ var _this = _super.call(this, csvFilePath
97
+ ? "".concat(message, " The flow results were written to ").concat(csvFilePath, ".")
98
+ : message) || this;
99
+ _this.flowFailure = true;
100
+ _this.name = 'FlowExecutionError';
101
+ _this.csvFilePath = csvFilePath;
102
+ return _this;
103
+ }
104
+ return FlowExecutionError;
105
+ }(Error));
106
+ exports.FlowExecutionError = FlowExecutionError;
107
+ var isFlowFailure = function (error) {
108
+ return (error === null || error === void 0 ? void 0 : error.flowFailure) === true;
109
+ };
110
+ exports.isFlowFailure = isFlowFailure;
111
+ /**
112
+ * Names the jobs that did not succeed, so the message a pipeline sees is about
113
+ * the flow rather than about the code that reported it.
114
+ *
115
+ * A job with no status never ran - its flow was failed by a predecessor - and is
116
+ * left out. The test is against `undefined` rather than truthiness: a status
117
+ * constant added later that happens to be falsy would otherwise be dropped from
118
+ * the message, and the message is the whole diagnosis.
119
+ */
120
+ var describeFailedJobs = function (flows) {
121
+ var failed = Object.values(flows).flatMap(function (flow) {
122
+ return flow.jobs
123
+ .filter(function (job) {
124
+ return job.status !== undefined && job.status !== flow_1.FlowWaveJobStatus.Success;
125
+ })
126
+ .map(function (job) { return "".concat(job.location, " (").concat(job.status, ")"); });
127
+ });
128
+ return failed.length
129
+ ? "The flow did not complete with all jobs successful: ".concat(failed.join(', '), ".")
130
+ : 'The flow did not complete with all jobs successful.';
131
+ };
132
+ exports.describeFailedJobs = describeFailedJobs;
57
133
  function execute(target_1, sasjs_1, authConfig_1, source_1, logFolder_1, csvFile_1) {
58
134
  return __awaiter(this, arguments, void 0, function (target, sasjs, authConfig, source, logFolder, csvFile, streamLog) {
59
135
  var _this = this;
@@ -146,10 +222,13 @@ function execute(target_1, sasjs_1, authConfig_1, source_1, logFolder_1, csvFile
146
222
  reject(flowStatus.message);
147
223
  _b = (0, internal_1.allFlowsCompleted)(flows), completed = _b.completed, completedWithAllSuccess = _b.completedWithAllSuccess;
148
224
  if (completed) {
225
+ // a flow that did not succeed is a failure of the flow, not of the CLI:
226
+ // reject with what did not run, so the command can report it and exit
227
+ // with a code that means "the flow failed"
149
228
  if (completedWithAllSuccess)
150
229
  resolve(csvFileRealPath);
151
230
  else
152
- resolve(false);
231
+ reject(new FlowExecutionError((0, exports.describeFailedJobs)(flows), csvFileRealPath));
153
232
  }
154
233
  return [2 /*return*/];
155
234
  }
@@ -4,6 +4,6 @@ import { ReturnCode } from '../../types/command';
4
4
  import { TargetCommand } from '../../types/command/targetCommand';
5
5
  export declare class FlowCommand extends TargetCommand {
6
6
  constructor(args: string[]);
7
- execute(): Promise<ReturnCode>;
8
- executeFlow(target: Target, sasjs: SASjs, authConfig: AuthConfig): Promise<ReturnCode>;
7
+ execute(): Promise<ReturnCode.Success | ReturnCode.InvalidCommand | ReturnCode.InternalError | ReturnCode.LogCheckError | ReturnCode.FlowError>;
8
+ executeFlow(target: Target, sasjs: SASjs, authConfig: AuthConfig): Promise<ReturnCode.Success | ReturnCode.InternalError | ReturnCode.LogCheckError | ReturnCode.FlowError>;
9
9
  }
@@ -67,7 +67,8 @@ var FlowSubCommand;
67
67
  })(FlowSubCommand || (FlowSubCommand = {}));
68
68
  var syntax = 'flow <subCommand> [options]';
69
69
  var usage = 'sasjs flow <execute> --source <flowPath> [options]';
70
- var description = 'Performs operations on flows of jobs.';
70
+ var description = 'Performs operations on flows of jobs.\n' +
71
+ 'Exits non-zero when a flow does not complete: 6 when a job did not succeed, 2 when the flow could not be executed at all.';
71
72
  var examples = [
72
73
  {
73
74
  command: 'sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv --target myTarget --streamLog',
@@ -163,6 +164,14 @@ var FlowCommand = /** @class */ (function (_super) {
163
164
  return [4 /*yield*/, (0, execute_1.execute)(target, sasjs, authConfig, source, logFolder, csvFile, streamLog)
164
165
  .then(function (csvFilePath) {
165
166
  var _a;
167
+ // execute() resolves a path or rejects. Anything else is a contract
168
+ // violation, and reporting it as a successful flow would hide it - the
169
+ // shape that used to arrive here was `false` for a failed flow, which is
170
+ // what turned a failure into a TypeError
171
+ if (typeof csvFilePath !== 'string') {
172
+ (0, utils_1.displayError)(new Error("The flow reported completion without a CSV path (resolved ".concat(typeof csvFilePath, ").")), 'An error has occurred when executing this flow.');
173
+ return command_1.ReturnCode.InternalError;
174
+ }
166
175
  var pathSepRegExp = new RegExp(path_1.default.sep.replace(/\\/g, '\\\\'), 'g');
167
176
  (_a = process.logger) === null || _a === void 0 ? void 0 : _a.info("CSV file located at: ".concat(csvFilePath.replace(pathSepRegExp, '/')));
168
177
  return command_1.ReturnCode.Success;
@@ -170,6 +179,10 @@ var FlowCommand = /** @class */ (function (_super) {
170
179
  .catch(function (err) {
171
180
  if ((0, logcheck_1.isLogCheckFailure)(err))
172
181
  return command_1.ReturnCode.LogCheckError;
182
+ if ((0, execute_1.isFlowFailure)(err)) {
183
+ (0, utils_1.displayError)(err, 'The flow did not complete successfully.');
184
+ return command_1.ReturnCode.FlowError;
185
+ }
173
186
  (0, utils_1.displayError)(err, 'An error has occurred when executing this flow.');
174
187
  return command_1.ReturnCode.InternalError;
175
188
  })];
@@ -207,9 +207,104 @@ describe('sasjs flow', function () {
207
207
  }
208
208
  });
209
209
  }); });
210
+ it('rejects with the failing job rather than resolving a value', function () { return __awaiter(void 0, void 0, void 0, function () {
211
+ var flows, error;
212
+ return __generator(this, function (_a) {
213
+ switch (_a.label) {
214
+ case 0:
215
+ flows = {
216
+ flow1: {
217
+ jobs: [{ location: 'goodJob' }, { location: 'badJob' }],
218
+ predecessors: []
219
+ }
220
+ };
221
+ setupMocks(flows, 'badJob');
222
+ return [4 /*yield*/, executeFlowWrapper().catch(function (thrown) { return thrown; })
223
+ // the marker, not `instanceof`: the built artefact is es5, where an Error
224
+ // subclass loses its prototype chain, so the assertion has to be the one
225
+ // that holds in both places
226
+ ];
227
+ case 1:
228
+ error = _a.sent();
229
+ // the marker, not `instanceof`: the built artefact is es5, where an Error
230
+ // subclass loses its prototype chain, so the assertion has to be the one
231
+ // that holds in both places
232
+ expect((0, execute_1.isFlowFailure)(error)).toBe(true);
233
+ expect(error.message).toEqual('The flow did not complete with all jobs successful: badJob (failure).');
234
+ return [2 /*return*/];
235
+ }
236
+ });
237
+ }); });
238
+ it('rejects with a flow error a caller can recognise', function () { return __awaiter(void 0, void 0, void 0, function () {
239
+ var flows;
240
+ return __generator(this, function (_a) {
241
+ switch (_a.label) {
242
+ case 0:
243
+ flows = {
244
+ flow1: { jobs: [{ location: 'badJob' }], predecessors: [] }
245
+ };
246
+ setupMocks(flows, 'badJob');
247
+ return [4 /*yield*/, expect(executeFlowWrapper()).rejects.toSatisfy(execute_1.isFlowFailure)];
248
+ case 1:
249
+ _a.sent();
250
+ return [2 /*return*/];
251
+ }
252
+ });
253
+ }); });
254
+ });
255
+ });
256
+ describe('FlowExecutionError', function () {
257
+ it('carries the flow results path, and says where they are', function () {
258
+ var error = new execute_1.FlowExecutionError('The flow did not complete with all jobs successful: /b (failure).', '/project/sasjsresults/flow-results.csv');
259
+ expect(error.csvFilePath).toEqual('/project/sasjsresults/flow-results.csv');
260
+ expect(error.message).toEqual('The flow did not complete with all jobs successful: /b (failure). ' +
261
+ 'The flow results were written to /project/sasjsresults/flow-results.csv.');
262
+ expect((0, execute_1.isFlowFailure)(error)).toBe(true);
263
+ });
264
+ it('says nothing about a path it was not given', function () {
265
+ expect(new execute_1.FlowExecutionError('The flow failed.').message).toEqual('The flow failed.');
266
+ });
267
+ });
268
+ describe('describeFailedJobs', function () {
269
+ it('names every job that did not succeed, across flows', function () {
270
+ var flows = {
271
+ flow1: {
272
+ jobs: [
273
+ { location: '/a', status: flow_1.FlowWaveJobStatus.Success },
274
+ { location: '/b', status: flow_1.FlowWaveJobStatus.Failure }
275
+ ]
276
+ },
277
+ flow2: {
278
+ jobs: [{ location: '/c', status: flow_1.FlowWaveJobStatus.Failure }]
279
+ }
280
+ };
281
+ expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /b (failure), /c (failure).');
282
+ });
283
+ it('leaves out jobs that did not run', function () {
284
+ var flows = {
285
+ flow1: {
286
+ jobs: [
287
+ { location: '/never-ran' },
288
+ { location: '/b', status: flow_1.FlowWaveJobStatus.Failure }
289
+ ]
290
+ }
291
+ };
292
+ expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /b (failure).');
293
+ });
294
+ it('names a job whose status is falsy rather than dropping it', function () {
295
+ // a status constant added later could be falsy; truthiness would silently
296
+ // omit that job from the only diagnosis the caller gets
297
+ var flows = {
298
+ flow1: { jobs: [{ location: '/zero', status: 0 }] }
299
+ };
300
+ expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /zero (0).');
301
+ });
302
+ it('states the failure without a list when no job carries a status', function () {
303
+ var flows = { flow1: { jobs: [{ location: '/a' }] } };
304
+ expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful.');
210
305
  });
211
306
  });
212
- var setupMocks = function (flows) {
307
+ var setupMocks = function (flows, failingJob) {
213
308
  jest.restoreAllMocks();
214
309
  jest.mock('../internal/validateParams');
215
310
  jest.mock('../internal/executeFlow');
@@ -223,9 +318,14 @@ var setupMocks = function (flows) {
223
318
  jest.spyOn(executeFlowModule, 'executeFlow').mockImplementation(function (flow) { return __awaiter(void 0, void 0, void 0, function () {
224
319
  return __generator(this, function (_a) {
225
320
  flow.execution = 'started';
226
- flow.jobs.forEach(function (job) { return (job.status = flow_1.FlowWaveJobStatus.Success); });
321
+ flow.jobs.forEach(function (job) {
322
+ job.status =
323
+ job.location === failingJob
324
+ ? flow_1.FlowWaveJobStatus.Failure
325
+ : flow_1.FlowWaveJobStatus.Success;
326
+ });
227
327
  return [2 /*return*/, {
228
- jobStatus: true,
328
+ jobStatus: !failingJob,
229
329
  flowStatus: { terminate: false, message: '' }
230
330
  }];
231
331
  });
@@ -95,6 +95,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
95
95
  };
96
96
  Object.defineProperty(exports, "__esModule", { value: true });
97
97
  var executeModule = __importStar(require("../execute"));
98
+ var execute_1 = require("../execute");
98
99
  var flowCommand_1 = require("../flowCommand");
99
100
  var utils_1 = require("@sasjs/utils");
100
101
  var configUtils = __importStar(require("../../../utils/config"));
@@ -264,6 +265,54 @@ describe('FlowCommand', function () {
264
265
  }
265
266
  });
266
267
  }); });
268
+ it('should return the flow code when the flow did not succeed', function () { return __awaiter(void 0, void 0, void 0, function () {
269
+ var command, _a;
270
+ return __generator(this, function (_b) {
271
+ switch (_b.label) {
272
+ case 0:
273
+ jest
274
+ .spyOn(executeModule, 'execute')
275
+ .mockImplementation(function () {
276
+ return Promise.reject(new execute_1.FlowExecutionError('badJob (failure)'));
277
+ });
278
+ command = new flowCommand_1.FlowCommand(__spreadArray(__spreadArray([], __read(defaultArgs), false), [
279
+ 'flow',
280
+ 'execute',
281
+ '--source',
282
+ source
283
+ ], false));
284
+ _a = expect;
285
+ return [4 /*yield*/, command.execute()];
286
+ case 1:
287
+ _a.apply(void 0, [_b.sent()]).toEqual(command_1.ReturnCode.FlowError);
288
+ return [2 /*return*/];
289
+ }
290
+ });
291
+ }); });
292
+ it('should treat a resolve without a CSV path as an internal error', function () { return __awaiter(void 0, void 0, void 0, function () {
293
+ var command, _a;
294
+ return __generator(this, function (_b) {
295
+ switch (_b.label) {
296
+ case 0:
297
+ // execute() resolves a path or rejects; anything else is a contract
298
+ // violation, and must not be reported as a green flow
299
+ jest
300
+ .spyOn(executeModule, 'execute')
301
+ .mockImplementation(function () { return Promise.resolve(false); });
302
+ command = new flowCommand_1.FlowCommand(__spreadArray(__spreadArray([], __read(defaultArgs), false), [
303
+ 'flow',
304
+ 'execute',
305
+ '--source',
306
+ source
307
+ ], false));
308
+ _a = expect;
309
+ return [4 /*yield*/, command.execute()];
310
+ case 1:
311
+ _a.apply(void 0, [_b.sent()]).toEqual(command_1.ReturnCode.InternalError);
312
+ return [2 /*return*/];
313
+ }
314
+ });
315
+ }); });
267
316
  it('should log the error and return the error code when execution is unsuccessful', function () { return __awaiter(void 0, void 0, void 0, function () {
268
317
  var args, command, returnCode;
269
318
  return __generator(this, function (_a) {
@@ -4,5 +4,6 @@ export declare enum ReturnCode {
4
4
  InternalError = 2,
5
5
  LintError = 3,
6
6
  AlreadyInitialised = 4,
7
- LogCheckError = 5
7
+ LogCheckError = 5,
8
+ FlowError = 6
8
9
  }
@@ -13,6 +13,10 @@ var ReturnCode;
13
13
  ReturnCode[ReturnCode["AlreadyInitialised"] = 4] = "AlreadyInitialised";
14
14
  // `logcheck` found issues at or above the configured failure policy. Kept
15
15
  // apart from LintError so a pipeline can tell a code-quality failure from a
16
- // run that SAS complained about.
16
+ // run SAS complained about.
17
17
  ReturnCode[ReturnCode["LogCheckError"] = 5] = "LogCheckError";
18
+ // A flow finished without every job succeeding. Distinct from InternalError,
19
+ // which also covers a bad source file or an unreachable server, so a pipeline
20
+ // can tell "the flow failed" from "the CLI could not run the flow".
21
+ ReturnCode[ReturnCode["FlowError"] = 6] = "FlowError";
18
22
  })(ReturnCode || (exports.ReturnCode = ReturnCode = {}));
package/package.json CHANGED
@@ -123,5 +123,5 @@
123
123
  "ws": "8.21.0",
124
124
  "nanoid": "3.3.18"
125
125
  },
126
- "version": "6.8.2"
126
+ "version": "6.8.3"
127
127
  }