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:
Devanshu Gupta 2025-08-11 18:59:49 +05:30 committed by GitHub
parent b2c28617ad
commit cb0a87e1a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 670 additions and 41 deletions

View file

@ -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%')}
/>

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -0,0 +1,5 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map
dist/*

View file

@ -0,0 +1,4 @@
# Microsoft_graph
Documentation on: https://docs.tooljet.com/docs/data-sources/microsoft_graph

View file

@ -0,0 +1,7 @@
'use strict';
const microsoft_graph = require('../lib');
describe('microsoft_graph', () => {
it.todo('needs tests');
});

View 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

View 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 };
}
}

View 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": []
}

View 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"
}
}
}
}

View 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;
};

View 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"
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

View file

@ -53,6 +53,9 @@
},
"scopes": {
"value": "full"
},
"oauth_type": {
"value": "custom_app"
}
},
"properties": {

View file

@ -207,6 +207,7 @@ function fetchEnvVariables(pluginKind, keyAppend) {
const dataSourcePrefix = {
googlecalendar: 'GOOGLE',
snowflake: 'SNOWFLAKE',
microsoft_graph: 'MICROSOFT',
};
const key = dataSourcePrefix[pluginKind] + '_' + keyAppend;
return key;

View file

@ -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"
}
]

View file

@ -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;