Skip to content

Commit a5a5739

Browse files
committed
move example plugin for testing
1 parent fb75a07 commit a5a5739

File tree

12 files changed

+715
-0
lines changed

12 files changed

+715
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"name": "building",
4+
"type": "Building",
5+
"icon": "building",
6+
"singular": "Building",
7+
"plural": "Buildings"
8+
}
9+
]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"rowTypes": [
3+
{
4+
"name": "stuff",
5+
"metadata": [
6+
{ "name": "enum", "displayName": "Enum", "shape": "string", "role": "label" },
7+
{ "name": "start", "displayName": "Start", "shape": "date" },
8+
{ "name": "unixStart", "displayName": "Unix Start", "shape": ["number", { "decimalPlaces": 0 }] },
9+
{ "name": "end", "displayName": "End", "shape": "date" },
10+
{ "name": "unixEnd", "displayName": "Unix End", "shape": ["number", { "decimalPlaces": 0 }] },
11+
{ "name": "interval", "displayName": "Interval", "shape": "string" },
12+
{ "name": "top", "displayName": "Top", "shape": "string" }
13+
]
14+
}
15+
],
16+
"dataSources":[
17+
{
18+
"name": "dataSourceUnscoped",
19+
"displayName": "Stuff (no scope)",
20+
"description": "Return stuff",
21+
"supportedScope": "none"
22+
},
23+
{
24+
"name": "appScopedProperties",
25+
"displayName": "App-scoped Properties",
26+
"description": "Get selected properties for scoped apps",
27+
"supportedScope": "list",
28+
"targetNodesProperties": ["id", "name", "sourceId", "appType", "appStatus" ]
29+
}
30+
],
31+
"matches":{ "__configId": { "type": "equals", "value": "{{configId}}" } },
32+
"dataStreams":[
33+
{
34+
"displayName": "Stuff",
35+
"description": "Returns Stuff",
36+
"dataSourceName": "dataSourceUnscoped",
37+
"definition": {
38+
"name": "stuffUnscoped",
39+
"dataSourceConfig": {},
40+
"rowPath": [],
41+
"matches": "none",
42+
"rowType": { "name": "stuff" }
43+
},
44+
"template": [
45+
{
46+
"name": "top",
47+
"type": "text",
48+
"label": "Optional, list top n",
49+
"title": "Optional, list top n",
50+
"help": "Pick how many rows of data to return",
51+
"validation": { "required": false },
52+
"placeholder": "10"
53+
}
54+
]
55+
},
56+
{
57+
"displayName": "App Health",
58+
"description": "Returns Health for scoped Apps",
59+
"dataSourceName": "appScopedProperties",
60+
"provides": "health",
61+
"definition": {
62+
"name": "appHealth",
63+
"timeframes": false,
64+
"dataSourceConfig": { "properties": ["appStatus", "appType"] },
65+
"rowPath": [],
66+
"matches": {
67+
"sourceType": { "type": "equals", "value": "mySortOfApp" }
68+
},
69+
"metadata": [
70+
{ "name": "appStatus", "displayName": "Status", "shape": ["state",
71+
{ "map": { "success": ["OK"], "warning": ["Degraded", "Installing"], "error": ["Broken"] } }
72+
]},
73+
{ "name": "name", "displayName": "Name", "shape": "string", "role": "label" },
74+
{ "name": "id", "visible": false, "shape": "string", "role": "id" },
75+
{ "name": "appType", "displayName": "App Type", "shape": "string" }
76+
]
77+
}
78+
}
79+
]
80+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import {
2+
authBegin,
3+
authCodeResponse,
4+
configureContext,
5+
dataSourceFns,
6+
defaultApiLimits,
7+
importStages,
8+
initialPagingContext, reportImportProblem,
9+
testConfig as testConfigImpl
10+
} from './handlerConfig.js';
11+
12+
export const HandlerFunctionEnum = Object.freeze({
13+
testConfig: 'testConfig',
14+
importObjects: 'importObjects',
15+
readDataSource: 'readDataSource',
16+
oAuth2: 'oAuth2'
17+
});
18+
19+
// ============================================================================
20+
//
21+
// testConfig
22+
//
23+
export async function testConfig(event, api) {
24+
const { pluginConfig } = event;
25+
const { log, report, patchConfig, runtimeContext } = api;
26+
27+
const context = {
28+
pluginConfig,
29+
30+
log, report, patchConfig, runtimeContext
31+
};
32+
33+
await configureContext(context, HandlerFunctionEnum.testConfig);
34+
35+
return testConfigImpl(context);
36+
}
37+
38+
// ============================================================================
39+
//
40+
// importObjects
41+
//
42+
export async function importObjects(event, api) {
43+
const { pluginConfig, pagingContext } = event;
44+
const { log, report, patchConfig, runtimeContext } = api;
45+
46+
const context = {
47+
vertices: [], edges: [],
48+
49+
pluginConfig, pagingContext,
50+
51+
log, report, patchConfig, runtimeContext,
52+
53+
apiLimits: Object.assign({}, defaultApiLimits, pluginConfig.testSettings?.apiLimits ?? {})
54+
};
55+
const pageAPI = (context) => {
56+
return {
57+
get: (key) => context.pagingContext[key],
58+
set: (key, value) => { context.pagingContext[key] = value; },
59+
clear: () => { context.pagingContext = {}; }
60+
};
61+
};
62+
context.pageAPI = pageAPI(context);
63+
context.reportImportProblem = reportImportProblem(context);
64+
65+
await configureContext(context, HandlerFunctionEnum.importObjects);
66+
67+
if (Array.isArray(importStages) && importStages.length > 0) {
68+
69+
if (!context.pageAPI.get('squaredUp_isInit')) {
70+
// Set initial paging context values
71+
context.pageAPI.set('squaredUp_stage', 0);
72+
for (const [key, value] of Object.entries(initialPagingContext)) {
73+
context.pageAPI.set(key, value);
74+
}
75+
context.pageAPI.set('squaredUp_isInit', true);
76+
}
77+
78+
// Run through the appropriate stages until we've been running for 10 minutes or we've created results larger than 2MB.
79+
const maxElapsedTimeMSecs = pluginConfig.testSettings?.maxElapsedTimeMSecs ?? 10 * 60 * 1000;
80+
const maxPayloadSize = pluginConfig.testSettings?.maxPayloadSize ?? 2 * 1024 * 1024;
81+
let stage = context.pageAPI.get('squaredUp_stage');
82+
context.log.debug('importObjects starts: ' +
83+
`stage=${stage}, ` +
84+
`apiLimits=${JSON.stringify(context.apiLimits)}, ` +
85+
`maxElapsedTimeMSecs=${maxElapsedTimeMSecs}, ` +
86+
`maxPayloadSize=${maxPayloadSize}`);
87+
const start = Date.now();
88+
let elapsed;
89+
let payloadSize;
90+
let rateLimited = false;
91+
do {
92+
context.pageAPI.set('rateLimitDelay', undefined);
93+
if (await importStages[stage](context)) {
94+
// Stage reported it has finished... step to the next one
95+
stage++;
96+
context.pageAPI.set('squaredUp_stage', stage);
97+
98+
if (stage >= importStages.length) {
99+
// No more stages, so set pagingContext to an empty object to
100+
// indicate import is complete
101+
context.pageAPI.clear();
102+
break;
103+
}
104+
}
105+
elapsed = Date.now() - start;
106+
const pagingContextSize = JSON.stringify(context.pagingContext).length;
107+
payloadSize = JSON.stringify({ vertices: context.vertices, edges: context.edges, pagingContext: context.pagingContext }).length;
108+
const rateLimitDelay = context.pageAPI.get('rateLimitDelay') ?? 0;
109+
if (rateLimitDelay) {
110+
// Stage reported it was rate limited, so wait synchronously before continuing if we have time, otherwise
111+
// end this page of import and return the results so far.
112+
if (elapsed + rateLimitDelay < maxElapsedTimeMSecs && payloadSize < maxPayloadSize) {
113+
context.log.debug(`importObjects rate limited: elapsed = ${elapsed}, synchronously delaying ${rateLimitDelay} msecs`);
114+
await new Promise((resolve) => setTimeout(resolve, rateLimitDelay));
115+
elapsed = Date.now() - start;
116+
} else {
117+
context.log.debug(`importObjects rate limited: elapsed = ${elapsed}, ending page early`);
118+
rateLimited = true;
119+
}
120+
}
121+
context.log.debug(`importObjects looping: elapsed = ${elapsed}, payloadSize=${payloadSize}, pagingContextSize=${pagingContextSize}`);
122+
} while (!rateLimited && elapsed < maxElapsedTimeMSecs && payloadSize < maxPayloadSize);
123+
context.log.debug('importObjects loop ends');
124+
}
125+
126+
// Return the results
127+
const result = {
128+
vertices: context.vertices,
129+
edges: context.edges,
130+
pagingContext: context.pagingContext
131+
};
132+
return result;
133+
134+
}
135+
136+
// ============================================================================
137+
//
138+
// readDataSource
139+
//
140+
export async function readDataSource(event, api) {
141+
const { pluginConfig, dataSource, dataSourceConfig, targetNodes, timeframe } = event;
142+
const { log, report, patchConfig, runtimeContext } = api;
143+
144+
const context = {
145+
pluginConfig, dataSource, dataSourceConfig, targetNodes, timeframe,
146+
log, report, patchConfig, runtimeContext
147+
};
148+
149+
await configureContext(context, HandlerFunctionEnum.readDataSource);
150+
151+
const dataSourceFn = dataSourceFns[dataSource.name];
152+
if (!dataSourceFn) {
153+
throw new Error(`No data source function was found for data source ${dataSource.name}`);
154+
}
155+
156+
return dataSourceFn(context);
157+
}
158+
159+
// ============================================================================
160+
//
161+
// oAuth2
162+
//
163+
export async function oAuth2 ({ pluginConfig, dataSourceConfig, oAuth2Config }, { log, report, patchConfig }) {
164+
const context = {
165+
pluginConfig,
166+
dataSourceConfig,
167+
oAuth2Config,
168+
log,
169+
report,
170+
patchConfig
171+
};
172+
173+
await configureContext(context, HandlerFunctionEnum.oAuth2);
174+
175+
switch (dataSourceConfig.oAuth2Stage) {
176+
case 'oAuth2Begin':
177+
return authBegin(context);
178+
179+
case 'oAuth2CodeResponse':
180+
return authCodeResponse(context);
181+
182+
default:
183+
throw new Error(`Invalid oAuth2Stage: "${dataSourceConfig.oAuth2Stage}"`);
184+
}
185+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { HandlerFunctionEnum } from './handler.js';
2+
import { stageApps } from './importObjects/apps.js';
3+
import { stageBuildings } from './importObjects/building.js';
4+
import { appScopedProperties } from './readDataSource/appScopedProperties.js';
5+
import { dataSourceUnscoped } from './readDataSource/dataSourceUnscoped.js';
6+
7+
// ============================================================================
8+
//
9+
// testConfig
10+
//
11+
export async function testConfig(context) {
12+
const messages = [];
13+
14+
if (typeof context.pluginConfig.serverUrl === 'string' && context.pluginConfig.serverUrl.startsWith('https:')) {
15+
messages.push({
16+
'status': 'success',
17+
'message': 'Testing passed'
18+
});
19+
} else {
20+
messages.push({
21+
'status': 'warning',
22+
'message': 'serverUrl is invalid'
23+
});
24+
messages.push({
25+
'status': 'error',
26+
'message': 'nothing works!'
27+
});
28+
}
29+
30+
const result = {
31+
link: 'https://yourCompany.com/docs/plugin/pluginsetup-examplehybrid',
32+
messages: messages
33+
};
34+
return result;
35+
36+
}
37+
38+
// ============================================================================
39+
//
40+
// importObjects
41+
//
42+
export const importStages = [
43+
stageApps,
44+
stageBuildings
45+
];
46+
47+
export const defaultApiLimits = {
48+
apps: 10,
49+
buildings: 3
50+
};
51+
52+
export const initialPagingContext = {
53+
appIndex: 0,
54+
buildingIndex: 0,
55+
nextToken: undefined
56+
};
57+
58+
export function reportImportProblem(context) {
59+
return (err, stage) => {
60+
if (['UnrecognizedClientException', 'InvalidSignatureException'].includes(err.name)) {
61+
context.report.error('The configured access key details are invalid');
62+
} else if (['AccessDeniedException', 'AccessDenied', 'UnauthorizedOperation'].includes(err.name)) {
63+
context.log.warn(`The configured access key has no permission to import ${stage} objects`);
64+
} else {
65+
context.log.warn(`${stage} objects failed to import: ${err.message}`);
66+
}
67+
};
68+
}
69+
70+
// ============================================================================
71+
//
72+
// readDataSource
73+
//
74+
export const dataSourceFns = {
75+
appScopedProperties,
76+
dataSourceUnscoped
77+
};
78+
79+
// ============================================================================
80+
//
81+
// oAuth
82+
//
83+
export async function authBegin(context) {
84+
// Only needed if the plugin requires OAuth - consult documentation for more information
85+
throw new Error('Not implemented');
86+
}
87+
88+
export async function authCodeResponse(context) {
89+
// Only needed if the plugin requires OAuth - consult documentation for more information
90+
throw new Error('Not implemented');
91+
}
92+
93+
94+
// ============================================================================
95+
//
96+
// Generic
97+
//
98+
export async function configureContext(context, functionName) {
99+
// Used to apply universal configuration to your context object
100+
// For instance, a client from a library or a frequently used value
101+
context.retryCount = 0;
102+
103+
// You can use the function name to take different actions depending on the function being called
104+
if ([HandlerFunctionEnum.oAuth2, HandlerFunctionEnum.testConfig].includes(functionName)) {
105+
// For example, when dealing with OAuth or configuration tests, you might prefer handling certain tasks manually
106+
return;
107+
}
108+
109+
// Track what warnings have been sent to the user so we don't bombard them with the same message
110+
context.warnings = new Set();
111+
}

0 commit comments

Comments
 (0)