mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add PATCH method support to API components
This commit is contained in:
@@ -167,7 +167,7 @@ export class AccessTokenService extends BaseService {
|
||||
userId: ObjectID,
|
||||
): Promise<UserGlobalAccessPermission> {
|
||||
// query for all projects user belongs to.
|
||||
const teamMembers: Array<TeamMember> = await TeamMemberService.findBy({
|
||||
let teamMembers: Array<TeamMember> = await TeamMemberService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
hasAcceptedInvitation: true,
|
||||
@@ -182,6 +182,10 @@ export class AccessTokenService extends BaseService {
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMembers) {
|
||||
teamMembers = [];
|
||||
}
|
||||
|
||||
const projectIds: Array<ObjectID> = teamMembers.map(
|
||||
(teamMember: TeamMember) => {
|
||||
return teamMember.projectId!;
|
||||
|
||||
70
Common/Server/Types/Workflow/Components/API/Patch.ts
Normal file
70
Common/Server/Types/Workflow/Components/API/Patch.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import ComponentCode, { RunOptions, RunReturnType } from "../../ComponentCode";
|
||||
import { ApiComponentUtils } from "./Utils";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import APIException from "Common/Types/Exception/ApiException";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ComponentMetadata, { Port } from "Common/Types/Workflow/Component";
|
||||
import ComponentID from "Common/Types/Workflow/ComponentID";
|
||||
import APIComponents from "Common/Types/Workflow/Components/API";
|
||||
import API from "Common/Utils/API";
|
||||
|
||||
export default class ApiPut extends ComponentCode {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
const Component: ComponentMetadata | undefined = APIComponents.find(
|
||||
(i: ComponentMetadata) => {
|
||||
return i.id === ComponentID.ApiPatch;
|
||||
},
|
||||
);
|
||||
|
||||
if (!Component) {
|
||||
throw new BadDataException("Component not found.");
|
||||
}
|
||||
|
||||
this.setMetadata(Component);
|
||||
}
|
||||
|
||||
public override async run(
|
||||
args: JSONObject,
|
||||
options: RunOptions,
|
||||
): Promise<RunReturnType> {
|
||||
const result: { args: JSONObject; successPort: Port; errorPort: Port } =
|
||||
ApiComponentUtils.sanitizeArgs(this.getMetadata(), args, options);
|
||||
|
||||
let apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null = null;
|
||||
|
||||
try {
|
||||
apiResult = await API.patch(
|
||||
args["url"] as URL,
|
||||
args["request-body"] as JSONObject,
|
||||
args["request-headers"] as Dictionary<string>,
|
||||
);
|
||||
|
||||
return Promise.resolve({
|
||||
returnValues: ApiComponentUtils.getReturnValues(apiResult),
|
||||
executePort: result.successPort,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof HTTPErrorResponse) {
|
||||
return Promise.resolve({
|
||||
returnValues: ApiComponentUtils.getReturnValues(err),
|
||||
executePort: result.successPort,
|
||||
});
|
||||
}
|
||||
|
||||
if (apiResult) {
|
||||
return Promise.resolve({
|
||||
returnValues: ApiComponentUtils.getReturnValues(apiResult),
|
||||
executePort: result.successPort,
|
||||
});
|
||||
}
|
||||
|
||||
throw options.onError(new APIException("Something wrong happened."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBa
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import Text from "Common/Types/Text";
|
||||
import ComponentID from "Common/Types/Workflow/ComponentID";
|
||||
import ApiPatch from "./API/Patch";
|
||||
|
||||
const Components: Dictionary<ComponentCode> = {
|
||||
[ComponentID.Webhook]: new WebhookTrigger(),
|
||||
@@ -48,6 +49,7 @@ const Components: Dictionary<ComponentCode> = {
|
||||
[ComponentID.ApiGet]: new ApiGet(),
|
||||
[ComponentID.ApiPost]: new ApiPost(),
|
||||
[ComponentID.ApiDelete]: new ApiDelete(),
|
||||
[ComponentID.ApiPatch]: new ApiPatch(),
|
||||
[ComponentID.ApiPut]: new ApiPut(),
|
||||
[ComponentID.SendEmail]: new Email(),
|
||||
[ComponentID.IfElse]: new IfElse(),
|
||||
|
||||
@@ -28,6 +28,7 @@ jest.setTimeout(60000); // Increase test timeout to 60 seconds becuase GitHub ru
|
||||
|
||||
describe("TeamMemberService", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
await TestDatabaseMock.connectDbMock();
|
||||
});
|
||||
@@ -81,7 +82,6 @@ describe("TeamMemberService", () => {
|
||||
|
||||
describe("onBeforeCreate", () => {
|
||||
it("should throw exception if the user limit for a project is reached", async () => {
|
||||
const SEATS_LIMIT: number = 5;
|
||||
|
||||
const user: User = await UserServiceHelper.genrateAndSaveRandomUser(
|
||||
null,
|
||||
@@ -91,7 +91,9 @@ describe("TeamMemberService", () => {
|
||||
);
|
||||
|
||||
const project: Project =
|
||||
await ProjectServiceHelper.generateAndSaveRandomProject(null, {
|
||||
await ProjectServiceHelper.generateAndSaveRandomProject({
|
||||
seatLimit: 2,
|
||||
}, {
|
||||
isRoot: true,
|
||||
userId: user.id!,
|
||||
});
|
||||
@@ -105,12 +107,6 @@ describe("TeamMemberService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
ProjectService.findOneById = jest.fn().mockResolvedValue({
|
||||
seatLimit: SEATS_LIMIT,
|
||||
paymentProviderSubscriptionSeats: SEATS_LIMIT,
|
||||
_id: project._id,
|
||||
});
|
||||
|
||||
const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
@@ -119,9 +115,31 @@ describe("TeamMemberService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
await TeamMemberService.create({
|
||||
data: tm,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
// create another team member
|
||||
|
||||
const user2: User = await UserServiceHelper.genrateAndSaveRandomUser(
|
||||
null,
|
||||
{
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const tm2: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user2._id!),
|
||||
teamId: new ObjectID(team._id!),
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
TeamMemberService.create({
|
||||
data: tm,
|
||||
data: tm2,
|
||||
props: { isRoot: true },
|
||||
}),
|
||||
).rejects.toThrow(Errors.TeamMemberService.LIMIT_REACHED);
|
||||
@@ -192,7 +210,7 @@ describe("TeamMemberService", () => {
|
||||
const project: Project =
|
||||
await ProjectServiceHelper.generateAndSaveRandomProject(null, {
|
||||
isRoot: true,
|
||||
userId: user.id!,
|
||||
userId: user.id!,
|
||||
});
|
||||
|
||||
const team: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
|
||||
@@ -208,13 +226,12 @@ describe("TeamMemberService", () => {
|
||||
const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user._id!),
|
||||
teamId: new ObjectID(team._id!),
|
||||
miscDataProps: { email: nonExistingUserEmail },
|
||||
},
|
||||
);
|
||||
const teamMember: TeamMember = await TeamMemberService.create({
|
||||
data: tm,
|
||||
miscDataProps: { email: nonExistingUserEmail },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
@@ -253,9 +270,7 @@ describe("TeamMemberService", () => {
|
||||
const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
|
||||
{
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user._id!),
|
||||
teamId: new ObjectID(team._id!),
|
||||
miscDataProps: { email: nonExistingUserEmail },
|
||||
teamId: new ObjectID(team._id!)
|
||||
},
|
||||
);
|
||||
|
||||
@@ -265,6 +280,7 @@ describe("TeamMemberService", () => {
|
||||
|
||||
await TeamMemberService.create({
|
||||
data: tm,
|
||||
miscDataProps: { email: nonExistingUserEmail },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
@@ -606,12 +622,12 @@ describe("TeamMemberService", () => {
|
||||
userId: user.id!,
|
||||
});
|
||||
|
||||
const team: Team = TeamServiceHelper.generateRandomTeam({
|
||||
let team: Team = TeamServiceHelper.generateRandomTeam({
|
||||
projectId: new ObjectID(project._id!),
|
||||
});
|
||||
team.shouldHaveAtLeastOneMember = true;
|
||||
|
||||
await TeamService.create({
|
||||
team = await TeamService.create({
|
||||
data: team,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
@@ -669,11 +685,11 @@ describe("TeamMemberService", () => {
|
||||
userId: user.id!,
|
||||
});
|
||||
|
||||
const team: Team = TeamServiceHelper.generateRandomTeam({
|
||||
let team: Team = TeamServiceHelper.generateRandomTeam({
|
||||
projectId: new ObjectID(project._id!),
|
||||
});
|
||||
team.shouldHaveAtLeastOneMember = true;
|
||||
await TeamService.create({
|
||||
team = await TeamService.create({
|
||||
data: team,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
@@ -720,7 +736,10 @@ describe("TeamMemberService", () => {
|
||||
|
||||
describe("refreshTokens", () => {
|
||||
it("should refresh user global and tenant access permissions", async () => {
|
||||
jest.restoreAllMocks();
|
||||
// spy on refreshUserGlobalAccessPermission and refreshUserTenantAccessPermission
|
||||
|
||||
jest.spyOn(AccessTokenService, "refreshUserGlobalAccessPermission");
|
||||
jest.spyOn(AccessTokenService, "refreshUserTenantAccessPermission");
|
||||
|
||||
const userId: ObjectID = new ObjectID(
|
||||
Faker.generateRandomObjectID().toString(),
|
||||
@@ -758,33 +777,85 @@ describe("TeamMemberService", () => {
|
||||
userId: user.id!,
|
||||
});
|
||||
|
||||
TeamMemberService.findBy = jest.fn().mockResolvedValue([
|
||||
const teamA: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
|
||||
{
|
||||
_id: Faker.generateRandomObjectID().toString(),
|
||||
userId: Faker.generateRandomObjectID().toString(),
|
||||
memberId: Faker.generateRandomObjectID().toString(),
|
||||
projectId: new ObjectID(project.id!),
|
||||
},
|
||||
{
|
||||
_id: Faker.generateRandomObjectID().toString(),
|
||||
userId: "duplicated_id",
|
||||
memberId: Faker.generateRandomObjectID().toString(),
|
||||
isRoot: true,
|
||||
},
|
||||
);
|
||||
|
||||
const teamB: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
|
||||
{
|
||||
projectId: new ObjectID(project.id!),
|
||||
},
|
||||
{
|
||||
_id: Faker.generateRandomObjectID().toString(),
|
||||
userId: "duplicated_id",
|
||||
memberId: Faker.generateRandomObjectID().toString(),
|
||||
isRoot: true,
|
||||
},
|
||||
{
|
||||
_id: Faker.generateRandomObjectID().toString(),
|
||||
memberId: Faker.generateRandomObjectID().toString(),
|
||||
},
|
||||
]);
|
||||
);
|
||||
|
||||
// user A
|
||||
|
||||
const user1: User = await UserService.create({
|
||||
data: UserServiceHelper.generateRandomUser(),
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
// user B
|
||||
|
||||
const user2: User = await UserService.create({
|
||||
data: UserServiceHelper.generateRandomUser(),
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
// add these to team A
|
||||
|
||||
const teamMemberA1: TeamMember =
|
||||
TeamMemberServiceHelper.generateRandomTeamMember({
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user1._id!),
|
||||
teamId: new ObjectID(teamA._id!),
|
||||
});
|
||||
|
||||
await TeamMemberService.create({
|
||||
data: teamMemberA1,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const teamMemberA2: TeamMember =
|
||||
TeamMemberServiceHelper.generateRandomTeamMember({
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user2._id!),
|
||||
teamId: new ObjectID(teamA._id!),
|
||||
});
|
||||
|
||||
await TeamMemberService.create({
|
||||
data: teamMemberA2,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
// add user 2 to team B
|
||||
|
||||
const teamMemberB2: TeamMember =
|
||||
TeamMemberServiceHelper.generateRandomTeamMember({
|
||||
projectId: new ObjectID(project._id!),
|
||||
userId: new ObjectID(user2._id!),
|
||||
teamId: new ObjectID(teamB._id!),
|
||||
});
|
||||
|
||||
|
||||
await TeamMemberService.create({
|
||||
|
||||
data: teamMemberB2,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
const count: number =
|
||||
await TeamMemberService.getUniqueTeamMemberCountInProject(
|
||||
new ObjectID(project._id!),
|
||||
);
|
||||
expect(count).toBe(2);
|
||||
expect(count).toBe(3); // user, user1, user2
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1047,6 +1118,13 @@ describe("TeamMemberService", () => {
|
||||
const SUBSCRIPTION_ID: string = "subscriptionId";
|
||||
|
||||
it("should update subscription seats based on unique team members", async () => {
|
||||
|
||||
// spy on change quantity
|
||||
jest.spyOn(BillingService, "changeQuantity");
|
||||
|
||||
// spy on update project
|
||||
jest.spyOn(ProjectService, "updateOneById");
|
||||
|
||||
const user1: User = await UserService.create({
|
||||
data: UserServiceHelper.generateRandomUser(),
|
||||
props: { isRoot: true },
|
||||
@@ -1130,6 +1208,12 @@ describe("TeamMemberService", () => {
|
||||
process.env["SUBSCRIPTION_PLAN_1"] = undefined;
|
||||
process.env["SUBSCRIPTION_PLAN_2"] = undefined;
|
||||
|
||||
// spy on change quantity
|
||||
jest.spyOn(BillingService, "changeQuantity");
|
||||
|
||||
// spy on update project
|
||||
jest.spyOn(ProjectService, "updateOneById");
|
||||
|
||||
const user: User = await UserServiceHelper.genrateAndSaveRandomUser(
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@ import ProjectService from "../../../../Server/Services/ProjectService";
|
||||
|
||||
export interface ProjectData {
|
||||
seatLimit?: number;
|
||||
subscriptionId: string;
|
||||
currentSeatCount?: number;
|
||||
subscriptionId?: string;
|
||||
}
|
||||
|
||||
export default class ProjectTestService {
|
||||
@@ -37,6 +38,8 @@ export default class ProjectTestService {
|
||||
project.seatLimit = data.seatLimit;
|
||||
}
|
||||
|
||||
project.paymentProviderSubscriptionSeats = data?.currentSeatCount || 0;
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = 0;
|
||||
project.autoRechargeSmsOrCallByBalanceInUSD = 0;
|
||||
project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD = 0;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
|
||||
export default class TeamMemberTestService {
|
||||
public static generateRandomTeamMember(data: {
|
||||
projectId: ObjectID;
|
||||
userId: ObjectID;
|
||||
userId?: ObjectID | undefined;
|
||||
teamId: ObjectID;
|
||||
miscDataProps?: JSONObject;
|
||||
}): TeamMember {
|
||||
const teamMember: TeamMember = new TeamMember();
|
||||
|
||||
// required fields
|
||||
teamMember.userId = data.userId;
|
||||
if(data.userId) {
|
||||
teamMember.userId = data.userId;
|
||||
}
|
||||
|
||||
teamMember.projectId = data.projectId;
|
||||
teamMember.teamId = data.teamId;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ enum HTTPMethod {
|
||||
DELETE = "DELETE",
|
||||
PUT = "PUT",
|
||||
HEAD = "HEAD",
|
||||
PATCH = "PATCH",
|
||||
}
|
||||
|
||||
export default HTTPMethod;
|
||||
|
||||
@@ -13,6 +13,7 @@ enum ComponentID {
|
||||
ApiPut = "api-put",
|
||||
ApiPost = "api-post",
|
||||
ApiDelete = "api-delete",
|
||||
ApiPatch = "api-patch",
|
||||
SendEmail = "send-email",
|
||||
IfElse = "if-else",
|
||||
}
|
||||
|
||||
@@ -182,6 +182,92 @@ const components: Array<ComponentMetadata> = [
|
||||
id: ComponentID.ApiPut,
|
||||
title: "API Put (JSON)",
|
||||
category: "API",
|
||||
description: "Send a PUT Request and get JSON Response",
|
||||
iconProp: IconProp.Globe,
|
||||
componentType: ComponentType.Component,
|
||||
arguments: [
|
||||
{
|
||||
id: "url",
|
||||
name: "URL",
|
||||
description: "URL to send request to.",
|
||||
type: ComponentInputType.URL,
|
||||
required: true,
|
||||
placeholder: "https://api.yourcompany.com",
|
||||
},
|
||||
{
|
||||
id: "request-body",
|
||||
name: "Request Body",
|
||||
description: "Request Body in JSON",
|
||||
type: ComponentInputType.JSON,
|
||||
required: false,
|
||||
placeholder: 'Example: {"key1": "value1", "key2": "value2", ....}',
|
||||
},
|
||||
{
|
||||
id: "request-headers",
|
||||
name: "Request Headers",
|
||||
description: "Request headers to send.",
|
||||
type: ComponentInputType.StringDictionary,
|
||||
required: false,
|
||||
isAdvanced: true,
|
||||
placeholder:
|
||||
'Example: {"header1": "value1", "header2": "value2", ....}',
|
||||
},
|
||||
],
|
||||
returnValues: [
|
||||
{
|
||||
id: "error",
|
||||
name: "Error",
|
||||
description: "Error, if there is any.",
|
||||
type: ComponentInputType.Text,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: "response-status",
|
||||
name: "Response Status",
|
||||
description: "Response Status (200, for example)",
|
||||
type: ComponentInputType.Number,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: "response-headers",
|
||||
name: "Response Headers",
|
||||
description: "Response Headers for this request",
|
||||
type: ComponentInputType.StringDictionary,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: "response-body",
|
||||
name: "Response Body",
|
||||
description: "Response Body",
|
||||
type: ComponentInputType.JSON,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
inPorts: [
|
||||
{
|
||||
title: "In",
|
||||
description:
|
||||
"Please connect components to this port for this component to work.",
|
||||
id: "in",
|
||||
},
|
||||
],
|
||||
outPorts: [
|
||||
{
|
||||
title: "Success",
|
||||
description: "This is executed when the message is successfully posted",
|
||||
id: "success",
|
||||
},
|
||||
{
|
||||
title: "Error",
|
||||
description: "This is executed when there is an error",
|
||||
id: "error",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: ComponentID.ApiPatch,
|
||||
title: "API Patch (JSON)",
|
||||
category: "API",
|
||||
description: "Send a PATCH Request and get JSON Response",
|
||||
iconProp: IconProp.Globe,
|
||||
componentType: ComponentType.Component,
|
||||
|
||||
@@ -123,6 +123,23 @@ export default class API {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public async patch<
|
||||
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
|
||||
>(
|
||||
path: Route,
|
||||
data?: JSONObject | JSONArray,
|
||||
headers?: Headers,
|
||||
options?: RequestOptions,
|
||||
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
|
||||
return await API.patch<T>(
|
||||
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
|
||||
data,
|
||||
headers,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public async post<
|
||||
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
|
||||
>(
|
||||
@@ -270,6 +287,32 @@ export default class API {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static async patch<
|
||||
T extends
|
||||
| JSONObject
|
||||
| JSONArray
|
||||
| BaseModel
|
||||
| Array<BaseModel>
|
||||
| AnalyticsBaseModel
|
||||
| Array<AnalyticsBaseModel>,
|
||||
>(
|
||||
url: URL,
|
||||
data?: JSONObject | JSONArray,
|
||||
headers?: Headers,
|
||||
options?: RequestOptions,
|
||||
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
|
||||
return await this.fetch(
|
||||
HTTPMethod.PATCH,
|
||||
url,
|
||||
data,
|
||||
headers,
|
||||
undefined,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public static async post<
|
||||
T extends
|
||||
| JSONObject
|
||||
|
||||
Reference in New Issue
Block a user