Skip to content

Commit

Permalink
feat(api): add /import endpoint to support bulk importing json data (#…
Browse files Browse the repository at this point in the history
…390)

* feat(db): add json bulk import method
* feat(import): add required default fields
* docs: update db schema description of ancestors
* feat(server): migrate to apollo-server-express
  • Loading branch information
Silthus committed Feb 15, 2024
1 parent 9c51732 commit d912a16
Show file tree
Hide file tree
Showing 49 changed files with 1,523 additions and 383 deletions.
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

0 comments on commit d912a16

Please sign in to comment.