Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tibetsprague committed Jan 31, 2023
2 parents ec8cc85 + 9252b64 commit 7e62629
Show file tree
Hide file tree
Showing 27 changed files with 1,604 additions and 809 deletions.
8 changes: 4 additions & 4 deletions APIs.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ On success this will return a JSON object that looks like:
```

If there is already a user with this email but they are a not member of the group, this call will send them an invitation to join the group. You will receive:
{ message: `User already exists, invite sent to group GROUP_NAME` }
`{ message: "User already exists, invite sent to group GROUP_NAME" }`

If there is already a user with this email and they are already a member of the group:
{ message: `User already exists, and is already a member of this group` }
`{ message: "User already exists, and is already a member of this group" }`

If there is already a user with this email and you didn't pass in a group you will receive:
`{ "message": "User already exists" }`
`{ message: "User already exists" }`

### Create a Group

Expand Down Expand Up @@ -183,7 +183,7 @@ Content-Type: application/json

This is a GraphQL based endpoint so you will want the pass in a raw POST data
Example GraphQL query:
NOTE: you will want to pass _either_ an email _or_ an id to query by. If you pass both only the email will be used to lookup the person.
NOTE: you will want to pass _either_ an email _or_ an id to query by. If you pass both only the id will be used to lookup the person.
```
{
"query": "query ($id: ID, $email: String) { person(id: $id, email: $email) { id name hasRegistered } }",
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## [5.3.0] - 2023-01-30

### Added
- Support for oAuth Authorization Code flow, to enable full API integrations with other apps!
- Initial Zapier triggers: When a member is added to a group, when a member leaves or is removed from a group, when a member of a group updates their profile information.
- Database tables for GroupRole and MemberRole to support adding roles or badges to group members.

## [5.2.1] - 2023-01-20

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Thanks for checking out our code. The documentation below may be incomplete or incorrect. We welcome pull requests! But we're a very small team, so we can't guarantee timely responses.

:heart:, [Edward](https://github.com/edwardwest), [Ray](https://github.com/razorman8669), [Lawrence](https://github.com/levity), [Minda](https://github.com/Minda), [Robbie](https://github.com/robbiecarlton), & [Connor](https://github.com/connoropolous)
:heart:, [Tibet](https://github.com/tibetsprague), [Loren](https://github.com/lorenjohnson), [Tom](https://github.com/thomasgwatson)

[![Code Climate](https://codeclimate.com/github/Hylozoic/hylo-node/badges/gpa.svg)](https://codeclimate.com/github/Hylozoic/hylo-node) [![Test Coverage](https://codeclimate.com/github/Hylozoic/hylo-node/badges/coverage.svg)](https://codeclimate.com/github/Hylozoic/hylo-node/coverage)

Expand Down
54 changes: 39 additions & 15 deletions api/graphql/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { envelop, useLazyLoadedSchema } from '@envelop/core'
import { useLazyLoadedSchema } from '@envelop/core'
const { createServer, GraphQLYogaError } = require('@graphql-yoga/node')
import { readFileSync } from 'fs'
import { join } from 'path'
Expand All @@ -7,10 +7,12 @@ import { presentQuerySet } from '../../lib/graphql-bookshelf-bridge/util'
import {
acceptGroupRelationshipInvite,
acceptJoinRequest,
addGroupRole,
addMember,
addModerator,
addPeopleToProjectRole,
addPostToCollection,
addRoleToMember,
addSkill,
addSkillToLearn,
addSuggestedSkillToGroup,
Expand All @@ -29,6 +31,7 @@ import {
createProject,
createProjectRole,
createSavedSearch,
createZapierTrigger,
login,
createTopic,
deactivateUser,
Expand All @@ -43,6 +46,7 @@ import {
deleteProjectRole,
deleteReaction,
deleteSavedSearch,
deleteZapierTrigger,
expireInvitation,
findOrCreateLinkPreviewByUrl,
findOrCreateLocation,
Expand Down Expand Up @@ -74,6 +78,7 @@ import {
removeModerator,
removePost,
removePostFromCollection,
removeRoleFromMember,
removeSkill,
removeSkillToLearn,
removeSuggestedSkillFromGroup,
Expand All @@ -87,6 +92,7 @@ import {
unlinkAccount,
updateComment,
updateGroup,
updateGroupRole,
updateGroupTopic,
updateGroupTopicFollow,
updateMe,
Expand Down Expand Up @@ -120,7 +126,8 @@ export const createRequestHandler = () =>
sails.log.info(inspect(variables))
}

if (req.session.userId) {
// Update user last active time unless this is an oAuth login
if (req.session.userId && !req.api_client) {
await User.query().where({ id: req.session.userId }).update({ last_active_at: new Date() })
}
},
Expand All @@ -136,19 +143,9 @@ function createSchema (expressContext) {
const { resolvers, fetchOne, fetchMany } = setupBridge(models)

let allResolvers
if (api_client) {
// TODO: check scope here, just api:write, just api_read, or both?
allResolvers = {
Query: makeApiQueries(fetchOne),
Mutation: makeApiMutations()
}
} else if (!userId) {
allResolvers = {
Query: makePublicQueries(userId, fetchOne, fetchMany),
Mutation: makePublicMutations(expressContext, fetchOne)
}
} else {
if (userId) {
// authenticated users
// TODO: look for api_client.scope to see what an oAuthed user is allowed to access

allResolvers = {
Query: makeAuthenticatedQueries(userId, fetchOne, fetchMany),
Expand All @@ -169,6 +166,18 @@ function createSchema (expressContext) {
}
}
}
} else if (api_client) {
// TODO: check scope here, just api:write, just api:read, or both?
allResolvers = {
Query: makeApiQueries(fetchOne, fetchMany),
Mutation: makeApiMutations()
}
} else {
// Not authenticated, only allow for public queries
allResolvers = {
Query: makePublicQueries(userId, fetchOne, fetchMany),
Mutation: makePublicMutations(expressContext, fetchOne)
}
}

return makeExecutableSchema({
Expand Down Expand Up @@ -279,6 +288,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

acceptJoinRequest: (root, { joinRequestId }) => acceptJoinRequest(userId, joinRequestId),

addGroupRole: (root, { groupId, color, name, emoji }) => addGroupRole({userId, groupId, color, name, emoji}),

addModerator: (root, { personId, groupId }) =>
addModerator(userId, personId, groupId),

Expand All @@ -288,6 +299,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
addPostToCollection: (root, { collectionId, postId }) =>
addPostToCollection(userId, collectionId, postId),

addRoleToMember: (root, { personId, groupRoleId, groupId }) => addRoleToMember({ personId, groupRoleId, groupId }),

addSkill: (root, { name }) => addSkill(userId, name),
addSkillToLearn: (root, { name }) => addSkillToLearn(userId, name),
addSuggestedSkillToGroup: (root, { groupId, name }) => addSuggestedSkillToGroup(userId, groupId, name),
Expand Down Expand Up @@ -323,6 +336,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

createSavedSearch: (root, { data }) => createSavedSearch(data),

createZapierTrigger: (root, { groupId, targetUrl, type }) => createZapierTrigger(userId, groupId, targetUrl, type),

joinGroup: (root, { groupId }) => joinGroup(groupId, userId),

joinProject: (root, { id }) => joinProject(id, userId),
Expand Down Expand Up @@ -353,6 +368,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

deleteSavedSearch: (root, { id }) => deleteSavedSearch(id),

deleteZapierTrigger: (root, { id }) => deleteZapierTrigger(userId, id),

expireInvitation: (root, {invitationId}) =>
expireInvitation(userId, invitationId),

Expand Down Expand Up @@ -418,6 +435,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
removePostFromCollection: (root, { collectionId, postId }) =>
removePostFromCollection(userId, collectionId, postId),

removeRoleFromMember: (root, { groupRoleId, personId, groupId }) => removeRoleFromMember({ groupRoleId, personId, userId, groupId }),

removeSkill: (root, { id, name }) => removeSkill(userId, id || name),
removeSkillToLearn: (root, { id, name }) => removeSkillToLearn(userId, id || name),
removeSuggestedSkillFromGroup: (root, { groupId, id, name }) => removeSuggestedSkillFromGroup(userId, groupId, id || name),
Expand All @@ -444,6 +463,8 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
unlinkAccount: (root, { provider }) =>
unlinkAccount(userId, provider),

updateGroupRole: (root, { groupRoleId, color, name, emoji, active, groupId }) => updateGroupRole({userId, groupRoleId, color, name, emoji, active, groupId}),

updateGroupSettings: (root, { id, changes }) =>
updateGroup(userId, id, changes),

Expand All @@ -469,10 +490,13 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {
}
}

export function makeApiQueries (fetchOne) {
export function makeApiQueries (fetchOne, fetchMany) {
return {
// you can specify id or slug, but not both
group: async (root, { id, slug }) => fetchOne('Group', slug || id, slug ? 'slug' : 'id'),

groups: (root, args) => fetchMany('Group', args),

// you can query by id or email, with id taking preference
person: (root, { id, email }) => fetchOne('Person', email || id, email ? 'email' : 'id')
}
Expand Down
36 changes: 34 additions & 2 deletions api/graphql/makeModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {
export default function makeModels (userId, isAdmin, apiClient) {
const nonAdminFilter = makeFilterToggle(!isAdmin)

// XXX: for now give API users more access, in the future track which groups each client can access
const apiFilter = makeFilterToggle(!apiClient)
// XXX: for now give super API users more access, in the future track which groups each client can access
const apiFilter = makeFilterToggle(!apiClient || !apiClient.super)

return {
Me: {
Expand Down Expand Up @@ -512,6 +512,21 @@ export default function makeModels (userId, isAdmin, apiClient) {
relations: ['createdBy', 'fromGroup', 'toGroup']
},

GroupRole: {
model: GroupRole,
attributes: [
'color',
'emoji',
'name',
'active',
'createdAt',
'updatedAt'
],
relations: [
'group'
]
},

CustomView: {
model: CustomView,
attributes: [
Expand Down Expand Up @@ -690,6 +705,23 @@ export default function makeModels (userId, isAdmin, apiClient) {
]
},

MemberRole: {
model: MemberRole,
attributes: [
'color',
'emoji',
'name',
'active',
'createdAt',
'updatedAt'
],
relations: [
'groupRole',
'group',
'user'
]
},

MessageThread: {
model: Post,
attributes: ['created_at', 'updated_at'],
Expand Down
10 changes: 10 additions & 0 deletions api/graphql/mutations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export {
leaveProject,
processStripeToken
} from './project'
export {
addGroupRole,
addRoleToMember,
removeRoleFromMember,
updateGroupRole
} from './role'
export { deleteSavedSearch, createSavedSearch } from './savedSearch'
export {
createTopic,
Expand All @@ -100,6 +106,10 @@ export {
updateStripeAccount,
verifyEmail
} from './user'
export {
createZapierTrigger,
deleteZapierTrigger
} from './zapier'

export async function updateMe (sessionId, userId, changes) {
const user = await User.find(userId)
Expand Down
86 changes: 86 additions & 0 deletions api/graphql/mutations/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { GraphQLYogaError } = require('@graphql-yoga/node')

export async function addGroupRole ({ groupId, color, name, emoji, userId }){
if (!userId) throw new GraphQLYogaError(`No userId passed into function`)

if (groupId && name && emoji) {
const groupMembership = await GroupMembership.forIds(userId, groupId).fetch()

if (groupMembership && (groupMembership.get('role') === GroupMembership.Role.MODERATOR)) {
return GroupRole.forge({ group_id: groupId, name, emoji, active: true, color }).save().then((savedGroupRole) => savedGroupRole)
} else {
throw new GraphQLYogaError(`User doesn't have required privileges to create group role`)
}
} else {
throw new GraphQLYogaError(`Invalid/undefined parameters to create group role: received ${JSON.stringify({ groupId, name, emoji })}`)
}
}

export async function updateGroupRole ({ groupRoleId, color, name, emoji, userId, active, groupId }){
if (!userId) throw new GraphQLYogaError(`No userId passed into function`)

if (groupRoleId) {
const groupMembership = await GroupMembership.forIds(userId, groupId).fetch()

if (groupMembership && (groupMembership.get('role') === GroupMembership.Role.MODERATOR)) {
return bookshelf.transaction(async transacting => {
const groupRole = await GroupRole.where({ id: groupRoleId}).fetch()
const verifiedActiveParam = (active == null) ? groupRole.get('active') : active
const updatedAttributes = {
color: color || groupRole.get('color'),
name: name || groupRole.get('name'),
emoji: emoji || groupRole.get('emoji'),
active: verifiedActiveParam,
}

if (verifiedActiveParam !== groupRole.get('active')) {
await bookshelf.knex.raw(`
UPDATE members_roles
SET active = ?
WHERE group_role_id = ?
`, [verifiedActiveParam, groupRoleId], { transacting })
}
return groupRole.save(updatedAttributes, { transacting }).then((savedGroupRole) => savedGroupRole)
})
} else {
throw new GraphQLYogaError(`User doesn't have required privileges to update a group role`)
}
} else {
throw new GraphQLYogaError(`Invalid/undefined parameters to update group role: received ${JSON.stringify({ groupId, name, emoji, groupRoleId, active })}`)
}

}

export async function addRoleToMember ({userId, groupRoleId, personId, groupId}){
if (!userId) throw new GraphQLYogaError(`No userId passed into function`)

if (personId && groupRoleId) {
const groupMembership = await GroupMembership.forIds(userId, groupId).fetch()

if (groupMembership && (groupMembership.get('role') === GroupMembership.Role.MODERATOR)) {
return MemberRole.forge({group_role_id: groupRoleId, user_id: personId, active: true, group_id: groupId}).save().then((savedMemberRole) => savedMemberRole)
} else {
throw new GraphQLYogaError(`User doesn't have required privileges to add role to member`)
}
} else {
throw new GraphQLYogaError(`Invalid/undefined parameters to add role to member: received ${JSON.stringify({ personId, groupRoleId })}`)
}
}

export async function removeRoleFromMember ({userId, memberRoleId, personId, groupId}){
if (!userId) throw new GraphQLYogaError(`No userId passed into function`)

if (personId && memberRoleId && groupId) {
const groupMembership = await GroupMembership.forIds(userId, groupId).fetch()

if (groupMembership && (groupMembership.get('role') === GroupMembership.Role.MODERATOR) || userId === personId) {
const memberRole = await MemberRole.query('where', 'id', '=', memberRoleId).fetch()
return memberRole.destroy()
} else {
throw new GraphQLYogaError(`User doesn't have required privileges to remove role from member`)
}
} else {
throw new GraphQLYogaError(`Invalid/undefined parameters to remove role from member: received ${JSON.stringify({ personId, memberRoleId })}`)
}

}
Loading

0 comments on commit 7e62629

Please sign in to comment.