Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96544df7d5 | ||
![]() |
036db63a33 |
@@ -1,11 +1,11 @@
|
|||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({
|
let flow = await request.currentUser.$relatedQuery('flows').insert({
|
||||||
name: 'Name your flow',
|
name: 'Name your flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
flow = await flow.createInitialSteps();
|
||||||
|
|
||||||
renderObject(response, flow, { status: 201 });
|
renderObject(response, flow, { status: 201 });
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,7 @@ export default async (request, response) => {
|
|||||||
.findById(request.params.flowId)
|
.findById(request.params.flowId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const createdActionStep = await flow.createStepAfter(
|
const createdActionStep = await flow.createActionStep(
|
||||||
request.body.previousStepId
|
request.body.previousStepId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`Flow model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"active": {
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"publishedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"remoteWebhookId": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,72 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`SamlAuthProvider model > jsonSchema should have the correct schema 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"active": {
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"certificate": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"defaultRoleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"emailAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"entryPoint": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"firstnameAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"issuer": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"roleAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"signatureAlgorithm": {
|
|
||||||
"enum": [
|
|
||||||
"sha1",
|
|
||||||
"sha256",
|
|
||||||
"sha512",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"surnameAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"certificate",
|
|
||||||
"signatureAlgorithm",
|
|
||||||
"entryPoint",
|
|
||||||
"issuer",
|
|
||||||
"firstnameAttributeName",
|
|
||||||
"surnameAttributeName",
|
|
||||||
"emailAttributeName",
|
|
||||||
"roleAttributeName",
|
|
||||||
"defaultRoleId",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -28,3 +28,14 @@ exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`SamlAuthProvidersRoleMapping model > relationMappings should have samlAuthProvider relation 1`] = `
|
||||||
|
{
|
||||||
|
"join": {
|
||||||
|
"from": "saml_auth_providers_role_mappings.saml_auth_provider_id",
|
||||||
|
"to": "saml_auth_providers.id",
|
||||||
|
},
|
||||||
|
"modelClass": [Function],
|
||||||
|
"relation": [Function],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@@ -1,77 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`Step model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"appKey": {
|
|
||||||
"maxLength": 255,
|
|
||||||
"minLength": 1,
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"connectionId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"flowId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"type": "integer",
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"default": "incomplete",
|
|
||||||
"enum": [
|
|
||||||
"incomplete",
|
|
||||||
"completed",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"enum": [
|
|
||||||
"action",
|
|
||||||
"trigger",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"webhookPath": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,81 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`User model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"format": "email",
|
|
||||||
"maxLength": 255,
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"fullName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"invitationToken": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"invitationTokenSentAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"minLength": 6,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"resetPasswordToken": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"resetPasswordTokenSentAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"roleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"default": "active",
|
|
||||||
"enum": [
|
|
||||||
"active",
|
|
||||||
"invited",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"trialExpiryDate": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"fullName",
|
|
||||||
"email",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -88,13 +88,15 @@ class Flow extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
static async populateStatusProperty(flows) {
|
static async afterFind(args) {
|
||||||
const referenceFlow = flows[0];
|
const { result } = args;
|
||||||
|
|
||||||
|
const referenceFlow = result[0];
|
||||||
|
|
||||||
if (referenceFlow) {
|
if (referenceFlow) {
|
||||||
const shouldBePaused = await referenceFlow.isPaused();
|
const shouldBePaused = await referenceFlow.isPaused();
|
||||||
|
|
||||||
for (const flow of flows) {
|
for (const flow of result) {
|
||||||
if (!flow.active) {
|
if (!flow.active) {
|
||||||
flow.status = 'draft';
|
flow.status = 'draft';
|
||||||
} else if (flow.active && shouldBePaused) {
|
} else if (flow.active && shouldBePaused) {
|
||||||
@@ -106,10 +108,6 @@ class Flow extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async afterFind(args) {
|
|
||||||
await this.populateStatusProperty(args.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
async lastInternalId() {
|
async lastInternalId() {
|
||||||
const lastExecution = await this.$relatedQuery('lastExecution');
|
const lastExecution = await this.$relatedQuery('lastExecution');
|
||||||
|
|
||||||
@@ -125,14 +123,13 @@ class Flow extends Base {
|
|||||||
return lastExecutions.map((execution) => execution.internalId);
|
return lastExecutions.map((execution) => execution.internalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get IncompleteStepsError() {
|
get IncompleteStepsError() {
|
||||||
return new ValidationError({
|
return new ValidationError({
|
||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message:
|
message: 'All steps should be completed before updating flow status!'
|
||||||
'All steps should be completed before updating flow status!',
|
}
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'incompleteStepsError',
|
type: 'incompleteStepsError',
|
||||||
@@ -151,48 +148,36 @@ class Flow extends Base {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
position: 2,
|
position: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.$query().withGraphFetched('steps');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStepById(stepId) {
|
async createActionStep(previousStepId) {
|
||||||
return await this.$relatedQuery('steps').findById(stepId).throwIfNotFound();
|
const previousStep = await this.$relatedQuery('steps')
|
||||||
}
|
.findById(previousStepId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
async insertActionStepAtPosition(position) {
|
const createdStep = await this.$relatedQuery('steps').insertAndFetch({
|
||||||
return await this.$relatedQuery('steps').insertAndFetch({
|
|
||||||
type: 'action',
|
type: 'action',
|
||||||
position,
|
position: previousStep.position + 1,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async getStepsAfterPosition(position) {
|
const nextSteps = await this.$relatedQuery('steps')
|
||||||
return await this.$relatedQuery('steps').where('position', '>', position);
|
.where('position', '>=', createdStep.position)
|
||||||
}
|
.whereNot('id', createdStep.id);
|
||||||
|
|
||||||
async updateStepPositionsFrom(startPosition, steps) {
|
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
|
||||||
const stepPositionUpdates = steps.map(async (step, index) => {
|
return await nextStep.$query().patchAndFetch({
|
||||||
return await step.$query().patch({
|
position: createdStep.position + index + 1,
|
||||||
position: startPosition + index,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(stepPositionUpdates);
|
await Promise.all(nextStepQueries);
|
||||||
}
|
|
||||||
|
|
||||||
async createStepAfter(previousStepId) {
|
|
||||||
const previousStep = await this.getStepById(previousStepId);
|
|
||||||
|
|
||||||
const nextSteps = await this.getStepsAfterPosition(previousStep.position);
|
|
||||||
|
|
||||||
const createdStep = await this.insertActionStepAtPosition(
|
|
||||||
previousStep.position + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateStepPositionsFrom(createdStep.position + 1, nextSteps);
|
|
||||||
|
|
||||||
return createdStep;
|
return createdStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async unregisterWebhook() {
|
async delete() {
|
||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
const trigger = await triggerStep?.getTriggerCommand();
|
const trigger = await triggerStep?.getTriggerCommand();
|
||||||
|
|
||||||
@@ -213,33 +198,15 @@ class Flow extends Base {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async deleteExecutionSteps() {
|
|
||||||
const executionIds = (
|
const executionIds = (
|
||||||
await this.$relatedQuery('executions').select('executions.id')
|
await this.$relatedQuery('executions').select('executions.id')
|
||||||
).map((execution) => execution.id);
|
).map((execution) => execution.id);
|
||||||
|
|
||||||
return await ExecutionStep.query()
|
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
||||||
.delete()
|
|
||||||
.whereIn('execution_id', executionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteExecutions() {
|
|
||||||
return await this.$relatedQuery('executions').delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSteps() {
|
|
||||||
return await this.$relatedQuery('steps').delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete() {
|
|
||||||
await this.unregisterWebhook();
|
|
||||||
|
|
||||||
await this.deleteExecutionSteps();
|
|
||||||
await this.deleteExecutions();
|
|
||||||
await this.deleteSteps();
|
|
||||||
|
|
||||||
|
await this.$relatedQuery('executions').delete();
|
||||||
|
await this.$relatedQuery('steps').delete();
|
||||||
await this.$query().delete();
|
await this.$query().delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,18 +291,6 @@ class Flow extends Base {
|
|||||||
return duplicatedFlowWithSteps;
|
return duplicatedFlowWithSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTriggerStep() {
|
|
||||||
return await this.$relatedQuery('steps').findOne({
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async isPaused() {
|
|
||||||
const user = await this.$relatedQuery('user').withSoftDeleted();
|
|
||||||
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
|
||||||
return allowedToRunFlows ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStatus(newActiveValue) {
|
async updateStatus(newActiveValue) {
|
||||||
if (this.active === newActiveValue) {
|
if (this.active === newActiveValue) {
|
||||||
return this;
|
return this;
|
||||||
@@ -344,7 +299,7 @@ class Flow extends Base {
|
|||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
|
|
||||||
if (triggerStep.status === 'incomplete') {
|
if (triggerStep.status === 'incomplete') {
|
||||||
throw Flow.IncompleteStepsError;
|
throw this.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trigger = await triggerStep.getTriggerCommand();
|
const trigger = await triggerStep.getTriggerCommand();
|
||||||
@@ -398,55 +353,60 @@ class Flow extends Base {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async throwIfHavingIncompleteSteps() {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
const incompleteStep = await this.$relatedQuery('steps').findOne({
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const oldFlow = opt.old;
|
||||||
|
|
||||||
|
const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({
|
||||||
status: 'incomplete',
|
status: 'incomplete',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (incompleteStep) {
|
if (incompleteStep) {
|
||||||
throw Flow.IncompleteStepsError;
|
throw this.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async throwIfHavingLessThanTwoSteps() {
|
const allSteps = await oldFlow.$relatedQuery('steps');
|
||||||
const allSteps = await this.$relatedQuery('steps');
|
|
||||||
|
|
||||||
if (allSteps.length < 2) {
|
if (allSteps.length < 2) {
|
||||||
throw new ValidationError({
|
throw new ValidationError({
|
||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message:
|
message: 'There should be at least one trigger and one action steps in the flow!'
|
||||||
'There should be at least one trigger and one action steps in the flow!',
|
}
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'insufficientStepsError',
|
type: 'insufficientStepsError',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
return;
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
if (this.active) {
|
|
||||||
await opt.old.throwIfHavingIncompleteSteps();
|
|
||||||
|
|
||||||
await opt.old.throwIfHavingLessThanTwoSteps();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
async $afterInsert(queryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
Telemetry.flowCreated(this);
|
Telemetry.flowCreated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
async $afterUpdate(opt, queryContext) {
|
||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
|
||||||
Telemetry.flowUpdated(this);
|
Telemetry.flowUpdated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTriggerStep() {
|
||||||
|
return await this.$relatedQuery('steps').findOne({
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isPaused() {
|
||||||
|
const user = await this.$relatedQuery('user').withSoftDeleted();
|
||||||
|
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
||||||
|
return allowedToRunFlows ? false : true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Flow;
|
export default Flow;
|
||||||
|
@@ -1,616 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import Flow from './flow.js';
|
|
||||||
import User from './user.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import Step from './step.js';
|
|
||||||
import Execution from './execution.js';
|
|
||||||
import Telemetry from '../helpers/telemetry/index.js';
|
|
||||||
import * as globalVariableModule from '../helpers/global-variable.js';
|
|
||||||
import { createFlow } from '../../test/factories/flow.js';
|
|
||||||
import { createStep } from '../../test/factories/step.js';
|
|
||||||
import { createExecution } from '../../test/factories/execution.js';
|
|
||||||
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
|
||||||
|
|
||||||
describe('Flow model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(Flow.tableName).toBe('flows');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
expect(Flow.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
|
||||||
it('should return correct associations', () => {
|
|
||||||
const relationMappings = Flow.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
steps: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Step,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'steps.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
triggerStep: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Step,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'steps.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
executions: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Execution,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'executions.flow_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastExecution: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Execution,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'executions.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: User,
|
|
||||||
join: {
|
|
||||||
from: 'flows.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('steps should return the steps', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
const orderBySpy = vi.fn();
|
|
||||||
|
|
||||||
relations.steps.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('position', 'asc');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('triggerStep should return the trigger step', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const whereSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.triggerStep.filter({ where: whereSpy });
|
|
||||||
|
|
||||||
expect(whereSpy).toHaveBeenCalledWith('type', 'trigger');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('lastExecution should return the last execution', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.lastExecution.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('populateStatusProperty', () => {
|
|
||||||
it('should assign "draft" to status property when a flow is not active', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: false });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue();
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('draft');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign "paused" to status property when a flow is active, but should be paused', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(true);
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('paused');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign "published" to status property when a flow is active', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(false);
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('published');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('afterFind should call Flow.populateStatusProperty', async () => {
|
|
||||||
const populateStatusPropertySpy = vi
|
|
||||||
.spyOn(Flow, 'populateStatusProperty')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await createFlow();
|
|
||||||
|
|
||||||
expect(populateStatusPropertySpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lastInternalId', () => {
|
|
||||||
it('should return internal ID of last execution when exists', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createExecution({ flowId: flow.id });
|
|
||||||
await createExecution({ flowId: flow.id });
|
|
||||||
const lastExecution = await createExecution({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.lastInternalId()).toBe(lastExecution.internalId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when no flow execution exists', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
expect(await flow.lastInternalId()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lastInternalIds', () => {
|
|
||||||
it('should return last internal IDs', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const internalIds = [
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
].map((execution) => execution.internalId);
|
|
||||||
|
|
||||||
expect(await flow.lastInternalIds()).toStrictEqual(internalIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return last 50 internal IDs by default', async () => {
|
|
||||||
const flow = new Flow();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockResolvedValue([]);
|
|
||||||
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
select: vi.fn().mockReturnThis(),
|
|
||||||
orderBy: vi.fn().mockReturnThis(),
|
|
||||||
limit: limitSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
await flow.lastInternalIds();
|
|
||||||
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('IncompleteStepsError should return validation error for incomplete steps', () => {
|
|
||||||
expect(() => {
|
|
||||||
throw Flow.IncompleteStepsError;
|
|
||||||
}).toThrowError(
|
|
||||||
'flow: All steps should be completed before updating flow status!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createInitialSteps should create one trigger and one action step', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
|
|
||||||
expect(steps.length).toBe(2);
|
|
||||||
|
|
||||||
expect(steps[0]).toMatchObject({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
position: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(steps[1]).toMatchObject({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'action',
|
|
||||||
position: 2,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getStepById should return the step with the given ID from the flow', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const step = await createStep({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.getStepById(step.id)).toStrictEqual(step);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('insertActionStepAtPosition should insert action step at given position', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
const createdStep = await flow.insertActionStepAtPosition(2);
|
|
||||||
|
|
||||||
expect(createdStep).toMatchObject({
|
|
||||||
type: 'action',
|
|
||||||
position: 2,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getStepsAfterPosition should return steps after the given position', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
await createStep({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.getStepsAfterPosition(1)).toMatchObject([
|
|
||||||
{ position: 2 },
|
|
||||||
{ position: 3 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updateStepPositionsFrom', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({ type: 'trigger', flowId: flow.id, position: 6 });
|
|
||||||
await createStep({ type: 'action', flowId: flow.id, position: 8 });
|
|
||||||
await createStep({ type: 'action', flowId: flow.id, position: 10 });
|
|
||||||
|
|
||||||
await flow.updateStepPositionsFrom(2, await flow.$relatedQuery('steps'));
|
|
||||||
|
|
||||||
expect(await flow.$relatedQuery('steps')).toMatchObject([
|
|
||||||
{ position: 2, type: 'trigger' },
|
|
||||||
{ position: 3, type: 'action' },
|
|
||||||
{ position: 4, type: 'action' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createStepAfter should create an action step after given step ID', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const triggerStep = await createStep({ type: 'trigger', flowId: flow.id });
|
|
||||||
const actionStep = await createStep({ type: 'action', flowId: flow.id });
|
|
||||||
|
|
||||||
const createdStep = await flow.createStepAfter(triggerStep.id);
|
|
||||||
|
|
||||||
const refetchedActionStep = await actionStep.$query();
|
|
||||||
|
|
||||||
expect(createdStep).toMatchObject({ type: 'action', position: 2 });
|
|
||||||
expect(refetchedActionStep.position).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unregisterWebhook', () => {
|
|
||||||
it('should unregister webhook on remote when supported', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const triggerStep = await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
appKey: 'typeform',
|
|
||||||
key: 'new-entry',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockResolvedValue();
|
|
||||||
|
|
||||||
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
|
|
||||||
type: 'webhook',
|
|
||||||
unregisterHook: unregisterHookSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
const globalVariableSpy = vi
|
|
||||||
.spyOn(globalVariableModule, 'default')
|
|
||||||
.mockResolvedValue('global-variable');
|
|
||||||
|
|
||||||
await flow.unregisterWebhook();
|
|
||||||
|
|
||||||
expect(unregisterHookSpy).toHaveBeenCalledWith('global-variable');
|
|
||||||
expect(globalVariableSpy).toHaveBeenCalledWith({
|
|
||||||
flow,
|
|
||||||
step: triggerStep,
|
|
||||||
connection: undefined,
|
|
||||||
app: await triggerStep.getApp(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should silently fail when unregistration fails', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
appKey: 'typeform',
|
|
||||||
key: 'new-entry',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
|
|
||||||
type: 'webhook',
|
|
||||||
unregisterHook: unregisterHookSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.unregisterWebhook()).toBe(undefined);
|
|
||||||
expect(unregisterHookSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing when trigger step is not webhook', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
expect(await flow.unregisterWebhook()).toBe(undefined);
|
|
||||||
expect(unregisterHookSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteExecutionSteps should delete related execution steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const execution = await createExecution({ flowId: flow.id });
|
|
||||||
const firstExecutionStep = await createExecutionStep({
|
|
||||||
executionId: execution.id,
|
|
||||||
});
|
|
||||||
const secondExecutionStep = await createExecutionStep({
|
|
||||||
executionId: execution.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await flow.deleteExecutionSteps();
|
|
||||||
|
|
||||||
expect(await firstExecutionStep.$query()).toBe(undefined);
|
|
||||||
expect(await secondExecutionStep.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteExecutions should delete related executions', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const firstExecution = await createExecution({ flowId: flow.id });
|
|
||||||
const secondExecution = await createExecution({ flowId: flow.id });
|
|
||||||
|
|
||||||
await flow.deleteExecutions();
|
|
||||||
|
|
||||||
expect(await firstExecution.$query()).toBe(undefined);
|
|
||||||
expect(await secondExecution.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteSteps should delete related steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
await flow.deleteSteps();
|
|
||||||
|
|
||||||
expect(await flow.$relatedQuery('steps')).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delete should delete the flow with its relations', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const unregisterWebhookSpy = vi
|
|
||||||
.spyOn(flow, 'unregisterWebhook')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteExecutionStepsSpy = vi
|
|
||||||
.spyOn(flow, 'deleteExecutionSteps')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteExecutionsSpy = vi
|
|
||||||
.spyOn(flow, 'deleteExecutions')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteStepsSpy = vi.spyOn(flow, 'deleteSteps').mockResolvedValue();
|
|
||||||
|
|
||||||
await flow.delete();
|
|
||||||
|
|
||||||
expect(unregisterWebhookSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteExecutionStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteExecutionsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(await flow.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.todo('duplicateFor');
|
|
||||||
|
|
||||||
it('getTriggerStep', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const triggerStep = await createStep({ flowId: flow.id, type: 'trigger' });
|
|
||||||
|
|
||||||
await createStep({ flowId: flow.id, type: 'action' });
|
|
||||||
|
|
||||||
expect(await flow.getTriggerStep()).toStrictEqual(triggerStep);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isPaused', () => {
|
|
||||||
it('should return true when user.isAllowedToRunFlows returns false', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(false);
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
withSoftDeleted: vi.fn().mockReturnThis(),
|
|
||||||
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.isPaused()).toBe(true);
|
|
||||||
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when user.isAllowedToRunFlows returns true', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(true);
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
withSoftDeleted: vi.fn().mockReturnThis(),
|
|
||||||
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.isPaused()).toBe(false);
|
|
||||||
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('throwIfHavingIncompleteSteps', () => {
|
|
||||||
it('should throw validation error with incomplete steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
flow.throwIfHavingIncompleteSteps()
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'flow: All steps should be completed before updating flow status!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when all steps are completed', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
status: 'completed',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
status: 'completed',
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.throwIfHavingIncompleteSteps()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('throwIfHavingLessThanTwoSteps', () => {
|
|
||||||
it('should throw validation error with less than two steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
flow.throwIfHavingLessThanTwoSteps()
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'flow: There should be at least one trigger and one action steps in the flow!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when there are at least two steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.throwIfHavingLessThanTwoSteps()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$beforeUpdate', () => {
|
|
||||||
it('should invoke throwIfHavingIncompleteSteps when flow is becoming active', async () => {
|
|
||||||
const flow = await createFlow({ active: false });
|
|
||||||
|
|
||||||
const throwIfHavingIncompleteStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const throwIfHavingLessThanTwoStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: true });
|
|
||||||
|
|
||||||
expect(throwIfHavingIncompleteStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(throwIfHavingLessThanTwoStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invoke throwIfHavingIncompleteSteps when flow is not becoming active', async () => {
|
|
||||||
const flow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const throwIfHavingIncompleteStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const throwIfHavingLessThanTwoStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await flow.$query().patch({});
|
|
||||||
|
|
||||||
expect(throwIfHavingIncompleteStepsSpy).not.toHaveBeenCalledOnce();
|
|
||||||
expect(throwIfHavingLessThanTwoStepsSpy).not.toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterInsert', () => {
|
|
||||||
it('should call super.$afterInsert', async () => {
|
|
||||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
|
|
||||||
|
|
||||||
await createFlow();
|
|
||||||
|
|
||||||
expect(superAfterInsertSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call Telemetry.flowCreated', async () => {
|
|
||||||
const telemetryFlowCreatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'flowCreated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
expect(telemetryFlowCreatedSpy).toHaveBeenCalledWith(flow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterUpdate', () => {
|
|
||||||
it('should call super.$afterUpdate', async () => {
|
|
||||||
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: false });
|
|
||||||
|
|
||||||
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('$afterUpdate should call Telemetry.flowUpdated', async () => {
|
|
||||||
const telemetryFlowUpdatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'flowUpdated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: false });
|
|
||||||
|
|
||||||
expect(telemetryFlowUpdatedSpy).toHaveBeenCalled({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,48 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
|
||||||
import Identity from './identity.ee';
|
|
||||||
import Base from './base';
|
|
||||||
|
|
||||||
describe('SamlAuthProvider model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(SamlAuthProvider.tableName).toBe('saml_auth_providers');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
|
||||||
expect(SamlAuthProvider.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
|
||||||
const relationMappings = SamlAuthProvider.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
identities: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Identity,
|
|
||||||
join: {
|
|
||||||
from: 'identities.provider_id',
|
|
||||||
to: 'saml_auth_providers.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
samlAuthProvidersRoleMappings: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
|
||||||
join: {
|
|
||||||
from: 'saml_auth_providers.id',
|
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = SamlAuthProvider.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['loginUrl', 'remoteLogoutUrl'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -93,14 +93,6 @@ class Step extends Base {
|
|||||||
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isTrigger() {
|
|
||||||
return this.type === 'trigger';
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAction() {
|
|
||||||
return this.type === 'action';
|
|
||||||
}
|
|
||||||
|
|
||||||
async computeWebhookPath() {
|
async computeWebhookPath() {
|
||||||
if (this.type === 'action') return null;
|
if (this.type === 'action') return null;
|
||||||
|
|
||||||
@@ -143,6 +135,24 @@ class Step extends Base {
|
|||||||
return webhookUrl;
|
return webhookUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
Telemetry.stepCreated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterUpdate(opt, queryContext) {
|
||||||
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
Telemetry.stepUpdated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTrigger() {
|
||||||
|
return this.type === 'trigger';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAction() {
|
||||||
|
return this.type === 'action';
|
||||||
|
}
|
||||||
|
|
||||||
async getApp() {
|
async getApp() {
|
||||||
if (!this.appKey) return null;
|
if (!this.appKey) return null;
|
||||||
|
|
||||||
@@ -160,7 +170,12 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getLastExecutionStep() {
|
async getLastExecutionStep() {
|
||||||
return await this.$relatedQuery('lastExecutionStep');
|
const lastExecutionStep = await this.$relatedQuery('executionSteps')
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.limit(1)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return lastExecutionStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextStep() {
|
async getNextStep() {
|
||||||
@@ -192,18 +207,19 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSetupFields() {
|
async getSetupFields() {
|
||||||
let substeps;
|
let setupSupsteps;
|
||||||
|
|
||||||
if (this.isTrigger) {
|
if (this.isTrigger) {
|
||||||
substeps = (await this.getTriggerCommand()).substeps;
|
setupSupsteps = (await this.getTriggerCommand()).substeps;
|
||||||
} else {
|
} else {
|
||||||
substeps = (await this.getActionCommand()).substeps;
|
setupSupsteps = (await this.getActionCommand()).substeps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupSubstep = substeps.find(
|
const existingArguments = setupSupsteps.find(
|
||||||
(substep) => substep.key === 'chooseTrigger'
|
(substep) => substep.key === 'chooseTrigger'
|
||||||
);
|
).arguments;
|
||||||
return setupSubstep.arguments;
|
|
||||||
|
return existingArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetupAndDynamicFields() {
|
async getSetupAndDynamicFields() {
|
||||||
@@ -310,17 +326,23 @@ class Step extends Base {
|
|||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.where('position', '>', this.position);
|
.where('position', '>', this.position);
|
||||||
|
|
||||||
await flow.updateStepPositionsFrom(this.position, nextSteps);
|
const nextStepQueries = nextSteps.map(async (nextStep) => {
|
||||||
|
await nextStep.$query().patch({
|
||||||
|
position: nextStep.position - 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(nextStepQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFor(user, newStepData) {
|
async updateFor(user, newStepData) {
|
||||||
const { appKey = this.appKey, connectionId, key, parameters } = newStepData;
|
const { connectionId, appKey, key, parameters } = newStepData;
|
||||||
|
|
||||||
if (connectionId && appKey) {
|
if (connectionId && (appKey || this.appKey)) {
|
||||||
await user.authorizedConnections
|
await user.authorizedConnections
|
||||||
.findOne({
|
.findOne({
|
||||||
id: connectionId,
|
id: connectionId,
|
||||||
key: appKey,
|
key: appKey || this.appKey,
|
||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
}
|
}
|
||||||
@@ -334,8 +356,8 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedStep = await this.$query().patchAndFetch({
|
const updatedStep = await this.$query().patchAndFetch({
|
||||||
key,
|
key: key,
|
||||||
appKey,
|
appKey: appKey,
|
||||||
connectionId: connectionId,
|
connectionId: connectionId,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
status: 'incomplete',
|
status: 'incomplete',
|
||||||
@@ -345,16 +367,6 @@ class Step extends Base {
|
|||||||
|
|
||||||
return updatedStep;
|
return updatedStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
Telemetry.stepCreated(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
|
||||||
await super.$afterUpdate(opt, queryContext);
|
|
||||||
Telemetry.stepUpdated(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Step;
|
export default Step;
|
||||||
|
@@ -1,504 +0,0 @@
|
|||||||
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
||||||
import appConfig from '../config/app.js';
|
|
||||||
import App from './app.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import Step from './step.js';
|
|
||||||
import Flow from './flow.js';
|
|
||||||
import Connection from './connection.js';
|
|
||||||
import ExecutionStep from './execution-step.js';
|
|
||||||
import Telemetry from '../helpers/telemetry/index.js';
|
|
||||||
import * as testRunModule from '../services/test-run.js';
|
|
||||||
import { createFlow } from '../../test/factories/flow.js';
|
|
||||||
import { createUser } from '../../test/factories/user.js';
|
|
||||||
import { createRole } from '../../test/factories/role.js';
|
|
||||||
import { createPermission } from '../../test/factories/permission.js';
|
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
|
||||||
import { createStep } from '../../test/factories/step.js';
|
|
||||||
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
|
||||||
|
|
||||||
describe('Step model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(Step.tableName).toBe('steps');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
expect(Step.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = Step.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['iconUrl', 'webhookUrl'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
|
||||||
it('should return correct associations', () => {
|
|
||||||
const relationMappings = Step.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
flow: {
|
|
||||||
relation: Base.BelongsToOneRelation,
|
|
||||||
modelClass: Flow,
|
|
||||||
join: {
|
|
||||||
from: 'steps.flow_id',
|
|
||||||
to: 'flows.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connection: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Connection,
|
|
||||||
join: {
|
|
||||||
from: 'steps.connection_id',
|
|
||||||
to: 'connections.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastExecutionStep: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: ExecutionStep,
|
|
||||||
join: {
|
|
||||||
from: 'steps.id',
|
|
||||||
to: 'execution_steps.step_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
executionSteps: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: ExecutionStep,
|
|
||||||
join: {
|
|
||||||
from: 'steps.id',
|
|
||||||
to: 'execution_steps.step_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('lastExecutionStep should return the trigger step', () => {
|
|
||||||
const relations = Step.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.lastExecutionStep.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('webhookUrl', () => {
|
|
||||||
it('should return it along with appConfig.webhookUrl when exists', () => {
|
|
||||||
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const step = new Step();
|
|
||||||
step.webhookPath = '/webhook-path';
|
|
||||||
|
|
||||||
expect(step.webhookUrl).toBe('https://automatisch.io/webhook-path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when webhookUrl does not exist', () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
expect(step.webhookUrl).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('iconUrl', () => {
|
|
||||||
it('should return step app icon absolute URL when app is set', () => {
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'gitlab';
|
|
||||||
|
|
||||||
expect(step.iconUrl).toBe(
|
|
||||||
'https://automatisch.io/apps/gitlab/assets/favicon.svg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when appKey is not set', () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
expect(step.iconUrl).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTrigger should return true when step type is trigger', () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
expect(step.isTrigger).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAction should return true when step type is action', () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(step.isAction).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.todo('computeWebhookPath');
|
|
||||||
|
|
||||||
describe('getWebhookUrl', () => {
|
|
||||||
it('should return absolute webhook URL when step type is trigger', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
vi.spyOn(step, 'computeWebhookPath').mockResolvedValue('/webhook-path');
|
|
||||||
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await step.getWebhookUrl()).toBe(
|
|
||||||
'https://automatisch.io/webhook-path'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when step type is action', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(await step.getWebhookUrl()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('getApp', () => {
|
|
||||||
it('should return app with the given appKey', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'gitlab';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
|
|
||||||
|
|
||||||
await step.getApp();
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith('gitlab');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null with no appKey', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
|
|
||||||
|
|
||||||
expect(await step.getApp()).toBe(null);
|
|
||||||
expect(findOneByKeySpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test should execute the flow and mark the step as completed', async () => {
|
|
||||||
const step = await createStep({ status: 'incomplete' });
|
|
||||||
|
|
||||||
const testRunSpy = vi.spyOn(testRunModule, 'default').mockResolvedValue();
|
|
||||||
|
|
||||||
const updatedStep = await step.test();
|
|
||||||
|
|
||||||
expect(testRunSpy).toHaveBeenCalledWith({ stepId: step.id });
|
|
||||||
expect(updatedStep.status).toBe('completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getLastExecutionStep should return last execution step', async () => {
|
|
||||||
const step = await createStep();
|
|
||||||
await createExecutionStep({ stepId: step.id });
|
|
||||||
const secondExecutionStep = await createExecutionStep({ stepId: step.id });
|
|
||||||
|
|
||||||
expect(await step.getLastExecutionStep()).toStrictEqual(
|
|
||||||
secondExecutionStep
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getNextStep should return the next step', async () => {
|
|
||||||
const firstStep = await createStep();
|
|
||||||
const secondStep = await createStep({ flowId: firstStep.flowId });
|
|
||||||
const thirdStep = await createStep({ flowId: firstStep.flowId });
|
|
||||||
|
|
||||||
expect(await secondStep.getNextStep()).toStrictEqual(thirdStep);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTriggerCommand', () => {
|
|
||||||
it('should return trigger command when app key and key are defined in trigger step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
step.key = 'catchRawWebhook';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
|
|
||||||
const triggerCommand = await step.getTriggerCommand();
|
|
||||||
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
|
|
||||||
expect(triggerCommand.key).toBe(step.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when key is not defined', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
|
|
||||||
expect(await step.getTriggerCommand()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getActionCommand', () => {
|
|
||||||
it('should return action comamand when app key and key are defined in action step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
step.appKey = 'ntfy';
|
|
||||||
step.key = 'sendMessage';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
|
|
||||||
const actionCommand = await step.getActionCommand();
|
|
||||||
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
|
|
||||||
expect(actionCommand.key).toBe(step.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when key is not defined', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
step.appKey = 'ntfy';
|
|
||||||
|
|
||||||
expect(await step.getActionCommand()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getSetupFields', () => {
|
|
||||||
it('should return trigger setup substep fields in trigger step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
step.key = 'catchRawWebhook';
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
expect(await step.getSetupFields()).toStrictEqual([
|
|
||||||
{
|
|
||||||
label: 'Wait until flow is done',
|
|
||||||
key: 'workSynchronously',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ label: 'Yes', value: true },
|
|
||||||
{ label: 'No', value: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return action setup substep fields in action step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'datastore';
|
|
||||||
step.key = 'getValue';
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(await step.getSetupFields()).toStrictEqual([
|
|
||||||
{
|
|
||||||
label: 'Key',
|
|
||||||
key: 'key',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
description: 'The key of your value to get.',
|
|
||||||
variables: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.todo('getSetupAndDynamicFields');
|
|
||||||
it.todo('createDynamicFields');
|
|
||||||
it.todo('createDynamicData');
|
|
||||||
it.todo('updateWebhookUrl');
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete the step and align the positions', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
|
|
||||||
await createStep({ flowId: flow.id, position: 2 });
|
|
||||||
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
|
|
||||||
await createStep({ flowId: flow.id, position: 4 });
|
|
||||||
|
|
||||||
await stepToDelete.delete();
|
|
||||||
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
const stepIds = steps.map((step) => step.id);
|
|
||||||
|
|
||||||
expect(stepIds).not.toContain(stepToDelete.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should align the positions of remaining steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
|
|
||||||
await createStep({ flowId: flow.id, position: 2 });
|
|
||||||
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
|
|
||||||
await createStep({ flowId: flow.id, position: 4 });
|
|
||||||
|
|
||||||
await stepToDelete.delete();
|
|
||||||
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
const stepPositions = steps.map((step) => step.position);
|
|
||||||
|
|
||||||
expect(stepPositions).toMatchObject([1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete related execution steps', async () => {
|
|
||||||
const step = await createStep();
|
|
||||||
const executionStep = await createExecutionStep({ stepId: step.id });
|
|
||||||
|
|
||||||
await step.delete();
|
|
||||||
|
|
||||||
expect(await executionStep.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateFor', async () => {
|
|
||||||
let step,
|
|
||||||
userRole,
|
|
||||||
user,
|
|
||||||
userConnection,
|
|
||||||
anotherUser,
|
|
||||||
anotherUserConnection;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
userRole = await createRole({ name: 'User' });
|
|
||||||
anotherUser = await createUser({ roleId: userRole.id });
|
|
||||||
user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
userConnection = await createConnection({
|
|
||||||
key: 'deepl',
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
anotherUserConnection = await createConnection({
|
|
||||||
key: 'deepl',
|
|
||||||
userId: anotherUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
action: 'read',
|
|
||||||
subject: 'Connection',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
step = await createStep();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update step with the given payload and mark it as incomplete', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
connectionId: anotherUserConnection.id,
|
|
||||||
parameters: {
|
|
||||||
key: 'value',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const anotherUserWithRoleAndPermissions = await anotherUser
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ permissions: true, role: true });
|
|
||||||
|
|
||||||
const updatedStep = await step.updateFor(
|
|
||||||
anotherUserWithRoleAndPermissions,
|
|
||||||
stepData
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedStep).toMatchObject({
|
|
||||||
...stepData,
|
|
||||||
status: 'incomplete',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invoke updateWebhookUrl', async () => {
|
|
||||||
const updateWebhookUrlSpy = vi
|
|
||||||
.spyOn(Step.prototype, 'updateWebhookUrl')
|
|
||||||
.mockResolvedValue();
|
|
||||||
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
};
|
|
||||||
|
|
||||||
await step.updateFor(user, stepData);
|
|
||||||
|
|
||||||
expect(updateWebhookUrlSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update step when inaccessible connection is given', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
connectionId: userConnection.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const anotherUserWithRoleAndPermissions = await anotherUser
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ permissions: true, role: true });
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
step.updateFor(anotherUserWithRoleAndPermissions, stepData)
|
|
||||||
).rejects.toThrowError('NotFoundError');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update step when given app key and key do not exist', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'not-existing-key',
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(() => step.updateFor(user, stepData)).rejects.toThrowError(
|
|
||||||
'DeepL does not have an action with the "not-existing-key" key!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterInsert', () => {
|
|
||||||
it('should call super.$afterInsert', async () => {
|
|
||||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
|
|
||||||
|
|
||||||
await createStep();
|
|
||||||
|
|
||||||
expect(superAfterInsertSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call Telemetry.stepCreated', async () => {
|
|
||||||
const telemetryStepCreatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'stepCreated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
expect(telemetryStepCreatedSpy).toHaveBeenCalledWith(step);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterUpdate', () => {
|
|
||||||
it('should call super.$afterUpdate', async () => {
|
|
||||||
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
await step.$query().patch({ position: 2 });
|
|
||||||
|
|
||||||
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('$afterUpdate should call Telemetry.stepUpdated', async () => {
|
|
||||||
const telemetryStepUpdatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'stepUpdated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
await step.$query().patch({ position: 2 });
|
|
||||||
|
|
||||||
expect(telemetryStepUpdatedSpy).toHaveBeenCalled({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -223,8 +223,8 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(password) {
|
login(password) {
|
||||||
return await bcrypt.compare(password, this.password);
|
return bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateResetPasswordToken() {
|
async generateResetPasswordToken() {
|
||||||
@@ -642,7 +642,7 @@ class User extends Base {
|
|||||||
can(action, subject) {
|
can(action, subject) {
|
||||||
const can = this.ability.can(action, subject);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
if (!can) throw new NotAuthorizedError();
|
||||||
|
|
||||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||||
|
|
||||||
|
@@ -1,583 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import appConfig from '../config/app.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import AccessToken from './access-token.js';
|
|
||||||
import Connection from './connection.js';
|
|
||||||
import Execution from './execution.js';
|
|
||||||
import Flow from './flow.js';
|
|
||||||
import Identity from './identity.ee.js';
|
|
||||||
import Permission from './permission.js';
|
|
||||||
import Role from './role.js';
|
|
||||||
import Step from './step.js';
|
|
||||||
import Subscription from './subscription.ee.js';
|
|
||||||
import UsageData from './usage-data.ee.js';
|
|
||||||
import User from './user.js';
|
|
||||||
import { createUser } from '../../test/factories/user.js';
|
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
|
||||||
import { createRole } from '../../test/factories/role.js';
|
|
||||||
import { createPermission } from '../../test/factories/permission.js';
|
|
||||||
import { createFlow } from '../../test/factories/flow.js';
|
|
||||||
import { createStep } from '../../test/factories/step.js';
|
|
||||||
import { createExecution } from '../../test/factories/execution.js';
|
|
||||||
|
|
||||||
describe('User model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(User.tableName).toBe('users');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
expect(User.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
|
||||||
it('should return correct associations', () => {
|
|
||||||
const relationMappings = User.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
accessTokens: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: AccessToken,
|
|
||||||
join: {
|
|
||||||
from: 'users.id',
|
|
||||||
to: 'access_tokens.user_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connections: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Connection,
|
|
||||||
join: {
|
|
||||||
from: 'users.id',
|
|
||||||
to: 'connections.user_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
flows: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Flow,
|
|
||||||
join: {
|
|
||||||
from: 'users.id',
|
|
||||||
to: 'flows.user_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
steps: {
|
|
||||||
relation: Base.ManyToManyRelation,
|
|
||||||
modelClass: Step,
|
|
||||||
join: {
|
|
||||||
from: 'users.id',
|
|
||||||
through: {
|
|
||||||
from: 'flows.user_id',
|
|
||||||
to: 'flows.id',
|
|
||||||
},
|
|
||||||
to: 'steps.flow_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
executions: {
|
|
||||||
relation: Base.ManyToManyRelation,
|
|
||||||
modelClass: Execution,
|
|
||||||
join: {
|
|
||||||
from: 'users.id',
|
|
||||||
through: {
|
|
||||||
from: 'flows.user_id',
|
|
||||||
to: 'flows.id',
|
|
||||||
},
|
|
||||||
to: 'executions.flow_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
usageData: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: UsageData,
|
|
||||||
join: {
|
|
||||||
from: 'usage_data.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentUsageData: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: UsageData,
|
|
||||||
join: {
|
|
||||||
from: 'usage_data.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Subscription,
|
|
||||||
join: {
|
|
||||||
from: 'subscriptions.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentSubscription: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Subscription,
|
|
||||||
join: {
|
|
||||||
from: 'subscriptions.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Role,
|
|
||||||
join: {
|
|
||||||
from: 'roles.id',
|
|
||||||
to: 'users.role_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Permission,
|
|
||||||
join: {
|
|
||||||
from: 'users.role_id',
|
|
||||||
to: 'permissions.role_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
identities: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Identity,
|
|
||||||
join: {
|
|
||||||
from: 'identities.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('currentUsageData should return the current usage data', () => {
|
|
||||||
const relations = User.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.currentUsageData.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('currentSubscription should return the current subscription', () => {
|
|
||||||
const relations = User.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.currentSubscription.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = User.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['acceptInvitationUrl'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
|
||||||
const user = new User();
|
|
||||||
user.invitationToken = 'invitation-token';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(user.acceptInvitationUrl).toBe(
|
|
||||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authenticate', () => {
|
|
||||||
it('should create and return the token for correct email and password', async () => {
|
|
||||||
const user = await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate(
|
|
||||||
'test-user@automatisch.io',
|
|
||||||
'sample-password'
|
|
||||||
);
|
|
||||||
|
|
||||||
const persistedToken = await AccessToken.query().findOne({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(token).toBe(persistedToken.token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined for existing email and incorrect password', async () => {
|
|
||||||
await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate(
|
|
||||||
'test-user@automatisch.io',
|
|
||||||
'wrong-password'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(token).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined for non-existing email', async () => {
|
|
||||||
await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
|
||||||
|
|
||||||
expect(token).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizedFlows', () => {
|
|
||||||
it('should return user flows with isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
await createFlow();
|
|
||||||
|
|
||||||
expect(await userWithRoleAndPermissions.authorizedFlows).toStrictEqual([
|
|
||||||
userFlow,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all flows without isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
const anotherUserFlow = await createFlow();
|
|
||||||
|
|
||||||
expect(await userWithRoleAndPermissions.authorizedFlows).toStrictEqual([
|
|
||||||
userFlow,
|
|
||||||
anotherUserFlow,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an authorization error without Flow read permission', async () => {
|
|
||||||
const user = new User();
|
|
||||||
|
|
||||||
expect(() => user.authorizedFlows).toThrowError(
|
|
||||||
'The user is not authorized!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizedSteps', () => {
|
|
||||||
it('should return user steps with isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
const userFlowStep = await createStep({ flowId: userFlow.id });
|
|
||||||
const anotherUserFlow = await createFlow();
|
|
||||||
await createStep({ flowId: anotherUserFlow.id });
|
|
||||||
|
|
||||||
expect(await userWithRoleAndPermissions.authorizedSteps).toStrictEqual([
|
|
||||||
userFlowStep,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all steps without isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
const userFlowStep = await createStep({ flowId: userFlow.id });
|
|
||||||
const anotherUserFlow = await createFlow();
|
|
||||||
const anotherUserFlowStep = await createStep({
|
|
||||||
flowId: anotherUserFlow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await userWithRoleAndPermissions.authorizedSteps).toStrictEqual([
|
|
||||||
userFlowStep,
|
|
||||||
anotherUserFlowStep,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an authorization error without Flow read permission', async () => {
|
|
||||||
const user = new User();
|
|
||||||
|
|
||||||
expect(() => user.authorizedSteps).toThrowError(
|
|
||||||
'The user is not authorized!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizedConnections', () => {
|
|
||||||
it('should return user connections with isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Connection',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userConnection = await createConnection({ userId: user.id });
|
|
||||||
await createConnection();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await userWithRoleAndPermissions.authorizedConnections
|
|
||||||
).toStrictEqual([userConnection]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all connections without isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Connection',
|
|
||||||
action: 'read',
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userConnection = await createConnection({ userId: user.id });
|
|
||||||
const anotherUserConnection = await createConnection();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await userWithRoleAndPermissions.authorizedConnections
|
|
||||||
).toStrictEqual([userConnection, anotherUserConnection]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an authorization error without Connection read permission', async () => {
|
|
||||||
const user = new User();
|
|
||||||
|
|
||||||
expect(() => user.authorizedConnections).toThrowError(
|
|
||||||
'The user is not authorized!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizedExecutions', () => {
|
|
||||||
it('should return user executions with isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Execution',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
const userFlowExecution = await createExecution({ flowId: userFlow.id });
|
|
||||||
await createExecution();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await userWithRoleAndPermissions.authorizedExecutions
|
|
||||||
).toStrictEqual([userFlowExecution]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all executions without isCreator condition', async () => {
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
subject: 'Execution',
|
|
||||||
action: 'read',
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
const userWithRoleAndPermissions = await user
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ role: true, permissions: true });
|
|
||||||
|
|
||||||
const userFlow = await createFlow({ userId: user.id });
|
|
||||||
const userFlowExecution = await createExecution({ flowId: userFlow.id });
|
|
||||||
const anotherUserFlowExecution = await createExecution();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await userWithRoleAndPermissions.authorizedExecutions
|
|
||||||
).toStrictEqual([userFlowExecution, anotherUserFlowExecution]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an authorization error without Execution read permission', async () => {
|
|
||||||
const user = new User();
|
|
||||||
|
|
||||||
expect(() => user.authorizedExecutions).toThrowError(
|
|
||||||
'The user is not authorized!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('login', () => {
|
|
||||||
it('should return true when the given password matches with the user password', async () => {
|
|
||||||
const user = await createUser({ password: 'sample-password' });
|
|
||||||
|
|
||||||
expect(await user.login('sample-password')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when the given password does not match with the user password', async () => {
|
|
||||||
const user = await createUser({ password: 'sample-password' });
|
|
||||||
|
|
||||||
expect(await user.login('wrong-password')).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generateResetPasswordToken should persist a random reset password token with the current date', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
const date = new Date(2024, 10, 11, 15, 17, 0, 0);
|
|
||||||
vi.setSystemTime(date);
|
|
||||||
|
|
||||||
const user = await createUser({
|
|
||||||
resetPasswordToken: null,
|
|
||||||
resetPasswordTokenSentAt: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.generateResetPasswordToken();
|
|
||||||
|
|
||||||
const refetchedUser = await user.$query();
|
|
||||||
|
|
||||||
expect(refetchedUser.resetPasswordToken.length).toBe(128);
|
|
||||||
expect(refetchedUser.resetPasswordTokenSentAt).toStrictEqual(date);
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generateInvitationToken should persist a random invitation token with the current date', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
const date = new Date(2024, 10, 11, 15, 26, 0, 0);
|
|
||||||
vi.setSystemTime(date);
|
|
||||||
|
|
||||||
const user = await createUser({
|
|
||||||
invitationToken: null,
|
|
||||||
invitationTokenSentAt: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.generateInvitationToken();
|
|
||||||
|
|
||||||
const refetchedUser = await user.$query();
|
|
||||||
|
|
||||||
expect(refetchedUser.invitationToken.length).toBe(128);
|
|
||||||
expect(refetchedUser.invitationTokenSentAt).toStrictEqual(date);
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resetPassword should persist given password and remove reset password token', async () => {
|
|
||||||
const user = await createUser({
|
|
||||||
resetPasswordToken: 'reset-password-token',
|
|
||||||
resetPasswordTokenSentAt: '2024-11-11T12:26:00.000Z',
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.resetPassword('new-password');
|
|
||||||
|
|
||||||
const refetchedUser = await user.$query();
|
|
||||||
|
|
||||||
expect(refetchedUser.resetPasswordToken).toBe(null);
|
|
||||||
expect(refetchedUser.resetPasswordTokenSentAt).toBe(null);
|
|
||||||
expect(await refetchedUser.login('new-password')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('acceptInvitation should persist given password, set user active and remove invitation token', async () => {
|
|
||||||
const user = await createUser({
|
|
||||||
invitationToken: 'invitation-token',
|
|
||||||
invitationTokenSentAt: '2024-11-11T12:26:00.000Z',
|
|
||||||
status: 'invited',
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.acceptInvitation('new-password');
|
|
||||||
|
|
||||||
const refetchedUser = await user.$query();
|
|
||||||
|
|
||||||
expect(refetchedUser.invitationToken).toBe(null);
|
|
||||||
expect(refetchedUser.invitationTokenSentAt).toBe(null);
|
|
||||||
expect(refetchedUser.status).toBe('active');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,5 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import Role from '../../src/models/role.js';
|
import Role from '../../src/models/role';
|
||||||
|
|
||||||
export const createRole = async (params = {}) => {
|
export const createRole = async (params = {}) => {
|
||||||
const name = faker.lorem.word();
|
const name = faker.lorem.word();
|
||||||
|
@@ -6,6 +6,18 @@ const createFlowMock = async (flow) => {
|
|||||||
status: flow.status,
|
status: flow.status,
|
||||||
createdAt: flow.createdAt.getTime(),
|
createdAt: flow.createdAt.getTime(),
|
||||||
updatedAt: flow.updatedAt.getTime(),
|
updatedAt: flow.updatedAt.getTime(),
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
status: 'incomplete',
|
||||||
|
type: 'trigger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 2,
|
||||||
|
status: 'incomplete',
|
||||||
|
type: 'action',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -29,8 +29,6 @@ function ControlledAutocomplete(props) {
|
|||||||
options = [],
|
options = [],
|
||||||
dependsOn = [],
|
dependsOn = [],
|
||||||
showOptionValue,
|
showOptionValue,
|
||||||
renderInput,
|
|
||||||
showHelperText = true,
|
|
||||||
...autocompleteProps
|
...autocompleteProps
|
||||||
} = props;
|
} = props;
|
||||||
let dependsOnValues = [];
|
let dependsOnValues = [];
|
||||||
@@ -107,18 +105,16 @@ function ControlledAutocomplete(props) {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
renderInput={(params) => renderInput(params, fieldState)}
|
|
||||||
/>
|
/>
|
||||||
{showHelperText && (
|
|
||||||
<FormHelperText
|
<FormHelperText
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={Boolean(fieldState.isTouched && fieldState.error)}
|
error={Boolean(fieldState.isTouched && fieldState.error)}
|
||||||
>
|
>
|
||||||
{fieldState.isTouched
|
{fieldState.isTouched
|
||||||
? fieldState.error?.message || description
|
? fieldState.error?.message || description
|
||||||
: description}
|
: description}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -136,8 +132,6 @@ ControlledAutocomplete.propTypes = {
|
|||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
renderInput: PropTypes.func.isRequired,
|
|
||||||
showHelperText: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlledAutocomplete;
|
export default ControlledAutocomplete;
|
||||||
|
@@ -13,14 +13,12 @@ function Form(props) {
|
|||||||
resolver,
|
resolver,
|
||||||
render,
|
render,
|
||||||
mode = 'all',
|
mode = 'all',
|
||||||
reValidateMode = 'onBlur',
|
|
||||||
automaticValidation = true,
|
|
||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode,
|
reValidateMode: 'onBlur',
|
||||||
resolver,
|
resolver,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
@@ -32,9 +30,7 @@ function Form(props) {
|
|||||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (automaticValidation) {
|
methods.trigger();
|
||||||
methods.trigger();
|
|
||||||
}
|
|
||||||
}, [methods.trigger, form]);
|
}, [methods.trigger, form]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -60,8 +56,6 @@ Form.propTypes = {
|
|||||||
render: PropTypes.func,
|
render: PropTypes.func,
|
||||||
resolver: PropTypes.func,
|
resolver: PropTypes.func,
|
||||||
mode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit', 'onTouched', 'all']),
|
mode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit', 'onTouched', 'all']),
|
||||||
reValidateMode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit']),
|
|
||||||
automaticValidation: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
||||||
|
@@ -38,21 +38,9 @@ function LoginForm() {
|
|||||||
const { token } = data;
|
const { token } = data;
|
||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errors = error?.response?.data?.errors
|
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
||||||
? Object.values(error.response.data.errors)
|
variant: 'error',
|
||||||
: [];
|
});
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
for (const [error] of errors) {
|
|
||||||
enqueueSnackbar(error, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,51 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useFormContext } from 'react-hook-form';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ControlledCheckbox from 'components/ControlledCheckbox';
|
|
||||||
|
|
||||||
const ActionField = ({ action, subject, disabled, name, syncIsCreator }) => {
|
|
||||||
const { formState, resetField } = useFormContext();
|
|
||||||
|
|
||||||
const actionDefaultValue =
|
|
||||||
formState.defaultValues?.[name]?.[subject.key]?.[action.key].value;
|
|
||||||
const conditionFieldName = `${name}.${subject.key}.${action.key}.conditions.isCreator`;
|
|
||||||
const conditionFieldTouched =
|
|
||||||
formState.touchedFields?.[name]?.[subject.key]?.[action.key]?.conditions
|
|
||||||
?.isCreator === true;
|
|
||||||
|
|
||||||
const handleSyncIsCreator = (newValue) => {
|
|
||||||
if (
|
|
||||||
syncIsCreator &&
|
|
||||||
actionDefaultValue === false &&
|
|
||||||
!conditionFieldTouched
|
|
||||||
) {
|
|
||||||
resetField(conditionFieldName, { defaultValue: newValue });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ControlledCheckbox
|
|
||||||
disabled={disabled}
|
|
||||||
name={`${name}.${subject.key}.${action.key}.value`}
|
|
||||||
dataTest={`${action.key.toLowerCase()}-checkbox`}
|
|
||||||
onChange={(e, value) => {
|
|
||||||
handleSyncIsCreator(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ActionField.propTypes = {
|
|
||||||
action: PropTypes.shape({
|
|
||||||
key: PropTypes.string.isRequired,
|
|
||||||
subjects: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
}),
|
|
||||||
subject: PropTypes.shape({
|
|
||||||
key: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
syncIsCreator: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionField;
|
|
@@ -25,6 +25,7 @@ function PermissionSettings(props) {
|
|||||||
subject,
|
subject,
|
||||||
actions,
|
actions,
|
||||||
conditions,
|
conditions,
|
||||||
|
defaultChecked,
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { getValues, resetField } = useFormContext();
|
const { getValues, resetField } = useFormContext();
|
||||||
@@ -33,7 +34,7 @@ function PermissionSettings(props) {
|
|||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
||||||
resetField(fieldName, { keepTouched: true });
|
resetField(fieldName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -44,7 +45,7 @@ function PermissionSettings(props) {
|
|||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
||||||
const value = getValues(fieldName);
|
const value = getValues(fieldName);
|
||||||
resetField(fieldName, { defaultValue: value, keepTouched: true });
|
resetField(fieldName, { defaultValue: value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -55,7 +56,6 @@ function PermissionSettings(props) {
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={cancel}
|
onClose={cancel}
|
||||||
data-test={`${subject}-role-conditions-modal`}
|
data-test={`${subject}-role-conditions-modal`}
|
||||||
keepMounted
|
|
||||||
>
|
>
|
||||||
<DialogTitle>{formatMessage('permissionSettings.title')}</DialogTitle>
|
<DialogTitle>{formatMessage('permissionSettings.title')}</DialogTitle>
|
||||||
|
|
||||||
@@ -65,10 +65,10 @@ function PermissionSettings(props) {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell component="th" />
|
<TableCell component="th" />
|
||||||
|
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
<TableCell component="th" key={action.key}>
|
<TableCell component="th" key={action.key}>
|
||||||
<Typography
|
<Typography
|
||||||
component="div"
|
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
align="center"
|
align="center"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -89,7 +89,7 @@ function PermissionSettings(props) {
|
|||||||
sx={{ '&:last-child td': { border: 0 } }}
|
sx={{ '&:last-child td': { border: 0 } }}
|
||||||
>
|
>
|
||||||
<TableCell scope="row">
|
<TableCell scope="row">
|
||||||
<Typography variant="subtitle2" component="div">
|
<Typography variant="subtitle2">
|
||||||
{condition.label}
|
{condition.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -99,13 +99,14 @@ function PermissionSettings(props) {
|
|||||||
key={`${action.key}.${condition.key}`}
|
key={`${action.key}.${condition.key}`}
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle2" component="div">
|
<Typography variant="subtitle2">
|
||||||
{action.subjects.includes(subject) && (
|
{action.subjects.includes(subject) && (
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
|
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
|
||||||
dataTest={`${
|
dataTest={`${
|
||||||
condition.key
|
condition.key
|
||||||
}-${action.key.toLowerCase()}-checkbox`}
|
}-${action.key.toLowerCase()}-checkbox`}
|
||||||
|
defaultValue={defaultChecked}
|
||||||
disabled={
|
disabled={
|
||||||
getValues(
|
getValues(
|
||||||
`${fieldPrefix}.${action.key}.value`,
|
`${fieldPrefix}.${action.key}.value`,
|
||||||
@@ -143,6 +144,7 @@ PermissionSettings.propTypes = {
|
|||||||
fieldPrefix: PropTypes.string.isRequired,
|
fieldPrefix: PropTypes.string.isRequired,
|
||||||
subject: PropTypes.string.isRequired,
|
subject: PropTypes.string.isRequired,
|
||||||
open: PropTypes.bool,
|
open: PropTypes.bool,
|
||||||
|
defaultChecked: PropTypes.bool,
|
||||||
actions: PropTypes.arrayOf(
|
actions: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
@@ -12,15 +12,15 @@ import TableRow from '@mui/material/TableRow';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import ControlledCheckbox from 'components/ControlledCheckbox';
|
||||||
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
||||||
import PermissionSettings from './PermissionSettings.ee';
|
import PermissionSettings from './PermissionSettings.ee';
|
||||||
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
|
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
|
||||||
import ActionField from './ActionField';
|
|
||||||
|
|
||||||
const PermissionCatalogField = ({
|
const PermissionCatalogField = ({
|
||||||
name = 'permissions',
|
name = 'permissions',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
syncIsCreator = false,
|
defaultChecked = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading: isPermissionCatalogLoading } =
|
const { data, isLoading: isPermissionCatalogLoading } =
|
||||||
usePermissionCatalog();
|
usePermissionCatalog();
|
||||||
@@ -39,7 +39,6 @@ const PermissionCatalogField = ({
|
|||||||
{permissionCatalog?.actions.map((action) => (
|
{permissionCatalog?.actions.map((action) => (
|
||||||
<TableCell component="th" key={action.key}>
|
<TableCell component="th" key={action.key}>
|
||||||
<Typography
|
<Typography
|
||||||
component="div"
|
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
align="center"
|
align="center"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -63,23 +62,20 @@ const PermissionCatalogField = ({
|
|||||||
data-test={`${subject.key}-permission-row`}
|
data-test={`${subject.key}-permission-row`}
|
||||||
>
|
>
|
||||||
<TableCell scope="row">
|
<TableCell scope="row">
|
||||||
<Typography variant="subtitle2" component="div">
|
<Typography variant="subtitle2">{subject.label}</Typography>
|
||||||
{subject.label}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{permissionCatalog?.actions.map((action) => (
|
{permissionCatalog?.actions.map((action) => (
|
||||||
<TableCell key={`${subject.key}.${action.key}`} align="center">
|
<TableCell key={`${subject.key}.${action.key}`} align="center">
|
||||||
<Typography variant="subtitle2" component="div">
|
<Typography variant="subtitle2">
|
||||||
{action.subjects.includes(subject.key) && (
|
{action.subjects.includes(subject.key) && (
|
||||||
<ActionField
|
<ControlledCheckbox
|
||||||
action={action}
|
|
||||||
subject={subject}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
name={name}
|
name={`${name}.${subject.key}.${action.key}.value`}
|
||||||
syncIsCreator={syncIsCreator}
|
dataTest={`${action.key.toLowerCase()}-checkbox`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!action.subjects.includes(subject.key) && '-'}
|
{!action.subjects.includes(subject.key) && '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -104,6 +100,7 @@ const PermissionCatalogField = ({
|
|||||||
subject={subject.key}
|
subject={subject.key}
|
||||||
actions={permissionCatalog?.actions}
|
actions={permissionCatalog?.actions}
|
||||||
conditions={permissionCatalog?.conditions}
|
conditions={permissionCatalog?.conditions}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -117,7 +114,7 @@ const PermissionCatalogField = ({
|
|||||||
PermissionCatalogField.propTypes = {
|
PermissionCatalogField.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
syncIsCreator: PropTypes.bool,
|
defaultChecked: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PermissionCatalogField;
|
export default PermissionCatalogField;
|
||||||
|
@@ -7,9 +7,9 @@ function Variable({ attributes, children, element, disabled }) {
|
|||||||
const focused = useFocused();
|
const focused = useFocused();
|
||||||
const label = (
|
const label = (
|
||||||
<>
|
<>
|
||||||
{children}
|
|
||||||
<span style={{ fontWeight: 500 }}>{element.name}</span>:{' '}
|
<span style={{ fontWeight: 500 }}>{element.name}</span>:{' '}
|
||||||
<span style={{ fontWeight: 300 }}>{element.sampleValue}</span>
|
<span style={{ fontWeight: 300 }}>{element.sampleValue}</span>
|
||||||
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@@ -11,17 +11,14 @@ export default function SubscriptionCancelledAlert() {
|
|||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const subscription = useSubscription();
|
const subscription = useSubscription();
|
||||||
const trial = useUserTrial();
|
const trial = useUserTrial();
|
||||||
|
|
||||||
|
if (subscription?.data?.status === 'active' || trial.hasTrial)
|
||||||
|
return <React.Fragment />;
|
||||||
|
|
||||||
const cancellationEffectiveDateObject = DateTime.fromISO(
|
const cancellationEffectiveDateObject = DateTime.fromISO(
|
||||||
subscription?.data?.cancellationEffectiveDate,
|
subscription?.data?.cancellationEffectiveDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
subscription?.data?.status === 'active' ||
|
|
||||||
trial.hasTrial ||
|
|
||||||
!cancellationEffectiveDateObject.isValid
|
|
||||||
)
|
|
||||||
return <React.Fragment />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
severity="warning"
|
severity="warning"
|
||||||
|
@@ -31,7 +31,6 @@ function TextField(props) {
|
|||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
'data-test': dataTest,
|
'data-test': dataTest,
|
||||||
showError = false,
|
|
||||||
...textFieldProps
|
...textFieldProps
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +47,6 @@ function TextField(props) {
|
|||||||
onBlur: controllerOnBlur,
|
onBlur: controllerOnBlur,
|
||||||
...field
|
...field
|
||||||
},
|
},
|
||||||
fieldState: { error },
|
|
||||||
}) => (
|
}) => (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
@@ -74,7 +72,6 @@ function TextField(props) {
|
|||||||
inputProps={{
|
inputProps={{
|
||||||
'data-test': dataTest,
|
'data-test': dataTest,
|
||||||
}}
|
}}
|
||||||
{...(showError && { helperText: error?.message, error: !!error })}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -92,7 +89,6 @@ TextField.propTypes = {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
showError: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TextField;
|
export default TextField;
|
||||||
|
@@ -44,7 +44,7 @@ function BillingCard(props) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" fontWeight="bold">
|
<Typography variant="h6" fontWeight="bold">
|
||||||
{title || '---'}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
@@ -119,12 +119,12 @@ export default function UsageDataInformation() {
|
|||||||
text: 'Upgrade plan',
|
text: 'Upgrade plan',
|
||||||
},
|
},
|
||||||
nextBillAmount: {
|
nextBillAmount: {
|
||||||
title: null,
|
title: '---',
|
||||||
action: null,
|
action: null,
|
||||||
text: null,
|
text: null,
|
||||||
},
|
},
|
||||||
nextBillDate: {
|
nextBillDate: {
|
||||||
title: null,
|
title: '---',
|
||||||
action: null,
|
action: null,
|
||||||
text: null,
|
text: null,
|
||||||
},
|
},
|
||||||
@@ -137,9 +137,7 @@ export default function UsageDataInformation() {
|
|||||||
text: formatMessage('usageDataInformation.cancelPlan'),
|
text: formatMessage('usageDataInformation.cancelPlan'),
|
||||||
},
|
},
|
||||||
nextBillAmount: {
|
nextBillAmount: {
|
||||||
title: subscription?.nextBillAmount
|
title: `€${subscription?.nextBillAmount}`,
|
||||||
? `€${subscription?.nextBillAmount}`
|
|
||||||
: null,
|
|
||||||
action: subscription?.updateUrl,
|
action: subscription?.updateUrl,
|
||||||
text: formatMessage('usageDataInformation.updatePaymentMethod'),
|
text: formatMessage('usageDataInformation.updatePaymentMethod'),
|
||||||
},
|
},
|
||||||
|
@@ -45,36 +45,3 @@ export function getPermissions(computedPermissions) {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getComputedPermissionsDefaultValues = (
|
|
||||||
data,
|
|
||||||
conditionsInitialValues,
|
|
||||||
) => {
|
|
||||||
if (!data) return {};
|
|
||||||
|
|
||||||
const conditions = {};
|
|
||||||
data.conditions.forEach((condition) => {
|
|
||||||
conditions[condition.key] =
|
|
||||||
conditionsInitialValues?.[condition.key] || false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
data.subjects.forEach((subject) => {
|
|
||||||
const subjectKey = subject.key;
|
|
||||||
result[subjectKey] = {};
|
|
||||||
|
|
||||||
data.actions.forEach((action) => {
|
|
||||||
const actionKey = action.key;
|
|
||||||
|
|
||||||
if (action.subjects.includes(subjectKey)) {
|
|
||||||
result[subjectKey][actionKey] = {
|
|
||||||
value: false,
|
|
||||||
conditions: { ...conditions },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
@@ -5,8 +5,6 @@ import Stack from '@mui/material/Stack';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
@@ -25,42 +23,6 @@ function generateFormRoleMappings(roleMappings) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueRemoteRoleName = (array, context, formatMessage) => {
|
|
||||||
const seen = new Set();
|
|
||||||
for (const [index, value] of array.entries()) {
|
|
||||||
if (seen.has(value.remoteRoleName)) {
|
|
||||||
const path = `${context.path}[${index}].remoteRoleName`;
|
|
||||||
return context.createError({
|
|
||||||
message: `${formatMessage('roleMappingsForm.remoteRoleName')} must be unique`,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
seen.add(value.remoteRoleName);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getValidationSchema = (formatMessage) =>
|
|
||||||
yup.object({
|
|
||||||
roleMappings: yup
|
|
||||||
.array()
|
|
||||||
.of(
|
|
||||||
yup.object({
|
|
||||||
roleId: yup
|
|
||||||
.string()
|
|
||||||
.required(`${formatMessage('roleMappingsForm.role')} is required`),
|
|
||||||
remoteRoleName: yup
|
|
||||||
.string()
|
|
||||||
.required(
|
|
||||||
`${formatMessage('roleMappingsForm.remoteRoleName')} is required`,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.test('unique-remoteRoleName', '', (value, ctx) => {
|
|
||||||
return uniqueRemoteRoleName(value, ctx, formatMessage);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function RoleMappings({ provider, providerLoading }) {
|
function RoleMappings({ provider, providerLoading }) {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
@@ -132,15 +94,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
<Typography variant="h3">
|
<Typography variant="h3">
|
||||||
{formatMessage('roleMappingsForm.title')}
|
{formatMessage('roleMappingsForm.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Form
|
<Form defaultValues={defaultValues} onSubmit={handleRoleMappingsUpdate}>
|
||||||
defaultValues={defaultValues}
|
|
||||||
onSubmit={handleRoleMappingsUpdate}
|
|
||||||
resolver={yupResolver(getValidationSchema(formatMessage))}
|
|
||||||
mode="onSubmit"
|
|
||||||
reValidateMode="onChange"
|
|
||||||
noValidate
|
|
||||||
automaticValidation={false}
|
|
||||||
>
|
|
||||||
<Stack direction="column" spacing={2}>
|
<Stack direction="column" spacing={2}>
|
||||||
<RoleMappingsFieldArray />
|
<RoleMappingsFieldArray />
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
@@ -55,7 +55,6 @@ function RoleMappingsFieldArray() {
|
|||||||
label={formatMessage('roleMappingsForm.remoteRoleName')}
|
label={formatMessage('roleMappingsForm.remoteRoleName')}
|
||||||
fullWidth
|
fullWidth
|
||||||
required
|
required
|
||||||
showError
|
|
||||||
/>
|
/>
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
name={`roleMappings.${index}.roleId`}
|
name={`roleMappings.${index}.roleId`}
|
||||||
@@ -63,17 +62,14 @@ function RoleMappingsFieldArray() {
|
|||||||
disablePortal
|
disablePortal
|
||||||
disableClearable
|
disableClearable
|
||||||
options={generateRoleOptions(roles)}
|
options={generateRoleOptions(roles)}
|
||||||
renderInput={(params, { error }) => (
|
renderInput={(params) => (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...params}
|
{...params}
|
||||||
label={formatMessage('roleMappingsForm.role')}
|
label={formatMessage('roleMappingsForm.role')}
|
||||||
required
|
required
|
||||||
error={!!error}
|
|
||||||
helperText={error?.message}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
loading={isRolesLoading}
|
loading={isRolesLoading}
|
||||||
showHelperText={false}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@@ -11,13 +11,9 @@ import Form from 'components/Form';
|
|||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import {
|
import { getPermissions } from 'helpers/computePermissions.ee';
|
||||||
getComputedPermissionsDefaultValues,
|
|
||||||
getPermissions,
|
|
||||||
} from 'helpers/computePermissions.ee';
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
||||||
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
|
||||||
|
|
||||||
export default function CreateRole() {
|
export default function CreateRole() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -25,21 +21,6 @@ export default function CreateRole() {
|
|||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const { mutateAsync: createRole, isPending: isCreateRolePending } =
|
const { mutateAsync: createRole, isPending: isCreateRolePending } =
|
||||||
useAdminCreateRole();
|
useAdminCreateRole();
|
||||||
const { data: permissionCatalogData } = usePermissionCatalog();
|
|
||||||
|
|
||||||
const defaultValues = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
computedPermissions: getComputedPermissionsDefaultValues(
|
|
||||||
permissionCatalogData?.data,
|
|
||||||
{
|
|
||||||
isCreator: true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[permissionCatalogData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRoleCreation = async (roleData) => {
|
const handleRoleCreation = async (roleData) => {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +64,7 @@ export default function CreateRole() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form onSubmit={handleRoleCreation} defaultValues={defaultValues}>
|
<Form onSubmit={handleRoleCreation}>
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<TextField
|
<TextField
|
||||||
required={true}
|
required={true}
|
||||||
@@ -100,7 +81,10 @@ export default function CreateRole() {
|
|||||||
data-test="description-input"
|
data-test="description-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PermissionCatalogField name="computedPermissions" />
|
<PermissionCatalogField
|
||||||
|
name="computedPermissions"
|
||||||
|
defaultChecked={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@@ -5,7 +5,6 @@ import Stack from '@mui/material/Stack';
|
|||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { merge } from 'lodash';
|
|
||||||
|
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -14,25 +13,21 @@ import PermissionCatalogField from 'components/PermissionCatalogField/index.ee';
|
|||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import {
|
import {
|
||||||
getComputedPermissionsDefaultValues,
|
|
||||||
getPermissions,
|
getPermissions,
|
||||||
getRoleWithComputedPermissions,
|
getRoleWithComputedPermissions,
|
||||||
} from 'helpers/computePermissions.ee';
|
} from 'helpers/computePermissions.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAdminUpdateRole from 'hooks/useAdminUpdateRole';
|
import useAdminUpdateRole from 'hooks/useAdminUpdateRole';
|
||||||
import useRole from 'hooks/useRole.ee';
|
import useRole from 'hooks/useRole.ee';
|
||||||
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
|
||||||
|
|
||||||
export default function EditRole() {
|
export default function EditRole() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { roleId } = useParams();
|
const { roleId } = useParams();
|
||||||
const { data: roleData, isLoading: isRoleLoading } = useRole({ roleId });
|
const { data, loading: isRoleLoading } = useRole({ roleId });
|
||||||
const { mutateAsync: updateRole, isPending: isUpdateRolePending } =
|
const { mutateAsync: updateRole, isPending: isUpdateRolePending } =
|
||||||
useAdminUpdateRole(roleId);
|
useAdminUpdateRole(roleId);
|
||||||
const { data: permissionCatalogData } = usePermissionCatalog();
|
const role = data?.data;
|
||||||
const role = roleData?.data;
|
|
||||||
const permissionCatalog = permissionCatalogData?.data;
|
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const handleRoleUpdate = async (roleData) => {
|
const handleRoleUpdate = async (roleData) => {
|
||||||
@@ -57,20 +52,7 @@ export default function EditRole() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = React.useMemo(() => {
|
const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
|
||||||
const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
|
|
||||||
const computedPermissionsDefaultValues =
|
|
||||||
getComputedPermissionsDefaultValues(permissionCatalog);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...roleWithComputedPermissions,
|
|
||||||
computedPermissions: merge(
|
|
||||||
{},
|
|
||||||
computedPermissionsDefaultValues,
|
|
||||||
roleWithComputedPermissions.computedPermissions,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}, [role, permissionCatalog]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
@@ -82,7 +64,10 @@ export default function EditRole() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form defaultValues={defaultValues} onSubmit={handleRoleUpdate}>
|
<Form
|
||||||
|
defaultValues={roleWithComputedPermissions}
|
||||||
|
onSubmit={handleRoleUpdate}
|
||||||
|
>
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
{isRoleLoading && (
|
{isRoleLoading && (
|
||||||
<>
|
<>
|
||||||
@@ -110,11 +95,12 @@ export default function EditRole() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PermissionCatalogField
|
<PermissionCatalogField
|
||||||
name="computedPermissions"
|
name="computedPermissions"
|
||||||
disabled={role?.isAdmin}
|
disabled={role?.isAdmin}
|
||||||
syncIsCreator
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
Reference in New Issue
Block a user