Skip to content

Commit

Permalink
Integrate geoindex into the app (solidcouch#120)
Browse files Browse the repository at this point in the history
but make sure that the app still falls back to slow querying when
geoindex is not set up, or when it is not available.

This is developed in parallel to the geoindex service
(https://github.com/solidcouch/geoindex)

Features:
- Notify geoindex when accommodation is created, updated, or deleted
- Use geoindex to display accommodations on the map, when available

New environment variables:
- `REACT_APP_GEOINDEX` - webId of the geoindex, if available

Improves solidcouch#64
Fixes solidcouch#70
  • Loading branch information
mrkvon committed Oct 4, 2024
1 parent a2c2406 commit 2073f82
Show file tree
Hide file tree
Showing 20 changed files with 790 additions and 126 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
REACT_APP_EMAIL_NOTIFICATIONS_TYPE: ${{ vars.EMAIL_NOTIFICATIONS_TYPE }}
REACT_APP_EMAIL_NOTIFICATIONS_SERVICE: ${{ vars.EMAIL_NOTIFICATIONS_SERVICE }}
REACT_APP_EMAIL_NOTIFICATIONS_IDENTITY: ${{ vars.EMAIL_NOTIFICATIONS_IDENTITY }}
REACT_APP_GEOINDEX: ${{ vars.GEOINDEX }}
BASE_URL: ${{ vars.BASE_URL }} # base url for clientid.jsonld

- name: Add CNAME
Expand Down
112 changes: 109 additions & 3 deletions cypress/e2e/accommodation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,16 @@ describe('accommodations offered by person', () => {
})
})

// go to offers page
beforeEach(() => {
/**
* Go to offers page
*/
const goToOffers = () => {
cy.contains('a', 'host').click()
cy.location().its('pathname').should('equal', '/host/offers')
})
}

// go to offers page
beforeEach(goToOffers)

it('should be able to navigate to my offers page from user menu', () => {
// through header, open edit-profile page
Expand Down Expand Up @@ -167,4 +172,105 @@ describe('accommodations offered by person', () => {
.and('not.contain.text', 'accommodation 2')
.and('contain.text', 'accommodation 3')
})

context('geoindex is set up', () => {
const geoindexService = 'https://geoindex.example.com/profile/card#bot'
beforeEach(() => {
cy.updateAppConfig({ geoindexService }, { waitForContent: 'travel' })
goToOffers()
})

it("should send info about creation into geoindex's inbox", () => {
cy.get<Person>('@me').then(me => {
// test creation
cy.intercept('POST', new URL('/inbox', geoindexService).toString(), {
statusCode: 201,
}).as('geoindexInbox')
cy.get('li[class^=MyOffers_accommodation]').should('have.length', 3)
cy.contains('button', 'Add Accommodation').click()

// move the map
moveFormMap(['l', 'u', 'i', 'l', 'l'])

// write some description
cy.get('textarea[name=description]').type(
'This is a new description in English',
)
cy.contains('button', 'Submit').click()

cy.testToast('Creating accommodation')
cy.testToast('Notifying indexing service')
cy.testAndCloseToast('Accommodation created')
cy.testAndCloseToast('Accommodation added to indexing service')

cy.wait('@geoindexInbox')
.its('request.body')
.should('containSubset', {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
actor: { type: 'Person', id: me.webId },
object: { type: 'Document' },
})
})
})

it("should send info about update into geoindex's inbox", () => {
cy.get<Person>('@me').then(me => {
// test update
cy.intercept('POST', new URL('/inbox', geoindexService).toString(), {
statusCode: 200,
}).as('geoindexInbox')

cy.contains('li[class^=MyOffers_accommodation]', 'accommodation 2')
.contains('button', 'Edit')
.click()
cy.get('textarea[name="description"]')
.clear()
.type('changed second accommodation')

moveFormMap(['o', 'o', 'l', 'd'])

cy.contains('button', 'Submit').click()
cy.testToast('Updating accommodation')
cy.testToast('Notifying indexing service')
cy.testAndCloseToast('Accommodation updated')
cy.testAndCloseToast('Accommodation updated in indexing service')

cy.wait('@geoindexInbox')
.its('request.body')
.should('containSubset', {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: { type: 'Person', id: me.webId },
object: { type: 'Document' },
})
})
})

it("should send info about deletion into geoindex's inbox", () => {
cy.get<Person>('@me').then(me => {
// test deletion
cy.intercept('POST', new URL('/inbox', geoindexService).toString(), {
statusCode: 204,
}).as('geoindexInbox')

cy.contains('li[class^=MyOffers_accommodation]', 'accommodation 2')
.contains('button', 'Delete')
.click()
cy.testToast('Deleting accommodation')
cy.testToast('Notifying indexing service')
cy.testAndCloseToast('Accommodation deleted')
cy.testAndCloseToast('Accommodation removed from indexing service')

cy.wait('@geoindexInbox')
.its('request.body')
.should('containSubset', {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Delete',
actor: { type: 'Person', id: me.webId },
object: { type: 'Document' },
})
})
})
})
})
130 changes: 121 additions & 9 deletions cypress/e2e/map.cy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { range } from 'lodash'
import ngeohash from 'ngeohash'
import { Person } from '../support/commands'
import { UserConfig } from '../support/css-authentication'
import { AccommodationConfig } from '../support/setup'

const users = range(10).map(i => (i === 0 ? 'me' : `user${i}`))

type PersonAccommodation = {
person: Person
accommodation: AccommodationConfig
geohash: string
}

describe('Map of accommodation offers', () => {
// create people and their accommodations
beforeEach(() => {
const accommodations: PersonAccommodation[] = []

users.forEach((tag, i) => {
cy.createPerson({
name: `Name ${i}`,
Expand All @@ -18,9 +27,19 @@ describe('Map of accommodation offers', () => {
description: { en: `accommodation of ${tag}` },
location:
i % 2 === 1 ? [5 * i + 5, 10 * i - 10] : [-10 * i + 40, 5 * i - 90],
}).as(`${tag}Accommodation`)
})
.as(`${tag}Accommodation`)
.then(accommodation => {
accommodations.push({
person,
accommodation,
geohash: ngeohash.encode(...accommodation.location, 10),
})
})
})
})

cy.wrap(accommodations).as('accommodations')
})

// sign in
Expand All @@ -37,17 +56,24 @@ describe('Map of accommodation offers', () => {
cy.location().its('pathname').should('equal', '/travel/search')
})

it('should show offers of community folks', () => {
const testOffers = () => {
cy.get('.leaflet-marker-icon').should('have.length', 10)
})
}
it('should show offers of community folks', testOffers)

it("[on click offer] should show detail with person's photo, name, offer description, link to write message", () => {
const testClickOffer = () => {
// TODO test person's photo
cy.get('.leaflet-marker-icon[alt="Accommodation offer from Name 2"]')
.first()
.click()
// cy.get('.leaflet-marker-icon[alt="Accommodation offer from Name 2"]')
// .first()
// .click()

cy.get<AccommodationConfig>('@user2Accommodation').then(accommodation => {
cy.get(
`.leaflet-marker-icon.geohash-${ngeohash.encode(
...accommodation.location,
10,
)}`,
).click()
cy.location()
.its('search')
.should('equal', `?hosting=${encodeURIComponent(accommodation.id)}`)
Expand All @@ -66,7 +92,93 @@ describe('Map of accommodation offers', () => {
`/messages/${encodeURIComponent(user.webId)}`,
)
})
})
}
it(
"[on click offer] should show detail with person's photo, name, offer description, link to write message",
testClickOffer,
)

context('geoindex is set up', () => {
const geoindexService = 'https://geoindex.example.com/profile/card#bot'
beforeEach(() => {
cy.updateAppConfig({ geoindexService }, { waitForContent: 'travel' })
})

it('should fetch offers in the displayed area using geoindex', () => {
cy.get<PersonAccommodation[]>('@accommodations').then(accommodations => {
cy.intercept(
'GET',
new URL('/query?object="*"', geoindexService).toString(),
req => {
expect(typeof req.query.object).to.equal('string')
const geohash = (req.query.object as string).replaceAll('"', '')

req.reply({
statusCode: 200,
body: getGeohashQueryResponse(geohash, accommodations),
})
},
).as('geoindexQuery')
cy.contains('a', 'travel').click()
cy.location().its('pathname').should('equal', '/travel/search')

const queries = '0123456789bcdefghjkmnpqrstuvwxyz'
.split('')
.map(c => `"${c}"`)

cy.wait('@geoindexQuery')
.its('request.query.object')
.should('be.oneOf', queries)

testOffers()
testClickOffer()
})
})

it('when the geoindex fails, it should fall back to the slow querying', () => {
cy.get<PersonAccommodation[]>('@accommodations').then(() => {
cy.intercept(
{
method: 'GET',
url: new URL('/query?object="*"', geoindexService).toString(),
},
{ forceNetworkError: true },
).as('geoindexQuery')
cy.contains('a', 'travel').click()
cy.location().its('pathname').should('equal', '/travel/search')

it('should fetch offers in the displayed area only (requires indexing)')
const queries = '0123456789bcdefghjkmnpqrstuvwxyz'
.split('')
.map(c => `"${c}"`)

cy.wait('@geoindexQuery')
.its('request.query.object')
.should('be.oneOf', queries)

testOffers()
testClickOffer()
})
})
})
})

/**
* Given a geohash and an array of accommodations, return turtle that only contains those located within the geohash, just like the geoindex service would respond
*/
const getGeohashQueryResponse = (
geohash: string,
accommodations: PersonAccommodation[],
) => {
const relevantAccommodations = accommodations.filter(a =>
a.geohash.startsWith(geohash),
)

const body = relevantAccommodations
.map(
a =>
`<${a.accommodation.id}> <https://example.com/ns#geohash> "${geohash}", "${a.geohash}".`,
)
.join('\n')

return body
}
1 change: 1 addition & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ You'll find [configurable options](#options) for the application here. We use en
- [default CreateReactApp options](https://create-react-app.dev/docs/advanced-configuration)
- `BASE_URL` - this is base url for ClientID in ./public/clientid.jsonld, it's disabled in development environment by default (dynamic clientID is used), defaults to http://localhost:3000 in development, and https://app.solidcouch.org for build
- `REACT_APP_ENABLE_DEV_CLIENT_ID` - enable static ClientID in development environment (see also `BASE_URL` option). If you set this option, you'll only be able to sign in with Solid Pod running on localhost! (dynamic clientID will be used by default)
- `REACT_APP_GEOINDEX` - webId of the geoindex, if available

## Usage

Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
"@comunica/query-sparql-link-traversal": "^0.1.0",
"@hookform/resolvers": "^3.3.4",
"@inrupt/solid-client-authn-browser": "^1.17.2",
"@ldhop/core": "^0.0.1-alpha.15",
"@ldhop/react": "^0.0.1-alpha.15",
"@ldhop/core": "^0.0.1-alpha.16",
"@ldhop/react": "^0.0.1-alpha.16",
"@ldo/ldo": "^0.0.1-alpha.23",
"@rdfjs/types": "^1.1.0",
"@reduxjs/toolkit": "^1.9.2",
"@szhsin/react-menu": "^3.4.0",
"@tanstack/react-query": "^5.28.4",
"@tanstack/react-query": "^5.59.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@turtlesocks/react-leaflet.locatecontrol": "^0.1.1",
"@types/he": "^1.2.3",
"@types/jest": "^27.0.1",
"@types/ngeohash": "^0.6.8",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
Expand All @@ -39,6 +40,7 @@
"leaflet.locatecontrol": "^0.79.0",
"lodash": "^4.17.21",
"n3": "^1.16.3",
"ngeohash": "^0.6.3",
"parse-link-header": "^2.0.0",
"rdf-namespaces": "^1.10.1",
"rdflib": "^2.2.32",
Expand Down Expand Up @@ -82,7 +84,7 @@
"build:ldo": "ldo build --input src/shapes --output src/ldo",
"postbuild:ldo": "yarn format",
"cy:dev": "concurrently --kill-others \"yarn cy:dev:app\" \"yarn cy:dev:css\" \"yarn cy:dev:open\"",
"cy:dev:app": "BROWSER=none REACT_APP_COMMUNITY=http://localhost:4000/test-community/community#us REACT_APP_EMAIL_NOTIFICATIONS_SERVICE=http://localhost:3005 REACT_APP_ENABLE_DEV_CLIENT_ID=true REACT_APP_EMAIL_NOTIFICATIONS_IDENTITY=http://localhost:4000/mailbot/profile/card#me REACT_APP_EMAIL_NOTIFICATIONS_TYPE=simple yarn start",
"cy:dev:app": "BROWSER=none REACT_APP_COMMUNITY=http://localhost:4000/test-community/community#us REACT_APP_EMAIL_NOTIFICATIONS_SERVICE=http://localhost:3005 REACT_APP_ENABLE_DEV_CLIENT_ID=true REACT_APP_EMAIL_NOTIFICATIONS_IDENTITY=http://localhost:4000/mailbot/profile/card#me REACT_APP_EMAIL_NOTIFICATIONS_TYPE=simple REACT_APP_GEOINDEX= yarn start",
"cy:dev:css": "community-solid-server -p 4000 -l error",
"cy:dev:open": "CYPRESS_COMMUNITY=\"http://localhost:4000/test-community/community#us\" CYPRESS_OTHER_COMMUNITY=\"http://localhost:4000/other-community/community#us\" CYPRESS_EMAIL_NOTIFICATIONS_IDENTITY=\"http://localhost:4000/mailbot/profile/card#me\" cypress open"
},
Expand Down
Loading

0 comments on commit 2073f82

Please sign in to comment.