Skip to content

Commit

Permalink
feat(import): rework bulk import to use gql api
Browse files Browse the repository at this point in the history
  • Loading branch information
Silthus committed Feb 11, 2024
1 parent d09167a commit 6addf8c
Show file tree
Hide file tree
Showing 18 changed files with 502 additions and 395 deletions.
83 changes: 54 additions & 29 deletions src/__tests__/import.test.ts → src/__tests__/bulkImport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@ import {muuidToString} from "../utils/helpers.js";
import MutableAreaDataSource from "../model/MutableAreaDataSource.js";
import exampleImportData from './import-example.json' assert {type: 'json'};
import {AreaType} from "../db/AreaTypes.js";
import {BulkImportResult} from "../db/import/json/import-json";
import {BulkImportResultType} from "../db/BulkImportTypes.js";
import MutableClimbDataSource from "../model/MutableClimbDataSource.js";

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

describe('/import', () => {
const endpoint = '/import'
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
Expand All @@ -19,18 +35,21 @@ describe('/import', () => {
let testArea: AreaType

let areas: MutableAreaDataSource
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)

areas = MutableAreaDataSource.getInstance()
climbs = MutableClimbDataSource.getInstance()
})

beforeEach(async () => {
await inMemoryDB.clear()
areas = MutableAreaDataSource.getInstance()
await areas.addCountry('usa')
testArea = await areas.addArea(user, "Test Area", null, "us")
})
Expand All @@ -43,63 +62,69 @@ describe('/import', () => {
it('should return 403 if no user', async () => {
const res = await queryAPI({
app,
endpoint,
body: exampleImportData
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.status).toBe(403)
expect(res.text).toBe('Forbidden')
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,
endpoint,
userUuid,
body: exampleImportData
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.status).toBe(403)
expect(res.text).toBe('Forbidden')
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,
endpoint,
userUuid,
roles: ['editor'],
body: exampleImportData
query,
operationName: 'bulkImportAreas',
variables: {input: exampleImportData}
})
expect(res.status).toBe(200)
})

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

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

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

const committedClimbs = await Promise.all(result.climbIds.map((id: string) => areas.findOneClimbByUUID(muuid.from(id))));
const committedClimbs = await Promise.all(result.addedOrUpdatedClimbs.map((climb) => climbs.findOneClimbByMUUID(climb._id)));
expect(committedClimbs.length).toBe(2);

const updatedAreas = await Promise.all(result.updatedAreaIds.map((areaId: any) => areas.findOneAreaByUUID(muuid.from(areaId))));
const updatedAreas = await Promise.all(result.updatedAreas.map((area) => areas.findOneAreaByUUID(area.metadata.area_id)));
expect(updatedAreas.length).toBe(1);
expect(updatedAreas[0].area_name).toBe("Updated Test Area");
})
Expand Down
100 changes: 25 additions & 75 deletions src/__tests__/import-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,117 +9,67 @@
"children": [
{
"areaName": "Indian Creek",
"description": "Indian Creek is a crack climbing mecca in the southeastern region of Utah, USA. Located within the [Bears Ears National Monument](https://en.wikipedia.org/wiki/Bears_Ears_National_Monument).",
"lng": -109.5724044642857,
"lat": 38.069429035714286,
"children": [
{
"areaName": "Supercrack Buttress",
"gradeContext": "US",
"description": "",
"lng": -109.54552,
"lat": 38.03635,
"bbox": [
-109.54609091005857,
38.03590033981814,
-109.54494908994141,
38.03679966018186
],
"climbs": [
{
"name": "The Key Flake",
"grade": "5.10",
"fa": "unknown",
"type": {
"disciplines": {
"trad": true
},
"safety": "UNSPECIFIED",
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"left_right_index": 1
},
"content": {
"description": "Cool off-width that requires off-width and face skills.",
"protection": "Anchors hidden up top. Need 80m to make it all the way down.",
"location": "Opposite keyhole flake. Obvious right leaning offwidth that starts atop 20 ft boulder."
}
"lng": -109.54552,
"lat": 38.03635,
"leftRightIndex": 1,
"description": "Cool off-width that requires off-width and face skills.",
"protection": "Anchors hidden up top. Need 80m to make it all the way down.",
"location": "Opposite keyhole flake. Obvious right leaning offwidth that starts atop 20 ft boulder."
},
{
"name": "Incredible Hand Crack",
"grade": "5.10",
"fa": "Rich Perch, John Bragg, Doug Snively, and Anne Tarver, 1978",
"type": {
"disciplines": {
"trad": true
},
"safety": "UNSPECIFIED",
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"left_right_index": 2
},
"content": {
"description": "Route starts at the top of the trail from the parking lot to Supercrack Buttress.",
"protection": "Cams from 2-2.5\". Heavy on 2.5\" (#2 Camalot)",
"location": ""
},
"leftRightIndex": 2,
"description": "Route starts at the top of the trail from the parking lot to Supercrack Buttress.",
"protection": "Cams from 2-2.5\". Heavy on 2.5\" (#2 Camalot)",
"pitches": [
{
"pitchNumber": 1,
"grade": "5.10",
"disciplines": {
"trad": true
},
"length": 100,
"boltsCount": 0,
"description": "A classic hand crack that widens slightly towards the top. Requires a range of cam sizes. Sustained and excellent quality."
},
{
"pitchNumber": 2,
"grade": "5.9",
"disciplines": {
"trad": true
},
"length": 30,
"boltsCount": 0,
"description": "Easier climbing with good protection. Features a mix of crack sizes. Shorter than the first pitch but equally enjoyable."
}
]
}
],
"gradeContext": "US",
"metadata": {
"isBoulder": false,
"isDestination": false,
"leaf": true,
"lnglat": {
"type": "Point",
"coordinates": [
-109.54552,
38.03635
]
},
"bbox": [
-109.54609091005857,
38.03590033981814,
-109.54494908994141,
38.03679966018186
]
},
"content": {
"description": ""
}
}
],
"metadata": {
"lnglat": {
"type": "Point",
"coordinates": [
-109.5724044642857,
38.069429035714286
]
}
},
"content": {
"description": "Indian Creek is a crack climbing mecca in the southeastern region of Utah, USA. Located within the [Bears Ears National Monument](https://en.wikipedia.org/wiki/Bears_Ears_National_Monument)."
}
]
}
]
}
Expand Down
11 changes: 0 additions & 11 deletions src/auth/local-dev/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,3 @@ export const localDevBypassAuthContext = (() => {
return { user }
}
})()

export const localDevBypassAuthMiddleware = async (req, res, next): Promise<any> => {
req.user = {
roles: ['user_admin', 'org_admin', 'editor'],
uuid: muuid.v4(),
isBuilder: false
}
req.userId = req.user.uuid
req.token = 'local-dev-bypass'
next()
}
13 changes: 0 additions & 13 deletions src/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,6 @@ export const createContext = async ({ req }): Promise<any> => {
}
}

export const authMiddleware = async (req, res, next): Promise<void> => {
try {
const { user, token } = await validateTokenAndExtractUser(req)
req.user = user
req.userId = user.uuid
req.token = token
next()
} catch (e) {
logger.error(`Can't verify JWT token ${e.toString() as string}`)
res.status(401).send('Unauthorized')
}
}

async function validateTokenAndExtractUser (req: Request): Promise<{ user: AuthUserType, token: string }> {
const { headers } = req
// eslint-disable-next-line @typescript-eslint/dot-notation
Expand Down
5 changes: 3 additions & 2 deletions src/auth/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { shield, allow, and, or } from 'graphql-shield'
import { isEditor, isUserAdmin, isOwner, isValidEmail, isMediaOwner } from './rules.js'
import { allow, and, or, shield } from 'graphql-shield'
import { isEditor, isMediaOwner, isOwner, isUserAdmin, isValidEmail } from './rules.js'

const permissions = shield({
Query: {
Expand All @@ -13,6 +13,7 @@ const permissions = shield({
updateArea: isEditor,
updateClimbs: isEditor,
deleteClimbs: isEditor,
bulkImportAreas: isEditor,
updateUserProfile: and(isOwner, isValidEmail),
addEntityTag: or(isMediaOwner, isUserAdmin),
removeEntityTag: or(isMediaOwner, isUserAdmin),
Expand Down
9 changes: 0 additions & 9 deletions src/auth/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ export const isEditor = rule()(async (parent, args, ctx, info) => {
return _hasUserUuid(ctx) && ctx.user.roles.includes('editor')
})

export const hasEditorRoleMiddleware = async (req, res, next): Promise<void> => {
const roles: string[] = req.user?.roles ?? []
if (_hasUserUuid(req) && roles.includes('editor')) {
next()
} else {
res.status(403).send('Forbidden')
}
}

export const isUserAdmin = rule()(async (parent, args, ctx, info) => {
return _hasUserUuid(ctx) && ctx.user.roles.includes('user_admin')
})
Expand Down
Loading

0 comments on commit 6addf8c

Please sign in to comment.