diff --git a/.github/workflows/db-backup.yml b/.github/workflows/db-backup.yml index f2e8af8589..232e0eec92 100644 --- a/.github/workflows/db-backup.yml +++ b/.github/workflows/db-backup.yml @@ -45,10 +45,12 @@ jobs: sudo apt install -y mongodb-database-tools - name: Generate public backup + # TODO: Unfix the version of boto3 and use the latest version + # - Github issue related: https://github.com/boto/boto3/issues/4400#issuecomment-2605715606 run: | python3 -m venv venv source venv/bin/activate - pip install boto3 + pip install boto3==1.35.97 ./bin/backup.sh ./bin/prune.sh ./bin/list.sh diff --git a/site/gatsby-site/gatsby-config.js b/site/gatsby-site/gatsby-config.js index 7d1f5ec68c..4f0a6a5b10 100755 --- a/site/gatsby-site/gatsby-config.js +++ b/site/gatsby-site/gatsby-config.js @@ -87,6 +87,7 @@ const plugins = [ 'classifications', 'reports', 'entities', + 'entity_relationships', ], connectionString: config.mongodb.connectionString, extraParams: { diff --git a/site/gatsby-site/i18n/locales/en/popovers.json b/site/gatsby-site/i18n/locales/en/popovers.json index 86e60d335f..95695b825f 100644 --- a/site/gatsby-site/i18n/locales/en/popovers.json +++ b/site/gatsby-site/i18n/locales/en/popovers.json @@ -79,6 +79,14 @@ "title": "Quiet", "text": "Quiet reports are those that will not be published in the 'Latest Reports' section of the homepage. Quiet reports are used for reports that are useful for internal data but do not need to be promoted. If you are unsure, leave this field blank." }, + "relatedEntities": { + "title": "Related Entities", + "text": "Other entities that are related to the same incident. For example, if the developer of an incident is this entity but the deployer is another entity, they are marked as related entities." + }, + "entityRelationships": { + "title": "Entity Relationships", + "text": "To what other entities this entity has a direct relationship. For example, 'Amazon' and 'Amazon Workers' could be linked entities." + }, "implicated_systems": { "title": "Implicated Systems", "text": "The AI systems involved in the incident. If multiple, press “enter” to begin a new item." diff --git a/site/gatsby-site/i18n/locales/es/entities.json b/site/gatsby-site/i18n/locales/es/entities.json index 35702e184a..10c60cb1ab 100644 --- a/site/gatsby-site/i18n/locales/es/entities.json +++ b/site/gatsby-site/i18n/locales/es/entities.json @@ -30,5 +30,8 @@ "Editing Entity": "Editando Entidad", "Back to Entity: {{name}}": "Volver a Entidad: {{name}}", "Entity updated successfully.": "Entidad actualizada con éxito.", - "Error updating Entity.": "Error al actualizar la Entidad." + "Error updating Entity.": "Error al actualizar la Entidad.", + "Entity Relationships": "Relaciones de Entidad", + "Add or remove relationships to other entities": "Agregar o eliminar relaciones con otras entidades", + "Loading related entities...": "Cargando entidades relacionadas..." } diff --git a/site/gatsby-site/i18n/locales/es/popovers.json b/site/gatsby-site/i18n/locales/es/popovers.json index 7a6f518847..c9c7292e80 100644 --- a/site/gatsby-site/i18n/locales/es/popovers.json +++ b/site/gatsby-site/i18n/locales/es/popovers.json @@ -79,6 +79,14 @@ "title": "¿Es un informe silencioso?", "text": "Los informes silenciosos son aquellos que no se publicarán en la sección 'Últimos informes' de la página de inicio. Los informes silenciosos se utilizan para informes que son útiles para datos internos pero que no necesitan promocionarse. Si no está seguro, deje este campo en blanco." }, + "relatedEntities": { + "title": "Entidades relacionadas", + "text": "Otras entidades que están relacionadas con el mismo incidente. Por ejemplo, si el desarrollador de un incidente es esta entidad pero el implementador es otra entidad, se marcan como entidades relacionadas." + }, + "entityRelationships": { + "title": "Relaciones de entidades", + "text": "¿Con qué otras entidades tiene esta entidad una relación directa? Por ejemplo, 'Amazon' y 'Trabajadores de Amazon' podrían ser entidades vinculadas." + }, "implicated_systems": { "title": "Sistemas implicados", "text": "Los sistemas de IA involucrados en el incidente. Si son múltiples, presione “enter” después del elemento para ingresar uno nuevo." diff --git a/site/gatsby-site/i18n/locales/es/validation.json b/site/gatsby-site/i18n/locales/es/validation.json index a66368aa72..d49f254cb2 100644 --- a/site/gatsby-site/i18n/locales/es/validation.json +++ b/site/gatsby-site/i18n/locales/es/validation.json @@ -39,5 +39,9 @@ "Alleged implicated AI Systems can't be longer than 200 characters": "Los sistemas implicados no pueden tener más de 200 caracteres", "*Alleged Implicated AI Systems is required": "Se requieren los sistemas de IA presuntamente implicados", "Implicated AI systems must have at least 3 characters": "Los sistemas de IA implicados deben tener al menos 3 caracteres", - "Implicated AI systems can't be longer than 200 characters": "Los sistemas de IA implicados no pueden tener más de 200 caracteres" + "Implicated AI systems can't be longer than 200 characters": "Los sistemas de IA implicados no pueden tener más de 200 caracteres", + "Each alleged Developer must have at least 3 characters and less than 200": "Cada desarrollador presunto debe tener al menos 3 caracteres y menos de 200", + "Each alleged Deployer must have at least 3 characters and less than 200": "Cada implementador presunto debe tener al menos 3 caracteres y menos de 200", + "Each alleged Harmed parties must have at least 3 characters and less than 200": "Cada parte perjudicada presunta debe tener al menos 3 caracteres y menos de 200", + "Each alleged Implicated AI system must have at least 3 characters and less than 200": "Cada sistema de IA implicado presunto debe tener al menos 3 caracteres y menos de 200" } diff --git a/site/gatsby-site/i18n/locales/fr/entities.json b/site/gatsby-site/i18n/locales/fr/entities.json index f8b7417112..37e2275977 100644 --- a/site/gatsby-site/i18n/locales/fr/entities.json +++ b/site/gatsby-site/i18n/locales/fr/entities.json @@ -28,6 +28,11 @@ "Back to Entity: {{name}}": "Retour à l'entité: {{name}}", "Entity updated successfully.": "Entité mise à jour avec succès.", "Error updating Entity.": "Erreur lors de la mise à jour de l'entité.", + "Entity Relationships": "Relations d'entité", + "Add or remove relationships to other entities": "Ajouter ou supprimer des relations avec d'autres entités", + "Loading related entities...": "Chargement des entités associées...", + "The AI implicated system is": "Le système d'IA impliqué est", + "The AI implicated systems are": "Les systèmes d'IA impliqués sont", "Alleged implicated AI system:": "Systèmes d'IA présumés impliqués:", "Alleged implicated AI systems:": "Systèmes d'IA présumés impliqués:", "Incident Responses": "Réponses aux incidents", diff --git a/site/gatsby-site/i18n/locales/fr/popovers.json b/site/gatsby-site/i18n/locales/fr/popovers.json index dd7e1b6d40..a85f267e1e 100644 --- a/site/gatsby-site/i18n/locales/fr/popovers.json +++ b/site/gatsby-site/i18n/locales/fr/popovers.json @@ -67,6 +67,14 @@ "title": "Est-ce un rapport silencieux ?", "text": "Les rapports silencieux sont ceux qui ne seront pas publiés dans la section « Derniers rapports » de la page d'accueil. Les rapports silencieux sont utilisés pour les rapports utiles pour les données internes mais qui n'ont pas besoin d'être promus. Si vous n'êtes pas sûr, laissez ce champ vide." }, + "relatedEntities": { + "title": "Entités liées", + "text": "Autres entités liées au même incident. Par exemple, si le développeur d'un incident est cette entité mais que le responsable de la mise en œuvre est une autre entité, ils sont marqués comme entités liées." + }, + "entityRelationships": { + "title": "Relations d'entité", + "text": "Avec quelles autres entités cette entité entretient-elle une relation directe ? Par exemple, « Amazon » et « Amazon Workers » pourraient être des entités liées." + }, "implicated_systems": { "title": "Systèmes d'IA impliqués dans l'incident", "text": "Si plusieurs, appuyez sur « entrée » pour commencer un nouvel élément" diff --git a/site/gatsby-site/i18n/locales/fr/validation.json b/site/gatsby-site/i18n/locales/fr/validation.json index e751b3c8df..694552129e 100644 --- a/site/gatsby-site/i18n/locales/fr/validation.json +++ b/site/gatsby-site/i18n/locales/fr/validation.json @@ -41,5 +41,9 @@ "Some data is missing.": "Certaines données manquent.", "Please review. Some data is missing.": "Veuillez vérifier. Certaines données manquent.", "Implicated Systems must have at least 3 characters": "Les systèmes impliqués doivent comporter au moins 3 caractères", - "Implicated Systems can't be longer than 200 characters": "Les systèmes impliqués ne peuvent pas comporter plus de 200 caractères" + "Implicated Systems can't be longer than 200 characters": "Les systèmes impliqués ne peuvent pas comporter plus de 200 caractères", + "Each alleged Developer must have at least 3 characters and less than 200": "Chaque développeur présumé doit comporter au moins 3 caractères et moins de 200", + "Each alleged Deployer must have at least 3 characters and less than 200": "Chaque implémenteur présumé doit comporter au moins 3 caractères et moins de 200", + "Each alleged Harmed parties must have at least 3 characters and less than 200": "Chaque partie lésée présumée doit comporter au moins 3 caractères et moins de 200", + "Each alleged Implicated AI system must have at least 3 characters and less than 200": "Chaque système d'IA impliqué présumé doit comporter au moins 3 caractères et moins de 200" } diff --git a/site/gatsby-site/i18n/locales/ja/entities.json b/site/gatsby-site/i18n/locales/ja/entities.json index 8d37e689ed..2ad4b1f0d4 100644 --- a/site/gatsby-site/i18n/locales/ja/entities.json +++ b/site/gatsby-site/i18n/locales/ja/entities.json @@ -29,6 +29,13 @@ "Back to Entity: {{name}}": "組織に戻る: {{name}}", "Entity updated successfully.": "組織が正常に更新されました。", "Error updating Entity.": "組織の更新中にエラーが発生しました。", + "Entity Relationships": "組織関係", + "Add or remove relationships to other entities": "他の組織との関係を追加または削除", + "Loading related entities...": "関連する組繹を読み込み中...", + "The AI implicated system is": "関連するAIシステムは です", + "The AI implicated systems are": "関連するAIシステムは です", + "Implicated AI system:": "AI関連システム:", + "Implicated AI systems:": "AI関連システム:", "Alleged implicated AI system:": "関与が疑われるAIシステム:", "Alleged implicated AI systems:": "関与が疑われるAIシステム:" } diff --git a/site/gatsby-site/i18n/locales/ja/popovers.json b/site/gatsby-site/i18n/locales/ja/popovers.json index 087271e061..1504bae272 100644 --- a/site/gatsby-site/i18n/locales/ja/popovers.json +++ b/site/gatsby-site/i18n/locales/ja/popovers.json @@ -75,6 +75,14 @@ "title": "あなたです!", "text": "現在ログイン中なのでこの投稿は自動的にあなたのアカウントに関連づけられます。匿名で投稿するには、ブラウザウィンドウを匿名モードで開いて投稿してください。" }, + "relatedEntities": { + "title": "関連団体", + "text": "同じインシデントに関連するその他のエンティティ。たとえば、インシデントの開発者がこのエンティティで、デプロイヤーが別のエンティティである場合、それらは関連エンティティとしてマークされます。" + }, + "entityRelationships": { + "title": "エンティティ関係", + "text": "このエンティティが他のどのエンティティと直接関係しているか。たとえば、「Amazon」と「Amazon Workers」はリンクされたエンティティである可能性があります。" + }, "implicated_systems": { "title": "インシデントに関与したAIシステム", "text": "複数ある場合は、新しい項目を入力するためにEnterキーを押してください" diff --git a/site/gatsby-site/i18n/locales/ja/validation.json b/site/gatsby-site/i18n/locales/ja/validation.json index 738dcd9508..db0fc75573 100644 --- a/site/gatsby-site/i18n/locales/ja/validation.json +++ b/site/gatsby-site/i18n/locales/ja/validation.json @@ -41,5 +41,9 @@ "*Incident Date required": "*インシデント日は必須です", "*Incident ID(s) must be a number": "*インシデントIDは数字でなければいけません", "Implicated Systems must have at least 3 characters": "関連システムは3文字以上でなければいけません", - "Implicated Systems can't be longer than 200 characters": "関連システムは200文字を超えることはできません" + "Implicated Systems can't be longer than 200 characters": "関連システムは200文字を超えることはできません", + "Each alleged Developer must have at least 3 characters and less than 200": "推定される開発者は3文字以上200文字以下でなければいけません", + "Each alleged Deployer must have at least 3 characters and less than 200": "推定されるデプロイヤーは3文字以上200文字以下でなければいけません", + "Each alleged Harmed parties must have at least 3 characters and less than 200": "推定される被害グループは3文字以上200文字以下でなければいけません", + "Each alleged Implicated AI system must have at least 3 characters and less than 200": "推定される関連AIシステムは3文字以上200文字以下でなければいけません" } diff --git a/site/gatsby-site/migrations/2024.07.30T18.37.35.create-entity-relationships-collection.js b/site/gatsby-site/migrations/2024.07.30T18.37.35.create-entity-relationships-collection.js new file mode 100644 index 0000000000..99392e2e6b --- /dev/null +++ b/site/gatsby-site/migrations/2024.07.30T18.37.35.create-entity-relationships-collection.js @@ -0,0 +1,26 @@ +const config = require('../config'); + +/** + * + * @param {{context: {client: import('mongodb').MongoClient}}} context + */ + +exports.up = async ({ context: { client } }) => { + await client.connect(); + + await client.db(config.realm.production_db.db_name).createCollection('entity_relationships'); +}; +/** + * + * @param {{context: {client: import('mongodb').MongoClient}}} context + */ + +exports.down = async ({ context: { client } }) => { + await client.connect(); + + try { + await client.db(config.realm.production_db.db_name).dropCollection('entity_relationships'); + } catch (e) { + console.log(e.message); + } +}; diff --git a/site/gatsby-site/page-creators/createEntitiesPages.js b/site/gatsby-site/page-creators/createEntitiesPages.js index c570cd2be4..960ac6fc55 100644 --- a/site/gatsby-site/page-creators/createEntitiesPages.js +++ b/site/gatsby-site/page-creators/createEntitiesPages.js @@ -4,7 +4,7 @@ const { computeEntities } = require('../src/utils/entities'); const createEntitiesPages = async (graphql, createPage) => { const { - data: { incidents, entities: entitiesData, responses }, + data: { incidents, entities: entitiesData, responses, entityRelationships }, } = await graphql(` { incidents: allMongodbAiidprodIncidents { @@ -32,6 +32,17 @@ const createEntitiesPages = async (graphql, createPage) => { title } } + + entityRelationships: allMongodbAiidprodEntityRelationships( + filter: { pred: { eq: "related" } } + ) { + nodes { + sub + obj + is_symmetric + pred + } + } } `); @@ -39,6 +50,7 @@ const createEntitiesPages = async (graphql, createPage) => { incidents: incidents.nodes, entities: entitiesData.nodes, responses: responses.nodes, + entityRelationships: entityRelationships.nodes, }); for (const entity of entities) { @@ -46,6 +58,11 @@ const createEntitiesPages = async (graphql, createPage) => { const pagePath = `/entities/${id}`; + const currentEntityRelationships = + entityRelationships.nodes.filter( + (rel) => (rel.sub === id || rel.obj === id) && rel.is_symmetric + ) || []; + createPage({ path: pagePath, component: path.resolve('./src/templates/entity.js'), @@ -59,6 +76,7 @@ const createEntitiesPages = async (graphql, createPage) => { incidentsImplicatedSystems: entity.incidentsImplicatedSystems, relatedEntities: entity.relatedEntities, responses: entity.responses, + entityRelationships: currentEntityRelationships, }, }); } @@ -68,6 +86,7 @@ const createEntitiesPages = async (graphql, createPage) => { component: path.resolve('./src/templates/entities.js'), context: { entities, + entityRelationships: entityRelationships.nodes, }, }); }; diff --git a/site/gatsby-site/playwright/e2e-full/api/lookupbyurl.spec.ts b/site/gatsby-site/playwright/e2e-full/api/lookupbyurl.spec.ts index d2dc423a5b..c5e52ac98d 100644 --- a/site/gatsby-site/playwright/e2e-full/api/lookupbyurl.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/api/lookupbyurl.spec.ts @@ -23,14 +23,24 @@ test.describe('/api/lookupbyurl endpoint', () => { title: "Kronos shift scheduling software a grind for Starbucks worker", url: "https://searchhrsoftware.techtarget.com/news/4500252451/Kronos-shift-scheduling-software-a-grind-for-Starbucks-worker", }, - ], - incidents: [ { - incident_id: 3, - title: "Kronos Scheduling Algorithm Allegedly Caused Financial Issues for Starbucks Employees", - url: "https://incidentdatabase.ai/cite/3", + report_number: 9, + title: "Response Issue Report 9", + url: "https://searchhrsoftware.techtarget.com/news/4500252451/Kronos-shift-scheduling-software-a-grind-for-Starbucks-worker", }, ], + incidents: [ + { + incident_id: 3, + title: "Kronos Scheduling Algorithm Allegedly Caused Financial Issues for Starbucks Employees", + url: "https://incidentdatabase.ai/cite/3", + }, + { + incident_id: 4, + title: "Test title 4", + url: "https://incidentdatabase.ai/cite/4", + }, + ], }, { url: "https://www.nytimes.com/interactive/2014/08/13/us/starbucks-workers-scheduling-hours.html", diff --git a/site/gatsby-site/playwright/e2e-full/apps/incidents.spec.ts b/site/gatsby-site/playwright/e2e-full/apps/incidents.spec.ts index 90ba9bb04a..930a25ffdb 100644 --- a/site/gatsby-site/playwright/e2e-full/apps/incidents.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/apps/incidents.spec.ts @@ -13,7 +13,7 @@ test.describe('Incidents App', () => { test('Should display a list of incidents', async ({ page }) => { await page.goto(url); - await expect(page.locator('[data-cy="row"]')).toHaveCount(3); + await expect(page.locator('[data-cy="row"]')).toHaveCount(4); }); test('Should display an empty list of incidents on Empty environment', async ({ page, runOnlyOnEmptyEnvironment }) => { @@ -262,7 +262,7 @@ test.describe('Incidents App', () => { await page.locator('[data-cy="table-view"] button:has-text("Issue Reports")').click(); await page.waitForSelector('[data-cy="row"]'); - await expect(page.locator('[data-cy="row"]')).toHaveCount(1); + await expect(page.locator('[data-cy="row"]')).toHaveCount(2); const firstRowLink = await page.locator('[data-cy="row"] td a').first().getAttribute('href'); expect(firstRowLink).toMatch(/^\/reports\/\d+$/); diff --git a/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts b/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts index 937837cab2..8e4961960b 100644 --- a/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts @@ -86,7 +86,7 @@ test.describe('Submitted reports', () => { await page.locator('[data-cy="promote-button"]').click(); - await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Incident 4 and Report 9'); + await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Incident 5 and Report 10'); const { data: { incidents } } = await query({ query: gql`{ @@ -118,7 +118,7 @@ test.describe('Submitted reports', () => { await page.locator('[data-cy="promote-to-report-button"]').click(); - await expect(page.locator('[data-cy="toast"]')).toContainText('Successfully promoted submission to Incident 1 and Report 9'); + await expect(page.locator('[data-cy="toast"]')).toContainText('Successfully promoted submission to Incident 1 and Report 10'); const { data: { incidents } } = await query({ query: gql`{ @@ -133,7 +133,7 @@ test.describe('Submitted reports', () => { `, }); - expect(incidents.find((i) => i.incident_id === 1).reports.map((r) => r.report_number)).toContain(9); + expect(incidents.find((i) => i.incident_id === 1).reports.map((r) => r.report_number)).toContain(10); }); test('Promotes a submission to a new report and links it to multiple incidents', async ({ page, login }) => { @@ -151,8 +151,8 @@ test.describe('Submitted reports', () => { await page.locator('[data-cy="promote-to-report-button"]').click(); - await expect(page.getByText('Successfully promoted submission to Incident 2 and Report 9')).toBeVisible(); - await expect(page.getByText('Successfully promoted submission to Incident 3 and Report 9')).toBeVisible(); + await expect(page.getByText('Successfully promoted submission to Incident 2 and Report 10')).toBeVisible(); + await expect(page.getByText('Successfully promoted submission to Incident 3 and Report 10')).toBeVisible(); const { data: { incidents } } = await query({ query: gql`{ @@ -167,8 +167,8 @@ test.describe('Submitted reports', () => { `, }); - expect(incidents.find((i) => i.incident_id === 2).reports.map((r) => r.report_number)).toContain(9); - expect(incidents.find((i) => i.incident_id === 3).reports.map((r) => r.report_number)).toContain(9); + expect(incidents.find((i) => i.incident_id === 2).reports.map((r) => r.report_number)).toContain(10); + expect(incidents.find((i) => i.incident_id === 3).reports.map((r) => r.report_number)).toContain(10); }); test('Promotes a submission to a new issue', async ({ page, login }) => { @@ -185,7 +185,7 @@ test.describe('Submitted reports', () => { await page.locator('[data-cy="promote-button"]').click(); - await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Issue 9'); + await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Issue 10'); const { data: { reports } } = await query({ query: gql`{ @@ -595,7 +595,7 @@ test.describe('Submitted reports', () => { await page.locator('[data-cy="promote-button"]').click(); - await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Incident 4 and Report 9'); + await expect(page.locator('[data-cy="toast"]').first()).toContainText('Successfully promoted submission to Incident 5 and Report 10'); const { data: { incident } } = await query({ query: gql`{ @@ -679,13 +679,13 @@ test.describe('Submitted reports', () => { embedding: null, epoch_date_modified: null, flagged_dissimilar_incidents: [], - incident_id: 4, + incident_id: 5, nlp_similar_incidents: [], title: "Incident title", tsne: null, reports: [ { - report_number: 9, + report_number: 10, user: { userId: "user1", }, diff --git a/site/gatsby-site/playwright/e2e-full/cite.spec.ts b/site/gatsby-site/playwright/e2e-full/cite.spec.ts index 089a5cd94c..9bc202d9d4 100644 --- a/site/gatsby-site/playwright/e2e-full/cite.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/cite.spec.ts @@ -220,7 +220,7 @@ test.describe('Cite pages', () => { await expect(page.locator('a:has-text("Previous Incident")')).toBeVisible(); - await expect(page.locator('a:has-text("Previous Incident")')).toHaveAttribute('href', '/cite/3'); + await expect(page.locator('a:has-text("Previous Incident")')).toHaveAttribute('href', '/cite/4'); }); test('Should render the header next/previous buttons', async ({ page }) => { @@ -417,7 +417,7 @@ test.describe('Cite pages', () => { await expect(page.locator('head meta[property="twitter:image"]')).toHaveAttribute('content'); }); - test('Should subscribe to incident updates (user authenticated)', async ({ page, login }) => { + test.skip('Should subscribe to incident updates (user authenticated)', async ({ page, login }) => { await init(); @@ -550,9 +550,9 @@ test.describe('Cite pages', () => { await init(); const incident: DBIncident = { - incident_id: 4, + incident_id: 6, title: 'Test Title', - description: 'Incident 4 description', + description: 'Incident 6 description', date: "2020-01-01", "Alleged deployer of AI system": ["entity-1"], "Alleged developer of AI system": ["entity-2"], @@ -565,10 +565,10 @@ test.describe('Cite pages', () => { await init({ aiidprod: { incidents: [incident] } }); - await page.goto('/cite/4'); + await page.goto('/cite/6'); - await expect(page.getByText('Incident 4: Test Title')).toBeVisible(); - await expect(page.getByText('Incident 4 description')).toBeVisible(); + await expect(page.getByText('Incident 6: Test Title')).toBeVisible(); + await expect(page.getByText('Incident 6 description')).toBeVisible(); await expect(page.getByText('Alleged: Entity 2 developed an AI system deployed by Entity 1, which harmed Entity 3.')).toBeVisible() }); diff --git a/site/gatsby-site/playwright/e2e-full/citeEdit.spec.ts b/site/gatsby-site/playwright/e2e-full/citeEdit.spec.ts index f55ad24c14..10b24a8eda 100644 --- a/site/gatsby-site/playwright/e2e-full/citeEdit.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/citeEdit.spec.ts @@ -226,7 +226,7 @@ test.describe('Edit report', () => { }` }); - expect(result.data.reports).toHaveLength(7); + expect(result.data.reports).toHaveLength(8); expect(result.data.reports).not.toContainEqual({ report_number: 3 }); expect(result.data.incident.reports).not.toContainEqual({ report_number: 3 }); }); diff --git a/site/gatsby-site/playwright/e2e/entities.spec.ts b/site/gatsby-site/playwright/e2e-full/entities.spec.ts similarity index 74% rename from site/gatsby-site/playwright/e2e/entities.spec.ts rename to site/gatsby-site/playwright/e2e-full/entities.spec.ts index bd53470e55..b353c58158 100644 --- a/site/gatsby-site/playwright/e2e/entities.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/entities.spec.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import config from '../config'; -import { test } from '../utils'; +import { query, test } from '../utils'; +import { gql } from '@apollo/client'; +import { init } from '../memory-mongo'; test.describe('Entities page', () => { const url = '/entities'; @@ -11,50 +13,64 @@ test.describe('Entities page', () => { }); test('Displays a list of entities', async ({ page, skipOnEmptyEnvironment }) => { + await init(); await page.goto(url); + const { data: { entities } } = await query({ + query: gql` + query { + entities { + entity_id + } + } + `, + }); await page.locator('[data-cy="entities"]').isVisible(); await page.locator('[data-cy="entities"] tr').count().then(count => { - expect(count).toBeGreaterThanOrEqual(10); + expect(count).toBeGreaterThanOrEqual(entities.length); }); }); test('Filter entities by name', async ({ page, skipOnEmptyEnvironment }) => { + await init(); await page.goto(url); - await page.locator('[data-cy="input-filter-Entity"]').fill('Amazon'); + await page.locator('[data-cy="input-filter-Entity"]').fill('Entity 1'); await page.locator('[data-cy="entities"] tr').count().then(count => { - expect(count).toBeGreaterThanOrEqual(11); + expect(count).toBeGreaterThanOrEqual(1); }); }); test('Filter entities by incident title', async ({ page, skipOnEmptyEnvironment }) => { + await init(); await page.goto(url); - await page.locator('[data-cy="input-filter-As Deployer and Developer"]').fill('taxi'); + await page.locator('[data-cy="input-filter-As Deployer"]').fill('incid'); await page.locator('[data-cy="entities"] tr').count().then(count => { expect(count).toBeGreaterThanOrEqual(1); }); - await page.locator('[data-cy="row"]:has-text("Cruise")').isVisible(); + await page.locator('[data-cy="row"]:has-text("Entity 1")').isVisible(); }); test('Entities row should be expandable', async ({ page, skipOnEmptyEnvironment }) => { + await init(); await page.goto(url); - await page.locator('[data-cy="input-filter-Entity"]').fill('Amazon'); - const row = await page.locator('[data-cy="row"]').filter({ hasText: 'Amazon' }).first(); + await page.locator('[data-cy="input-filter-Entity"]').fill('Entity 1'); + const row = await page.locator('[data-cy="row"]').filter({ hasText: 'Entity 1' }).first(); await row.locator('[title="Toggle Row Expanded"]').first().click(); - const cell = await row.locator('[data-cy="cell-incidentsAsBoth"]'); + const cell = await row.locator('[data-cy="cell-incidentsAsDeployer"]'); await cell.locator('ul').isVisible(); await cell.locator('ul > li').count().then(count => { - expect(count).toBeGreaterThanOrEqual(14); + expect(count).toBeGreaterThanOrEqual(2); }); }); test('Should display Entity responses', async ({ page, skipOnEmptyEnvironment }) => { + await init(); await page.goto(url); await expect(page.locator('[data-cy="header-responses"]')).toBeVisible(); await page.locator('[data-cy="cell-responses"]').count().then(count => { - expect(count).toBeGreaterThanOrEqual(10); + expect(count).toBeGreaterThanOrEqual(1); }); - await page.locator('[data-cy="input-filter-Entity"]').fill('microsoft'); - await expect(page.locator('[data-cy="cell-responses"]').first()).toHaveText('3 Incident responses'); + await page.locator('[data-cy="input-filter-Entity"]').fill('Entity 1'); + await expect(page.locator('[data-cy="cell-responses"]').first()).toHaveText('1 Incident responses'); }); test('Should be able to sort', async ({ page, skipOnEmptyEnvironment }) => { @@ -67,7 +83,6 @@ test.describe('Entities page', () => { expect(await page.locator('[data-cy="edit-entity-btn"]').count()).toBe(0); await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); - await page.goto(url); const editButton = page.locator('[data-cy="edit-entity-btn"]').first(); expect(await editButton.getAttribute('href')).toBe('/entities/edit?entity_id=facebook'); await editButton.click(); diff --git a/site/gatsby-site/playwright/e2e-full/entityEdit.spec.ts b/site/gatsby-site/playwright/e2e-full/entityEdit.spec.ts index e2b0559fbe..3de1b9df9a 100644 --- a/site/gatsby-site/playwright/e2e-full/entityEdit.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/entityEdit.spec.ts @@ -97,10 +97,83 @@ test.describe('Edit Entity', () => { await page.getByText("Save", { exact: true }).click(); const updateEntityRequest = await waitForRequest('UpdateEntity'); - expect(updateEntityRequest.postDataJSON().variables.filter.entity_id.EQ).toBe(entity_id); - expect(updateEntityRequest.postDataJSON().variables.update.set.name).toBe(values.name); + expect(updateEntityRequest.postDataJSON().variables.input.entity_id).toBe(entity_id); + expect(updateEntityRequest.postDataJSON().variables.input.name).toBe(values.name); await expect(page.locator('.tw-toast')).toContainText('Error updating Entity.'); } ); + + test('Should successfully add Entity Relationship', async ({ page, login, skipOnEmptyEnvironment }) => { + + await init(); + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await page.goto(url); + + await page.locator('[data-cy="entity-relationships"]').fill("Entity 2"); + await page.locator("#ta-entity-relationships-item-0").click(); + + await page.getByText("Save", { exact: true }).click(); + + await expect(page.locator('.tw-toast')).toContainText('Entity updated successfully.'); + + const { data: entityRelationships } = await query({ + query: gql`{ + entity_relationships(filter: { sub: { EQ: "${entity_id}" } }) { + sub { + entity_id + } + obj { + entity_id + } + } + }`, + }); + + expect(entityRelationships.entity_relationships).toHaveLength(1); + + await expect(page.locator('.tw-toast')).toContainText('Entity updated successfully.'); + }); + + test('Should successfully remove Entity Relationship', async ({ page, login, skipOnEmptyEnvironment }) => { + + await init({ + aiidprod: { + entity_relationships: [ + { + sub: 'entity-2', + obj: entity_id, + is_symmetric: true, + }, + ], + }, + }); + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await page.goto(`/entities/edit?entity_id=${entity_id}`); + + await page.locator('.rbt.Typeahead .rbt-token-remove-button').first().click(); + + await page.getByText("Save", { exact: true }).click(); + + await expect(page.locator('.tw-toast')).toContainText('Entity updated successfully.'); + + const { data: entityRelationships } = await query({ + query: gql`{ + entity_relationships(filter: { sub: { EQ: "${entity_id}" } }) { + sub { + entity_id + } + obj { + entity_id + } + } + }`, + }); + + expect(entityRelationships.entity_relationships).toHaveLength(0); + + await expect(page.locator('.tw-toast')).toContainText('Entity updated successfully.'); + }); }); diff --git a/site/gatsby-site/playwright/e2e-full/incidents/new.spec.ts b/site/gatsby-site/playwright/e2e-full/incidents/new.spec.ts index c4840b6c44..79f5e3e2e8 100644 --- a/site/gatsby-site/playwright/e2e-full/incidents/new.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/incidents/new.spec.ts @@ -10,7 +10,13 @@ test.describe('New Incident page', () => { test('Should successfully create a new incident', async ({ page, login }) => { - await init(); + await init({ + customData: { + users: [ + { userId: 'johndoe', first_name: 'John', last_name: 'Doe', roles: ['admin'] }, + ] + } + }); await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); @@ -39,7 +45,7 @@ test.describe('New Incident page', () => { await page.getByText('Save').click(); - await page.getByText(`You have successfully create Incident 4. View incident`).waitFor(); + await page.getByText(`You have successfully create Incident 5. View incident`).waitFor(); }); test('Should clone an incident', async ({ page, login }) => { @@ -48,7 +54,7 @@ test.describe('New Incident page', () => { await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD, { customData: { roles: ['admin'], first_name: 'John', last_name: 'Doe' } }); - const newIncidentId = 4; + const newIncidentId = 5; await page.goto(`${url}/?incident_id=3`); diff --git a/site/gatsby-site/playwright/e2e-full/pageCreators/createEntitiesPages.spec.ts b/site/gatsby-site/playwright/e2e-full/pageCreators/createEntitiesPages.spec.ts index 75113fe2a5..84b7f717ff 100644 --- a/site/gatsby-site/playwright/e2e-full/pageCreators/createEntitiesPages.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/pageCreators/createEntitiesPages.spec.ts @@ -69,6 +69,16 @@ test.describe('createEntitiesPages', () => { { report_number: 5 }, ], }, + entityRelationships: { + nodes: [ + { + sub: 'entity-2', + "obj": 'entity-3', + "is_symmetric": true, + pred: 'related', + } + ], + } }, }; diff --git a/site/gatsby-site/playwright/e2e-full/submit.spec.ts b/site/gatsby-site/playwright/e2e-full/submit.spec.ts index 24e5fdac4d..b8d89a08aa 100644 --- a/site/gatsby-site/playwright/e2e-full/submit.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/submit.spec.ts @@ -547,6 +547,18 @@ test.describe('The Submit form', () => { ); + test('Should **not** show a list of related reports if no data entered', async ({ page, skipOnEmptyEnvironment }) => { + + await page.goto(url); + + const parentLocator = page.locator(`[data-cy="related-reports"]`); + + const childrenCount = await parentLocator.locator('> *').count(); + + await expect(childrenCount).toBe(0); + + } + ); test('Should *not* show a list of related reports', async ({ page, skipOnEmptyEnvironment }) => { @@ -1523,4 +1535,187 @@ test.describe('The Submit form', () => { await expect(page.locator('.tw-toast:has-text("Report successfully added to review queue. You can see your submission")')).toBeVisible(); await expect(page.locator(':text("Please review. Some data is missing.")')).not.toBeVisible(); }); + + test('Should show an error for inputs with two or fewer characters in developers, deployers, harmed_parties, and implicated_systems', async ({ page }) => { + await conditionalIntercept( + page, + '**/parseNews**', + () => true, + parseNews, + 'parseNews' + ); + + await trackRequest( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindSubmissions', + 'findSubmissions' + ); + + await page.goto(url); + + await waitForRequest('findSubmissions'); + + await page.locator('input[name="url"]').fill( + `https://www.arstechnica.com/gadgets/2017/11/youtube-to-crack-down-on-inappropriate-content-masked-as-kids-cartoons/` + ); + + await page.locator('button:has-text("Fetch info")').click(); + + await waitForRequest('parseNews'); + + await page.locator('[name="incident_date"]').fill('2020-01-01'); + + await expect(page.locator('.form-has-errors')).not.toBeVisible(); + + await page.locator('[data-cy="to-step-2"]').click(); + + await page.locator('[data-cy="to-step-3"]').click(); + + await page.locator('input[name="developers"]').fill('ab'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="developers-input"] .rbt-close').click(); + + await page.locator('input[name="developers"]').fill('NewDev'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for deployers field + await page.locator('input[name="deployers"]').fill('cd'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Deployer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="deployers-input"] .rbt-close').click(); + + await page.locator('input[name="deployers"]').fill('NewDep'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Deployer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for harmed_parties field + await page.locator('input[name="harmed_parties"]').fill('ef'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Harmed parties must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="harmed_parties-input"] .rbt-close').click(); + + await page.locator('input[name="harmed_parties"]').fill('NewHarmed'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Harmed parties must have at least 3 characters and less than 200')).not.toBeVisible(); + + await page.locator('input[name="implicated_systems"]').fill('gh'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Implicated AI system must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="implicated_systems-input"] .rbt-close').click(); + + await page.locator('input[name="implicated_systems"]').fill('NewSystem'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Implicated AI system must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for "New selection" behavior + await page.locator('input[name="developers"]').fill('xy'); + await page.locator('#developers-tags .dropdown-item:has-text("New selection: xy")').click(); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('div').filter({ hasText: /^xy×Remove$/ }).getByLabel('Remove').click(); + + await page.locator('input[name="developers"]').fill('ValidDev'); + await page.locator('#developers-tags .dropdown-item:has-text("New selection: ValidDev")').click(); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Submit to ensure the form does not proceed with errors + await page.locator('button[type="submit"]').click(); + await expect(page.locator('.tw-toast:has-text("Report successfully added to review queue. You can see your submission")')).toBeVisible(); + await expect(page.locator(':text("Please review. Some data is missing.")')).not.toBeVisible(); + }); + + test('Should show an error for inputs with 200 or more characters in developers, deployers, harmed_parties, and implicated_systems', async ({ page }) => { + await init(); + await conditionalIntercept( + page, + '**/parseNews**', + () => true, + parseNews, + 'parseNews' + ); + + await trackRequest( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindSubmissions', + 'findSubmissions' + ); + + await page.goto(url); + + await waitForRequest('findSubmissions'); + + await page.locator('input[name="url"]').fill( + `https://www.arstechnica.com/gadgets/2017/11/youtube-to-crack-down-on-inappropriate-content-masked-as-kids-cartoons/` + ); + + await page.locator('button:has-text("Fetch info")').click(); + + await waitForRequest('parseNews'); + + await page.locator('[name="incident_date"]').fill('2020-01-01'); + + await expect(page.locator('.form-has-errors')).not.toBeVisible(); + + await page.locator('[data-cy="to-step-2"]').click(); + + await page.locator('[data-cy="to-step-3"]').click(); + + await page.locator('input[name="developers"]').fill('This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="developers-input"] .rbt-close').click(); + + await page.locator('input[name="developers"]').fill('NewDev'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for deployers field + await page.locator('input[name="deployers"]').fill('This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Deployer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="deployers-input"] .rbt-close').click(); + + await page.locator('input[name="deployers"]').fill('NewDep'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Deployer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for harmed_parties field + await page.locator('input[name="harmed_parties"]').fill('This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Harmed parties must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="harmed_parties-input"] .rbt-close').click(); + + await page.locator('input[name="harmed_parties"]').fill('NewHarmed'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Harmed parties must have at least 3 characters and less than 200')).not.toBeVisible(); + + await page.locator('input[name="implicated_systems"]').fill('This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Implicated AI system must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('[data-testid="implicated_systems-input"] .rbt-close').click(); + + await page.locator('input[name="implicated_systems"]').fill('NewSystem'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=Each alleged Implicated AI system must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Check for "New selection" behavior + await page.locator('input[name="developers"]').fill('This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error'); + await page.locator('#developers-tags .dropdown-item:has-text("New selection: This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error")').click(); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).toBeVisible(); + await page.locator('div').filter({ hasText: /^This test input text is designed to have precisely two hundred characters total so it works perfectly for checking HTML input validation to ensure that anything this length or longer should show error×Remove$/ }).getByLabel('Remove').click(); + + await page.locator('input[name="developers"]').fill('ValidDev'); + await page.locator('#developers-tags .dropdown-item:has-text("New selection: ValidDev")').click(); + await expect(page.locator('text=Each alleged Developer must have at least 3 characters and less than 200')).not.toBeVisible(); + + // Submit to ensure the form does not proceed with errors + await page.locator('button[type="submit"]').click(); + await expect(page.locator('.tw-toast:has-text("Report successfully added to review queue. You can see your submission")')).toBeVisible(); + await expect(page.locator(':text("Please review. Some data is missing.")')).not.toBeVisible(); + }); + + }); \ No newline at end of file diff --git a/site/gatsby-site/playwright/e2e/discover.spec.ts b/site/gatsby-site/playwright/e2e/discover.spec.ts index 0308e2ca21..f2cd486348 100644 --- a/site/gatsby-site/playwright/e2e/discover.spec.ts +++ b/site/gatsby-site/playwright/e2e/discover.spec.ts @@ -247,7 +247,7 @@ test.describe('The Discover app', () => { await expect(modal).not.toBeVisible(); }); - test('Opens an archive link', async ({ page, skipOnEmptyEnvironment }) => { + test.skip('Opens an archive link', async ({ page, skipOnEmptyEnvironment }) => { test.slow(); diff --git a/site/gatsby-site/playwright/memory-mongo.ts b/site/gatsby-site/playwright/memory-mongo.ts index e2b466d04e..e67400d92d 100644 --- a/site/gatsby-site/playwright/memory-mongo.ts +++ b/site/gatsby-site/playwright/memory-mongo.ts @@ -13,6 +13,7 @@ import candidates from './seeds/aiidprod/candidates'; import duplicates from './seeds/aiidprod/duplicates'; import users from './seeds/customData/users'; +import entity_relationships from './seeds/aiidprod/entity_relationships'; import subscriptions from './seeds/customData/subscriptions'; import reportsHistory from './seeds/history/reportsHistory'; @@ -50,6 +51,7 @@ export const init = async (seed?: Record +& { sub: string } & {obj: string}; + +const entity_relationships: DBEntity_Relationship[] = [ + { + sub: 'entity-2', + "obj": 'entity-3', + "is_symmetric": true, + pred: 'related', + } +] + +export default entity_relationships; \ No newline at end of file diff --git a/site/gatsby-site/playwright/seeds/aiidprod/incidents.ts b/site/gatsby-site/playwright/seeds/aiidprod/incidents.ts index ecf90e2521..79ab25e70a 100644 --- a/site/gatsby-site/playwright/seeds/aiidprod/incidents.ts +++ b/site/gatsby-site/playwright/seeds/aiidprod/incidents.ts @@ -90,6 +90,49 @@ const incidents: DBIncident[] = [ // "created_at": 1407974400000 implicated_systems: ["entity-1"], }, + { + incident_id: 4, + date: "2014-08-14", + reports: [ + 9 + ], + "Alleged deployer of AI system": [ + "entity-1" + ], + "Alleged developer of AI system": [], + "Alleged harmed or nearly harmed parties": [], + implicated_systems: [], + description: "Test description 4", + title: "Test title 4", + editors: [ + "619b47ea5eed5334edfa3bbc" + ], + nlp_similar_incidents: [ + { + incident_id: 1, + similarity: 0.9988328814506531 + }, + ], + editor_similar_incidents: [], + editor_dissimilar_incidents: [], + flagged_dissimilar_incidents: [], + embedding: { + vector: [ + -0.06841292232275009, + 0.08255906403064728 + ], + from_reports: [ + 16, + 17 + ] + }, + tsne: { + x: 0.0487331398239335, + y: 0.38604577108881916 + }, + // this field is currently present in the database but not mapped to any graphql fueld + // "created_at": 1407974400000 + }, ] export default incidents; \ No newline at end of file diff --git a/site/gatsby-site/playwright/seeds/aiidprod/reports.ts b/site/gatsby-site/playwright/seeds/aiidprod/reports.ts index de103d826c..a77273a2f1 100644 --- a/site/gatsby-site/playwright/seeds/aiidprod/reports.ts +++ b/site/gatsby-site/playwright/seeds/aiidprod/reports.ts @@ -274,7 +274,49 @@ const items: DBReport[] = [ title: "", editor_notes: "", user: "", - } + }, + { + authors: [ + "News Writer" + ], + date_downloaded: new Date(1555113600000), + date_modified: new Date(1592092800000), + date_published: new Date(1440633600000), + date_submitted: new Date(1559347200000), + description: "Response Report", + epoch_date_downloaded: 1555113600, + epoch_date_modified: 1592092800, + epoch_date_published: 1440633600, + epoch_date_submitted: 1559347200, + image_url: "https://cdn.ttgtmedia.com/visuals/searchFinancialApplications/hr_technology/financialapplications_article_004.jpg", + language: "en", + report_number: 9, + source_domain: "searchhrsoftware.techtarget.com", + submitters: [ + "Catherine Olsson" + ], + text: "Response report 9 text", + title: "Response Issue Report 9", + url: "https://searchhrsoftware.techtarget.com/news/4500252451/Kronos-shift-scheduling-software-a-grind-for-Starbucks-worker", + tags: ["response"], + plain_text: "Response report 9 text", + editor_notes: "", + cloudinary_id: "reports/cdn.ttgtmedia.com/visuals/searchFinancialApplications/hr_technology/financialapplications_article_004.jpg", + embedding: { + vector: [ + -0.09370243549346924, + 0.10497249662876129 + ], + from_text_hash: "6dfba3c22a24c31e017dfbe8594e312cc127cb38" + }, + is_incident_report: false, + + // TODO: ditto + user: 'user1', + + // TODO: ditto + // created_at: 1559347200000 + }, ] export default items; \ No newline at end of file diff --git a/site/gatsby-site/server/fields/common.ts b/site/gatsby-site/server/fields/common.ts index ec5f70bfc5..a92b38dc09 100644 --- a/site/gatsby-site/server/fields/common.ts +++ b/site/gatsby-site/server/fields/common.ts @@ -212,6 +212,7 @@ export const createNotificationsOnNewIncident = async (fullDocument: DBIncident, type: 'new-incidents', incident_id: incidentId, processed: false, + created_at: new Date(), }); const entityFields: (keyof DBIncident)[] = [ @@ -239,6 +240,7 @@ export const createNotificationsOnNewIncident = async (fullDocument: DBIncident, incident_id: incidentId, entity_id: entityId, processed: false, + created_at: new Date(), }); } } @@ -363,6 +365,7 @@ export const logReportHistory = async (updated: DBReport, context: Context) => { const reportHistory: DBReportHistory = { ...updated, modifiedBy: context.user?.id ?? '', + created_at: new Date(), _id: undefined, } @@ -376,6 +379,7 @@ export const logIncidentHistory = async (updated: DBIncident, context: Context) const incidentHistory: DBIncidentHistory = { ...updated, modifiedBy: context.user?.id ?? '', + created_at: new Date(), _id: undefined, } diff --git a/site/gatsby-site/server/fields/entities.ts b/site/gatsby-site/server/fields/entities.ts index 32e3f546f6..44723b5bcc 100644 --- a/site/gatsby-site/server/fields/entities.ts +++ b/site/gatsby-site/server/fields/entities.ts @@ -1,28 +1,108 @@ -import { GraphQLFieldConfigMap } from "graphql"; +import { GraphQLFieldConfigMap, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; import { allow } from "graphql-shield"; import { generateMutationFields, generateQueryFields } from "../utils"; import { Context } from "../interfaces"; import { EntityType } from "../types/entity"; - +import { GraphQLDateTime, GraphQLJSONObject } from "graphql-scalars"; export const queryFields: GraphQLFieldConfigMap = { - ...generateQueryFields({ collectionName: 'entities', Type: EntityType }) + ...generateQueryFields({ collectionName: 'entities', Type: EntityType }) } +const UpdateOneEntityPayload = new GraphQLObjectType({ + name: 'UpdateOneEntityPayload', + fields: { + created_at: { type: GraphQLDateTime }, + date_modified: { type: GraphQLDateTime }, + entity_id: { type: new GraphQLNonNull(GraphQLString) }, + name: { type: new GraphQLNonNull(GraphQLString) }, + entity_relationships_to_add: { type: new GraphQLList(GraphQLString) }, + entity_relationships_to_remove: { type: new GraphQLList(GraphQLString) }, + }, +}); + +const UpdateOneEntityInput = new GraphQLInputObjectType({ + name: 'UpdateOneEntityInput', + fields: { + created_at: { type: GraphQLDateTime }, + date_modified: { type: GraphQLDateTime }, + entity_id: { type: new GraphQLNonNull(GraphQLString) }, + name: { type: new GraphQLNonNull(GraphQLString) }, + entity_relationships_to_add: { type: new GraphQLList(GraphQLJSONObject) }, + entity_relationships_to_remove: { type: new GraphQLList(GraphQLJSONObject) }, + }, +}); export const mutationFields: GraphQLFieldConfigMap = { - ...generateMutationFields({ collectionName: 'entities', Type: EntityType, generateFields: ['updateOne', 'upsertOne'] }), -} + ...generateMutationFields({ collectionName: 'entities', Type: EntityType, generateFields: ['updateOne', 'upsertOne'] }), -export const permissions = { - Query: { - entity: allow, - entities: allow, + updateEntityAndRelationships: { + type: new GraphQLNonNull(UpdateOneEntityPayload), + args: { + input: { type: new GraphQLNonNull(UpdateOneEntityInput) }, }, - Mutation: { - updateOneEntity: allow, - upsertOneEntity: allow, + + resolve: async (_source, { input }, context) => { + + const entities = context.client.db('aiidprod').collection("entities"); + const entity_relationships = context.client.db('aiidprod').collection("entity_relationships"); + + const target = await entities.findOne({ entity_id: input.entity_id }); + + if (!target) { + throw new Error('Entity not found'); + } + + const { _id: undefined, ...entity } = target; + + const updatedInput = { + ...input, + } + delete updatedInput._id; + delete updatedInput.entity_relationships_to_add + delete updatedInput.entity_relationships_to_remove + + await entities.updateOne({ entity_id: input.entity_id }, { $set: { ...updatedInput } }); + + const newRelationships = input.entity_relationships_to_add.map((entity_relationship: any) => ({ + sub: updatedInput.entity_id, + obj: entity_relationship.id, + is_symmetric: true, + pred: 'related', + })) + + if (newRelationships.length > 0) { + await entity_relationships.insertMany(newRelationships); + } + + const oldRelationships = input.entity_relationships_to_remove.map((entity_relationship: any) => ({ + sub: updatedInput.entity_id, + obj: entity_relationship.id, + is_symmetric: true, + pred: 'related', + })) + + if (oldRelationships.length > 0) { + await entity_relationships.deleteMany({ $or: oldRelationships.map((entity_relationship: any) => ({ sub: entity_relationship.sub, obj: entity_relationship.obj })) }); + } + + return { + ...input + }; } + } +} + +export const permissions = { + Query: { + entity: allow, + entities: allow, + }, + Mutation: { + updateEntityAndRelationships: allow, + upsertOneEntity: allow, + updateOneEntity: allow + } } \ No newline at end of file diff --git a/site/gatsby-site/server/fields/entity_relationships.ts b/site/gatsby-site/server/fields/entity_relationships.ts new file mode 100644 index 0000000000..c16b1fa738 --- /dev/null +++ b/site/gatsby-site/server/fields/entity_relationships.ts @@ -0,0 +1,28 @@ +import { GraphQLFieldConfigMap, GraphQLObjectType } from "graphql"; +import { allow } from "graphql-shield"; +import { generateMutationFields, generateQueryFields } from "../utils"; +import { Context } from "../interfaces"; +import { Entity_relationshipType } from "../types/entity_relationship"; + + +export const queryFields: GraphQLFieldConfigMap = { + + ...generateQueryFields({ collectionName: 'entity_relationships', Type: Entity_relationshipType }) +} + + +export const mutationFields: GraphQLFieldConfigMap = { + + ...generateMutationFields({ collectionName: 'entity_relationships', Type: Entity_relationshipType, generateFields: ['updateOne', 'upsertOne', 'deleteOne'] }), +} + +export const permissions = { + Query: { + entity_relationships: allow, + }, + Mutation: { + updateOneEntity_relationship: allow, + upsertOneEntity_relationship: allow, + deleteOneEntity_relationship: allow, + } +} \ No newline at end of file diff --git a/site/gatsby-site/server/fields/reports.ts b/site/gatsby-site/server/fields/reports.ts index 1d97b4376c..355f553c5f 100644 --- a/site/gatsby-site/server/fields/reports.ts +++ b/site/gatsby-site/server/fields/reports.ts @@ -4,6 +4,7 @@ import { generateMutationFields, generateQueryFields, getQueryResolver } from ". import { isRole } from "../rules"; import { linkReportsToIncidents, logReportHistory } from "./common"; import { ReportType } from "../types/report"; +import { Context, DBReport } from "../interfaces"; export const queryFields: GraphQLFieldConfigMap = { @@ -185,10 +186,10 @@ export const mutationFields: GraphQLFieldConfigMap = { type: new GraphQLNonNull(CreateVariantInput), }, }, - resolve: async (source, { input }, context) => { + resolve: async (source, { input }, context: Context) => { const incidents = context.client.db('aiidprod').collection("incidents"); - const reports = context.client.db('aiidprod').collection("reports"); + const reports = context.client.db('aiidprod').collection("reports"); const parentIncident = await incidents.findOne({ incident_id: input.incidentId }); @@ -196,7 +197,9 @@ export const mutationFields: GraphQLFieldConfigMap = { throw `Incident ${input.incidentId} not found`; } - const report_number = (await reports.find({}).sort({ report_number: -1 }).limit(1).next()).report_number + 1; + const lastReport = await reports.find({}).sort({ report_number: -1 }).limit(1).next(); + + const report_number = lastReport ? lastReport.report_number + 1 : 1; const now = new Date(); @@ -225,9 +228,10 @@ export const mutationFields: GraphQLFieldConfigMap = { language: 'en', tags: ['variant:unreviewed'], inputs_outputs: input.variant.inputs_outputs, + user: context.user?.id ?? '', }; - await reports.insertOne({ ...newReport, report_number: newReport.report_number }); + await reports.insertOne({ ...newReport, report_number: newReport.report_number, created_at: new Date() }); const incident_ids = [input.incidentId]; const report_numbers = [newReport.report_number]; diff --git a/site/gatsby-site/server/fields/submissions.ts b/site/gatsby-site/server/fields/submissions.ts index d9a2f253ce..e3d0a83e10 100644 --- a/site/gatsby-site/server/fields/submissions.ts +++ b/site/gatsby-site/server/fields/submissions.ts @@ -8,7 +8,7 @@ import { GraphQLInputObjectType } from 'graphql'; import { generateMutationFields, generateQueryFields } from '../utils'; -import { Context, DBIncident, DBSubmission } from '../interfaces'; +import { Context, DBIncident, DBNotification, DBSubmission } from '../interfaces'; import { allow } from 'graphql-shield'; import { ObjectIdScalar } from '../scalars'; import { isRole } from '../rules'; @@ -62,7 +62,7 @@ export const mutationFields: GraphQLFieldConfigMap = { // TODO: Strictly type these collections using the DB* types const reports = context.client.db('aiidprod').collection("reports"); - const notificationsCollection = context.client.db('customData').collection("notifications"); + const notificationsCollection = context.client.db('customData').collection("notifications"); const incidentsHistory = context.client.db('history').collection("incidents"); const reportsHistory = context.client.db('history').collection("reports"); @@ -108,6 +108,7 @@ export const mutationFields: GraphQLFieldConfigMap = { implicated_systems: submission.implicated_systems || [], editor_notes: submission.editor_notes ?? '', flagged_dissimilar_incidents: [], + created_at: new Date(), } if (submission.embedding) { newIncident.embedding = { @@ -115,11 +116,11 @@ export const mutationFields: GraphQLFieldConfigMap = { from_reports: [report_number] } } - if(submission.epoch_date_modified) { + if (submission.epoch_date_modified) { newIncident.epoch_date_modified = submission.epoch_date_modified; } - await incidents.insertOne({ ...newIncident, incident_id: newIncident.incident_id }); + await incidents.insertOne({ ...newIncident, incident_id: newIncident.incident_id, created_at: new Date() }); await createNotificationsOnNewIncident(newIncident, context); @@ -131,7 +132,8 @@ export const mutationFields: GraphQLFieldConfigMap = { type: 'submission-promoted', incident_id: newIncident.incident_id, processed: false, - userId: submission.user + userId: submission.user, + created_at: new Date(), }); } @@ -139,6 +141,7 @@ export const mutationFields: GraphQLFieldConfigMap = { ...newIncident, reports: [report_number], incident_id: newIncident.incident_id, + created_at: new Date(), }; if (submission.user) { @@ -204,6 +207,7 @@ export const mutationFields: GraphQLFieldConfigMap = { ...incidentValues, reports: [...incidentValues.reports!, report_number], embedding, + created_at: new Date(), } if (submission.user) { @@ -245,11 +249,11 @@ export const mutationFields: GraphQLFieldConfigMap = { newReport.user = submission.user; } - if(submission.epoch_date_modified) { + if (submission.epoch_date_modified) { newReport.epoch_date_modified = submission.epoch_date_modified; } - await reports.insertOne({ ...newReport, report_number: newReport.report_number }); + await reports.insertOne({ ...newReport, report_number: newReport.report_number, created_at: new Date() }); const incident_ids: number[] = parentIncidents.map(incident => incident.incident_id!); const report_numbers: number[] = [newReport.report_number]; diff --git a/site/gatsby-site/server/generated/gql.ts b/site/gatsby-site/server/generated/gql.ts index ae1391a9a1..d064a848b0 100644 --- a/site/gatsby-site/server/generated/gql.ts +++ b/site/gatsby-site/server/generated/gql.ts @@ -24,7 +24,9 @@ const documents = { "\n mutation UpsertEntity($filter: EntityFilterType!, $update: EntityInsertType!) {\n upsertOneEntity(filter: $filter, update: $update) {\n entity_id\n name\n }\n }\n": types.UpsertEntityDocument, "\n query FindEntities {\n entities {\n entity_id\n name\n }\n }\n": types.FindEntitiesDocument, "\n query FindEntity($filter: EntityFilterType) {\n entity(filter: $filter) {\n entity_id\n name\n created_at\n date_modified\n }\n }\n": types.FindEntityDocument, - "\n mutation UpdateEntity($filter: EntityFilterType!, $update: EntityUpdateType!) {\n updateOneEntity(filter: $filter, update: $update) {\n entity_id\n }\n }\n": types.UpdateEntityDocument, + "\n mutation UpdateEntity($input: UpdateOneEntityInput!) {\n updateEntityAndRelationships(input: $input) {\n entity_id\n }\n }\n": types.UpdateEntityDocument, + "\n query FindEntity_relationships($filter: Entity_relationshipFilterType) {\n entity_relationships(filter: $filter) {\n _id\n created_at\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n is_symmetric\n }\n }\n": types.FindEntity_RelationshipsDocument, + "\n mutation UpdateEntity_relationship(\n $filter: Entity_relationshipFilterType!\n $update: Entity_relationshipUpdateType!\n ) {\n updateOneEntity_relationship(filter: $filter, update: $update) {\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n }\n }\n": types.UpdateEntity_RelationshipDocument, "\n query FindIncident($filter: IncidentFilterType) {\n incident(filter: $filter) {\n incident_id\n title\n description\n editors {\n userId\n first_name\n last_name\n }\n date\n AllegedDeployerOfAISystem {\n entity_id\n name\n }\n AllegedDeveloperOfAISystem {\n entity_id\n name\n }\n AllegedHarmedOrNearlyHarmedParties {\n entity_id\n name\n }\n implicated_systems {\n entity_id\n name\n }\n nlp_similar_incidents {\n incident_id\n similarity\n }\n editor_similar_incidents\n editor_dissimilar_incidents\n flagged_dissimilar_incidents\n reports {\n report_number\n }\n embedding {\n from_reports\n vector\n }\n editor_notes\n }\n }\n": types.FindIncidentDocument, "\n query FindIncidentsTable($filter: IncidentFilterType) {\n incidents(filter: $filter) {\n incident_id\n title\n description\n editors {\n userId\n first_name\n last_name\n }\n date\n AllegedDeployerOfAISystem {\n entity_id\n name\n }\n AllegedDeveloperOfAISystem {\n entity_id\n name\n }\n AllegedHarmedOrNearlyHarmedParties {\n entity_id\n name\n }\n implicated_systems {\n entity_id\n name\n }\n reports {\n report_number\n }\n }\n }\n": types.FindIncidentsTableDocument, "\n query FindIncidentEntities($filter: IncidentFilterType) {\n incident(filter: $filter) {\n incident_id\n AllegedDeployerOfAISystem {\n entity_id\n name\n }\n AllegedDeveloperOfAISystem {\n entity_id\n name\n }\n AllegedHarmedOrNearlyHarmedParties {\n entity_id\n name\n }\n implicated_systems {\n entity_id\n name\n }\n }\n }\n": types.FindIncidentEntitiesDocument, @@ -134,7 +136,15 @@ export function gql(source: "\n query FindEntity($filter: EntityFilterType) {\n /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\n mutation UpdateEntity($filter: EntityFilterType!, $update: EntityUpdateType!) {\n updateOneEntity(filter: $filter, update: $update) {\n entity_id\n }\n }\n"): (typeof documents)["\n mutation UpdateEntity($filter: EntityFilterType!, $update: EntityUpdateType!) {\n updateOneEntity(filter: $filter, update: $update) {\n entity_id\n }\n }\n"]; +export function gql(source: "\n mutation UpdateEntity($input: UpdateOneEntityInput!) {\n updateEntityAndRelationships(input: $input) {\n entity_id\n }\n }\n"): (typeof documents)["\n mutation UpdateEntity($input: UpdateOneEntityInput!) {\n updateEntityAndRelationships(input: $input) {\n entity_id\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n query FindEntity_relationships($filter: Entity_relationshipFilterType) {\n entity_relationships(filter: $filter) {\n _id\n created_at\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n is_symmetric\n }\n }\n"): (typeof documents)["\n query FindEntity_relationships($filter: Entity_relationshipFilterType) {\n entity_relationships(filter: $filter) {\n _id\n created_at\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n is_symmetric\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation UpdateEntity_relationship(\n $filter: Entity_relationshipFilterType!\n $update: Entity_relationshipUpdateType!\n ) {\n updateOneEntity_relationship(filter: $filter, update: $update) {\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateEntity_relationship(\n $filter: Entity_relationshipFilterType!\n $update: Entity_relationshipUpdateType!\n ) {\n updateOneEntity_relationship(filter: $filter, update: $update) {\n pred\n sub {\n entity_id\n name\n }\n obj {\n entity_id\n name\n }\n }\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/site/gatsby-site/server/generated/graphql.ts b/site/gatsby-site/server/generated/graphql.ts index b8e7cf15aa..99a0a3f412 100644 --- a/site/gatsby-site/server/generated/graphql.ts +++ b/site/gatsby-site/server/generated/graphql.ts @@ -15,6 +15,8 @@ export type Scalars = { Int: { input: number; output: number; } Float: { input: number; output: number; } DateTime: { input: any; output: any; } + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ + JSONObject: { input: any; output: any; } Long: { input: any; output: any; } ObjectId: { input: any; output: any; } }; @@ -756,6 +758,65 @@ export type EntityUpdateType = { set?: InputMaybe; }; +export type Entity_Relationship = { + __typename?: 'Entity_relationship'; + _id?: Maybe; + created_at?: Maybe; + is_symmetric?: Maybe; + obj?: Maybe; + pred?: Maybe; + sub?: Maybe; +}; + +export type Entity_RelationshipFilterType = { + AND?: InputMaybe>>; + NOR?: InputMaybe>>; + OR?: InputMaybe>>; + _id?: InputMaybe; + created_at?: InputMaybe; + is_symmetric?: InputMaybe; + obj?: InputMaybe; + pred?: InputMaybe; + sub?: InputMaybe; +}; + +export type Entity_RelationshipInsertType = { + _id?: InputMaybe; + created_at?: InputMaybe; + is_symmetric?: InputMaybe; + obj?: InputMaybe; + pred?: InputMaybe; + sub?: InputMaybe; +}; + +export type Entity_RelationshipObjRelationInput = { + link?: InputMaybe; +}; + +export type Entity_RelationshipSetType = { + _id?: InputMaybe; + created_at?: InputMaybe; + is_symmetric?: InputMaybe; + obj?: InputMaybe; + pred?: InputMaybe; + sub?: InputMaybe; +}; + +export type Entity_RelationshipSortType = { + _id?: InputMaybe; + created_at?: InputMaybe; + is_symmetric?: InputMaybe; + pred?: InputMaybe; +}; + +export type Entity_RelationshipSubRelationInput = { + link?: InputMaybe; +}; + +export type Entity_RelationshipUpdateType = { + set?: InputMaybe; +}; + export type FieldList = { __typename?: 'FieldList'; complete_from?: Maybe; @@ -866,6 +927,7 @@ export type History_Incident = { AllegedDeveloperOfAISystem?: Maybe>>; AllegedHarmedOrNearlyHarmedParties?: Maybe>>; _id?: Maybe; + created_at?: Maybe; date: Scalars['String']['output']; description?: Maybe; editor_dissimilar_incidents?: Maybe>>; @@ -920,6 +982,7 @@ export type History_IncidentFilterType = { NOR?: InputMaybe>>; OR?: InputMaybe>>; _id?: InputMaybe; + created_at?: InputMaybe; date?: InputMaybe; description?: InputMaybe; editor_dissimilar_incidents?: InputMaybe; @@ -1002,6 +1065,7 @@ export enum History_IncidentSortByInput { export type History_IncidentSortType = { _id?: InputMaybe; + created_at?: InputMaybe; date?: InputMaybe; description?: InputMaybe; editor_notes?: InputMaybe; @@ -1061,6 +1125,7 @@ export type History_Report = { _id?: Maybe; authors?: Maybe>>; cloudinary_id?: Maybe; + created_at?: Maybe; date_downloaded: Scalars['DateTime']['output']; date_modified: Scalars['DateTime']['output']; date_published: Scalars['DateTime']['output']; @@ -1133,6 +1198,7 @@ export type History_ReportFilterType = { _id?: InputMaybe; authors?: InputMaybe; cloudinary_id?: InputMaybe; + created_at?: InputMaybe; date_downloaded?: InputMaybe; date_modified?: InputMaybe; date_published?: InputMaybe; @@ -1212,6 +1278,7 @@ export enum History_ReportSortByInput { export type History_ReportSortType = { _id?: InputMaybe; cloudinary_id?: InputMaybe; + created_at?: InputMaybe; date_downloaded?: InputMaybe; date_modified?: InputMaybe; date_published?: InputMaybe; @@ -1244,6 +1311,7 @@ export type Incident = { AllegedDeveloperOfAISystem?: Maybe>>; AllegedHarmedOrNearlyHarmedParties?: Maybe>>; _id?: Maybe; + created_at?: Maybe; date: Scalars['String']['output']; description?: Maybe; editor_dissimilar_incidents?: Maybe>>; @@ -1351,6 +1419,7 @@ export type IncidentFilterType = { NOR?: InputMaybe>>; OR?: InputMaybe>>; _id?: InputMaybe; + created_at?: InputMaybe; date?: InputMaybe; description?: InputMaybe; editor_dissimilar_incidents?: InputMaybe; @@ -1381,6 +1450,7 @@ export type IncidentInsertType = { AllegedDeveloperOfAISystem?: InputMaybe; AllegedHarmedOrNearlyHarmedParties?: InputMaybe; _id?: InputMaybe; + created_at?: InputMaybe; date: Scalars['String']['input']; description?: InputMaybe; editor_dissimilar_incidents?: InputMaybe>>; @@ -1562,6 +1632,7 @@ export type IncidentSetType = { AllegedDeveloperOfAISystem?: InputMaybe; AllegedHarmedOrNearlyHarmedParties?: InputMaybe; _id?: InputMaybe; + created_at?: InputMaybe; date?: InputMaybe; description?: InputMaybe; editor_dissimilar_incidents?: InputMaybe>>; @@ -1598,6 +1669,7 @@ export enum IncidentSortByInput { export type IncidentSortType = { _id?: InputMaybe; + created_at?: InputMaybe; date?: InputMaybe; description?: InputMaybe; editor_notes?: InputMaybe; @@ -1859,6 +1931,7 @@ export type Mutation = { deleteOneCandidate?: Maybe; deleteOneChecklist?: Maybe; deleteOneDuplicate?: Maybe; + deleteOneEntity_relationship?: Maybe; deleteOneNotification?: Maybe; deleteOneQuickadd?: Maybe; deleteOneReport?: Maybe; @@ -1888,6 +1961,7 @@ export type Mutation = { replaceOneIncident?: Maybe; replaceOneNotification?: Maybe; replaceOneUser?: Maybe; + updateEntityAndRelationships: UpdateOneEntityPayload; updateManyCandidates?: Maybe; updateManyChecklists?: Maybe; updateManyDuplicates?: Maybe; @@ -1899,6 +1973,7 @@ export type Mutation = { updateOneCandidate?: Maybe; updateOneDuplicate?: Maybe; updateOneEntity?: Maybe; + updateOneEntity_relationship?: Maybe; updateOneIncident?: Maybe; updateOneNotification?: Maybe; updateOneQuickadd?: Maybe; @@ -1912,6 +1987,7 @@ export type Mutation = { upsertOneClassification?: Maybe; upsertOneDuplicate?: Maybe; upsertOneEntity?: Maybe; + upsertOneEntity_relationship?: Maybe; upsertOneNotification?: Maybe; upsertOneQuickadd?: Maybe; upsertOneSubscription?: Maybe; @@ -1982,6 +2058,13 @@ export type MutationDeleteOneDuplicateArgs = { }; +export type MutationDeleteOneEntity_RelationshipArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; +}; + + export type MutationDeleteOneNotificationArgs = { query: NotificationQueryInput; }; @@ -2111,6 +2194,11 @@ export type MutationReplaceOneUserArgs = { }; +export type MutationUpdateEntityAndRelationshipsArgs = { + input: UpdateOneEntityInput; +}; + + export type MutationUpdateManyCandidatesArgs = { filter: CandidateFilterType; update: CandidateUpdateType; @@ -2159,6 +2247,12 @@ export type MutationUpdateOneEntityArgs = { }; +export type MutationUpdateOneEntity_RelationshipArgs = { + filter: Entity_RelationshipFilterType; + update: Entity_RelationshipUpdateType; +}; + + export type MutationUpdateOneIncidentArgs = { filter: IncidentFilterType; update: IncidentUpdateType; @@ -2236,6 +2330,12 @@ export type MutationUpsertOneEntityArgs = { }; +export type MutationUpsertOneEntity_RelationshipArgs = { + filter: Entity_RelationshipFilterType; + update: Entity_RelationshipInsertType; +}; + + export type MutationUpsertOneNotificationArgs = { data: NotificationInsertInput; query?: InputMaybe; @@ -2256,6 +2356,7 @@ export type MutationUpsertOneSubscriptionArgs = { export type Notification = { __typename?: 'Notification'; _id?: Maybe; + created_at?: Maybe; entity_id?: Maybe; incident_id?: Maybe; isUpdate?: Maybe; @@ -2271,6 +2372,7 @@ export type NotificationFilterType = { NOR?: InputMaybe>>; OR?: InputMaybe>>; _id?: InputMaybe; + created_at?: InputMaybe; entity_id?: InputMaybe; incident_id?: InputMaybe; isUpdate?: InputMaybe; @@ -2351,6 +2453,7 @@ export enum NotificationSortByInput { export type NotificationSortType = { _id?: InputMaybe; + created_at?: InputMaybe; entity_id?: InputMaybe; incident_id?: InputMaybe; isUpdate?: InputMaybe; @@ -2508,6 +2611,8 @@ export type Query = { duplicates?: Maybe>>; entities?: Maybe>>; entity?: Maybe; + entity_relationship?: Maybe; + entity_relationships?: Maybe>>; history_incident?: Maybe; history_incidents?: Maybe>>; history_report?: Maybe; @@ -2602,6 +2707,20 @@ export type QueryEntityArgs = { }; +export type QueryEntity_RelationshipArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; +}; + + +export type QueryEntity_RelationshipsArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; +}; + + export type QueryHistory_IncidentArgs = { filter?: InputMaybe; pagination?: InputMaybe; @@ -2812,6 +2931,7 @@ export type Report = { _id?: Maybe; authors: Array>; cloudinary_id: Scalars['String']['output']; + created_at?: Maybe; date_downloaded: Scalars['DateTime']['output']; date_modified: Scalars['DateTime']['output']; date_published: Scalars['DateTime']['output']; @@ -2911,6 +3031,7 @@ export type ReportFilterType = { _id?: InputMaybe; authors?: InputMaybe; cloudinary_id?: InputMaybe; + created_at?: InputMaybe; date_downloaded?: InputMaybe; date_modified?: InputMaybe; date_published?: InputMaybe; @@ -2943,6 +3064,7 @@ export type ReportInsertType = { _id?: InputMaybe; authors: Array>; cloudinary_id: Scalars['String']['input']; + created_at?: InputMaybe; date_downloaded: Scalars['DateTime']['input']; date_modified: Scalars['DateTime']['input']; date_published: Scalars['DateTime']['input']; @@ -2975,6 +3097,7 @@ export type ReportSetType = { _id?: InputMaybe; authors?: InputMaybe>>; cloudinary_id?: InputMaybe; + created_at?: InputMaybe; date_downloaded?: InputMaybe; date_modified?: InputMaybe; date_published?: InputMaybe; @@ -3051,6 +3174,7 @@ export enum ReportSortByInput { export type ReportSortType = { _id?: InputMaybe; cloudinary_id?: InputMaybe; + created_at?: InputMaybe; date_downloaded?: InputMaybe; date_modified?: InputMaybe; date_published?: InputMaybe; @@ -4243,6 +4367,25 @@ export type UpdateManyPayload = { modifiedCount: Scalars['Int']['output']; }; +export type UpdateOneEntityInput = { + created_at?: InputMaybe; + date_modified?: InputMaybe; + entity_id: Scalars['String']['input']; + entity_relationships_to_add?: InputMaybe>>; + entity_relationships_to_remove?: InputMaybe>>; + name: Scalars['String']['input']; +}; + +export type UpdateOneEntityPayload = { + __typename?: 'UpdateOneEntityPayload'; + created_at?: Maybe; + date_modified?: Maybe; + entity_id: Scalars['String']['output']; + entity_relationships_to_add?: Maybe>>; + entity_relationships_to_remove?: Maybe>>; + name: Scalars['String']['output']; +}; + export type UpdateOneReportTranslationInput = { language: Scalars['String']['input']; plain_text: Scalars['String']['input']; @@ -4434,12 +4577,26 @@ export type FindEntityQueryVariables = Exact<{ export type FindEntityQuery = { __typename?: 'Query', entity?: { __typename?: 'Entity', entity_id: string, name: string, created_at?: any | null, date_modified?: any | null } | null }; export type UpdateEntityMutationVariables = Exact<{ - filter: EntityFilterType; - update: EntityUpdateType; + input: UpdateOneEntityInput; +}>; + + +export type UpdateEntityMutation = { __typename?: 'Mutation', updateEntityAndRelationships: { __typename?: 'UpdateOneEntityPayload', entity_id: string } }; + +export type FindEntity_RelationshipsQueryVariables = Exact<{ + filter?: InputMaybe; +}>; + + +export type FindEntity_RelationshipsQuery = { __typename?: 'Query', entity_relationships?: Array<{ __typename?: 'Entity_relationship', _id?: any | null, created_at?: any | null, pred?: string | null, is_symmetric?: boolean | null, sub?: { __typename?: 'Entity', entity_id: string, name: string } | null, obj?: { __typename?: 'Entity', entity_id: string, name: string } | null } | null> | null }; + +export type UpdateEntity_RelationshipMutationVariables = Exact<{ + filter: Entity_RelationshipFilterType; + update: Entity_RelationshipUpdateType; }>; -export type UpdateEntityMutation = { __typename?: 'Mutation', updateOneEntity?: { __typename?: 'Entity', entity_id: string } | null }; +export type UpdateEntity_RelationshipMutation = { __typename?: 'Mutation', updateOneEntity_relationship?: { __typename?: 'Entity_relationship', pred?: string | null, sub?: { __typename?: 'Entity', entity_id: string, name: string } | null, obj?: { __typename?: 'Entity', entity_id: string, name: string } | null } | null }; export type FindIncidentQueryVariables = Exact<{ filter?: InputMaybe; @@ -4778,7 +4935,9 @@ export const InsertDuplicateDocument = {"kind":"Document","definitions":[{"kind" export const UpsertEntityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertEntity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EntityFilterType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"update"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EntityInsertType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsertOneEntity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"update"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const FindEntitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindEntities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const FindEntityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindEntity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"EntityFilterType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"created_at"}},{"kind":"Field","name":{"kind":"Name","value":"date_modified"}}]}}]}}]} as unknown as DocumentNode; -export const UpdateEntityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEntity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EntityFilterType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"update"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EntityUpdateType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneEntity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"update"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateEntityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEntity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateOneEntityInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateEntityAndRelationships"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}}]}}]}}]} as unknown as DocumentNode; +export const FindEntity_RelationshipsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindEntity_relationships"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Entity_relationshipFilterType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_relationships"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"created_at"}},{"kind":"Field","name":{"kind":"Name","value":"pred"}},{"kind":"Field","name":{"kind":"Name","value":"sub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"obj"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"is_symmetric"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateEntity_RelationshipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEntity_relationship"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Entity_relationshipFilterType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"update"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Entity_relationshipUpdateType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneEntity_relationship"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"update"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pred"}},{"kind":"Field","name":{"kind":"Name","value":"sub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"obj"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindIncidentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindIncident"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"IncidentFilterType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident_id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"editors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeployerOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeveloperOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedHarmedOrNearlyHarmedParties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"implicated_systems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"nlp_similar_incidents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident_id"}},{"kind":"Field","name":{"kind":"Name","value":"similarity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"editor_similar_incidents"}},{"kind":"Field","name":{"kind":"Name","value":"editor_dissimilar_incidents"}},{"kind":"Field","name":{"kind":"Name","value":"flagged_dissimilar_incidents"}},{"kind":"Field","name":{"kind":"Name","value":"reports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"report_number"}}]}},{"kind":"Field","name":{"kind":"Name","value":"embedding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"from_reports"}},{"kind":"Field","name":{"kind":"Name","value":"vector"}}]}},{"kind":"Field","name":{"kind":"Name","value":"editor_notes"}}]}}]}}]} as unknown as DocumentNode; export const FindIncidentsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindIncidentsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"IncidentFilterType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incidents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident_id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"editors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeployerOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeveloperOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedHarmedOrNearlyHarmedParties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"implicated_systems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"report_number"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindIncidentEntitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindIncidentEntities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"IncidentFilterType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"incident_id"}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeployerOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedDeveloperOfAISystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"AllegedHarmedOrNearlyHarmedParties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"implicated_systems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entity_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/site/gatsby-site/server/local.ts b/site/gatsby-site/server/local.ts index 274be0d12c..a304355495 100644 --- a/site/gatsby-site/server/local.ts +++ b/site/gatsby-site/server/local.ts @@ -34,11 +34,17 @@ import { } from './fields/incidents'; import { - queryFields as submissionsQueryFields, - mutationFields as submissionsMutationFields, - permissions as submissionsPermissions + queryFields as submissionsQueryFields, + mutationFields as submissionsMutationFields, + permissions as submissionsPermissions } from './fields/submissions'; +import { + queryFields as entity_relationshipsQueryFields, + mutationFields as entity_relationshipsMutationFields, + permissions as entity_relationshipsPermissions +} from './fields/entity_relationships'; + import { queryFields as classificationsQueryFields, mutationFields as classificationsMutationFields, @@ -105,6 +111,7 @@ export const getSchema = () => { ...entitiesQueryFields, ...usersQueryFields, ...submissionsQueryFields, + ...entity_relationshipsQueryFields, ...classificationsQueryFields, ...taxaQueryFields, ...candidatesQueryFields, @@ -126,6 +133,7 @@ export const getSchema = () => { ...entitiesMutationFields, ...usersMutationFields, ...submissionsMutationFields, + ...entity_relationshipsMutationFields, ...classificationsMutationFields, ...candidatesMutationFields, ...subscriptionsMutationFields, @@ -163,6 +171,7 @@ export const getSchema = () => { ...entitiesPermissions.Query, ...usersPermissions.Query, ...submissionsPermissions.Query, + ...entity_relationshipsPermissions.Query, ...classificationsPermissions.Query, ...taxaPermissions.Query, ...candidatesPermissions.Query, @@ -181,6 +190,7 @@ export const getSchema = () => { ...entitiesPermissions.Mutation, ...usersPermissions.Mutation, ...submissionsPermissions.Mutation, + ...entity_relationshipsPermissions.Mutation, ...classificationsPermissions.Mutation, ...candidatesPermissions.Mutation, ...subscriptionsPermissions.Mutation, diff --git a/site/gatsby-site/server/schema.ts b/site/gatsby-site/server/schema.ts index d492a93c8a..e4e10300a8 100644 --- a/site/gatsby-site/server/schema.ts +++ b/site/gatsby-site/server/schema.ts @@ -15,6 +15,9 @@ const gatewaySchema = stitchSchemas({ ['Incident.editors']: { validationLevel: ValidationLevel.Off, }, + ['CandidateEmbedding.vector']: { + validationLevel: ValidationLevel.Off, + }, } } }); diff --git a/site/gatsby-site/server/tests/entities.spec.ts b/site/gatsby-site/server/tests/entities.spec.ts new file mode 100644 index 0000000000..201b179dac --- /dev/null +++ b/site/gatsby-site/server/tests/entities.spec.ts @@ -0,0 +1,87 @@ +import { expect, jest, it } from '@jest/globals'; +import { ApolloServer } from "@apollo/server"; +import { makeRequest, seedFixture, startTestServer } from "./utils"; +import * as context from '../context'; + +describe(`Entities`, () => { + let server: ApolloServer, url: string; + + beforeAll(async () => { + ({ server, url } = await startTestServer()); + }); + + afterAll(async () => { + await server?.stop(); + }); + + + it(`Update Entity and its relationships`, async () => { + + const mutationData = { + query: ` + mutation UpdateEntity($input: UpdateOneEntityInput!) { + updateEntityAndRelationships(input: $input) { + entity_id + name + } + } + `, + variables: { + + "input": { + "entity_id": "entity1", + "name": "Entity 1", + "entity_relationships_to_add": [{ id: "entity3", label: "Entity3" }], + "entity_relationships_to_remove": [{ id: "entity2", label: "Entity2" }] + } + } + }; + + + await seedFixture({ + customData: { + users: [ + { + userId: "123", + roles: [], + } + ], + }, + aiidprod: { + entities: [ + { + entity_id: "entity1", + name: "Entity 1", + + }, + { + entity_id: "entity2", + name: "Entity 2", + + } + ], + entity_relationships: [ + { + sub: "entity1", + obj: "entity2", + is_symmetric: true, + pred: "related" + } + ] + + } + }); + + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: "123" }) + + const response = await makeRequest(url, mutationData); + + expect(response.body.data).toMatchObject({ + updateEntityAndRelationships: { + entity_id: "entity1", + name: "Entity 1" + } + }) + }); +}); diff --git a/site/gatsby-site/server/tests/notifications.spec.ts b/site/gatsby-site/server/tests/notifications.spec.ts index caf9944a42..79bfcc96f9 100644 --- a/site/gatsby-site/server/tests/notifications.spec.ts +++ b/site/gatsby-site/server/tests/notifications.spec.ts @@ -1540,4 +1540,138 @@ describe(`Notifications`, () => { }, ]); }); + + it('Should not crash if no recipients found', async () => { + + const notifications: DBNotification[] = [ + { + processed: false, + type: 'new-incidents', + incident_id: 1, + }, + ] + + const subscriptions: DBSubscription[] = [ + { + type: 'new-incidents', + userId: '123', + }, + { + type: 'incident', + userId: '123', + incident_id: 1, + }, + { + type: 'submission-promoted', + userId: '123', + incident_id: 1, + } + ] + + const users: DBUser[] = [ + { + userId: "123", + roles: ['admin'], + } + ] + + const entities: DBEntity[] = [ + { + entity_id: 'entity-1', + name: 'Entity 1', + } + ] + + const incidents: DBIncident[] = [ + { + incident_id: 1, + title: 'Incident 1', + description: 'Incident 1 description', + "Alleged deployer of AI system": [], + "Alleged developer of AI system": [], + "Alleged harmed or nearly harmed parties": [], + date: new Date().toISOString(), + editors: [], + reports: [1], + implicated_systems: [], + } + ] + + const reports: DBReport[] = [ + { + report_number: 1, + title: 'Report 1', + description: 'Report 1 description', + authors: [], + cloudinary_id: 'cloudinary_id', + date_downloaded: new Date().toISOString(), + date_modified: new Date().toISOString(), + date_published: new Date().toISOString(), + date_submitted: new Date().toISOString(), + epoch_date_downloaded: 1, + epoch_date_modified: 1, + epoch_date_published: 1, + epoch_date_submitted: 1, + image_url: 'image_url', + language: 'en', + plain_text: 'plain_text', + source_domain: 'source_domain', + submitters: [], + tags: [], + text: 'text', + url: 'url', + user: 'user_id', + } + ] + + await seedFixture({ + customData: { + users, + notifications, + subscriptions, + }, + aiidprod: { + incidents, + entities, + reports, + } + }); + + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: "123" }) + + // No recipients + jest.spyOn(common, 'getUserAdminData').mockResolvedValue(null); + + const sendEmailMock = jest.spyOn(emails, 'sendEmail').mockImplementation(() => { + throw new Error('Failed to send email'); + }); + + + await processNotifications(); + + expect(sendEmailMock).not.toHaveBeenCalled(); + + const result = await makeRequest(url, { + query: ` + query { + notifications { + type + incident_id + processed + entity_id + } + } + `}); + + // notifications should be marked as processed + expect(result.body.data.notifications).toMatchObject([ + { + type: 'new-incidents', + incident_id: 1, + processed: true, + entity_id: null, + }, + ]); + }); }); diff --git a/site/gatsby-site/server/types/entity_relationship.ts b/site/gatsby-site/server/types/entity_relationship.ts new file mode 100644 index 0000000000..3368c6a0b6 --- /dev/null +++ b/site/gatsby-site/server/types/entity_relationship.ts @@ -0,0 +1,22 @@ +import { GraphQLBoolean, GraphQLObjectType, GraphQLString } from "graphql"; +import { ObjectIdScalar } from "../scalars"; +import { GraphQLDateTime } from "graphql-scalars"; +import { EntityType } from "./entity"; +import { getRelationshipConfig } from "../utils"; + +export const Entity_relationshipType = new GraphQLObjectType({ + name: 'Entity_relationship', + fields: { + _id: { type: ObjectIdScalar }, + pred: { type: GraphQLString }, + is_symmetric: { type: GraphQLBoolean }, + obj: getRelationshipConfig(EntityType, GraphQLString, 'obj', 'entity_id', 'entities', 'aiidprod'), + sub: getRelationshipConfig(EntityType, GraphQLString, 'sub', 'entity_id', 'entities', 'aiidprod'), + created_at: { type: GraphQLDateTime }, + }, +}); + +//@ts-ignore +Entity_relationshipType.getFields().obj.dependencies = ['obj'] +//@ts-ignore +Entity_relationshipType.getFields().sub.dependencies = ['sub'] \ No newline at end of file diff --git a/site/gatsby-site/server/types/incidentHistory.ts b/site/gatsby-site/server/types/incidentHistory.ts index 77ca3c2ff2..85fbefaf7f 100644 --- a/site/gatsby-site/server/types/incidentHistory.ts +++ b/site/gatsby-site/server/types/incidentHistory.ts @@ -1,6 +1,7 @@ import { GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; import { ObjectIdScalar } from "../scalars"; import { IncidentEmbeddingType, NlpSimilarIncidentType, TsneType } from "./types"; +import { GraphQLDateTime } from "graphql-scalars"; export const IncidentHistoryType = new GraphQLObjectType({ name: 'History_incident', @@ -9,6 +10,7 @@ export const IncidentHistoryType = new GraphQLObjectType({ AllegedDeployerOfAISystem: { type: new GraphQLList(GraphQLString), resolve: (source) => source['Alleged deployer of AI system'] }, AllegedDeveloperOfAISystem: { type: new GraphQLList(GraphQLString), resolve: (source) => source['Alleged developer of AI system'] }, AllegedHarmedOrNearlyHarmedParties: { type: new GraphQLList(GraphQLString), resolve: (source) => source['Alleged harmed or nearly harmed parties'] }, + created_at: { type: GraphQLDateTime }, implicated_systems: { type: new GraphQLList(GraphQLString) }, date: { type: new GraphQLNonNull(GraphQLString) }, description: { type: GraphQLString }, diff --git a/site/gatsby-site/server/types/incidents.ts b/site/gatsby-site/server/types/incidents.ts index f91bb0a708..f14ee7fd15 100644 --- a/site/gatsby-site/server/types/incidents.ts +++ b/site/gatsby-site/server/types/incidents.ts @@ -5,6 +5,7 @@ import { getListRelationshipConfig, getListRelationshipExtension, getListRelatio import { UserType } from "./user"; import { IncidentEmbeddingType, NlpSimilarIncidentType, TsneType } from "./types"; import { ReportType } from "./report"; +import { GraphQLDateTime } from "graphql-scalars"; export const IncidentType = new GraphQLObjectType({ name: 'Incident', @@ -49,6 +50,7 @@ export const IncidentType = new GraphQLObjectType({ nlp_similar_incidents: { type: new GraphQLList(NlpSimilarIncidentType) }, reports: getListRelationshipConfig(ReportType, GraphQLInt, 'reports', 'report_number', 'reports', 'aiidprod'), tsne: { type: TsneType }, + created_at: { type: GraphQLDateTime }, }, }); diff --git a/site/gatsby-site/server/types/notification.ts b/site/gatsby-site/server/types/notification.ts index cc6cf7ad28..2a7d489d39 100644 --- a/site/gatsby-site/server/types/notification.ts +++ b/site/gatsby-site/server/types/notification.ts @@ -16,5 +16,6 @@ export const NotificationType = new GraphQLObjectType({ sentDate: { type: GraphQLDateTime }, type: { type: GraphQLString }, userId: getRelationshipConfig(UserType, GraphQLString, 'userId', 'userId', 'users', 'customData'), + created_at: { type: GraphQLDateTime }, }), }); \ No newline at end of file diff --git a/site/gatsby-site/server/types/report.ts b/site/gatsby-site/server/types/report.ts index 05811bfc0b..2235473278 100644 --- a/site/gatsby-site/server/types/report.ts +++ b/site/gatsby-site/server/types/report.ts @@ -1,5 +1,5 @@ import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; -import { DateTimeResolver } from "graphql-scalars"; +import { DateTimeResolver, GraphQLDateTime } from "graphql-scalars"; import { ObjectIdScalar } from "../scalars"; import { getRelationshipConfig } from "../utils"; import { Context } from "../interfaces"; @@ -20,6 +20,7 @@ export const ReportType = new GraphQLObjectType({ _id: { type: ObjectIdScalar }, authors: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, cloudinary_id: { type: new GraphQLNonNull(GraphQLString) }, + created_at: { type: GraphQLDateTime }, date_downloaded: { type: new GraphQLNonNull(DateTimeResolver) }, date_modified: { type: new GraphQLNonNull(DateTimeResolver) }, date_published: { type: new GraphQLNonNull(DateTimeResolver) }, diff --git a/site/gatsby-site/server/types/reportHistory.ts b/site/gatsby-site/server/types/reportHistory.ts index 14f8ccbd1d..e3899ebfde 100644 --- a/site/gatsby-site/server/types/reportHistory.ts +++ b/site/gatsby-site/server/types/reportHistory.ts @@ -1,5 +1,5 @@ import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; -import { DateTimeResolver } from "graphql-scalars"; +import { DateTimeResolver, GraphQLDateTime } from "graphql-scalars"; import { ObjectIdScalar } from "../scalars"; import { EmbeddingType } from "./types"; @@ -9,6 +9,7 @@ export const ReportHistoryType = new GraphQLObjectType({ _id: { type: ObjectIdScalar }, authors: { type: new GraphQLList(GraphQLString) }, cloudinary_id: { type: GraphQLString }, + created_at: { type: GraphQLDateTime }, date_downloaded: { type: new GraphQLNonNull(DateTimeResolver) }, date_modified: { type: new GraphQLNonNull(DateTimeResolver) }, date_published: { type: new GraphQLNonNull(DateTimeResolver) }, diff --git a/site/gatsby-site/server/utils.ts b/site/gatsby-site/server/utils.ts index 5094df8a5c..af80cd633c 100644 --- a/site/gatsby-site/server/utils.ts +++ b/site/gatsby-site/server/utils.ts @@ -675,7 +675,7 @@ export function generateMutationFields({ const db = context.client.db(databaseName); const collection = db.collection(collectionName); - const result = await collection.insertOne(insert); + const result = await collection.insertOne({ ...insert, created_at: new Date() }); const inserted = await collection.findOne({ _id: result.insertedId }); @@ -822,7 +822,7 @@ export function generateMutationFields({ let update: any = await parseRelationshipFields(Type, args.update, mongoUpdate, context); update = await parseDBMappings(Type, update); - await collection.updateOne(filter, { $set: update }, { ...projection, upsert: true }); + await collection.updateOne(filter, { $set: update, $setOnInsert: { created_at: new Date() } }, { ...projection, upsert: true }); const updated = await collection.findOne(filter); diff --git a/site/gatsby-site/src/components/RelatedIncidents.js b/site/gatsby-site/src/components/RelatedIncidents.js index 86b7100729..65dd4b61ad 100644 --- a/site/gatsby-site/src/components/RelatedIncidents.js +++ b/site/gatsby-site/src/components/RelatedIncidents.js @@ -166,6 +166,7 @@ const RelatedIncidents = ({ incident, setFieldValue = null, columns = Object.keys(allSearchColumns), + triggerSearch = null, }) => { const searchColumns = {}; @@ -249,6 +250,8 @@ const RelatedIncidents = ({ {Object.keys(searchColumns).map((key) => { const column = searchColumns[key]; + if (!triggerSearch) return null; + return ( rows.filter((row) => { return row.values[field].some((incident) => ['incident_id', 'title'].some((field) => - incident[field].toString().toLowerCase().includes(value) + incident[field].toString().toLowerCase().includes(value.toLowerCase()) ) ); }); @@ -265,6 +265,13 @@ export default function EntitiesTable({ data, className = '', ...props }) { filter: entitiesFilter, sortType: sortByCount, }, + { + title: t('Entity Relationships'), + accessor: 'entityRelationships', + Cell: EntitiesCell, + filter: entitiesFilter, + sortType: sortByCount, + }, { title: t('Incident Responses'), accessor: 'responses', diff --git a/site/gatsby-site/src/components/entities/EntityCard.js b/site/gatsby-site/src/components/entities/EntityCard.js index 523cd95d28..9418676afd 100644 --- a/site/gatsby-site/src/components/entities/EntityCard.js +++ b/site/gatsby-site/src/components/entities/EntityCard.js @@ -45,7 +45,7 @@ export default function EntityCard({ entity, ...props }) {
{sections - .filter((section) => entity[section.key].length) + .filter((section) => entity[section.key]?.length) .map((section) => { const [open, setOpen] = useState(false); diff --git a/site/gatsby-site/src/components/forms/Label.js b/site/gatsby-site/src/components/forms/Label.js index 1db90ea6ab..568029ac05 100644 --- a/site/gatsby-site/src/components/forms/Label.js +++ b/site/gatsby-site/src/components/forms/Label.js @@ -6,14 +6,16 @@ import { Trans, useTranslation } from 'react-i18next'; import Link from 'components/ui/Link'; import { Tooltip } from 'flowbite-react'; -const Label = ({ popover, label = '', required = false, showPopover = true }) => { +const Label = ({ popover, label = '', required = false, showPopover = true, className = '' }) => { const [show, setShow] = useState(false); const { i18n } = useTranslation(['popovers']); if (!i18n.exists(popover, { ns: 'popovers' })) { return ( -
- {isInvalid ? errors[name] : null} + {isInvalid && Array.isArray(errors[name]) ? ( + [...new Set(errors[name])].map((error, index) => ( + + {error} + + )) + ) : ( + {isInvalid ? errors[name] : null} + )}
diff --git a/site/gatsby-site/src/components/submissions/SubmissionForm.js b/site/gatsby-site/src/components/submissions/SubmissionForm.js index b634a0b1d6..97e50e2af5 100644 --- a/site/gatsby-site/src/components/submissions/SubmissionForm.js +++ b/site/gatsby-site/src/components/submissions/SubmissionForm.js @@ -199,7 +199,12 @@ const SubmissionForm = ({ onChange = null }) => { btnDisabled={!!errors.url || !touched.url || parsingNews} btnText={t('Fetch info')} /> - + { {...TextInputGroupProps} /> - + { incident={values} setFieldValue={setFieldValue} columns={['byDatePublished']} + triggerSearch={values['date_published']?.length} /> { incident={values} setFieldValue={setFieldValue} columns={['byIncidentId']} + triggerSearch={values['incident_ids']?.length} />
diff --git a/site/gatsby-site/src/graphql/entities.js b/site/gatsby-site/src/graphql/entities.js index e4402279cf..53de39601b 100644 --- a/site/gatsby-site/src/graphql/entities.js +++ b/site/gatsby-site/src/graphql/entities.js @@ -30,8 +30,8 @@ export const FIND_ENTITY = gql(` `); export const UPDATE_ENTITY = gql(` - mutation UpdateEntity($filter: EntityFilterType!, $update: EntityUpdateType!) { - updateOneEntity(filter: $filter, update: $update) { + mutation UpdateEntity($input: UpdateOneEntityInput!) { + updateEntityAndRelationships(input: $input) { entity_id } } diff --git a/site/gatsby-site/src/graphql/entity_relationships.js b/site/gatsby-site/src/graphql/entity_relationships.js new file mode 100644 index 0000000000..93e90e97fe --- /dev/null +++ b/site/gatsby-site/src/graphql/entity_relationships.js @@ -0,0 +1,39 @@ +import { gql } from '../../server/generated'; + +export const FIND_ENTITY_RELATIONSHIPS = gql` + query FindEntity_relationships($filter: Entity_relationshipFilterType) { + entity_relationships(filter: $filter) { + _id + created_at + pred + sub { + entity_id + name + } + obj { + entity_id + name + } + is_symmetric + } + } +`; + +export const UPDATE_ENTITY_RELATIONSHIP = gql` + mutation UpdateEntity_relationship( + $filter: Entity_relationshipFilterType! + $update: Entity_relationshipUpdateType! + ) { + updateOneEntity_relationship(filter: $filter, update: $update) { + pred + sub { + entity_id + name + } + obj { + entity_id + name + } + } + } +`; diff --git a/site/gatsby-site/src/pages/entities/edit.js b/site/gatsby-site/src/pages/entities/edit.js index 231b11e90b..8245c4da60 100644 --- a/site/gatsby-site/src/pages/entities/edit.js +++ b/site/gatsby-site/src/pages/entities/edit.js @@ -3,7 +3,7 @@ import TextInputGroup from '../../components/forms/TextInputGroup'; import { StringParam, useQueryParam, withDefault } from 'use-query-params'; import useToastContext, { SEVERITY } from '../../hooks/useToast'; import { Button, Spinner } from 'flowbite-react'; -import { FIND_ENTITY, UPDATE_ENTITY } from '../../graphql/entities'; +import { FIND_ENTITIES, FIND_ENTITY, UPDATE_ENTITY } from '../../graphql/entities'; import { useMutation, useQuery } from '@apollo/client/react/hooks'; import { Form, Formik } from 'formik'; import { useTranslation, Trans } from 'react-i18next'; @@ -11,6 +11,9 @@ import { Link } from 'gatsby'; import DefaultSkeleton from 'elements/Skeletons/Default'; import * as Yup from 'yup'; import { format } from 'date-fns'; +import { Typeahead } from 'react-bootstrap-typeahead'; +import Label from '../../components/forms/Label'; +import { FIND_ENTITY_RELATIONSHIPS } from '../../graphql/entity_relationships'; const schema = Yup.object().shape({ name: Yup.string().required(), @@ -21,8 +24,16 @@ function EditEntityPage(props) { const [entity, setEntity] = useState(null); + const [entities, setEntities] = useState([]); + const [entityId] = useQueryParam('entity_id', withDefault(StringParam, '')); + const [entityRelationships, setEntityRelationships] = useState([]); + + const [updatedEntityRelationships, setUpdatedEntityRelationships] = useState([]); + + const { data: entitiesData, loading: loadingEntities } = useQuery(FIND_ENTITIES); + const { data: entityData, loading: loadingEntity, @@ -35,6 +46,56 @@ function EditEntityPage(props) { const [updateEntityMutation] = useMutation(UPDATE_ENTITY); + const { + data: entityRelationshipsData, + refetch: refetchEntityRelationships, + loading: loadingEntityRelationships, + } = useQuery(FIND_ENTITY_RELATIONSHIPS, { + variables: { + filter: { + OR: [{ sub: { EQ: entityId } }, { obj: { EQ: entityId }, is_symmetric: { EQ: true } }], + }, + }, + }); + + useEffect(() => { + if (!loadingEntities && entitiesData?.entities) { + let entitiesOptions = entitiesData.entities.map((entity) => { + return { + id: entity.entity_id, + label: entity.name, + }; + }); + + let options = []; + + if (!loadingEntityRelationships && entityRelationshipsData?.entity_relationships) { + options = entityRelationshipsData.entity_relationships.map((entityRelationship) => { + if (entityRelationship.sub.entity_id === entityId) { + return { + id: entityRelationship.obj.entity_id, + label: entitiesData.entities.find( + (entity) => entity.entity_id === entityRelationship.obj.entity_id + ).name, + }; + } else { + return { + id: entityRelationship.sub.entity_id, + label: entitiesData.entities.find( + (entity) => entity.entity_id === entityRelationship.sub.entity_id + ).name, + }; + } + }); + } + setEntityRelationships(options); + setUpdatedEntityRelationships(options); + + entitiesOptions = entitiesOptions.filter((entity) => entity.id !== entityId); + setEntities(entitiesOptions); + } + }, [loadingEntities, entitiesData, entityRelationshipsData, loadingEntityRelationships]); + const addToast = useToastContext(); useEffect(() => { @@ -47,21 +108,30 @@ function EditEntityPage(props) { const handleSubmit = async (values) => { try { + // Process the updated relationships and perform necessary updates in the database + const relationshipsToAdd = updatedEntityRelationships.filter( + (rel) => !entityRelationships.some((er) => er.id === rel.id) + ); + + const relationshipsToRemove = entityRelationships.filter( + (er) => !updatedEntityRelationships.some((rel) => rel.id === er.id) + ); + await updateEntityMutation({ variables: { - filter: { - entity_id: { EQ: entityId }, - }, - update: { - set: { - name: values.name, - date_modified: new Date().toISOString(), - }, + input: { + entity_id: entityId, + created_at: values.created_at, + name: values.name, + date_modified: new Date().toISOString(), + entity_relationships_to_add: relationshipsToAdd, + entity_relationships_to_remove: relationshipsToRemove, }, }, }); refetch(); + refetchEntityRelationships(); addToast({ message: t('Entity updated successfully.'), @@ -153,6 +223,31 @@ function EditEntityPage(props) { } disabled={true} /> + +