-
Notifications
You must be signed in to change notification settings - Fork 3
WIP: Feat/user channel create delete #68
base: master
Are you sure you want to change the base?
Changes from all commits
c10765e
5ea091e
1a46f36
843717f
ed236f7
5c246da
ec5c810
4f4354d
dd840ff
e9af638
4a78951
4d5dc53
8b55972
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
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. | ||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where does |
||
}) | ||
|
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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?