Repository: openops-cloud/openops
Pull request #507: Add seed table logic for known cost types
Author: login: rmn-snpk
Created: 2025-04-28T07:46:38Z | Merged: 2025-04-29T12:13:54Z

<!--
Ensure the title clearly reflects what was changed.
Provide a clear and concise description of the changes made. The PR should only contain the changes related to the issue, and no other unrelated changes.
-->

Fixes OPS-1628.

## Additional Notes

<!--
Why was it changed?
Any relevant context or links?
Add refactoring notes (if applicable), preferably as comments in the code (if you refactored code, explain what was moved/changed and why).
-->

## Testing Checklist

Check all that apply:

- [X] I tested the feature thoroughly, including edge cases

- [X] I verified all affected areas still work as expected

- [X] Automated tests were added/updated if necessary

- [X] Changes are backwards compatible with any existing data, otherwise a migration script is provided



Diff:
diff --git a/packages/server/api/src/app/database/seeds/openops-knonw-cost-types-by-application-seed.ts b/packages/server/api/src/app/database/seeds/openops-knonw-cost-types-by-application-seed.ts
new file mode 100644
index 0000000000..6d4fcea1d1
--- /dev/null
+++ b/packages/server/api/src/app/database/seeds/openops-knonw-cost-types-by-application-seed.ts
@@ -0,0 +1,44 @@
+import { getTableByName } from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { FlagEntity } from '../../flags/flag.entity';
+import { SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME } from '../../openops-tables/template-tables/create-known-cost-types-by-application-table';
+import { seedTemplateTablesService } from '../../openops-tables/template-tables/seed-tables-for-templates';
+import { databaseConnection } from '../database-connection';
+
+const KNOWN_COST_TYPES_BY_APPLICATION = 'KNOWNCOSTTYPES';
+
+const tableAlreadyCreated = async (): Promise<boolean> => {
+  const flagRepo = databaseConnection().getRepository(FlagEntity);
+  const tablesSeedFlag = await flagRepo.findOneBy({
+    id: KNOWN_COST_TYPES_BY_APPLICATION,
+  });
+  return tablesSeedFlag?.value === true;
+};
+
+const setTableSeedFlag = async (): Promise<void> => {
+  const flagRepo = databaseConnection().getRepository(FlagEntity);
+
+  await flagRepo.save({
+    id: KNOWN_COST_TYPES_BY_APPLICATION,
+    value: true,
+  });
+};
+
+export const seedKnownCostTypesByApplicationTable = async (): Promise<void> => {
+  if (await tableAlreadyCreated()) {
+    logger.info(
+      `Skip: ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} already seeded`,
+    );
+    return;
+  }
+
+  const table = await getTableByName(
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+  );
+
+  if (!table) {
+    await seedTemplateTablesService.createKnownCostTypesByApplicationTable();
+  }
+
+  await setTableSeedFlag();
+};
diff --git a/packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts b/packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
new file mode 100644
index 0000000000..781d4e8edf
--- /dev/null
+++ b/packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
@@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  getFields,
+  getPrimaryKeyFieldFromFields,
+  makeOpenOpsTablesPatch,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { createTable } from '../create-table';
+
+export const SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME =
+  'Known cost types by application';
+
+export async function createKnownCostTypesByApplicationTable(
+  token: string,
+  databaseId: number,
+) {
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Start`,
+  );
+
+  const table = await createTable(
+    databaseId,
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+    [['ID']],
+    token,
+  );
+
+  const fields = await getFields(table.id, token);
+  const primaryField = getPrimaryKeyFieldFromFields(fields);
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding primary field ID with id: ${primaryField.id}`,
+  );
+  await makeOpenOpsTablesPatch<unknown>(
+    `api/database/fields/${primaryField.id}/`,
+    {
+      name: 'ID',
+      type: 'uuid',
+    },
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
+  );
+
+  await addField(token, table.id, {
+    name: 'Verified',
+    type: 'boolean',
+  });
+
+  await addField(token, table.id, {
+    name: 'Total Cost',
+    type: 'number',
+    number_decimal_places: 10,
+  });
+
+  await addField(token, table.id, {
+    name: 'Total Consumed Quantity',
+    type: 'number',
+    number_decimal_places: 0,
+  });
+
+  const fieldNames = ['Application', 'SKU id', 'Service Category'];
+
+  for (const fieldName of fieldNames) {
+    await addField(token, table.id, { name: fieldName, type: 'text' });
+  }
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Done`,
+  );
+}
+
+async function addField(token: string, tableId: number, fieldBody: any) {
+  const createFieldEndpoint = `api/database/fields/table/${tableId}/`;
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding field ${fieldBody.name}`,
+  );
+  const field = await makeOpenOpsTablesPost<{ id: number }>(
+    createFieldEndpoint,
+    fieldBody,
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding field ${fieldBody.name} with id: ${field.id}`,
+  );
+}
diff --git a/packages/server/api/src/app/openops-tables/template-tables/seed-tables-for-templates.ts b/packages/server/api/src/app/openops-tables/template-tables/seed-tables-for-templates.ts
index 5744100f5e..def692c423 100644
--- a/packages/server/api/src/app/openops-tables/template-tables/seed-tables-for-templates.ts
+++ b/packages/server/api/src/app/openops-tables/template-tables/seed-tables-for-templates.ts
@@ -7,6 +7,7 @@ import { createAggregatedCostsTable } from './create-aggregated-costs-table';
 import { createAutoEc2InstancesShutdownTable } from './create-auto-ec2-instances-shutdown-table';
 import { createBusinessUnitsTable } from './create-business-units-table';
 import { createIdleEbsVolumesToDeleteTable } from './create-idle-ebs-volumes-to-delete-table';
+import { createKnownCostTypesByApplicationTable } from './create-known-cost-types-by-application-table';
 import { createOpportunitiesTable } from './create-opportunities-table';
 import { createResourceBuTagAssignmentTable } from './create-resource-bu-tag-assignment-table';
 import { createTagOwnerMappingTable } from './create-tag-owner-mapping-table';
@@ -44,4 +45,13 @@ export const seedTemplateTablesService = {
 
     await createAggregatedCostsTable(databaseId, token);
   },
+
+  async createKnownCostTypesByApplicationTable() {
+    const { token } = await authenticateDefaultUserInOpenOpsTables();
+    const databaseId = await getDefaultDatabaseId(token);
+
+    await createKnownCostTypesByApplicationTable(token, databaseId);
+
+    logger.info('[Seeding Known cost types by application table] Done');
+  },
 };
diff --git a/packages/server/api/src/main.ts b/packages/server/api/src/main.ts
index c762864722..e765457443 100644
--- a/packages/server/api/src/main.ts
+++ b/packages/server/api/src/main.ts
@@ -12,6 +12,7 @@ import { seedDevData } from './app/database/seeds/dev-seeds';
 import { seedFocusDataAggregationTemplateTable } from './app/database/seeds/openops-aggregated-costs-seed';
 import * as analytics from './app/database/seeds/openops-analytics-seed';
 import { deleteOldOpportunitiesTable } from './app/database/seeds/openops-delete-old-opportunities-table';
+import { seedKnownCostTypesByApplicationTable } from './app/database/seeds/openops-knonw-cost-types-by-application-seed';
 import { seedOpportunitesTemplateTable } from './app/database/seeds/openops-opportunities-table-seed';
 import { updateOpenopsTablesDatabase } from './app/database/seeds/openops-tables-rename-database';
 import { upsertAdminUser } from './app/database/seeds/seed-admin';
@@ -61,6 +62,7 @@ const main = async (): Promise<void> => {
     await seedTemplateTables();
     await seedOpportunitesTemplateTable();
     await seedFocusDataAggregationTemplateTable();
+    await seedKnownCostTypesByApplicationTable();
     await analytics.seedAnalytics();
 
     initializeLock();


Reviews:
user: login: bigfluffycookie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:07:34Z
user: login: rmn-snpk
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T10:05:08Z
user: login: rmn-snpk
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T10:05:15Z
user: login: bigfluffycookie
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:13:38Z

Review comments:
diff_hunk: @@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  getFields,
+  getPrimaryKeyFieldFromFields,
+  makeOpenOpsTablesPatch,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { createTable } from '../create-table';
+
+export const SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME =
+  'Known cost types by application';
+
+export async function createKnownCostTypesByApplicationTable(
+  token: string,
+  databaseId: number,
+) {
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Start`,
+  );
+
+  const table = await createTable(
+    databaseId,
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+    [['ID']],
+    token,
+  );
+
+  const fields = await getFields(table.id, token);
+  const primaryField = getPrimaryKeyFieldFromFields(fields);
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding primary field ID with id: ${primaryField.id}`,
+  );
+  await makeOpenOpsTablesPatch<unknown>(
+    `api/database/fields/${primaryField.id}/`,
+    {
+      name: 'ID',
+      type: 'uuid',
+    },
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
+  );
+
+  await addField(token, table.id, {
+    name: 'Verified',
+    type: 'boolean',
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalCost',
path: packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
user: login: bigfluffycookie
body: Total Cost
created_at: 2025-04-28T15:07:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  getFields,
+  getPrimaryKeyFieldFromFields,
+  makeOpenOpsTablesPatch,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { createTable } from '../create-table';
+
+export const SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME =
+  'Known cost types by application';
+
+export async function createKnownCostTypesByApplicationTable(
+  token: string,
+  databaseId: number,
+) {
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Start`,
+  );
+
+  const table = await createTable(
+    databaseId,
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+    [['ID']],
+    token,
+  );
+
+  const fields = await getFields(table.id, token);
+  const primaryField = getPrimaryKeyFieldFromFields(fields);
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding primary field ID with id: ${primaryField.id}`,
+  );
+  await makeOpenOpsTablesPatch<unknown>(
+    `api/database/fields/${primaryField.id}/`,
+    {
+      name: 'ID',
+      type: 'uuid',
+    },
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
+  );
+
+  await addField(token, table.id, {
+    name: 'Verified',
+    type: 'boolean',
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalCost',
+    type: 'number',
+    number_decimal_places: 10,
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalConsumedQuantity',
path: packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
user: login: bigfluffycookie
body: Total Consumed Quantity
created_at: 2025-04-28T15:07:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  getFields,
+  getPrimaryKeyFieldFromFields,
+  makeOpenOpsTablesPatch,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { createTable } from '../create-table';
+
+export const SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME =
+  'Known cost types by application';
+
+export async function createKnownCostTypesByApplicationTable(
+  token: string,
+  databaseId: number,
+) {
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Start`,
+  );
+
+  const table = await createTable(
+    databaseId,
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+    [['ID']],
+    token,
+  );
+
+  const fields = await getFields(table.id, token);
+  const primaryField = getPrimaryKeyFieldFromFields(fields);
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding primary field ID with id: ${primaryField.id}`,
+  );
+  await makeOpenOpsTablesPatch<unknown>(
+    `api/database/fields/${primaryField.id}/`,
+    {
+      name: 'ID',
+      type: 'uuid',
+    },
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
+  );
+
+  await addField(token, table.id, {
+    name: 'Verified',
+    type: 'boolean',
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalCost',
path: packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
user: login: rmn-snpk
body: Done
created_at: 2025-04-29T10:05:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  axiosTablesSeedRetryConfig,
+  createAxiosHeaders,
+  getFields,
+  getPrimaryKeyFieldFromFields,
+  makeOpenOpsTablesPatch,
+  makeOpenOpsTablesPost,
+} from '@openops/common';
+import { logger } from '@openops/server-shared';
+import { createTable } from '../create-table';
+
+export const SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME =
+  'Known cost types by application';
+
+export async function createKnownCostTypesByApplicationTable(
+  token: string,
+  databaseId: number,
+) {
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Start`,
+  );
+
+  const table = await createTable(
+    databaseId,
+    SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
+    [['ID']],
+    token,
+  );
+
+  const fields = await getFields(table.id, token);
+  const primaryField = getPrimaryKeyFieldFromFields(fields);
+
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] Before adding primary field ID with id: ${primaryField.id}`,
+  );
+  await makeOpenOpsTablesPatch<unknown>(
+    `api/database/fields/${primaryField.id}/`,
+    {
+      name: 'ID',
+      type: 'uuid',
+    },
+    createAxiosHeaders(token),
+    axiosTablesSeedRetryConfig,
+  );
+  logger.debug(
+    `[Seeding ${SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME} table] After adding primary field ID with id: ${primaryField.id}`,
+  );
+
+  await addField(token, table.id, {
+    name: 'Verified',
+    type: 'boolean',
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalCost',
+    type: 'number',
+    number_decimal_places: 10,
+  });
+
+  await addField(token, table.id, {
+    name: 'TotalConsumedQuantity',
path: packages/server/api/src/app/openops-tables/template-tables/create-known-cost-types-by-application-table.ts
user: login: rmn-snpk
body: Done
created_at: 2025-04-29T10:05:14Z
author_association: NONE

PR comments:
user: login: linear[bot]
created_at: 2025-04-28T07:46:41Z
body: <p><a href="https://linear.app/openops/issue/OPS-1628/upload-new-detect-new-or-unknown-cost-types-as-template-and-seed-table">OPS-1628 Upload new "Detect new or unknown cost types" as template and seed table</a></p>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-29T10:06:05Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=openops-cloud_openops&pullRequest=507) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=openops-cloud_openops&pullRequest=507&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=openops-cloud_openops&pullRequest=507&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=openops-cloud_openops&pullRequest=507&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=openops-cloud_openops&pullRequest=507&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=openops-cloud_openops&pullRequest=507&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=openops-cloud_openops&pullRequest=507)


author_association: NONE