@nodegx/preview 0.2.6 → 0.2.8

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.
@@ -32,6 +32,11 @@
32
32
  * (`security.json.before-<time>`) and the backend installs the project's. A restart, or a deploy
33
33
  * with the same rules, leaves `data/security.json` as it is (Richard, 2026-10-23: a reship
34
34
  * brings the project's rules; an edit made on the server lasts until the project's change).
35
+ * 4b. Installs the project's workflows and triggers (`backend/nodegx.automations.json`, P127
36
+ * BKO-001) by the same rule: on a first start, and after a deploy that changed them, the
37
+ * backend's `data/triggers.json` and `data/workflow-defs/` are moved aside (`.before-<time>`) and
38
+ * the backend loads the project's. Webhook secrets stay in `data/secrets.json`, so a sender's
39
+ * secret survives a reship. A restart, or a deploy with the same ones, leaves them as they are.
35
40
  * 5. Refuses rules that are switched off (`"devOpen": true`), which is how a development backend's
36
41
  * data arrives — and the backend's own interlock only refuses that on a public bind, while this
37
42
  * app is usually behind a proxy on 127.0.0.1. Refused before anything in the data is changed.
@@ -58,6 +63,13 @@ const BACKEND_RULES = 'security.json';
58
63
  /** Which rules the last deploy installed (their checksum), in the data folder. */
59
64
  const RULES_MARKER = 'deployed-rules.json';
60
65
  const WORKFLOW_SUFFIX = '.workflow.json';
66
+ /** The project's workflows and triggers (P127 BKO-001). */
67
+ const AUTOMATIONS_FILE = 'nodegx.automations.json';
68
+ /** The backend's own copies of them, in the data folder. */
69
+ const BACKEND_TRIGGERS = 'triggers.json';
70
+ const BACKEND_WORKFLOWS = 'workflow-defs';
71
+ /** Which automations the last deploy installed (their checksum), in the data folder. */
72
+ const AUTOMATIONS_MARKER = 'deployed-automations.json';
61
73
 
62
74
  /** The Node this app needs, and the one property that decides it. */
63
75
  const NODE_NEEDED = '22.13';
@@ -240,6 +252,74 @@ function planRules(dataDir) {
240
252
  return { action, current, shipped, config, sha256 };
241
253
  }
242
254
 
255
+ /**
256
+ * What to do with the project's workflows and triggers: the same four answers as `planRules`. The
257
+ * backend has some when it has a `triggers.json` or any workflow definition.
258
+ */
259
+ function planAutomations(dataDir) {
260
+ const shipped = path.join(BACKEND, AUTOMATIONS_FILE);
261
+ const triggers = path.join(dataDir, BACKEND_TRIGGERS);
262
+ const workflows = path.join(dataDir, BACKEND_WORKFLOWS);
263
+ if (!fs.existsSync(shipped)) return { action: 'none', triggers, workflows };
264
+ const bytes = fs.readFileSync(shipped);
265
+ try {
266
+ JSON.parse(bytes.toString('utf8'));
267
+ } catch (error) {
268
+ fail(
269
+ `This deploy's workflows and triggers (backend/${AUTOMATIONS_FILE}) are not valid JSON: ${error.message}. Deploy the project again.`
270
+ );
271
+ }
272
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
273
+ let installed = null;
274
+ try {
275
+ installed = JSON.parse(fs.readFileSync(path.join(dataDir, AUTOMATIONS_MARKER), 'utf8')).sha256;
276
+ } catch {
277
+ installed = null;
278
+ }
279
+ const hasSome = fs.existsSync(triggers) || (fs.existsSync(workflows) && fs.readdirSync(workflows).length > 0);
280
+ let action = 'replace';
281
+ if (!hasSome) action = 'install';
282
+ else if (installed === sha256) action = 'unchanged';
283
+ return { action, triggers, workflows, sha256 };
284
+ }
285
+
286
+ /** Carry out the plan. Returns where the previous ones were moved, when they were. */
287
+ function applyAutomations(plan, dataDir) {
288
+ if (plan.action !== 'install' && plan.action !== 'replace') return [];
289
+ const moved = [];
290
+ if (plan.action === 'replace') {
291
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
292
+ for (const from of [plan.triggers, plan.workflows]) {
293
+ if (!fs.existsSync(from)) continue;
294
+ const to = `${from}.before-${stamp}`;
295
+ fs.renameSync(from, to);
296
+ moved.push(to);
297
+ }
298
+ }
299
+ fs.writeFileSync(
300
+ path.join(dataDir, AUTOMATIONS_MARKER),
301
+ JSON.stringify({ sha256: plan.sha256, installedAt: new Date().toISOString() }, null, 2) + '\n'
302
+ );
303
+ return moved;
304
+ }
305
+
306
+ function describeAutomations(plan, moved) {
307
+ switch (plan.action) {
308
+ case 'none':
309
+ return "[start] workflows and triggers: this deploy carries none; the backend's own run\n";
310
+ case 'unchanged':
311
+ return '[start] workflows and triggers: unchanged (this deploy\'s are the ones already installed)\n';
312
+ case 'install':
313
+ return `[start] workflows and triggers: installing this deploy's backend/${AUTOMATIONS_FILE}\n`;
314
+ default:
315
+ return (
316
+ `[start] workflows and triggers: replaced with this deploy's backend/${AUTOMATIONS_FILE}` +
317
+ (moved.length ? `; the previous ones are kept as ${moved.join(' and ')}` : '') +
318
+ '\n'
319
+ );
320
+ }
321
+ }
322
+
243
323
  /** Carry out the plan. Returns where the previous rules were moved, when they were. */
244
324
  function applyRules(plan, dataDir) {
245
325
  if (plan.action !== 'install' && plan.action !== 'replace') return null;
@@ -339,6 +419,10 @@ async function main() {
339
419
  const plan = planRules(dataDir);
340
420
  refuseDevOpen(plan);
341
421
  process.stdout.write(describeRules(plan, `${plan.current}.before-<time>`).replace('[start]', '[check]'));
422
+ const automationsPlan = planAutomations(dataDir);
423
+ process.stdout.write(
424
+ describeAutomations(automationsPlan, [`${BACKEND_TRIGGERS}.before-<time>`, `${BACKEND_WORKFLOWS}.before-<time>`]).replace('[start]', '[check]')
425
+ );
342
426
  process.exit(0);
343
427
  }
344
428
  port = await choosePort(argv, process.env);
@@ -349,8 +433,10 @@ async function main() {
349
433
  fs.mkdirSync(dataDir, { recursive: true });
350
434
  const rules = planRules(dataDir);
351
435
  refuseDevOpen(rules);
436
+ const automations = planAutomations(dataDir);
352
437
  const movedTo = applyRules(rules, dataDir);
353
438
  process.stdout.write(describeRules(rules, movedTo));
439
+ process.stdout.write(describeAutomations(automations, applyAutomations(automations, dataDir)));
354
440
  const functions = installFunctions(dataDir);
355
441
  process.stdout.write(
356
442
  functions.length
@@ -361,7 +447,9 @@ async function main() {
361
447
  const args = ['serve', '--data-dir', dataDir, '--port', String(port), '--serve-app', APP, '--enforce-rules'];
362
448
  // The backend installs the project's rules only when it has none, and says so loudly when it
363
449
  // does not — so it is told about them only on a start that installs them.
364
- if (rules.action === 'install' || rules.action === 'replace') args.push('--project-dir', BACKEND);
450
+ // The same for the workflows and triggers, which it loads only into a backend that has none.
451
+ const installs = (plan) => plan.action === 'install' || plan.action === 'replace';
452
+ if (installs(rules) || installs(automations)) args.push('--project-dir', BACKEND);
365
453
  const name = appName();
366
454
  if (name) args.push('--backend-name', name);
367
455
  args.push(...withoutOwnArgs(argv));