many-to-many relation on Parse.com Cloud Code - javascript

I have a class "Team", which can have many Users.
I need to create a from/to relation every time a user is added to a Team. Every user can rate and be rated by all his teammates.
This is my Cloud code. I can't seem to get this working.
// add Team relations for every saved player
Parse.Cloud.afterSave("Team", function(request) {
// Team being saved
var team = request.object,
relation = team.relation("players"),
query = relation.query();
query.find({
// players on this Team
success: function(results) {
// all players on this Team
var i, j, player, teammate;
for (i = 0; i < results.length; i++) {
player = results[i];
// for each player add a new Rate relation (from/to)
for (j = 0; j < results.length; j++) {
teammate = results[i];
// if it's same player, escape relation
if (player == teammate) { continue; }
var Rate = Parse.Object.extend("Rate"),
rate = new Rate();
// create the relation from this player
var fromRelation = rate.relation('from');
fromRelation.add(player);
// to every other player on the team
var toRelation = rate.relation('to');
toRelation.add(teammate);
rate.save(null, {
success: function(_team) {
},
error: function(_team, error) {
}
});
}
}
response.success('rate relation success');
},
error: function(error) {
response.error('rate relation error');
}
});
});
Adding a player to a team is working great, it runs on Client side.
Classes setup on Data Browser:

Your class is "Rates" but you are using this code:
var Rate = Parse.Object.extend("Rate"),
rate = new Rate();
change to
var Rate = Parse.Object.extend("Rates"),
rate = new Rate();
Also, make sure your object is created before adding a relation.
var Rate = Parse.Object.extend("Rate"),
rate = new Rate();
[.... MOVED BELOW .....}
rate.save(null, {
success: function(_team) {
[.... now create and save relations ...]
},

Solved it this way.
The only thing annoying me is the fact that if I try this, I get an error:
"fromRelation is expecting _User, but got _Team", why?
player = results[i];
var fromRelation = rate.relation('from');
fromRelation.add(player);
I had to create 2 new vars, fromPlayer and toPlayer, although it seems dumb to me, anyway it works.
Parse.Cloud.define("updateTeamPlayers", function(request, response) {
var Team = Parse.Object.extend("Team"),
team = new Team();
team.id = request.params.id;
var relation = team.relation("players"),
query = relation.query();
query.find({
success: function(results) {
var i, j, player, teammate, queryArray = [], teammateRes = results;
for (i = 0; i < results.length; i++) {
player = results[i];
// for each player add a new Rate relation (from/to)
for (j = 0; j < teammateRes.length; j++) {
teammate = teammateRes[j];
// if it's same player, escape relation
if (player == teammate) { continue; }
var Rate = Parse.Object.extend("Rates"),
rate = new Rate();
// create the relation from this player
var fromPlayer = new Parse.User();
fromPlayer.id = player.id;
var fromRelation = rate.relation('from');
fromRelation.add(fromPlayer);
// to every other player on the team
var toPlayer = new Parse.User();
toPlayer.id = teammate.id;
var toRelation = rate.relation('to');
toRelation.add(toPlayer);
queryArray.push(rate);
}
}
Parse.Object.saveAll(queryArray, {
success: function(_allsaved) {
response.success('rate save success:' + _allsaved);
},
error: function(error) {
response.error('rate save error:' + error + ' =|= ' + error.message);
}
});
// end suucess
},
error: function() {
response.error("team failed");
}
});
});

Related

Searching two tables in one function in DynamoDB

I am trying to link two tables in DynamoDB for an Amazon Alexa skill. I am using two tables one is named 'yesno' and the other 'fixtures'. The fixtures table has a list of 22 names in each record and these names are in the 'yesno' table along with the column 'goals'. Here you can see the tables in more detail. Name Table:
Fixtures Table:
As you can see there are names that link the two databases together. I use the team1 column to search the fixtures table and use the name column to search the name table. Here is my code for searching:
function readDynamoItem(params2, callback) {
var AWS = require('aws-sdk');
AWS.config.update({region: AWSregion});
var dynamodb = new AWS.DynamoDB();
const names = new Array();
console.log('reading item from DynamoDB table');
dynamodb.scan(params2, function (err, data){
if (err) console.log(err, err.stack); // an error occurred
else{
console.log(data); // successful response
//tried to put a automatic loop for the long bit of code after this but didnt work so anyone with insight on this too would be helpful
/*for(var i = 1; i <= 11; i++){
var str = "T1S";
var pos = i.toString();
pos = str.concat(pos);
names[i] = jsonToString(data.Items[0].pos);
}
for(var j = 1; j <= 11; j++){
str = "T2S";
pos = j.toString();
pos = str.concat(pos);
names[(j+11)] = jsonToString(data.Items[0].pos);
}
*/
names[1] = jsonToString(data.Items[0].T1S1);
names[2] = jsonToString(data.Items[0].T1S2);
names[3] = jsonToString(data.Items[0].T1S3);
names[4] = jsonToString(data.Items[0].T1S4);
names[5] = jsonToString(data.Items[0].T1S5);
names[6] = jsonToString(data.Items[0].T1S6);
names[7] = jsonToString(data.Items[0].T1S7);
names[8] = jsonToString(data.Items[0].T1S8);
names[9] = jsonToString(data.Items[0].T1S9);
names[10] = jsonToString(data.Items[0].T1S10);
names[11] = jsonToString(data.Items[0].T1S11);
names[12] = jsonToString(data.Items[0].T2S1);
names[13] = jsonToString(data.Items[0].T2S2);
names[14] = jsonToString(data.Items[0].T2S3);
names[15] = jsonToString(data.Items[0].T2S4);
names[16] = jsonToString(data.Items[0].T2S5);
names[17] = jsonToString(data.Items[0].T2S6);
names[18] = jsonToString(data.Items[0].T2S7);
names[19] = jsonToString(data.Items[0].T2S8);
names[20] = jsonToString(data.Items[0].T2S9);
names[21] = jsonToString(data.Items[0].T2S10);
names[22] = jsonToString(data.Items[0].T2S11);
}
});
var goals = new Array();
//for loop to be used later when expanding
//for(var i = 1; i <= 22; i++){
var params = {
TableName: 'yesno',
FilterExpression: 'name = :value',
ExpressionAttributeValues: {':value': {"S": names[2]}}
};
dynamodb.scan(params, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else{
console.log(data); // successful response
var temp = jsonToString(data.Items[0].goals);
goals[1] = temp;
}
callback(goals[1]);
});
//}
}
function jsonToString(str){
str = JSON.stringify(str);
str = str.replace('{\"S\":\"', '');
str = str.replace('\"}', '');
return str;
}
I am trying to use the goals array to print each persons goals off but right now it won't even print one persons and instead will print an undefined object of some sort. I'm guessing it just can't search the names table using the names array. The main bit of code I am having a problem with is when searching the yesno table as you can see in this code:
var goals = new Array();
//for loop to be used later when expanding
//for(var i = 1; i <= 22; i++){
var params = {
TableName: 'yesno',
FilterExpression: 'name = :value',
ExpressionAttributeValues: {':value': {"S": names[2]}}
};
dynamodb.scan(params, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else{
console.log(data); // successful response
var temp = jsonToString(data.Items[0].goals);
goals[1] = temp;
}
callback(goals[1]);
});
//}
I know for sure there is nothing wrong with the implementation but here it is just in case it is helpful:
const handlers = {
'LaunchRequest': function () {
this.response.speak('welcome to magic answers. ask me a yes or no question.').listen('try again');
this.emit(':responseReady');
},
'MyIntent': function () {
var MyQuestion = this.event.request.intent.slots.MyQuestion.value;
console.log('MyQuestion : ' + MyQuestion);
const params2 = {
TableName: 'Fixtures',
FilterExpression: 'team1 = :value',
ExpressionAttributeValues: {':value': {"S": MyQuestion.toLowerCase()}}
};
//const params3 = {
// TableName: 'Fixtures',
// FilterExpression: 'team2 = :value',
// ExpressionAttributeValues: {':value': {"S": MyQuestion.toLowerCase()}}
//};
readDynamoItem(params2, myResult=>{
var say = MyQuestion;
say = myResult;
say = 'The top scorer for ' + MyQuestion + ' is ' + myResult;
this.response.speak(say).listen('try again');
this.emit(':responseReady');
});
},
'AMAZON.HelpIntent': function () {
this.response.speak('ask me a yes or no question.').listen('try again');
this.emit(':responseReady');
},
'AMAZON.CancelIntent': function () {
this.response.speak('Goodbye!');
this.emit(':responseReady');
},
'AMAZON.StopIntent': function () {
this.response.speak('Goodbye!');
this.emit(':responseReady');
}
}
;

Javascript global variables and references to them and their parts

I'm writing little snippets to learn more about using Javascript with API's, and have stumbled onto another problem I can't figure out on my own. I have a global variable (object?) "coins", read in from the API, and its' data field "symbol". I can use "symbol" to reference the data held there, in part of my code, without any errors. Later in the code, I use it again, and I get an error about it being undefined, despite the fact that the values returned from using it, are both defined, and, what I expected. While we are at it, maybe someone can tell me why I assign values to global variables (declared outside of all of the functions), but the variables when called, are "undefined". To see it in action, visit www.mattox.space/XCR and open up dev tools.
/*
FLOW:
get ALL coins, store NAME and SYMBOL into an object.
loop over the names object comparing to $SYMBOL text from form, return the NAME when found.
hit the API again, with the $NAME added to the URL.
create a table row.
insert data from second API hit, into table row
SOMEWHERE in there, do the USD conversion from BTC.
*/
//var name = getName();
var bitcoinValue = 0;
var coins = new Array;
var form = ""; // Value pulled from the form
var symbol = ""; // "id" on the table
var id = ""; // value pulled from the table at coins[i].id matched to coins[i].symbol
var formSym = "";
var formUSD = 0;
var formBTC = 0;
var form24h = 0;
function run() {
getFormData();
allTheCoins("https://api.coinmarketcap.com/v1/ticker/");
testGlobal();
}
function testGlobal() {
console.log("These are hopefully the values of the global variables");
console.log(formSym + " testGlobal");
console.log(formUSD + " testGlobal");
console.log(formBTC + " testGlobal");
console.log(form24h + " testGlobal");
}
function getFormData(){ //This function works GREAT!
form = document.getElementById("symbol").value //THIS WORKS
form = form.toUpperCase(); //THIS WORKS
}
function allTheCoins(URL) {
var tickerRequest = new XMLHttpRequest();
tickerRequest.open('GET', URL);
tickerRequest.send();
tickerRequest.onload = function() {
if (tickerRequest.status >= 200 && tickerRequest.status < 400) {
var input = JSON.parse(tickerRequest.responseText);
for(var i in input)
coins.push(input[i]);
testFunction(coins);
}
else {
console.log("We connected to the server, but it returned an error.");
}
console.log(formSym + " allTheCoins!"); // NOPE NOPE NOPE
console.log(formUSD) + " allTheCoins!"; // NOPE NOPE NOPE
console.log(formBTC + " allTheCoins!"); // NOPE NOPE NOPE
console.log(form24h + " allTheCoins!"); // NOPE NOPE NOPE
}
}
function testFunction(coins) {
for (var i = 0; i < coins.length; i++) {
if (coins[i].symbol == form) { // But right here, I get an error.
formSym = coins[i].name;
formUSD = coins[i].price_usd;
formBTC = coins[i].price_btc;
form24h = coins[i].percent_change_24h;
console.log(formSym + " testFunction");
console.log(formUSD + " testFunction");
console.log(formBTC + " testFunction");
console.log(form24h + " testFunction");
//DO EVERYTHING RIGHT HERE! On second thought, no, this needs fixed.
}
else if (i > coins.length) {
formSym = "Error";
formUSD = 0;
formBTC = 0;
form24h = 0;
}
}
}
/*
if (24h >= 0) {
colorRED
}
else {
colorGreen
}
*/
here is a possible way of doing it that you can get inspired by. its based on a httpRequest promise that set the headers and method.
let allTheCoins = obj => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(obj.method || obj.method, obj.url);
if (obj.headers) {
Object.keys(obj.headers).forEach(key => {
xhr.setRequestHeader(key, obj.headers[key]);
});
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject(xhr.statusText);
xhr.send(obj.body);
});
};
allTheCoins({
url: "https://api.coinmarketcap.com/v1/ticker/",
method: "GET",
headers: {"Accept-Encoding": "gzip"}
})
.then(data => {
ParseCoins(data);
})
.catch(error => {
console.log("We connected to the server, but it returned an error.");
});
function ParseCoins(data) {
const coins = JSON.parse(data);
const form = getFormVal();/*retrieve form val*/
const id = getTableId(); /*retrieve table id*/
const bitcoinValue = getBitcoinVal();/*retrieve bitcoin Value*/
const final_result = [];
for (let i = 0, len = coins[0].length; i < len; i++) {
const coin = coins[0][i];
for (let ii in coin) {
if (coin.hasOwnProperty(ii)) {
if (coin[ii].symbol == form) {
let element = {
formSym: coin[ii].name,
formUSD: coin[ii].price_usd,
formBTC: coin[ii].price_btc,
form24h: coin[ii].percent_change_24h
};
final_result.push(element);
}
}
}
}
coontinueElseWhere(final_result);
}

Another Parse 'success/error not called' error

I recently posted an issue I had with another Parse CloudCode method, were the error was thrown that Error: success/error was not called. I am having that issue again but with a different method/scenario.
Parse.Cloud.define("background", function(request, response) {
var moments = require("cloud/moments.js");
var now = moments.moment();
var query = new Parse.Query("Group");
query.find({
success: function(results) {
for (var i = 0; i < results.length; i++) {
var object = results[i];
var events = object.get("Events");
var getUsers = false;
for (var q = 0; q < events.length; q++) {
var e = events[q];
if (e.get("date") == now) {
getUsers = true;
break;
}
}
if (getUsers == true) {
for (var q = 0; q < events.length; q++) {
var e = events[q];
if (e.get("date") == now) {
var relation = object.relation("created");
var partOne = e.get("name");
var outString1 = partOne.concat(" is now");
// generate a query based on that relation
var query = relation.query();
Parse.Push.send({
where: query, // Set our Installation query
data: {
alert: outString1
}
}, {
success: function() {
// Push was successful
},
error: function(error) {
// Handle error
}
});
var relation2 = object.relation("joined");
var partOnee = e.get("name");
var outString = partOnee.concat(" is now");
// generate a query based on that relation
var query2 = relation.query();
Parse.Push.send({
where: query2, // Set our Installation query
data: {
alert: outString
}
}, {
success: function() {
// Push was successful
},
error: function(error) {
// Handle error
}
});
e.destroy();
}
}
}
}
}
});
response.success();
});
Since this method involves more than just a simple query and return (as it has the for loop among other things) I am a bit confused on how to implement the Parse Promise stuff. If anyone could assist me in how I should go about implementing the promise stuff it would be much appreciated.
Parse documentation is very clear on how to use Promises and how to rewrite your pyramid code with .then() blocks instead.

using variable to change Parse.com query

I am using the following Parse.com Javascript query and need to switch the type of query based on a variable.
function searchParseExercises (queryParam, ActiveFilters, searchParam) {
var exercise = [];
var TagSearch = Parse.Object.extend("Exercises");
var query = new Parse.Query(TagSearch);
query.containsAll("tags", ActiveFilters);
query.limit(20);
query.find({
success: function(results) {
var exerciseData = {};
for (var i = 0; i < results.length; i++) {
var object = results[i];
var exerciseData = {
exerciseName : object.get('exerciseName'),
exerciseDescription : object.get('exerciseDescription'),
images : object.get('images'),
}
exercise.push(exerciseData);
}
$scope.allExercises = exercise;
},
error: function(error) {
$ionicPopup.alert({
title: "Error: " + error.code + " " + error.message
});
}
});
}
To clarify the requirement I have both a text search and filter search in my template. If there is a text search then it should perform:
query.contains("exerciseName", searchParam);
If there are ActiveFilters then it should perform this:
query.containsAll("tags", ActiveFilters);
However if both variables are present (searchParam and ActiveFilters)
then it should perform a Compound Query.
I have no idea how I can wire all this up cleanly.
What I understood from your question:
if (ActiveFilters && searchParam) {
Parse.Query.or(ActiveFilters, searchParam).find({
success: function(results) {
// results contains a list of players that either have won a lot of games or won only a few games.
},
error: function(error) {
// There was an error.
}
}
else if (ActiveFilters) {
query.containsAll("tags", ActiveFilters);
}
else if (searchParam) {
query.contains("exerciseName", searchParam);
}

Getting JSON from a Function in javascript

So this will be a lot of code, but what matter is on line 22-25, and line 87-91. The rest of the code works. I have a nested function and want to return the JSON string. Using console.log I can tell it is running properly but will not return the JSON string. Look for the part that says //---------this part-------. There are two parts that I am asking about.
exports.post = function(request, response) {
var mssql = request.service.mssql;
//var data = '{"userID":"ryan3030#vt.edu1"}';
var inputJSON = request.body.JSONtext;
var json = JSON.parse(inputJSON);
var userID = json.userID;
mssql.query("EXEC getMeetingInfo ?", [userID],
{
success: function(results3) {
var meetingsToday = results3.length;
var meetingID = results3[0].meetingID;
var meetingName = results3[0].meetingName;
var meetingDescription = results3[0].meetingDescription;
var meetingLength = results3[0].meetingLength;
var meetingNotes = results3[0].meetingNotes;
var hostUserID = results3[0].hostUserID;
//--------------------------------------THIS PART------------------------------
var JSONfinal = allInfoFunction(mssql, meetingID, userID, meetingName, meetingDescription, meetingLength, meetingNotes, hostUserID, meetingsToday);
console.log(JSONfinal);//DOES NOT WORk
response.send(statusCodes.OK, JSONfinal);//DOES NOT WORK
//---------------------------------BETWEEN THESE----------------------------------
},
error: function(err) {
console.log("error is: " + err);
response.send(statusCodes.OK, { message : err });
}
});
};
function allInfoFunction(mssql, meetingID, userID, meetingName, meetingDescription, meetingLength, meetingNotes, hostUserID, meetingsToday){
mssql.query("EXEC getLocation ?", [meetingID],
{ success: function(results2) {
var meetingLocation = results2[0].meetingLocation;
var JSONlocation = {"meetingLocation": meetingLocation};
mssql.query("EXEC getDateTime ?", [meetingID],
{ success: function(results1) {
var length = results1.length;
var dateTime = [];
dateTime[0] = results1[0].meetingDateTime;
for (var x= 1; x < length; x++) {
dateTime[x] = results1[x].meetingDateTime;
}
//console.log(dateTime);
mssql.query("EXEC getDateTimeVote",
{ success: function(results) {
//console.log(results);
var JSONoutput2 = {};
var JSONtemp = [];
var length2 = results.length;
for(var j = 0; j < length; j++){
var vote = false;
var counter = 0;
for(var z = 0; z < length2; z++){
var a = new Date(results[z].meetingDateTime);
var b = new Date(results1[j].meetingDateTime);
if(a.getTime() === b.getTime()){
counter = counter + 1;
}
if((a.getTime() === b.getTime()) && (results[z].userID == userID)){
vote = true;
}
}
var meetingTimeInput = {"time": b, "numVotes": counter, "vote": vote}
JSONtemp.push(meetingTimeInput);
JSONoutput2.meetingTime = JSONtemp;
}
var JSONfinal = {};
var mainInfoArray = [];
var JSONmainInfo = {meetingID: meetingID, meetingName: meetingName, meetingDescription: meetingDescription, meetingLength: meetingLength, meetingNotes: meetingNotes, hostUserID: hostUserID, meetingLocation: meetingLocation };
JSONmainInfo.meetingTime = JSONtemp;
JSONfinal.numMeetingsToday = meetingsToday;
mainInfoArray.push(JSONmainInfo);
JSONfinal.meetingList = mainInfoArray;
//response.send(statusCodes.OK, JSONfinal);
//---------------------------------------AND THIS PART-------------------------------
console.log(JSON.stringify(JSONfinal));//This outputs the correct thing
var lastOne = JSON.stringify(JSONfinal);
return lastOne; //ths dosent work
//-------------------------------------BETWEEN THESE-----------------------------------
},
error: function(err) {
console.log("error is: " + err);
//response.send(statusCodes.OK, { message : err });
}
});
},
error: function(err) {
console.log("error is: " + err);
//response.send(statusCodes.OK, { message : err });
}
});
},
error: function(err) {
console.log("error is: " + err);
//response.send(statusCodes.OK, { message : err });
}
});
}
I've done a little refactoring to your code to take a more modular approach. It becomes quite tricky to debug something like what you have above. Here it is:
exports.post = function(request, response) {
var mssql = request.service.mssql;
var inputJSON = request.body.JSONtext;
var json = JSON.parse(inputJSON);
var userID = json.userID;
var JSONFinal = {};
getMeetingInfo(userID);
function getMeetingInfo(userID){
mssql.query("EXEC getMeetingInfo ?", [userID], {
success: function(results){
JSONFinal.meetingsToday = results.length;
JSONFinal.meetingID = results[0].meetingID;
JSONFinal.meetingName = results[0].meetingName;
JSONFinal.meetingDescription = results[0].meetingDescription;
JSONFinal.meetingLength = results[0].meetingLength;
JSONFinal.meetingNotes = results[0].meetingNotes;
JSONFinal.hostUserID = results[0].hostUserID;
// Call next function
getLocation(JSONFinal);
},
error: function(err) {
console.log("error is: " + err);
response.send(statusCodes.OK, { message : err });
}
});
}
function getLocation(){
mssql.query("EXEC getLocation ?", [JSONFinal.meetingID], {
success: function(results) {
JSONFinal.meetingLocation = results[0].meetingLocation;
// Call next function
getDateTime(JSONFinal);
},
error: function(err) {
console.log("error is: " + err);
}
});
}
function getDateTime(){
mssql.query("EXEC getDateTime ?", [JSONFinal.meetingID], {
success: function(results) {
var length = results.length;
var dateTime = [];
for (var x= 0; x < length; x++) {
dateTime[x] = results[x].meetingDateTime;
}
// Call next function
getDateTimeVote(dateTime);
},
error: function(err){
console.log("error is: " + err);
}
});
}
function getDateTimeVote(dateTime){
mssql.query("EXEC getDateTimeVote", {
success: function(results) {
var JSONtemp = [];
var length2 = results.length;
for(var j = 0; j < dateTime.length; j++){
var vote = false;
var counter = 0;
for(var z = 0; z < length2; z++){
var a = new Date(results[z].meetingDateTime);
var b = new Date(results1[j].meetingDateTime);
if(a.getTime() === b.getTime()){
counter = counter + 1;
}
if((a.getTime() === b.getTime()) && (results[z].userID == userID)){
vote = true;
}
}
var meetingTimeInput = {"time": b, "numVotes": counter, "vote": vote}
JSONtemp.push(meetingTimeInput);
}
var JSONmainInfo = {
meetingID: JSONFinal.meetingID,
meetingName: JSONFinal.meetingName,
meetingDescription: JSONFinal.meetingDescription,
meetingLength: JSONFinal.meetingLength,
meetingNotes: JSONFinal.meetingNotes,
hostUserID: JSONFinal.hostUserID,
meetingLocation: JSONFinal.meetingLocation
meetingTime: JSONtemp
};
var JSONfinal = {
numMeetingsToday: JSONFinal.meetingsToday,
meetingsList: [JSONmainInfo]
};
// Call next function
sendResponse(JSON.stringify(JSONfinal));
},
error: function(err) {
console.log("error is: " + err);
}
});
}
function sendResponse(data){
response.send(statusCodes.OK, data);
}
};
It looks a lot different but the functionality is pretty much the same. The key point if you look at the code is that each subsequent function is executed only after the success of the previous function. This effectively chains the methods together so that they are always executed in order and is the key point of difference.
One thing to note here is the similarity between your
allInfoFunction(...
and my
getMeetingInfo(userID)
Both of these will return undefined more or less immediately, after the MSSQL query is sent to the server. This is because the request is fired asynchronously, and the javascript continues to run through the code. After it's fired of the asynchronous request, it has nothing left to do in that function, so it returns. There is no return value specified so it returns nothing (or undefined if you will).
So all in all, the code in the question code will return undefined and then attempt to send a response before anything has actually happened. This code fixes that problem by firing the sendResponse after all the queries have been executed.
I refactored this code in a text editor and haven't run it, or checked it for errors, so follow the basic outline all you like but it may not be a good idea to copy and paste it without checking it's not broken

Categories

Resources