diff --git a/.github/workflows/test-api.yml b/.github/workflows/test-api.yml index d9154c07f0..3ebcc65620 100644 --- a/.github/workflows/test-api.yml +++ b/.github/workflows/test-api.yml @@ -58,15 +58,15 @@ jobs: - name: Jest run run: npm run test:api:ci env: - REALM_APP_ID: ${{ vars.GATSBY_REALM_APP_ID }} - REALM_API_APP_ID: ${{ vars.REALM_API_APP_ID }} - REALM_API_GROUP_ID: ${{ vars.REALM_API_GROUP_ID }} - REALM_API_PUBLIC_KEY: ${{ secrets.REALM_API_PUBLIC_KEY }} - REALM_API_PRIVATE_KEY: ${{ secrets.REALM_API_PRIVATE_KEY }} - REALM_GRAPHQL_API_KEY: ${{ secrets.REALM_GRAPHQL_API_KEY }} + REALM_APP_ID: "" + REALM_API_APP_ID: "" + REALM_API_GROUP_ID: "" + REALM_API_PUBLIC_KEY: "" + REALM_API_PRIVATE_KEY: "" + REALM_GRAPHQL_API_KEY: "" API_MONGODB_CONNECTION_STRING: "" # dinamically set by globalSetup.ts - E2E_ADMIN_USERNAME: ${{ secrets.E2E_ADMIN_USERNAME }} - E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} + E2E_ADMIN_USERNAME: "" + E2E_ADMIN_PASSWORD: "" ROLLBAR_POST_SERVER_ITEM_ACCESS_TOKEN: ${{ secrets.GATSBY_ROLLBAR_TOKEN }} - name: Upload coverage reports to Codecov diff --git a/site/gatsby-site/codegen.ts b/site/gatsby-site/codegen.ts index dd4d9b9cf3..4b5388eed0 100644 --- a/site/gatsby-site/codegen.ts +++ b/site/gatsby-site/codegen.ts @@ -4,7 +4,7 @@ import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { overwrite: true, schema: ["./server/schema.ts"], - require: ['ts-node/register'], + require: ['ts-node/register', 'dotenv/config'], generates: { "server/generated/graphql.ts": { plugins: ["typescript", "typescript-resolvers", "typescript-mongodb"] diff --git a/site/gatsby-site/cypress/e2e/integration/classificationsEditor.cy.js b/site/gatsby-site/cypress/e2e/integration/classificationsEditor.cy.js deleted file mode 100644 index 82be551a20..0000000000 --- a/site/gatsby-site/cypress/e2e/integration/classificationsEditor.cy.js +++ /dev/null @@ -1,449 +0,0 @@ -import { maybeIt } from '../../support/utils'; -import classificationsMock from '../../fixtures/classifications/editor.json'; -import classificationsUpsertMock from '../../fixtures/classifications/editorUpsert.json'; -import editorCSETV1Mock from '../../fixtures/classifications/editorCSETV1.json'; -import { gql } from '@apollo/client'; - -describe('Classifications Editor', () => { - before('before', function () { - // Skip all tests if the environment is empty since /cite/{incident_id} and /reports/{reportNumber} pages are not available - Cypress.env('isEmptyEnvironment') && this.skip(); - }); - - const incidentId = 2; - - const reportNumber = 2658; - - const incidentURL = `/cite/${incidentId}`; - - const reportURL = `/reports/${reportNumber}`; - - function editAndSubmitForm() { - cy.get('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"]').within(() => { - cy.contains('Edit').click(); - - cy.get('[data-cy="Notes"] textarea').scrollIntoView(); - - cy.get('[data-cy="Notes"] textarea').clear().type('Test notes'); - - cy.get('[data-cy="Full Description"] input').scrollIntoView(); - - cy.get('[data-cy="Full Description"] input').clear().type('Test description'); - - cy.contains('Submit').click(); - - cy.contains('Submit').should('be.disabled'); - }); - } - - function setField({ short_name, display_type, permitted_values }) { - let value = permitted_values?.length > 0 ? permitted_values[0] : 'Test'; - - cy.get(`[data-cy="${short_name}"]`).first().scrollIntoView(); - - return cy - .get(`[data-cy="${short_name}"]`) - .first() - .within(() => { - switch (display_type) { - case 'enum': - permitted_values.length <= 5 - ? cy.get(`[value="${value}"]`).check() - : cy.get(`select`).select(value); - - break; - - case 'bool': - cy.get(`[id="${short_name}-yes"]`).click(); - value = true; - - break; - - case 'string': - cy.get(`input`).type(value); - - break; - - case 'date': - cy.get(`input`).type('2023-01-01'); - value = '2023-01-01'; - - break; - - case 'location': - cy.get(`input`).type(`${value}{enter}`); - - break; - - case 'list': - cy.get(`input[type="text"]`).type(`${value}{enter}`); - value = [value]; - - break; - - case 'multi': - cy.get(`input[value="${value}"]`).click(); - value = [value]; - - break; - - case 'long_string': - cy.get(`textarea`).type(value); - - break; - - case 'int': - cy.get(`input`).type('1'); - value = 1; - - break; - - //TODO: skip data type for now - case 'object-list': - break; - - default: - throw new Error(`Unknown display type: ${display_type} for ${short_name}`); - } - }) - .then(() => { - return cy.wrap(value); - }); - } - - it(`Shouldn't show the classifications editor for unauthenticated users`, () => { - cy.visit(incidentURL); - - cy.get('[data-cy="classifications-editor"]').should('not.exist'); - }); - - maybeIt( - 'Should show classifications editor on incident page and save edited values', - { scrollBehavior: 'center' }, - () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindClassifications', - 'FindClassifications', - classificationsMock - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpsertClassification', - 'UpsertClassification', - classificationsUpsertMock - ); - - cy.visit(incidentURL); - - cy.waitForStableDOM(); - - cy.get('[data-cy="classifications-editor"]').should('be.visible'); - - cy.get('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"]').should('be.visible'); - - editAndSubmitForm(); - - cy.wait('@UpsertClassification').then((xhr) => { - expect(xhr.request.body.variables.query.reports).to.be.undefined; - expect(xhr.request.body.variables.query.incidents).deep.eq({ incident_id: incidentId }); - expect(xhr.request.body.variables.query.namespace).eq('CSETv0'); - - expect(xhr.request.body.variables.data.incidents).to.deep.eq({ link: [incidentId] }); - expect(xhr.request.body.variables.data.reports).to.deep.eq({ link: [reportNumber, 2659] }); - expect(xhr.request.body.variables.data.notes).to.eq('Test notes'); - expect( - xhr.request.body.variables.data.attributes.find((a) => a.short_name == 'Full Description') - .value_json - ).to.eq('"Test description"'); - }); - } - ); - - maybeIt( - 'Should show classifications editor on report page and save edited values', - { scrollBehavior: 'center' }, - () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindClassifications', - 'FindClassifications', - classificationsMock - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpsertClassification', - 'UpsertClassification', - classificationsUpsertMock - ); - - cy.visit(reportURL); - - cy.waitForStableDOM(); - - cy.get('[data-cy="classifications-editor"]').should('be.visible'); - - cy.get('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"]').should('be.visible'); - - editAndSubmitForm(); - - cy.wait('@UpsertClassification').then((xhr) => { - expect(xhr.request.body.variables.query.incidents).to.be.undefined; - expect(xhr.request.body.variables.query.reports).deep.eq({ report_number: reportNumber }); - expect(xhr.request.body.variables.query.namespace).eq('CSETv0'); - - expect(xhr.request.body.variables.data.incidents).to.deep.eq({ link: [] }); - expect(xhr.request.body.variables.data.reports).to.deep.eq({ link: [reportNumber, 2659] }); - expect(xhr.request.body.variables.data.notes).to.eq('Test notes'); - expect( - xhr.request.body.variables.data.attributes.find((a) => a.short_name == 'Full Description') - .value_json - ).to.eq('"Test description"'); - }); - } - ); - - maybeIt( - 'Should show classifications editor on report page and add a new classification', - { scrollBehavior: 'center' }, - () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindClassifications', - 'FindClassifications', - { - data: { - classifications: [], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpsertClassification', - 'UpsertClassification', - classificationsUpsertMock - ); - - cy.visit(reportURL); - - cy.waitForStableDOM(); - - cy.get('[data-cy="classifications-editor"]').should('be.visible'); - - cy.contains('Select a taxonomy').click(); - - cy.contains('GMF').click(); - - cy.contains('Add').click(); - - cy.get('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"]').should('be.visible'); - - cy.get('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"]').within(() => { - cy.contains('Edit').click(); - - cy.get('[data-cy="Notes"] textarea').scrollIntoView(); - - cy.get('[data-cy="Notes"] textarea').clear().type('Test notes'); - - cy.contains('Submit').click(); - }); - - cy.wait('@UpsertClassification').then((xhr) => { - expect(xhr.request.body.variables.query.incidents).to.be.undefined; - expect(xhr.request.body.variables.query.reports).deep.eq({ report_number: reportNumber }); - expect(xhr.request.body.variables.query.namespace).eq('GMF'); - - expect(xhr.request.body.variables.data.incidents).to.deep.eq({ link: [] }); - expect(xhr.request.body.variables.data.reports).to.deep.eq({ link: [reportNumber] }); - expect(xhr.request.body.variables.data.notes).to.eq('Test notes'); - }); - } - ); - - [ - { - namespace: 'CSETv0', - }, - { - namespace: 'GMF', - }, - { - namespace: 'CSETv1', - }, - ].forEach(({ namespace }) => { - maybeIt( - `Should properly display and store ${namespace} classification values`, - { scrollBehavior: 'center' }, - () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindClassifications', - 'FindClassifications', - { - data: { - classifications: [], - }, - } - ); - - cy.visit(incidentURL); - - cy.waitForStableDOM(); - - cy.query({ - query: gql` - query TaxasQuery($namespace: String) { - taxas(query: { namespace: $namespace }) { - namespace - field_list { - permitted_values - display_type - short_name - mongo_type - } - } - } - `, - variables: { namespace }, - }).then(({ data: { taxas } }) => { - for (const taxa of taxas) { - const { namespace } = taxa; - - cy.contains('Select a taxonomy').scrollIntoView(); - - cy.contains('Select a taxonomy').click(); - - cy.get('[data-testid="flowbite-tooltip"]') - .contains(new RegExp(`^${namespace}$`)) - .click(); - - cy.contains('Add').click(); - - cy.waitForStableDOM(); - - cy.conditionalIntercept( - '**/graphql', - (req) => - req.body.operationName == 'UpsertClassification' && - req.body.variables.query.namespace == namespace, - `Upsert-${namespace}`, - classificationsUpsertMock - ); - - cy.get(`[data-cy="classifications-editor"] [data-cy="taxonomy-${namespace}"]`).within( - () => { - cy.contains('Edit').click(); - - const selectedValues = {}; - - for (const field of taxa.field_list) { - setField({ - short_name: field.short_name, - display_type: field.display_type, - permitted_values: field.permitted_values, - }).then((value) => { - selectedValues[field.short_name] = value; - }); - } - - cy.contains('Submit').click(); - - cy.wait(`@Upsert-${namespace}`, { timeout: 20000 }).then((xhr) => { - expect(xhr.request.body.variables.query.namespace).eq(namespace); - - for (const field of taxa.field_list) { - expect( - xhr.request.body.variables.data.attributes.filter( - (a) => a.short_name == field.short_name - ) - ).to.have.lengthOf(1); - - const skippedFields = [ - 'Known AI Technology Snippets', - 'Known AI Technical Failure Snippets', - 'Entities', - 'Known AI Goal Snippets', - 'Potential AI Goal Snippets', - 'Potential AI Technology Snippets', - 'Potential AI Technical Failure Snippets', - ]; - - if (!skippedFields.includes(field.short_name)) { - expect( - xhr.request.body.variables.data.attributes.find( - (a) => a.short_name == field.short_name - ) - ).to.deep.eq({ - short_name: field.short_name, - value_json: JSON.stringify(selectedValues[field.short_name]), - }); - } - } - }); - - cy.waitForStableDOM(); - } - ); - } - }); - } - ); - }); - - maybeIt('Should synchronize duplicate fields', { scrollBehavior: 'center' }, () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindClassifications', - 'FindClassifications', - editorCSETV1Mock - ); - - cy.visit(incidentURL); - - cy.waitForStableDOM(); - - cy.get('[data-cy="taxonomy-CSETv1"]').first().scrollIntoView(); - - cy.get('[data-cy="taxonomy-CSETv1"]').contains('Edit').click(); - - cy.waitForStableDOM(); - - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"]').first().scrollIntoView(); - - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"]').first().contains('yes').click(); - - cy.waitForStableDOM(); - - cy.waitForStableDOM(); - - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').first().check(); - - cy.waitForStableDOM(); - - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]') - .last() - .should('be.checked'); - - // Clicking unchecks the input for both fields - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').last().click(); - - cy.waitForStableDOM(); - - cy.get('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]') - .first() - .should('not.be.checked'); - }); -}); diff --git a/site/gatsby-site/globalSetup.ts b/site/gatsby-site/globalSetup.ts index 47f552fcad..5429d89457 100644 --- a/site/gatsby-site/globalSetup.ts +++ b/site/gatsby-site/globalSetup.ts @@ -1,11 +1,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'; export = async function globalSetup() { - + const instance = await MongoMemoryServer.create(); const uri = instance.getUri(); (global as any).__MONGOINSTANCE = instance; + console.log(`\n mock API_MONGODB_CONNECTION_STRING ${uri} \n\n`, ); + process.env.API_MONGODB_CONNECTION_STRING = uri.slice(0, uri.lastIndexOf('/')); }; diff --git a/site/gatsby-site/jest.config.ts b/site/gatsby-site/jest.config.ts index d85394e855..52e3a6db7e 100644 --- a/site/gatsby-site/jest.config.ts +++ b/site/gatsby-site/jest.config.ts @@ -18,6 +18,7 @@ const config: Config = { globalSetup: "./globalSetup.ts", globalTeardown: "./globalTeardown.ts", verbose: true, + setupFiles: ["dotenv/config"], testMatch: [ "**/server/tests/**/*.spec.ts", ], diff --git a/site/gatsby-site/migrations/2024.07.09T16.52.19.cset-annotator-1-2-3-publish-items.js b/site/gatsby-site/migrations/2024.07.09T16.52.19.cset-annotator-1-2-3-publish-items.js new file mode 100644 index 0000000000..750eefa507 --- /dev/null +++ b/site/gatsby-site/migrations/2024.07.09T16.52.19.cset-annotator-1-2-3-publish-items.js @@ -0,0 +1,53 @@ +/** @type {import('umzug').MigrationFn} */ +const config = require('../config'); + +/** @type {import('umzug').MigrationFn} */ +exports.up = async ({ context: { client } }) => { + const annotationStatusesToPublish = [ + '"2. Initial annotation complete"', + '"3. In peer review"', + '"4. Peer review complete"', + '"5. In quality control"', + '"6. Complete and final"', + ]; + + const classifications = client + .db(config.realm.production_db.db_name) + .collection('classifications'); + + const namespaces = ['CSETv1_Annotator-1', 'CSETv1_Annotator-2', 'CSETv1_Annotator-3']; + + for (const namespace of namespaces) { + const csetV1Classifications = await classifications.find({ + namespace: namespace, + publish: false, + }); + + let counter = 0; + + while (await csetV1Classifications.hasNext()) { + const csetV1Classification = await csetV1Classifications.next(); + + const attributes = csetV1Classification.attributes; + + const annotationStatusAttribute = attributes.find( + (attr) => + attr.short_name == 'Annotation Status' && + annotationStatusesToPublish.includes(attr.value_json) + ); + + if (annotationStatusAttribute) { + await classifications.updateOne( + { _id: csetV1Classification._id }, + { $set: { publish: true } } + ); + counter++; + } + } + + console.log(`"${namespace}" Annotations published:`, counter); + } +}; + +/** @type {import('umzug').MigrationFn} */ +exports.down = async () => {}; diff --git a/site/gatsby-site/package.json b/site/gatsby-site/package.json index c547d8387c..d9aeda4722 100644 --- a/site/gatsby-site/package.json +++ b/site/gatsby-site/package.json @@ -128,8 +128,8 @@ "cypress:run": "cypress run", "test:e2e": "start-server-and-test start http://localhost:8000 cypress:open", "test:e2e:ci": "start-server-and-test start http://localhost:8000 cypress:run", - "test:api": "jest --setupFiles dotenv/config --forceExit", - "test:api:ci": "jest --forceExit", + "test:api": "jest --setupFiles dotenv/config --runInBand --forceExit", + "test:api:ci": "jest --runInBand --forceExit", "codegen": "graphql-codegen --config codegen.ts", "start:api": "node --env-file=.env --watch -r ts-node/register server/index.ts" }, diff --git a/site/gatsby-site/playwright/e2e/integration/classificationsEditor.spec.ts b/site/gatsby-site/playwright/e2e/integration/classificationsEditor.spec.ts new file mode 100644 index 0000000000..8d271307cf --- /dev/null +++ b/site/gatsby-site/playwright/e2e/integration/classificationsEditor.spec.ts @@ -0,0 +1,308 @@ +import { test, conditionalIntercept, waitForRequest, setEditorText, query } from '../../utils'; +import classificationsMock from '../../fixtures/classifications/editor.json'; +import classificationsUpsertMock from '../../fixtures/classifications/editorUpsert.json'; +import editorCSETV1Mock from '../../fixtures/classifications/editorCSETV1.json'; +import { gql } from '@apollo/client'; +import { expect } from '@playwright/test'; + +test.describe('Classifications Editor', () => { + + const incidentId = 2; + const reportNumber = 2658; + const incidentURL = `/cite/${incidentId}`; + const reportURL = `/reports/${reportNumber}`; + + async function editAndSubmitForm(page) { + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] >> text=Edit').click(); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] [data-cy="Notes"] textarea').scrollIntoViewIfNeeded(); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] [data-cy="Notes"] textarea').fill('Test notes'); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] [data-cy="Full Description"] input').scrollIntoViewIfNeeded(); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] [data-cy="Full Description"] input').fill('Test description'); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] >> text=Submit').click(); + await expect(page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"] >> text=Submit')).toBeDisabled(); + } + + async function setField(page, { short_name, display_type, permitted_values }) { + let value = permitted_values?.length > 0 ? permitted_values[0] : 'Test'; + await page.locator(`[data-cy="${short_name}"]`).first().scrollIntoViewIfNeeded(); + + const fieldLocator = page.locator(`[data-cy="${short_name}"]`).first(); + await fieldLocator.scrollIntoViewIfNeeded(); + + switch (display_type) { + case 'enum': + permitted_values.length <= 5 + ? await fieldLocator.locator(`[value="${value}"]`).check() + : await fieldLocator.locator('select').selectOption(value); + break; + case 'bool': + await fieldLocator.locator(`[id="${short_name}-yes"]`).click(); + value = true; + break; + case 'string': + await fieldLocator.locator('input').fill(value); + break; + case 'date': + await fieldLocator.locator('input').fill('2023-01-01'); + value = '2023-01-01'; + break; + case 'location': + await fieldLocator.locator('input').fill(`${value}`); + await page.keyboard.press('Enter'); + break; + case 'list': + await fieldLocator.locator('input[type="text"]').fill(`${value}`); + await page.keyboard.press('Enter'); + value = [value]; + break; + case 'multi': + await fieldLocator.locator(`input[value="${value}"]`).click(); + value = [value]; + break; + case 'long_string': + await fieldLocator.locator('textarea').fill(value); + break; + case 'int': + await fieldLocator.locator('input').fill('1'); + value = 1; + break; + case 'object-list': + break; + default: + throw new Error(`Unknown display type: ${display_type} for ${short_name}`); + } + + return value; + } + + test('Shouldn\'t show the classifications editor for unauthenticated users', async ({ page, skipOnEmptyEnvironment }) => { + await page.goto(incidentURL); + await expect(page.locator('[data-cy="classifications-editor"]')).not.toBeVisible(); + }); + + test('Should show classifications editor on incident page and save edited values', async ({ page, login, skipOnEmptyEnvironment }) => { + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindClassifications', + classificationsMock, + 'FindClassifications' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpsertClassification', + classificationsUpsertMock, + 'UpsertClassification' + ); + + await page.goto(incidentURL); + await waitForRequest('FindClassifications'); + await expect(page.locator('[data-cy="classifications-editor"]')).toBeVisible(); + await expect(page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"]')).toBeVisible(); + + await editAndSubmitForm(page); + + const upsertClassificationRequest = await waitForRequest('UpsertClassification'); + const variables = upsertClassificationRequest.postDataJSON().variables; + expect(variables.query.reports).toBeUndefined(); + expect(variables.query.incidents).toEqual({ incident_id: incidentId }); + expect(variables.query.namespace).toBe('CSETv0'); + expect(variables.data.incidents).toEqual({ link: [incidentId] }); + expect(variables.data.reports).toEqual({ link: [reportNumber, 2659] }); + expect(variables.data.notes).toBe('Test notes'); + expect(variables.data.attributes.find((a) => a.short_name == 'Full Description').value_json).toBe('"Test description"'); + }); + + test('Should show classifications editor on report page and save edited values', async ({ page, login, skipOnEmptyEnvironment }) => { + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindClassifications', + classificationsMock, + 'FindClassifications' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpsertClassification', + classificationsUpsertMock, + 'UpsertClassification' + ); + + await page.goto(reportURL); + await waitForRequest('FindClassifications'); + await expect(page.locator('[data-cy="classifications-editor"]')).toBeVisible(); + await expect(page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-CSETv0"]')).toBeVisible(); + + await editAndSubmitForm(page); + + const upsertClassificationRequest = await waitForRequest('UpsertClassification'); + const variables = upsertClassificationRequest.postDataJSON().variables; + expect(variables.query.incidents).toBeUndefined(); + expect(variables.query.reports).toEqual({ report_number: reportNumber }); + expect(variables.query.namespace).toBe('CSETv0'); + expect(variables.data.incidents).toEqual({ link: [] }); + expect(variables.data.reports).toEqual({ link: [reportNumber, 2659] }); + expect(variables.data.notes).toBe('Test notes'); + expect(variables.data.attributes.find((a) => a.short_name == 'Full Description').value_json).toBe('"Test description"'); + }); + + test('Should show classifications editor on report page and add a new classification', async ({ page, login, skipOnEmptyEnvironment }) => { + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindClassifications', + { data: { classifications: [] } }, + 'FindClassifications' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpsertClassification', + classificationsUpsertMock, + 'UpsertClassification' + ); + + await page.goto(reportURL); + await waitForRequest('FindClassifications'); + await expect(page.locator('[data-cy="classifications-editor"]')).toBeVisible(); + await page.locator('text=Select a taxonomy').click(); + await page.locator('text=GMF').click(); + await page.locator('text=Add').first().click(); + await expect(page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"]')).toBeVisible(); + + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"] [data-cy="Notes"] textarea').scrollIntoViewIfNeeded(); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"] [data-cy="Notes"] textarea').fill('Test notes'); + await page.locator('[data-cy="classifications-editor"] [data-cy="taxonomy-GMF"] >> text=Submit').click(); + + const upsertClassificationRequest = await waitForRequest('UpsertClassification'); + const variables = upsertClassificationRequest.postDataJSON().variables; + expect(variables.query.incidents).toBeUndefined(); + expect(variables.query.reports).toEqual({ report_number: reportNumber }); + expect(variables.query.namespace).toBe('GMF'); + expect(variables.data.incidents).toEqual({ link: [] }); + expect(variables.data.reports).toEqual({ link: [reportNumber] }); + expect(variables.data.notes).toBe('Test notes'); + }); + + const namespaces = ['CSETv0', 'GMF', 'CSETv1']; + for (const namespace of namespaces) { + test(`Should properly display and store ${namespace} classification values`, async ({ page, login, skipOnEmptyEnvironment }) => { + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindClassifications', + { data: { classifications: [] } }, + 'FindClassifications' + ); + + await page.goto(incidentURL); + await waitForRequest('FindClassifications'); + + const { data: { taxas } } = await query({ + query: gql` + query TaxasQuery($namespace: String) { + taxas(query: { namespace: $namespace }) { + namespace + field_list { + permitted_values + display_type + short_name + mongo_type + } + } + } + `, + variables: { namespace } + }); + + for (const taxa of taxas) { + await page.locator('text=Select a taxonomy').scrollIntoViewIfNeeded(); + await page.locator('text=Select a taxonomy').click(); + await page.locator(`[data-testid="flowbite-tooltip"] >> text=${namespace}`).first().click(); + await page.locator('text=Add').first().click(); + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpsertClassification' && req.postDataJSON()?.variables?.query.namespace == namespace, + classificationsUpsertMock, + `Upsert-${namespace}`, + ); + + // await page.locator(`[data-cy="classifications-editor"] [data-cy="taxonomy-${namespace}"] text=Edit`).click(); + const selectedValues = {}; + + for (const field of taxa.field_list) { + const value = await setField(page, { + short_name: field.short_name, + display_type: field.display_type, + permitted_values: field.permitted_values + }); + selectedValues[field.short_name] = value; + } + + await page.locator(`[data-cy="classifications-editor"] [data-cy="taxonomy-${namespace}"] >> text=Submit`).click(); + + const upsertRequest = await waitForRequest(`Upsert-${namespace}`); + const variables = upsertRequest.postDataJSON().variables; + expect(variables.query.namespace).toBe(namespace); + + for (const field of taxa.field_list) { + const skippedFields = [ + 'Known AI Technology Snippets', + 'Known AI Technical Failure Snippets', + 'Entities', + 'Known AI Goal Snippets', + 'Potential AI Goal Snippets', + 'Potential AI Technology Snippets', + 'Potential AI Technical Failure Snippets' + ]; + + if (!skippedFields.includes(field.short_name)) { + expect(variables.data.attributes.find((a) => a.short_name == field.short_name)).toEqual({ + short_name: field.short_name, + value_json: JSON.stringify(selectedValues[field.short_name]) + }); + } + } + + } + }); + } + + test('Should synchronize duplicate fields', async ({ page, login, skipOnEmptyEnvironment }) => { + await login(process.env.E2E_ADMIN_USERNAME, process.env.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindClassifications', + editorCSETV1Mock, + 'FindClassifications' + ); + + await page.goto(incidentURL); + await waitForRequest('FindClassifications'); + + await page.locator('[data-cy="taxonomy-CSETv1"]').first().scrollIntoViewIfNeeded(); + await page.locator('[data-cy="taxonomy-CSETv1"] >> text=Edit').click(); + await page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"]').first().scrollIntoViewIfNeeded(); + await page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] >> text=yes').first().click(); + await page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').first().check(); + await expect(page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').last()).toBeChecked(); + await page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').last().click(); + await expect(page.locator('[data-cy="taxonomy-CSETv1"] [data-cy="AI System"] [value="yes"]').first()).not.toBeChecked(); + }); +}); diff --git a/site/gatsby-site/playwright/e2e/login.spec.ts b/site/gatsby-site/playwright/e2e/login.spec.ts index c216994ecc..84288eebaf 100644 --- a/site/gatsby-site/playwright/e2e/login.spec.ts +++ b/site/gatsby-site/playwright/e2e/login.spec.ts @@ -37,7 +37,7 @@ test.describe('Login', () => { ); test('Should redirect to specific page after login if redirectTo is provided', - async ({ page, skipOnEmptyEnvironment }) => { + async ({ page, skipOnEmptyEnvironment, login }) => { const redirectTo = '/cite/10/'; await page.goto(`${url}?redirectTo=${redirectTo}`); @@ -75,7 +75,7 @@ test.describe('Login', () => { await expect(page).toHaveURL('/forgotpassword/', { timeout: 30000 }); }); - test('Should give the option to resend Email verification if the user is not confirmed', async ({ page }) => { + test('Should give the option to resend Email verification if the user is not confirmed', async ({ page, login }) => { await conditionalIntercept( page, '**/login', diff --git a/site/gatsby-site/cypress/fixtures/classifications/editor.json b/site/gatsby-site/playwright/fixtures/classifications/editor.json similarity index 100% rename from site/gatsby-site/cypress/fixtures/classifications/editor.json rename to site/gatsby-site/playwright/fixtures/classifications/editor.json diff --git a/site/gatsby-site/cypress/fixtures/classifications/editorCSETV1.json b/site/gatsby-site/playwright/fixtures/classifications/editorCSETV1.json similarity index 100% rename from site/gatsby-site/cypress/fixtures/classifications/editorCSETV1.json rename to site/gatsby-site/playwright/fixtures/classifications/editorCSETV1.json diff --git a/site/gatsby-site/cypress/fixtures/classifications/editorUpsert.json b/site/gatsby-site/playwright/fixtures/classifications/editorUpsert.json similarity index 100% rename from site/gatsby-site/cypress/fixtures/classifications/editorUpsert.json rename to site/gatsby-site/playwright/fixtures/classifications/editorUpsert.json diff --git a/site/gatsby-site/server/config.ts b/site/gatsby-site/server/config.ts index fd0994cb9c..18245daf0d 100644 --- a/site/gatsby-site/server/config.ts +++ b/site/gatsby-site/server/config.ts @@ -1,6 +1,4 @@ interface Config { - E2E_ADMIN_PASSWORD: string - E2E_ADMIN_USERNAME: string REALM_API_APP_ID: string REALM_API_GROUP_ID: string REALM_API_PRIVATE_KEY: string @@ -13,8 +11,6 @@ interface Config { }; const config: Config = { - E2E_ADMIN_PASSWORD: process.env.E2E_ADMIN_PASSWORD!, - E2E_ADMIN_USERNAME: process.env.E2E_ADMIN_USERNAME!, REALM_API_APP_ID: process.env.REALM_API_APP_ID!, REALM_API_GROUP_ID: process.env.REALM_API_GROUP_ID!, REALM_API_PRIVATE_KEY: process.env.REALM_API_PRIVATE_KEY!, diff --git a/site/gatsby-site/server/context.ts b/site/gatsby-site/server/context.ts index 2031a0583d..bd2e2806de 100644 --- a/site/gatsby-site/server/context.ts +++ b/site/gatsby-site/server/context.ts @@ -14,7 +14,7 @@ function extractToken(header: string) { return null; } -async function verifyToken(token: string) { +export const verifyToken = async (token: string) => { const loginResponse = await fetch( `https://realm.mongodb.com/api/admin/v3.0/auth/providers/mongodb-cloud/login`, diff --git a/site/gatsby-site/server/fields/quickadds.ts b/site/gatsby-site/server/fields/quickadds.ts index e38b2be64e..d1eb1e6b1b 100644 --- a/site/gatsby-site/server/fields/quickadds.ts +++ b/site/gatsby-site/server/fields/quickadds.ts @@ -1,15 +1,11 @@ -import { MongoClient } from "mongodb"; -import { QuickAdd } from "../generated/graphql"; -import { GraphQLFieldConfigMap, GraphQLList, GraphQLObjectType, GraphQLString } from "graphql"; -import { GraphQLLong } from "graphql-scalars"; -import { getGraphQLInsertType, getGraphQLQueryArgs, getMongoDbQueryResolver } from "graphql-to-mongodb"; +import { GraphQLFieldConfigMap, GraphQLInt, GraphQLObjectType, GraphQLString } from "graphql"; import { allow } from "graphql-shield"; import { isAdmin } from "../rules"; import { ObjectIdScalar } from "../scalars"; -import { DeleteManyPayload } from "../types"; +import { generateMutationFields, generateQueryFields } from "../utils"; const QuickAddType = new GraphQLObjectType({ - name: 'QuickAdd', + name: 'Quickadd', fields: { _id: { type: ObjectIdScalar, @@ -18,7 +14,7 @@ const QuickAddType = new GraphQLObjectType({ type: GraphQLString, }, incident_id: { - type: GraphQLLong, + type: GraphQLInt, }, source_domain: { type: GraphQLString, @@ -31,66 +27,25 @@ const QuickAddType = new GraphQLObjectType({ export const queryFields: GraphQLFieldConfigMap = { - quickadds: { - type: new GraphQLList(QuickAddType), - args: getGraphQLQueryArgs(QuickAddType), - resolve: getMongoDbQueryResolver(QuickAddType, async (filter, projection, options, obj, args, context) => { - - const db = (context.client as MongoClient).db('aiidprod') - const collection = db.collection('quickadd'); - - const items = await collection.find(filter, projection).toArray(); - - return items; - }), - } + ...generateQueryFields({ collectionName: 'quickadd', Type: QuickAddType }) } export const mutationFields: GraphQLFieldConfigMap = { - deleteManyQuickadds: { - type: DeleteManyPayload, - args: getGraphQLQueryArgs(QuickAddType), - resolve: getMongoDbQueryResolver( - QuickAddType, - async (filter, projection, options, obj, args, context) => { - - const db = (context.client as MongoClient).db('aiidprod'); - const collection = db.collection('quickadd'); - - const result = await collection.deleteMany(filter); - - return { deletedCount: result.deletedCount! }; - }, - { - differentOutputType: true, - } - ) - }, - - insertOneQuickadd: { - type: QuickAddType, - args: { data: { type: getGraphQLInsertType(QuickAddType) } }, - resolve: async (_: unknown, { data }: { data: QuickAdd }, { client }: { client: MongoClient }) => { - - const db = client.db('aiidprod'); - const collection = db.collection('quickadd'); - - const result = await collection.insertOne(data); - - const inserted = await collection.findOne({ _id: result.insertedId }); - - return inserted; - } - } + ...generateMutationFields({ collectionName: 'quickadd', Type: QuickAddType }), } export const permissions = { Query: { + quickadd: allow, quickadds: allow, }, Mutation: { + deleteOneQuickadd: isAdmin, deleteManyQuickadds: isAdmin, insertOneQuickadd: allow, + insertManyQuickadds: isAdmin, + updateOneQuickadd: isAdmin, + updateManyQuickadds: isAdmin, } } \ No newline at end of file diff --git a/site/gatsby-site/server/generated/graphql.ts b/site/gatsby-site/server/generated/graphql.ts index 61eb221cd1..235c1d9e1f 100644 --- a/site/gatsby-site/server/generated/graphql.ts +++ b/site/gatsby-site/server/generated/graphql.ts @@ -15,9 +15,7 @@ export type Scalars = { Int: { input: number; output: number; } Float: { input: number; output: number; } DateTime: { input: any; output: any; } - /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */ - Long: { input: bigint; output: bigint; } - /** Mongo object id scalar type */ + Long: { input: any; output: any; } ObjectId: { input: any; output: any; } }; @@ -740,11 +738,9 @@ export type Classification = { __typename?: 'Classification'; _id?: Maybe; attributes?: Maybe>>; - incidents: Array>; namespace: Scalars['String']['output']; notes?: Maybe; publish?: Maybe; - reports: Array>; }; export type ClassificationAttribute = { @@ -1867,28 +1863,6 @@ export type History_ReportUpdateInput = { user_unset?: InputMaybe; }; -export type Incident = { - __typename?: 'Incident'; - AllegedDeployerOfAISystem?: Maybe>>; - AllegedDeveloperOfAISystem?: Maybe>>; - AllegedHarmedOrNearlyHarmedParties?: Maybe>>; - _id?: Maybe; - date: Scalars['String']['output']; - description?: Maybe; - editor_dissimilar_incidents?: Maybe>>; - editor_notes?: Maybe; - editor_similar_incidents?: Maybe>>; - editors: Array>; - embedding?: Maybe; - epoch_date_modified?: Maybe; - flagged_dissimilar_incidents?: Maybe>>; - incident_id: Scalars['Int']['output']; - nlp_similar_incidents?: Maybe>>; - reports: Array>; - title: Scalars['String']['output']; - tsne?: Maybe; -}; - export type IncidentAllegedDeployerOfAiSystemRelationInput = { create?: InputMaybe>>; link?: InputMaybe>>; @@ -2223,79 +2197,77 @@ export type InsertManyPayload = { insertedIds: Array>; }; -export type LinkReportsToIncidentsInput = { - incident_ids?: InputMaybe>>; - report_numbers?: InputMaybe>>; -}; - -export type LogIncidentHistoryPayload = { - __typename?: 'LogIncidentHistoryPayload'; - incident_id?: Maybe; -}; - -export type LogReportHistoryPayload = { - __typename?: 'LogReportHistoryPayload'; - report_number?: Maybe; -}; - -/** Filter type for Long scalar */ -export type LongFilter = { +/** Filter type for Int scalar */ +export type IntFilter = { /** $all */ - ALL?: InputMaybe>>; + ALL?: InputMaybe>>; /** $eq */ - EQ?: InputMaybe; + EQ?: InputMaybe; /** $gt */ - GT?: InputMaybe; + GT?: InputMaybe; /** $gte */ - GTE?: InputMaybe; + GTE?: InputMaybe; /** $in */ - IN?: InputMaybe>>; + IN?: InputMaybe>>; /** $lt */ - LT?: InputMaybe; + LT?: InputMaybe; /** $lte */ - LTE?: InputMaybe; + LTE?: InputMaybe; /** $ne */ - NE?: InputMaybe; + NE?: InputMaybe; /** DEPRECATED: use NE */ - NEQ?: InputMaybe; + NEQ?: InputMaybe; /** $nin */ - NIN?: InputMaybe>>; + NIN?: InputMaybe>>; /** $not */ - NOT?: InputMaybe; + NOT?: InputMaybe; /** DEPRECATED: Switched to the more intuitive operator fields */ opr?: InputMaybe; /** DEPRECATED: Switched to the more intuitive operator fields */ - value?: InputMaybe; + value?: InputMaybe; /** DEPRECATED: Switched to the more intuitive operator fields */ - values?: InputMaybe>>; + values?: InputMaybe>>; }; -/** Filter type for $not of Long scalar */ -export type LongNotFilter = { +/** Filter type for $not of Int scalar */ +export type IntNotFilter = { /** $all */ - ALL?: InputMaybe>>; + ALL?: InputMaybe>>; /** $eq */ - EQ?: InputMaybe; + EQ?: InputMaybe; /** $gt */ - GT?: InputMaybe; + GT?: InputMaybe; /** $gte */ - GTE?: InputMaybe; + GTE?: InputMaybe; /** $in */ - IN?: InputMaybe>>; + IN?: InputMaybe>>; /** $lt */ - LT?: InputMaybe; + LT?: InputMaybe; /** $lte */ - LTE?: InputMaybe; + LTE?: InputMaybe; /** $ne */ - NE?: InputMaybe; + NE?: InputMaybe; /** $nin */ - NIN?: InputMaybe>>; + NIN?: InputMaybe>>; +}; + +export type LinkReportsToIncidentsInput = { + incident_ids?: InputMaybe>>; + report_numbers?: InputMaybe>>; +}; + +export type LogIncidentHistoryPayload = { + __typename?: 'LogIncidentHistoryPayload'; + incident_id?: Maybe; +}; + +export type LogReportHistoryPayload = { + __typename?: 'LogReportHistoryPayload'; + report_number?: Maybe; }; export type Mutation = { __typename?: 'Mutation'; - /** Placeholder field to avoid empty mutation type */ - _?: Maybe; createDefaultAdminUser?: Maybe; createVariant?: Maybe; deleteManyCandidates?: Maybe; @@ -2320,10 +2292,8 @@ export type Mutation = { deleteOneEntity?: Maybe; deleteOneHistory_incident?: Maybe; deleteOneHistory_report?: Maybe; - deleteOneIncident?: Maybe; deleteOneNotification?: Maybe; deleteOneQuickadd?: Maybe; - deleteOneReport?: Maybe; deleteOneSubmission?: Maybe; deleteOneSubscription?: Maybe; deleteOneTaxa?: Maybe; @@ -2351,15 +2321,12 @@ export type Mutation = { insertOneEntity?: Maybe; insertOneHistory_incident?: Maybe; insertOneHistory_report?: Maybe; - insertOneIncident?: Maybe; insertOneNotification?: Maybe; - insertOneQuickadd?: Maybe; - insertOneReport?: Maybe; + insertOneQuickadd?: Maybe; insertOneSubmission?: Maybe; insertOneSubscription?: Maybe; insertOneTaxa?: Maybe; insertOneUser?: Maybe; - linkReportsToIncidents?: Maybe>>; logIncidentHistory?: Maybe; logReportHistory?: Maybe; processNotifications?: Maybe; @@ -2371,10 +2338,7 @@ export type Mutation = { replaceOneEntity?: Maybe; replaceOneHistory_incident?: Maybe; replaceOneHistory_report?: Maybe; - replaceOneIncident?: Maybe; replaceOneNotification?: Maybe; - replaceOneQuickadd?: Maybe; - replaceOneReport?: Maybe; replaceOneSubmission?: Maybe; replaceOneSubscription?: Maybe; replaceOneTaxa?: Maybe; @@ -2401,11 +2365,8 @@ export type Mutation = { updateOneEntity?: Maybe; updateOneHistory_incident?: Maybe; updateOneHistory_report?: Maybe; - updateOneIncident?: Maybe; updateOneNotification?: Maybe; updateOneQuickadd?: Maybe; - updateOneReport?: Maybe; - updateOneReportTranslation?: Maybe; updateOneSubmission?: Maybe; updateOneSubscription?: Maybe; updateOneTaxa?: Maybe; @@ -2417,10 +2378,7 @@ export type Mutation = { upsertOneEntity?: Maybe; upsertOneHistory_incident?: Maybe; upsertOneHistory_report?: Maybe; - upsertOneIncident?: Maybe; upsertOneNotification?: Maybe; - upsertOneQuickadd?: Maybe; - upsertOneReport?: Maybe; upsertOneSubmission?: Maybe; upsertOneSubscription?: Maybe; upsertOneTaxa?: Maybe; @@ -2484,9 +2442,9 @@ export type MutationDeleteManyNotificationsArgs = { export type MutationDeleteManyQuickaddsArgs = { - filter?: InputMaybe; + filter?: InputMaybe; pagination?: InputMaybe; - sort?: InputMaybe; + sort?: InputMaybe; }; @@ -2550,18 +2508,15 @@ export type MutationDeleteOneHistory_ReportArgs = { }; -export type MutationDeleteOneIncidentArgs = { - query: IncidentQueryInput; -}; - - export type MutationDeleteOneNotificationArgs = { query: NotificationQueryInput; }; -export type MutationDeleteOneReportArgs = { - query: ReportQueryInput; +export type MutationDeleteOneQuickaddArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; @@ -2636,7 +2591,7 @@ export type MutationInsertManyNotificationsArgs = { export type MutationInsertManyQuickaddsArgs = { - data: Array; + data?: InputMaybe>>; }; @@ -2700,23 +2655,13 @@ export type MutationInsertOneHistory_ReportArgs = { }; -export type MutationInsertOneIncidentArgs = { - data: IncidentInsertInput; -}; - - export type MutationInsertOneNotificationArgs = { data: NotificationInsertInput; }; export type MutationInsertOneQuickaddArgs = { - data?: InputMaybe; -}; - - -export type MutationInsertOneReportArgs = { - data: ReportInsertInput; + data?: InputMaybe; }; @@ -2740,11 +2685,6 @@ export type MutationInsertOneUserArgs = { }; -export type MutationLinkReportsToIncidentsArgs = { - input?: InputMaybe; -}; - - export type MutationLogIncidentHistoryArgs = { input?: InputMaybe; }; @@ -2802,29 +2742,12 @@ export type MutationReplaceOneHistory_ReportArgs = { }; -export type MutationReplaceOneIncidentArgs = { - data: IncidentInsertInput; - query?: InputMaybe; -}; - - export type MutationReplaceOneNotificationArgs = { data: NotificationInsertInput; query?: InputMaybe; }; -export type MutationReplaceOneQuickaddArgs = { - data: QuickaddInsertInput; -}; - - -export type MutationReplaceOneReportArgs = { - data: ReportInsertInput; - query?: InputMaybe; -}; - - export type MutationReplaceOneSubmissionArgs = { data: SubmissionInsertInput; query?: InputMaybe; @@ -2904,7 +2827,8 @@ export type MutationUpdateManyNotificationsArgs = { export type MutationUpdateManyQuickaddsArgs = { - set: QuickaddUpdateInput; + filter: QuickaddFilterType; + update: QuickaddUpdateType; }; @@ -2980,12 +2904,6 @@ export type MutationUpdateOneHistory_ReportArgs = { }; -export type MutationUpdateOneIncidentArgs = { - query?: InputMaybe; - set: IncidentUpdateInput; -}; - - export type MutationUpdateOneNotificationArgs = { query?: InputMaybe; set: NotificationUpdateInput; @@ -2993,18 +2911,8 @@ export type MutationUpdateOneNotificationArgs = { export type MutationUpdateOneQuickaddArgs = { - set: QuickaddUpdateInput; -}; - - -export type MutationUpdateOneReportArgs = { - query?: InputMaybe; - set: ReportUpdateInput; -}; - - -export type MutationUpdateOneReportTranslationArgs = { - input?: InputMaybe; + filter: QuickaddFilterType; + update: QuickaddUpdateType; }; @@ -3074,29 +2982,12 @@ export type MutationUpsertOneHistory_ReportArgs = { }; -export type MutationUpsertOneIncidentArgs = { - data: IncidentInsertInput; - query?: InputMaybe; -}; - - export type MutationUpsertOneNotificationArgs = { data: NotificationInsertInput; query?: InputMaybe; }; -export type MutationUpsertOneQuickaddArgs = { - data: QuickaddInsertInput; -}; - - -export type MutationUpsertOneReportArgs = { - data: ReportInsertInput; - query?: InputMaybe; -}; - - export type MutationUpsertOneSubmissionArgs = { data: SubmissionInsertInput; query?: InputMaybe; @@ -3274,15 +3165,15 @@ export type ObjectIdNotFilter = { }; export enum Opr { - All = '$all', - Eql = '$eq', - Gt = '$gt', - Gte = '$gte', - In = '$in', - Lt = '$lt', - Lte = '$lte', - Ne = '$ne', - Nin = '$nin' + All = 'ALL', + Eql = 'EQL', + Gt = 'GT', + Gte = 'GTE', + In = 'IN', + Lt = 'LT', + Lte = 'LTE', + Ne = 'NE', + Nin = 'NIN' } export type PaginationType = { @@ -3320,14 +3211,10 @@ export type Query = { history_incidents: Array>; history_report?: Maybe; history_reports: Array>; - incident?: Maybe; - incidents: Array>; notification?: Maybe; notifications: Array>; quickadd?: Maybe; - quickadds?: Maybe>>; - report?: Maybe; - reports: Array>; + quickadds?: Maybe>>; risks?: Maybe>>; submission?: Maybe; submissions: Array>; @@ -3424,18 +3311,6 @@ export type QueryHistory_ReportsArgs = { }; -export type QueryIncidentArgs = { - query?: InputMaybe; -}; - - -export type QueryIncidentsArgs = { - limit?: InputMaybe; - query?: InputMaybe; - sortBy?: InputMaybe; -}; - - export type QueryNotificationArgs = { query?: InputMaybe; }; @@ -3448,22 +3323,17 @@ export type QueryNotificationsArgs = { }; -export type QueryQuickaddsArgs = { - filter?: InputMaybe; +export type QueryQuickaddArgs = { + filter?: InputMaybe; pagination?: InputMaybe; - sort?: InputMaybe; + sort?: InputMaybe; }; -export type QueryReportArgs = { - query?: InputMaybe; -}; - - -export type QueryReportsArgs = { - limit?: InputMaybe; - query?: InputMaybe; - sortBy?: InputMaybe; +export type QueryQuickaddsArgs = { + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; @@ -3519,57 +3389,60 @@ export type QueryUsersArgs = { sortBy?: InputMaybe; }; -export type QuickAdd = { - __typename?: 'QuickAdd'; +export type Quickadd = { + __typename?: 'Quickadd'; _id?: Maybe; date_submitted?: Maybe; - incident_id?: Maybe; + incident_id?: Maybe; source_domain?: Maybe; url?: Maybe; }; -export type QuickAddFilterType = { - AND?: InputMaybe>>; - NOR?: InputMaybe>>; - OR?: InputMaybe>>; +export type QuickaddFilterType = { + AND?: InputMaybe>>; + NOR?: InputMaybe>>; + OR?: InputMaybe>>; _id?: InputMaybe; date_submitted?: InputMaybe; - incident_id?: InputMaybe; + incident_id?: InputMaybe; source_domain?: InputMaybe; url?: InputMaybe; }; -export type QuickAddInsertType = { +export type QuickaddIncType = { + incident_id?: InputMaybe; +}; + +export type QuickaddInsertInput = { _id?: InputMaybe; - date_submitted?: InputMaybe; + date_submitted: Scalars['String']['input']; incident_id?: InputMaybe; source_domain?: InputMaybe; - url?: InputMaybe; + url: Scalars['String']['input']; }; -export type QuickAddSortType = { - _id?: InputMaybe; - date_submitted?: InputMaybe; - incident_id?: InputMaybe; - source_domain?: InputMaybe; - url?: InputMaybe; +export type QuickaddInsertType = { + _id?: InputMaybe; + date_submitted?: InputMaybe; + incident_id?: InputMaybe; + source_domain?: InputMaybe; + url?: InputMaybe; }; -export type Quickadd = { - __typename?: 'Quickadd'; - _id?: Maybe; - date_submitted: Scalars['String']['output']; - incident_id?: Maybe; - source_domain?: Maybe; - url: Scalars['String']['output']; +export type QuickaddSetOnInsertType = { + _id?: InputMaybe; + date_submitted?: InputMaybe; + incident_id?: InputMaybe; + source_domain?: InputMaybe; + url?: InputMaybe; }; -export type QuickaddInsertInput = { +export type QuickaddSetType = { _id?: InputMaybe; - date_submitted: Scalars['String']['input']; - incident_id?: InputMaybe; + date_submitted?: InputMaybe; + incident_id?: InputMaybe; source_domain?: InputMaybe; - url: Scalars['String']['input']; + url?: InputMaybe; }; export enum QuickaddSortByInput { @@ -3585,6 +3458,14 @@ export enum QuickaddSortByInput { IdDesc = '_ID_DESC' } +export type QuickaddSortType = { + _id?: InputMaybe; + date_submitted?: InputMaybe; + incident_id?: InputMaybe; + source_domain?: InputMaybe; + url?: InputMaybe; +}; + export type QuickaddUpdateInput = { _id?: InputMaybe; _id_unset?: InputMaybe; @@ -3598,43 +3479,10 @@ export type QuickaddUpdateInput = { url_unset?: InputMaybe; }; -export type Report = { - __typename?: 'Report'; - _id?: Maybe; - authors: Array>; - cloudinary_id: Scalars['String']['output']; - date_downloaded: Scalars['DateTime']['output']; - date_modified: Scalars['DateTime']['output']; - date_published: Scalars['DateTime']['output']; - date_submitted: Scalars['DateTime']['output']; - description?: Maybe; - editor_notes?: Maybe; - embedding?: Maybe; - epoch_date_downloaded: Scalars['Int']['output']; - epoch_date_modified: Scalars['Int']['output']; - epoch_date_published: Scalars['Int']['output']; - epoch_date_submitted: Scalars['Int']['output']; - flag?: Maybe; - image_url: Scalars['String']['output']; - inputs_outputs?: Maybe>>; - is_incident_report?: Maybe; - language: Scalars['String']['output']; - plain_text: Scalars['String']['output']; - quiet?: Maybe; - report_number: Scalars['Int']['output']; - source_domain: Scalars['String']['output']; - submitters: Array>; - tags: Array>; - text: Scalars['String']['output']; - title: Scalars['String']['output']; - translations?: Maybe; - url: Scalars['String']['output']; - user?: Maybe; -}; - - -export type ReportTranslationsArgs = { - input?: InputMaybe; +export type QuickaddUpdateType = { + inc?: InputMaybe; + set?: InputMaybe; + setOnInsert?: InputMaybe; }; export type ReportEmbedding = { @@ -4095,8 +3943,8 @@ export type RisksPayloadPrecedentTsne = { }; export enum SortType { - Asc = 1, - Desc = -1 + Asc = 'ASC', + Desc = 'DESC' } /** Filter type for String scalar */ @@ -4682,7 +4530,6 @@ export type Subscription = { __typename?: 'Subscription'; _id?: Maybe; entityId?: Maybe; - incident_id?: Maybe; type: Scalars['String']['output']; userId: User; }; @@ -5623,7 +5470,6 @@ export type ResolversTypes = { History_reportQueryInput: History_ReportQueryInput; History_reportSortByInput: History_ReportSortByInput; History_reportUpdateInput: History_ReportUpdateInput; - Incident: ResolverTypeWrapper; IncidentAllegedDeployerOfAISystemRelationInput: IncidentAllegedDeployerOfAiSystemRelationInput; IncidentAllegedDeveloperOfAISystemRelationInput: IncidentAllegedDeveloperOfAiSystemRelationInput; IncidentAllegedHarmedOrNearlyHarmedPartiesRelationInput: IncidentAllegedHarmedOrNearlyHarmedPartiesRelationInput; @@ -5646,12 +5492,12 @@ export type ResolversTypes = { IncidentTsneUpdateInput: IncidentTsneUpdateInput; IncidentUpdateInput: IncidentUpdateInput; InsertManyPayload: ResolverTypeWrapper; + IntFilter: IntFilter; + IntNotFilter: IntNotFilter; LinkReportsToIncidentsInput: LinkReportsToIncidentsInput; LogIncidentHistoryPayload: ResolverTypeWrapper; LogReportHistoryPayload: ResolverTypeWrapper; Long: ResolverTypeWrapper; - LongFilter: LongFilter; - LongNotFilter: LongNotFilter; Mutation: ResolverTypeWrapper<{}>; Notification: ResolverTypeWrapper; NotificationInsertInput: NotificationInsertInput; @@ -5667,15 +5513,17 @@ export type ResolversTypes = { PromoteSubmissionToReportInput: PromoteSubmissionToReportInput; PromoteSubmissionToReportPayload: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; - QuickAdd: ResolverTypeWrapper; - QuickAddFilterType: QuickAddFilterType; - QuickAddInsertType: QuickAddInsertType; - QuickAddSortType: QuickAddSortType; Quickadd: ResolverTypeWrapper; + QuickaddFilterType: QuickaddFilterType; + QuickaddIncType: QuickaddIncType; QuickaddInsertInput: QuickaddInsertInput; + QuickaddInsertType: QuickaddInsertType; + QuickaddSetOnInsertType: QuickaddSetOnInsertType; + QuickaddSetType: QuickaddSetType; QuickaddSortByInput: QuickaddSortByInput; + QuickaddSortType: QuickaddSortType; QuickaddUpdateInput: QuickaddUpdateInput; - Report: ResolverTypeWrapper; + QuickaddUpdateType: QuickaddUpdateType; ReportEmbedding: ResolverTypeWrapper; ReportEmbeddingInsertInput: ReportEmbeddingInsertInput; ReportEmbeddingQueryInput: ReportEmbeddingQueryInput; @@ -5838,7 +5686,6 @@ export type ResolversParentTypes = { History_reportInsertInput: History_ReportInsertInput; History_reportQueryInput: History_ReportQueryInput; History_reportUpdateInput: History_ReportUpdateInput; - Incident: Incident; IncidentAllegedDeployerOfAISystemRelationInput: IncidentAllegedDeployerOfAiSystemRelationInput; IncidentAllegedDeveloperOfAISystemRelationInput: IncidentAllegedDeveloperOfAiSystemRelationInput; IncidentAllegedHarmedOrNearlyHarmedPartiesRelationInput: IncidentAllegedHarmedOrNearlyHarmedPartiesRelationInput; @@ -5860,12 +5707,12 @@ export type ResolversParentTypes = { IncidentTsneUpdateInput: IncidentTsneUpdateInput; IncidentUpdateInput: IncidentUpdateInput; InsertManyPayload: InsertManyPayload; + IntFilter: IntFilter; + IntNotFilter: IntNotFilter; LinkReportsToIncidentsInput: LinkReportsToIncidentsInput; LogIncidentHistoryPayload: LogIncidentHistoryPayload; LogReportHistoryPayload: LogReportHistoryPayload; Long: Scalars['Long']['output']; - LongFilter: LongFilter; - LongNotFilter: LongNotFilter; Mutation: {}; Notification: Notification; NotificationInsertInput: NotificationInsertInput; @@ -5879,14 +5726,16 @@ export type ResolversParentTypes = { PromoteSubmissionToReportInput: PromoteSubmissionToReportInput; PromoteSubmissionToReportPayload: PromoteSubmissionToReportPayload; Query: {}; - QuickAdd: QuickAdd; - QuickAddFilterType: QuickAddFilterType; - QuickAddInsertType: QuickAddInsertType; - QuickAddSortType: QuickAddSortType; Quickadd: Quickadd; + QuickaddFilterType: QuickaddFilterType; + QuickaddIncType: QuickaddIncType; QuickaddInsertInput: QuickaddInsertInput; + QuickaddInsertType: QuickaddInsertType; + QuickaddSetOnInsertType: QuickaddSetOnInsertType; + QuickaddSetType: QuickaddSetType; + QuickaddSortType: QuickaddSortType; QuickaddUpdateInput: QuickaddUpdateInput; - Report: Report; + QuickaddUpdateType: QuickaddUpdateType; ReportEmbedding: ReportEmbedding; ReportEmbeddingInsertInput: ReportEmbeddingInsertInput; ReportEmbeddingQueryInput: ReportEmbeddingQueryInput; @@ -6092,11 +5941,9 @@ export type ChecklistRiskPrecedentResolvers = { _id?: Resolver, ParentType, ContextType>; attributes?: Resolver>>, ParentType, ContextType>; - incidents?: Resolver>, ParentType, ContextType>; namespace?: Resolver; notes?: Resolver, ParentType, ContextType>; publish?: Resolver, ParentType, ContextType>; - reports?: Resolver>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -6225,28 +6072,6 @@ export type History_ReportEmbeddingResolvers; }; -export type IncidentResolvers = { - AllegedDeployerOfAISystem?: Resolver>>, ParentType, ContextType>; - AllegedDeveloperOfAISystem?: Resolver>>, ParentType, ContextType>; - AllegedHarmedOrNearlyHarmedParties?: Resolver>>, ParentType, ContextType>; - _id?: Resolver, ParentType, ContextType>; - date?: Resolver; - description?: Resolver, ParentType, ContextType>; - editor_dissimilar_incidents?: Resolver>>, ParentType, ContextType>; - editor_notes?: Resolver, ParentType, ContextType>; - editor_similar_incidents?: Resolver>>, ParentType, ContextType>; - editors?: Resolver>, ParentType, ContextType>; - embedding?: Resolver, ParentType, ContextType>; - epoch_date_modified?: Resolver, ParentType, ContextType>; - flagged_dissimilar_incidents?: Resolver>>, ParentType, ContextType>; - incident_id?: Resolver; - nlp_similar_incidents?: Resolver>>, ParentType, ContextType>; - reports?: Resolver>, ParentType, ContextType>; - title?: Resolver; - tsne?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type IncidentEmbeddingResolvers = { from_reports?: Resolver>>, ParentType, ContextType>; vector?: Resolver>>, ParentType, ContextType>; @@ -6285,7 +6110,6 @@ export interface LongScalarConfig extends GraphQLScalarTypeConfig = { - _?: Resolver, ParentType, ContextType>; createDefaultAdminUser?: Resolver, ParentType, ContextType, Partial>; createVariant?: Resolver, ParentType, ContextType, Partial>; deleteManyCandidates?: Resolver, ParentType, ContextType, Partial>; @@ -6310,10 +6134,8 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; deleteOneHistory_incident?: Resolver, ParentType, ContextType, RequireFields>; deleteOneHistory_report?: Resolver, ParentType, ContextType, RequireFields>; - deleteOneIncident?: Resolver, ParentType, ContextType, RequireFields>; deleteOneNotification?: Resolver, ParentType, ContextType, RequireFields>; - deleteOneQuickadd?: Resolver, ParentType, ContextType>; - deleteOneReport?: Resolver, ParentType, ContextType, RequireFields>; + deleteOneQuickadd?: Resolver, ParentType, ContextType, Partial>; deleteOneSubmission?: Resolver, ParentType, ContextType, RequireFields>; deleteOneSubscription?: Resolver, ParentType, ContextType, RequireFields>; deleteOneTaxa?: Resolver, ParentType, ContextType, RequireFields>; @@ -6328,7 +6150,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; insertManyIncidents?: Resolver, ParentType, ContextType, RequireFields>; insertManyNotifications?: Resolver, ParentType, ContextType, RequireFields>; - insertManyQuickadds?: Resolver, ParentType, ContextType, RequireFields>; + insertManyQuickadds?: Resolver, ParentType, ContextType, Partial>; insertManyReports?: Resolver, ParentType, ContextType, RequireFields>; insertManySubmissions?: Resolver, ParentType, ContextType, RequireFields>; insertManySubscriptions?: Resolver, ParentType, ContextType, RequireFields>; @@ -6341,15 +6163,12 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; insertOneHistory_incident?: Resolver, ParentType, ContextType, RequireFields>; insertOneHistory_report?: Resolver, ParentType, ContextType, RequireFields>; - insertOneIncident?: Resolver, ParentType, ContextType, RequireFields>; insertOneNotification?: Resolver, ParentType, ContextType, RequireFields>; - insertOneQuickadd?: Resolver, ParentType, ContextType, Partial>; - insertOneReport?: Resolver, ParentType, ContextType, RequireFields>; + insertOneQuickadd?: Resolver, ParentType, ContextType, Partial>; insertOneSubmission?: Resolver, ParentType, ContextType, RequireFields>; insertOneSubscription?: Resolver, ParentType, ContextType, RequireFields>; insertOneTaxa?: Resolver, ParentType, ContextType, RequireFields>; insertOneUser?: Resolver, ParentType, ContextType, RequireFields>; - linkReportsToIncidents?: Resolver>>, ParentType, ContextType, Partial>; logIncidentHistory?: Resolver, ParentType, ContextType, Partial>; logReportHistory?: Resolver, ParentType, ContextType, Partial>; processNotifications?: Resolver, ParentType, ContextType>; @@ -6361,10 +6180,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; replaceOneHistory_incident?: Resolver, ParentType, ContextType, RequireFields>; replaceOneHistory_report?: Resolver, ParentType, ContextType, RequireFields>; - replaceOneIncident?: Resolver, ParentType, ContextType, RequireFields>; replaceOneNotification?: Resolver, ParentType, ContextType, RequireFields>; - replaceOneQuickadd?: Resolver, ParentType, ContextType, RequireFields>; - replaceOneReport?: Resolver, ParentType, ContextType, RequireFields>; replaceOneSubmission?: Resolver, ParentType, ContextType, RequireFields>; replaceOneSubscription?: Resolver, ParentType, ContextType, RequireFields>; replaceOneTaxa?: Resolver, ParentType, ContextType, RequireFields>; @@ -6378,7 +6194,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; updateManyIncidents?: Resolver, ParentType, ContextType, RequireFields>; updateManyNotifications?: Resolver, ParentType, ContextType, RequireFields>; - updateManyQuickadds?: Resolver, ParentType, ContextType, RequireFields>; + updateManyQuickadds?: Resolver, ParentType, ContextType, RequireFields>; updateManyReports?: Resolver, ParentType, ContextType, RequireFields>; updateManySubmissions?: Resolver, ParentType, ContextType, RequireFields>; updateManySubscriptions?: Resolver, ParentType, ContextType, RequireFields>; @@ -6391,11 +6207,8 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; updateOneHistory_incident?: Resolver, ParentType, ContextType, RequireFields>; updateOneHistory_report?: Resolver, ParentType, ContextType, RequireFields>; - updateOneIncident?: Resolver, ParentType, ContextType, RequireFields>; updateOneNotification?: Resolver, ParentType, ContextType, RequireFields>; - updateOneQuickadd?: Resolver, ParentType, ContextType, RequireFields>; - updateOneReport?: Resolver, ParentType, ContextType, RequireFields>; - updateOneReportTranslation?: Resolver, ParentType, ContextType, Partial>; + updateOneQuickadd?: Resolver, ParentType, ContextType, RequireFields>; updateOneSubmission?: Resolver, ParentType, ContextType, RequireFields>; updateOneSubscription?: Resolver, ParentType, ContextType, RequireFields>; updateOneTaxa?: Resolver, ParentType, ContextType, RequireFields>; @@ -6407,10 +6220,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; upsertOneHistory_incident?: Resolver, ParentType, ContextType, RequireFields>; upsertOneHistory_report?: Resolver, ParentType, ContextType, RequireFields>; - upsertOneIncident?: Resolver, ParentType, ContextType, RequireFields>; upsertOneNotification?: Resolver, ParentType, ContextType, RequireFields>; - upsertOneQuickadd?: Resolver, ParentType, ContextType, RequireFields>; - upsertOneReport?: Resolver, ParentType, ContextType, RequireFields>; upsertOneSubmission?: Resolver, ParentType, ContextType, RequireFields>; upsertOneSubscription?: Resolver, ParentType, ContextType, RequireFields>; upsertOneTaxa?: Resolver, ParentType, ContextType, RequireFields>; @@ -6431,8 +6241,6 @@ export interface ObjectIdScalarConfig extends GraphQLScalarTypeConfig = { incident_ids?: Resolver>>, ParentType, ContextType>; report_number?: Resolver, ParentType, ContextType>; @@ -6455,14 +6263,10 @@ export type QueryResolvers>, ParentType, ContextType, RequireFields>; history_report?: Resolver, ParentType, ContextType, Partial>; history_reports?: Resolver>, ParentType, ContextType, RequireFields>; - incident?: Resolver, ParentType, ContextType, Partial>; - incidents?: Resolver>, ParentType, ContextType, RequireFields>; notification?: Resolver, ParentType, ContextType, Partial>; notifications?: Resolver>, ParentType, ContextType, RequireFields>; - quickadd?: Resolver, ParentType, ContextType>; - quickadds?: Resolver>>, ParentType, ContextType, Partial>; - report?: Resolver, ParentType, ContextType, Partial>; - reports?: Resolver>, ParentType, ContextType, RequireFields>; + quickadd?: Resolver, ParentType, ContextType, Partial>; + quickadds?: Resolver>>, ParentType, ContextType, Partial>; risks?: Resolver>>, ParentType, ContextType, Partial>; submission?: Resolver, ParentType, ContextType, Partial>; submissions?: Resolver>, ParentType, ContextType, RequireFields>; @@ -6474,58 +6278,15 @@ export type QueryResolvers>, ParentType, ContextType, RequireFields>; }; -export type QuickAddResolvers = { +export type QuickaddResolvers = { _id?: Resolver, ParentType, ContextType>; date_submitted?: Resolver, ParentType, ContextType>; - incident_id?: Resolver, ParentType, ContextType>; + incident_id?: Resolver, ParentType, ContextType>; source_domain?: Resolver, ParentType, ContextType>; url?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; -export type QuickaddResolvers = { - _id?: Resolver, ParentType, ContextType>; - date_submitted?: Resolver; - incident_id?: Resolver, ParentType, ContextType>; - source_domain?: Resolver, ParentType, ContextType>; - url?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}; - -export type ReportResolvers = { - _id?: Resolver, ParentType, ContextType>; - authors?: Resolver>, ParentType, ContextType>; - cloudinary_id?: Resolver; - date_downloaded?: Resolver; - date_modified?: Resolver; - date_published?: Resolver; - date_submitted?: Resolver; - description?: Resolver, ParentType, ContextType>; - editor_notes?: Resolver, ParentType, ContextType>; - embedding?: Resolver, ParentType, ContextType>; - epoch_date_downloaded?: Resolver; - epoch_date_modified?: Resolver; - epoch_date_published?: Resolver; - epoch_date_submitted?: Resolver; - flag?: Resolver, ParentType, ContextType>; - image_url?: Resolver; - inputs_outputs?: Resolver>>, ParentType, ContextType>; - is_incident_report?: Resolver, ParentType, ContextType>; - language?: Resolver; - plain_text?: Resolver; - quiet?: Resolver, ParentType, ContextType>; - report_number?: Resolver; - source_domain?: Resolver; - submitters?: Resolver>, ParentType, ContextType>; - tags?: Resolver>, ParentType, ContextType>; - text?: Resolver; - title?: Resolver; - translations?: Resolver, ParentType, ContextType, Partial>; - url?: Resolver; - user?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type ReportEmbeddingResolvers = { from_text_hash?: Resolver, ParentType, ContextType>; vector?: Resolver>>, ParentType, ContextType>; @@ -6587,8 +6348,6 @@ export type RisksPayloadPrecedentTsneResolvers; }; -export type SortTypeResolvers = { ASC: 1, DESC: -1 }; - export type SubmissionResolvers = { _id?: Resolver, ParentType, ContextType>; authors?: Resolver>, ParentType, ContextType>; @@ -6641,7 +6400,6 @@ export type SubmissionNlp_Similar_IncidentResolvers = { _id?: SubscriptionResolver, "_id", ParentType, ContextType>; entityId?: SubscriptionResolver, "entityId", ParentType, ContextType>; - incident_id?: SubscriptionResolver, "incident_id", ParentType, ContextType>; type?: SubscriptionResolver; userId?: SubscriptionResolver; }; @@ -6762,7 +6520,6 @@ export type Resolvers = { History_incidentTsne?: History_IncidentTsneResolvers; History_report?: History_ReportResolvers; History_reportEmbedding?: History_ReportEmbeddingResolvers; - Incident?: IncidentResolvers; IncidentEmbedding?: IncidentEmbeddingResolvers; IncidentNlp_similar_incident?: IncidentNlp_Similar_IncidentResolvers; IncidentTsne?: IncidentTsneResolvers; @@ -6773,12 +6530,9 @@ export type Resolvers = { Mutation?: MutationResolvers; Notification?: NotificationResolvers; ObjectId?: GraphQLScalarType; - Opr?: OprResolvers; PromoteSubmissionToReportPayload?: PromoteSubmissionToReportPayloadResolvers; Query?: QueryResolvers; - QuickAdd?: QuickAddResolvers; Quickadd?: QuickaddResolvers; - Report?: ReportResolvers; ReportEmbedding?: ReportEmbeddingResolvers; ReportTranslation?: ReportTranslationResolvers; RisksPayloadItem?: RisksPayloadItemResolvers; @@ -6786,7 +6540,6 @@ export type Resolvers = { RisksPayloadPrecedentEmbedding?: RisksPayloadPrecedentEmbeddingResolvers; RisksPayloadPrecedentNlp_similar_incident?: RisksPayloadPrecedentNlp_Similar_IncidentResolvers; RisksPayloadPrecedentTsne?: RisksPayloadPrecedentTsneResolvers; - SortType?: SortTypeResolvers; Submission?: SubmissionResolvers; SubmissionEmbedding?: SubmissionEmbeddingResolvers; SubmissionNlp_similar_incident?: SubmissionNlp_Similar_IncidentResolvers; diff --git a/site/gatsby-site/server/interfaces.ts b/site/gatsby-site/server/interfaces.ts new file mode 100644 index 0000000000..4a3e3835b0 --- /dev/null +++ b/site/gatsby-site/server/interfaces.ts @@ -0,0 +1,11 @@ +import { IncomingMessage } from 'http'; +import { MongoClient } from 'mongodb'; + +export interface Context { + user: { + id: string, + roles: string[], + } | null, + req: IncomingMessage, + client: MongoClient, +} \ No newline at end of file diff --git a/site/gatsby-site/server/local.ts b/site/gatsby-site/server/local.ts index 459bd205f0..93b7474981 100644 --- a/site/gatsby-site/server/local.ts +++ b/site/gatsby-site/server/local.ts @@ -3,7 +3,8 @@ import { mutationFields as quickAddsMutationFields, permissions as quickAddsPermissions, } from './fields/quickadds'; -import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; + +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; import { shield, deny } from 'graphql-shield'; import { applyMiddleware } from 'graphql-middleware'; import { ObjectIdScalar } from './scalars'; @@ -40,7 +41,6 @@ export const getSchema = () => { Query: { "*": deny, ...quickAddsPermissions.Query, - }, Mutation: { "*": deny, diff --git a/site/gatsby-site/server/remote.ts b/site/gatsby-site/server/remote.ts index f7c5f18191..79d944cce8 100644 --- a/site/gatsby-site/server/remote.ts +++ b/site/gatsby-site/server/remote.ts @@ -26,17 +26,22 @@ const userExecutor = buildHTTPExecutor({ const ignoreTypes = [ - 'QuickAdd', + 'Quickadd', 'QuickaddQueryInput', ]; const ignoredQueries = [ + 'quickadd', 'quickadds', ]; const ignoredMutations = [ + 'deleteOneQuickadds', 'deleteManyQuickadds', 'insertOneQuickadd', + 'insertManyQuickadds', + 'updateOneQuickadd', + 'updateManyQuickadds', ] export const getSchema = () => { diff --git a/site/gatsby-site/server/tests/fixtures/quickadds.ts b/site/gatsby-site/server/tests/fixtures/quickadds.ts new file mode 100644 index 0000000000..356ba5ca90 --- /dev/null +++ b/site/gatsby-site/server/tests/fixtures/quickadds.ts @@ -0,0 +1,157 @@ +import { ObjectId } from "bson"; +import { Fixture, serializeId } from "../utils"; +import { Quickadd } from "../../generated/graphql"; + +const quickadd1 = { + _id: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e4'), + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 1, + source_domain: "example.com", + url: "http://example.com" +} + +const quickadd2 = { + _id: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e5'), + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 2, + source_domain: "example2.com", + url: "http://example2.com" +} + +const quickadd3 = { + _id: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e6'), + date_submitted: "2021-09-14T00:00:00.000Z", + incident_id: 2, + source_domain: "example3.com", + url: "http://example3.com" +} + +const quickadd4 = { + _id: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e7'), + date_submitted: "2021-09-14T00:00:00.000Z", + incident_id: 2, + source_domain: "example4.com", + url: "http://example4.com" +} + +const quickadd5 = { + _id: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e8'), + date_submitted: "2024-09-14T00:00:00.000Z", + incident_id: 3, + source_domain: "example5.com", + url: "http://example5.com" +} + + +const fixture: Fixture = { + name: 'quickadd', + query: ` + date_submitted + incident_id + source_domain + url + `, + seeds: { + quickadd: [ + quickadd1, + quickadd2, + quickadd3, + quickadd4, + quickadd5 + ], + }, + testSingular: { + filter: { _id: { EQ: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e4') } }, + result: serializeId(quickadd1) + }, + testPluralFilter: { + filter: { + _id: { EQ: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e4') }, + }, + result: [ + serializeId(quickadd1) + ], + }, + testPluralSort: { + sort: { _id: "ASC" }, + result: [ + serializeId(quickadd1), + serializeId(quickadd2), + serializeId(quickadd3), + serializeId(quickadd4), + serializeId(quickadd5), + ], + }, + testPluralPagination: { + pagination: { limit: 2, skip: 2 }, + sort: { _id: "ASC" }, + result: [ + serializeId(quickadd3), + serializeId(quickadd4), + ] + }, + + testUpdateOne: { + filter: { _id: { EQ: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e4') } }, + set: { url: 'https://edited.com' }, + result: { url: 'https://edited.com' } + }, + testUpdateMany: { + filter: { incident_id: { EQ: 2 } }, + set: { url: 'https://edited.com' }, + result: { modifiedCount: 3, matchedCount: 3 } + }, + testInsertOne: { + insert: { + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 1, + source_domain: "example.com", + url: "http://example.com" + }, + result: { + _id: expect.any(String), + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 1, + source_domain: "example.com", + url: "http://example.com" + } + }, + testInsertMany: { + insert: [ + { + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 1, + source_domain: "example.com", + url: "http://example.com" + }, + { + date_submitted: "2020-09-14T00:00:00.000Z", + incident_id: 2, + source_domain: "example2.com", + url: "http://example2.com" + } + ], + result: { insertedIds: [expect.any(String), expect.any(String)] } + } + , + testDeleteOne: { + filter: { _id: { EQ: new ObjectId('60a7c5b7b4f5b8a6d8f9c7e4') } }, + result: { _id: '60a7c5b7b4f5b8a6d8f9c7e4' } + }, + testDeleteMany: { + filter: { incident_id: { EQ: 2 } }, + result: { deletedCount: 3 }, + }, + roles: { + singular: [], + plural: [], + insertOne: [], + insertMany: ['admin'], + updateOne: ['admin'], + updateMany: ['admin'], + deleteOne: ['admin'], + deleteMany: ['admin'], + }, +} + +export default fixture; \ No newline at end of file diff --git a/site/gatsby-site/server/tests/mutation-fields.spec.ts b/site/gatsby-site/server/tests/mutation-fields.spec.ts new file mode 100644 index 0000000000..832bddfc4b --- /dev/null +++ b/site/gatsby-site/server/tests/mutation-fields.spec.ts @@ -0,0 +1,259 @@ +import { expect, jest, it } from '@jest/globals'; +import { ApolloServer } from "@apollo/server"; +import { pluralize, singularize } from "../utils"; +import capitalize from 'lodash/capitalize'; +import { makeRequest, seedCollection, seedFixture, seedUsers, startTestServer } from "./utils"; +import * as context from '../context'; + +import quickaddsFixture from './fixtures/quickadds'; + +const fixtures = [ + quickaddsFixture, +] + +fixtures.forEach((collection) => { + + const singularName = singularize(collection.name); + const pluralName = pluralize(collection.name); + + const filterTypeName = `${capitalize(singularName)}FilterType`; + const insertTypeName = `${capitalize(singularName)}InsertType`; + const updateTypeName = `${capitalize(singularName)}UpdateType`; + + const insertOneFieldName = `insertOne${capitalize(singularName)}`; + const insertManyFieldName = `insertMany${capitalize(pluralName)}`; + + const updateOneFieldName = `updateOne${capitalize(singularName)}`; + const updateManyFieldName = `updateMany${capitalize(pluralName)}`; + + const deleteOneFieldName = `deleteOne${capitalize(singularName)}`; + const deleteManyFieldName = `deleteMany${capitalize(pluralName)}`; + + describe(`${collection.name} generated mutation fields`, () => { + let server: ApolloServer, url: string; + + beforeAll(async () => { + ({ server, url } = await startTestServer()); + }); + + afterAll(async () => { + await server?.stop(); + }); + + it(`${insertOneFieldName} mutation`, async () => { + + await seedCollection({ name: collection.name, docs: [] }); + + await seedUsers([{ userId: 'user1', roles: collection.roles.insertOne }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + const mutationData = { + query: ` + mutation ($data: ${insertTypeName}!) { + ${insertOneFieldName}(data: $data) { + _id + ${collection.query} + } + } + `, + variables: { + data: collection.testInsertOne.insert, + } + }; + + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[insertOneFieldName]).toMatchObject(collection.testInsertOne.result) + + + if (collection.roles.insertOne.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + + it(`${insertManyFieldName} mutation`, async () => { + + await seedCollection({ name: collection.name, docs: [] }); + + await seedUsers([{ userId: 'user1', roles: collection.roles.insertMany }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + const mutationData = { + query: ` + mutation ($data: [${insertTypeName}!]) { + ${insertManyFieldName}(data: $data) { + insertedIds + } + + } + `, + variables: { data: collection.testInsertMany.insert } + }; + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[insertManyFieldName]).toMatchObject(collection.testInsertMany.result); + + + if (collection.roles.insertMany.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${updateOneFieldName} mutation`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.updateOne }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + const mutationData = { + query: ` + mutation($filter: ${filterTypeName}!, $update: ${updateTypeName}!) { + ${updateOneFieldName} (filter: $filter, update: $update) { + _id + ${collection.query} + } + } + `, + variables: { + "filter": collection.testUpdateOne.filter, + "update": { set: collection.testUpdateOne.set }, + }, + } + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[updateOneFieldName]).toMatchObject(collection.testUpdateOne.result) + expect(response.statusCode).toBe(200); + + + if (collection.roles.updateOne.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${updateManyFieldName} mutation`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.updateMany }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + + const mutationData = { + query: ` + mutation($filter: ${filterTypeName}!, $update: ${updateTypeName}!) { + ${updateManyFieldName}(filter: $filter, update: $update) { + modifiedCount + matchedCount + } + } + `, + variables: { + "filter": collection.testUpdateMany.filter, + "update": { "set": collection.testUpdateMany.set } + } + }; + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[updateManyFieldName]).toMatchObject(collection.testUpdateMany.result); + expect(response.statusCode).toBe(200); + + + if (collection.roles.updateMany.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${deleteOneFieldName} mutation`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.deleteOne }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + + const mutationData = { + query: ` + mutation Test($filter: ${filterTypeName}!) { + ${deleteOneFieldName}(filter: $filter) { + _id + } + } + `, + variables: { filter: collection.testDeleteOne.filter } + }; + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[deleteOneFieldName]).toMatchObject(collection.testDeleteOne.result); + + if (collection.roles.deleteOne.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${deleteManyFieldName} mutation`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.deleteOne }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + + const mutationData = { + query: ` + mutation Test($filter: ${filterTypeName}!) { + ${deleteManyFieldName}(filter: $filter) { + deletedCount + } + } + `, + variables: { filter: collection.testDeleteMany.filter } + }; + + const response = await makeRequest(url, mutationData); + + expect(response.body.data[deleteManyFieldName]).toMatchObject(collection.testDeleteMany.result); + + + if (collection.roles.deleteMany.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, mutationData); + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + }); +}); diff --git a/site/gatsby-site/server/tests/query-fields.spec.ts b/site/gatsby-site/server/tests/query-fields.spec.ts new file mode 100644 index 0000000000..7c7838a6a6 --- /dev/null +++ b/site/gatsby-site/server/tests/query-fields.spec.ts @@ -0,0 +1,172 @@ +import { ApolloServer } from "@apollo/server"; +import request from 'supertest'; +import { makeRequest, seedFixture, seedUsers, startTestServer } from "./utils"; +import { pluralize, singularize } from "../utils"; +import capitalize from 'lodash/capitalize'; +import quickaddsFixture from './fixtures/quickadds'; + +import * as context from '../context'; + +const fixtures = [ + quickaddsFixture, +] + +fixtures.forEach((collection) => { + + const singularName = singularize(collection.name); + const pluralName = pluralize(collection.name); + + const filterTypeName = `${capitalize(singularName)}FilterType`; + const sortTypeName = `${capitalize(singularName)}SortType`; + + describe(`${collection.name} - generated query fields`, () => { + let server: ApolloServer, url: string; + + beforeAll(async () => { + ({ server, url } = await startTestServer()); + }); + + afterAll(async () => { + await server?.stop(); + }); + + it(`${singularName} query`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.singular }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + + const queryData = { + query: ` + query ($filter: ${filterTypeName}!) { + ${singularName} (filter: $filter) { + _id + ${collection.query} + } + } + `, + variables: { filter: collection.testSingular.filter }, + }; + + const response = await makeRequest(url, queryData); + + expect(response.body.data[singularName]).toMatchObject(collection.testSingular.result); + + + if (collection.roles.singular.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, queryData); + + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${pluralName} query with filter`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.plural }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + const queryData = { + query: ` + query ($filter: ${filterTypeName}!) { + ${pluralName} (filter: $filter) { + _id + ${collection.query} + } + } + `, + variables: { filter: collection.testPluralFilter.filter }, + }; + + const response = await request(url).post('/').send(queryData); + + expect(response.body.data[pluralName]).toMatchObject(collection.testPluralFilter.result); + + + if (collection.roles.plural.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, queryData); + + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${pluralName} query with sort`, async () => { + + await seedFixture(collection.seeds); + + await seedUsers([{ userId: 'user1', roles: collection.roles.plural }]) + + jest.spyOn(context, 'verifyToken').mockResolvedValue({ sub: 'user1' }) + + + const queryData = { + query: ` + query ($sort: ${sortTypeName}!) { + ${pluralName} (sort: $sort) { + _id + ${collection.query} + } + } + `, + variables: { sort: collection.testPluralSort.sort }, + }; + + const response = await request(url).post('/').send(queryData); + + expect(response.body.data[pluralName]).toMatchObject(collection.testPluralSort.result); + + + if (collection.roles.plural.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, queryData); + + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + + it(`${pluralName} query with pagination`, async () => { + + await seedFixture(collection.seeds); + + const queryData = { + query: ` + query ($pagination: PaginationType, $sort: ${sortTypeName}!) { + ${pluralName} (pagination: $pagination, sort: $sort) { + _id + ${collection.query} + } + } + `, + variables: { pagination: collection.testPluralPagination.pagination, sort: collection.testPluralPagination.sort } + }; + + const response = await request(url).post('/').send(queryData); + + expect(response.body.data[pluralName]).toHaveLength(collection.testPluralPagination.result.length); + expect(response.body.data[pluralName]).toMatchObject(collection.testPluralPagination.result); + + + if (collection.roles.plural.length) { + + await seedUsers([{ userId: 'user1', roles: ['invalid'] }]) + + const response = await makeRequest(url, queryData); + + expect(response.body.errors[0].message).toBe('not authorized'); + } + }); + }); +}) diff --git a/site/gatsby-site/server/tests/quickadds.spec.ts b/site/gatsby-site/server/tests/quickadds.spec.ts deleted file mode 100644 index c58d71e178..0000000000 --- a/site/gatsby-site/server/tests/quickadds.spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { ApolloServer } from "@apollo/server"; -import request from 'supertest'; -import { login, seedCollection, startTestServer } from "./utils"; -import { ObjectId } from "mongodb"; -import config from "../config"; - -describe('Quickadds', () => { - let server: ApolloServer, url: string; - - beforeAll(async () => { - ({ server, url } = await startTestServer()); - }); - - afterAll(async () => { - await server?.stop(); - }); - - it(`quickadds default query`, async () => { - - await seedCollection({ - name: 'quickadd', docs: [ - { - "date_submitted": "2020-09-14T00:00:00.000Z", - "incident_id": 1, - "source_domain": "example.com", - "url": "http://example.com" - } - ] - }) - - const queryData = { - query: ` - query { - quickadds { - __typename - _id - date_submitted - incident_id - source_domain - url - } - } - `, - variables: { query: {} }, - }; - - const response = await request(url).post('/').send(queryData); - - expect(response.statusCode).toBe(200); - - expect(response.body.data.quickadds.length).toBe(1); - - const [quickadd] = response.body.data.quickadds; - - expect(quickadd.incident_id).toBe(1); - expect(quickadd.source_domain).toBe('example.com'); - expect(quickadd.url).toBe('http://example.com'); - expect(quickadd._id).toMatch(/^[0-9a-fA-F]{24}$/); - }); - - it(`quickadds query with incident_id filter`, async () => { - - await seedCollection({ - name: 'quickadd', docs: [ - { - _id: new ObjectId('5f5f3e3e3e3e3e3e3e3e3e3e'), - date_submitted: "2020-09-14T00:00:00.000Z", - incident_id: 1, - source_domain: "example1.com", - url: "http://example1.com" - }, - { - _id: new ObjectId('5f5f3e3e3e3e3e3e3e3e3e3f'), - date_submitted: "2020-09-14T00:00:00.000Z", - incident_id: 2, - source_domain: "example2.com", - url: "http://example2.com" - } - ] - }) - - const queryData = { - query: ` - query ($filter: QuickAddFilterType!) { - quickadds(filter: $filter) { - _id - date_submitted - incident_id - source_domain - url - } - } - `, - variables: { filter: { _id: { EQ: '5f5f3e3e3e3e3e3e3e3e3e3f' } } }, - }; - - const response = await request(url).post('/').send(queryData); - - - expect(response.body.data.quickadds.length).toBe(1); - - const [quickadd] = response.body.data.quickadds; - - expect(quickadd.incident_id).toBe(2); - expect(quickadd.source_domain).toBe('example2.com'); - expect(quickadd.url).toBe('http://example2.com'); - expect(quickadd._id).toMatch(/^[0-9a-fA-F]{24}$/); - }); - - it(`deleteManyQuickadds mutation throws if missing roles`, async () => { - - const mutationData = { - query: ` - mutation { - deleteManyQuickadds(filter: { _id: { EQ: "5f5f3e3e3e3e3e3e3e3e3e3f" } }) { - deletedCount - } - } - `, - }; - - const response = await request(url).post('/').send(mutationData); - - expect(response.body.errors[0].message).toBe('not authorized'); - - expect(response.body.errors.length).toBe(1); - - expect(response.statusCode).toBe(200); - }); - - it(`deleteManyQuickadds mutation`, async () => { - - const authData = await login(config.E2E_ADMIN_USERNAME!, config.E2E_ADMIN_PASSWORD!); - - await seedCollection({ - name: 'quickadd', - docs: [ - { - _id: new ObjectId('5f5f3e3e3e3e3e3e3e3e3e3e'), - date_submitted: "2020-09-14T00:00:00.000Z", - incident_id: 1, - source_domain: "example1.com", - url: "http://example1.com" - }, - { - _id: new ObjectId('5f5f3e3e3e3e3e3e3e3e3e3f'), - date_submitted: "2020-09-14T00:00:00.000Z", - incident_id: 2, - source_domain: "example2.com", - url: "http://example2.com" - } - ] - }); - - await seedCollection({ - name: 'users', - database: 'customData', - docs: [ - { - userId: authData.user_id, - roles: ['admin'] - } - ] - }); - - const mutationData = { - query: ` - mutation { - deleteManyQuickadds(filter: { _id: { EQ: "5f5f3e3e3e3e3e3e3e3e3e3e" } }) { - deletedCount - } - } - `, - }; - - const response = await request(url) - .post('/') - .set('Authorization', `Bearer ${authData.access_token}`) - .send(mutationData) - - expect(response.body.data).toMatchObject({ - deleteManyQuickadds: { - deletedCount: 1 - } - }); - - }); - - it(`insertOneQuickadd mutation`, async () => { - - await seedCollection({ - name: 'quickadd', - docs: [] - }); - - const mutationData = { - query: ` - mutation Test($data: QuickAddInsertType!) { - insertOneQuickadd(data: $data) { - _id - date_submitted - incident_id - source_domain - url - } - } - `, - variables: { - data: { - date_submitted: "2020-09-14T00:00:00.000Z", - incident_id: 1, - source_domain: "example.com", - url: "http://example.com" - } - } - }; - - const response = await request(url) - .post('/') - .send(mutationData); - - expect(response.body.data.insertOneQuickadd).toMatchObject({ - date_submitted: '2020-09-14T00:00:00.000Z', - incident_id: 1, - source_domain: 'example.com', - url: 'http://example.com' - }) - - expect(response.statusCode).toBe(200); - }); -}); \ No newline at end of file diff --git a/site/gatsby-site/server/tests/reports.spec.ts b/site/gatsby-site/server/tests/reports.spec.ts deleted file mode 100644 index 37c36c7377..0000000000 --- a/site/gatsby-site/server/tests/reports.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ApolloServer } from "@apollo/server"; -import request from 'supertest'; -import { startTestServer } from "./utils"; - -describe('Reports', () => { - let server: ApolloServer, url: string; - - beforeAll(async () => { - ({ server, url } = await startTestServer()); - }); - - afterAll(async () => { - await server?.stop(); - }); - - it(`report query aliased translations`, async () => { - - const queryData = { - query: ` - query { - report(query: { report_number: 1991 }) { - title - translations_es: translations(input: "es") { - title - } - } - } - `, - }; - - const response = await request(url).post('/').send(queryData); - - expect(response.body.data).toMatchObject({ - report: { - title: expect.any(String), - translations_es: { - title: expect.any(String) - } - } - }) - }); -}); \ No newline at end of file diff --git a/site/gatsby-site/server/tests/utils.ts b/site/gatsby-site/server/tests/utils.ts index d994f838fd..f1b8ab4fab 100644 --- a/site/gatsby-site/server/tests/utils.ts +++ b/site/gatsby-site/server/tests/utils.ts @@ -1,9 +1,10 @@ import { startStandaloneServer } from "@apollo/server/standalone"; import { schema } from "../schema"; -import { context } from "../context"; -import { MongoClient } from "mongodb"; +import { MongoClient, ObjectId } from "mongodb"; import { ApolloServer } from "@apollo/server"; import config from '../config'; +import supertest from 'supertest'; +import * as context from '../context'; export const startTestServer = async () => { @@ -13,7 +14,7 @@ export const startTestServer = async () => { const client = new MongoClient(config.API_MONGODB_CONNECTION_STRING); - const { url } = await startStandaloneServer(server, { context: ({ req }) => context({ req, client }), listen: { port: 0 } }); + const { url } = await startStandaloneServer(server, { context: ({ req }) => context.context({ req, client }), listen: { port: 0 } }); return { server, url } } @@ -38,30 +39,198 @@ export const seedCollection = async ({ name, docs, database = 'aiidprod', drop = } } -interface AuthResponse { - access_token: string; - refresh_token: string; - user_id: string; -} +export const seedUsers = async (users: { userId: string, roles: string[] }[], { drop } = { drop: true }) => { -export const login = async (username: string, password: string) => { - - const response = await fetch(`https://services.cloud.mongodb.com/api/client/v2.0/app/${config.REALM_APP_ID}/auth/providers/local-userpass/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - username, - password, - }) + await seedCollection({ + name: 'users', + database: 'customData', + docs: users, + drop, }); +} - if (!response.ok) { - throw new Error(`Error login in! \n\n ${await response.text()}`); - } +export const makeRequest = async (url: string, data: { variables?: Record, query: string }) => { - const data: AuthResponse = await response.json(); + return supertest(url) + .post('/') + .set('Authorization', `Bearer dummyToken`) + .send(data); +} - return data; +export function serializeId string } }>(obj: T): T { return ({ ...obj, _id: obj._id?.toHexString() }) } + +export function removeId string } }>(obj: T): T { return ({ ...obj, _id: undefined }) } + + +/** + * Interface representing the structure of a fixture for testing api operations. + * + * @template T - The database object type of the main entity being tested. This usually coincides with the graphql object type, but sometimes they may differ like with Incidents. + * @template Y - The generated graphql object type of the main entity being tested. + */ +export interface Fixture { + /** The name of the database collection being tested. */ + name: string; + + /** The graphql query used to perform graphql operations. */ + query: string; + + /** Seed data for the database, organized by collection name. */ + seeds: Record[]>; + + /** + * Configuration for testing the singular field i.e. incident + */ + testSingular: { + /** Filter to identify the document by its ObjectId. */ + filter: { _id: { EQ: ObjectId } }; + + /** Expected result for the query. */ + result: Partial; + }; + + /** + * Configuration for testing the plural field i.e. incidents + */ + testPluralFilter: { + /** Filter to identify the documents by their ObjectId. */ + filter: { _id: { EQ: ObjectId } }; + + /** Expected results for the query. */ + result: Partial[]; + }; + + /** + * Configuration for testing the plural field with sorting. + */ + testPluralSort: { + /** Sorting criteria for the query. */ + sort: unknown; + + /** Expected results for the query. */ + result: Partial[]; + }; + + /** + * Configuration for testing the plural field with pagination. + */ + testPluralPagination: { + /** Pagination parameters: limit and skip. */ + pagination: { limit: number; skip: number }; + + /** Sorting criteria for the query. */ + sort: unknown; + + /** Expected results for the query. */ + result: Partial[]; + }; + + /** + * Configuration for testing the updateOne mutation . + */ + testUpdateOne: { + /** Filter to identify the document by its ObjectId. */ + filter: { _id: { EQ: ObjectId } }; + + /** Partial update to apply to the document. */ + set: Partial; + + /** Expected result for the update operation. */ + result: Partial; + }; + + /** + * Configuration for testing the updateMany mutation. + */ + testUpdateMany: { + /** Filter to identify the documents by various criteria. */ + filter: { [key: string]: { EQ: any } }; + + /** Partial update to apply to the documents. */ + set: Partial; + + /** Expected result for the update operation, including modified and matched counts. */ + result: { modifiedCount: number; matchedCount: number }; + }; + + /** + * Configuration for testing the insertOne mutation. + */ + testInsertOne: { + /** Object to be inserted. */ + insert: Partial; + + /** Expected result for the insert operation. */ + result: Partial; + }; + + /** + * Configuration for testing the insertMany mutation. + */ + testInsertMany: { + /** Objects to be inserted. */ + insert: Partial[]; + + /** Expected result for the insert operation, including inserted IDs. */ + result: { insertedIds: string[] }; + }; + + /** + * Configuration for testing the deleteOne mutation. + */ + testDeleteOne: { + /** Filter to identify the document by its ObjectId. */ + filter: { _id: { EQ: ObjectId } }; + + /** Expected result for the delete operation, including the ID of the deleted document. */ + result: { _id: string }; + }; + + /** + * Configuration for testing the deleteMany mutation. + */ + testDeleteMany: { + /** Filter to identify the documents by various criteria. */ + filter: { [key: string]: { EQ: any } }; + + /** Expected result for the delete operation, including the count of deleted documents. */ + result: { deletedCount: number }; + }; + + /** + * Roles configuration for generated fields and mutations. + */ + roles: { + /** Roles allowed to query the singular field. */ + singular: string[]; + + /** Roles allowed to query the plural field. */ + plural: string[]; + + /** Roles allowed to call the insertOne mutation. */ + insertOne: string[]; + + /** Roles allowed to call the insertMany mutation. */ + insertMany: string[]; + + /** Roles allowed to call the updateOne mutation. */ + updateOne: string[]; + + /** Roles allowed to call the updateMany mutation. */ + updateMany: string[]; + + /** Roles allowed to call the deleteOne mutation. */ + deleteOne: string[]; + + /** Roles allowed to call the deleteMany mutation. */ + deleteMany: string[]; + }; +} + +export const seedFixture = async (seeds: Record[]>) => { + + for (const [collection, docs] of Object.entries(seeds)) { + + await seedCollection({ name: collection, docs }); + } } \ No newline at end of file diff --git a/site/gatsby-site/server/types.ts b/site/gatsby-site/server/types.ts index d691a6b518..6700fca331 100644 --- a/site/gatsby-site/server/types.ts +++ b/site/gatsby-site/server/types.ts @@ -1,4 +1,6 @@ -import { GraphQLObjectType, GraphQLNonNull, GraphQLInt } from 'graphql'; +import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLList } from 'graphql'; +import { ObjectIdScalar } from './scalars'; + export const DeleteManyPayload = new GraphQLObjectType({ name: 'DeleteManyPayload', @@ -7,4 +9,26 @@ export const DeleteManyPayload = new GraphQLObjectType({ type: new GraphQLNonNull(GraphQLInt), }, }, +}); + +export const InsertManyPayload = new GraphQLObjectType({ + name: 'InsertManyPayload', + fields: { + insertedIds: { + type: new GraphQLNonNull(new GraphQLList(ObjectIdScalar)), + }, + }, +}); + + +export const UpdateManyPayload = new GraphQLObjectType({ + name: 'UpdateManyPayload', + fields: { + modifiedCount: { + type: new GraphQLNonNull(GraphQLInt), + }, + matchedCount: { + type: new GraphQLNonNull(GraphQLInt), + }, + }, }); \ No newline at end of file diff --git a/site/gatsby-site/server/utils.ts b/site/gatsby-site/server/utils.ts new file mode 100644 index 0000000000..8e8479a468 --- /dev/null +++ b/site/gatsby-site/server/utils.ts @@ -0,0 +1,156 @@ +import { GraphQLFieldConfigMap, GraphQLList, GraphQLObjectType } from "graphql"; +import { getGraphQLInsertType, getGraphQLQueryArgs, getGraphQLUpdateArgs, getMongoDbQueryResolver, getMongoDbUpdateResolver } from "graphql-to-mongodb"; +import { Context } from "./interfaces"; +import capitalize from 'lodash/capitalize'; +import { DeleteManyPayload, InsertManyPayload, UpdateManyPayload } from "./types"; + + +export function pluralize(s: string) { + return s.endsWith('s') ? s : s + 's'; +} + +export function singularize(s: string) { + return s.endsWith('s') ? s.slice(0, -1) : s; +} + +export function generateQueryFields({ collectionName, databaseName = 'aiidprod', Type, }: { collectionName: string, databaseName?: string, Type: GraphQLObjectType }): GraphQLFieldConfigMap { + + const singularName = singularize(collectionName); + const pluralName = pluralize(collectionName); + + return { + [`${singularName}`]: { + type: Type, + args: getGraphQLQueryArgs(Type), + resolve: getMongoDbQueryResolver(Type, async (filter, projection, options, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const item = await collection.findOne(filter, options); + + return item; + }), + }, + + [`${pluralName}`]: { + type: new GraphQLList(Type), + args: getGraphQLQueryArgs(Type), + resolve: getMongoDbQueryResolver(Type, async (filter, projection, options, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const items = await collection.find(filter, options).toArray(); + + return items; + }), + }, + } +} + +export function generateMutationFields({ collectionName, databaseName = 'aiidprod', Type, }: { collectionName: string, databaseName?: string, Type: GraphQLObjectType }): GraphQLFieldConfigMap { + + const singularName = capitalize(singularize(collectionName)); + const pluralName = capitalize(pluralize(collectionName)); + + return { + [`deleteOne${singularName}`]: { + type: Type, + args: getGraphQLQueryArgs(Type), + resolve: getMongoDbQueryResolver(Type, async (filter, projection, options, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const target = await collection.findOne(filter); + + await collection.deleteOne(filter); + + return target; + }), + }, + + [`deleteMany${pluralName}`]: { + type: DeleteManyPayload, + args: getGraphQLQueryArgs(Type), + resolve: getMongoDbQueryResolver(Type, async (filter, projection, options, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const result = await collection.deleteMany(filter); + + return { deletedCount: result.deletedCount! }; + }, { + differentOutputType: true, + }), + }, + + + [`insertOne${singularName}`]: { + type: Type, + args: { data: { type: getGraphQLInsertType(Type) } }, + resolve: async (_: unknown, { data }, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const result = await collection.insertOne(data); + + const inserted = await collection.findOne({ _id: result.insertedId }); + + return inserted; + }, + }, + + + [`insertMany${pluralName}`]: { + type: InsertManyPayload, + args: { data: { type: new GraphQLList(getGraphQLInsertType(Type)) } }, + resolve: async (_: unknown, { data }, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const result = await collection.insertMany(data); + + return { insertedIds: Object.values(result.insertedIds) }; + }, + }, + + [`updateOne${singularName}`]: { + type: Type, + args: getGraphQLUpdateArgs(Type), + resolve: getMongoDbUpdateResolver(Type, async (filter, update, options, projection, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + await collection.updateOne(filter, update, options); + + const updated = await collection.findOne(filter); + + return updated; + }), + }, + + [`updateMany${pluralName}`]: { + type: UpdateManyPayload, + args: getGraphQLUpdateArgs(Type), + resolve: getMongoDbUpdateResolver(Type, async (filter, update, options, projection, obj, args, context: Context) => { + + const db = context.client.db(databaseName); + const collection = db.collection(collectionName); + + const result = await collection.updateMany(filter, update, options); + + return { modifiedCount: result.modifiedCount!, matchedCount: result.matchedCount }; + }, { + differentOutputType: true, + validateUpdateArgs: true, + }), + }, + } +} + diff --git a/site/gatsby-site/src/components/Layout.js b/site/gatsby-site/src/components/Layout.js index ad1a4a6506..53422d11e7 100644 --- a/site/gatsby-site/src/components/Layout.js +++ b/site/gatsby-site/src/components/Layout.js @@ -4,10 +4,13 @@ import config from '../../config.js'; import Footer from './layout/Footer'; import Header from './ui/Header'; import { useLayoutContext } from 'contexts/LayoutContext'; +import { useMenuContext } from '../contexts/MenuContext'; const Layout = ({ children, className, sidebarCollapsed = false, location }) => { const { rightSidebar } = useLayoutContext(); + const { isCollapsed } = useMenuContext(); + return ( <>
@@ -26,10 +29,13 @@ const Layout = ({ children, className, sidebarCollapsed = false, location }) => )}
{children}
diff --git a/site/gatsby-site/src/graphql/quickadd.js b/site/gatsby-site/src/graphql/quickadd.js index daa20b2745..cd2446ba0a 100644 --- a/site/gatsby-site/src/graphql/quickadd.js +++ b/site/gatsby-site/src/graphql/quickadd.js @@ -1,7 +1,7 @@ import gql from 'graphql-tag'; export const FIND_QUICKADD = gql` - query AllQuickAdd($filter: QuickAddFilterType!) { + query AllQuickAdd($filter: QuickaddFilterType!) { quickadds(filter: $filter) { _id date_submitted @@ -12,7 +12,7 @@ export const FIND_QUICKADD = gql` `; export const DELETE_QUICKADD = gql` - mutation DeleteOneQuickAdd($filter: QuickAddFilterType) { + mutation DeleteOneQuickAdd($filter: QuickaddFilterType) { deleteManyQuickadds(filter: $filter) { deletedCount } @@ -20,7 +20,7 @@ export const DELETE_QUICKADD = gql` `; export const INSERT_QUICKADD = gql` - mutation InsertQuickAdd($data: QuickAddInsertType!) { + mutation InsertQuickAdd($data: QuickaddInsertType!) { insertOneQuickadd(data: $data) { _id } diff --git a/site/gatsby-site/src/tailwind.css b/site/gatsby-site/src/tailwind.css index e2e338b3f9..69634de60a 100644 --- a/site/gatsby-site/src/tailwind.css +++ b/site/gatsby-site/src/tailwind.css @@ -774,4 +774,14 @@ .editors-dropdown *[data-testid="flowbite-tooltip-target"] button span { @apply w-full justify-between !important; } + + .w-content-sidebar, .max-w-content-sidebar { + width: calc(100% - 256px); + max-width: calc(100% - 256px); + } + + .w-content, .max-w-content { + width: calc(100% - 56px); + max-width: calc(100% - 56px); + } } diff --git a/site/gatsby-site/src/templates/landingPage.js b/site/gatsby-site/src/templates/landingPage.js index 5bf4e61b47..d595374fb6 100644 --- a/site/gatsby-site/src/templates/landingPage.js +++ b/site/gatsby-site/src/templates/landingPage.js @@ -69,7 +69,7 @@ const LandingPage = (props) => { return ( // Tailwind has max-w-6xl but no plain w-6xl... 72rem = 6xl -
+
@@ -81,8 +81,7 @@ const LandingPage = (props) => { {latestIncidents.length > 0 && (
- {/* https://github.com/responsible-ai-collaborative/aiid/issues/2956 */} -
+