@knocklabs/cli 0.1.0-rc.2 → 0.1.0-rc.4

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.
Files changed (38) hide show
  1. package/README.md +82 -6
  2. package/dist/commands/commit/index.js +4 -18
  3. package/dist/commands/commit/promote.js +4 -17
  4. package/dist/commands/translation/list.js +82 -0
  5. package/dist/commands/translation/pull.js +124 -0
  6. package/dist/commands/translation/push.js +130 -0
  7. package/dist/commands/translation/validate.js +122 -0
  8. package/dist/commands/workflow/activate.js +5 -18
  9. package/dist/commands/workflow/new.js +3 -3
  10. package/dist/commands/workflow/pull.js +70 -17
  11. package/dist/commands/workflow/push.js +3 -3
  12. package/dist/commands/workflow/validate.js +3 -3
  13. package/dist/lib/api-v1.js +38 -2
  14. package/dist/lib/base-command.js +2 -2
  15. package/dist/lib/helpers/error.js +16 -8
  16. package/dist/lib/helpers/flag.js +63 -3
  17. package/dist/lib/helpers/fs.js +52 -0
  18. package/dist/lib/helpers/json.js +6 -2
  19. package/dist/lib/helpers/object.js +43 -0
  20. package/dist/lib/helpers/page.js +3 -1
  21. package/dist/lib/helpers/request.js +17 -49
  22. package/dist/lib/helpers/ux.js +42 -0
  23. package/dist/lib/marshal/translation/helpers.js +185 -0
  24. package/dist/lib/marshal/translation/index.js +19 -0
  25. package/dist/lib/marshal/translation/reader.js +118 -0
  26. package/dist/lib/marshal/translation/types.js +4 -0
  27. package/dist/lib/marshal/translation/writer.js +86 -0
  28. package/dist/lib/marshal/workflow/generator.js +46 -5
  29. package/dist/lib/marshal/workflow/helpers.js +2 -0
  30. package/dist/lib/marshal/workflow/reader.js +136 -117
  31. package/dist/lib/marshal/workflow/writer.js +235 -98
  32. package/dist/lib/{helpers/dir-context.js → run-context/helpers.js} +1 -1
  33. package/dist/lib/run-context/index.js +22 -0
  34. package/dist/lib/{run-context.js → run-context/loader.js} +22 -7
  35. package/dist/lib/run-context/types.js +4 -0
  36. package/oclif.manifest.json +253 -1
  37. package/package.json +11 -10
  38. package/dist/lib/helpers/spinner.js +0 -20
@@ -9,9 +9,9 @@ function _export(target, all) {
9
9
  });
10
10
  }
11
11
  _export(exports, {
12
- validateTemplateFilePathFormat: ()=>validateTemplateFilePathFormat,
13
12
  readWorkflowDir: ()=>readWorkflowDir,
14
- readTemplateFile: ()=>readTemplateFile
13
+ checkIfValidExtractedFilePathFormat: ()=>checkIfValidExtractedFilePathFormat,
14
+ readExtractedFileSync: ()=>readExtractedFileSync
15
15
  });
16
16
  const _nodePath = /*#__PURE__*/ _interopRequireWildcard(require("node:path"));
17
17
  const _fsExtra = /*#__PURE__*/ _interopRequireWildcard(require("fs-extra"));
@@ -21,7 +21,6 @@ const _json = require("../../helpers/json");
21
21
  const _liquid = require("../../helpers/liquid");
22
22
  const _object = require("../../helpers/object");
23
23
  const _helpers = require("./helpers");
24
- const _types = require("./types");
25
24
  function _getRequireWildcardCache(nodeInterop) {
26
25
  if (typeof WeakMap !== "function") return null;
27
26
  var cacheBabelInterop = new WeakMap();
@@ -61,66 +60,95 @@ function _interopRequireWildcard(obj, nodeInterop) {
61
60
  }
62
61
  return newObj;
63
62
  }
64
- const validateTemplateFilePathFormat = (relpath, workflowDirCtx)=>{
63
+ // For now we support up to two levels of content extraction in workflow.json.
64
+ // (e.g. workflow.json, then visual_blocks.json)
65
+ const MAX_EXTRACTION_LEVEL = 2;
66
+ // The following files are exepected to have valid json content, and should be
67
+ // decoded and joined into the main workflow.json.
68
+ const DECODABLE_JSON_FILES = new Set([
69
+ _helpers.VISUAL_BLOCKS_JSON
70
+ ]);
71
+ /*
72
+ * Validate the file path format of an extracted field. The file path must be:
73
+ *
74
+ * 1) Expressed as a relative path.
75
+ *
76
+ * For exmaple:
77
+ * subject@: "email_1/subject.html" // GOOD
78
+ * subject@: "./email_1/subject.html" // GOOD
79
+ * subject@: "/workflow-x/email_1/subject.html" // BAD
80
+ *
81
+ * 2) The resolved path must be contained inside the workflow directory
82
+ *
83
+ * For exmaple (workflow-y is a different workflow dir in this example):
84
+ * subject@: "./email_1/subject.html" // GOOD
85
+ * subject@: "../workflow-y/email_1/subject.html" // BAD
86
+ *
87
+ * Note: does not validate the presence of the file nor the uniqueness of the
88
+ * file path.
89
+ */ const checkIfValidExtractedFilePathFormat = (relpath, sourceFileAbspath)=>{
65
90
  if (typeof relpath !== "string") return false;
66
91
  if (_nodePath.isAbsolute(relpath)) return false;
67
- const abspath = _nodePath.resolve(workflowDirCtx.abspath, relpath);
68
- const pathDiff = _nodePath.relative(workflowDirCtx.abspath, abspath);
92
+ const extractedFileAbspath = _nodePath.resolve(sourceFileAbspath, relpath);
93
+ const pathDiff = _nodePath.relative(sourceFileAbspath, extractedFileAbspath);
69
94
  return !pathDiff.startsWith("..");
70
95
  };
71
96
  /*
72
- * Validate that a file exists at the given relative path in the directory.
73
- */ const validateTemplateFileExists = async (relpath, workflowDirCtx)=>{
74
- const abspath = _nodePath.resolve(workflowDirCtx.abspath, relpath);
75
- return _fsExtra.pathExists(abspath);
97
+ * Validate the extracted file path based on its format and uniqueness (but not
98
+ * the presence).
99
+ *
100
+ * Note, the uniqueness check is based on reading from and writing to
101
+ * uniqueFilePaths, which is MUTATED in place.
102
+ */ const validateExtractedFilePath = (val, workflowDirCtx, uniqueFilePaths, objPathToFieldStr)=>{
103
+ const workflowJsonPath = _nodePath.resolve(workflowDirCtx.abspath, _helpers.WORKFLOW_JSON);
104
+ // Validate the file path format, and that it is unique per workflow.
105
+ if (!checkIfValidExtractedFilePathFormat(val, workflowJsonPath) || typeof val !== "string" || val in uniqueFilePaths) {
106
+ const error = new _error.JsonDataError("must be a relative path string to a unique file within the directory", objPathToFieldStr);
107
+ return error;
108
+ }
109
+ // Keep track of all the valid extracted file paths that have been seen, so
110
+ // we can validate each file path's uniqueness as we traverse.
111
+ uniqueFilePaths[val] = true;
112
+ return undefined;
76
113
  };
77
- const readTemplateFile = async (relpath, workflowDirCtx)=>{
114
+ const readExtractedFileSync = (relpath, workflowDirCtx, objPathToFieldStr = "")=>{
115
+ // Check if the file actually exists at the given file path.
78
116
  const abspath = _nodePath.resolve(workflowDirCtx.abspath, relpath);
79
- // First read all template files as text content and check for valid liquid
80
- // syntax given it is supported across all message templates and formats.
81
- const content = await _fsExtra.readFile(abspath, "utf8");
82
- const liquidParseError = (0, _liquid.validateLiquidSyntax)(content);
83
- return liquidParseError ? [
84
- undefined,
85
- [
86
- liquidParseError
87
- ]
88
- ] : [
89
- content,
90
- []
91
- ];
92
- };
93
- /*
94
- * Validates that a given value is a valid template file path and the file
95
- * actually exists, before reading the file content.
96
- */ const maybeReadTemplateFile = async (val, workflowDirCtx, extractedFilePaths, pathToFieldStr)=>{
97
- // Validate the file path format, and that it is unique per workflow.
98
- if (!validateTemplateFilePathFormat(val, workflowDirCtx) || typeof val !== "string" || val in extractedFilePaths) {
99
- const error = new _error.JsonDataError("must be a relative path string to a unique file within the directory", pathToFieldStr);
117
+ const exists = _fsExtra.pathExistsSync(abspath);
118
+ if (!exists) {
119
+ const error = new _error.JsonDataError("must be a relative path string to a file that exists", objPathToFieldStr);
100
120
  return [
101
121
  undefined,
102
122
  error
103
123
  ];
104
124
  }
105
- // Keep track of all the extracted file paths that have been seen, so we
106
- // can validate each file path's uniqueness as we traverse.
107
- extractedFilePaths[val] = true;
108
- // Check a file actually exists at the given file path.
109
- const exists = await validateTemplateFileExists(val, workflowDirCtx);
110
- if (!exists) {
111
- const error = new _error.JsonDataError("must be a relative path string to a file that exists", pathToFieldStr);
125
+ // Read the file and check for valid liquid syntax given it is supported
126
+ // across all message templates and file extensions.
127
+ const contentStr = _fsExtra.readFileSync(abspath, "utf8");
128
+ const liquidParseError = (0, _liquid.validateLiquidSyntax)(contentStr);
129
+ if (liquidParseError) {
130
+ const error = new _error.JsonDataError(`points to a file that contains invalid liquid syntax (${relpath})\n\n` + (0, _error.formatErrors)([
131
+ liquidParseError
132
+ ], {
133
+ indentBy: 2
134
+ }), objPathToFieldStr);
112
135
  return [
113
136
  undefined,
114
137
  error
115
138
  ];
116
139
  }
117
- // Read the template file and inline the content into the workflow json
118
- // under the same field name but without the @ filepath marker.
119
- const [content, contentErrors] = await readTemplateFile(val, workflowDirCtx);
120
- if (contentErrors.length > 0) {
121
- const error = new _error.JsonDataError(`points to a file with invalid content (${val})\n\n` + (0, _error.formatErrors)(contentErrors, {
140
+ // If the file is expected to contain decodable json, then parse the contentStr
141
+ // as such.
142
+ const fileName = _nodePath.basename(abspath.toLowerCase());
143
+ const decodable = DECODABLE_JSON_FILES.has(fileName);
144
+ const [content, jsonParseErrors] = decodable ? (0, _json.parseJson)(contentStr) : [
145
+ contentStr,
146
+ []
147
+ ];
148
+ if (jsonParseErrors.length > 0) {
149
+ const error = new _error.JsonDataError(`points to a file with invalid content (${relpath})\n\n` + (0, _error.formatErrors)(jsonParseErrors, {
122
150
  indentBy: 2
123
- }), pathToFieldStr);
151
+ }), objPathToFieldStr);
124
152
  return [
125
153
  undefined,
126
154
  error
@@ -131,79 +159,70 @@ const readTemplateFile = async (relpath, workflowDirCtx)=>{
131
159
  undefined
132
160
  ];
133
161
  };
134
- const compileTemplateFiles = async (workflowDirCtx, workflowJson)=>{
162
+ const joinExtractedFiles = async (workflowDirCtx, workflowJson)=>{
163
+ // Tracks any errors encountered during traversal. Mutated in place.
135
164
  const errors = [];
136
- const extractedFilePaths = {};
137
- const objPath = new _object.ObjPath();
138
- // 1. Make sure we have a list of steps to look through.
139
- if (workflowJson.steps === undefined) {
140
- return [
141
- workflowJson,
142
- errors
143
- ];
144
- }
145
- if (!Array.isArray(workflowJson.steps)) {
146
- errors.push(new _error.JsonDataError("must be an array of workflow steps", objPath.to("steps").str));
147
- return [
148
- workflowJson,
149
- errors
150
- ];
151
- }
152
- // 2. Make sure we can reach `steps[i].template` for channel steps.
153
- const steps = workflowJson.steps || [];
154
- const pathToSteps = objPath.push("steps").checkout();
155
- for (const [stepIdx, step] of steps.entries()){
156
- objPath.reset(pathToSteps).push(stepIdx);
157
- if (!(0, _lodash.isPlainObject)(step)) {
158
- errors.push(new _error.JsonDataError("must be a workflow step object", objPath.str));
159
- continue;
160
- }
161
- if (step.type === undefined) {
162
- errors.push(new _error.JsonDataError("must have a `type` field", objPath.str));
163
- continue;
164
- }
165
- // Not a channel step, nothing more to do.
166
- if (step.type !== _types.StepType.Channel) {
167
- continue;
168
- }
169
- if (step.template === undefined) {
170
- errors.push(new _error.JsonDataError("must have a `template` field containing a template object", objPath.str));
171
- continue;
172
- }
173
- if (!(0, _lodash.isPlainObject)(step.template)) {
174
- errors.push(new _error.JsonDataError("must be a template object", objPath.to("template").str));
175
- continue;
176
- }
177
- // 3. For a given template, look for any extracted template content, read
178
- // the extracted template files, then inline the content.
179
- objPath.push("template");
180
- for (const [field, val] of Object.entries(step.template)){
181
- if (field.startsWith("settings")) continue;
182
- if (!_helpers.FILEPATH_MARKED_RE.test(field)) continue;
183
- const pathToFieldStr = objPath.to(field).str;
184
- // eslint-disable-next-line no-await-in-loop
185
- const [content, error] = await maybeReadTemplateFile(val, workflowDirCtx, extractedFilePaths, pathToFieldStr);
186
- if (error) {
187
- errors.push(error);
188
- continue;
165
+ // Tracks each new valid extracted file path seen (rebased to be relative to
166
+ // workflow.json) in the workflow json node. Mutated in place, and used
167
+ // to validate the uniqueness of an extracted path encountered.
168
+ const uniqueFilePaths = {};
169
+ // Tracks each extracted file path (rebased) that gets inlined with its object
170
+ // path location, per each traversal iteration. Mutated in place, and used for
171
+ // rebasing an extracted path to be relative to the location of the workflow
172
+ // json file.
173
+ const joinedFilePathsPerLevel = [];
174
+ for (const [idx] of Array.from({
175
+ length: MAX_EXTRACTION_LEVEL
176
+ }).entries()){
177
+ const currJoinedFilePaths = {};
178
+ const prevJoinedFilePaths = joinedFilePathsPerLevel[idx - 1] || {};
179
+ (0, _object.mapValuesDeep)(workflowJson, (value, key, parts)=>{
180
+ // If not marked with the @ suffix, there's nothing to do.
181
+ if (!_helpers.FILEPATH_MARKED_RE.test(key)) return;
182
+ const objPathToFieldStr = _object.ObjPath.stringify(parts);
183
+ const inlinObjPathStr = objPathToFieldStr.replace(_helpers.FILEPATH_MARKED_RE, "");
184
+ // If there is inlined content present already, then nothing more to do.
185
+ if ((0, _lodash.hasIn)(workflowJson, inlinObjPathStr)) return;
186
+ // Check if the extracted path found at the current field path belongs to
187
+ // a node whose parent or grandparent has been previously joined earlier
188
+ // in the tree. If so, rebase the extracted path to be a relative path to
189
+ // the workflow json.
190
+ const lastFound = (0, _object.getLastFound)(prevJoinedFilePaths, parts);
191
+ const prevJoinedFilePath = typeof lastFound === "string" ? lastFound : undefined;
192
+ const rebasedFilePath = prevJoinedFilePath ? _nodePath.join(_nodePath.dirname(prevJoinedFilePath), value) : value;
193
+ const invalidFilePathError = validateExtractedFilePath(rebasedFilePath, workflowDirCtx, uniqueFilePaths, objPathToFieldStr);
194
+ if (invalidFilePathError) {
195
+ errors.push(invalidFilePathError);
196
+ // Wipe the invalid file path in the node so the final workflow json
197
+ // object ends up with only valid file paths, this way workflow writer
198
+ // can see only valid file paths and use those when pulling. Also set
199
+ // the inlined field path in workflow object with empty content so we
200
+ // know we've looked at this extracted file path.
201
+ (0, _lodash.set)(workflowJson, objPathToFieldStr, undefined);
202
+ (0, _lodash.set)(workflowJson, inlinObjPathStr, undefined);
203
+ return;
189
204
  }
190
- const inlinePathStr = pathToFieldStr.replace(_helpers.FILEPATH_MARKED_RE, "");
191
- (0, _lodash.set)(workflowJson, inlinePathStr, content);
192
- }
193
- if (!step.template.settings) continue;
194
- objPath.push("settings");
195
- for (const [field, val] of Object.entries(step.template.settings)){
196
- if (!_helpers.FILEPATH_MARKED_RE.test(field)) continue;
197
- const pathToFieldStr = objPath.to(field).str;
198
- // eslint-disable-next-line no-await-in-loop
199
- const [content, error] = await maybeReadTemplateFile(val, workflowDirCtx, extractedFilePaths, pathToFieldStr);
200
- if (error) {
201
- errors.push(error);
202
- continue;
205
+ // By this point we have a valid extracted file path, so attempt to read
206
+ // the file at the file path.
207
+ const [content, readExtractedFileError] = readExtractedFileSync(rebasedFilePath, workflowDirCtx, objPathToFieldStr);
208
+ if (readExtractedFileError) {
209
+ errors.push(readExtractedFileError);
210
+ // Replace the extracted file path with the rebased one, and set the
211
+ // inlined field path in workflow object with empty content, so we know
212
+ // we do not need to try inlining again.
213
+ (0, _lodash.set)(workflowJson, objPathToFieldStr, rebasedFilePath);
214
+ (0, _lodash.set)(workflowJson, inlinObjPathStr, undefined);
215
+ return;
203
216
  }
204
- const inlinePathStr = pathToFieldStr.replace(_helpers.FILEPATH_MARKED_RE, "");
205
- (0, _lodash.set)(workflowJson, inlinePathStr, content);
206
- }
217
+ // Inline the file content and replace the extracted file path with a
218
+ // rebased one.
219
+ (0, _lodash.set)(workflowJson, objPathToFieldStr, rebasedFilePath);
220
+ (0, _lodash.set)(workflowJson, inlinObjPathStr, content);
221
+ // Track joined file paths from the current join level.
222
+ (0, _lodash.set)(currJoinedFilePaths, inlinObjPathStr, rebasedFilePath);
223
+ });
224
+ // Finally save all the joined file paths from this traversal iteration.
225
+ joinedFilePathsPerLevel[idx] = currJoinedFilePaths;
207
226
  }
208
227
  return [
209
228
  workflowJson,
@@ -212,7 +231,7 @@ const compileTemplateFiles = async (workflowDirCtx, workflowJson)=>{
212
231
  };
213
232
  const readWorkflowDir = async (workflowDirCtx, opts = {})=>{
214
233
  const { abspath } = workflowDirCtx;
215
- const { withTemplateFiles =false , withReadonlyField =false } = opts;
234
+ const { withExtractedFiles =false , withReadonlyField =false } = opts;
216
235
  const dirExists = await _fsExtra.pathExists(abspath);
217
236
  if (!dirExists) throw new Error(`${abspath} does not exist`);
218
237
  const workflowJsonPath = await (0, _helpers.lsWorkflowJson)(abspath);
@@ -223,7 +242,7 @@ const readWorkflowDir = async (workflowDirCtx, opts = {})=>{
223
242
  workflowJson = withReadonlyField ? workflowJson : (0, _object.omitDeep)(workflowJson, [
224
243
  "__readonly"
225
244
  ]);
226
- return withTemplateFiles ? compileTemplateFiles(workflowDirCtx, workflowJson) : [
245
+ return withExtractedFiles ? joinExtractedFiles(workflowDirCtx, workflowJson) : [
227
246
  workflowJson,
228
247
  []
229
248
  ];