mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
Feat/ms graph pre release (#13581)
* ee commit * merge commit * feat: updated openapi operation component * updated query operation sepctype * fix: updated query dropdown style * init plugin * init plugin * feat: config dropdown update * feat: added ms plugin * fix: plugin connection name * submodule reference updated * plugin label updated * added back margin top class --------- Co-authored-by: Ganesh Kumar <ganesh8056234@gmail.com>
This commit is contained in:
parent
b2c28617ad
commit
cb0a87e1a1
19 changed files with 670 additions and 41 deletions
|
|
@ -63,7 +63,10 @@ const ApiEndpointInput = (props) => {
|
|||
|
||||
// Check if specUrl is an object (multiple specs) or string (single spec)
|
||||
const isMultiSpec = typeof props.specUrl === 'object' && !Array.isArray(props.specUrl);
|
||||
const [selectedSpecType, setSelectedSpecType] = useState(isMultiSpec ? Object.keys(props.specUrl)[0] || '' : null);
|
||||
// Initialize selectedSpecType from props.options.specType if available
|
||||
const [selectedSpecType, setSelectedSpecType] = useState(
|
||||
isMultiSpec ? props.options?.specType || Object.keys(props.specUrl)[0] || '' : null
|
||||
);
|
||||
|
||||
const fetchOpenApiSpec = (specUrlOrType) => {
|
||||
setLoadingSpec(true);
|
||||
|
|
@ -177,6 +180,7 @@ const ApiEndpointInput = (props) => {
|
|||
[paramName]: parsedValue,
|
||||
},
|
||||
},
|
||||
...(isMultiSpec && { specType: selectedSpecType }), // Include specType if multiSpec
|
||||
};
|
||||
setOptions(newOptions);
|
||||
props.optionsChanged(newOptions);
|
||||
|
|
@ -204,25 +208,24 @@ const ApiEndpointInput = (props) => {
|
|||
|
||||
if (path && operation) {
|
||||
return (
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="me-2" style={{ minWidth: '60px' }}>
|
||||
<span className={`badge bg-${operationColorMapping[operation]}`}>{operation.toUpperCase()}</span>
|
||||
<div>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className={`badge bg-${operationColorMapping[operation]} me-2`}>{operation.toUpperCase()}</span>
|
||||
<span>{path}</span>
|
||||
</div>
|
||||
<div className="flex-grow-1">
|
||||
<div>{path}</div>
|
||||
{summary && !isSelected && (
|
||||
<small className="text-muted d-block" style={{ fontSize: '0.875em' }}>
|
||||
{summary && !isSelected && (
|
||||
<div>
|
||||
<small className="d-block" style={{ fontSize: '0.875em', color: '#a4a8ab', marginTop: '1px' }}>
|
||||
{summary}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return 'Select an operation';
|
||||
}
|
||||
};
|
||||
|
||||
const categorizeOperations = (operation, path, acc, category) => {
|
||||
const operationData = specJson.paths[path][operation];
|
||||
const summary = operationData?.summary || '';
|
||||
|
|
@ -286,7 +289,14 @@ const ApiEndpointInput = (props) => {
|
|||
request: props.options?.params?.request ?? {},
|
||||
};
|
||||
setLoadingSpec(true);
|
||||
setOptions({ ...props.options, params: queryParams });
|
||||
|
||||
// Initialize options with specType if multiSpec
|
||||
const initialOptions = {
|
||||
...props.options,
|
||||
params: queryParams,
|
||||
...(isMultiSpec && { specType: selectedSpecType }),
|
||||
};
|
||||
setOptions(initialOptions);
|
||||
|
||||
if (!isMultiSpec) {
|
||||
fetchOpenApiSpec();
|
||||
|
|
@ -299,10 +309,30 @@ const ApiEndpointInput = (props) => {
|
|||
}
|
||||
}, [selectedSpecType]);
|
||||
|
||||
const handleSpecTypeChange = (val) => {
|
||||
setSelectedSpecType(val);
|
||||
// When spec type changes, immediately update options with new specType
|
||||
const newOptions = {
|
||||
...options,
|
||||
specType: val,
|
||||
// Clear operation-specific data when changing spec type
|
||||
operation: null,
|
||||
path: null,
|
||||
selectedOperation: null,
|
||||
params: {
|
||||
path: {},
|
||||
query: {},
|
||||
request: {},
|
||||
},
|
||||
};
|
||||
setOptions(newOptions);
|
||||
props.optionsChanged(newOptions);
|
||||
};
|
||||
|
||||
const specTypeOptions = isMultiSpec
|
||||
? Object.keys(props.specUrl).map((key) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
}))
|
||||
: [];
|
||||
|
||||
|
|
@ -312,13 +342,16 @@ const ApiEndpointInput = (props) => {
|
|||
{isMultiSpec && (
|
||||
<div className="d-flex g-2 mb-3">
|
||||
<div className="col-3 form-label">
|
||||
<label className="form-label">{props.t('globals.specType', 'Spec Type')}</label>
|
||||
<label className="form-label">{props.t('globals.specType', 'Entity')}</label>
|
||||
</div>
|
||||
<div className="col flex-grow-1">
|
||||
<Select
|
||||
options={specTypeOptions}
|
||||
value={{ value: selectedSpecType, label: selectedSpecType }}
|
||||
onChange={(val) => setSelectedSpecType(val)}
|
||||
value={{
|
||||
value: selectedSpecType,
|
||||
label: selectedSpecType.charAt(0).toUpperCase() + selectedSpecType.slice(1),
|
||||
}}
|
||||
onChange={(val) => handleSpecTypeChange(val)}
|
||||
width={'100%'}
|
||||
styles={queryManagerSelectComponentStyle(props.darkMode, '100%')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ const OAuthWrapper = ({
|
|||
options?.auth_type?.value === 'oauth2' &&
|
||||
options?.grant_type?.value === 'authorization_code' &&
|
||||
multiple_auth_enabled !== true;
|
||||
const dataSourceNameCapitalize = capitalize(selectedDataSource?.plugin?.name || selectedDataSource?.kind);
|
||||
|
||||
const dataSourceNameCapitalize = capitalize(
|
||||
selectedDataSource?.plugin?.manifestFile?.data?.source?.name || selectedDataSource?.kind
|
||||
);
|
||||
const hostUrl = window.public_config?.TOOLJET_HOST;
|
||||
const subPathUrl = window.public_config?.SUB_PATH;
|
||||
const fullUrl = `${hostUrl}${subPathUrl ? subPathUrl : '/'}oauth2/authorize`;
|
||||
|
|
@ -72,7 +73,6 @@ const OAuthWrapper = ({
|
|||
return (
|
||||
<div>
|
||||
<div>
|
||||
<label className="form-label">Connection type</label>
|
||||
<OAuth
|
||||
isGrpc={false}
|
||||
grant_type={options?.grant_type?.value}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,13 @@ const CommonOAuthFields = ({
|
|||
}),
|
||||
shallow
|
||||
);
|
||||
const [isCloud, setIsCloud] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (oauthTypes?.default_value && !options?.oauth_type?.value) {
|
||||
optionchanged('oauth_type', oauthTypes.default_value);
|
||||
}
|
||||
setIsCloud(checkIfToolJetCloud(tooljetVersion));
|
||||
}, []);
|
||||
|
||||
const oauthTypeOptions = React.useMemo(() => {
|
||||
|
|
@ -97,7 +99,7 @@ const CommonOAuthFields = ({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{oauthTypes?.required && (
|
||||
{oauthTypes?.required && oauthTypeOptions && oauthTypeOptions.length > 1 && (
|
||||
<div className="col-md-12">
|
||||
<label className="form-label mt-3">OAuth type</label>
|
||||
<Select
|
||||
|
|
@ -359,16 +361,18 @@ const OAuthConfiguration = ({
|
|||
return (
|
||||
<div>
|
||||
<div className="row mt-3">
|
||||
<label className="form-label" data-cy="label-grant-type">
|
||||
Grant type
|
||||
</label>
|
||||
<Select
|
||||
options={grantTypeOptions()}
|
||||
value={grant_type}
|
||||
onChange={(value) => optionchanged('grant_type', value)}
|
||||
width={'100%'}
|
||||
useMenuPortal={false}
|
||||
/>
|
||||
{allowed_grant_types && allowed_grant_types.length > 1 && (
|
||||
<div>
|
||||
<label className="form-label">Grant type</label>
|
||||
<Select
|
||||
options={grantTypeOptions()}
|
||||
value={grant_type}
|
||||
onChange={(value) => optionchanged('grant_type', value)}
|
||||
width={'100%'}
|
||||
useMenuPortal={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CommonOAuthFields
|
||||
clientConfig={clientConfig}
|
||||
tokenConfig={tokenConfig}
|
||||
|
|
|
|||
|
|
@ -59,14 +59,19 @@ const OAuth = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
options={authOptions(isGrpc)}
|
||||
value={auth_type}
|
||||
onChange={(value) => optionchanged('auth_type', value)}
|
||||
width={'100%'}
|
||||
useMenuPortal={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{authOptions(isGrpc).length > 1 && (
|
||||
<div>
|
||||
<label className="form-label">Connection type</label>
|
||||
<Select
|
||||
options={authOptions(isGrpc)}
|
||||
value={auth_type}
|
||||
onChange={(value) => optionchanged('auth_type', value)}
|
||||
width={'100%'}
|
||||
useMenuPortal={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ElementToRender
|
||||
add_token_to={add_token_to}
|
||||
header_prefix={header_prefix}
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce', 'googlecalendar'];
|
||||
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce', 'googlecalendar', 'microsoft_graph'];
|
||||
const name = selectedDataSource.name;
|
||||
const kind = selectedDataSource?.kind;
|
||||
const pluginId = selectedDataSourcePluginId;
|
||||
|
|
@ -965,7 +965,15 @@ class DataSourceManagerComponent extends React.Component {
|
|||
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
|
||||
? `https://docs.tooljet.ai/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
||||
: `https://docs.tooljet.ai/docs/data-sources/${selectedDataSource?.kind}`;
|
||||
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce', 'googlecalendar', 'snowflake'];
|
||||
const OAuthDs = [
|
||||
'slack',
|
||||
'zendesk',
|
||||
'googlesheets',
|
||||
'salesforce',
|
||||
'googlecalendar',
|
||||
'snowflake',
|
||||
'microsoft_graph',
|
||||
];
|
||||
return (
|
||||
pluginsLoaded && (
|
||||
<div>
|
||||
|
|
|
|||
5
marketplace/plugins/microsoft_graph/.gitignore
vendored
Normal file
5
marketplace/plugins/microsoft_graph/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
||||
dist/*
|
||||
4
marketplace/plugins/microsoft_graph/README.md
Normal file
4
marketplace/plugins/microsoft_graph/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Microsoft_graph
|
||||
|
||||
Documentation on: https://docs.tooljet.com/docs/data-sources/microsoft_graph
|
||||
7
marketplace/plugins/microsoft_graph/__tests__/index.js
Normal file
7
marketplace/plugins/microsoft_graph/__tests__/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const microsoft_graph = require('../lib');
|
||||
|
||||
describe('microsoft_graph', () => {
|
||||
it.todo('needs tests');
|
||||
});
|
||||
13
marketplace/plugins/microsoft_graph/lib/icon.svg
Normal file
13
marketplace/plugins/microsoft_graph/lib/icon.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_791_45)">
|
||||
<path d="M11.4919 11.5073H2V2.07062H11.4919V11.5073Z" fill="#F1511B"/>
|
||||
<path d="M21.972 11.5073H12.4803V2.07062H21.972V11.5073Z" fill="#80CC28"/>
|
||||
<path d="M11.4916 21.9302H2V12.4936H11.4916V21.9302Z" fill="#00ADEF"/>
|
||||
<path d="M21.972 21.9302H12.4803V12.4936H21.972V21.9302Z" fill="#FBBC09"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_791_45">
|
||||
<rect width="20" height="20" fill="white" transform="translate(2 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
302
marketplace/plugins/microsoft_graph/lib/index.ts
Normal file
302
marketplace/plugins/microsoft_graph/lib/index.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
import {
|
||||
QueryError,
|
||||
QueryResult,
|
||||
QueryService,
|
||||
User,
|
||||
App,
|
||||
validateAndSetRequestOptionsBasedOnAuthType,
|
||||
} from '@tooljet-marketplace/common';
|
||||
import { SourceOptions, ConvertedFormat } from './types';
|
||||
import got, { OptionsOfTextResponseBody, Headers } from 'got';
|
||||
|
||||
export default class Microsoft_graph implements QueryService {
|
||||
authUrl(source_options: SourceOptions) {
|
||||
const { clientId, clientSecret, scopes, tenantId, redirectUri } = this.getOAuthCredentials(source_options);
|
||||
|
||||
if (!clientId) {
|
||||
throw new Error('Client Id is required');
|
||||
}
|
||||
if (!clientSecret) {
|
||||
throw new Error('Client Secret is required');
|
||||
}
|
||||
if (!scopes) {
|
||||
throw new Error('Scope is required');
|
||||
}
|
||||
if (!tenantId) {
|
||||
throw new Error('Tenant is required');
|
||||
}
|
||||
|
||||
const authState = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
response_type: 'code',
|
||||
redirect_uri: redirectUri,
|
||||
response_mode: 'query',
|
||||
scope: scopes,
|
||||
state: authState,
|
||||
});
|
||||
|
||||
const baseUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
|
||||
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async accessDetailsFrom(authCode: string, source_options): Promise<object> {
|
||||
const { clientId, clientSecret, tenantId, scopes, redirectUri } = this.getOAuthCredentials(source_options);
|
||||
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
|
||||
const tokenRequestBody = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authCode,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: scopes,
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await got(tokenEndpoint, {
|
||||
method: 'post',
|
||||
headers,
|
||||
form: tokenRequestBody,
|
||||
});
|
||||
|
||||
const tokenData = JSON.parse(response.body);
|
||||
const authDetails = [];
|
||||
|
||||
if (tokenData.access_token) {
|
||||
authDetails.push(['access_token', tokenData.access_token]);
|
||||
}
|
||||
if (tokenData.refresh_token) {
|
||||
authDetails.push(['refresh_token', tokenData.refresh_token]);
|
||||
}
|
||||
if (tokenData.expires_in) {
|
||||
authDetails.push(['expires_in', tokenData.expires_in.toString()]);
|
||||
}
|
||||
if (tokenData.token_type) {
|
||||
authDetails.push(['token_type', tokenData.token_type]);
|
||||
}
|
||||
if (tokenData.scope) {
|
||||
authDetails.push(['scope', tokenData.scope]);
|
||||
}
|
||||
|
||||
return authDetails;
|
||||
} catch (error) {
|
||||
throw new QueryError('Authorization Error', error.message, { error: error });
|
||||
}
|
||||
}
|
||||
|
||||
async run(
|
||||
sourceOptions: any,
|
||||
queryOptions: any,
|
||||
dataSourceId: string,
|
||||
dataSourceUpdatedAt: string,
|
||||
context?: { user?: User; app?: App }
|
||||
): Promise<QueryResult> {
|
||||
let result = {};
|
||||
if (sourceOptions['oauth_type'] === 'tooljet_app') {
|
||||
sourceOptions['client_id'] = process.env.MICROSOFT_CLIENT_ID;
|
||||
sourceOptions['client_secret'] = process.env.MICROSOFT_CLIENT_SECRET;
|
||||
}
|
||||
const operation = queryOptions.operation;
|
||||
const accessToken = sourceOptions['access_token'];
|
||||
const baseUrl = 'https://graph.microsoft.com/v1.0';
|
||||
const path = queryOptions['path'];
|
||||
let url = `${baseUrl}${path}`;
|
||||
const pathParams = queryOptions['params']['path'];
|
||||
const queryParams = queryOptions['params']['query'];
|
||||
const bodyParams = queryOptions['params']['request'];
|
||||
|
||||
for (const param of Object.keys(pathParams)) {
|
||||
url = url.replace(`{${param}}`, pathParams[param]);
|
||||
}
|
||||
|
||||
let requestOptions;
|
||||
if (sourceOptions['multiple_auth_enabled']) {
|
||||
const customHeaders = { 'tj-x-forwarded-for': '::1' };
|
||||
const newSourcOptions = this.constructSourceOptions(sourceOptions);
|
||||
const authValidatedRequestOptions = this.convertQueryOptions(queryOptions, customHeaders);
|
||||
|
||||
const _requestOptions = await validateAndSetRequestOptionsBasedOnAuthType(
|
||||
newSourcOptions,
|
||||
context,
|
||||
authValidatedRequestOptions as any,
|
||||
{ kind: 'microsoft_graph' }
|
||||
);
|
||||
if (_requestOptions.status === 'needs_oauth') return _requestOptions;
|
||||
requestOptions = _requestOptions.data as OptionsOfTextResponseBody;
|
||||
} else {
|
||||
requestOptions =
|
||||
operation === 'get' || operation === 'delete'
|
||||
? {
|
||||
method: operation,
|
||||
headers: this.authHeader(accessToken),
|
||||
searchParams: queryParams,
|
||||
}
|
||||
: {
|
||||
method: operation,
|
||||
headers: this.authHeader(accessToken),
|
||||
json: bodyParams,
|
||||
searchParams: queryParams,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const response = await got(url, requestOptions);
|
||||
if (response && response.body) {
|
||||
try {
|
||||
result = JSON.parse(response.body);
|
||||
} catch (parseError) {
|
||||
result = response.body;
|
||||
}
|
||||
} else {
|
||||
result = 'Query Success';
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body) {
|
||||
try {
|
||||
result = JSON.parse(error.response.body);
|
||||
} catch (parseError) {
|
||||
result = error.response.body;
|
||||
}
|
||||
const message = result?.['error']?.['message'];
|
||||
throw new QueryError('Query could not be completed', message, result || {});
|
||||
} else {
|
||||
const errorMessage = error?.message === 'Query could not be completed' ? error?.description : error?.message;
|
||||
throw new QueryError('Query could not be completed', errorMessage, error || {});
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'ok',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
authHeader(token: string): Headers {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
private constructSourceOptions(sourceOptions) {
|
||||
const baseUrl = 'https://graph.microsoft.com/v1.0';
|
||||
const tenantId = sourceOptions['tenant_id'] || 'common';
|
||||
const accessTokenUrl = sourceOptions['access_token_url'];
|
||||
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
|
||||
const scope = 'https://graph.microsoft.com/.default';
|
||||
|
||||
const addSourceOptions = {
|
||||
url: baseUrl,
|
||||
auth_url: authUrl,
|
||||
add_token_to: 'header',
|
||||
header_prefix: 'Bearer ',
|
||||
access_token_url: accessTokenUrl,
|
||||
audience: '',
|
||||
username: '',
|
||||
password: '',
|
||||
bearer_token: '',
|
||||
client_auth: 'header',
|
||||
headers: [
|
||||
['', ''],
|
||||
['tj-x-forwarded-for', '::1'],
|
||||
],
|
||||
custom_query_params: [['', '']],
|
||||
custom_auth_params: [['', '']],
|
||||
access_token_custom_headers: [['', '']],
|
||||
ssl_certificate: 'none',
|
||||
retry_network_errors: true,
|
||||
scopes: this.encodeOAuthScope(scope),
|
||||
};
|
||||
|
||||
const newSourceOptions = {
|
||||
...sourceOptions,
|
||||
...addSourceOptions,
|
||||
};
|
||||
|
||||
return newSourceOptions;
|
||||
}
|
||||
|
||||
private encodeOAuthScope(scope: string): string {
|
||||
return encodeURIComponent(scope);
|
||||
}
|
||||
|
||||
private convertQueryOptions(queryOptions: any, customHeaders?: Record<string, string>): any {
|
||||
// Extract operation and params
|
||||
const { operation, params } = queryOptions;
|
||||
|
||||
// Start building the result
|
||||
const result: ConvertedFormat = {
|
||||
method: operation.toLowerCase(),
|
||||
headers: customHeaders || {},
|
||||
};
|
||||
|
||||
// Convert query params to URLSearchParams if they exist
|
||||
if (params.query && Object.keys(params.query).length > 0) {
|
||||
const urlParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params.query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => urlParams.append(key, String(v)));
|
||||
} else {
|
||||
urlParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result.searchParams = urlParams;
|
||||
}
|
||||
|
||||
if (!['get', 'delete'].includes(result.method) && params.request) {
|
||||
result.json = params.request;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private normalizeSourceOptions(source_options: any): Record<string, any> {
|
||||
if (!Array.isArray(source_options)) {
|
||||
return source_options;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
source_options.forEach((item) => {
|
||||
normalized[item.key] = item.value;
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private getOptionValue(option: any): any {
|
||||
if (option?.value !== undefined) {
|
||||
return option.value;
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
getOAuthCredentials(source_options: any) {
|
||||
const options = this.normalizeSourceOptions(source_options);
|
||||
const oauthType = this.getOptionValue(options.oauth_type);
|
||||
let clientId = this.getOptionValue(options.client_id);
|
||||
let clientSecret = this.getOptionValue(options.client_secret);
|
||||
const tenantId = this.getOptionValue(options.tenant_id);
|
||||
const accessTokenUrl = this.getOptionValue(options.access_token_url);
|
||||
const scopes = this.getOptionValue(options.scopes);
|
||||
|
||||
if (oauthType === 'tooljet_app') {
|
||||
clientId = process.env.MICROSOFT_CLIENT_ID;
|
||||
clientSecret = process.env.MICROSOFT_CLIENT_SECRET;
|
||||
}
|
||||
|
||||
const host = process.env.TOOLJET_HOST;
|
||||
const subpath = process.env.SUB_PATH;
|
||||
const fullUrl = `${host}${subpath ? subpath : '/'}`;
|
||||
const redirectUri = `${fullUrl}oauth2/authorize`;
|
||||
|
||||
return { clientId, clientSecret, tenantId, accessTokenUrl, scopes, redirectUri };
|
||||
}
|
||||
}
|
||||
142
marketplace/plugins/microsoft_graph/lib/manifest.json
Normal file
142
marketplace/plugins/microsoft_graph/lib/manifest.json
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"title": "Microsoft Graph",
|
||||
"description": "Plugin to use Microsoft 365 services (Outlook, Calendars, OneDrive etc)",
|
||||
"type": "api",
|
||||
"source": {
|
||||
"name": "Microsoft Graph",
|
||||
"kind": "microsoft_graph",
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
},
|
||||
"options": {
|
||||
"tenant_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"access_token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
}
|
||||
},
|
||||
"customTesting": true
|
||||
},
|
||||
"defaults": {
|
||||
"username": {
|
||||
"value": ""
|
||||
},
|
||||
"account": {
|
||||
"value": ""
|
||||
},
|
||||
"password": {
|
||||
"value": ""
|
||||
},
|
||||
"database": {
|
||||
"value": ""
|
||||
},
|
||||
"schema": {
|
||||
"value": ""
|
||||
},
|
||||
"warehouse": {
|
||||
"value": ""
|
||||
},
|
||||
"role": {
|
||||
"value": ""
|
||||
},
|
||||
"client_id": {
|
||||
"value": ""
|
||||
},
|
||||
"client_secret": {
|
||||
"value": ""
|
||||
},
|
||||
"tenant_id": {
|
||||
"value": ""
|
||||
},
|
||||
"access_token_url": {
|
||||
"value": "https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token"
|
||||
},
|
||||
"redirect_url": {
|
||||
"value": ""
|
||||
},
|
||||
"auth_url": {
|
||||
"value": ""
|
||||
},
|
||||
"allowed_auth_types": {
|
||||
"value": "oauth2"
|
||||
},
|
||||
"auth_type": {
|
||||
"value": "oauth2"
|
||||
},
|
||||
"grant_type": {
|
||||
"value": "authorization_code"
|
||||
},
|
||||
"scopes": {
|
||||
"value": "https://graph.microsoft.com/.default"
|
||||
},
|
||||
"access_token_custom_headers": {
|
||||
"value": [
|
||||
["Content-Type", "application/x-www-form-urlencoded"]
|
||||
]
|
||||
},
|
||||
"oauth_type": {
|
||||
"value": "custom_app"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"tenant_id": {
|
||||
"label": "Tenant",
|
||||
"key": "tenant_id",
|
||||
"type": "text",
|
||||
"description": "Enter tenant"
|
||||
},
|
||||
"access_token_url": {
|
||||
"label": "Access token URL",
|
||||
"key": "access_token_url",
|
||||
"type": "text",
|
||||
"description": "Enter access token url",
|
||||
"helpText": "Replace < {{tenant_id}} > with the actual tenent value"
|
||||
},
|
||||
"oauth": {
|
||||
"key": "oauth",
|
||||
"type": "react-component-oauth",
|
||||
"description": "A component for Microsoft Graph",
|
||||
"oauth_configs": {
|
||||
"allowed_field_groups": {
|
||||
"authorization_code": [
|
||||
"client_id",
|
||||
"client_secret"
|
||||
]
|
||||
},
|
||||
"allowed_auth_types": [
|
||||
"oauth2"
|
||||
],
|
||||
"oauthTypes": {
|
||||
"required": true,
|
||||
"default_value": "custom_app",
|
||||
"editions": {
|
||||
"ce": [
|
||||
"custom_app"
|
||||
],
|
||||
"ee": [
|
||||
"custom_app"
|
||||
],
|
||||
"cloud": [
|
||||
"custom_app",
|
||||
"tooljet_app"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allowed_grant_types": [
|
||||
"authorization_code"
|
||||
],
|
||||
"allowed_scope_field": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
23
marketplace/plugins/microsoft_graph/lib/operations.json
Normal file
23
marketplace/plugins/microsoft_graph/lib/operations.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "Microsoft Graph datasource",
|
||||
"description": "A schema defining Microsoft_graph datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"operation": {
|
||||
"label": "",
|
||||
"key": "ms_graph_operation",
|
||||
"type": "react-component-api-endpoint",
|
||||
"description": "Single select dropdown for operation",
|
||||
"specUrl": {
|
||||
"Outlook": "https://raw.githubusercontent.com/adishM98/base-repo-testing/refs/heads/main/outlook.json",
|
||||
"Calendar": "https://raw.githubusercontent.com/adishM98/base-repo-testing/refs/heads/main/calendar.json",
|
||||
"Users": "https://raw.githubusercontent.com/adishM98/base-repo-testing/refs/heads/main/users.json",
|
||||
"Teams": "https://raw.githubusercontent.com/adishM98/base-repo-testing/refs/heads/main/teams.yaml",
|
||||
"OneDrive": "https://raw.githubusercontent.com/adishM98/base-repo-testing/refs/heads/main/onedrive.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
marketplace/plugins/microsoft_graph/lib/types.ts
Normal file
30
marketplace/plugins/microsoft_graph/lib/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { OptionsOfTextResponseBody } from 'got';
|
||||
|
||||
export type SourceOptions = {
|
||||
client_id: OptionData;
|
||||
client_secret: OptionData;
|
||||
scopes: OptionData;
|
||||
tenant_id: OptionData;
|
||||
};
|
||||
export type QueryOptions = {
|
||||
operation: string;
|
||||
};
|
||||
|
||||
type OptionData = {
|
||||
value: string;
|
||||
encypted: boolean;
|
||||
};
|
||||
|
||||
export type ConvertedFormat = {
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
searchParams?: URLSearchParams;
|
||||
json?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type QueryResult = {
|
||||
status: 'ok' | 'failed' | 'needs_oauth';
|
||||
errorMessage?: string;
|
||||
data: Array<object> | object | OptionsOfTextResponseBody;
|
||||
metadata?: Array<object> | object;
|
||||
};
|
||||
27
marketplace/plugins/microsoft_graph/package.json
Normal file
27
marketplace/plugins/microsoft_graph/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/microsoft_graph",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"build": "ncc build lib/index.ts -o dist",
|
||||
"watch": "ncc build lib/index.ts -o dist --watch"
|
||||
},
|
||||
"homepage": "https://github.com/tooljet/tooljet#readme",
|
||||
"dependencies": {
|
||||
"@tooljet-marketplace/common": "^1.0.0",
|
||||
"got": "^14.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
11
marketplace/plugins/microsoft_graph/tsconfig.json
Normal file
11
marketplace/plugins/microsoft_graph/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@
|
|||
},
|
||||
"scopes": {
|
||||
"value": "full"
|
||||
},
|
||||
"oauth_type": {
|
||||
"value": "custom_app"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ function fetchEnvVariables(pluginKind, keyAppend) {
|
|||
const dataSourcePrefix = {
|
||||
googlecalendar: 'GOOGLE',
|
||||
snowflake: 'SNOWFLAKE',
|
||||
microsoft_graph: 'MICROSOFT',
|
||||
};
|
||||
const key = dataSourcePrefix[pluginKind] + '_' + keyAppend;
|
||||
return key;
|
||||
|
|
|
|||
|
|
@ -245,5 +245,13 @@
|
|||
"id": "googlecalendar",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Sat, 12 Jul 2025 14:33:45 GMT"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft Graph",
|
||||
"description": "Plugin to use Microsoft 365 services (Outlook, Calendars, OneDrive etc)",
|
||||
"version": "1.0.0",
|
||||
"id": "microsoft_graph",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Thu, 31 Jul 2025 08:56:30 GMT"
|
||||
}
|
||||
]
|
||||
|
|
@ -530,7 +530,9 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
|
|||
// Auth flow starts from datasource config page
|
||||
if (
|
||||
!isMultiAuthEnabled &&
|
||||
['googlesheets', 'slack', 'zendesk', 'salesforce', 'googlecalendar', 'snowflake'].includes(dataSource.kind)
|
||||
['googlesheets', 'slack', 'zendesk', 'salesforce', 'googlecalendar', 'snowflake', 'microsoft_graph'].includes(
|
||||
dataSource.kind
|
||||
)
|
||||
) {
|
||||
tokenOptions = await this.fetchAPITokenFromPlugins(dataSource, code, sourceOptions);
|
||||
}
|
||||
|
|
@ -642,6 +644,7 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
|
|||
const dataSourcePrefix = {
|
||||
googlecalendar: 'GOOGLE',
|
||||
snowflake: 'SNOWFLAKE',
|
||||
microsoft_graph: 'MICROSFT',
|
||||
};
|
||||
const key = dataSourcePrefix[pluginKind] + '_' + keyAppend;
|
||||
return key;
|
||||
|
|
|
|||
Loading…
Reference in a new issue