Skip to content

Commit 10ce687

Browse files
authored
Merge pull request #155 from developmentseed/feature/organizations
Organizations model
2 parents d71938a + 74a82d9 commit 10ce687

File tree

6 files changed

+505
-8
lines changed

6 files changed

+505
-8
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
### Added
9+
- Model for collections of teams called "organizations"
10+
- New roles for organizations, "organization owner" and "organization manager"
11+
12+
## beta1
13+
### TODO complete this
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
exports.up = async (knex) => {
2+
await knex.schema.createTable('organization', (table) => {
3+
table.increments('id')
4+
table.string('name').notNullable().unique()
5+
table.text('description')
6+
table.timestamps(false, true)
7+
})
8+
9+
await knex.schema.createTable('organization_owner', (table) => {
10+
table.increments('id')
11+
table.integer('organization_id').references('id').inTable('organization').onDelete('CASCADE')
12+
table.integer('osm_id')
13+
table.unique(['organization_id', 'osm_id'])
14+
})
15+
16+
await knex.schema.createTable('organization_manager', (table) => {
17+
table.increments('id')
18+
table.integer('organization_id').references('id').inTable('organization').onDelete('CASCADE')
19+
table.integer('osm_id')
20+
table.unique(['organization_id', 'osm_id'])
21+
})
22+
23+
await knex.schema.createTable('organization_team', table => {
24+
table.increments('id')
25+
table.integer('team_id').references('id').inTable('team').onDelete('CASCADE')
26+
table.integer('organization_id').references('id').inTable('organization').onDelete('CASCADE')
27+
table.unique(['organization_id', 'team_id'])
28+
})
29+
}
30+
31+
exports.down = async (knex) => {
32+
await knex.schema.dropTable('organization_team')
33+
await knex.schema.dropTable('organization_manager')
34+
await knex.schema.dropTable('organization_owner')
35+
await knex.schema.dropTable('organization')
36+
}

app/lib/organization.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
const db = require('../db')
2+
const team = require('./team')
3+
const { map, prop, contains } = require('ramda')
4+
const { unpack } = require('./utils')
5+
6+
/**
7+
* Get an organization
8+
*
9+
* @param {int} id - organization id
10+
* @return {promise}
11+
*/
12+
async function get (id) {
13+
const conn = await db()
14+
return unpack(conn('organization').where('id', id))
15+
}
16+
17+
/**
18+
* Get an organization's owners
19+
* @param {int} id - organization id
20+
* @return {promise}
21+
*/
22+
async function getOwners (id) {
23+
const conn = await db()
24+
return conn('organization_owner').where('organization_id', id)
25+
}
26+
27+
/**
28+
* Get an organization's managers
29+
* @param {int} id - organization id
30+
* @return {promise}
31+
*/
32+
async function getManagers (id) {
33+
const conn = await db()
34+
return conn('organization_manager').where('organization_id', id)
35+
}
36+
37+
/**
38+
* Create an organization
39+
* Organizations have owners so we give an osm id as the second param
40+
*
41+
* @param {object} data - params for an organization
42+
* @param {string} data.name - name of the organization
43+
* @param {int} osmId - osm id of the owner
44+
* @return {promise}
45+
*/
46+
async function create (data, osmId) {
47+
if (!osmId) throw new Error('owner osm id is required as second argument')
48+
49+
if (!data.name) throw new Error('data.name property is required')
50+
const conn = await db()
51+
52+
return conn.transaction(async trx => {
53+
const [row] = await trx('organization').insert(data).returning('*')
54+
await trx('organization_owner').insert({ organization_id: row.id, osm_id: osmId })
55+
await trx('organization_manager').insert({ organization_id: row.id, osm_id: osmId })
56+
return row
57+
})
58+
}
59+
60+
/**
61+
* Destroy an organization
62+
*
63+
* @param {int} id - organization id
64+
* @return {promise}
65+
*/
66+
async function destroy (id) {
67+
const conn = await db()
68+
return conn('organization').where('id', id).del()
69+
}
70+
71+
/**
72+
* Update an organization
73+
*
74+
* @param {int} id - organization id
75+
* @param {object} data - params for an organization
76+
* @return {promise}
77+
*/
78+
async function update (id, data) {
79+
if (!data.name) throw new Error('data.name property is required')
80+
81+
const conn = await db()
82+
return unpack(conn('organization').where('id', id).update(data).returning('*'))
83+
}
84+
85+
/**
86+
* Add organization owner
87+
*
88+
* @param {int} id - organization id
89+
* @param {int} osmId - osm id of the owner
90+
* @return {promise}
91+
*/
92+
async function addOwner (id, osmId) {
93+
const conn = await db()
94+
return unpack(conn('organization_owner').insert({ organization_id: id, osm_id: osmId }))
95+
}
96+
97+
/**
98+
* Remove organization owner
99+
* There has to be at least one owner for an organization
100+
*
101+
* @param {int} id - organization id
102+
* @param {int} osmId - osm id of the owner
103+
* @return {promise}
104+
*/
105+
async function removeOwner (id, osmId) {
106+
const conn = await db()
107+
const owners = map(prop('osm_id'), await getOwners(id))
108+
109+
if (!contains(osmId, owners)) {
110+
throw new Error('osmId is not an owner')
111+
}
112+
113+
if (owners.length === 1) {
114+
throw new Error('cannot remove owner because there must be at least one owner')
115+
}
116+
117+
return unpack(conn('organization_owner').where({ organization_id: id, osm_id: osmId }).del())
118+
}
119+
120+
/**
121+
* Add organization manager
122+
*
123+
* @param {int} id - organization id
124+
* @param {int} osmId - osm id of the manager
125+
* @return {promise}
126+
*/
127+
async function addManager (id, osmId) {
128+
const conn = await db()
129+
return unpack(conn('organization_manager').insert({ organization_id: id, osm_id: osmId }))
130+
}
131+
132+
/**
133+
* Remove organization manager
134+
* There can be 0 managers in an organization
135+
*
136+
* @param {int} id - organization id
137+
* @param {int} osmId - osm id of the manager
138+
* @return {promise}
139+
*/
140+
async function removeManager (id, osmId) {
141+
const conn = await db()
142+
const managers = map(prop('osm_id'), await getManagers(id))
143+
144+
if (!contains(osmId, managers)) {
145+
throw new Error('osmId is not a manager')
146+
}
147+
148+
return unpack(conn('organization_manager').where({ organization_id: id, osm_id: osmId }).del())
149+
}
150+
151+
/**
152+
* Create organization team
153+
*
154+
* An organization team is a team that is assigned to the organization
155+
* at creation time. Only organization managers can create teams
156+
*
157+
* @param {int} organizationId - organization id
158+
* @param {object} data - params for team (see team.create function)
159+
* @param {int} osmId - id of the organization manager
160+
* @return {promise}
161+
*/
162+
async function createOrgTeam (organizationId, data, osmId) {
163+
const conn = await db()
164+
165+
return conn.transaction(async trx => {
166+
const record = await team.create(data, osmId, trx)
167+
await trx('organization_team').insert({ team_id: record.id, organization_id: organizationId })
168+
})
169+
}
170+
171+
/**
172+
* Checks if the osm user is an owner of a team
173+
* @param {int} organizationId - organization id
174+
* @param {int} osmId - osm id
175+
* @returns boolean
176+
*/
177+
async function isOwner (organizationId, osmId) {
178+
if (!organizationId) throw new Error('organization id is required as first argument')
179+
if (!osmId) throw new Error('osm id is required as second argument')
180+
const conn = await db()
181+
const result = await conn('organization_owner').where({ organization_id: organizationId, osm_id: osmId })
182+
return result.length > 0
183+
}
184+
185+
/**
186+
* Checks if the osm user is a manager of a team
187+
* @param {int} organizationId - organization id
188+
* @param {int} osmId - osm id
189+
* @returns boolean
190+
*/
191+
async function isManager (organizationId, osmId) {
192+
if (!organizationId) throw new Error('organization id is required as first argument')
193+
if (!osmId) throw new Error('osm id is required as second argument')
194+
const conn = await db()
195+
const result = await conn('organization_manager').where({ organization_id: organizationId, osm_id: osmId })
196+
return result.length > 0
197+
}
198+
199+
module.exports = {
200+
get,
201+
create,
202+
destroy,
203+
update,
204+
addOwner,
205+
removeOwner,
206+
addManager,
207+
removeManager,
208+
getOwners,
209+
getManagers,
210+
isOwner,
211+
isManager,
212+
createOrgTeam
213+
}

app/lib/team.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
const db = require('../db')
22
const knexPostgis = require('knex-postgis')
3-
const { head } = require('ramda')
43
const join = require('url-join')
54
const xml2js = require('xml2js')
5+
const { unpack } = require('./utils')
66
const request = require('request-promise-native')
77

88
const { serverRuntimeConfig } = require('../../next.config')
99

10-
async function unpack (promise) {
11-
return promise.then(head)
12-
}
13-
1410
/**
1511
* resolveMemberNames
1612
* Get the member details for osm ids
@@ -84,12 +80,13 @@ async function getModerators (id) {
8480
*
8581
* @param options
8682
* @param {int} options.osmId - filter by whether osmId is a member
83+
* @param {int} options.organizationId - filter by whether team belongs to organization
8784
* @param {Array[float]} options.bbox - filter for teams whose location is in bbox (xmin, ymin, xmax, ymax)
8885
* @return {Promise[Array]}
8986
**/
9087
async function list (options) {
9188
options = options || {}
92-
const { osmId, bbox } = options
89+
const { osmId, bbox, organizationId } = options
9390

9491
const conn = await db()
9592
const st = knexPostgis(conn)
@@ -102,6 +99,12 @@ async function list (options) {
10299
})
103100
}
104101

102+
if (organizationId) {
103+
query = query.whereIn('id', function () {
104+
this.select('team_id').from('organization_team').where('organization_id', organizationId)
105+
})
106+
}
107+
105108
if (bbox) {
106109
query = query.where(st.boundingBoxContained('location', st.makeEnvelope(...bbox)))
107110
}
@@ -133,12 +136,14 @@ async function listModeratedBy (osmId) {
133136
* @param {string} data.name - name of the team
134137
* @param {geojson} data.location - lat/lon of team
135138
* @param {int} osmId - id of first moderator
139+
* @param {object=} trx - optional parameter for database connection to re-use connection in case of nested
140+
* transactions. This is used when a team is created as part of an organization
136141
* @return {promise}
137142
**/
138-
async function create (data, osmId) {
143+
async function create (data, osmId, trx) {
139144
if (!osmId) throw new Error('moderator osm id is required as second argument')
140145
if (!data.name) throw new Error('data.name property is required')
141-
const conn = await db()
146+
const conn = trx || await db()
142147
const st = knexPostgis(conn)
143148

144149
// convert location to postgis geom

app/lib/utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { head } = require('ramda')
2+
3+
async function unpack (promise) {
4+
return promise.then(head)
5+
}
6+
7+
module.exports = {
8+
unpack
9+
}

0 commit comments

Comments
 (0)