* 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:
mgszy 2024-08-19 17:05:53 +01:00 коммит произвёл GitHub
Родитель 344373cfbf
Коммит 5844c9e8ef
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 2047 добавлений и 0 удалений

Просмотреть файл

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