@things-factory/integration-base 10.1.113 → 10.1.116
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/README.md +3 -3
- package/dist-server/engine/connector/operato-connector.js +1 -1
- package/dist-server/engine/connector/operato-connector.js.map +1 -1
- package/dist-server/engine/scenario-engine.d.ts +20 -0
- package/dist-server/engine/scenario-engine.js +43 -6
- package/dist-server/engine/scenario-engine.js.map +1 -1
- package/dist-server/routers/scenario-schedule-callback-router.js +6 -6
- package/dist-server/routers/scenario-schedule-callback-router.js.map +1 -1
- package/dist-server/routers/scenario-schedule-run.d.ts +29 -0
- package/dist-server/routers/scenario-schedule-run.js +65 -0
- package/dist-server/routers/scenario-schedule-run.js.map +1 -0
- package/dist-server/service/connection/connection-mutation.js +47 -3
- package/dist-server/service/connection/connection-mutation.js.map +1 -1
- package/dist-server/service/connection/connection-query.d.ts +9 -0
- package/dist-server/service/connection/connection-query.js +34 -0
- package/dist-server/service/connection/connection-query.js.map +1 -1
- package/dist-server/service/connection/connection-secrets.d.ts +104 -0
- package/dist-server/service/connection/connection-secrets.js +188 -0
- package/dist-server/service/connection/connection-secrets.js.map +1 -0
- package/dist-server/service/connection/connection.js.map +1 -1
- package/dist-server/service/domain-attribute/domain-attribute-query.d.ts +4 -0
- package/dist-server/service/domain-attribute/domain-attribute-query.js +9 -1
- package/dist-server/service/domain-attribute/domain-attribute-query.js.map +1 -1
- package/dist-server/service/domain-attribute/domain-attribute-type.d.ts +1 -0
- package/dist-server/service/domain-attribute/domain-attribute-type.js +8 -1
- package/dist-server/service/domain-attribute/domain-attribute-type.js.map +1 -1
- package/dist-server/service/domain-attribute/secret-keys.d.ts +13 -0
- package/dist-server/service/domain-attribute/secret-keys.js +85 -0
- package/dist-server/service/domain-attribute/secret-keys.js.map +1 -0
- package/dist-server/service/scenario-instance/scenario-instance-type.d.ts +15 -0
- package/dist-server/service/scenario-instance/scenario-instance-type.js +4 -2
- package/dist-server/service/scenario-instance/scenario-instance-type.js.map +1 -1
- package/dist-server/service/secret-hint.d.ts +20 -0
- package/dist-server/service/secret-hint.js +41 -0
- package/dist-server/service/secret-hint.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -8
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
package/README.md
CHANGED
|
@@ -381,11 +381,11 @@ const scheduledScenario = {
|
|
|
381
381
|
- **Role-based access control** (RBAC) integration
|
|
382
382
|
- **Domain-based isolation** for multi-tenant environments
|
|
383
383
|
- **OAuth2 authentication** for external GraphQL services
|
|
384
|
-
- **
|
|
384
|
+
- **Connection secrets live in EnvVar** (ADR-0112) — a connector parameter declared `type: 'secret'` is refused in `Connection.params`, kept in the EnvVar `Connection::<connection>::<param>` (always encrypted at rest), and left out when `params` is read, except by the edge appliance the connection is assigned to. `Connection.params` itself is plain JSON.
|
|
385
385
|
|
|
386
386
|
### Best Practices
|
|
387
|
-
-
|
|
388
|
-
-
|
|
387
|
+
- Declare every secret parameter `type: 'secret'` with `useDomainAttribute: true`; a test walks every connector for it
|
|
388
|
+
- Put passwords, tokens and API keys in the EnvVar, never in `params`
|
|
389
389
|
- Implement IP-based access control where needed
|
|
390
390
|
- Regular security audits of scenario configurations
|
|
391
391
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operato-connector.js","sourceRoot":"","sources":["../../../server/engine/connector/operato-connector.ts"],"names":[],"mappings":"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","sourcesContent":["import 'cross-fetch/polyfill'\n\nimport { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client/core'\nimport { setContext } from '@apollo/client/link/context'\n\nimport WebSocket from 'ws'\nimport { createClient } from 'graphql-ws'\nimport { GraphQLWsLink } from '@apollo/client/link/subscriptions'\nimport { getMainDefinition } from '@apollo/client/utilities'\nimport gql from 'graphql-tag'\n\nimport { ConnectionManager } from '../connection-manager'\nimport { resolveRunnableSteps } from '../../service/scenario/resolve-steps'\nimport { Connector } from '../types'\nimport { InputConnection } from '../../service/connection/connection-type'\n\nimport { Scenario } from '../../service/scenario/scenario'\nimport { ScenarioInstance } from '../../service/scenario-instance/scenario-instance-type'\n\nimport { getRepository, GraphqlLocalClient, Domain } from '@things-factory/shell'\nimport { User, checkUserHasRole } from '@things-factory/auth-base'\n\nconst debug = require('debug')('things-factory:integration-base:operato-connector')\n\nconst defaultOptions: any = {\n watchQuery: {\n fetchPolicy: 'no-cache',\n errorPolicy: 'ignore'\n },\n query: {\n fetchPolicy: 'no-cache', //'network-only'\n errorPolicy: 'all'\n },\n mutate: {\n errorPolicy: 'all'\n }\n}\n\nexport const GRAPHQL_URI = '/graphql'\nexport const SUBSCRIPTION_URI = GRAPHQL_URI\n\ninterface SubscriberData {\n tag: string\n scenario: any\n subscriptionObserver: any\n}\n\nexport class OperatoConnector implements Connector {\n private context: any\n\n async ready(connectionConfigs: InputConnection[]) {\n await Promise.all(connectionConfigs.map(this.connect.bind(this)))\n\n ConnectionManager.logger.info('operato-connector connections are ready')\n }\n\n async connect(connection: InputConnection) {\n const {\n endpoint: uri,\n params: { authKey, domain, subscriptionHandlers = {} }\n } = connection\n\n if (!authKey || !domain) {\n throw new Error('some connection paramter missing.')\n }\n\n const domainOwner = await getRepository(User).findOne({\n where: {\n id: connection.domain.owner\n }\n })\n\n this.context = {\n domain: connection.domain,\n user: domainOwner\n /* TODO: domainOwner 대신 특정 유저를 지정할 수 있도록 개선해야함. 모든 커넥션에 유저를 지정하는 기능으로 일반화할 필요가 있는 지 고민해야함 */\n }\n\n const httpLink = createHttpLink({\n uri: uri\n })\n\n /* \n CHECKPOINT: \n 1. GraphqQLWsLink를 사용하면 setContext를 통한 추가 헤더 설정이 무시됩니다.\n 따라서, GraphQLWsLink를 사용하려면, connectionParams를 통해 헤더를 설정해야 합니다.\n \n 2. 서버에서 실행시, webSocketImpl을 명시적으로 지정해야 합니다.\n */\n const wsLink = new GraphQLWsLink(\n createClient({\n url: uri.replace(/^http/, 'ws'),\n keepAlive: 10_000,\n retryAttempts: 1_000_000,\n shouldRetry: e => true,\n webSocketImpl: WebSocket,\n connectionParams: {\n headers: {\n 'x-things-factory-domain': domain,\n authorization: authKey ? `Bearer ${authKey}` : ''\n }\n }\n })\n )\n\n const splitLink = split(\n ({ query }) => {\n const def = getMainDefinition(query)\n return def.kind === 'OperationDefinition' && def.operation === 'subscription'\n },\n wsLink,\n setContext((_, { headers }) => {\n return {\n headers: {\n ...headers,\n 'x-things-factory-domain': domain,\n authorization: authKey ? `Bearer ${authKey}` : ''\n }\n }\n }).concat(httpLink)\n )\n\n const cache = new InMemoryCache()\n\n const client = new ApolloClient({\n defaultOptions,\n cache,\n link: splitLink\n })\n\n const subscriptions: SubscriberData[] = []\n const tags = Object.keys(subscriptionHandlers).filter(tag => tag && subscriptionHandlers[tag])\n\n /* 시나리오 조회 및 subscription 등록을 순차적으로 처리 */\n for (const tag of tags) {\n const scenarioName = subscriptionHandlers[tag]\n\n // fetch a scenario\n const selectedScenario = await getRepository(Scenario).findOne({\n where: {\n name: scenarioName\n },\n relations: ['domain']\n })\n\n /* 시나리오가 DB에 없으면 경고 로그를 남기고 건너뜀 */\n if (!selectedScenario) {\n ConnectionManager.logger.warn(\n `(${connection.name}) scenario not found for tag \"${tag}\" (scenarioName: \"${scenarioName}\") - skipping subscription`\n )\n continue\n }\n\n // [버전관리] tag 구독으로 실행되는 시나리오도 운영 실행이므로 released 스냅샷을 사용(없으면 draft 폴백).\n selectedScenario.steps = await resolveRunnableSteps(selectedScenario, { mode: 'released' })\n\n const subscription = client.subscribe({\n query: gql`\n subscription {\n data(tag: \"${tag}\") {\n tag\n data\n }\n }\n `\n })\n\n const subscriptionObserver = subscription.subscribe({\n next: async data => {\n /* subscription 수신 시 에러가 발생해도 프로세스가 죽지 않도록 catch 처리 */\n try {\n debug('received pubsub msg.:', data?.data)\n await this.runScenario(subscriptions, data?.data?.data)\n } catch (e) {\n ConnectionManager.logger.error(`(${connection.name}:${tag}) runScenario failed`, e)\n }\n },\n error: error => {\n ConnectionManager.logger.error(`(${connection.name}:${connection.endpoint}) subscription error`, error)\n },\n complete: () => {\n ConnectionManager.logger.info(`(${connection.name}:${connection.endpoint}) subscription complete`)\n }\n })\n\n ConnectionManager.logger.info(\n `(${connection.name}:${connection.endpoint}) subscription closed flag: ${subscriptionObserver.closed}`\n )\n\n subscriptions.push({\n tag,\n scenario: selectedScenario,\n subscriptionObserver\n })\n ConnectionManager.logger.info(`(${tag}:${scenarioName}) subscription closed flag: ${subscriptionObserver.closed}`)\n }\n\n client['subscriptions'] = subscriptions\n ConnectionManager.addConnectionInstance(connection, client)\n\n ConnectionManager.logger.info(\n `operato-connector connection(${connection.name}:${connection.endpoint}) is connected`\n )\n }\n\n async disconnect(connection: InputConnection) {\n const client = ConnectionManager.getConnectionInstance(connection)\n const subscriptions: SubscriberData[] = client['subscriptions']\n subscriptions.forEach(subscription => subscription.subscriptionObserver.unsubscribe())\n client.stop()\n ConnectionManager.removeConnectionInstance(connection)\n\n ConnectionManager.logger.info(`operato-connector connection(${connection.name}) is disconnected`)\n }\n\n async runScenario(subscriptions: SubscriberData[], variables: any): Promise<ScenarioInstance> {\n const { domain, user } = this.context\n const { tag } = variables\n\n if (!tag) {\n throw new Error(`tag is invalid - ${tag}`)\n }\n\n const scenario = subscriptions.find(subscription => subscription.tag === tag)?.scenario\n if (!scenario) {\n throw new Error(`scenario is not found - ${tag}`)\n }\n\n if (!(await checkUserHasRole(scenario.roleId, domain, user))) {\n throw new Error(`Unauthorized! ${scenario.name} doesn't have required role.`)\n }\n\n /* create a scenario instance */\n const instanceName = scenario.name + '-' + String(Date.now())\n const instance = new ScenarioInstance(instanceName, scenario, {\n user,\n domain,\n variables,\n client: GraphqlLocalClient.client\n })\n\n // run scenario\n await instance.run()\n return instance\n }\n\n get parameterSpec() {\n return [\n {\n type: 'string',\n name: 'authKey',\n label: 'auth-key',\n useDomainAttribute: true\n },\n {\n type: 'string',\n name: 'domain',\n label: 'domain'\n },\n {\n type: 'tag-scenarios',\n name: 'subscriptionHandlers',\n label: 'subscription-handlers'\n }\n ]\n }\n\n get taskPrefixes() {\n return ['graphql']\n }\n\n get help() {\n return 'integration/connector/operato-connector'\n }\n\n get description() {\n return 'Operato Graphql Connector'\n }\n}\n\nConnectionManager.registerConnector('operato-connector', new OperatoConnector())\n"]}
|
|
1
|
+
{"version":3,"file":"operato-connector.js","sourceRoot":"","sources":["../../../server/engine/connector/operato-connector.ts"],"names":[],"mappings":"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","sourcesContent":["import 'cross-fetch/polyfill'\n\nimport { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client/core'\nimport { setContext } from '@apollo/client/link/context'\n\nimport WebSocket from 'ws'\nimport { createClient } from 'graphql-ws'\nimport { GraphQLWsLink } from '@apollo/client/link/subscriptions'\nimport { getMainDefinition } from '@apollo/client/utilities'\nimport gql from 'graphql-tag'\n\nimport { ConnectionManager } from '../connection-manager'\nimport { resolveRunnableSteps } from '../../service/scenario/resolve-steps'\nimport { Connector } from '../types'\nimport { InputConnection } from '../../service/connection/connection-type'\n\nimport { Scenario } from '../../service/scenario/scenario'\nimport { ScenarioInstance } from '../../service/scenario-instance/scenario-instance-type'\n\nimport { getRepository, GraphqlLocalClient, Domain } from '@things-factory/shell'\nimport { User, checkUserHasRole } from '@things-factory/auth-base'\n\nconst debug = require('debug')('things-factory:integration-base:operato-connector')\n\nconst defaultOptions: any = {\n watchQuery: {\n fetchPolicy: 'no-cache',\n errorPolicy: 'ignore'\n },\n query: {\n fetchPolicy: 'no-cache', //'network-only'\n errorPolicy: 'all'\n },\n mutate: {\n errorPolicy: 'all'\n }\n}\n\nexport const GRAPHQL_URI = '/graphql'\nexport const SUBSCRIPTION_URI = GRAPHQL_URI\n\ninterface SubscriberData {\n tag: string\n scenario: any\n subscriptionObserver: any\n}\n\nexport class OperatoConnector implements Connector {\n private context: any\n\n async ready(connectionConfigs: InputConnection[]) {\n await Promise.all(connectionConfigs.map(this.connect.bind(this)))\n\n ConnectionManager.logger.info('operato-connector connections are ready')\n }\n\n async connect(connection: InputConnection) {\n const {\n endpoint: uri,\n params: { authKey, domain, subscriptionHandlers = {} }\n } = connection\n\n if (!authKey || !domain) {\n throw new Error('some connection paramter missing.')\n }\n\n const domainOwner = await getRepository(User).findOne({\n where: {\n id: connection.domain.owner\n }\n })\n\n this.context = {\n domain: connection.domain,\n user: domainOwner\n /* TODO: domainOwner 대신 특정 유저를 지정할 수 있도록 개선해야함. 모든 커넥션에 유저를 지정하는 기능으로 일반화할 필요가 있는 지 고민해야함 */\n }\n\n const httpLink = createHttpLink({\n uri: uri\n })\n\n /* \n CHECKPOINT: \n 1. GraphqQLWsLink를 사용하면 setContext를 통한 추가 헤더 설정이 무시됩니다.\n 따라서, GraphQLWsLink를 사용하려면, connectionParams를 통해 헤더를 설정해야 합니다.\n \n 2. 서버에서 실행시, webSocketImpl을 명시적으로 지정해야 합니다.\n */\n const wsLink = new GraphQLWsLink(\n createClient({\n url: uri.replace(/^http/, 'ws'),\n keepAlive: 10_000,\n retryAttempts: 1_000_000,\n shouldRetry: e => true,\n webSocketImpl: WebSocket,\n connectionParams: {\n headers: {\n 'x-things-factory-domain': domain,\n authorization: authKey ? `Bearer ${authKey}` : ''\n }\n }\n })\n )\n\n const splitLink = split(\n ({ query }) => {\n const def = getMainDefinition(query)\n return def.kind === 'OperationDefinition' && def.operation === 'subscription'\n },\n wsLink,\n setContext((_, { headers }) => {\n return {\n headers: {\n ...headers,\n 'x-things-factory-domain': domain,\n authorization: authKey ? `Bearer ${authKey}` : ''\n }\n }\n }).concat(httpLink)\n )\n\n const cache = new InMemoryCache()\n\n const client = new ApolloClient({\n defaultOptions,\n cache,\n link: splitLink\n })\n\n const subscriptions: SubscriberData[] = []\n const tags = Object.keys(subscriptionHandlers).filter(tag => tag && subscriptionHandlers[tag])\n\n /* 시나리오 조회 및 subscription 등록을 순차적으로 처리 */\n for (const tag of tags) {\n const scenarioName = subscriptionHandlers[tag]\n\n // fetch a scenario\n const selectedScenario = await getRepository(Scenario).findOne({\n where: {\n name: scenarioName\n },\n relations: ['domain']\n })\n\n /* 시나리오가 DB에 없으면 경고 로그를 남기고 건너뜀 */\n if (!selectedScenario) {\n ConnectionManager.logger.warn(\n `(${connection.name}) scenario not found for tag \"${tag}\" (scenarioName: \"${scenarioName}\") - skipping subscription`\n )\n continue\n }\n\n // [버전관리] tag 구독으로 실행되는 시나리오도 운영 실행이므로 released 스냅샷을 사용(없으면 draft 폴백).\n selectedScenario.steps = await resolveRunnableSteps(selectedScenario, { mode: 'released' })\n\n const subscription = client.subscribe({\n query: gql`\n subscription {\n data(tag: \"${tag}\") {\n tag\n data\n }\n }\n `\n })\n\n const subscriptionObserver = subscription.subscribe({\n next: async data => {\n /* subscription 수신 시 에러가 발생해도 프로세스가 죽지 않도록 catch 처리 */\n try {\n debug('received pubsub msg.:', data?.data)\n await this.runScenario(subscriptions, data?.data?.data)\n } catch (e) {\n ConnectionManager.logger.error(`(${connection.name}:${tag}) runScenario failed`, e)\n }\n },\n error: error => {\n ConnectionManager.logger.error(`(${connection.name}:${connection.endpoint}) subscription error`, error)\n },\n complete: () => {\n ConnectionManager.logger.info(`(${connection.name}:${connection.endpoint}) subscription complete`)\n }\n })\n\n ConnectionManager.logger.info(\n `(${connection.name}:${connection.endpoint}) subscription closed flag: ${subscriptionObserver.closed}`\n )\n\n subscriptions.push({\n tag,\n scenario: selectedScenario,\n subscriptionObserver\n })\n ConnectionManager.logger.info(`(${tag}:${scenarioName}) subscription closed flag: ${subscriptionObserver.closed}`)\n }\n\n client['subscriptions'] = subscriptions\n ConnectionManager.addConnectionInstance(connection, client)\n\n ConnectionManager.logger.info(\n `operato-connector connection(${connection.name}:${connection.endpoint}) is connected`\n )\n }\n\n async disconnect(connection: InputConnection) {\n const client = ConnectionManager.getConnectionInstance(connection)\n const subscriptions: SubscriberData[] = client['subscriptions']\n subscriptions.forEach(subscription => subscription.subscriptionObserver.unsubscribe())\n client.stop()\n ConnectionManager.removeConnectionInstance(connection)\n\n ConnectionManager.logger.info(`operato-connector connection(${connection.name}) is disconnected`)\n }\n\n async runScenario(subscriptions: SubscriberData[], variables: any): Promise<ScenarioInstance> {\n const { domain, user } = this.context\n const { tag } = variables\n\n if (!tag) {\n throw new Error(`tag is invalid - ${tag}`)\n }\n\n const scenario = subscriptions.find(subscription => subscription.tag === tag)?.scenario\n if (!scenario) {\n throw new Error(`scenario is not found - ${tag}`)\n }\n\n if (!(await checkUserHasRole(scenario.roleId, domain, user))) {\n throw new Error(`Unauthorized! ${scenario.name} doesn't have required role.`)\n }\n\n /* create a scenario instance */\n const instanceName = scenario.name + '-' + String(Date.now())\n const instance = new ScenarioInstance(instanceName, scenario, {\n user,\n domain,\n variables,\n client: GraphqlLocalClient.client\n })\n\n // run scenario\n await instance.run()\n return instance\n }\n\n get parameterSpec() {\n return [\n {\n type: 'secret',\n name: 'authKey',\n label: 'auth-key',\n useDomainAttribute: true\n },\n {\n type: 'string',\n name: 'domain',\n label: 'domain'\n },\n {\n type: 'tag-scenarios',\n name: 'subscriptionHandlers',\n label: 'subscription-handlers'\n }\n ]\n }\n\n get taskPrefixes() {\n return ['graphql']\n }\n\n get help() {\n return 'integration/connector/operato-connector'\n }\n\n get description() {\n return 'Operato Graphql Connector'\n }\n}\n\nConnectionManager.registerConnector('operato-connector', new OperatoConnector())\n"]}
|
|
@@ -7,6 +7,26 @@ export declare class ScenarioEngine {
|
|
|
7
7
|
static getScenarioInstances(domain: any, scenarioName?: any): ScenarioInstance[];
|
|
8
8
|
static load(instanceName: any, scenarioConfig: any, context?: any): Promise<any>;
|
|
9
9
|
static run(instanceName: any, scenarioConfig: any, context?: any): Promise<ScenarioInstanceRunResult>;
|
|
10
|
+
/** Whether an instance by this name is loaded on this domain in this process. */
|
|
11
|
+
static isRunning(domainId: string, instanceName: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Run a scenario across its iteration's domains **and wait**, saying what happened (ADR-0110 adoption 2, step 5).
|
|
14
|
+
*
|
|
15
|
+
* `loadIteration` returns at once and `run` swallows a failure, so a scheduled run could never tell the
|
|
16
|
+
* scheduler's discipline that it failed. This one awaits every domain and returns the failures by domain.
|
|
17
|
+
* A domain where an instance by that name is already loaded in this process is not run again and is named in
|
|
18
|
+
* `alreadyRunning` — `run` would hand back the running instance and report nothing.
|
|
19
|
+
*/
|
|
20
|
+
static runIteration(instanceName: any, scenarioConfig: any, context?: any): Promise<{
|
|
21
|
+
domains: number;
|
|
22
|
+
failed: {
|
|
23
|
+
subdomain: string;
|
|
24
|
+
error: string;
|
|
25
|
+
}[];
|
|
26
|
+
alreadyRunning: string[];
|
|
27
|
+
}>;
|
|
28
|
+
/** Create and run one instance. Throws what the run throws. */
|
|
29
|
+
private static runInstance;
|
|
10
30
|
/**
|
|
11
31
|
* Loads and executes a scenario across multiple domains based on iteration
|
|
12
32
|
* @param instanceName - The name for the scenario instance
|
|
@@ -65,6 +65,48 @@ class ScenarioEngine {
|
|
|
65
65
|
return instance;
|
|
66
66
|
}
|
|
67
67
|
static async run(instanceName, scenarioConfig, context) {
|
|
68
|
+
try {
|
|
69
|
+
return await ScenarioEngine.runInstance(instanceName, scenarioConfig, context);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const domain = context?.domain || scenarioConfig?.domain;
|
|
73
|
+
console.error(`An exception occurred while executing the scenario instance named "${instanceName}" on the domain "${domain?.name}"`, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Whether an instance by this name is loaded on this domain in this process. */
|
|
77
|
+
static isRunning(domainId, instanceName) {
|
|
78
|
+
return !!ScenarioEngine.scenarioInstances[domainId]?.[instanceName];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run a scenario across its iteration's domains **and wait**, saying what happened (ADR-0110 adoption 2, step 5).
|
|
82
|
+
*
|
|
83
|
+
* `loadIteration` returns at once and `run` swallows a failure, so a scheduled run could never tell the
|
|
84
|
+
* scheduler's discipline that it failed. This one awaits every domain and returns the failures by domain.
|
|
85
|
+
* A domain where an instance by that name is already loaded in this process is not run again and is named in
|
|
86
|
+
* `alreadyRunning` — `run` would hand back the running instance and report nothing.
|
|
87
|
+
*/
|
|
88
|
+
static async runIteration(instanceName, scenarioConfig, context) {
|
|
89
|
+
const { domain: scenarioDomain, iteration = 'SELF' } = scenarioConfig;
|
|
90
|
+
const domain = context?.domain || scenarioDomain;
|
|
91
|
+
const targetDomains = await ScenarioEngine.getTargetDomains(domain, iteration);
|
|
92
|
+
const failed = [];
|
|
93
|
+
const alreadyRunning = [];
|
|
94
|
+
for (const target of targetDomains) {
|
|
95
|
+
if (ScenarioEngine.isRunning(target.id, instanceName)) {
|
|
96
|
+
alreadyRunning.push(target.subdomain);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await ScenarioEngine.runInstance(instanceName, { ...scenarioConfig, domain: target }, { ...context, domain: target });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
failed.push({ subdomain: target.subdomain, error: String(error?.message ?? error).slice(0, 300) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { domains: targetDomains.length, failed, alreadyRunning };
|
|
107
|
+
}
|
|
108
|
+
/** Create and run one instance. Throws what the run throws. */
|
|
109
|
+
static async runInstance(instanceName, scenarioConfig, context) {
|
|
68
110
|
const { domain: scenarioDomain } = scenarioConfig;
|
|
69
111
|
const domain = context?.domain || scenarioDomain;
|
|
70
112
|
debug('run', domain.subdomain, instanceName);
|
|
@@ -91,12 +133,7 @@ class ScenarioEngine {
|
|
|
91
133
|
}
|
|
92
134
|
});
|
|
93
135
|
scenarioInstances[instanceName] = instance;
|
|
94
|
-
|
|
95
|
-
return await instance.run();
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
console.error(`An exception occurred while executing the scenario instance named "${instanceName}" on the domain "${domain.name}"`, err);
|
|
99
|
-
}
|
|
136
|
+
return await instance.run();
|
|
100
137
|
}
|
|
101
138
|
/**
|
|
102
139
|
* Loads and executes a scenario across multiple domains based on 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 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 try {\n return await instance.run()\n } catch (err) {\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 /**\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 { 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"]}
|
|
@@ -7,7 +7,8 @@ const shell_1 = require("@things-factory/shell");
|
|
|
7
7
|
const auth_base_1 = require("@things-factory/auth-base");
|
|
8
8
|
const scenario_1 = require("../service/scenario/scenario");
|
|
9
9
|
const resolve_steps_1 = require("../service/scenario/resolve-steps");
|
|
10
|
-
const
|
|
10
|
+
const env_1 = require("@things-factory/env");
|
|
11
|
+
const scenario_schedule_run_1 = require("./scenario-schedule-run");
|
|
11
12
|
exports.scenarioScheduleCallbackRouter = new koa_router_1.default();
|
|
12
13
|
/* When a callback occurs from the scheduler when a scheduled scenario is on schedule. */
|
|
13
14
|
exports.scenarioScheduleCallbackRouter.post('/callback-schedule-for-scenario', async (context, next) => {
|
|
@@ -48,11 +49,10 @@ exports.scenarioScheduleCallbackRouter.post('/callback-schedule-for-scenario', a
|
|
|
48
49
|
// [버전관리] 스케줄(운영) 실행은 최신 릴리즈 스냅샷을 사용. 릴리즈 없으면 draft 폴백.
|
|
49
50
|
scenario.steps = await (0, resolve_steps_1.resolveRunnableSteps)(scenario, { mode: 'released' });
|
|
50
51
|
const user = (userId && (await (0, shell_1.getRepository)(auth_base_1.User).findOne({ where: { id: userId } }))) || scenario.creator;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/* variables: schedule 등록시 variables 는 설정할 방법이 없음. */
|
|
55
|
-
});
|
|
52
|
+
/* variables: schedule 등록시 variables 는 설정할 방법이 없음. */
|
|
53
|
+
const answer = await (0, scenario_schedule_run_1.startScheduledScenario)(scenario, scenario, { domain, user }, env_1.logger);
|
|
54
|
+
/* A refusal is an answer too — 200 with the reason, so the scheduler does not retry it every minute. */
|
|
56
55
|
context.status = 200;
|
|
56
|
+
context.body = answer;
|
|
57
57
|
});
|
|
58
58
|
//# sourceMappingURL=scenario-schedule-callback-router.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenario-schedule-callback-router.js","sourceRoot":"","sources":["../../server/routers/scenario-schedule-callback-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,iDAAqD;AACrD,yDAAgD;AAGhD,2DAAuD;AACvD,qEAAwE;AACxE
|
|
1
|
+
{"version":3,"file":"scenario-schedule-callback-router.js","sourceRoot":"","sources":["../../server/routers/scenario-schedule-callback-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,iDAAqD;AACrD,yDAAgD;AAGhD,2DAAuD;AACvD,qEAAwE;AACxE,6CAA4C;AAE5C,mEAAgE;AAEnD,QAAA,8BAA8B,GAAG,IAAI,oBAAM,EAAE,CAAA;AAE1D,yFAAyF;AACzF,sCAA8B,CAAC,IAAI,CAAC,iCAAiC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC7F,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAA+B,CAAA;IACxE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,CAAA;IAE7B,4BAA4B;IAC5B,yBAAyB;IACzB,oBAAoB;IACpB,oBAAoB;IACpB,qBAAqB;IAErB,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEhC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;IACvE,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,MAAM,CAAA;IAEpE,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,aAAa,UAAU,kCAAkC,CAAC,CAAA;IAC7F,CAAC;IAED,IAAI,QAAQ,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAA;IACnF,CAAC;IAED,IAAI,IAAI,IAAI,UAAU,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;IAClF,CAAC;IAED,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IACvF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAA,qBAAa,EAAC,mBAAQ,CAAC,CAAC,OAAO,CAAC;QACrD,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;QACnD,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;KACjC,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,EAAE,SAAS,IAAI,UAAU,aAAa,CAAC,CAAA;IACnG,CAAC;IAED,uDAAuD;IACvD,QAAQ,CAAC,KAAK,GAAG,MAAM,IAAA,oCAAoB,EAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;IAE3E,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAA;IAE3G,qDAAqD;IACrD,MAAM,MAAM,GAAG,MAAM,IAAA,8CAAsB,EAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,YAAM,CAAC,CAAA;IAEzF,wGAAwG;IACxG,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;IACpB,OAAO,CAAC,IAAI,GAAG,MAAM,CAAA;AACvB,CAAC,CAAC,CAAA","sourcesContent":["import Router from 'koa-router'\n\nimport { getRepository } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { ScheduleRegisterRequest } from '@things-factory/scheduler-client'\n\nimport { Scenario } from '../service/scenario/scenario'\nimport { resolveRunnableSteps } from '../service/scenario/resolve-steps'\nimport { logger } from '@things-factory/env'\n\nimport { startScheduledScenario } from './scenario-schedule-run'\n\nexport const scenarioScheduleCallbackRouter = new Router()\n\n/* When a callback occurs from the scheduler when a scheduled scenario is on schedule. */\nscenarioScheduleCallbackRouter.post('/callback-schedule-for-scenario', async (context, next) => {\n const { client, task } = context.request.body as ScheduleRegisterRequest\n const { userId } = task?.data\n\n // application: Application,\n // group: `${domain.id}`,\n // type: 'scenario',\n // key: scenario.id,\n // operation: 'start'\n\n const { domain } = context.state\n\n if (!domain) {\n throw new Error('cannot identify the current domain.')\n }\n\n if (!client || typeof client !== 'object') {\n throw new Error('client property should be a part of callback body.')\n }\n\n const { group: domainId, key: scenarioId, operation, type } = client\n\n if (!domainId || !scenarioId) {\n throw new Error(`group(${domainId}) and key(${scenarioId}) properties should not be empty`)\n }\n\n if (domainId != domain.id) {\n throw new Error('client property(group) is not matched with the current domain.')\n }\n\n if (type != 'scenario') {\n throw new Error('client property(type) is not matched with the required value.')\n }\n\n if (operation != 'start') {\n throw new Error('client property(operation) is not matched with the required value.')\n }\n\n const scenario = await getRepository(Scenario).findOne({\n where: { domain: { id: domainId }, id: scenarioId },\n relations: ['domain', 'creator']\n })\n\n if (!scenario) {\n throw new Error(`Scenario having given scenarioId(${domain?.subdomain}:${scenarioId}) not found`)\n }\n\n // [버전관리] 스케줄(운영) 실행은 최신 릴리즈 스냅샷을 사용. 릴리즈 없으면 draft 폴백.\n scenario.steps = await resolveRunnableSteps(scenario, { mode: 'released' })\n\n const user = (userId && (await getRepository(User).findOne({ where: { id: userId } }))) || scenario.creator\n\n /* variables: schedule 등록시 variables 는 설정할 방법이 없음. */\n const answer = await startScheduledScenario(scenario, scenario, { domain, user }, logger)\n\n /* A refusal is an answer too — 200 with the reason, so the scheduler does not retry it every minute. */\n context.status = 200\n context.body = answer\n})\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { startLeasedRun, type LeasedRunAnswer } from '@things-factory/task-lease';
|
|
2
|
+
/** Scheduled runs of one scenario are not closer together than this. */
|
|
3
|
+
export declare const SCHEDULE_FLOOR_MS = 5000;
|
|
4
|
+
export declare const scheduleLeaseName: (domainId: string, scenarioId: string) => string;
|
|
5
|
+
export type ScheduledRunAnswer = {
|
|
6
|
+
ran: true;
|
|
7
|
+
} | {
|
|
8
|
+
ran: false;
|
|
9
|
+
why: 'scenario-schedule-held' | 'scenario-schedule-lease-error';
|
|
10
|
+
detail?: string;
|
|
11
|
+
};
|
|
12
|
+
export interface ScheduledRunLog {
|
|
13
|
+
info: (what: string) => void;
|
|
14
|
+
warn: (what: string) => void;
|
|
15
|
+
error: (what: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/** What a leased-run answer means to the scheduler. */
|
|
18
|
+
export declare function scheduledRunAnswerOf(answer: LeasedRunAnswer): ScheduledRunAnswer;
|
|
19
|
+
/** Start the scheduled run of one scenario, or say why not. The run itself goes on after this returns. */
|
|
20
|
+
export declare function startScheduledScenario(scenario: {
|
|
21
|
+
id?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
}, config: any, context: {
|
|
24
|
+
domain: {
|
|
25
|
+
id: string;
|
|
26
|
+
subdomain: string;
|
|
27
|
+
};
|
|
28
|
+
user?: unknown;
|
|
29
|
+
}, log: ScheduledRunLog, start?: typeof startLeasedRun): Promise<ScheduledRunAnswer>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scheduleLeaseName = exports.SCHEDULE_FLOOR_MS = void 0;
|
|
4
|
+
exports.scheduledRunAnswerOf = scheduledRunAnswerOf;
|
|
5
|
+
exports.startScheduledScenario = startScheduledScenario;
|
|
6
|
+
const task_lease_1 = require("@things-factory/task-lease");
|
|
7
|
+
const scenario_engine_1 = require("../engine/scenario-engine");
|
|
8
|
+
/*
|
|
9
|
+
* A scheduled scenario run under the shared discipline (ADR-0110 adoption 2, step 5, 보탬 3).
|
|
10
|
+
*
|
|
11
|
+
* The scheduler calls back on whichever instance sits behind its URL, on the cron's beat, whether or not the last
|
|
12
|
+
* run has ended. Before this, every callback started a run: two instances both ran it, a run still going was
|
|
13
|
+
* silently joined (the engine handed back the running instance and said STARTED), and a failing run was started
|
|
14
|
+
* again on the next beat at full rate.
|
|
15
|
+
*
|
|
16
|
+
* one at a time lease `scenario-schedule:<domain>:<scenario>`; a callback that finds it held is refused
|
|
17
|
+
* floor after a run, callbacks closer than SCHEDULE_FLOOR_MS are refused
|
|
18
|
+
* backoff after a failed run, callbacks are refused for the backoff of the consecutive failures
|
|
19
|
+
*
|
|
20
|
+
* A refusal answers 200 with its reason. The scheduler retries any other answer every 60 seconds without end
|
|
21
|
+
* (`retry_dlq`, `max_retry_count: -1`), which would turn one refused overlap into a stream of them.
|
|
22
|
+
*
|
|
23
|
+
* Only the scheduled path takes the lease. A person starting the scenario by hand, and the boot-time load, keep
|
|
24
|
+
* the engine's own in-process rule; a scheduled run that finds such an instance loaded here says so.
|
|
25
|
+
*/
|
|
26
|
+
/** Scheduled runs of one scenario are not closer together than this. */
|
|
27
|
+
exports.SCHEDULE_FLOOR_MS = 5_000;
|
|
28
|
+
const scheduleLeaseName = (domainId, scenarioId) => `scenario-schedule:${domainId}:${scenarioId}`;
|
|
29
|
+
exports.scheduleLeaseName = scheduleLeaseName;
|
|
30
|
+
/** What a leased-run answer means to the scheduler. */
|
|
31
|
+
function scheduledRunAnswerOf(answer) {
|
|
32
|
+
if (answer.started)
|
|
33
|
+
return { ran: true };
|
|
34
|
+
/* This package has strictNullChecks off, so `started` does not narrow the union by itself. */
|
|
35
|
+
const refused = answer;
|
|
36
|
+
return refused.why === 'held'
|
|
37
|
+
? { ran: false, why: 'scenario-schedule-held' }
|
|
38
|
+
: { ran: false, why: 'scenario-schedule-lease-error', detail: refused.error };
|
|
39
|
+
}
|
|
40
|
+
/** Start the scheduled run of one scenario, or say why not. The run itself goes on after this returns. */
|
|
41
|
+
async function startScheduledScenario(scenario, config, context, log, start = task_lease_1.startLeasedRun) {
|
|
42
|
+
const where = `${context.domain.subdomain}:${scenario.name}`;
|
|
43
|
+
const answer = await start({
|
|
44
|
+
name: (0, exports.scheduleLeaseName)(context.domain.id, String(scenario.id)),
|
|
45
|
+
floorMs: exports.SCHEDULE_FLOOR_MS,
|
|
46
|
+
work: async () => {
|
|
47
|
+
const result = await scenario_engine_1.ScenarioEngine.runIteration(scenario.name, config, context);
|
|
48
|
+
if (result.alreadyRunning.length) {
|
|
49
|
+
log.warn(`[scenario-schedule] "${where}" is already loaded on ${result.alreadyRunning.join(', ')} in this process — not started again`);
|
|
50
|
+
}
|
|
51
|
+
if (result.failed.length) {
|
|
52
|
+
throw new Error(result.failed.map(one => `${one.subdomain}: ${one.error}`).join(' · '));
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onFailure: f => log.error(`[scenario-schedule] "${where}" failed (${f.consecutive} in a row) — scheduled runs rest ${Math.round(f.restMs / 1000)}s — ${f.error}`),
|
|
56
|
+
onRecovered: r => log.info(`[scenario-schedule] "${where}" ran again after ${r.afterFailures} failed run(s)`),
|
|
57
|
+
onLeaseError: e => log.error(`[scenario-schedule] "${where}" lease "${e.name}" — ${e.error}`)
|
|
58
|
+
});
|
|
59
|
+
const said = scheduledRunAnswerOf(answer);
|
|
60
|
+
if (said.ran === false && said.why === 'scenario-schedule-held') {
|
|
61
|
+
log.warn(`[scenario-schedule] "${where}" refused: still running, or resting after its last run`);
|
|
62
|
+
}
|
|
63
|
+
return said;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=scenario-schedule-run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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'\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 } | { ran: false; why: 'scenario-schedule-held' | 'scenario-schedule-lease-error'; 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 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"]}
|
|
@@ -9,11 +9,40 @@ const connection_manager_1 = require("../../engine/connection-manager");
|
|
|
9
9
|
const connection_1 = require("./connection");
|
|
10
10
|
const connection_type_1 = require("./connection-type");
|
|
11
11
|
const domain_inheritance_1 = require("../../utils/domain-inheritance");
|
|
12
|
+
const connection_secrets_1 = require("./connection-secrets");
|
|
13
|
+
/**
|
|
14
|
+
* The params a write stores (ADR-0112): a secret the caller sends is refused, a secret the caller
|
|
15
|
+
* leaves out is kept from the stored row.
|
|
16
|
+
*
|
|
17
|
+
* A write that changes the connector type keeps none of the old type's secrets. They were secrets
|
|
18
|
+
* under the old type's spec; the new type may not declare them, and then reading would show them.
|
|
19
|
+
* When the old type is not registered here nobody can say which were secret, so none of its params
|
|
20
|
+
* are carried over.
|
|
21
|
+
*/
|
|
22
|
+
function paramsToStore(written, stored) {
|
|
23
|
+
const type = written.type ?? stored?.type;
|
|
24
|
+
const name = written.name ?? stored?.name;
|
|
25
|
+
const typeChanged = Boolean(stored && written.type && written.type !== stored.type);
|
|
26
|
+
if (written.params === undefined) {
|
|
27
|
+
return typeChanged ? { params: (0, connection_secrets_1.withoutSecretsOf)(stored.type, stored.params) } : {};
|
|
28
|
+
}
|
|
29
|
+
(0, connection_secrets_1.refuseSecretParams)(type, name, written.params);
|
|
30
|
+
return { params: typeChanged ? written.params : (0, connection_secrets_1.keepStoredSecrets)(type, written.params, stored?.params) };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A write names an existing row only through this: the row of that id or name **in the request's
|
|
34
|
+
* domain**. `save` writes by primary key, so an id from another domain left in the object would
|
|
35
|
+
* overwrite that domain's row - the lookup alone does not stop it.
|
|
36
|
+
*/
|
|
37
|
+
function notInThisDomain(connection) {
|
|
38
|
+
return new shell_1.Refusal('connection-not-in-domain', { connection }, `There is no connection "${connection}" in this domain.`);
|
|
39
|
+
}
|
|
12
40
|
let ConnectionMutation = class ConnectionMutation {
|
|
13
41
|
async createConnection(connection, context) {
|
|
14
42
|
const { domain, user, tx } = context.state;
|
|
15
43
|
return await tx.getRepository(connection_1.Connection).save({
|
|
16
44
|
...connection,
|
|
45
|
+
...paramsToStore(connection),
|
|
17
46
|
domain,
|
|
18
47
|
creator: user,
|
|
19
48
|
updater: user
|
|
@@ -25,9 +54,14 @@ let ConnectionMutation = class ConnectionMutation {
|
|
|
25
54
|
const connection = await repository.findOne({
|
|
26
55
|
where: { domain: { id: domain.id }, name }
|
|
27
56
|
});
|
|
57
|
+
if (!connection) {
|
|
58
|
+
throw notInThisDomain(name);
|
|
59
|
+
}
|
|
28
60
|
return await repository.save({
|
|
29
61
|
...connection,
|
|
30
62
|
...patch,
|
|
63
|
+
...paramsToStore(patch, connection),
|
|
64
|
+
id: connection.id,
|
|
31
65
|
updater: user
|
|
32
66
|
});
|
|
33
67
|
}
|
|
@@ -39,9 +73,11 @@ let ConnectionMutation = class ConnectionMutation {
|
|
|
39
73
|
const connectionRepo = tx.getRepository(connection_1.Connection);
|
|
40
74
|
if (_createRecords.length > 0) {
|
|
41
75
|
for (let i = 0; i < _createRecords.length; i++) {
|
|
42
|
-
|
|
76
|
+
/* A new row: an id it carries would make `save` overwrite the row of that id, wherever it is. */
|
|
77
|
+
const { id, ...newRecord } = _createRecords[i];
|
|
43
78
|
const result = await connectionRepo.save({
|
|
44
79
|
...newRecord,
|
|
80
|
+
...paramsToStore(newRecord),
|
|
45
81
|
domain,
|
|
46
82
|
creator: user,
|
|
47
83
|
updater: user
|
|
@@ -52,10 +88,15 @@ let ConnectionMutation = class ConnectionMutation {
|
|
|
52
88
|
if (_updateRecords.length > 0) {
|
|
53
89
|
for (let i = 0; i < _updateRecords.length; i++) {
|
|
54
90
|
const newRecord = _updateRecords[i];
|
|
55
|
-
const connection = await connectionRepo.findOneBy({ id: newRecord.id });
|
|
91
|
+
const connection = await connectionRepo.findOneBy({ id: newRecord.id, domain: { id: domain.id } });
|
|
92
|
+
if (!connection) {
|
|
93
|
+
throw notInThisDomain(newRecord.name || newRecord.id);
|
|
94
|
+
}
|
|
56
95
|
const result = await connectionRepo.save({
|
|
57
96
|
...connection,
|
|
58
97
|
...newRecord,
|
|
98
|
+
...paramsToStore(newRecord, connection),
|
|
99
|
+
id: connection.id,
|
|
59
100
|
updater: user
|
|
60
101
|
});
|
|
61
102
|
results.push({ ...result, cuFlag: 'M' });
|
|
@@ -163,6 +204,7 @@ let ConnectionMutation = class ConnectionMutation {
|
|
|
163
204
|
await repository.save({
|
|
164
205
|
...sameId,
|
|
165
206
|
...connection,
|
|
207
|
+
...paramsToStore(connection, sameId),
|
|
166
208
|
domain,
|
|
167
209
|
updater: user
|
|
168
210
|
});
|
|
@@ -171,6 +213,7 @@ let ConnectionMutation = class ConnectionMutation {
|
|
|
171
213
|
}
|
|
172
214
|
await repository.save({
|
|
173
215
|
...connection,
|
|
216
|
+
...paramsToStore(connection),
|
|
174
217
|
domain,
|
|
175
218
|
updater: user,
|
|
176
219
|
creator: user
|
|
@@ -264,6 +307,7 @@ tslib_1.__decorate([
|
|
|
264
307
|
tslib_1.__metadata("design:returntype", Promise)
|
|
265
308
|
], ConnectionMutation.prototype, "importConnections", null);
|
|
266
309
|
exports.ConnectionMutation = ConnectionMutation = tslib_1.__decorate([
|
|
267
|
-
(0, type_graphql_1.Resolver)(connection_1.Connection)
|
|
310
|
+
(0, type_graphql_1.Resolver)(connection_1.Connection),
|
|
311
|
+
(0, type_graphql_1.UseMiddleware)(shell_1.SpeakRefusals)
|
|
268
312
|
], ConnectionMutation);
|
|
269
313
|
//# sourceMappingURL=connection-mutation.js.map
|