@things-factory/integration-base 10.1.117 → 10.1.119
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.
- package/dist-server/engine/connection-manager.js +8 -2
- package/dist-server/engine/connection-manager.js.map +1 -1
- package/dist-server/engine/connector/headless-connector.d.ts +1 -0
- package/dist-server/engine/connector/headless-connector.js +5 -1
- package/dist-server/engine/connector/headless-connector.js.map +1 -1
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +5 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/integration-runtime.d.ts +47 -0
- package/dist-server/engine/integration-runtime.js +78 -0
- package/dist-server/engine/integration-runtime.js.map +1 -0
- package/dist-server/engine/resource-pool/headless-pool.js +6 -1
- package/dist-server/engine/resource-pool/headless-pool.js.map +1 -1
- package/dist-server/engine/scenario-engine.js +4 -0
- package/dist-server/engine/scenario-engine.js.map +1 -1
- package/dist-server/engine/types.d.ts +10 -0
- package/dist-server/engine/types.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +8 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/routers/scenario-schedule-run.d.ts +2 -1
- package/dist-server/routers/scenario-schedule-run.js +9 -0
- package/dist-server/routers/scenario-schedule-run.js.map +1 -1
- package/dist-server/service/connection/connection.js +3 -0
- package/dist-server/service/connection/connection.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **Whether integration runs in this process** — on by default, turned off once by an application that runs
|
|
4
|
+
* it elsewhere (ADR-0110 addition 2, ruling 2026-10-05).
|
|
5
|
+
*
|
|
6
|
+
* ── Why ────────────────────────────────────────────────────────────────────
|
|
7
|
+
* An application that depends on this module gets, at boot, every connection connected (a headless connection
|
|
8
|
+
* launches a browser) and every active scenario started. operato-plant serves its screens from one process and
|
|
9
|
+
* runs integration — headless browsers and scheduled scenarios — in another (`MES_PROCESS_ROLE=worker`). The
|
|
10
|
+
* serving process must not launch a browser or run a scenario because a setting was forgotten.
|
|
11
|
+
*
|
|
12
|
+
* ── What off means ────────────────────────────────────────────────────────
|
|
13
|
+
* Only running stops: the boot does not connect or load anything, the headless pool is not made, and the four
|
|
14
|
+
* ways in — connecting a connection, loading a scenario, starting an iteration, a scheduled run — are refused by
|
|
15
|
+
* name (`integration-runtime-off`) with the reason the application gave, which says where integration runs.
|
|
16
|
+
* Reading and saving connection and scenario definitions works as before.
|
|
17
|
+
*
|
|
18
|
+
* ── Once, and early ───────────────────────────────────────────────────────
|
|
19
|
+
* `disable` must come before `bootstrap-module-start` (an application calls it while its module loads; the
|
|
20
|
+
* shell loads every module before it emits the hook). Called after the boot it does not try to stop what is
|
|
21
|
+
* already running — it throws `integration-runtime-disabled-too-late`. There is no `enable`: a process that
|
|
22
|
+
* was told integration runs elsewhere does not change its mind.
|
|
23
|
+
*
|
|
24
|
+
* The state is on `globalThis`, so a second copy of this module in one process (hand copies, duplicate
|
|
25
|
+
* installs) reads the same answer.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.IntegrationRuntime = exports.IntegrationRuntimeOff = exports.INTEGRATION_RUNTIME_DISABLED_TOO_LATE = exports.INTEGRATION_RUNTIME_OFF = void 0;
|
|
29
|
+
exports.INTEGRATION_RUNTIME_OFF = 'integration-runtime-off';
|
|
30
|
+
exports.INTEGRATION_RUNTIME_DISABLED_TOO_LATE = 'integration-runtime-disabled-too-late';
|
|
31
|
+
const STATE = Symbol.for('things-factory.integration-base.runtime');
|
|
32
|
+
function state() {
|
|
33
|
+
const holder = globalThis;
|
|
34
|
+
return (holder[STATE] ??= { booted: false });
|
|
35
|
+
}
|
|
36
|
+
/** A refusal because integration does not run in this process. */
|
|
37
|
+
class IntegrationRuntimeOff extends Error {
|
|
38
|
+
constructor(what, reason) {
|
|
39
|
+
super(`${exports.INTEGRATION_RUNTIME_OFF}: ${what} does not run in this process — ${reason}`);
|
|
40
|
+
this.code = exports.INTEGRATION_RUNTIME_OFF;
|
|
41
|
+
this.name = 'IntegrationRuntimeOff';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.IntegrationRuntimeOff = IntegrationRuntimeOff;
|
|
45
|
+
exports.IntegrationRuntime = {
|
|
46
|
+
/** True unless an application turned integration off in this process. */
|
|
47
|
+
get enabled() {
|
|
48
|
+
return !state().off;
|
|
49
|
+
},
|
|
50
|
+
/** Why it is off, as the application said it; undefined while on. */
|
|
51
|
+
get reason() {
|
|
52
|
+
return state().off?.reason;
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Turn integration off in this process, for good. `reason` should say where it runs instead — it is shown to
|
|
56
|
+
* whoever presses a connect button here.
|
|
57
|
+
*/
|
|
58
|
+
disable(reason) {
|
|
59
|
+
const s = state();
|
|
60
|
+
if (s.off)
|
|
61
|
+
return;
|
|
62
|
+
if (s.booted) {
|
|
63
|
+
throw new Error(`${exports.INTEGRATION_RUNTIME_DISABLED_TOO_LATE}: integration was turned off after the application booted; connections and scenarios may already be running — turn it off while the module loads`);
|
|
64
|
+
}
|
|
65
|
+
s.off = { reason: String(reason ?? '').trim() || 'integration is turned off in this process' };
|
|
66
|
+
},
|
|
67
|
+
/** Throw the named refusal when off. `what` names what was asked for. */
|
|
68
|
+
assertEnabled(what) {
|
|
69
|
+
const off = state().off;
|
|
70
|
+
if (off)
|
|
71
|
+
throw new IntegrationRuntimeOff(what, off.reason);
|
|
72
|
+
},
|
|
73
|
+
/** The boot has happened; from here `disable` is too late. Called by this module's boot hook. */
|
|
74
|
+
markBooted() {
|
|
75
|
+
state().booted = true;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=integration-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-runtime.js","sourceRoot":"","sources":["../../server/engine/integration-runtime.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEU,QAAA,uBAAuB,GAAG,yBAAyB,CAAA;AACnD,QAAA,qCAAqC,GAAG,uCAAuC,CAAA;AAO5F,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAA;AAEnE,SAAS,KAAK;IACZ,MAAM,MAAM,GAAG,UAAiE,CAAA;IAChF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;AAC9C,CAAC;AAED,kEAAkE;AAClE,MAAa,qBAAsB,SAAQ,KAAK;IAE9C,YAAY,IAAY,EAAE,MAAc;QACtC,KAAK,CAAC,GAAG,+BAAuB,KAAK,IAAI,mCAAmC,MAAM,EAAE,CAAC,CAAA;QAF9E,SAAI,GAAG,+BAAuB,CAAA;QAGrC,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAA;IACrC,CAAC;CACF;AAND,sDAMC;AAEY,QAAA,kBAAkB,GAAG;IAChC,yEAAyE;IACzE,IAAI,OAAO;QACT,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,CAAA;IACrB,CAAC;IAED,qEAAqE;IACrE,IAAI,MAAM;QACR,OAAO,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,CAAA;IAC5B,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,MAAc;QACpB,MAAM,CAAC,GAAG,KAAK,EAAE,CAAA;QACjB,IAAI,CAAC,CAAC,GAAG;YAAE,OAAM;QACjB,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,GAAG,6CAAqC,kJAAkJ,CAC3L,CAAA;QACH,CAAC;QACD,CAAC,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,2CAA2C,EAAE,CAAA;IAChG,CAAC;IAED,yEAAyE;IACzE,aAAa,CAAC,IAAY;QACxB,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC,GAAG,CAAA;QACvB,IAAI,GAAG;YAAE,MAAM,IAAI,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC5D,CAAC;IAED,iGAAiG;IACjG,UAAU;QACR,KAAK,EAAE,CAAC,MAAM,GAAG,IAAI,CAAA;IACvB,CAAC;CACF,CAAA","sourcesContent":["/**\n * **Whether integration runs in this process** — on by default, turned off once by an application that runs\n * it elsewhere (ADR-0110 addition 2, ruling 2026-10-05).\n *\n * ── Why ────────────────────────────────────────────────────────────────────\n * An application that depends on this module gets, at boot, every connection connected (a headless connection\n * launches a browser) and every active scenario started. operato-plant serves its screens from one process and\n * runs integration — headless browsers and scheduled scenarios — in another (`MES_PROCESS_ROLE=worker`). The\n * serving process must not launch a browser or run a scenario because a setting was forgotten.\n *\n * ── What off means ────────────────────────────────────────────────────────\n * Only running stops: the boot does not connect or load anything, the headless pool is not made, and the four\n * ways in — connecting a connection, loading a scenario, starting an iteration, a scheduled run — are refused by\n * name (`integration-runtime-off`) with the reason the application gave, which says where integration runs.\n * Reading and saving connection and scenario definitions works as before.\n *\n * ── Once, and early ───────────────────────────────────────────────────────\n * `disable` must come before `bootstrap-module-start` (an application calls it while its module loads; the\n * shell loads every module before it emits the hook). Called after the boot it does not try to stop what is\n * already running — it throws `integration-runtime-disabled-too-late`. There is no `enable`: a process that\n * was told integration runs elsewhere does not change its mind.\n *\n * The state is on `globalThis`, so a second copy of this module in one process (hand copies, duplicate\n * installs) reads the same answer.\n */\n\nexport const INTEGRATION_RUNTIME_OFF = 'integration-runtime-off'\nexport const INTEGRATION_RUNTIME_DISABLED_TOO_LATE = 'integration-runtime-disabled-too-late'\n\ninterface RuntimeState {\n off?: { reason: string }\n booted: boolean\n}\n\nconst STATE = Symbol.for('things-factory.integration-base.runtime')\n\nfunction state(): RuntimeState {\n const holder = globalThis as unknown as Record<symbol, RuntimeState | undefined>\n return (holder[STATE] ??= { booted: false })\n}\n\n/** A refusal because integration does not run in this process. */\nexport class IntegrationRuntimeOff extends Error {\n readonly code = INTEGRATION_RUNTIME_OFF\n constructor(what: string, reason: string) {\n super(`${INTEGRATION_RUNTIME_OFF}: ${what} does not run in this process — ${reason}`)\n this.name = 'IntegrationRuntimeOff'\n }\n}\n\nexport const IntegrationRuntime = {\n /** True unless an application turned integration off in this process. */\n get enabled(): boolean {\n return !state().off\n },\n\n /** Why it is off, as the application said it; undefined while on. */\n get reason(): string | undefined {\n return state().off?.reason\n },\n\n /**\n * Turn integration off in this process, for good. `reason` should say where it runs instead — it is shown to\n * whoever presses a connect button here.\n */\n disable(reason: string): void {\n const s = state()\n if (s.off) return\n if (s.booted) {\n throw new Error(\n `${INTEGRATION_RUNTIME_DISABLED_TOO_LATE}: integration was turned off after the application booted; connections and scenarios may already be running — turn it off while the module loads`\n )\n }\n s.off = { reason: String(reason ?? '').trim() || 'integration is turned off in this process' }\n },\n\n /** Throw the named refusal when off. `what` names what was asked for. */\n assertEnabled(what: string): void {\n const off = state().off\n if (off) throw new IntegrationRuntimeOff(what, off.reason)\n },\n\n /** The boot has happened; from here `disable` is too late. Called by this module's boot hook. */\n markBooted(): void {\n state().booted = true\n }\n}\n"]}
|
|
@@ -9,6 +9,7 @@ exports.default = getHeadlessPool;
|
|
|
9
9
|
exports.getPoolStats = getPoolStats;
|
|
10
10
|
exports.forceCleanupPool = forceCleanupPool;
|
|
11
11
|
const shell_1 = require("@things-factory/shell");
|
|
12
|
+
const integration_runtime_1 = require("../integration-runtime");
|
|
12
13
|
/*
|
|
13
14
|
* ── The pool is made when the application boots, not when this module is loaded (2026-10-04) ──
|
|
14
15
|
* It used to be made at import, with `min: 2`, so loading this module launched two headless Chromes. The shell's
|
|
@@ -24,6 +25,8 @@ const shell_1 = require("@things-factory/shell");
|
|
|
24
25
|
*/
|
|
25
26
|
let integrationPool;
|
|
26
27
|
function pool() {
|
|
28
|
+
/* Off in this process: no browser is launched here, at boot or on use (ADR-0110 addition 2). */
|
|
29
|
+
integration_runtime_1.IntegrationRuntime.assertEnabled('the headless browser pool');
|
|
27
30
|
integrationPool ??= (0, shell_1.getOrCreateHeadlessPool)('integration', {
|
|
28
31
|
min: 2,
|
|
29
32
|
max: 20,
|
|
@@ -37,7 +40,9 @@ function pool() {
|
|
|
37
40
|
return integrationPool;
|
|
38
41
|
}
|
|
39
42
|
(0, shell_1.onHook)('bootstrap-module-start', () => {
|
|
40
|
-
|
|
43
|
+
integration_runtime_1.IntegrationRuntime.markBooted();
|
|
44
|
+
if (integration_runtime_1.IntegrationRuntime.enabled)
|
|
45
|
+
pool();
|
|
41
46
|
});
|
|
42
47
|
/**
|
|
43
48
|
* Get the integration headless pool
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headless-pool.js","sourceRoot":"","sources":["../../../server/engine/resource-pool/headless-pool.ts"],"names":[],"mappings":";AAAA;;;GAGG;;
|
|
1
|
+
{"version":3,"file":"headless-pool.js","sourceRoot":"","sources":["../../../server/engine/resource-pool/headless-pool.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AA8CH,0CAWC;AAgD2B,kCAAO;AA1CnC,oCA8BC;AAMD,4CAGC;AApGD,iDAAqH;AAErH,gEAA2D;AAE3D;;;;;;;;;;;;GAYG;AACH,IAAI,eAAuE,CAAA;AAE3E,SAAS,IAAI;IACX,gGAAgG;IAChG,wCAAkB,CAAC,aAAa,CAAC,2BAA2B,CAAC,CAAA;IAC7D,eAAe,KAAK,IAAA,+BAAuB,EAAC,aAAa,EAAE;QACzD,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,EAAE;QACP,IAAI,EAAE,CAAC,GAAG,mCAA2B,CAAC,KAAK,EAAE,GAAG,mCAA2B,CAAC,aAAa,CAAC;QAC1F,oBAAoB,EAAE,KAAK;QAC3B,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,IAAI;QACjB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;KACpB,CAAC,CAAA;IACF,OAAO,eAAe,CAAA;AACxB,CAAC;AAED,IAAA,cAAM,EAAC,wBAAwB,EAAE,GAAG,EAAE;IACpC,wCAAkB,CAAC,UAAU,EAAE,CAAA;IAC/B,IAAI,wCAAkB,CAAC,OAAO;QAAE,IAAI,EAAE,CAAA;AACxC,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,SAAgB,eAAe;IAC7B,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE;QAC/B,OAAO,EAAE,CAAC,QAAa,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpD,IAAI,EAAE,CAAC,EAAE,iDAAiD;QAC1D,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,GAAG,EAAE,EAAE;QACP,GAAG,EAAE,CAAC;KACP,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,YAAY;IAC1B,MAAM,QAAQ,GAAG,IAAA,uBAAe,GAAE,CAAA;IAClC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,WAAW,CAAA;IAE7C,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,OAAO;YACL,IAAI,EAAE,CAAC;YACP,SAAS,EAAE,CAAC;YACZ,QAAQ,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;YACV,GAAG,EAAE,EAAE;YACP,GAAG,EAAE,CAAC;YACN,YAAY,EAAE,CAAC;YACf,cAAc,EAAE,CAAC;YACjB,aAAa,EAAE,CAAC;YAChB,aAAa,EAAE,CAAC;YAChB,iBAAiB,EAAE,CAAC;YACpB,sBAAsB,EAAE,CAAC;YACzB,eAAe,EAAE,GAAG;YACpB,YAAY,EAAE,IAAI;SACnB,CAAA;IACH,CAAC;IAED,8CAA8C;IAC9C,OAAO;QACL,GAAG,gBAAgB,CAAC,UAAU;QAC9B,GAAG,gBAAgB,CAAC,UAAU;QAC9B,sBAAsB,EAAE,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,sBAAsB,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC;QAC3G,eAAe,EAAE,gBAAgB,CAAC,UAAU,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;KAC9E,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB;IAC9B,2CAA2C;IAC3C,OAAO,eAAe,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;AACtE,CAAC","sourcesContent":["/**\n * Integration Base Headless Pool\n * Using the unified headless pool system from @things-factory/shell\n */\n\nimport { getAllPoolStats, getOrCreateHeadlessPool, HEADLESS_POOL_ARGUMENT_SETS, onHook } from '@things-factory/shell'\n\nimport { IntegrationRuntime } from '../integration-runtime'\n\n/*\n * ── The pool is made when the application boots, not when this module is loaded (2026-10-04) ──\n * It used to be made at import, with `min: 2`, so loading this module launched two headless Chromes. The shell's\n * module loader loads every things-factory module in an application's dependency tree, so **any** process that\n * loaded the shell — every DB test of operato-warehouse among them — launched two browsers it never used. Their\n * sockets keep the event loop alive, and the shell's cleanup waits for `beforeExit`, which never comes while\n * they are open: the test file passed in half a second and its process did not end (measured: warehouse\n * `master-change-approval.db.test.cjs`, two chrome-headless-shell children and two TCP sockets to 127.0.0.1).\n *\n * Now it is made on `bootstrap-module-start`, as board-service's label pool is — an application still has two\n * warm browsers from boot — or on first use, whichever comes first. A process that never boots an application\n * and never acquires launches nothing.\n */\nlet integrationPool: ReturnType<typeof getOrCreateHeadlessPool> | undefined\n\nfunction pool() {\n /* Off in this process: no browser is launched here, at boot or on use (ADR-0110 addition 2). */\n IntegrationRuntime.assertEnabled('the headless browser pool')\n integrationPool ??= getOrCreateHeadlessPool('integration', {\n min: 2,\n max: 20,\n args: [...HEADLESS_POOL_ARGUMENT_SETS.basic, ...HEADLESS_POOL_ARGUMENT_SETS.keychain_safe],\n acquireTimeoutMillis: 15000,\n testOnBorrow: true,\n enableStats: true,\n enableRecovery: true,\n enableCleanup: true\n })\n return integrationPool\n}\n\nonHook('bootstrap-module-start', () => {\n IntegrationRuntime.markBooted()\n if (IntegrationRuntime.enabled) pool()\n})\n\n/**\n * Get the integration headless pool\n * @returns Pool instance with acquire/release methods\n */\nexport function getHeadlessPool() {\n return {\n acquire: () => pool().acquire(),\n release: (resource: any) => pool().release(resource),\n size: 0, // These will be dynamically calculated if needed\n available: 0,\n borrowed: 0,\n pending: 0,\n max: 20,\n min: 2\n }\n}\n\n/**\n * Get pool statistics\n * @returns Detailed pool statistics\n */\nexport function getPoolStats() {\n const allStats = getAllPoolStats()\n const integrationStats = allStats.integration\n\n if (!integrationStats) {\n return {\n size: 0,\n available: 0,\n borrowed: 0,\n pending: 0,\n max: 20,\n min: 2,\n totalCreated: 0,\n totalDestroyed: 0,\n totalAcquired: 0,\n totalReleased: 0,\n currentlyAcquired: 0,\n averageAcquisitionTime: 0,\n utilizationRate: '0',\n lastActivity: null\n }\n }\n\n // Convert to format expected by existing code\n return {\n ...integrationStats.poolStatus,\n ...integrationStats.statistics,\n averageAcquisitionTime: parseInt(integrationStats.statistics.averageAcquisitionTime.replace('ms', '')) || 0,\n utilizationRate: integrationStats.poolStatus.utilizationRate.replace('%', '')\n }\n}\n\n/**\n * Force cleanup pool - delegates to unified system\n * @returns Promise<void>\n */\nexport function forceCleanupPool(): Promise<void> {\n /* Nothing made yet is nothing to clean. */\n return integrationPool ? integrationPool.reset() : Promise.resolve()\n}\n\n// Legacy compatibility - re-export the main function\nexport { getHeadlessPool as default }\n"]}
|
|
@@ -5,6 +5,7 @@ const shell_1 = require("@things-factory/shell");
|
|
|
5
5
|
const scenario_1 = require("../service/scenario/scenario");
|
|
6
6
|
const scenario_instance_type_1 = require("../service/scenario-instance/scenario-instance-type");
|
|
7
7
|
const pending_queue_1 = require("./pending-queue");
|
|
8
|
+
const integration_runtime_1 = require("./integration-runtime");
|
|
8
9
|
const auth_base_1 = require("@things-factory/auth-base");
|
|
9
10
|
const resolve_steps_1 = require("../service/scenario/resolve-steps");
|
|
10
11
|
const debug = require('debug')('things-factory:integration-base:scenario-engine');
|
|
@@ -32,6 +33,7 @@ class ScenarioEngine {
|
|
|
32
33
|
return instances.filter(instance => !scenarioName || instance.scenarioName == scenarioName);
|
|
33
34
|
}
|
|
34
35
|
static async load(instanceName, scenarioConfig, context) {
|
|
36
|
+
integration_runtime_1.IntegrationRuntime.assertEnabled(`scenario "${instanceName}"`);
|
|
35
37
|
const { domain: scenarioDomain } = scenarioConfig;
|
|
36
38
|
const domain = context?.domain || scenarioDomain;
|
|
37
39
|
debug('load', domain.subdomain, instanceName);
|
|
@@ -86,6 +88,7 @@ class ScenarioEngine {
|
|
|
86
88
|
* `alreadyRunning` — `run` would hand back the running instance and report nothing.
|
|
87
89
|
*/
|
|
88
90
|
static async runIteration(instanceName, scenarioConfig, context) {
|
|
91
|
+
integration_runtime_1.IntegrationRuntime.assertEnabled(`scenario "${instanceName}"`);
|
|
89
92
|
const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig;
|
|
90
93
|
const domain = context?.domain || scenarioDomain;
|
|
91
94
|
const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration);
|
|
@@ -143,6 +146,7 @@ class ScenarioEngine {
|
|
|
143
146
|
* @returns Promise that resolves immediately, but scenarios execute sequentially
|
|
144
147
|
*/
|
|
145
148
|
static async loadIteration(instanceName, scenarioConfig, context) {
|
|
149
|
+
integration_runtime_1.IntegrationRuntime.assertEnabled(`scenario "${instanceName}"`);
|
|
146
150
|
const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig;
|
|
147
151
|
const domain = context?.domain || scenarioDomain;
|
|
148
152
|
debug('load-iteration', domain.subdomain, instanceName, iteration);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario-engine.js","sourceRoot":"","sources":["../../server/engine/scenario-engine.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository, Domain, GraphqlLocalClient, pubsub } from '@things-factory/shell'\n\nimport { Scenario } from '../service/scenario/scenario'\nimport {\n ScenarioInstance,\n ScenarioInstanceRunResult,\n ScenarioInstanceStartResult,\n ScenarioInstanceStatus\n} from '../service/scenario-instance/scenario-instance-type'\n\nimport { PendingQueue } from './pending-queue'\nimport { User } from '@things-factory/auth-base'\nimport { resolveRunnableSteps } from '../service/scenario/resolve-steps'\n\nconst debug = require('debug')('things-factory:integration-base:scenario-engine')\n\nexport class ScenarioEngine {\n private static scenarioInstances = {} // TODO impl. by WeakMap\n private static pendingQueues = {}\n\n public static getPendingQueue(domain) {\n if (!ScenarioEngine.pendingQueues[domain.id]) {\n ScenarioEngine.pendingQueues[domain.id] = new PendingQueue(queue => {\n pubsub.publish('scenario-queue-state', {\n scenarioQueueState: {\n domain,\n queue\n }\n })\n })\n }\n\n return ScenarioEngine.pendingQueues[domain.id]\n }\n\n public static getScenarioInstance(domain, instanceName): ScenarioInstance {\n return ScenarioEngine.scenarioInstances[domain.id]?.[instanceName]\n }\n\n public static getScenarioInstances(domain, scenarioName?): ScenarioInstance[] {\n const instances: ScenarioInstance[] = Object.values(ScenarioEngine.scenarioInstances[domain.id] || {}) || []\n return instances.filter(instance => !scenarioName || instance.scenarioName == scenarioName)\n }\n\n public static async load(instanceName, scenarioConfig, context?) {\n const { domain: scenarioDomain } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('load', domain.subdomain, instanceName)\n\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n scenarioInstances = ScenarioEngine.scenarioInstances[domain.id] = {}\n }\n\n var scenarioInstance = scenarioInstances[instanceName]\n if (scenarioInstance) {\n debug('load - duplicated', instanceName)\n return scenarioInstance\n }\n\n var instance = new ScenarioInstance(instanceName, scenarioConfig, {\n ...context,\n user:\n context?.user ||\n (await getRepository(User).findOne({\n where: {\n id: scenarioConfig.creatorId\n }\n })),\n client: GraphqlLocalClient.client,\n disposer: () => {\n this.unload(domain, instanceName)\n }\n })\n\n instance.start().catch(err => {\n /* IMPORTANT. if you didn't catch exception here, system will be exit */\n console.error(\n `An exception occurred while executing the scenario instance named \"${instanceName}\" on the domain \"${domain.name}\"`,\n err\n )\n })\n\n scenarioInstances[instanceName] = instance\n\n return instance\n }\n\n public static async run(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceRunResult> {\n try {\n return await ScenarioEngine.runInstance(instanceName, scenarioConfig, context)\n } catch (err) {\n const domain = context?.domain || scenarioConfig?.domain\n console.error(\n `An exception occurred while executing the scenario instance named \"${instanceName}\" on the domain \"${domain?.name}\"`,\n err\n )\n }\n }\n\n /** Whether an instance by this name is loaded on this domain in this process. */\n public static isRunning(domainId: string, instanceName: string): boolean {\n return !!ScenarioEngine.scenarioInstances[domainId]?.[instanceName]\n }\n\n /**\n * Run a scenario across its iteration's domains **and wait**, saying what happened (ADR-0110 adoption 2, step 5).\n *\n * `loadIteration` returns at once and `run` swallows a failure, so a scheduled run could never tell the\n * scheduler's discipline that it failed. This one awaits every domain and returns the failures by domain.\n * A domain where an instance by that name is already loaded in this process is not run again and is named in\n * `alreadyRunning` — `run` would hand back the running instance and report nothing.\n */\n public static async runIteration(\n instanceName,\n scenarioConfig,\n context?\n ): Promise<{ domains: number; failed: { subdomain: string; error: string }[]; alreadyRunning: string[] }> {\n const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration)\n\n const failed: { subdomain: string; error: string }[] = []\n const alreadyRunning: string[] = []\n\n for (const target of targetDomains) {\n if (ScenarioEngine.isRunning(target.id, instanceName)) {\n alreadyRunning.push(target.subdomain)\n continue\n }\n try {\n await ScenarioEngine.runInstance(instanceName, { ...scenarioConfig, domain: target }, { ...context, domain: target })\n } catch (error) {\n failed.push({ subdomain: target.subdomain, error: String((error as any)?.message ?? error).slice(0, 300) })\n }\n }\n\n return { domains: targetDomains.length, failed, alreadyRunning }\n }\n\n /** Create and run one instance. Throws what the run throws. */\n private static async runInstance(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceRunResult> {\n const { domain: scenarioDomain } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('run', domain.subdomain, instanceName)\n\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n scenarioInstances = ScenarioEngine.scenarioInstances[domain.id] = {}\n }\n\n var scenarioInstance = scenarioInstances[instanceName]\n if (scenarioInstance) {\n debug('load - duplicated', instanceName)\n return scenarioInstance\n }\n\n var instance = new ScenarioInstance(instanceName, scenarioConfig, {\n ...context,\n user:\n context?.user ||\n (await getRepository(User).findOne({\n where: {\n id: scenarioConfig.creatorId\n }\n })),\n client: GraphqlLocalClient.client,\n disposer: () => {\n this.unload(domain, instanceName)\n }\n })\n\n scenarioInstances[instanceName] = instance\n\n return await instance.run()\n }\n\n /**\n * Loads and executes a scenario across multiple domains based on iteration\n * @param instanceName - The name for the scenario instance\n * @param scenarioConfig - The scenario configuration\n * @param context - The execution context\n * @returns Promise that resolves immediately, but scenarios execute sequentially\n */\n public static async loadIteration(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceStartResult> {\n const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('load-iteration', domain.subdomain, instanceName, iteration)\n\n // Get target domains based on iteration\n const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration)\n\n // Start sequential execution in background\n ScenarioEngine.executeSequentially(instanceName, scenarioConfig, context, targetDomains)\n\n // Return immediately with execution info\n return {\n state: ScenarioInstanceStatus.STARTED,\n message: `Scenario iteration started for ${targetDomains.length} domains`,\n timestamp: new Date()\n }\n }\n\n /**\n * Executes scenarios sequentially across domains\n * @param instanceName - The name for the scenario instance\n * @param scenarioConfig - The scenario configuration\n * @param context - The execution context\n * @param targetDomains - Array of target domains\n */\n private static async executeSequentially(instanceName, scenarioConfig, context, targetDomains) {\n const results = []\n\n for (let i = 0; i < targetDomains.length; i++) {\n const targetDomain = targetDomains[i]\n\n try {\n console.log(`Executing scenario on domain ${targetDomain.subdomain} (${i + 1}/${targetDomains.length})`)\n\n const result = await ScenarioEngine.run(\n instanceName,\n {\n ...scenarioConfig,\n domain: targetDomain\n },\n {\n ...context,\n domain: targetDomain\n }\n )\n\n results.push(result)\n\n console.log(`Completed scenario on domain ${targetDomain.subdomain}`)\n } catch (error) {\n console.error(`Failed to execute scenario on domain ${targetDomain.subdomain}:`, error)\n }\n }\n\n return results[0]\n }\n\n /**\n * Gets target domains based on iteration\n * @param domain - The source domain\n * @param iteration - The iteration (SELF, CHILDREN, SELF & CHILDREN)\n * @returns Array of target domains\n */\n private static async getTargetDomains(domain: any, iteration: string): Promise<any[]> {\n switch (iteration) {\n case 'SELF':\n return [domain]\n\n case 'CHILDREN':\n const childDomains = await getRepository(Domain).find({\n where: { parent: { id: domain.id } }\n })\n return childDomains\n\n case 'SELF & CHILDREN':\n const allDomains = await getRepository(Domain).find({\n where: [{ id: domain.id }, { parent: { id: domain.id } }]\n })\n return allDomains\n\n default:\n return [domain]\n }\n }\n\n public static async unload(domain, instanceName) {\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n return\n }\n\n var instance = scenarioInstances[instanceName]\n if (!instance) {\n return\n }\n\n delete scenarioInstances[instanceName]\n\n instance.unload()\n }\n\n // will be deprecated soon\n public static async loadAll() {\n const SCENARIOS = await getRepository(Scenario).find({\n where: { active: true },\n relations: ['domain']\n })\n\n debug(\n 'load-all',\n SCENARIOS.map(scenario => `${scenario.domain.subdomain}: ${scenario.name}`)\n )\n\n // [버전관리] 부팅 시 자동 실행은 운영 실행이므로 released 스냅샷을 사용(없으면 draft 폴백).\n for (const scenario of SCENARIOS) {\n scenario.steps = await resolveRunnableSteps(scenario, { mode: 'released' })\n ScenarioEngine.loadIteration(scenario.name, scenario)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scenario-engine.js","sourceRoot":"","sources":["../../server/engine/scenario-engine.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository, Domain, GraphqlLocalClient, pubsub } from '@things-factory/shell'\n\nimport { Scenario } from '../service/scenario/scenario'\nimport {\n ScenarioInstance,\n ScenarioInstanceRunResult,\n ScenarioInstanceStartResult,\n ScenarioInstanceStatus\n} from '../service/scenario-instance/scenario-instance-type'\n\nimport { PendingQueue } from './pending-queue'\nimport { IntegrationRuntime } from './integration-runtime'\nimport { User } from '@things-factory/auth-base'\nimport { resolveRunnableSteps } from '../service/scenario/resolve-steps'\n\nconst debug = require('debug')('things-factory:integration-base:scenario-engine')\n\nexport class ScenarioEngine {\n private static scenarioInstances = {} // TODO impl. by WeakMap\n private static pendingQueues = {}\n\n public static getPendingQueue(domain) {\n if (!ScenarioEngine.pendingQueues[domain.id]) {\n ScenarioEngine.pendingQueues[domain.id] = new PendingQueue(queue => {\n pubsub.publish('scenario-queue-state', {\n scenarioQueueState: {\n domain,\n queue\n }\n })\n })\n }\n\n return ScenarioEngine.pendingQueues[domain.id]\n }\n\n public static getScenarioInstance(domain, instanceName): ScenarioInstance {\n return ScenarioEngine.scenarioInstances[domain.id]?.[instanceName]\n }\n\n public static getScenarioInstances(domain, scenarioName?): ScenarioInstance[] {\n const instances: ScenarioInstance[] = Object.values(ScenarioEngine.scenarioInstances[domain.id] || {}) || []\n return instances.filter(instance => !scenarioName || instance.scenarioName == scenarioName)\n }\n\n public static async load(instanceName, scenarioConfig, context?) {\n IntegrationRuntime.assertEnabled(`scenario \"${instanceName}\"`)\n const { domain: scenarioDomain } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('load', domain.subdomain, instanceName)\n\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n scenarioInstances = ScenarioEngine.scenarioInstances[domain.id] = {}\n }\n\n var scenarioInstance = scenarioInstances[instanceName]\n if (scenarioInstance) {\n debug('load - duplicated', instanceName)\n return scenarioInstance\n }\n\n var instance = new ScenarioInstance(instanceName, scenarioConfig, {\n ...context,\n user:\n context?.user ||\n (await getRepository(User).findOne({\n where: {\n id: scenarioConfig.creatorId\n }\n })),\n client: GraphqlLocalClient.client,\n disposer: () => {\n this.unload(domain, instanceName)\n }\n })\n\n instance.start().catch(err => {\n /* IMPORTANT. if you didn't catch exception here, system will be exit */\n console.error(\n `An exception occurred while executing the scenario instance named \"${instanceName}\" on the domain \"${domain.name}\"`,\n err\n )\n })\n\n scenarioInstances[instanceName] = instance\n\n return instance\n }\n\n public static async run(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceRunResult> {\n try {\n return await ScenarioEngine.runInstance(instanceName, scenarioConfig, context)\n } catch (err) {\n const domain = context?.domain || scenarioConfig?.domain\n console.error(\n `An exception occurred while executing the scenario instance named \"${instanceName}\" on the domain \"${domain?.name}\"`,\n err\n )\n }\n }\n\n /** Whether an instance by this name is loaded on this domain in this process. */\n public static isRunning(domainId: string, instanceName: string): boolean {\n return !!ScenarioEngine.scenarioInstances[domainId]?.[instanceName]\n }\n\n /**\n * Run a scenario across its iteration's domains **and wait**, saying what happened (ADR-0110 adoption 2, step 5).\n *\n * `loadIteration` returns at once and `run` swallows a failure, so a scheduled run could never tell the\n * scheduler's discipline that it failed. This one awaits every domain and returns the failures by domain.\n * A domain where an instance by that name is already loaded in this process is not run again and is named in\n * `alreadyRunning` — `run` would hand back the running instance and report nothing.\n */\n public static async runIteration(\n instanceName,\n scenarioConfig,\n context?\n ): Promise<{ domains: number; failed: { subdomain: string; error: string }[]; alreadyRunning: string[] }> {\n IntegrationRuntime.assertEnabled(`scenario \"${instanceName}\"`)\n const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration)\n\n const failed: { subdomain: string; error: string }[] = []\n const alreadyRunning: string[] = []\n\n for (const target of targetDomains) {\n if (ScenarioEngine.isRunning(target.id, instanceName)) {\n alreadyRunning.push(target.subdomain)\n continue\n }\n try {\n await ScenarioEngine.runInstance(instanceName, { ...scenarioConfig, domain: target }, { ...context, domain: target })\n } catch (error) {\n failed.push({ subdomain: target.subdomain, error: String((error as any)?.message ?? error).slice(0, 300) })\n }\n }\n\n return { domains: targetDomains.length, failed, alreadyRunning }\n }\n\n /** Create and run one instance. Throws what the run throws. */\n private static async runInstance(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceRunResult> {\n const { domain: scenarioDomain } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('run', domain.subdomain, instanceName)\n\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n scenarioInstances = ScenarioEngine.scenarioInstances[domain.id] = {}\n }\n\n var scenarioInstance = scenarioInstances[instanceName]\n if (scenarioInstance) {\n debug('load - duplicated', instanceName)\n return scenarioInstance\n }\n\n var instance = new ScenarioInstance(instanceName, scenarioConfig, {\n ...context,\n user:\n context?.user ||\n (await getRepository(User).findOne({\n where: {\n id: scenarioConfig.creatorId\n }\n })),\n client: GraphqlLocalClient.client,\n disposer: () => {\n this.unload(domain, instanceName)\n }\n })\n\n scenarioInstances[instanceName] = instance\n\n return await instance.run()\n }\n\n /**\n * Loads and executes a scenario across multiple domains based on iteration\n * @param instanceName - The name for the scenario instance\n * @param scenarioConfig - The scenario configuration\n * @param context - The execution context\n * @returns Promise that resolves immediately, but scenarios execute sequentially\n */\n public static async loadIteration(instanceName, scenarioConfig, context?): Promise<ScenarioInstanceStartResult> {\n IntegrationRuntime.assertEnabled(`scenario \"${instanceName}\"`)\n const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig\n const domain = context?.domain || scenarioDomain\n\n debug('load-iteration', domain.subdomain, instanceName, iteration)\n\n // Get target domains based on iteration\n const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration)\n\n // Start sequential execution in background\n ScenarioEngine.executeSequentially(instanceName, scenarioConfig, context, targetDomains)\n\n // Return immediately with execution info\n return {\n state: ScenarioInstanceStatus.STARTED,\n message: `Scenario iteration started for ${targetDomains.length} domains`,\n timestamp: new Date()\n }\n }\n\n /**\n * Executes scenarios sequentially across domains\n * @param instanceName - The name for the scenario instance\n * @param scenarioConfig - The scenario configuration\n * @param context - The execution context\n * @param targetDomains - Array of target domains\n */\n private static async executeSequentially(instanceName, scenarioConfig, context, targetDomains) {\n const results = []\n\n for (let i = 0; i < targetDomains.length; i++) {\n const targetDomain = targetDomains[i]\n\n try {\n console.log(`Executing scenario on domain ${targetDomain.subdomain} (${i + 1}/${targetDomains.length})`)\n\n const result = await ScenarioEngine.run(\n instanceName,\n {\n ...scenarioConfig,\n domain: targetDomain\n },\n {\n ...context,\n domain: targetDomain\n }\n )\n\n results.push(result)\n\n console.log(`Completed scenario on domain ${targetDomain.subdomain}`)\n } catch (error) {\n console.error(`Failed to execute scenario on domain ${targetDomain.subdomain}:`, error)\n }\n }\n\n return results[0]\n }\n\n /**\n * Gets target domains based on iteration\n * @param domain - The source domain\n * @param iteration - The iteration (SELF, CHILDREN, SELF & CHILDREN)\n * @returns Array of target domains\n */\n private static async getTargetDomains(domain: any, iteration: string): Promise<any[]> {\n switch (iteration) {\n case 'SELF':\n return [domain]\n\n case 'CHILDREN':\n const childDomains = await getRepository(Domain).find({\n where: { parent: { id: domain.id } }\n })\n return childDomains\n\n case 'SELF & CHILDREN':\n const allDomains = await getRepository(Domain).find({\n where: [{ id: domain.id }, { parent: { id: domain.id } }]\n })\n return allDomains\n\n default:\n return [domain]\n }\n }\n\n public static async unload(domain, instanceName) {\n var scenarioInstances = ScenarioEngine.scenarioInstances[domain.id]\n if (!scenarioInstances) {\n return\n }\n\n var instance = scenarioInstances[instanceName]\n if (!instance) {\n return\n }\n\n delete scenarioInstances[instanceName]\n\n instance.unload()\n }\n\n // will be deprecated soon\n public static async loadAll() {\n const SCENARIOS = await getRepository(Scenario).find({\n where: { active: true },\n relations: ['domain']\n })\n\n debug(\n 'load-all',\n SCENARIOS.map(scenario => `${scenario.domain.subdomain}: ${scenario.name}`)\n )\n\n // [버전관리] 부팅 시 자동 실행은 운영 실행이므로 released 스냅샷을 사용(없으면 draft 폴백).\n for (const scenario of SCENARIOS) {\n scenario.steps = await resolveRunnableSteps(scenario, { mode: 'released' })\n ScenarioEngine.loadIteration(scenario.name, scenario)\n }\n }\n}\n"]}
|
|
@@ -18,6 +18,16 @@ export interface Connector {
|
|
|
18
18
|
* 기반 connector 가 true 로 선언.
|
|
19
19
|
*/
|
|
20
20
|
sessionExclusive?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Connects when a task first asks, never at boot — a property of the connector, not of a connection row
|
|
23
|
+
* (ADR-0110, ruling 2026-10-05). For a connector that signs in to someone else's system: a process restart does
|
|
24
|
+
* not log in, and an account added after boot is read at that first use (the connection's parameters are
|
|
25
|
+
* resolved then), so it runs without a restart. Once made, the instance — its session — is used again; a task
|
|
26
|
+
* does not sign in per call.
|
|
27
|
+
*
|
|
28
|
+
* Default false: every other connector still connects at boot.
|
|
29
|
+
*/
|
|
30
|
+
connectsOnDemand?: boolean;
|
|
21
31
|
/**
|
|
22
32
|
* 이 connector 의 자연스러운 inheritance default — 자식 도메인이 부모의 Connection
|
|
23
33
|
* 정의를 inherit 할 때 어떻게 동작하는 게 자연인지를 connector 작성자가 선언.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/engine/types.ts"],"names":[],"mappings":"","sourcesContent":["import { Connection, PropertySpec, ScenarioInstance, ScenarioInstanceStatus, Step } from '../service'\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nexport interface Connector {\n ready(connections: Connection[]): Promise<any>\n connect(connection: Connection): Promise<any>\n disconnect(connection: Connection): Promise<any>\n parameterSpec: PropertySpec[]\n taskPrefixes?: string[]\n description?: string\n help?: string\n /**\n * 외부 세션 (로그인 상태) 을 보유한 connector 인지 여부.\n * true 이면 ConnectionManager.withSession 이 connection name 단위 mutex 로\n * 동시 acquireSessionPage 호출을 직렬화 — 같은 외부 계정의 중복 로그인 충돌 방지.\n *\n * 기본값 false (stateless 또는 자체 세션 관리). HeadlessConnector 등 로그인 세션\n * 기반 connector 가 true 로 선언.\n */\n sessionExclusive?: boolean\n\n /**\n * 이 connector 의 자연스러운 inheritance default — 자식 도메인이 부모의 Connection\n * 정의를 inherit 할 때 어떻게 동작하는 게 자연인지를 connector 작성자가 선언.\n *\n * 값은 string literal 'isolate' | 'share' — runtime 호환을 위해 string 으로\n * 비교 (Connection.inheritanceMode column 의 값과 직접 비교 가능하도록).\n *\n * **결정 가이드** (connector 작성자용):\n * 1. 외부 자원이 도메인(테넌트)별로 다른 identity·자격증명·세션이 필요한가?\n * - 예 (stateful + per-tenant: 로그인 connector, 테넌트별 DB pool) → 'isolate'\n * - 아니오 (공용 자원: 단일 브로커·서버 / stateless API) → 'share'\n *\n * 2. 자식 도메인이 부모의 cookies·세션·params 를 그대로 봐도 되는가?\n * - 안 됨 (자료 격리·자격증명 격리 필요) → 'isolate'\n * - 됨 (모두 같은 자원 공유) → 'share'\n *\n * 3. 명시 안 하면 framework 가 ISOLATE 폴백 (격리 우선). 자원 효율이 중요한\n * connector 작성자는 'share' 를 명시 선언.\n *\n * Connection.inheritanceMode 가 명시되면 그것이 우선. 운영자가 특수 케이스를\n * 위해 connector default 를 override 할 수 있음.\n */\n inheritanceMode?: 'isolate' | 'share'\n}\n\nexport type Context = {\n /**\n * Represents the domain context.\n */\n domain: Domain\n\n /**\n * User information.\n */\n user: User\n\n /**\n * Language code, for example 'en', 'ko'.\n */\n lng: string\n\n /**\n * Flag to indicate if the IP is unsafe, can be undefined.\n */\n unsafeIP: boolean | undefined\n\n /**\n * List of prohibited privileges, can be undefined.\n */\n prohibitedPrivileges: { category: string; privilege: string }[] | undefined\n\n /**\n * Logger for logging purposes.\n */\n logger: any\n\n /**\n * Function to publish events or messages.\n */\n publish: Function\n\n /**\n * Function to load resources or data.\n */\n load: Function\n\n /**\n * Current status of the scenario instance.\n */\n state: ScenarioInstanceStatus\n\n /**\n * General data storage object.\n */\n data: any\n\n /**\n * Variables related to the context.\n */\n variables: Object\n\n /**\n * Local GraphQL client object.\n */\n client: any /* graphql local client */\n\n /**\n * Root object, can be used for various purposes.\n */\n root?: ScenarioInstance\n\n /**\n * Array of function closures.\n */\n closures: Function[]\n\n /**\n * Function to check the state.\n */\n checkState: Function\n\n /**\n * MQTT subscriber context object.\n */\n __mqtt_subscriber?: any\n\n /**\n * socket listener context object.\n */\n __socket_listener?: any\n\n /**\n * csv readline context object.\n */\n __csv_resources?: any\n}\n\nexport type TaskHandler = (\n step: Step,\n context: Context\n) => Promise<{\n next?: string\n state?: ScenarioInstanceStatus\n data?: any\n}>\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/engine/types.ts"],"names":[],"mappings":"","sourcesContent":["import { Connection, PropertySpec, ScenarioInstance, ScenarioInstanceStatus, Step } from '../service'\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nexport interface Connector {\n ready(connections: Connection[]): Promise<any>\n connect(connection: Connection): Promise<any>\n disconnect(connection: Connection): Promise<any>\n parameterSpec: PropertySpec[]\n taskPrefixes?: string[]\n description?: string\n help?: string\n /**\n * 외부 세션 (로그인 상태) 을 보유한 connector 인지 여부.\n * true 이면 ConnectionManager.withSession 이 connection name 단위 mutex 로\n * 동시 acquireSessionPage 호출을 직렬화 — 같은 외부 계정의 중복 로그인 충돌 방지.\n *\n * 기본값 false (stateless 또는 자체 세션 관리). HeadlessConnector 등 로그인 세션\n * 기반 connector 가 true 로 선언.\n */\n sessionExclusive?: boolean\n\n /**\n * Connects when a task first asks, never at boot — a property of the connector, not of a connection row\n * (ADR-0110, ruling 2026-10-05). For a connector that signs in to someone else's system: a process restart does\n * not log in, and an account added after boot is read at that first use (the connection's parameters are\n * resolved then), so it runs without a restart. Once made, the instance — its session — is used again; a task\n * does not sign in per call.\n *\n * Default false: every other connector still connects at boot.\n */\n connectsOnDemand?: boolean\n\n /**\n * 이 connector 의 자연스러운 inheritance default — 자식 도메인이 부모의 Connection\n * 정의를 inherit 할 때 어떻게 동작하는 게 자연인지를 connector 작성자가 선언.\n *\n * 값은 string literal 'isolate' | 'share' — runtime 호환을 위해 string 으로\n * 비교 (Connection.inheritanceMode column 의 값과 직접 비교 가능하도록).\n *\n * **결정 가이드** (connector 작성자용):\n * 1. 외부 자원이 도메인(테넌트)별로 다른 identity·자격증명·세션이 필요한가?\n * - 예 (stateful + per-tenant: 로그인 connector, 테넌트별 DB pool) → 'isolate'\n * - 아니오 (공용 자원: 단일 브로커·서버 / stateless API) → 'share'\n *\n * 2. 자식 도메인이 부모의 cookies·세션·params 를 그대로 봐도 되는가?\n * - 안 됨 (자료 격리·자격증명 격리 필요) → 'isolate'\n * - 됨 (모두 같은 자원 공유) → 'share'\n *\n * 3. 명시 안 하면 framework 가 ISOLATE 폴백 (격리 우선). 자원 효율이 중요한\n * connector 작성자는 'share' 를 명시 선언.\n *\n * Connection.inheritanceMode 가 명시되면 그것이 우선. 운영자가 특수 케이스를\n * 위해 connector default 를 override 할 수 있음.\n */\n inheritanceMode?: 'isolate' | 'share'\n}\n\nexport type Context = {\n /**\n * Represents the domain context.\n */\n domain: Domain\n\n /**\n * User information.\n */\n user: User\n\n /**\n * Language code, for example 'en', 'ko'.\n */\n lng: string\n\n /**\n * Flag to indicate if the IP is unsafe, can be undefined.\n */\n unsafeIP: boolean | undefined\n\n /**\n * List of prohibited privileges, can be undefined.\n */\n prohibitedPrivileges: { category: string; privilege: string }[] | undefined\n\n /**\n * Logger for logging purposes.\n */\n logger: any\n\n /**\n * Function to publish events or messages.\n */\n publish: Function\n\n /**\n * Function to load resources or data.\n */\n load: Function\n\n /**\n * Current status of the scenario instance.\n */\n state: ScenarioInstanceStatus\n\n /**\n * General data storage object.\n */\n data: any\n\n /**\n * Variables related to the context.\n */\n variables: Object\n\n /**\n * Local GraphQL client object.\n */\n client: any /* graphql local client */\n\n /**\n * Root object, can be used for various purposes.\n */\n root?: ScenarioInstance\n\n /**\n * Array of function closures.\n */\n closures: Function[]\n\n /**\n * Function to check the state.\n */\n checkState: Function\n\n /**\n * MQTT subscriber context object.\n */\n __mqtt_subscriber?: any\n\n /**\n * socket listener context object.\n */\n __socket_listener?: any\n\n /**\n * csv readline context object.\n */\n __csv_resources?: any\n}\n\nexport type TaskHandler = (\n step: Step,\n context: Context\n) => Promise<{\n next?: string\n state?: ScenarioInstanceStatus\n data?: any\n}>\n"]}
|
package/dist-server/index.d.ts
CHANGED
package/dist-server/index.js
CHANGED
|
@@ -8,7 +8,15 @@ tslib_1.__exportStar(require("./service/index.js"), exports);
|
|
|
8
8
|
require("./routes.js");
|
|
9
9
|
require("./restful/index.js"); /* APIs */
|
|
10
10
|
const index_js_1 = require("./engine/index.js");
|
|
11
|
+
const integration_runtime_js_1 = require("./engine/integration-runtime.js");
|
|
12
|
+
tslib_1.__exportStar(require("./engine/integration-runtime.js"), exports);
|
|
11
13
|
process.on('bootstrap-module-start', async ({ app, config, client }) => {
|
|
14
|
+
integration_runtime_js_1.IntegrationRuntime.markBooted();
|
|
15
|
+
if (!integration_runtime_js_1.IntegrationRuntime.enabled) {
|
|
16
|
+
/* Integration runs in another process; this one connects nothing and starts no scenario. */
|
|
17
|
+
console.log(`Scenario Engine is off in this process — ${integration_runtime_js_1.IntegrationRuntime.reason}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
12
20
|
try {
|
|
13
21
|
await index_js_1.ConnectionManager.ready();
|
|
14
22
|
await index_js_1.ScenarioEngine.loadAll();
|
package/dist-server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,gEAAqC;AACrC,4DAAiC;AACjC,iEAAsC;AACtC,6DAAkC;AAElC,uBAAoB;AACpB,8BAA2B,CAAC,UAAU;AAEtC,gDAAqE;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,gEAAqC;AACrC,4DAAiC;AACjC,iEAAsC;AACtC,6DAAkC;AAElC,uBAAoB;AACpB,8BAA2B,CAAC,UAAU;AAEtC,gDAAqE;AACrE,4EAAoE;AAEpE,0EAA+C;AAE/C,OAAO,CAAC,EAAE,CAAC,wBAA+B,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAO,EAAE,EAAE;IACjF,2CAAkB,CAAC,UAAU,EAAE,CAAA;IAC/B,IAAI,CAAC,2CAAkB,CAAC,OAAO,EAAE,CAAC;QAChC,4FAA4F;QAC5F,OAAO,CAAC,GAAG,CAAC,4CAA4C,2CAAkB,CAAC,MAAM,EAAE,CAAC,CAAA;QACpF,OAAM;IACR,CAAC;IACD,IAAI,CAAC;QACH,MAAM,4BAAiB,CAAC,KAAK,EAAE,CAAA;QAC/B,MAAM,yBAAc,CAAC,OAAO,EAAE,CAAA;QAE9B,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;IAClD,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,4BAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA2C,EAAE,EAAE,CAAC,CAAA;IACjF,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["export * from './migrations/index.js'\nexport * from './engine/index.js'\nexport * from './controllers/index.js'\nexport * from './service/index.js'\n\nimport './routes.js'\nimport './restful/index.js' /* APIs */\n\nimport { ConnectionManager, ScenarioEngine } from './engine/index.js'\nimport { IntegrationRuntime } from './engine/integration-runtime.js'\n\nexport * from './engine/integration-runtime.js'\n\nprocess.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {\n IntegrationRuntime.markBooted()\n if (!IntegrationRuntime.enabled) {\n /* Integration runs in another process; this one connects nothing and starts no scenario. */\n console.log(`Scenario Engine is off in this process — ${IntegrationRuntime.reason}`)\n return\n }\n try {\n await ConnectionManager.ready()\n await ScenarioEngine.loadAll()\n\n console.log('Scenario Engine has just started.')\n } catch (ex) {\n ConnectionManager.logger.error('Just has failed to start Scenario Engine.', ex)\n }\n})\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { startLeasedRun, type LeasedRunAnswer } from '@things-factory/task-lease';
|
|
2
|
+
import { INTEGRATION_RUNTIME_OFF } from '../engine/integration-runtime';
|
|
2
3
|
/** Scheduled runs of one scenario are not closer together than this. */
|
|
3
4
|
export declare const SCHEDULE_FLOOR_MS = 5000;
|
|
4
5
|
export declare const scheduleLeaseName: (domainId: string, scenarioId: string) => string;
|
|
@@ -6,7 +7,7 @@ export type ScheduledRunAnswer = {
|
|
|
6
7
|
ran: true;
|
|
7
8
|
} | {
|
|
8
9
|
ran: false;
|
|
9
|
-
why: 'scenario-schedule-held' | 'scenario-schedule-lease-error';
|
|
10
|
+
why: 'scenario-schedule-held' | 'scenario-schedule-lease-error' | typeof INTEGRATION_RUNTIME_OFF;
|
|
10
11
|
detail?: string;
|
|
11
12
|
};
|
|
12
13
|
export interface ScheduledRunLog {
|
|
@@ -5,6 +5,7 @@ exports.scheduledRunAnswerOf = scheduledRunAnswerOf;
|
|
|
5
5
|
exports.startScheduledScenario = startScheduledScenario;
|
|
6
6
|
const task_lease_1 = require("@things-factory/task-lease");
|
|
7
7
|
const scenario_engine_1 = require("../engine/scenario-engine");
|
|
8
|
+
const integration_runtime_1 = require("../engine/integration-runtime");
|
|
8
9
|
/*
|
|
9
10
|
* A scheduled scenario run under the shared discipline (ADR-0110 adoption 2, step 5, 보탬 3).
|
|
10
11
|
*
|
|
@@ -40,6 +41,14 @@ function scheduledRunAnswerOf(answer) {
|
|
|
40
41
|
/** Start the scheduled run of one scenario, or say why not. The run itself goes on after this returns. */
|
|
41
42
|
async function startScheduledScenario(scenario, config, context, log, start = task_lease_1.startLeasedRun) {
|
|
42
43
|
const where = `${context.domain.subdomain}:${scenario.name}`;
|
|
44
|
+
/*
|
|
45
|
+
* Integration does not run in this process. Answered like any refusal — 200 with the reason — so the
|
|
46
|
+
* scheduler does not retry it every minute; the callback address belongs on the process where it runs.
|
|
47
|
+
*/
|
|
48
|
+
if (!integration_runtime_1.IntegrationRuntime.enabled) {
|
|
49
|
+
log.warn(`[scenario-schedule] "${where}" was called back on a process where integration is off — ${integration_runtime_1.IntegrationRuntime.reason}`);
|
|
50
|
+
return { ran: false, why: integration_runtime_1.INTEGRATION_RUNTIME_OFF, detail: integration_runtime_1.IntegrationRuntime.reason };
|
|
51
|
+
}
|
|
43
52
|
const answer = await start({
|
|
44
53
|
name: (0, exports.scheduleLeaseName)(context.domain.id, String(scenario.id)),
|
|
45
54
|
floorMs: exports.SCHEDULE_FLOOR_MS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario-schedule-run.js","sourceRoot":"","sources":["../../server/routers/scenario-schedule-run.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"scenario-schedule-run.js","sourceRoot":"","sources":["../../server/routers/scenario-schedule-run.ts"],"names":[],"mappings":"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","sourcesContent":["import { startLeasedRun, type LeasedRunAnswer } from '@things-factory/task-lease'\n\nimport { ScenarioEngine } from '../engine/scenario-engine'\nimport { IntegrationRuntime, INTEGRATION_RUNTIME_OFF } from '../engine/integration-runtime'\n\n/*\n * A scheduled scenario run under the shared discipline (ADR-0110 adoption 2, step 5, 보탬 3).\n *\n * The scheduler calls back on whichever instance sits behind its URL, on the cron's beat, whether or not the last\n * run has ended. Before this, every callback started a run: two instances both ran it, a run still going was\n * silently joined (the engine handed back the running instance and said STARTED), and a failing run was started\n * again on the next beat at full rate.\n *\n * one at a time lease `scenario-schedule:<domain>:<scenario>`; a callback that finds it held is refused\n * floor after a run, callbacks closer than SCHEDULE_FLOOR_MS are refused\n * backoff after a failed run, callbacks are refused for the backoff of the consecutive failures\n *\n * A refusal answers 200 with its reason. The scheduler retries any other answer every 60 seconds without end\n * (`retry_dlq`, `max_retry_count: -1`), which would turn one refused overlap into a stream of them.\n *\n * Only the scheduled path takes the lease. A person starting the scenario by hand, and the boot-time load, keep\n * the engine's own in-process rule; a scheduled run that finds such an instance loaded here says so.\n */\n\n/** Scheduled runs of one scenario are not closer together than this. */\nexport const SCHEDULE_FLOOR_MS = 5_000\n\nexport const scheduleLeaseName = (domainId: string, scenarioId: string) => `scenario-schedule:${domainId}:${scenarioId}`\n\nexport type ScheduledRunAnswer =\n | { ran: true }\n | { ran: false; why: 'scenario-schedule-held' | 'scenario-schedule-lease-error' | typeof INTEGRATION_RUNTIME_OFF; detail?: string }\n\nexport interface ScheduledRunLog {\n info: (what: string) => void\n warn: (what: string) => void\n error: (what: string) => void\n}\n\n/** What a leased-run answer means to the scheduler. */\nexport function scheduledRunAnswerOf(answer: LeasedRunAnswer): ScheduledRunAnswer {\n if (answer.started) return { ran: true }\n /* This package has strictNullChecks off, so `started` does not narrow the union by itself. */\n const refused = answer as Extract<LeasedRunAnswer, { started: false }>\n return refused.why === 'held'\n ? { ran: false, why: 'scenario-schedule-held' }\n : { ran: false, why: 'scenario-schedule-lease-error', detail: (refused as { error?: string }).error }\n}\n\n/** Start the scheduled run of one scenario, or say why not. The run itself goes on after this returns. */\nexport async function startScheduledScenario(\n scenario: { id?: string; name?: string },\n config: any,\n context: { domain: { id: string; subdomain: string }; user?: unknown },\n log: ScheduledRunLog,\n start: typeof startLeasedRun = startLeasedRun\n): Promise<ScheduledRunAnswer> {\n const where = `${context.domain.subdomain}:${scenario.name}`\n\n /*\n * Integration does not run in this process. Answered like any refusal — 200 with the reason — so the\n * scheduler does not retry it every minute; the callback address belongs on the process where it runs.\n */\n if (!IntegrationRuntime.enabled) {\n log.warn(`[scenario-schedule] \"${where}\" was called back on a process where integration is off — ${IntegrationRuntime.reason}`)\n return { ran: false, why: INTEGRATION_RUNTIME_OFF, detail: IntegrationRuntime.reason }\n }\n\n const answer = await start({\n name: scheduleLeaseName(context.domain.id, String(scenario.id)),\n floorMs: SCHEDULE_FLOOR_MS,\n work: async () => {\n const result = await ScenarioEngine.runIteration(scenario.name, config, context)\n if (result.alreadyRunning.length) {\n log.warn(\n `[scenario-schedule] \"${where}\" is already loaded on ${result.alreadyRunning.join(', ')} in this process — not started again`\n )\n }\n if (result.failed.length) {\n throw new Error(result.failed.map(one => `${one.subdomain}: ${one.error}`).join(' · '))\n }\n },\n onFailure: f =>\n log.error(\n `[scenario-schedule] \"${where}\" failed (${f.consecutive} in a row) — scheduled runs rest ${Math.round(f.restMs / 1000)}s — ${f.error}`\n ),\n onRecovered: r => log.info(`[scenario-schedule] \"${where}\" ran again after ${r.afterFailures} failed run(s)`),\n onLeaseError: e => log.error(`[scenario-schedule] \"${where}\" lease \"${e.name}\" — ${e.error}`)\n })\n\n const said = scheduledRunAnswerOf(answer)\n if (said.ran === false && said.why === 'scenario-schedule-held') {\n log.warn(`[scenario-schedule] \"${where}\" refused: still running, or resting after its last run`)\n }\n return said\n}\n"]}
|
|
@@ -8,6 +8,7 @@ const auth_base_1 = require("@things-factory/auth-base");
|
|
|
8
8
|
const env_1 = require("@things-factory/env");
|
|
9
9
|
const shell_1 = require("@things-factory/shell");
|
|
10
10
|
const engine_1 = require("../../engine");
|
|
11
|
+
const integration_runtime_1 = require("../../engine/integration-runtime");
|
|
11
12
|
const proxy_connector_1 = require("../../engine/connector/proxy-connector");
|
|
12
13
|
const domain_inheritance_1 = require("../../utils/domain-inheritance");
|
|
13
14
|
const ORMCONFIG = env_1.config.get('ormconfig', {});
|
|
@@ -45,6 +46,8 @@ let Connection = class Connection {
|
|
|
45
46
|
*
|
|
46
47
|
*/
|
|
47
48
|
async connect() {
|
|
49
|
+
/* Every connect — at boot, from the screen, from a scenario — comes through here. */
|
|
50
|
+
integration_runtime_1.IntegrationRuntime.assertEnabled(`connecting "${this.name}"`);
|
|
48
51
|
const { type, edge } = this;
|
|
49
52
|
const connector = edge ? proxy_connector_1.ProxyConnector.instance : engine_1.ConnectionManager.getConnector(type);
|
|
50
53
|
await connector.connect({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../../server/service/connection/connection.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, ObjectType, registerEnumType } from 'type-graphql'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\n\nimport { User, Appliance } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\nimport { Domain, EnvVar, ScalarObject, encryptJsonTransformer, getRepository } from '@things-factory/shell'\n\nimport { ConnectionManager } from '../../engine'\nimport { ProxyConnector } from '../../engine/connector/proxy-connector'\nimport { lookupDomainEnvVar } from '../../utils/domain-inheritance'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\nexport enum ConnectionStatus {\n CONNECTED = 'CONNECTED', // Represents an active, established connection.\n DISCONNECTED = 'DISCONNECTED' // Represents a terminated or inactive connection.\n}\n\nregisterEnumType(ConnectionStatus, {\n name: 'ConnectionStatus',\n description: 'Enumeration of possible states for a connection.'\n})\n\n/**\n * 자식 도메인이 부모의 Connection 정의를 inherit 할 때 동작.\n * - ISOLATE: 자식별로 새 instance 생성. cookies/세션·EnvVar 자격증명 격리. HeadlessConnector\n * 같은 stateful connector 의 자연 default.\n * - SHARE: 부모의 단일 instance 를 자식들이 공유. cookies/params 공통. 공용 외부 API\n * (stateless) connector 의 자연 default.\n *\n * null 이면 Connector.inheritanceMode default 적용, 그것도 없으면 ISOLATE.\n */\nexport enum ConnectionInheritanceMode {\n ISOLATE = 'isolate',\n SHARE = 'share'\n}\n\nregisterEnumType(ConnectionInheritanceMode, {\n name: 'ConnectionInheritanceMode',\n description:\n 'Child domain inheritance mode — ISOLATE: per-child instance with isolated session/credentials. SHARE: single parent instance reused by all children.'\n})\n\n@Entity()\n@Index('ix_connection_0', (connection: Connection) => [connection.domain, connection.name], { unique: true })\n@ObjectType({ description: 'Represents a configured connection to an external system or service.' })\nexport class Connection {\n /**\n * Unique identifier for the connection, generated in UUID format.\n */\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for the connection.' })\n readonly id: string\n\n /**\n * Many-to-One relationship with the Domain entity.\n */\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain to which this connection belongs.' })\n domain: Domain\n\n /**\n * Stores the ID of the associated Domain.\n */\n @RelationId((connection: Connection) => connection.domain)\n domainId: string\n\n /**\n * The name of the connection.\n */\n @Column()\n @Field({ description: 'The name of the connection.' })\n name: string\n\n /**\n * Optional description for the connection.\n */\n @Column({\n nullable: true\n })\n @Field({ nullable: true, description: 'A detailed description of the connection.' })\n description: string\n\n /**\n * The type of the connection.\n */\n @Column()\n @Field({ nullable: true, description: 'The type of the connection (e.g., tcp, http, mqtt).' })\n type: string\n\n /**\n * Many-to-One relationship with the Appliance entity which delegate the connection. Optional field.\n */\n @ManyToOne(type => Appliance, { nullable: true })\n @Field({ nullable: true, description: 'The edge appliance that delegates this connection, if any.' })\n edge: Appliance\n\n /**\n * Stores the ID of the Appliance who delegate the connection.\n */\n @RelationId((connection: Connection) => connection.edge)\n edgeId: string\n\n /**\n * The endpoint for the connection.\n */\n @Column()\n @Field({ nullable: true, description: 'The endpoint URL or address for the connection.' })\n endpoint: string\n\n /**\n * Whether to automatically connect when the application starts\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description: 'Whether to automatically connect when the application starts'\n })\n active: boolean\n\n /**\n * Indicates whether the connection should be created on-demand when needed.\n */\n @Column({ default: false })\n @Field({\n nullable: true,\n description: 'When true, connection is created on-demand when needed and cleaned up after use.'\n })\n onDemand: boolean\n\n /**\n * 자식 도메인이 부모 도메인의 Connection 정의를 inherit 할 때의 인스턴스 정책.\n *\n * - `ISOLATE`: 자식 도메인마다 새 인스턴스 생성. cookies/세션 상태와 useDomainAttribute\n * 기반 자격증명이 자식 도메인 단위로 격리됨. 자식별 다른 자격증명·세션이 필요한 stateful\n * connector (예: HeadlessConnector — Allbaro / KISCON 등) 의 자연 default.\n *\n * - `SHARE`: 자식 도메인이 부모의 단일 인스턴스를 그대로 공유. cookies/세션/params 모두\n * 부모 인스턴스의 것이 자식에도 그대로 적용. useDomainAttribute 기반 자식 EnvVar\n * override 는 무효화됨. 공용 자원 (단일 브로커·서버) 또는 stateless connector\n * (HTTP, GraphQL) 의 자연 default.\n *\n * - `null`: Connector 가 선언한 default 적용. Connector 도 default 가 없으면 ISOLATE\n * 폴백 (격리·안전 우선).\n *\n * 우선순위: Connection.inheritanceMode → Connector.inheritanceMode → ISOLATE.\n *\n * **호환성**: 본 컬럼은 platform v10 시점에 도입. 그 이전에 만들어진 모든 Connection\n * 레코드는 NULL 값으로 들어가 위 폴백 체인에 따라 ISOLATE 가 적용되어 기존 거동\n * (자식별 격리 인스턴스 생성) 이 그대로 유지됨. SHARE 는 명시적으로 켜야만 발동되는\n * opt-in.\n *\n * **데이터 격리 주의**: SHARE 를 켤 경우 자식 도메인들은 같은 인스턴스의 cookies/세션을\n * 공유함. 자식 도메인별로 다른 외부 자격증명을 사용하려는 경우 SHARE 는 부적절.\n */\n @Column({ type: 'varchar', nullable: true })\n @Field(type => ConnectionInheritanceMode, {\n nullable: true,\n description:\n 'Inheritance mode for child domains. ISOLATE = per-child instance with isolated session. SHARE = shared parent instance. null = use Connector default (falls back to ISOLATE).'\n })\n inheritanceMode?: ConnectionInheritanceMode\n\n /**\n * The status of the connection, using the ConnectionStatus type.\n */\n @Field({ nullable: true, description: 'The current status of the connection (e.g., CONNECTED, DISCONNECTED).' })\n state: ConnectionStatus\n\n /**\n * Additional parameters for the connection, stored as a JSON string.\n *\n * [Caution]\n * 이 컬럼타입은 postgres 데이터베이스에서는 varchar 타입을 유지한다.\n * 이는 데이터베이스 타입을 변경하면 기존 데이터가 손실될 수 있기 때문이다.\n */\n // 비밀값은 이 칸에 두지 않는다 (ADR-0112). spec 이 `type: 'secret'` 인 칸은 쓰기에서 거절되고\n // EnvVar `Connection::<name>::<param>`(늘 암호문) 에 둔다. 읽기에서도 빠진다 — connection-secrets.ts.\n // 그래서 이 칸 전체를 암호화하지 않는다.\n @Column({\n type: DATABASE_TYPE == 'postgres' ? 'varchar' : 'simple-json',\n nullable: true,\n transformer:\n DATABASE_TYPE == 'postgres'\n ? {\n to: (value: any) => JSON.stringify(value),\n from: (value: string) => {\n try {\n return JSON.parse(value)\n } catch (error) {\n return null\n }\n }\n }\n : undefined\n })\n @Field(type => ScalarObject, { nullable: true, description: 'A key-value map of parameters for the connection.' })\n params: { [key: string]: any }\n\n /**\n * The date and time when the connection was created.\n */\n @CreateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the connection was created.' })\n createdAt: Date\n\n /**\n * The date and time when the connection was last updated.\n */\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the connection was last updated.' })\n updatedAt: Date\n\n /**\n * Many-to-One relationship with the User entity who created the connection. Optional field.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true, description: 'The user who created the connection.' })\n creator: User\n\n /**\n * Stores the ID of the User who created the connection.\n */\n @RelationId((connection: Connection) => connection.creator)\n creatorId: string\n\n /**\n * Many-to-One relationship with the User entity who last updated the connection.\n * Optional field.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true, description: 'The user who last updated the connection.' })\n updater: User\n\n /**\n * Stores the ID of the User who last updated the connection.\n *\n */\n @RelationId((connection: Connection) => connection.updater)\n updaterId: string\n\n /**\n * Runtime marker — DB 컬럼이 아니라 메모리 전용 플래그.\n *\n * 자식 도메인이 자기 Connection 레코드 없이 부모의 정의를 inherit 할 때,\n * `ConnectionManager.getConnectionEntityByName` 의 parent fallback 분기가\n * 부모 entity 의 clone 에 이 marker (= 부모의 domainId) 를 부착한다.\n *\n * `getConnectionInstanceByName` 가 이 marker 가 truthy 인 entity 를 보면\n * onDemand 플래그와 동일하게 자식 도메인용 instance 를 자동 생성한다 — 운영자가\n * 부모 record 의 onDemand 를 별도로 켤 필요 없이 inheritance 가 자연스럽게 동작.\n *\n * inheritanceMode = ISOLATE 인 경우에만 부착.\n */\n __inheritedFrom?: string\n\n /**\n * Runtime marker — DB 컬럼이 아니라 메모리 전용 플래그. SHARE 모드의 짝.\n *\n * 자식 도메인이 부모의 Connection 정의를 inherit 할 때, inheritanceMode = SHARE 면\n * 자식이 부모의 단일 instance 를 그대로 사용. parent fallback 분기가 부모 entity 를\n * 그대로 반환하면서 이 marker (= 부모의 domainId) 를 부착.\n *\n * `getConnectionInstanceByName` 가 이 marker 를 보면 자식 instance 자동 생성 없이\n * `connections[__sharedFrom][name]` 를 그대로 반환 — 부모·자식이 같은 instance.\n *\n * inheritanceMode = SHARE 인 경우에만 부착. `__inheritedFrom` 과 mutually exclusive.\n */\n __sharedFrom?: string\n\n /**\n * Asynchronous method to establish the connection.\n *\n */\n async connect() {\n const { type, edge } = this\n const connector = edge ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n\n await connector.connect({\n ...this,\n params: await this.getResolvedParameters() // 🔐 해결된 파라미터 사용\n })\n }\n\n /**\n * @brief Asynchronous method to disconnect the connection.\n *\n */\n async disconnect() {\n try {\n const { type, edge } = this\n const connector = edge ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n await connector.disconnect(this)\n } finally {\n }\n }\n\n /**\n * Gets a parameter value with EnvVar override support.\n *\n * **Priority (`useDomainAttribute: true` 인 필드)**:\n * 1. **EnvVar** — ancestor closest-wins (자식 도메인이 가장 가까움)\n * 2. **params** 명시값 (parent Connection 의 default 역할)\n * 3. defaultValue\n *\n * **Priority (`useDomainAttribute: false` 또는 미지정 필드)**:\n * 1. **params** 명시값\n * 2. defaultValue\n *\n * 의미론: `useDomainAttribute: true` 는 \"이 필드는 도메인별 override 가능\" 선언.\n * 따라서 EnvVar 가 본 소스, params 는 모든 자손에 동일 적용되는 default.\n * 자식 도메인 EnvVar override 가 자연스럽게 동작 (시공사 → 프로젝트 자격증명 차별화 등).\n *\n * @param key - The parameter key\n * @param defaultValue - Optional default value\n * @returns The parameter value or default value\n */\n async getParameter(key: string, defaultValue?: any): Promise<any> {\n const connector = ConnectionManager.getConnector(this.type)\n const paramSpec = connector?.parameterSpec?.find(spec => spec.name === key)\n\n // 1. useDomainAttribute 필드는 EnvVar 우선 조회 (ancestor closest-wins)\n if (paramSpec?.useDomainAttribute) {\n const envVarKey = `Connection::${this.name}::${key}`\n const domain = await getRepository(Domain).findOne({ where: { id: this.domainId } })\n if (domain) {\n const value = await lookupDomainEnvVar(domain, envVarKey)\n if (value !== undefined) {\n // params 에 명시값이 있는데 EnvVar 가 override 한 경우 진단 로그\n const paramValue = this.params?.[key]\n if (paramValue !== null && paramValue !== undefined && paramValue !== '') {\n ConnectionManager.logger.info(\n `[domain-attr] Connection '${this.name}'.${key}: params default overridden by EnvVar (domain='${domain.subdomain || domain.id}')`\n )\n }\n return value\n }\n }\n }\n\n // 2. params 명시값 (default 역할)\n if (this.params?.[key] !== null && this.params?.[key] !== undefined && this.params?.[key] !== '') {\n return this.params[key]\n }\n\n // 3. defaultValue\n return defaultValue\n }\n\n /**\n * Gets all parameters with resolved values (params + Domain attributes).\n *\n * @returns Object with all resolved parameter values\n */\n async getResolvedParameters(): Promise<{ [key: string]: any }> {\n const resolved = {}\n\n const connector = ConnectionManager.getConnector(this.type)\n const paramSpecs = connector?.parameterSpec || []\n\n for (const spec of paramSpecs) {\n const key = spec.name\n resolved[key] = await this.getParameter(key)\n }\n\n return resolved\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../../server/service/connection/connection.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, ObjectType, registerEnumType } from 'type-graphql'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\n\nimport { User, Appliance } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\nimport { Domain, EnvVar, ScalarObject, encryptJsonTransformer, getRepository } from '@things-factory/shell'\n\nimport { ConnectionManager } from '../../engine'\nimport { IntegrationRuntime } from '../../engine/integration-runtime'\nimport { ProxyConnector } from '../../engine/connector/proxy-connector'\nimport { lookupDomainEnvVar } from '../../utils/domain-inheritance'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\nexport enum ConnectionStatus {\n CONNECTED = 'CONNECTED', // Represents an active, established connection.\n DISCONNECTED = 'DISCONNECTED' // Represents a terminated or inactive connection.\n}\n\nregisterEnumType(ConnectionStatus, {\n name: 'ConnectionStatus',\n description: 'Enumeration of possible states for a connection.'\n})\n\n/**\n * 자식 도메인이 부모의 Connection 정의를 inherit 할 때 동작.\n * - ISOLATE: 자식별로 새 instance 생성. cookies/세션·EnvVar 자격증명 격리. HeadlessConnector\n * 같은 stateful connector 의 자연 default.\n * - SHARE: 부모의 단일 instance 를 자식들이 공유. cookies/params 공통. 공용 외부 API\n * (stateless) connector 의 자연 default.\n *\n * null 이면 Connector.inheritanceMode default 적용, 그것도 없으면 ISOLATE.\n */\nexport enum ConnectionInheritanceMode {\n ISOLATE = 'isolate',\n SHARE = 'share'\n}\n\nregisterEnumType(ConnectionInheritanceMode, {\n name: 'ConnectionInheritanceMode',\n description:\n 'Child domain inheritance mode — ISOLATE: per-child instance with isolated session/credentials. SHARE: single parent instance reused by all children.'\n})\n\n@Entity()\n@Index('ix_connection_0', (connection: Connection) => [connection.domain, connection.name], { unique: true })\n@ObjectType({ description: 'Represents a configured connection to an external system or service.' })\nexport class Connection {\n /**\n * Unique identifier for the connection, generated in UUID format.\n */\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for the connection.' })\n readonly id: string\n\n /**\n * Many-to-One relationship with the Domain entity.\n */\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'The domain to which this connection belongs.' })\n domain: Domain\n\n /**\n * Stores the ID of the associated Domain.\n */\n @RelationId((connection: Connection) => connection.domain)\n domainId: string\n\n /**\n * The name of the connection.\n */\n @Column()\n @Field({ description: 'The name of the connection.' })\n name: string\n\n /**\n * Optional description for the connection.\n */\n @Column({\n nullable: true\n })\n @Field({ nullable: true, description: 'A detailed description of the connection.' })\n description: string\n\n /**\n * The type of the connection.\n */\n @Column()\n @Field({ nullable: true, description: 'The type of the connection (e.g., tcp, http, mqtt).' })\n type: string\n\n /**\n * Many-to-One relationship with the Appliance entity which delegate the connection. Optional field.\n */\n @ManyToOne(type => Appliance, { nullable: true })\n @Field({ nullable: true, description: 'The edge appliance that delegates this connection, if any.' })\n edge: Appliance\n\n /**\n * Stores the ID of the Appliance who delegate the connection.\n */\n @RelationId((connection: Connection) => connection.edge)\n edgeId: string\n\n /**\n * The endpoint for the connection.\n */\n @Column()\n @Field({ nullable: true, description: 'The endpoint URL or address for the connection.' })\n endpoint: string\n\n /**\n * Whether to automatically connect when the application starts\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description: 'Whether to automatically connect when the application starts'\n })\n active: boolean\n\n /**\n * Indicates whether the connection should be created on-demand when needed.\n */\n @Column({ default: false })\n @Field({\n nullable: true,\n description: 'When true, connection is created on-demand when needed and cleaned up after use.'\n })\n onDemand: boolean\n\n /**\n * 자식 도메인이 부모 도메인의 Connection 정의를 inherit 할 때의 인스턴스 정책.\n *\n * - `ISOLATE`: 자식 도메인마다 새 인스턴스 생성. cookies/세션 상태와 useDomainAttribute\n * 기반 자격증명이 자식 도메인 단위로 격리됨. 자식별 다른 자격증명·세션이 필요한 stateful\n * connector (예: HeadlessConnector — Allbaro / KISCON 등) 의 자연 default.\n *\n * - `SHARE`: 자식 도메인이 부모의 단일 인스턴스를 그대로 공유. cookies/세션/params 모두\n * 부모 인스턴스의 것이 자식에도 그대로 적용. useDomainAttribute 기반 자식 EnvVar\n * override 는 무효화됨. 공용 자원 (단일 브로커·서버) 또는 stateless connector\n * (HTTP, GraphQL) 의 자연 default.\n *\n * - `null`: Connector 가 선언한 default 적용. Connector 도 default 가 없으면 ISOLATE\n * 폴백 (격리·안전 우선).\n *\n * 우선순위: Connection.inheritanceMode → Connector.inheritanceMode → ISOLATE.\n *\n * **호환성**: 본 컬럼은 platform v10 시점에 도입. 그 이전에 만들어진 모든 Connection\n * 레코드는 NULL 값으로 들어가 위 폴백 체인에 따라 ISOLATE 가 적용되어 기존 거동\n * (자식별 격리 인스턴스 생성) 이 그대로 유지됨. SHARE 는 명시적으로 켜야만 발동되는\n * opt-in.\n *\n * **데이터 격리 주의**: SHARE 를 켤 경우 자식 도메인들은 같은 인스턴스의 cookies/세션을\n * 공유함. 자식 도메인별로 다른 외부 자격증명을 사용하려는 경우 SHARE 는 부적절.\n */\n @Column({ type: 'varchar', nullable: true })\n @Field(type => ConnectionInheritanceMode, {\n nullable: true,\n description:\n 'Inheritance mode for child domains. ISOLATE = per-child instance with isolated session. SHARE = shared parent instance. null = use Connector default (falls back to ISOLATE).'\n })\n inheritanceMode?: ConnectionInheritanceMode\n\n /**\n * The status of the connection, using the ConnectionStatus type.\n */\n @Field({ nullable: true, description: 'The current status of the connection (e.g., CONNECTED, DISCONNECTED).' })\n state: ConnectionStatus\n\n /**\n * Additional parameters for the connection, stored as a JSON string.\n *\n * [Caution]\n * 이 컬럼타입은 postgres 데이터베이스에서는 varchar 타입을 유지한다.\n * 이는 데이터베이스 타입을 변경하면 기존 데이터가 손실될 수 있기 때문이다.\n */\n // 비밀값은 이 칸에 두지 않는다 (ADR-0112). spec 이 `type: 'secret'` 인 칸은 쓰기에서 거절되고\n // EnvVar `Connection::<name>::<param>`(늘 암호문) 에 둔다. 읽기에서도 빠진다 — connection-secrets.ts.\n // 그래서 이 칸 전체를 암호화하지 않는다.\n @Column({\n type: DATABASE_TYPE == 'postgres' ? 'varchar' : 'simple-json',\n nullable: true,\n transformer:\n DATABASE_TYPE == 'postgres'\n ? {\n to: (value: any) => JSON.stringify(value),\n from: (value: string) => {\n try {\n return JSON.parse(value)\n } catch (error) {\n return null\n }\n }\n }\n : undefined\n })\n @Field(type => ScalarObject, { nullable: true, description: 'A key-value map of parameters for the connection.' })\n params: { [key: string]: any }\n\n /**\n * The date and time when the connection was created.\n */\n @CreateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the connection was created.' })\n createdAt: Date\n\n /**\n * The date and time when the connection was last updated.\n */\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the connection was last updated.' })\n updatedAt: Date\n\n /**\n * Many-to-One relationship with the User entity who created the connection. Optional field.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true, description: 'The user who created the connection.' })\n creator: User\n\n /**\n * Stores the ID of the User who created the connection.\n */\n @RelationId((connection: Connection) => connection.creator)\n creatorId: string\n\n /**\n * Many-to-One relationship with the User entity who last updated the connection.\n * Optional field.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field({ nullable: true, description: 'The user who last updated the connection.' })\n updater: User\n\n /**\n * Stores the ID of the User who last updated the connection.\n *\n */\n @RelationId((connection: Connection) => connection.updater)\n updaterId: string\n\n /**\n * Runtime marker — DB 컬럼이 아니라 메모리 전용 플래그.\n *\n * 자식 도메인이 자기 Connection 레코드 없이 부모의 정의를 inherit 할 때,\n * `ConnectionManager.getConnectionEntityByName` 의 parent fallback 분기가\n * 부모 entity 의 clone 에 이 marker (= 부모의 domainId) 를 부착한다.\n *\n * `getConnectionInstanceByName` 가 이 marker 가 truthy 인 entity 를 보면\n * onDemand 플래그와 동일하게 자식 도메인용 instance 를 자동 생성한다 — 운영자가\n * 부모 record 의 onDemand 를 별도로 켤 필요 없이 inheritance 가 자연스럽게 동작.\n *\n * inheritanceMode = ISOLATE 인 경우에만 부착.\n */\n __inheritedFrom?: string\n\n /**\n * Runtime marker — DB 컬럼이 아니라 메모리 전용 플래그. SHARE 모드의 짝.\n *\n * 자식 도메인이 부모의 Connection 정의를 inherit 할 때, inheritanceMode = SHARE 면\n * 자식이 부모의 단일 instance 를 그대로 사용. parent fallback 분기가 부모 entity 를\n * 그대로 반환하면서 이 marker (= 부모의 domainId) 를 부착.\n *\n * `getConnectionInstanceByName` 가 이 marker 를 보면 자식 instance 자동 생성 없이\n * `connections[__sharedFrom][name]` 를 그대로 반환 — 부모·자식이 같은 instance.\n *\n * inheritanceMode = SHARE 인 경우에만 부착. `__inheritedFrom` 과 mutually exclusive.\n */\n __sharedFrom?: string\n\n /**\n * Asynchronous method to establish the connection.\n *\n */\n async connect() {\n /* Every connect — at boot, from the screen, from a scenario — comes through here. */\n IntegrationRuntime.assertEnabled(`connecting \"${this.name}\"`)\n const { type, edge } = this\n const connector = edge ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n\n await connector.connect({\n ...this,\n params: await this.getResolvedParameters() // 🔐 해결된 파라미터 사용\n })\n }\n\n /**\n * @brief Asynchronous method to disconnect the connection.\n *\n */\n async disconnect() {\n try {\n const { type, edge } = this\n const connector = edge ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n await connector.disconnect(this)\n } finally {\n }\n }\n\n /**\n * Gets a parameter value with EnvVar override support.\n *\n * **Priority (`useDomainAttribute: true` 인 필드)**:\n * 1. **EnvVar** — ancestor closest-wins (자식 도메인이 가장 가까움)\n * 2. **params** 명시값 (parent Connection 의 default 역할)\n * 3. defaultValue\n *\n * **Priority (`useDomainAttribute: false` 또는 미지정 필드)**:\n * 1. **params** 명시값\n * 2. defaultValue\n *\n * 의미론: `useDomainAttribute: true` 는 \"이 필드는 도메인별 override 가능\" 선언.\n * 따라서 EnvVar 가 본 소스, params 는 모든 자손에 동일 적용되는 default.\n * 자식 도메인 EnvVar override 가 자연스럽게 동작 (시공사 → 프로젝트 자격증명 차별화 등).\n *\n * @param key - The parameter key\n * @param defaultValue - Optional default value\n * @returns The parameter value or default value\n */\n async getParameter(key: string, defaultValue?: any): Promise<any> {\n const connector = ConnectionManager.getConnector(this.type)\n const paramSpec = connector?.parameterSpec?.find(spec => spec.name === key)\n\n // 1. useDomainAttribute 필드는 EnvVar 우선 조회 (ancestor closest-wins)\n if (paramSpec?.useDomainAttribute) {\n const envVarKey = `Connection::${this.name}::${key}`\n const domain = await getRepository(Domain).findOne({ where: { id: this.domainId } })\n if (domain) {\n const value = await lookupDomainEnvVar(domain, envVarKey)\n if (value !== undefined) {\n // params 에 명시값이 있는데 EnvVar 가 override 한 경우 진단 로그\n const paramValue = this.params?.[key]\n if (paramValue !== null && paramValue !== undefined && paramValue !== '') {\n ConnectionManager.logger.info(\n `[domain-attr] Connection '${this.name}'.${key}: params default overridden by EnvVar (domain='${domain.subdomain || domain.id}')`\n )\n }\n return value\n }\n }\n }\n\n // 2. params 명시값 (default 역할)\n if (this.params?.[key] !== null && this.params?.[key] !== undefined && this.params?.[key] !== '') {\n return this.params[key]\n }\n\n // 3. defaultValue\n return defaultValue\n }\n\n /**\n * Gets all parameters with resolved values (params + Domain attributes).\n *\n * @returns Object with all resolved parameter values\n */\n async getResolvedParameters(): Promise<{ [key: string]: any }> {\n const resolved = {}\n\n const connector = ConnectionManager.getConnector(this.type)\n const paramSpecs = connector?.parameterSpec || []\n\n for (const spec of paramSpecs) {\n const key = spec.name\n resolved[key] = await this.getParameter(key)\n }\n\n return resolved\n }\n}\n"]}
|