Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ yarn.lock
.tagoio
.tago-lock.tagoio init.lock
tagoconfig
.tago-lock.dev.lock
12 changes: 10 additions & 2 deletions src/analysis/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ import { userDel } from "../services/user/remove";
// import { deleteAlert } from "../services/alerts/remove";
// import { editAlert } from "../services/alerts/edit";

function _fixDashCustomBtnID(environment: { [key: string]: any }, scope: Data[]) {
const data = (scope as any).find((x: any) => x.entity_table_button_id);
if (data) {
environment._widget_exec = data.entity_table_button_id;
}
}

/**
* This function is the main function of the analysis.
* @param context The context of the analysis, containing the environment variables and parameters.
Expand Down Expand Up @@ -91,8 +98,9 @@ async function startAnalysis(context: TagoContext, scope: Data[]): Promise<void>

//Plan routing
router.register(planAdd).whenInputFormID("create-plan");
router.register(planDel).whenVariableLike("plan_").whenWidgetExec("delete");
router.register(planEdit).whenVariableLike("plan_").whenWidgetExec("edit");
_fixDashCustomBtnID(environment, scope);
router.register(planDel).whenCustomBtnID("delete-plan");;
router.register(planEdit).whenCustomBtnID("edit-plan");;

// //Alert routing
// router.register(createAlert).whenInputFormID("create-alert");
Expand Down
18 changes: 10 additions & 8 deletions src/lib/edit.tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { TagsObj } from "@tago-io/sdk/lib/types";
* @param debug
* @returns
*/
function TagResolver(rawTags: TagsObj[], debug: boolean = false) {
function TagResolver(rawTags: TagsObj[], debug: boolean = false, type: "device" | "entity" = "device") {
const tags = JSON.parse(JSON.stringify(rawTags)) as TagsObj[];
const newTags: TagsObj[] = [];

Expand All @@ -25,7 +25,7 @@ function TagResolver(rawTags: TagsObj[], debug: boolean = false) {
* @param {string} value value of the Tag
* @returns
*/
setTag: function (key: string, value: string) {
setTag: (key: string, value: string) => {
if (typeof key !== "string") {
throw "[TagResolver] key is not a string";
}
Expand All @@ -36,15 +36,15 @@ function TagResolver(rawTags: TagsObj[], debug: boolean = false) {
if (!tagExist || tagExist.value !== value) {
newTags.push({ key, value });
}
return this;
return tagResolver;
},

/**
* Apply the changes to the tags
* @param {string} deviceID Device ID to apply the changes
* @returns
*/
apply: async function (deviceID: string) {
apply: async (deviceID: string) => {
if (debug) {
return newTags;
}
Expand All @@ -58,17 +58,19 @@ function TagResolver(rawTags: TagsObj[], debug: boolean = false) {
}
}

await Resources.devices.edit(deviceID, { tags });
if (type === "device") {
await Resources.devices.edit(deviceID, { tags });
} else {
await Resources.entities.edit(deviceID, { tags });
}
},

/**
* Check if there is any change to be applied.
* @returns {boolean} true if there is any change to be applied.
* @memberof TagResolver
*/
hasChanged: function () {
return newTags.length > 0;
},
hasChanged: () => newTags.length > 0,
};

return tagResolver;
Expand Down
90 changes: 90 additions & 0 deletions src/lib/entity-name-exists.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { fetchEntityList } from './fetch-entity-list';
import { entityNameExists } from './entity-name-exists';

// Mock the fetchEntityList module
vi.mock('./fetch-entity-list');

describe('entityNameExists', () => {
beforeEach(() => {
// Clear mock before each test
vi.clearAllMocks();
});

describe('when creating a new entity', () => {
it('should return true if entity with same name exists', async () => {
const mockTags = [{ key: 'type', value: 'test' }];

vi.mocked(fetchEntityList).mockResolvedValueOnce([
{ id: '1', name: 'test-entity', tags: mockTags },
]);

const result = await entityNameExists({
name: 'test-entity',
tags: mockTags,
isEdit: false,
});

expect(result).toBe(true);
expect(fetchEntityList).toHaveBeenCalledWith({
name: 'test-entity',
tags: mockTags,
});
});

it('should return false if no entity with same name exists', async () => {
vi.mocked(fetchEntityList).mockResolvedValueOnce([]);

const result = await entityNameExists({
name: 'unique-entity',
tags: [{ key: 'type', value: 'test' }],
});

expect(result).toBe(false);
});
});

describe('when editing an entity', () => {
it('should return true if multiple entities with same name exist', async () => {
const mockTags = [{ key: 'type', value: 'test' }];

vi.mocked(fetchEntityList).mockResolvedValueOnce([
{ id: '1', name: 'test-entity', tags: mockTags },
{ id: '2', name: 'test-entity', tags: mockTags },
]);

const result = await entityNameExists({
name: 'test-entity',
tags: mockTags,
isEdit: true,
});

expect(result).toBe(true);
});

it('should return false if only one entity with same name exists', async () => {
const mockTags = [{ key: 'type', value: 'test' }];

vi.mocked(fetchEntityList).mockResolvedValueOnce([
{ id: '1', name: 'test-entity', tags: mockTags },
]);

const result = await entityNameExists({
name: 'test-entity',
tags: mockTags,
isEdit: true,
});

expect(result).toBe(false);
});
});

it('should handle error from fetchEntityList', async () => {
vi.mocked(fetchEntityList).mockRejectedValueOnce(new Error('API Error'));

await expect(entityNameExists({
name: 'test-entity',
tags: [{ key: 'type', value: 'test' }],
})).rejects.toThrow('API Error');
});
});
33 changes: 33 additions & 0 deletions src/lib/entity-name-exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TagsObj } from "@tago-io/sdk/lib/types";
import { fetchEntityList } from "./fetch-entity-list";

interface EntitySource {
name: string;
tags: TagsObj[];
isEdit?: boolean;
}

/**
* The Entity Creation and Edit utilize this method.
* @description Check if entity name exists
* @param {string} name Entity name
* @param {TagsObj[]} tags Entity tags
* @param {boolean} isEdit When editing a entity, if a entity with the same name already exists, it should return two entities.
* This is because the frontend automatically handles the editing process.
*/
async function entityNameExists({ name, tags, isEdit = false }: EntitySource) {
const entity = await fetchEntityList({
name,
tags,
});

if (isEdit && entity.length > 1) {
return true;
} else if (!isEdit && entity.length > 0) {
return true;
}

return false;
}

export { entityNameExists };
32 changes: 32 additions & 0 deletions src/lib/fetch-entity-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Resources } from "@tago-io/sdk";
import { EntityListItem, EntityQuery } from "@tago-io/sdk/lib/modules/Resources/entities.types";

type FetchEntityResponse = Pick<EntityListItem, "id" | "name" | "tags" | "created_at">;
/**
* Fetchs the entity list using filters.
* Automatically apply pagination to not run on throughtput errors.
* @param filter filter conditions of the request
* @returns
*/
async function fetchEntityList(filter: EntityQuery["filter"]): Promise<FetchEntityResponse[]> {
let entity_list: FetchEntityResponse[] = [];

for (let index = 1; index < 9999; index++) {
const amount = 100;
const foundEntities = await Resources.entities.list({
page: index,
fields: ["id", "name", "tags", "created_at"],
filter,
amount,
});

entity_list = entity_list.concat(foundEntities);
if (foundEntities.length < amount) {
return entity_list;
}
}

return entity_list;
}

export { fetchEntityList, FetchEntityResponse };
16 changes: 16 additions & 0 deletions src/lib/get-zod-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ZodError } from "zod";

/**
* Function to get the error message from zod
* @param error
* @returns
*/

async function getZodError(error: any) {
if (error instanceof ZodError) {
throw error.issues.shift()?.message;
}
throw error;
}

export { getZodError };
41 changes: 41 additions & 0 deletions src/lib/undo-entity-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DeviceListScope } from "@tago-io/sdk/lib/modules/Utils/router/router.types";
import { TagResolver } from "./edit.tag";
import { Resources } from "@tago-io/sdk";
import { EntityInfo } from "@tago-io/sdk/lib/modules/Resources/entities.types";

/**
* Reverts changes made to a site's device list and tags.
*
* @param {Object} params - The parameters for the function.
* @param {DeviceListScope[]} params.scope - The scope of the device list changes, scoped to a single device.
* @param {EntityInfo} params.entityInfo - Information about the site entity, including tags.
*
* @returns {Promise<void>} A promise that resolves when the changes have been undone.
*
* @remarks
* This function reverts changes made to the name and tags of a device within a site.
* It uses the `TagResolver` to manage tag changes and applies them if any changes are detected.
*/
async function undoEntityChanges({ scope, entityInfo }: { scope: DeviceListScope[]; entityInfo: EntityInfo }) {
const tagResolver = TagResolver(entityInfo.tags);

// Entity editions are always scoped to a single entity.
const entityScope = scope[0];

for (const key of Object.keys(entityScope)) {
if (key === "name") {
const old_name = entityScope?.old?.[key] as string;
await Resources.entities.edit(entityInfo.id, { name: old_name });
} else if (key.includes("tags.")) {
const tag_key = key.replace("tags.", "");
const old_value = entityScope?.old?.[key] as string;
tagResolver.setTag(tag_key, old_value);
}
}

if (tagResolver.hasChanged()) {
await tagResolver.apply(entityInfo.id);
}
}

export { undoEntityChanges };
73 changes: 73 additions & 0 deletions src/lib/url-creator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect } from "vitest";
import { createURL } from "./url-creator";

describe("url-creator", () => {
describe("createURL", () => {
it("should create a URL with base only", () => {
const url = createURL()
.setBase("https://example.com")
.build();

expect(url).toBe("https://example.com");
});

it("should create a URL with one parameter", () => {
const url = createURL()
.setBase("https://example.com")
.addParam("id", "123")
.build();

expect(url).toBe("https://example.com?id=123");
});

it("should create a URL with multiple parameters", () => {
const url = createURL()
.setBase("https://example.com")
.addParam("id", "123")
.addParam("name", "test")
.addParam("type", "user")
.build();

expect(url).toBe("https://example.com?id=123&name=test&type=user");
});

it("should handle empty parameters", () => {
const url = createURL()
.setBase("https://example.com")
.addParam("empty", "")
.build();

expect(url).toBe("https://example.com?empty=");
});

it("should handle special characters in parameters", () => {
const url = createURL()
.setBase("https://example.com")
.addParam("query", "test&value")
.addParam("space", "test value")
.build();

expect(url).toBe("https://example.com?query=test%26value&space=test+value");
});

it("should handle chaining methods", () => {
const url = createURL()
.setBase("https://example.com")
.addParam("id", "123")
.addParam("name", "test")
.setBase("https://new-example.com")
.addParam("type", "user")
.build();

expect(url).toBe("https://new-example.com?id=123&name=test&type=user");
});

it("should handle empty base URL", () => {
const url = createURL()
.addParam("id", "123")
.build();

expect(url).toBe("?id=123");
});
});
});
Loading