I am trying to build an alexa custom skill. I am facing an issue where I am trying to get a responses from the user for a question which the skill asks the user.
User : Tell me marks of student1 ?
Alexa: Subject
User: Maths
Alexa : student1 marks in maths is {xyz}
or if user doesnt give any input:
User : Tell me marks of student1 ?
Alexa: Subject
User: No Answer
Alexa : Gives marks of all subject for student1.
I am using Node.js.
Please tell me how to do this.
It's your lucky day. This is how I would do this with multiple MODES in Node.js.
Disclaimer: I am providing this lengthy response because I am desperate for reputation points and would like to become
a developer evangelist. ;)
Given the Node.js SDK and it's many features this is the format I would use.
// To zip and upload to lambda
// cd Desktop/StudentSkill
// sudo rm -r foo.zip
// zip -r foo.zip .s
'use strict';
var Alexa = require("alexa-sdk");
var appId = 'YOUR-AMAZON-ALEXA-SKILL-ID';
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = appId;
alexa.registerHandlers(newSessionHandlers, studentSkillSessionHandlers, specificClassSessionHandlers, functionHandlers);
alexa.execute();
};
var states = {
STUDENTMODE: '_STUDENTMODE',
CLASSMODE: '_CLASSMODE',
};
//Variables
var myStudents = {'josh':{'math':'A Plus','english':'A Plus','gym':'C'},'sam':{'math':'A Plus','english':'A minus','gym':'B Plus'}}; //add more classes.
//Could add an intent to create new students.
//var newStudent = {name:{'math':null,'english':null,'gym':null}};
//Could also add an intent to set grades
var newSessionHandlers = {
'NewSession': function() {
this.handler.state = states.STUDENTMODE;
var message = `Welcome to the School Skill, You may add students, edit grades, and review grades. For a list of uses say information. `; //this skill only contains reviewing grades.
var reprompt = ` try saying, grades for josh. `;
this.emit(':ask',message,reprompt);
},
'Unhandled': function() {
console.log("UNHANDLED");
this.emit('NewSession');
}
};
/////////////////////////////////
var studentSkillSessionHandlers = Alexa.CreateStateHandler(states.STUDENTMODE, {//Your location
'NewSession': function () {
this.emit('NewSession'); // Uses the handler in newSessionHandlers
},
//Primary Intents
'GetGradeIntent': function () { // Sampe Utterance: Tell me the marks of {student} or Grades for {student}
this.handler.state = states.CLASSMODE; //Change mode to accept a class, the intent handler getClassIntent is only available in CLASSMODE
this.attributes['CURRENTSTUDENT'] = this.event.request.intent.slots.student.value;
var message = ` which of `+this.attributes['CURRENTSTUDENT']+`'s classes would you like the grade for, name a class or say all. `;
var reprompt = message;
this.emit(':ask',message,reprompt);
},
//Help Intents
"InformationIntent": function() {
console.log("INFORMATION");
var message = ` Try saying, Tell me the marks of josh. `;
this.emit(':ask', message, message);
},
"AMAZON.StopIntent": function() {
console.log("STOPINTENT");
this.emit(':tell', "Goodbye!");
},
"AMAZON.CancelIntent": function() {
console.log("CANCELINTENT");
this.emit(':tell', "Goodbye!");
},
'AMAZON.HelpIntent': function() {
var message = helpMessage;
this.emit(':ask', message, message);
},
//Unhandled
'Unhandled': function() {
console.log("UNHANDLED");
var reprompt = ` That was not an appropriate response. which student would you like grades for. Say, grades for josh. `;
this.emit(':ask', reprompt, reprompt);
}
});
////////////////////////////
/////////////////////////////////
var specificClassSessionHandlers = Alexa.CreateStateHandler(states.CLASSMODE, {//Your location
'NewSession': function () {
this.emit('NewSession'); // Uses the handler in newSessionHandlers
},
//Primary Intents
'GetClassIntent': function () { // {className} class. ex: gym class, math class, english class. It helps to have a word that's not a slot. but amazon may pick it up correctly if you just say {className}
this.attributes['CLASSNAME'] = this.event.request.intent.slots.className.value;
var message = ``;
var reprompt = ``;
if(this.attributes['CLASSNAME'] != undefined){
message = ` I didn't get that class name. would you please repeat it. `;
reprompt = message;
}else{
grade = myStudents[this.attributes['CURRENTSTUDENT']][this.attributes['CLASSNAME']];
if(grade != undefined){
this.handler.state = states.STUDENTMODE; //Answer was present. return to student mode.
message = this.attributes['CURRENTSTUDENT']+`'s `+[this.attributes['CLASSNAME']+` grade is `+aAn(grade)+` `+grade+`. What else would you like to know?`; //Josh's math grade is an A plus.
reprompt = `what else would you like to know?`;
}else{
message = this.attributes['CURRENTSTUDENT']+` does not appear to have a grade for `+[this.attributes['CLASSNAME']+` please try again with a different class or say back.`;
reprompt = `please try again with a different class or say back.`;
}
}
var message = this.attributes['FROM'] + ' .. '+ ProFirstCity;
var reprompt = ProFirstReprompt;
this.emit(':ask',message,reprompt);
},
"AllIntent": function() {// Utterance: All, All Classes
message = ``;
//Not going to code.
//Pseudo code
// for each in json object myStudents[this.attributes['CURRENTSTUDENT']] append to message class name and grade.
this.emit(':ask', message, message);
},
"BackIntent": function() {// Utterance: Back, go back
var message = ` Who's grade would you like to know. try saying, grades for josh. `;
this.emit(':ask', message, message);
},
//Help Intents
"InformationIntent": function() {
console.log("INFORMATION");
var message = ` You've been asked for which of `+this.attributes['CURRENTSTUDENT']+`'s classes you'd his grade. Please name a class or say back. `;
this.emit(':ask', message, 'Name a class or say back.');
},
"AMAZON.StopIntent": function() {
console.log("STOPINTENT");
this.emit(':tell', "Goodbye!");
},
"AMAZON.CancelIntent": function() {
console.log("CANCELINTENT");
this.emit(':tell', "Goodbye!");
},
'AMAZON.HelpIntent': function() {
var message = helpMessage;
this.emit(':ask', message, message);
},
//Unhandled
'Unhandled': function() {
console.log("UNHANDLED");
var reprompt = ' That was not an appropriate response. Name a class or say back.';
this.emit(':ask', reprompt, reprompt);
}
});
////////////////////////////////////////////////////////////
var functionHandlers = {//NOT USED IN THIS APP //Note tied to a specific mode.
};
//#############################HELPERS VVVV#########################
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function clone(a) {
return JSON.parse(JSON.stringify(a));
}
function responseRandomizer(responseType){
let len = responseType.length;
let index = getRandomInt(0,len-1);
return responseType[index];
}
var vowels = {}
function aAn(word){
if(word != ''){
let first = word[0];
if(/[aAeEiIoOuU]/.test(first)){
return 'an';
}else{
return 'a';
}
}else{
return '';
}
}
Please note: this code was adapted from a live skill but has not been tested on it's own.
To start off requesting a follow up question you need to understand the different stateHandlers. When you invoke a new skill it goes to the newSessionHandlers from there you can run some sort of setup code and then change the MODE to a lobby to capture the main intents for the skill. I've named this lobby STUDENTMODE. Inside STUDENTMODE you can ask for the grades of a student and you could theoretically create a new student or add a class or what not. If you use the existing intent GetGradeIntent and supply it an apropriate name it will save the name of the student in the session state and change the mode to CLASSMODE which only accepts the Intents ClassNameIntent and BackIntent. If you try to invoke some other intent you will be reprompted for the name of a class by the UnhandledIntent. Upon provideing an appropriate class or saying "all" you will be provided a response and the mode will be changed back to STUDENTMODE. this plops you back in the lobby where you can ask questions about other students. Voila!
This process of changing modes is much better than Multi Part Intent Schema's like "Tell me grades for {studentName} in {mathClass}". While this can certainly work one reason modes are better is that they allow you to properly handle errors if one of the input values is incorrect, like student name or class name. You can easily ask for a single piece of information instead of asking the user to restate the entire multi part intent. It also allows you to handle getting multiple pieces of information one small piece at a time with ample instructions as apposed to allowing Alexa to continue to ask reprompt questions until all the required slots are filled.
One items I didn't cover.
Where are you storing your students? I have them hard coded into the lambda function. You could connect to amazon's dynamodb and store your session states there so they are available on the next session. that's actually as simple as adding.
alexa.dynamoDBTableName = 'NAME-OF-YOUR-DynamoDB-TABLE'; //You literally dont need any other code it just does the saving and loading??!! WHAT?
to your function here.
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = appId;
alexa.dynamoDBTableName = 'NAME-OF-YOUR-DynamoDB-TABLE'; //You literally don't need any other code it just does the saving and loading??!! WHAT?
alexa.registerHandlers(newSessionHandlers, studentSkillSessionHandlers, specificClassSessionHandlers, functionHandlers);
alexa.execute();
};
You'll need to create a dynamoDB data table and an IAm permission to allow your lambda function to access it. Then, magically, Alexa will create a single row in your data table for each unique user. A single teacher could easily add the students in their class. However, if you're looking for each teacher in a school to access one master database this is likely not the correct approach. There are likely other tutorials on how to connect Alexa to a single data table across multiple users.
I believe the core concern of your question was answered by the
different MODES where you can block out unwanted intents. If you
found this response helpful I take payment in trident layers and
reputation. Thanks!
Well, it is complicated. There is couple of approaches here you can either follow a dialogue scheme
https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/dialog-interface-reference
Or you can use intents.
you will have Student intents and Subject Intent.
"intents": [
{
"intent": "Student",
"slots": [
{
"name": "Students",
"type": "LIST_OF_STUDENTS"
}
]
},
{
"intent": "Subject",
"slots": [
{
"name": "Subjects",
"type": "LIST_OF_SUBJECTS"
}
]
}
You will need a dynamo db table where you keep Student name, and in your skill scheme, you will have a list of students, and subjects.
I can not write the whole skill for you, it is way too complicated.
Just follow a tutorial then ask a specific question.
https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
MAKE ME A SKILL is not a question....
On thing to be aware of is that Alexa doesn't respond to "no answer". So you'll need to coach the user to respond with "all" or "none".
Related
I am trying to create a wyr so the user can pick an emoji. I have used variations of the code below and I'm am unable to get the message to send to the message the bot sends.
I did try the return (); but maybe I did it wrong because it did not work. Here is the code I am currently using.
module.exports = {
name: "wyr",
description: "Would You Rather?",
execute(message) {
message.channel.send(dowyr());
function dowyr() {
var theWyr = ["Would you rather...\n\:a: Have the ability to go to the future?\n\:b: Have the ability to go to the past?"];
return wry = theWyr[Math.floor(Math.random() * theWyr.length)];
message.react("🅰️");
message.react("🅱️");
}
},
};
message is the message the user sent, so you need to point the .react() method to the message the bot sent, you can use .then() for that.
So your code would be:
module.exports = {
name: "wyr",
description: "Would You Rather?",
execute(message) {
message.channel.send(dowyr()).then((msg) => {
msg.react("🅰️");
msg.react("🅱️");
});
function dowyr() {
var theWyr = ["Would you rather...\n\:a: Have the ability to go to the future?\n\:b: Have the ability to go to the past?"];
return wry = theWyr[Math.floor(Math.random() * theWyr.length)];
}
},
};
Perhaps I am mis-using onDisonnect(), but I looked at the example code on the firebase.blog and am doing my best.
When a user submits a user name, I call the code below, which adds the username to a firebase db. Then on disconnection, I want the username to be deleted from the db. This would mean that the db would only show users that are connected to the app at that moment in time.
I am doing it this way so I can then call the data and then map through the array to display currently logged-in users.
I have made two attempts in deleting the name, which you can see in the code below under con.onDisconnect().remove();, neither of which work the way I need. That said, if I log in once again from the same computer, the first user name replaces the second user name!
Here is my code
setName = e => {
e.preventDefault()
let name = this.state.name;
let connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', function (snap) {
if (snap.val() === true) {
// Connected
let con = myConnectionsRef.push();
myConnectionsRef.set({
name
})
// On disconnect
con.onDisconnect().remove();
myConnectionsRef.orderByChild('name').equalTo(name).once('child_added', function (snapshot) {
snapshot.ref.remove();
// var nameRef = firebase.database().ref('users/'+name);
// nameRef.remove()
})
}
});
Where am I going wrong? Is there a better way to use onDisconnect? From the example on the fb forum, it isn't clear where I would put that block of code, hence why I am attempting to do it this way.
Thanks.
If I understand correctly what is your goal, you don't need to do
myConnectionsRef.orderByChild('name').equalTo(name).once('child_added', function (snapshot) {
snapshot.ref.remove();
// var nameRef = firebase.database().ref('users/'+name);
// nameRef.remove()
})
as the onDisconnect().remove() call will take care of that.
Also, as explained in the blog article you refer to (as well as shown in the doc):
The onDisconnect() call shall be before the call to set() itself. This is to
avoid a race condition where you set the user's presence to true and
the client disconnects before the onDisconnect() operation takes
effect, leaving a ghost user.
So the following code should do the trick:
setName = e => {
e.preventDefault()
let name = this.state.name;
const connectedRef = firebase.database().ref('.info/connected');
const usersRef = firebase.database().ref('users');
connectedRef.on('value', function (snap) {
if (snap.val() === true) {
// Connected
const con = usersRef.child(name); //Here we define a Reference
// When I disconnect, remove the data at the Database location corresponding to the Reference defined above
con.onDisconnect().remove();
// Add this name to the list of users
con.set(true); //Here we write data (true) to the Database location corresponding to the Reference defined above
}
});
The users node will display the list of connected users by name, as follows:
- users
- James: true
- Renaud: true
I'm in the process of building a community connector and am scratching my head; the documentation states:
getData()
Returns the tabular data for the given request.
Request
#param {Object} request A JavaScript object containing the data
request parameters.
The request parameter contains user provided values and additional
information that can be used to complete the data request. It has the
following structure:
{ "configParams": object, "scriptParams": {
"sampleExtraction": boolean,
"lastRefresh": string }, "dateRange": {
"startDate": string,
"endDate": string }, "fields": [
{
object(Field)
} ] }
I've correctly setup getConfig() (at least, my configurations are requested from the user), but my getData function is not being passed a configParams object. Here's my code.
function getConfig(request) {
var Harvest = HarvestService({
token: getHarvestAuthService().getAccessToken()
});
var accounts = Harvest.accounts.list();
var options = accounts.map(function(account) {
return {
label: account.name,
value: account.id
};
});
var config = {
configParams: [
{
type: 'SELECT_SINGLE',
name: 'harvestAccountId',
displayName: 'Harvest Account ID',
helpText: 'The ID of the Harvest Account to pull data from.',
options: options
}
],
dateRangeRequired: true
};
return config;
}
function getData(request) {
var startDate = request.dateRange.startDate;
var endDate = request.dateRange.endDate;
var accountId = request.configParams.harvestAccountId;
var harvestAuthService = getHarvestAuthService();
var Harvest = HarvestService({
token: harvestAuthService.getAccessToken(),
account: accountId
});
var fieldKeys = request.fields.map(function(field) { return field.name; });
var entries = Harvest.entries.list({
startDate: new Date(startDate),
endDate: new Date(endDate)
});
var rows = entries.map(entryToRow);
return {
schema: request.fields,
rows: rows,
cachedData: false
};
}
When I test/debug, I can select an Account at the config step, the schema is correctly returned, but I get the following exception when I try and add a widget to the report:
Script error message:
TypeError: Cannot read property "harvestAccountId" from undefined.
Script error cause: USER Script
error stacktrace: getData:244
Any advice greatly appreciated.
Found out the problem - the issue was that the value attribute of the option was a number, but it MUST be a string:
https://developers.google.com/datastudio/connector/reference#getconfig
Leaving this here in case anyone else gets stuck on this. Your config select options for your Data Studio Community Connector must have strings for both the label and the value, and nobody will coerce them for you. Fix was this:
var options = accounts.map(function(account) {
return {
label: account.name,
value: account.id + ''
};
});
Usually, request.configParams is undefined when there are no configuration values passed from the user config.
When testing the connector, are you selecting a value in the dropdown for harvestAccountId?
If you plan to share this connector with other users, it might be a good idea to have a default value for harvestAccountId in case the user does not select an option.
You can use Apps Script logging to see the response for getConfig() to ensure that right values are getting passed for options. Then you can also log the request for getData() to have a better understanding of what exactly is getting passed in the request.
Leaving this in case it helps anyone, note that the config params in the UI although they have a placeholder value need to be physically populated to appear in the request. Indeed, if none of these are filled in the configParams value does not appear in the request object.
I recently had an issue with Alamofire (More generally with asynchronous calls)
I have two models, Listings and Users. Listings contains a user's email, and I would also like to get the user's first and last name (I understand I could solve this in the backend as well, however, I would like to see if there is a frontend solution as this problem comes up for something more complicated as well)
Currently I'm making a GET request to get all listings, and I'm looping through them, and making another GET request to get firstname, lastname.
I need to wait to get the result of this get request, or at the minimum append it to my listings dictionary. Likewise, before I do anything else (Move on to the next screen of my app), I'd like to have all the listings be linked to a firstname, lastname. Because theres a loop, this specifically seems to cause some issues (ie if it was just two nested GET requests, it could be in a callback). Is there an easy way to get around this. I've attached psuedocode below:
GET Request to grab listings:
for each listing:
GET request to grab first_name, last_name
Once all listings have gotten first_name, last_name -> Load next page
The answer for your question is called a dispatch group
Dispatch groups can be entered and left only to execute some code when no code is currently inside the dispatch group.
GET Request to grab listings{
var downloadGroup = dispatch_group_create()
//Create a dispatch group
for each listing{
dispatch_group_enter(downloadGroup)
//Enter the dispatch group
GET request to grab first_name, last_name (Async){
dispatch_group_leave(downloadGroup)
//Leave the dispatch group
}
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue()) {
//Run this code when all GET requests are finished
}
}
As demonstrated by this code.
Source and interesting reading material about dispatching: Grand Central Dispatch Tutorial for Swift by Ray Wenderlich
With Scala-style futures and promises you can do something like this:
let future: Future<[Listing]> = fetchListings().flatMap { listings in
listings.traverse { listing in
fetchUser(listing.userId).map { user in
listing.userName = "\(user.firstName) \(user.lastName)"
return listing
}
}
}
The result of the above expression is a future whose value is an array of listings.
Print the listing's user name, once the above expression is finished:
future.onSuccess { listings in
listings.forEach {
print($0.userName)
}
}
Scala-style future and promise libraries: BrightFutures or FutureLib
Below a ready-to-use code example which you can paste into a playgrounds file to experiment with any of the above libraries (works in FutureLib, BrightFutures might require slight modifications).
import FutureLib
import Foundation
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
class Listing {
let userId: Int
init(userId: Int) {
self.userId = userId
userName = ""
}
var userName: String
}
struct User {
let id: Int
let firstName: String
let lastName: String
init (_ id: Int, firstName: String, lastName: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
}
}
func fetchListings() -> Future<[Listing]> {
NSLog("start fetching listings...")
return Promise.resolveAfter(1.0) {
NSLog("finished fetching listings.")
return (1...10).map { Listing(userId: $0) }
}.future!
}
// Given a user ID, fetch a user:
func fetchUser(id: Int) -> Future<User> {
NSLog("start fetching user[\(id)]...")
return Promise.resolveAfter(1.0) {
NSLog("finished fetching user[\(id)].")
return User(id, firstName: "first\(id)", lastName: "last\(id)")
}.future!
}
let future: Future<[Listing]> = fetchListings().flatMap { listings in
listings.traverse { listing in
fetchUser(listing.userId).map { user in
listing.userName = "\(user.firstName) \(user.lastName)"
return listing
}
}
}
future.onSuccess { listings in
listings.forEach {
print($0.userName)
}
}
Here's a possible solution:
GET Request to grab listings:
var n = the number of listings
var i = 0 //the number of retrieved
for each listing:
GET request to grab first_name, last_name, callback: function(response){
assign first/last name using response.
i+=1
if(i==n)
load_next_page()
}
So what this does is keep a counter of how many firstname/lastname records you've fetched. Make sure that you handle cases where a call to get the name fails.
Or, as suggested in a comment on the question, you could use promises. They make async code like this much nicer.
I have recently followed a tutorial over on Thinkster for creating a web app using Angular and Firebase.
The tutorial uses the Firebase simpleLogin method allows a 'profile' to be created that includes a username.
Factory:
app.factory('Auth', function($firebaseSimpleLogin, $firebase, FIREBASE_URL, $rootScope) {
var ref = new Firebase(FIREBASE_URL);
var auth = $firebaseSimpleLogin(ref);
var Auth = {
register: function(user) {
return auth.$createUser(user.email, user.password);
},
createProfile: function(user) {
var profile = {
username: user.username,
md5_hash: user.md5_hash
};
var profileRef = $firebase(ref.child('profile'));
return profileRef.$set(user.uid, profile);
},
login: function(user) {
return auth.$login('password', user);
},
logout: function() {
auth.$logout();
},
resolveUser: function() {
return auth.$getCurrentUser();
},
signedIn: function() {
return !!Auth.user.provider;
},
user: {}
};
$rootScope.$on('$firebaseSimpleLogin:login', function(e, user) {
angular.copy(user, Auth.user);
Auth.user.profile = $firebase(ref.child('profile').child(Auth.user.uid)).$asObject();
console.log(Auth.user);
});
$rootScope.$on('$firebaseSimpleLogin:logout', function() {
console.log('logged out');
if (Auth.user && Auth.user.profile) {
Auth.user.profile.$destroy();
}
angular.copy({}, Auth.user);
});
return Auth;
});
Controller:
$scope.register = function() {
Auth.register($scope.user).then(function(user) {
return Auth.login($scope.user).then(function() {
user.username = $scope.user.username;
return Auth.createProfile(user);
}).then(function() {
$location.path('/');
});
}, function(error) {
$scope.error = error.toString();
});
};
At the very end of the tutorial there is a 'next steps' section which includes:
Enforce username uniqueness-- this one is tricky, check out Firebase priorities and see if you can use them to query user profiles by username
I have searched and searched but can't find a clear explanation of how to do this, particularly in terms of the setPriority() function of Firebase
I'm quite the Firebase newbie so any help here would be gratefully recieved.
There are a few similar questions, but I can't seem to get my head around how to sort this out.
Enormous thanks in advance.
EDIT
From Marein's answer I have updated the register function in my controller to:
$scope.register = function() {
var ref = new Firebase(FIREBASE_URL);
var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);
q.once('value', function(snapshot) {
if (snapshot.val() === null) {
Auth.register($scope.user).then(function(user) {
return Auth.login($scope.user).then(function() {
user.username = $scope.user.username;
return Auth.createProfile(user);
}).then(function() {
$location.path('/');
});
}, function(error) {
$scope.error = error.toString();
});
} else {
// username already exists, ask user for a different name
}
});
};
But it is throwing an 'undefined is not a function' error in the line var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);. I have commented out the code after and tried just console.log(q) but still no joy.
EDIT 2
The issue with the above was that the Thinkster tutorial uses Firebase 0.8 and orderByChild is available only in later versions. Updated and Marein's answer is perfect.
There are two things to do here, a client-side check and a server-side rule.
At the client side, you want to check whether the username already exists, so that you can tell the user that their input is invalid, before sending it to the server. Where exactly you implement this up to you, but the code would look something like this:
var ref = new Firebase('https://YourFirebase.firebaseio.com');
var q = ref.child('profiles').orderByChild('username').equalTo(newUsername);
q.once('value', function(snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
} else {
// username already exists, ask user for a different name
}
});
You can use this to check before writing to the server. However, what if a user is malicious and decides to use the JS console to write to the server anyway? To prevent this you need server-side security.
I tried to come up with an example solution but I ran into a problem. Hopefully someone more knowledgeable will come along. My problem is as follows. Let's say your database structure looks like this:
{
"profiles" : {
"profile1" : {
"username" : "Nick",
"md5_hash" : "..."
},
"profile2" : {
"username" : "Marein",
"md5_hash" : "..."
}
}
}
When adding a new profile, you'd want to have a rule ensuring that no profile object with the same username property exists. However, as far as I know the Firebase security language does not support this, with this data structure.
A solution would be to change the datastructure to use username as the key for each profile (instead of profile1, profile2, ...). That way there can only ever be one object with that username, automatically. Database structure would be:
{
"profiles" : {
"Nick" : {
"md5_hash" : "..."
},
"Marein" : {
"md5_hash" : "..."
}
}
}
This might be a viable solution in this case. However, what if not only the username, but for example also the email has to be unique? They can't both be the object key (unless we use string concatenation...).
One more thing that comes to mind is to, in addition to the list of profiles, keep a separate list of usernames and a separate list of emails as well. Then those can be used easily in security rules to check whether the given username and email already exist. The rules would look something like this:
{
"rules" : {
".write" : true,
".read" : true,
"profiles" : {
"$profile" : {
"username" : {
".validate" : "!root.child('usernames').child(newData.val()).exists()"
}
}
},
"usernames" : {
"$username" : {
".validate" : "newData.isString()"
}
}
}
}
However now we run into another problem; how to ensure that when a new profile is created, the username (and email) are also placed into these new lists? [1]
This in turn can be solved by taking the profile creation code out of the client and placing it on a server instead. The client would then need to ask the server to create a new profile, and the server would ensure that all the necessary tasks are executed.
However, it seems we have gone very far down a hole to answer this question. Perhaps I have overlooked something and things are simpler than they seem. Any thoughts are appreciated.
Also, apologies if this answer is more like a question than an answer, I'm new to SO and not sure yet what is appropriate as an answer.
[1] Although maybe you could argue that this does not need to be ensured, as a malicious user would only harm themselves by not claiming their unique identity?
I had a similar problem. But it was after registering the user with password and email. In the user profile could save a user name that must be unique and I have found a solution, maybe this can serve you.
Query for username unique in Firebase
var ref = new Firebase(FIREBASE_URL + '/users');
ref.orderByChild("username").equalTo(profile.username).on("child_added", function(snapshot) {
if (currentUser != snapshot.key()) {
scope.used = true;
}
});
ref.orderByChild("username").equalTo(profile.username).once("value", function(snap) {
//console.log("initial data loaded!", Object.keys(snap.val()).length === count);
if (scope.used) {
console.log('username already exists');
scope.used = false;
}else{
console.log('username doesnt exists, update it');
userRef.child('username').set(profile.username);
}
});
};