Tribal/mszy/sits (#3614)
* Initial Commit * Changed to include production, test and development endpoints * tweaking the urls * Moved from incorrectly placed in independent-publisher-connectors * Tweaking the readme * tweaking readme and name of action * Included etag support for PUT requests * - added missing bits to the readme.md - removed empty strings - Made the clientid [DUMMY] * Added the missing definitions * Changed 201 description to be created * Added a sentence in the publisher section * added the word Publisher * - Cosmetic changes to make it clear that an entity will be replaced - Made If-Match property visible and mandatory as it is required - Tweaked readme to change update to replcace - Removed known issue which has been resolved with Microsoft * Added new SITS connector * changed so that the icon colour is valid * added required headers * added SITS.UI and removed clientid * Some wording tweaks * More minor wording tweaks --------- Co-authored-by: mszy-tribal <81627647+mszy-tribal@users.noreply.github.com>
This commit is contained in:
Родитель
344373cfbf
Коммит
5844c9e8ef
|
@ -0,0 +1,41 @@
|
|||
# Tribal - SITS
|
||||
Streamline the day-to-day administration of student management to enhance student experience.
|
||||
|
||||
## Publisher: Tribal
|
||||
We provide the expertise, software and services required to underpin student success.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Documentation
|
||||
[EMEA Documentation](https://help.tribaledge.com/emea/edge/EdgeEducation.htm)
|
||||
[APAC Documentation](https://help.tribaledge.com/apac/edge/EdgeEducation.htm)
|
||||
|
||||
## Supported Operations
|
||||
**Record** refers to one of the available business records in Tribal like such as Person, Schools, Desks, Applications etc.
|
||||
|
||||
### Triggers
|
||||
- `When an event happens`: Triggers anytime an event on an entity happens within Tribal SITS.
|
||||
|
||||
### Actions
|
||||
- `Create an entity`: Creates an entity within Tribal Maytas.
|
||||
- `Read record`: Reads a record within Tribal SITS.
|
||||
- `Read records`: Reads many records within Tribal SITS.
|
||||
- `Update record`: Update a record in Tribal SITS.
|
||||
- `Delete record`: Delete a record in Tribal SITS.
|
||||
- `Send an HTTP request`: Performs a custom request on a relative path for Tribal SITS.
|
||||
|
||||
## Obtaining Credentials
|
||||
1. Sign in to create a connection using your Tribal account to define the following:
|
||||
- Environment such as Live, Test or Development.
|
||||
- Region such as APAC or EMEA.
|
||||
- Edge Tenant ID as supplied by Tribal.
|
||||
|
||||
2. On sign in, you must enable the following permissions:
|
||||
- Events Connector Endpoint
|
||||
- Connect to the web hooks
|
||||
|
||||
## Known Issues and Limitations
|
||||
None
|
||||
|
||||
## Deployment instructions
|
||||
Please use [these instructions](https://docs.microsoft.com/en-us/connectors/custom-connectors/paconn-cli) to deploy this connector as custom connector in Microsoft Power Automate and Power Apps
|
|
@ -0,0 +1,322 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// Power Automate connector script
|
||||
/// </summary>
|
||||
public class Script : ScriptBase
|
||||
{
|
||||
/// <summary>
|
||||
///EqualsSeparator
|
||||
/// </summary>
|
||||
private static readonly char[] EqualsSeparator = new[] { '=' };
|
||||
private static readonly string SegmentPatternWithRecord = "Record";
|
||||
private static readonly string ServiceDomain = "siw_api/";
|
||||
public override async Task<HttpResponseMessage> ExecuteAsync()
|
||||
{
|
||||
if (Context!.OperationId == "RawRequest")
|
||||
{
|
||||
return await HandleRawRequestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await HandleNonRawRequestsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> HandleRawRequestAsync()
|
||||
{
|
||||
var request = Context!.Request;
|
||||
if(request?.Content == null)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent("Content not defined on request")
|
||||
};
|
||||
}
|
||||
var content = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
var obj = JObject.Parse(content);
|
||||
var body = GetValue<string>(obj, "body");
|
||||
|
||||
var verb = GetValue<string>(obj, "verb");
|
||||
|
||||
var url = $"{request.RequestUri}{ServiceDomain}{GetValue<string>(obj, "relativeUrl")?.TrimStart('/')}";
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent($"Invalid Url Format '{url}'")
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(body))
|
||||
{
|
||||
request.Content = CreateJsonContent(body);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(verb))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent("Verb must be provided.")
|
||||
};
|
||||
}
|
||||
|
||||
url = AppendQueryParamsRawRequest(url, obj);
|
||||
|
||||
request.Method = new HttpMethod(verb);
|
||||
request.RequestUri = new Uri(url);
|
||||
|
||||
foreach (var token in GetValue<JArray>(obj, "headers") ?? new JArray())
|
||||
{
|
||||
var item = token.Value<JObject>();
|
||||
|
||||
var key = GetValue<string>(item, "headerKey");
|
||||
if (key == null)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent("Headers are invalid.")
|
||||
};
|
||||
}
|
||||
|
||||
var value = GetValue<string>(item, "headerValue");
|
||||
// Need to to add without validation because the etag causes it to fail
|
||||
request.Headers.TryAddWithoutValidation(key, value);
|
||||
}
|
||||
|
||||
return await Context.SendAsync(request, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string AppendQueryParamsRawRequest(string url, JObject obj)
|
||||
{
|
||||
var tokens = GetValue<JArray>(obj, "query") ?? new JArray();
|
||||
if(tokens.Count > 0)
|
||||
{
|
||||
url += "?";
|
||||
}
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var item = token.Value<JObject>();
|
||||
|
||||
var key = GetValue<string>(item, "queryKey");
|
||||
var value = GetValue<string>(item, "queryValue");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
url += $"{key}={value}&";
|
||||
}
|
||||
}
|
||||
|
||||
if (url.EndsWith("&"))
|
||||
{
|
||||
url = url.TrimEnd('&');
|
||||
}
|
||||
|
||||
if (url.EndsWith("?"))
|
||||
{
|
||||
url = url.TrimEnd('?');
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> HandleNonRawRequestsAsync()
|
||||
{
|
||||
var request = Context?.Request;
|
||||
if (request == null)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent("Request context is not available.")
|
||||
};
|
||||
}
|
||||
|
||||
var uri = request.RequestUri;
|
||||
if (uri == null)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent("Request URI is not available.")
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var queryParams = ExtractQueryParams(uri);
|
||||
var verb = GetLastSegment(uri);
|
||||
var route = DecodeQueryParam(queryParams, "route");
|
||||
var version = DecodeQueryParam(queryParams, "version");
|
||||
|
||||
var url = BuildUrl(uri, route);
|
||||
|
||||
var content = await request.Content!.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var obj = JObject.Parse(content);
|
||||
|
||||
url = ReplaceRouteValues(url, obj);
|
||||
|
||||
url = AppendQueryParams(url, obj);
|
||||
|
||||
AddHeadersToRequest(request, obj, version);
|
||||
|
||||
request.RequestUri = new Uri(url);
|
||||
request.Method = new HttpMethod(verb);
|
||||
|
||||
var body = GetValue<JObject>(obj, "body");
|
||||
|
||||
request.Content = CreateJsonContent(JsonConvert.SerializeObject(body));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = CreateJsonContent(ex.Message)
|
||||
};
|
||||
}
|
||||
|
||||
return await Context!.SendAsync(request, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExtractQueryParams
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
private static Dictionary<string, string> ExtractQueryParams(Uri uri)
|
||||
{
|
||||
if (uri == null || string.IsNullOrEmpty(uri.Query))
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
return uri.Query.TrimStart('?')
|
||||
.Split('&')
|
||||
.Where(part => !string.IsNullOrEmpty(part) && part.Contains('='))
|
||||
.Select(part => part.Split(EqualsSeparator, 2))
|
||||
.ToDictionary(
|
||||
part => part[0],
|
||||
part => part.Length > 1 ? part[1] : string.Empty,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
/// <summary>
|
||||
/// GetLastSegment
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetLastSegment(Uri uri)
|
||||
{
|
||||
if (uri.Segments.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("The URI does not contain any segments.");
|
||||
}
|
||||
string lastSegment = uri.Segments[uri.Segments.Length - 1];
|
||||
|
||||
if (lastSegment.Contains(SegmentPatternWithRecord))
|
||||
{
|
||||
return lastSegment.Split(new[] { SegmentPatternWithRecord }, StringSplitOptions.None)[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The last segment does not match the expected pattern.");
|
||||
}
|
||||
/// <summary>
|
||||
/// DecodeQueryParam
|
||||
/// </summary>
|
||||
/// <param name="queryParams"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
private static string DecodeQueryParam(Dictionary<string, string>? queryParams, string key)
|
||||
{
|
||||
return HttpUtility.UrlDecode(queryParams?[key])!;
|
||||
}
|
||||
/// <summary>
|
||||
/// BuildUrl
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="route"></param>
|
||||
/// <returns></returns>
|
||||
private static string BuildUrl(Uri uri, string route)
|
||||
{
|
||||
if (uri.Segments.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("The URI does not contain any segments.");
|
||||
}
|
||||
string lastSegment = uri.Segments[uri.Segments.Length - 1];
|
||||
return uri.ToString()
|
||||
.Replace(lastSegment, ServiceDomain + route?.TrimStart('/'))
|
||||
.Split('?')[0];
|
||||
}
|
||||
/// <summary>
|
||||
/// ReplaceRouteValues
|
||||
/// </summary>
|
||||
/// <param name="part"></param>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
private static string ReplaceRouteValues(string part, JObject obj)
|
||||
{
|
||||
part = HttpUtility.UrlDecode(part);
|
||||
|
||||
var routeValues = GetValue<JObject>(obj, "Path") ?? new JObject();
|
||||
|
||||
foreach (var routeValue in routeValues)
|
||||
{
|
||||
part = part.Replace($"{{{routeValue.Key}}}", routeValue.Value?.ToString());
|
||||
}
|
||||
return part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AppendQueryParams
|
||||
/// </summary>
|
||||
/// <param name="part"></param>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
private static string AppendQueryParams(string part, JObject obj)
|
||||
{
|
||||
part += "?";
|
||||
var queries = GetValue<JObject>(obj, "Query") ?? new JObject();
|
||||
foreach (var query in queries)
|
||||
{
|
||||
part += $"{query.Key}={query.Value}&";
|
||||
}
|
||||
|
||||
if (part.EndsWith("&"))
|
||||
{
|
||||
part = part.TrimEnd('&');
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AddHeadersToRequest
|
||||
/// </summary>
|
||||
/// <param name="request">Request</param>
|
||||
/// <param name="obj">Body</param>
|
||||
/// <param name="version">Version</param>
|
||||
private static void AddHeadersToRequest(HttpRequestMessage request, JObject obj, string version)
|
||||
{
|
||||
var headers = GetValue<JObject>(obj, "Header") ?? new JObject();
|
||||
foreach (var header in headers)
|
||||
{
|
||||
var value = header.Value?.Value<string>() ?? string.Empty;
|
||||
request.Headers.TryAddWithoutValidation(header.Key, value);
|
||||
}
|
||||
request.Headers.TryAddWithoutValidation("version", version);
|
||||
}
|
||||
/// <summary>
|
||||
/// GetValue
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
private static T? GetValue<T>(JObject? obj, string key) where T : class
|
||||
{
|
||||
return obj?.GetValue(key, StringComparison.InvariantCulture)?.Value<T>();
|
||||
}
|
||||
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"properties": {
|
||||
"connectionParameters": {
|
||||
"token": {
|
||||
"type": "oauthSetting",
|
||||
"oAuthSettings": {
|
||||
"identityProvider": "oauth2generic",
|
||||
"clientId": "[Dummy]",
|
||||
"scopes": [
|
||||
"SITS.UI",
|
||||
"sits.documents.api.read",
|
||||
"sits.documents.api.write",
|
||||
"sits.finance.api.write",
|
||||
"sits.referencedata.api.read",
|
||||
"sits.students.api.read",
|
||||
"sits.students.api.write",
|
||||
"sits.students.sensitivecharacteristics.api.read",
|
||||
"SITSGateway.Connector.MetaData",
|
||||
"web_hooks",
|
||||
"edge",
|
||||
"openid",
|
||||
"edge_identity",
|
||||
"offline_access"
|
||||
],
|
||||
"redirectMode": "Global",
|
||||
"redirectUrl": "https://global.consent.azure-apim.net/redirect",
|
||||
"properties": {
|
||||
"IsFirstParty": "False",
|
||||
"IsOnbehalfofLoginSupported": false
|
||||
},
|
||||
"customParameters": {
|
||||
"authorizationUrlTemplate": {
|
||||
"value": "https://identity{environment}/{region}/ids/{tenantId}/connect/authorize"
|
||||
},
|
||||
"tokenUrlTemplate": {
|
||||
"value": "https://identity{environment}/{region}/ids/{tenantId}/connect/token"
|
||||
},
|
||||
"refreshUrlTemplate": {
|
||||
"value": "https://identity{environment}/{region}/ids/{tenantId}/connect/token"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"token:environment": {
|
||||
"type": "string",
|
||||
"uiDefinition": {
|
||||
"constraints": {
|
||||
"tabIndex": 0,
|
||||
"required": "true",
|
||||
"allowedValues": [
|
||||
{
|
||||
"text": "Live",
|
||||
"value": ".tribaledge.com"
|
||||
},
|
||||
{
|
||||
"text": "Development",
|
||||
"value": "-master.edge.tribaldev.net"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Environment",
|
||||
"displayName": "Environment",
|
||||
"tooltip": "Environment"
|
||||
}
|
||||
},
|
||||
"token:region": {
|
||||
"type": "string",
|
||||
"uiDefinition": {
|
||||
"constraints": {
|
||||
"tabIndex": 0,
|
||||
"required": "true",
|
||||
"allowedValues": [
|
||||
{
|
||||
"text": "Emea",
|
||||
"value": "emea"
|
||||
},
|
||||
{
|
||||
"text": "Apac",
|
||||
"value": "apac"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Region",
|
||||
"displayName": "Region",
|
||||
"tooltip": "Region"
|
||||
}
|
||||
},
|
||||
"token:tenantId": {
|
||||
"type": "string",
|
||||
"uiDefinition": {
|
||||
"constraints": {
|
||||
"tabIndex": 2,
|
||||
"required": "true"
|
||||
},
|
||||
"description": "Tenant Id for Tribal Edge",
|
||||
"displayName": "Edge Tenant Id",
|
||||
"tooltip": "Tenant Id for Tribal Edge"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconBrandColor": "#0077C4",
|
||||
"scriptOperations": [
|
||||
"ReadRecord",
|
||||
"ReadRecords",
|
||||
"DeleteRecord",
|
||||
"CreateRecord",
|
||||
"UpdateRecord",
|
||||
"RawRequest"
|
||||
],
|
||||
"capabilities": [],
|
||||
"policyTemplateInstances": [
|
||||
{
|
||||
"templateId": "dynamichosturl",
|
||||
"title": "Host URL",
|
||||
"parameters": {
|
||||
"x-ms-apimTemplateParameter.urlTemplate": "https://api@connectionParameters('token:environment')/@connectionParameters('token:region')/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"templateId": "setheader",
|
||||
"title": "Tenant Header",
|
||||
"parameters": {
|
||||
"x-ms-apimTemplateParameter.name": "tenantId",
|
||||
"x-ms-apimTemplateParameter.value": "@connectionParameters('token:tenantId')",
|
||||
"x-ms-apimTemplateParameter.existsAction": "override",
|
||||
"x-ms-apimTemplate-policySection": "Request"
|
||||
}
|
||||
}
|
||||
],
|
||||
"publisher": "Tribal Education Ltd."
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче