mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(core): Don't create multiple owners when importing credentials or workflows (#9112)
This commit is contained in:
@@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
import * as testDb from '../shared/testDb';
|
||||
import { getAllCredentials } from '../shared/db/credentials';
|
||||
import { getAllCredentials, getAllSharedCredentials } from '../shared/db/credentials';
|
||||
import { createMember, createOwner } from '../shared/db/users';
|
||||
|
||||
const oclifConfig = new Config({ root: __dirname });
|
||||
|
||||
async function importCredential(argv: string[]) {
|
||||
const importer = new ImportCredentialsCommand(argv, oclifConfig);
|
||||
await importer.init();
|
||||
await importer.run();
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
mockInstance(InternalHooks);
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
@@ -17,7 +24,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['Credentials']);
|
||||
await testDb.truncate(['Credentials', 'SharedCredentials', 'User']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -25,25 +32,202 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
test('import:credentials should import a credential', async () => {
|
||||
const before = await getAllCredentials();
|
||||
expect(before.length).toBe(0);
|
||||
const importer = new ImportCredentialsCommand(
|
||||
['--input=./test/integration/commands/importCredentials/credentials.json'],
|
||||
oclifConfig,
|
||||
);
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit');
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await importCredential([
|
||||
'--input=./test/integration/commands/importCredentials/credentials.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({ credentialsId: '123', userId: owner.id, role: 'credential:owner' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('import:credentials should import a credential from separated files', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// import credential the first time, assigning it to the owner
|
||||
await importCredential([
|
||||
'--separate',
|
||||
'--input=./test/integration/commands/importCredentials/separate',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
name: 'cred-aws-test',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
userId: owner.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:credentials --userId ...` should fail if the credential exists already and is owned by somebody else', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const member = await createMember();
|
||||
|
||||
// import credential the first time, assigning it to the owner
|
||||
await importCredential([
|
||||
'--input=./test/integration/commands/importCredentials/credentials.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
// making sure the import worked
|
||||
const before = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(before).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
userId: owner.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await importer.init();
|
||||
try {
|
||||
await importer.run();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('process.exit');
|
||||
}
|
||||
const after = await getAllCredentials();
|
||||
expect(after.length).toBe(1);
|
||||
expect(after[0].name).toBe('cred-aws-test');
|
||||
expect(after[0].id).toBe('123');
|
||||
mockExit.mockRestore();
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
|
||||
// Import again while updating the name we try to assign the
|
||||
// credential to another user.
|
||||
await expect(
|
||||
importCredential([
|
||||
'--input=./test/integration/commands/importCredentials/credentials-updated.json',
|
||||
`--userId=${member.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with id "123" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
// only the name was updated
|
||||
name: 'cred-aws-test',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
userId: owner.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("only update credential, don't create or update owner if `--userId` is not passed", async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createOwner();
|
||||
const member = await createMember();
|
||||
|
||||
// import credential the first time, assigning it to a member
|
||||
await importCredential([
|
||||
'--input=./test/integration/commands/importCredentials/credentials.json',
|
||||
`--userId=${member.id}`,
|
||||
]);
|
||||
|
||||
// making sure the import worked
|
||||
const before = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(before).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
userId: member.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import again only updating the name and omitting `--userId`
|
||||
await importCredential([
|
||||
'--input=./test/integration/commands/importCredentials/credentials-updated.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
// only the name was updated
|
||||
name: 'cred-aws-prod',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
userId: member.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
import * as testDb from '../shared/testDb';
|
||||
import { getAllWorkflows } from '../shared/db/workflows';
|
||||
import { getAllSharedWorkflows, getAllWorkflows } from '../shared/db/workflows';
|
||||
import { createMember, createOwner } from '../shared/db/users';
|
||||
|
||||
const oclifConfig = new Config({ root: __dirname });
|
||||
|
||||
async function importWorkflow(argv: string[]) {
|
||||
const importer = new ImportWorkflowsCommand(argv, oclifConfig);
|
||||
await importer.init();
|
||||
await importer.run();
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
mockInstance(InternalHooks);
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
@@ -17,7 +24,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['Workflow']);
|
||||
await testDb.truncate(['Workflow', 'SharedWorkflow', 'User']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -25,53 +32,186 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
test('import:workflow should import active workflow and deactivate it', async () => {
|
||||
const before = await getAllWorkflows();
|
||||
expect(before.length).toBe(0);
|
||||
const importer = new ImportWorkflowsCommand(
|
||||
['--separate', '--input=./test/integration/commands/importWorkflows/separate'],
|
||||
oclifConfig,
|
||||
);
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit');
|
||||
});
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
|
||||
await importer.init();
|
||||
try {
|
||||
await importer.run();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('process.exit');
|
||||
}
|
||||
const after = await getAllWorkflows();
|
||||
expect(after.length).toBe(2);
|
||||
expect(after[0].name).toBe('active-workflow');
|
||||
expect(after[0].active).toBe(false);
|
||||
expect(after[1].name).toBe('inactive-workflow');
|
||||
expect(after[1].active).toBe(false);
|
||||
mockExit.mockRestore();
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await importWorkflow([
|
||||
'--separate',
|
||||
'--input=./test/integration/commands/importWorkflows/separate',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
workflows: [
|
||||
expect.objectContaining({ name: 'active-workflow', active: false }),
|
||||
expect.objectContaining({ name: 'inactive-workflow', active: false }),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }),
|
||||
expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('import:workflow should import active workflow from combined file and deactivate it', async () => {
|
||||
const before = await getAllWorkflows();
|
||||
expect(before.length).toBe(0);
|
||||
const importer = new ImportWorkflowsCommand(
|
||||
['--input=./test/integration/commands/importWorkflows/combined/combined.json'],
|
||||
oclifConfig,
|
||||
);
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit');
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await importWorkflow([
|
||||
'--input=./test/integration/commands/importWorkflows/combined/combined.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
workflows: [
|
||||
expect.objectContaining({ name: 'active-workflow', active: false }),
|
||||
expect.objectContaining({ name: 'inactive-workflow', active: false }),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }),
|
||||
expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:workflow --userId ...` should fail if the workflow exists already and is owned by somebody else', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const member = await createMember();
|
||||
|
||||
// Import workflow the first time, assigning it to a member.
|
||||
await importWorkflow([
|
||||
'--input=./test/integration/commands/importWorkflows/combined-with-update/original.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
const before = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure the workflow and sharing have been created.
|
||||
expect(before).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
userId: owner.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await importer.init();
|
||||
try {
|
||||
await importer.run();
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('process.exit');
|
||||
}
|
||||
const after = await getAllWorkflows();
|
||||
expect(after.length).toBe(2);
|
||||
expect(after[0].name).toBe('active-workflow');
|
||||
expect(after[0].active).toBe(false);
|
||||
expect(after[1].name).toBe('inactive-workflow');
|
||||
expect(after[1].active).toBe(false);
|
||||
mockExit.mockRestore();
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import the same workflow again, with another name but the same ID, and try
|
||||
// to assign it to the member.
|
||||
await expect(
|
||||
importWorkflow([
|
||||
'--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json',
|
||||
`--userId=${member.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with id "998" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure there is no new sharing and that the name DID NOT change.
|
||||
expect(after).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
userId: owner.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("only update the workflow, don't create or update the owner if `--userId` is not passed", async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createOwner();
|
||||
const member = await createMember();
|
||||
|
||||
// Import workflow the first time, assigning it to a member.
|
||||
await importWorkflow([
|
||||
'--input=./test/integration/commands/importWorkflows/combined-with-update/original.json',
|
||||
`--userId=${member.id}`,
|
||||
]);
|
||||
|
||||
const before = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure the workflow and sharing have been created.
|
||||
expect(before).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
userId: member.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import the same workflow again, with another name but the same ID.
|
||||
await importWorkflow([
|
||||
'--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure there is no new sharing and that the name changed.
|
||||
expect(after).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow updated' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
userId: member.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"createdAt": "2023-07-10T14:50:49.193Z",
|
||||
"updatedAt": "2023-10-27T13:34:42.917Z",
|
||||
"id": "123",
|
||||
"name": "cred-aws-prod",
|
||||
"data": {
|
||||
"region": "eu-west-1",
|
||||
"accessKeyId": "999999999999",
|
||||
"secretAccessKey": "aaaaaaaaaaaaa"
|
||||
},
|
||||
"type": "aws"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"createdAt": "2023-07-10T14:50:49.193Z",
|
||||
"updatedAt": "2023-10-27T13:34:42.917Z",
|
||||
"id": "123",
|
||||
"name": "cred-aws-test",
|
||||
"data": {
|
||||
"region": "eu-west-1",
|
||||
"accessKeyId": "999999999999",
|
||||
"secretAccessKey": "aaaaaaaaaaaaa"
|
||||
},
|
||||
"type": "aws"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"name": "active-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"name": "active-workflow updated",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user