Add execution resource

This commit is contained in:
ricardo 2022-04-28 20:44:24 -04:00
parent e8ab7d5468
commit ffdce3b1a2
9 changed files with 530 additions and 68 deletions

2
package-lock.json generated
View file

@ -134,7 +134,7 @@
"eslint-plugin-vue": "^7.16.0",
"eventsource": "^1.0.7",
"express": "^4.16.4",
"express-openapi-validator": "^4.13.7",
"express-openapi-validator": "^4.13.6",
"fast-glob": "^3.2.5",
"fflate": "^0.7.0",
"file-saver": "^2.0.2",

View file

@ -0,0 +1,66 @@
import { In, Equal, Not, ObjectLiteral, MoreThan, LessThan } from 'typeorm';
import { Db, IExecutionFlattedDb } from '../..';
import { ExecutionStatus } from '../publicApiRequest';
function getStatusCondition(status: ExecutionStatus): ObjectLiteral {
const condition: ObjectLiteral = {};
if (status === 'success') {
condition.finished = true;
} else if (status === 'running') {
condition.stoppedAt = Equal(null);
} else if (status === 'waiting') {
condition.waitTill = Not(null);
} else if (status === 'error') {
condition.stoppedAt = Not(null);
}
return condition;
}
export async function getExecutions(data: {
limit: number;
lastId?: number;
workflowIds?: number[];
status?: ExecutionStatus;
}): Promise<IExecutionFlattedDb[]> {
const executions = await Db.collections.Execution.find({
where: {
...(data.lastId && { id: LessThan(data.lastId) }),
...(data.status && { ...getStatusCondition(data.status) }),
...(data.workflowIds && { workflowId: In(data.workflowIds) }),
},
order: { id: 'DESC' },
take: data.limit,
});
return executions;
}
export async function getExecutionsCount(data: {
limit: number;
lastId?: number;
workflowIds?: number[];
status?: ExecutionStatus;
}): Promise<number> {
const executions = await Db.collections.Execution.count({
where: {
id: LessThan(data.lastId),
...(data.status && { ...getStatusCondition(data.status) }),
...(data.workflowIds && { workflowId: In(data.workflowIds) }),
},
take: data.limit,
});
return executions;
}
export async function getExecution(id: number): Promise<IExecutionFlattedDb | undefined> {
const execution = await Db.collections.Execution.findOne({
where: {
id,
},
});
return execution;
}
export async function deleteExecution(execution: IExecutionFlattedDb): Promise<void> {
await Db.collections.Execution.remove(execution);
}

View file

@ -0,0 +1,24 @@
import { User } from '../../databases/entities/User';
import { Db } from '../..';
export async function getSharedWorkflowIds(user: User): Promise<number[]> {
const sharedWorkflows = await Db.collections.SharedWorkflow.find({
where: {
user,
},
});
return sharedWorkflows.map((workflow) => workflow.workflowId);
}
export async function getWorkflowAccess(
user: User,
workflowId: string | undefined,
): Promise<boolean> {
const sharedWorkflows = await Db.collections.SharedWorkflow.find({
where: {
user,
workflow: { id: workflowId },
},
});
return !!sharedWorkflows.length;
}

View file

@ -4,11 +4,11 @@
/* eslint-disable import/no-cycle */
// eslint-disable-next-line import/no-extraneous-dependencies
import { pick } from 'lodash';
import { In } from 'typeorm';
import { validate as uuidValidate } from 'uuid';
import { OpenAPIV3, Format } from 'express-openapi-validator/dist/framework/types';
import express = require('express');
import validator from 'validator';
import { In } from 'typeorm';
import { User } from '../databases/entities/User';
import type { Role } from '../databases/entities/Role';
import { ActiveWorkflowRunner, Db, InternalHooksManager, ITelemetryUserDeletionData } from '..';
@ -18,39 +18,57 @@ import { SharedWorkflow } from '../databases/entities/SharedWorkflow';
import { SharedCredentials } from '../databases/entities/SharedCredentials';
import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
interface IPaginationOffsetDecoded {
offset: number;
limit: number;
}
export type OperationID = 'getUsers' | 'getUser';
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
const { offset, limit } = JSON.parse(Buffer.from(cursor, 'base64').toString());
return {
offset,
limit,
};
type PaginationBase = { limit: number };
type PaginationOffsetDecoded = PaginationBase & { offset: number };
type PaginationCursorDecoded = PaginationBase & { lastId: number };
type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number };
type CursorPagination = PaginationBase & { lastId: number; numberOfNextRecords: number };
export const decodeCursor = (cursor: string): PaginationOffsetDecoded | PaginationCursorDecoded => {
return JSON.parse(Buffer.from(cursor, 'base64').toString()) as
| PaginationCursorDecoded
| PaginationOffsetDecoded;
};
export const encodeNextCursor = (
offset: number,
limit: number,
numberOfRecords: number,
): string | null => {
const retrieveRecordsLength = offset + limit;
if (retrieveRecordsLength < numberOfRecords) {
const encodeOffSetPagination = (pagination: OffsetPagination): string | null => {
if (pagination.numberOfTotalRecords > pagination.offset + pagination.limit) {
return Buffer.from(
JSON.stringify({
limit,
offset: offset + limit,
limit: pagination.limit,
offset: pagination.offset + pagination.limit,
}),
).toString('base64');
}
return null;
};
const encodeCursoPagination = (pagination: CursorPagination): string | null => {
if (pagination.numberOfNextRecords) {
return Buffer.from(
JSON.stringify({
lastId: pagination.lastId,
limit: pagination.limit,
}),
).toString('base64');
}
return null;
};
export const encodeNextCursor = (
pagination: OffsetPagination | CursorPagination,
): string | null => {
if ('offset' in pagination) {
return encodeOffSetPagination(pagination);
}
return encodeCursoPagination(pagination);
};
export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
return {
user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt', 'isPending'],
@ -193,13 +211,13 @@ export async function getAllUsersAndCount(data: {
limit?: number;
offset?: number;
}): Promise<[User[], number]> {
const users = await Db.collections.User!.find({
const users = await Db.collections.User.find({
where: {},
relations: data?.includeRole ? ['globalRole'] : undefined,
skip: data.offset,
take: data.limit,
});
const count = await Db.collections.User!.count();
const count = await Db.collections.User.count();
return [users, count];
}

View file

@ -11,10 +11,9 @@ import type { UserRequest } from '../requests';
import * as UserManagementMailer from '../UserManagement/email/UserManagementMailer';
import { decodeCursor, getGlobalMemberRole } from './helpers';
import { Role, PaginatatedRequest } from './publicApiRequest';
type Role = 'owner' | 'member';
const instanceOwnerSetup = (
export const instanceOwnerSetup = (
req: express.Request,
res: express.Response,
next: express.NextFunction,
@ -36,8 +35,8 @@ const emailSetup = (
next();
};
const authorize =
(role: [Role]) =>
export const authorize =
(role: Role[]) =>
(req: express.Request, res: express.Response, next: express.NextFunction): any => {
const {
globalRole: { name: userRole },
@ -76,17 +75,22 @@ const transferingToDeletedUser = (
next();
};
const validCursor = (
req: UserRequest.Get,
export const validCursor = (
req: PaginatatedRequest,
res: express.Response,
next: express.NextFunction,
): any => {
if (req.query.cursor) {
const { cursor } = req.query;
try {
const { offset, limit } = decodeCursor(cursor);
req.query.offset = offset;
req.query.limit = limit;
const paginationData = decodeCursor(cursor);
if ('offset' in paginationData) {
req.query.offset = paginationData.offset;
req.query.limit = paginationData.limit;
} else {
req.query.lastId = paginationData.lastId;
req.query.limit = paginationData.limit;
}
} catch (error) {
return res.status(400).json({
message: 'An invalid cursor was used',

View file

@ -0,0 +1,54 @@
/* eslint-disable import/no-cycle */
import express from 'express';
import type { User } from '../databases/entities/User';
export type ExecutionStatus = 'error' | 'running' | 'success' | 'waiting' | null;
export type Role = 'owner' | 'member';
export type AuthlessRequest<
RouteParams = {},
ResponseBody = {},
RequestBody = {},
RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery>;
export type AuthenticatedRequest<
RouteParams = {},
ResponseBody = {},
RequestBody = {},
RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
user: User;
};
export type PaginatatedRequest = AuthenticatedRequest<
{},
{},
{},
{
limit?: number;
cursor?: string;
offset?: number;
lastId?: number;
}
>;
export declare namespace ExecutionRequest {
type GetAll = AuthenticatedRequest<
{},
{},
{},
{
status?: ExecutionStatus;
limit?: number;
cursor?: string;
offset?: number;
workflowId?: number;
lastId?: number;
}
>;
type Get = AuthenticatedRequest<{ executionId: number }, {}, {}, {}>;
type Delete = Get;
}

View file

@ -0,0 +1,129 @@
import express = require('express');
import { ExecutionRequest } from '../../publicApiRequest';
import { encodeNextCursor } from '../../helpers';
import { authorize, instanceOwnerSetup, validCursor } from '../../middlewares';
import {
getExecutions,
getExecution,
deleteExecution,
getExecutionsCount,
} from '../../Services/execution';
import { getSharedWorkflowIds, getWorkflowAccess } from '../../Services/workflow';
export = {
deleteExecution: [
instanceOwnerSetup,
authorize(['owner', 'member']),
async (req: ExecutionRequest.Delete, res: express.Response): Promise<express.Response> => {
const { executionId } = req.params;
const execution = await getExecution(executionId);
if (execution === undefined) {
return res.status(404).json({
message: 'Execution not found.',
});
}
if (req.user.globalRole.name === 'owner') {
await deleteExecution(execution);
return res.json(execution);
}
const userHasAccessToWorkflow = await getWorkflowAccess(req.user, execution.workflowId);
if (userHasAccessToWorkflow) {
await deleteExecution(execution);
return res.json(execution);
}
return res.status(404).json({
message: 'Execution not found.',
});
},
],
getExecution: [
instanceOwnerSetup,
authorize(['owner', 'member']),
async (req: ExecutionRequest.Get, res: express.Response): Promise<express.Response> => {
const { executionId } = req.params;
const execution = await getExecution(executionId);
if (execution === undefined) {
return res.status(404).json({
message: 'Execution not found.',
});
}
if (req.user.globalRole.name === 'owner') {
return res.json(execution);
}
const userHasAccessToWorkflow = await getWorkflowAccess(req.user, execution.workflowId);
if (userHasAccessToWorkflow) {
return res.json(execution);
}
return res.status(404).json({
message: 'Execution not found.',
});
},
],
getExecutions: [
instanceOwnerSetup,
authorize(['owner', 'member']),
validCursor,
async (req: ExecutionRequest.GetAll, res: express.Response): Promise<express.Response> => {
const {
lastId = undefined,
limit = 100,
status = undefined,
workflowId = undefined,
} = req.query;
const filters = {
status,
limit,
lastId,
...(workflowId && { workflowIds: [workflowId] }),
};
if (req.user.globalRole.name === 'owner') {
const executions = await getExecutions(filters);
filters.lastId = executions.slice(-1)[0].id as number;
const count = await getExecutionsCount(filters);
return res.json({
data: executions,
nextCursor: encodeNextCursor({
lastId: filters.lastId,
limit,
numberOfNextRecords: count,
}),
});
}
const sharedWorkflowsIds = [];
if (!workflowId) {
const sharedWorkflows = await getSharedWorkflowIds(req.user);
sharedWorkflowsIds.push(...sharedWorkflows);
}
const executions = await getExecutions(filters);
const count = await getExecutionsCount(filters);
return res.json({
data: executions,
nextCursor: encodeNextCursor({
lastId: executions.slice(-1)[0].id as number,
limit,
numberOfNextRecords: count,
}),
});
},
],
};

View file

@ -135,7 +135,11 @@ export = {
return res.json({
data: clean(users, { includeRole }),
nextCursor: encodeNextCursor(offset, limit, count),
nextCursor: encodeNextCursor({
offset,
limit,
numberOfTotalRecords: count,
}),
});
},
],

View file

@ -18,8 +18,43 @@ servers:
tags:
- name: User
description: Operations about user
- name: Execution
description: Operations about execution
paths:
/users:
post:
x-eov-operation-id: createUsers
x-eov-operation-handler: v1/handlers/Users
tags:
- User
summary: Invite a user
description: Invites a user to your instance. Only available for the instance owner.
operationId: createUser
requestBody:
description: Created user object.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInformation'
required: true
responses:
"200":
description: A User object
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInformation'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
get:
x-eov-operation-id: getUsers
x-eov-operation-handler: v1/handlers/Users
@ -66,38 +101,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
x-eov-operation-id: createUsers
x-eov-operation-handler: v1/handlers/Users
tags:
- User
summary: Invite a user
description: Invites a user to your instance. Only available for the instance owner.
operationId: createUser
requestBody:
description: Created user object.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInformation'
required: true
responses:
"200":
description: A User object
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInformation'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users/{identifier}:
get:
x-eov-operation-id: getUser
@ -177,6 +180,121 @@ paths:
$ref: '#/components/schemas/Error'
"404":
description: User not found
/executions:
get:
x-eov-operation-id: getExecutions
x-eov-operation-handler: v1/handlers/Executions
tags:
- Execution
summary: Retrieve all executions
description: Retrieve all executions from your instance.
parameters:
- name: status
in: query
description: Status to filter the executions by.
required: false
schema:
type: string
enum: ['error', 'running', 'success', 'waiting']
default: 'any'
- name: workflowId
in: query
description: Workflow to filter the executions by.
required: false
schema:
type: number
example: 1000
- name: limit
in: query
description: The maximum number of items to return.
required: false
schema:
type: number
example: 100
default: 100
- name: cursor
in: query
description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request's response. Default value fetches the first "page" of the collection. See pagination for more detail.
required: false
style: form
schema:
type: string
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
responses:
"200":
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutionDetailsResponse'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"404":
description: User not found
/executions/{executionId}:
get:
x-eov-operation-id: getExecution
x-eov-operation-handler: v1/handlers/Executions
tags:
- Execution
summary: Retrieve an execution
description: Retrieve an execution from you instance.
parameters:
- name: executionId
in: path
description: The ID of the execution.
required: true
schema:
type: number
responses:
"200":
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutionInformation'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"404":
description: User not found
delete:
x-eov-operation-id: deleteExecution
x-eov-operation-handler: v1/handlers/Executions
tags:
- Execution
summary: Delete an execution
description: Deletes an execution from your instance.
parameters:
- name: executionId
in: path
description: The ID of the execution to be deleted.
required: true
schema:
type: number
responses:
"200":
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutionInformation'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"404":
description: User not found
components:
schemas:
Error:
@ -233,6 +351,18 @@ components:
readOnly: true
globalRole:
$ref: '#/components/schemas/RoleInformation'
ExecutionDetailsResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/ExecutionInformation'
nextCursor:
type: string
description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
UserDetailsResponse:
type: object
properties:
@ -245,6 +375,39 @@ components:
description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
ExecutionInformation:
type: object
properties:
id:
type: number
example: 1000
data:
type: string
finished:
type: boolean
example: true
mode:
type: string
enum: ['cli', 'error', 'integrated', 'internal', 'manual', 'retry', 'trigger', 'webhook']
retryOf:
type: string
nullable: true
retrySuccessId:
type: string
nullable: true
startedAt:
type: string
format: date-time
stoppedAt:
type: string
format: date-time
workflowId:
type: string
example: 1000
waitTill:
type: string
nullable: true
format: date-time
RoleInformation:
type: object
properties: