forest-cli
Version:
The Lumberjacks' toolbelt is the Forest CLI which makes easy to manage your back office application directly from the terminal.
160 lines • 12.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectStaleSidecars = exports.planSidecarUploads = exports.hasWorkflowBpmnOps = exports.stripWorkflowBpmnOps = exports.partitionMissingSidecars = exports.resolveRemoteWorkflow = exports.sidecarPath = exports.isSafeWorkflowId = void 0;
const path_1 = __importDefault(require("path"));
const diff_1 = require("./diff");
const errors_1 = require("./errors");
const patch_rules_1 = require("./patch-rules");
/**
* A workflow id becomes a sidecar filename (`workflows/<id>.bpmn`). Because that
* id comes from a layout file, a crafted file could set it to `../../.ssh/id_rsa`
* and make `apply --with-workflows` read an arbitrary file (or `pull` write
* outside the workflows dir). Only accept ids that are a single, traversal-free
* path segment — Forest ids are UUIDs, so this is deliberately strict.
*/
function isSafeWorkflowId(id) {
return (typeof id === 'string' &&
id.length > 0 &&
!id.includes('/') &&
!id.includes('\\') &&
!id.includes('..') &&
path_1.default.basename(id) === id);
}
exports.isSafeWorkflowId = isSafeWorkflowId;
/** The sidecar path for a workflow id, or null when the id is unsafe. */
function sidecarPath(dir, id) {
return isSafeWorkflowId(id) ? path_1.default.join(dir, `${id}.bpmn`) : null;
}
exports.sidecarPath = sidecarPath;
/**
* Resolve a file workflow to its target-env counterpart by DELEGATING to the
* diff engine's own resolver (`remoteResolver` in diff.ts), so the sidecar
* upload targets the very workflow the domain patch addressed — including on
* duplicate names, where the resolver's Map makes the LAST remote win. A
* dev→prod promote typically matches by NAME (each env mints its own ids).
*/
function resolveRemoteWorkflow(remoteWorkflows, workflow) {
return (0, diff_1.remoteResolver)(remoteWorkflows)(workflow);
}
exports.resolveRemoteWorkflow = resolveRemoteWorkflow;
/**
* Split the missing-sidecar workflows by what actually happens to their BPMN:
* - `targetKeepsOwn`: the workflow exists in the target env (id-then-name), so
* the strip of `bpmnAwsS3Identifier` leaves the target's own BPMN untouched;
* - `createdWithoutBpmn`: the workflow matches nothing in the target — this
* apply `add`s it, and with its declared pointer stripped and no sidecar to
* upload, it is created with NO BPMN at all. The caller must warn explicitly:
* "keeps its own" would be a lie here.
*/
function partitionMissingSidecars(missing, remoteWorkflows) {
const targetKeepsOwn = [];
const createdWithoutBpmn = [];
missing.forEach(workflow => {
if (resolveRemoteWorkflow(remoteWorkflows, workflow))
targetKeepsOwn.push(workflow);
else
createdWithoutBpmn.push(workflow);
});
return { createdWithoutBpmn, targetKeepsOwn };
}
exports.partitionMissingSidecars = partitionMissingSidecars;
/**
* When `--with-workflows`, BPMN transport is owned by the sidecar upload, not by
* the JSON patch: `bpmnAwsS3Identifier` is a per-environment S3 pointer and is not
* portable. Drop it from the diff so a pulled file never patches a target workflow
* to the source env's (nonexistent) version — for workflows with a sidecar the
* upload links the fresh id; for those without, the target keeps its own BPMN.
*/
function stripWorkflowBpmnOps(ops) {
return ops
.filter(op => !/\/bpmnAwsS3Identifier$/.test(op.path))
.map(op => {
if (op.op !== 'add' || !op.value || typeof op.value !== 'object')
return op;
const value = { ...op.value };
delete value.bpmnAwsS3Identifier;
return { ...op, value };
});
}
exports.stripWorkflowBpmnOps = stripWorkflowBpmnOps;
/**
* True when the plan would write a `bpmnAwsS3Identifier` into the target — either
* as a replace of the pointer itself or inside an added workflow. Used by `apply`
* WITHOUT `--with-workflows` to warn on cross-environment applies: the pointer is
* env-local, so patching it into another env plants a dead S3 reference.
*/
function hasWorkflowBpmnOps(ops) {
return ops.some(op => {
if (/\/bpmnAwsS3Identifier$/.test(op.path))
return true;
if (op.op !== 'add' || !op.value || typeof op.value !== 'object')
return false;
const pointer = op.value.bpmnAwsS3Identifier;
return pointer !== undefined && pointer !== null;
});
}
exports.hasWorkflowBpmnOps = hasWorkflowBpmnOps;
/**
* Decide which sidecars need uploading, resolving each to its TARGET-env
* workflow first (id-then-name, like the diff) so that:
* - a same-name workflow with a different id (dev→prod promote) is uploaded
* under the TARGET id, never the source one;
* - byte-identical BPMN is skipped (idempotent re-apply), including in that
* name-matched case.
*/
async function planSidecarUploads(manager, scope, sidecars, remoteWorkflows, renderingId) {
const decided = await Promise.all(sidecars.map(async (sidecar) => {
const remote = resolveRemoteWorkflow(remoteWorkflows, sidecar.workflow);
if (!remote)
return { changed: true, upload: { ...sidecar, target: null } };
const target = {
collectionId: String(remote.collectionId ?? sidecar.workflow.collectionId),
id: String(remote.id),
};
const upload = { ...sidecar, target };
const version = remote.bpmnAwsS3Identifier;
if (typeof version !== 'string' || !version)
return { changed: true, upload };
try {
const stored = await manager.getWorkflowBpmn(scope, target.id, target.collectionId, version, renderingId);
return { changed: stored !== sidecar.bpmn, upload };
}
catch (error) {
// Auth/permission failures must surface, not be reframed as "changed".
if (error instanceof errors_1.LayoutApiError && (error.status === 401 || error.status === 403)) {
throw error;
}
return { changed: true, upload };
}
}));
return decided.filter(entry => entry.changed).map(entry => entry.upload);
}
exports.planSidecarUploads = planSidecarUploads;
/**
* A managed sidecar filename: `<id>.bpmn` where the id has the shape of a REAL
* server workflow id (numeric or UUID — the same `NUM_OR_UUID` class the patch
* whitelist uses). Managed sidecars are the only files pull ever writes, so
* they are the only files it may ever delete.
*/
const MANAGED_SIDECAR = new RegExp(`^${patch_rules_1.NUM_OR_UUID}\\.bpmn$`);
/**
* The sidecar filenames a pull should prune: files named after a server
* workflow id (`<num-or-uuid>.bpmn`) that matches no workflow left in the
* environment. Everything else is preserved:
* - files whose basename is not a server-shaped id (a user's own BPMN files,
* e.g. `review.bpmn` — pull never wrote them, so pull never deletes them);
* - sidecars of workflows still present in the env, even when their download
* was skipped (dead S3 ref) — that sidecar may be the last copy of the BPMN,
* and a backup tool must never destroy the backup.
*/
function selectStaleSidecars(entries, keepIds) {
return entries.filter(entry => {
const match = MANAGED_SIDECAR.exec(entry);
return match !== null && !keepIds.has(match[1]);
});
}
exports.selectStaleSidecars = selectStaleSidecars;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid29ya2Zsb3ctc2lkZWNhci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zZXJ2aWNlcy9sYXlvdXQvd29ya2Zsb3ctc2lkZWNhci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFhQSxnREFBd0I7QUFFeEIsaUNBQXdDO0FBQ3hDLHFDQUEwQztBQUMxQywrQ0FBNEM7QUFFNUM7Ozs7OztHQU1HO0FBQ0gsU0FBZ0IsZ0JBQWdCLENBQUMsRUFBVztJQUMxQyxPQUFPLENBQ0wsT0FBTyxFQUFFLEtBQUssUUFBUTtRQUN0QixFQUFFLENBQUMsTUFBTSxHQUFHLENBQUM7UUFDYixDQUFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDO1FBQ2pCLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUM7UUFDbEIsQ0FBQyxFQUFFLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQztRQUNsQixjQUFJLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxLQUFLLEVBQUUsQ0FDekIsQ0FBQztBQUNKLENBQUM7QUFURCw0Q0FTQztBQUVELHlFQUF5RTtBQUN6RSxTQUFnQixXQUFXLENBQUMsR0FBVyxFQUFFLEVBQVc7SUFDbEQsT0FBTyxnQkFBZ0IsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsY0FBSSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7QUFDcEUsQ0FBQztBQUZELGtDQUVDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsU0FBZ0IscUJBQXFCLENBQ25DLGVBQStDLEVBQy9DLFFBQXVDO0lBRXZDLE9BQU8sSUFBQSxxQkFBYyxFQUFDLGVBQWUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBQ25ELENBQUM7QUFMRCxzREFLQztBQUtEOzs7Ozs7OztHQVFHO0FBQ0gsU0FBZ0Isd0JBQXdCLENBQ3RDLE9BQXlCLEVBQ3pCLGVBQStDO0lBRS9DLE1BQU0sY0FBYyxHQUFxQixFQUFFLENBQUM7SUFDNUMsTUFBTSxrQkFBa0IsR0FBcUIsRUFBRSxDQUFDO0lBRWhELE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLEVBQUU7UUFDekIsSUFBSSxxQkFBcUIsQ0FBQyxlQUFlLEVBQUUsUUFBUSxDQUFDO1lBQUUsY0FBYyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQzs7WUFDL0Usa0JBQWtCLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3pDLENBQUMsQ0FBQyxDQUFDO0lBRUgsT0FBTyxFQUFFLGtCQUFrQixFQUFFLGNBQWMsRUFBRSxDQUFDO0FBQ2hELENBQUM7QUFiRCw0REFhQztBQUVEOzs7Ozs7R0FNRztBQUNILFNBQWdCLG9CQUFvQixDQUNsQyxHQUFRO0lBRVIsT0FBTyxHQUFHO1NBQ1AsTUFBTSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQyx3QkFBd0IsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDO1NBQ3JELEdBQUcsQ0FBQyxFQUFFLENBQUMsRUFBRTtRQUNSLElBQUksRUFBRSxDQUFDLEVBQUUsS0FBSyxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUMsS0FBSyxJQUFJLE9BQU8sRUFBRSxDQUFDLEtBQUssS0FBSyxRQUFRO1lBQUUsT0FBTyxFQUFFLENBQUM7UUFFNUUsTUFBTSxLQUFLLEdBQUcsRUFBRSxHQUFJLEVBQUUsQ0FBQyxLQUFpQyxFQUFFLENBQUM7UUFDM0QsT0FBTyxLQUFLLENBQUMsbUJBQW1CLENBQUM7UUFFakMsT0FBTyxFQUFFLEdBQUcsRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDO0lBQzFCLENBQUMsQ0FBQyxDQUFDO0FBQ1AsQ0FBQztBQWJELG9EQWFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxTQUFnQixrQkFBa0IsQ0FDaEMsR0FBeUQ7SUFFekQsT0FBTyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxFQUFFO1FBQ25CLElBQUksd0JBQXdCLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUN4RCxJQUFJLEVBQUUsQ0FBQyxFQUFFLEtBQUssS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDLEtBQUssSUFBSSxPQUFPLEVBQUUsQ0FBQyxLQUFLLEtBQUssUUFBUTtZQUFFLE9BQU8sS0FBSyxDQUFDO1FBRS9FLE1BQU0sT0FBTyxHQUFJLEVBQUUsQ0FBQyxLQUFpQyxDQUFDLG1CQUFtQixDQUFDO1FBRTFFLE9BQU8sT0FBTyxLQUFLLFNBQVMsSUFBSSxPQUFPLEtBQUssSUFBSSxDQUFDO0lBQ25ELENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQVhELGdEQVdDO0FBa0JEOzs7Ozs7O0dBT0c7QUFDSSxLQUFLLFVBQVUsa0JBQWtCLENBQ3RDLE9BQXNCLEVBQ3RCLEtBQWtCLEVBQ2xCLFFBQXVCLEVBQ3ZCLGVBQStDLEVBQy9DLFdBQW1CO0lBRW5CLE1BQU0sT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FDL0IsUUFBUSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUMsT0FBTyxFQUFDLEVBQUU7UUFDM0IsTUFBTSxNQUFNLEdBQUcscUJBQXFCLENBQUMsZUFBZSxFQUFFLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUN4RSxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxFQUFFLEdBQUcsT0FBTyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsRUFBRSxDQUFDO1FBRTVFLE1BQU0sTUFBTSxHQUFHO1lBQ2IsWUFBWSxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsWUFBWSxJQUFJLE9BQU8sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDO1lBQzFFLEVBQUUsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztTQUN0QixDQUFDO1FBQ0YsTUFBTSxNQUFNLEdBQUcsRUFBRSxHQUFHLE9BQU8sRUFBRSxNQUFNLEVBQUUsQ0FBQztRQUN0QyxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsbUJBQW1CLENBQUM7UUFDM0MsSUFBSSxPQUFPLE9BQU8sS0FBSyxRQUFRLElBQUksQ0FBQyxPQUFPO1lBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFFOUUsSUFBSTtZQUNGLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLGVBQWUsQ0FDMUMsS0FBSyxFQUNMLE1BQU0sQ0FBQyxFQUFFLEVBQ1QsTUFBTSxDQUFDLFlBQVksRUFDbkIsT0FBTyxFQUNQLFdBQVcsQ0FDWixDQUFDO1lBRUYsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLEtBQUssT0FBTyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsQ0FBQztTQUNyRDtRQUFDLE9BQU8sS0FBSyxFQUFFO1lBQ2QsdUVBQXVFO1lBQ3ZFLElBQUksS0FBSyxZQUFZLHVCQUFjLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxLQUFLLEdBQUcsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxFQUFFO2dCQUNyRixNQUFNLEtBQUssQ0FBQzthQUNiO1lBRUQsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7U0FDbEM7SUFDSCxDQUFDLENBQUMsQ0FDSCxDQUFDO0lBRUYsT0FBTyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUMzRSxDQUFDO0FBMUNELGdEQTBDQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxlQUFlLEdBQUcsSUFBSSxNQUFNLENBQUMsSUFBSSx5QkFBVyxVQUFVLENBQUMsQ0FBQztBQUU5RDs7Ozs7Ozs7O0dBU0c7QUFDSCxTQUFnQixtQkFBbUIsQ0FBQyxPQUFpQixFQUFFLE9BQTRCO0lBQ2pGLE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRTtRQUM1QixNQUFNLEtBQUssR0FBRyxlQUFlLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBRTFDLE9BQU8sS0FBSyxLQUFLLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDbEQsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDO0FBTkQsa0RBTUMifQ==