Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add /import endpoint to support bulk importing json data #390

Merged
merged 19 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .run/Template Jest.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="JavaScriptTestRunnerJest">
<node-interpreter value="project" />
<node-options value="--experimental-vm-modules" />
<jest-options value="--runInBand" />
<envs />
<scope-kind value="ALL" />
<method v="2" />
</configuration>
</component>
29 changes: 24 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
],
"skipFiles": [
"<node_internals>/**"
],
]
},
{
"type": "node",
Expand All @@ -28,7 +28,7 @@
],
"skipFiles": [
"<node_internals>/**"
],
]
},
{
"type": "node",
Expand All @@ -41,7 +41,7 @@
],
"skipFiles": [
"<node_internals>/**"
],
]
},
{
"type": "node",
Expand All @@ -55,7 +55,7 @@
"outFiles": [
"${workspaceFolder}/build/**/*.js"
],
"console": "integratedTerminal",
"console": "integratedTerminal"
},
{
"type": "node",
Expand Down Expand Up @@ -90,7 +90,26 @@
"history"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"name": "vscode-jest-tests.v2",
"request": "launch",
"env": {
"NODE_OPTIONS": "--experimental-vm-modules"
},
"args": [
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand",
"--watchAll=false",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
"javascript.format.enable": false,
"javascript.format.semicolons": "remove",
"typescript.format.enable": false
}
}
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"supertest": "^6.3.3",
"ts-jest": "^29.0.5",
"ts-standard": "^12.0.0",
"typescript": "4.9.5"
"typescript": "4.9.5",
"wait-for-expect": "^3.0.2"
},
"dependencies": {
"@babel/runtime": "^7.17.2",
Expand All @@ -36,15 +37,18 @@
"@turf/helpers": "^6.5.0",
"@types/uuid": "^8.3.3",
"apollo-datasource-mongodb": "^0.5.4",
"apollo-server": "^3.9.0",
"apollo-server-core": "^3.13.0",
"apollo-server-express": "^3.13.0",
"auth0": "^3.4.0",
"axios": "^1.3.6",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"dot-object": "^2.1.4",
"dotenv": "^10.0.0",
"express": "^4.18.2",
"glob": "^10.2.2",
"graphql": "^16.5.0",
"graphql": "^16.8.1",
"graphql-middleware": "^6.1.31",
"graphql-shield": "^7.5.0",
"i18n-iso-countries": "^7.5.0",
Expand Down Expand Up @@ -104,4 +108,4 @@
"engines": {
"node": ">=16.14.0"
}
}
}
17 changes: 11 additions & 6 deletions src/__tests__/areas.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { ApolloServer } from 'apollo-server'
import { ApolloServer } from 'apollo-server-express'
import muuid from 'uuid-mongodb'
import { jest } from '@jest/globals'
import MutableAreaDataSource from '../model/MutableAreaDataSource.js'
import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js'
import { AreaType } from '../db/AreaTypes.js'
import { OrgType, OrganizationType, OrganizationEditableFieldsType } from '../db/OrganizationTypes.js'
import { OrganizationEditableFieldsType, OrganizationType, OrgType } from '../db/OrganizationTypes.js'
import { queryAPI, setUpServer } from '../utils/testUtils.js'
import { muuidToString } from '../utils/helpers.js'
import { InMemoryDB } from '../utils/inMemoryDB.js'
import express from 'express'

jest.setTimeout(60000)

describe('areas API', () => {
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let inMemoryDB
let app: express.Application
let inMemoryDB: InMemoryDB

// Mongoose models for mocking pre-existing state.
let areas: MutableAreaDataSource
Expand All @@ -24,7 +27,7 @@ describe('areas API', () => {
let wa: AreaType

beforeAll(async () => {
({ server, inMemoryDB } = await setUpServer())
({ server, inMemoryDB, app } = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
Expand Down Expand Up @@ -77,7 +80,8 @@ describe('areas API', () => {
query: areaQuery,
operationName: 'area',
variables: { input: wa.metadata.area_id },
userUuid
userUuid,
app
})

expect(response.statusCode).toBe(200)
Expand All @@ -92,7 +96,8 @@ describe('areas API', () => {
query: areaQuery,
operationName: 'area',
variables: { input: ca.metadata.area_id },
userUuid
userUuid,
app
})
expect(response.statusCode).toBe(200)
const areaResult = response.body.data.area
Expand Down
137 changes: 137 additions & 0 deletions src/__tests__/bulkImport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {ApolloServer} from "apollo-server-express";
import muuid from "uuid-mongodb";
import express from "express";
import {InMemoryDB} from "../utils/inMemoryDB.js";
import {queryAPI, setUpServer} from "../utils/testUtils.js";
import {muuidToString} from "../utils/helpers.js";
import exampleImportData from './import-example.json' assert {type: 'json'};
import {AreaType} from "../db/AreaTypes.js";
import {BulkImportResultType} from "../db/BulkImportTypes.js";
import MutableClimbDataSource from "../model/MutableClimbDataSource.js";
import BulkImportDataSource from "../model/BulkImportDataSource.js";

describe('bulkImportAreas', () => {
const query = `
mutation bulkImportAreas($input: BulkImportInput!) {
bulkImportAreas(input: $input) {
addedAreas {
uuid
metadata {
area_id
}
}
updatedAreas {
uuid
metadata {
area_id
}
}
addedOrUpdatedClimbs {
id
}
}
}
`

let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let app: express.Application
let inMemoryDB: InMemoryDB
let testArea: AreaType

let bulkImport: BulkImportDataSource
let climbs: MutableClimbDataSource

beforeAll(async () => {
({server, inMemoryDB, app} = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
userUuid = muuidToString(user)

bulkImport = BulkImportDataSource.getInstance()
climbs = MutableClimbDataSource.getInstance()
})

beforeEach(async () => {
await inMemoryDB.clear()
await bulkImport.addCountry('usa')
testArea = await bulkImport.addArea(user, "Test Area", null, "us")
})

afterAll(async () => {
await server.stop()
await inMemoryDB.close()
})

it('should return 403 if no user', async () => {
const res = await queryAPI({
app,
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.statusCode).toBe(200)
expect(res.body.errors[0].message).toBe('Not Authorised!')
})

it('should return 403 if user is not an editor', async () => {
const res = await queryAPI({
app,
userUuid,
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.statusCode).toBe(200)
expect(res.body.errors[0].message).toBe('Not Authorised!')
})

it('should return 200 if user is an editor', async () => {
const res = await queryAPI({
app,
userUuid,
roles: ['editor'],
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.status).toBe(200)
})

it('should import data', async () => {
const res = await queryAPI({
app,
userUuid,
roles: ['editor'],
query,
operationName: 'bulkImportAreas',
variables: {
input: {
areas: [
...exampleImportData.areas,
{
uuid: testArea.metadata.area_id,
areaName: "Updated Test Area",
}
]
}
}
});
expect(res.body.errors).toBeFalsy()

const result = res.body.data.bulkImportAreas as BulkImportResultType
expect(result.addedAreas.length).toBe(4)

const committedAreas = await Promise.all(result.addedAreas.map((area) => bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id))));
expect(committedAreas.length).toBe(4);

const committedClimbs = await Promise.all(result.addedOrUpdatedClimbs.map((climb) => climbs.findOneClimbByMUUID(climb._id)));
expect(committedClimbs.length).toBe(2);

const updatedAreas = await Promise.all(result.updatedAreas.map((area) => bulkImport.findOneAreaByUUID(muuid.from(area.metadata.area_id))));
expect(updatedAreas.length).toBe(1);
expect(updatedAreas[0].area_name).toBe("Updated Test Area");
})
});
14 changes: 9 additions & 5 deletions src/__tests__/history.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { ApolloServer } from 'apollo-server'
import { ApolloServer } from 'apollo-server-express'
import muuid from 'uuid-mongodb'
import { jest } from '@jest/globals'
import MutableAreaDataSource from '../model/MutableAreaDataSource.js'
import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js'
import MutableClimbDataSource from '../model/MutableClimbDataSource.js'
import { AreaType } from '../db/AreaTypes.js'
import { OrgType, OrganizationType } from '../db/OrganizationTypes.js'
import { OrganizationType, OrgType } from '../db/OrganizationTypes.js'
import { muuidToString } from '../utils/helpers.js'
import { queryAPI, setUpServer } from '../utils/testUtils.js'
import { InMemoryDB } from '../utils/inMemoryDB.js'
import express from 'express'

jest.setTimeout(60000)

describe('history API', () => {
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let inMemoryDB
let app: express.Application
let inMemoryDB: InMemoryDB

// Mongoose models for mocking pre-existing state.
let areas: MutableAreaDataSource
let organizations: MutableOrganizationDataSource
let climbs: MutableClimbDataSource

beforeAll(async () => {
({ server, inMemoryDB } = await setUpServer())
({ server, inMemoryDB, app } = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
Expand Down Expand Up @@ -107,7 +110,8 @@ describe('history API', () => {
const resp = await queryAPI({
query: QUERY_RECENT_CHANGE_HISTORY,
variables: { filter: {} },
userUuid
userUuid,
app
})
expect(resp.statusCode).toBe(200)
const histories = resp.body.data.getChangeHistory
Expand Down
Loading
Loading