Skip to content
This repository was archived by the owner on Mar 2, 2021. It is now read-only.

WIP: Feat/user channel create delete #68

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"projects": {
"production": "firebase-radio4000",
"staging": "radio4000-staging"
}
"projects": {
"production": "firebase-radio4000",
"staging": "radio4000-staging",
"dev": "radio4000-hugurp"
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
# compiled output
/dist
/tmp
.firebase/hosting*
database-debug.log

# dependencies
/node_modules
Expand All @@ -20,3 +22,4 @@ yarn-error.log
# vs studio code
jsconfig.json
typings/
pnpm-lock.yaml
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,34 @@ To install, you'll need node.js (installed with nvm) and git. Then run:
```
git clone [email protected]:internet4000/radio4000-api.git
cd radio4000-api
nvm install 8.0.0 // install node 8
nvm use 8.0.0 // use node 8
yarn // install npm dependencies with yarn
yarn login // login firebase
yarn start // start the server
nvm install 10.10.0 // install node 10
nvm use 10.10.0 // use node 10
npm // install npm dependencies with yarn
Comment on lines +166 to +168
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need 10.10.0 or just 10.x?

Suggested change
nvm install 10.10.0 // install node 10
nvm use 10.10.0 // use node 10
npm // install npm dependencies with yarn
nvm use 10
npm install

npm run firebase-login
npm start // start the server
open http://localhost:4001
```

You can test it using `yarn test` which runs `ava` on the `test` folder.
You can test it using `npm test` which runs `ava` on the `test` folder.


```
gcloud auth login // required login with gcloud, again!
npm run functions-shell // interactively call your firebase functions in a shell
```

```
yarn functions-shell // interactively call your firebase functions in a shell
# to mock unauthenticated user
myDatabaseFunction('data', {authMode: 'USER'})
# to mock end user
myDatabaseFunction('data', {auth: {uid: 'abcd'}})

```

Docs:
- gcloud auth: https://github.com/firebase/firebase-tools/issues/1708#issuecomment-581714542
- https://firebase.google.com/docs/functions/local-shell

## Deployment

The `master` branch will automatically deploy to staging. And `production` branch to production.
Expand All @@ -185,17 +199,17 @@ Also see the `.travis.yml` file.

To deploy manually, do this:

1. `yarn global add firebase-tools`
1. `npm global add firebase-tools`
2. `firebase login`

**Deploying to staging**

1. `yarn deploy-rules && yarn deploy-api`
1. `npm run deploy-rules && npm run deploy-api`
2. Visit https://us-central1-radio4000-staging.cloudfunctions.net/api/

**Deploying to production**

1. `yarn deploy-rules-production && yarn deploy-api-production`
1. `npm run deploy-rules-production && npm run deploy-api-production`
2. Visit https://api.radio4000.com/

## Frequently and not-frequently asked questions (FAQNFAQ)
Expand Down
2 changes: 1 addition & 1 deletion database.rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
// write: only the user owner can write a channel
// write: only a user with no channel can write a new one to himself
".write": "auth != null && (root.child('users').child(auth.uid).child('channels').child($channelID).exists() || (!data.exists() && !root.child('users').child(auth.uid).child('channels').exists()))",
".validate": "newData.hasChildren(['slug', 'title', 'created']) || !newData.exists()",
".validate": "newData.hasChildren(['title']) || !newData.exists()",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe slug was removed because it's now generated in the backend, but what about created?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should generate it in the Backend as well. My mistake!


"channelPublic": {
".validate": "newData.isString() && root.child('channelPublics').child(newData.val()).child('channel').val() == $channelID"
Expand Down
8 changes: 8 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,13 @@
},
"functions": {
"source": "."
},
"emulators": {
"functions": {
"port": 5001
},
"database": {
"port": 9000
}
}
}
218 changes: 218 additions & 0 deletions functions/channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
const admin = require('firebase-admin')
const slugify = require('@sindresorhus/slugify')

const deleteChannelFollowersReferences = async (dbRootRef, channelId, channelPublic) => {
if (!channelId || !channelPublic) return

let channelFollowersRef = dbRootRef.child(`/channelPublics/${channelPublic}/followers`)
let channelFollowersSnap
try {
channelFollowersSnap = await channelFollowersRef.once('value')
} catch (error) {
console.error('Error getting channel.followers')
}

const followers = channelFollowersSnap.val()

if (!followers || !followers.length) return

let updates = {}
Object.keys(followers).forEach(followerId => {
updates[`/channels/${followerId}/favoriteChannels/${channelId}`] = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does channelId come from?

})

return dbRootRef.update(updates)
}

const getUniqueChannelSlug = async (channelData) => {
let {title, slug} = channelData
slug = slugify(slug || title)
let channelsRef = admin.database().ref(`/channels`)

const channelWithSameSlug = await channelsRef
.orderByChild('slug')
.equalTo(slug)
.once(snapshot => snapshot.val())

if (channelWithSameSlug && channelWithSameSlug.length) {
let randomString = Math.random().toString(36).substring(7);
return `${slug}-${randomString}`
} else {
return slug
}
}


/*
a channel is created
*/
const handleChannelCreate = async (snapshot, context) => {
const newChannel = snapshot.val()
const {id: channelId} = context.params
const {auth} = context

if (!auth) {
console.error('Channel create called without auth')
return
}

const {uid} = auth

if (!channelId || !uid) {
console.error('Channel create called without channelId or auth.uid')
return
}

// find current-user at ref: /users/:currentUser
let userChannelRef = snapshot.ref
let dbRootRef = userChannelRef.parent.parent

// validate slug, or generate it
let channelSlug
try {
channelSlug = await getUniqueChannelSlug(newChannel)
}

try {
await userChannelRef.update({
slug: channelSlug,
created: admin.database.ServerValue.TIMESTAMP,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so nice if we don't have to deal with timestamps in the frontend

updated: admin.database.ServerValue.TIMESTAMP
})
} catch (error) {
console.error('Error setting slug')
}

// find current-user at ref: /users/:currentUser
let userRef = dbRootRef.child(`/users/${uid}`)

// on user add .channels[channelId]: true
try {
await userRef.child(`channels/${channelId}`).set(true)
} catch (error) {
console.error('Error settting user.channels[channelId]')
}

// new /channelPublics/
let channelPublicsRef = dbRootRef.child('/channelPublics')

// add channelPublic.channel = channelId
let channelPublic
try {
channelPublic = await channelPublicsRef.push({
channel: channelId
})
} catch (error) {
console.error('Error setting channelPublic.channel')
}

// add channel.channelPublic = channelPublic.id
try {
await userChannelRef.child('channelPublic').set(channelPublic.key)
} catch (error) {
console.error('Error setting channel.channelPublic')
}
}

/*
update channel
*/
const handleChannelUpdate = async (change, context) => {
const {id: channelId} = context.params
const {auth} = context

if (!auth) {
console.error('Channel update called without auth')
return
}

const {uid} = auth

if (!channelId || !uid) {
console.error('Channel update called without channelId or auth.uid')
return
}

let userChannelRef = change.after.ref
const newValue = change.after.val()
const previousValue = change.before.val()

if (!newValue.slug || newValue.slug !== previousValue.slug) {
// validate slug, or generate it
let channelSlug
try {
channelSlug = await getUniqueChannelSlug(newValue)
}

try {
await userChannelRef.update({
slug: channelSlug
})
} catch (error) {
console.error('Error setting slug from channelData')
}
}
}


/*
when a channel is deleted
*/
const handleChannelDelete = async (change, context) => {
const channel = change.val()
const {id: channelId} = context.params
const {auth} = context

let channelPublic
if (channel) {
channelPublic = channel.channelPublic
}

if (!auth) {
console.error('Channel delete called without auth')
return
}

const {uid} = auth

if (!channelId || !uid) {
console.error('Channel delete called without channelId or auth.uid')
return
}

// find current-user at ref: /users/:currentUser
let userChannelRef = change.ref
let dbRootRef = userChannelRef.parent.parent

if (channelPublic) {
try {
await deleteChannelFollowersReferences(dbRootRef, channelId, channelPublic)
} catch (error) {
console.error('Error deleting channel\'s followers.favorite[channelId] refs')
}
}

// find current-channel-public at ref: /channelPublic/:channel.channelPublic
let channelPublicRef = dbRootRef.child(`/channelPublics/${channelPublic}`)
try {
await channelPublicRef.set(null)
} catch (error) {
console.error('Error deleting /channelPublics/:channel.channelPublic')
}

// find current-user at ref: /users/:currentUser
let userRef = dbRootRef.child(`/users/${uid}`)
try {
await userRef.child(`/channels/${channelId}`).set(null)
} catch (error) {
console.error('Error removing channel on user')
}

}


module.exports = {
handleChannelCreate,
handleChannelUpdate,
handleChannelDelete
}
Loading