Additional Features

In this section, we'll continue the Stack Overflow example and implement up/down votes on Questions. We'll explore enumerations, parameterized properties, parameterized projections, and aggregations. Enumerations We'll start with an enumeration to indicate the direction of a vote. The upper-case literal names are used within the model. The quoted strings are pretty names that will appear in service request and response bodies. Partial inference, composite keys Next we'll define the vote class. Stack Overflow restricts voting based on how many times you've voted and whether the post was edited since the last vote. So votes must be . Votes must be , to keep track of who cast the vote. A user can only cast one vote, so the key should be (questionId, createdById). There is no auto-generated property. has a natural, composite key. Normally, we'd allow the property to be inferred based on the keyword, but then it would be inferred without the modifier. A user cannot upvote and downvote the same question, so is not part of the key. Associations A vote is like many-to-many mapping from Question to User. Services We'll create an upsert service for creating and editing votes. No create service is necessary because there's no auto-incrementing id. The main criteria uses the global which is the name of the currently logged in principal. The validation criteria uses the global , less 5 minutes in milliseconds, to check whether the vote was created within the last 5 minutes. The DELETE service is very similar. It just doesn't need to validate that the vote exists, and it doesn't need to validate that the voter and the question author are different users. Parameterized properties When viewing a question on Stack Overflow, you can see your own vote if you've cast one. We add as a parameterized property, though there are no parameters in this example. This criteria is another example using the global . Once the parameterized property is defined, it can be included in a projection using the same syntax as an association end. In fact, a parameterized property without any parameters can be thought of as a uni-directional association. Parameterized projections The definition of above is fine, and fairly similar to the and properties in the real StackOverflow api. However, let's add some perhaps unrealistic variants that take parameters, just to see what parameterized properties can do. Parameterized properties can be included in read projections. There are two ways to provide the parameter, as a constant or as a projection parameter. Here we define a projection that parameterizes twice, once for each . Here we define a parameterized projection, that passes its parameter through to . Parameterized service When using a parameterized projection, the service must also be parameterized. Parameterized responses When a json response includes parameterized properties, the parameters become part of the field names. Aggregations StackOverflow's documentation states: Voting scores, as displayed, are the sum of the up and down votes on a post. Vote counts are the individual up and down votes that make up the score. You can view the vote counts by clicking on the score of a post. This will break the score into upvotes and downvotes. We add two aggregation properties to Question; and . Associations and parameterized properties are defined using criteria. Aggregation properties are defined using a fuller expression language. These aggregations use the function. Other available functions include , , , and . Once these aggregation properties are defined in a class, they can be included in read projections like normal properties. They cannot be included in write projections. enumeration VoteDirection { UP("up"), DOWN("down"), } class QuestionVote systemTemporal audited { questionId: Long key; createdById: String key createdBy; direction: VoteDirection; } systemTemporal audited id QuestionVote createdById audited key direction association QuestionHasVotes { question: Question[1..1] final; votes: QuestionVote[0..*]; relationship this.id == QuestionVote.questionId } association UserHasQuestionVotes { user: User[1..1] final; votes: QuestionVote[0..*]; relationship this.userId == QuestionVote.createdById } service QuestionVoteResource { upsert upsertQuestionVote( questionId: Long[1..1] path, user: User[1..1] user, questionVote: QuestionVote[1] body) { // service : http; url : /question/vote/{questionId: Long[1..1]}; criteria: this.question.id == questionId && this.createdById == user.userId; after : this.question.createdById != user.userId && (this.createdOn > now - (5 * 60 * 1000) || this.systemFrom < this.question.systemFrom); } upsert upsertQuestionVote( questionVote: QuestionVote[1] body, user: User[1..1] user) { // service : rpc; url : /question/vote; criteria: this.question.id == questionVote.question.id && this.createdById == user.userId; after : this.question.createdById != user.userId && (this.createdOn > now - (5 * 60 * 1000) || this.systemFrom < this.question.systemFrom); } /question/vote/{questionId: Long[1..1]} PUT { multiplicity: one; criteria : this.questionId == questionId && this.createdById == userId; validate : this.question.createdById != userId && (this == null || this.createdOn > now - (5 * 60 * 1000) || this.systemFrom < this.question.systemFrom); projection : QuestionVoteWriteProjection; } } userId now service QuestionVote { /question/vote/{questionId: Long[1..1]} // PUT ... DELETE { multiplicity: one; criteria : this.questionId == questionId && this.createdById == userId; validate : this.createdOn > now - (5 * 60 * 1000) || this.systemFrom < this.question.systemFrom); projection : QuestionVoteWriteProjection; } } class Question systemTemporal versioned audited { // ... myVote(): QuestionVote[0..1] { this.votes.createdById == userId } } myVote() userId myVote() upvoted downvoted class Question systemTemporal versioned audited { // ... votesByDirection(direction: VoteDirection[1..1]): QuestionVote[0..*] { this.votes.direction == direction } voteByUser(userId: String[1..1] userId): QuestionVote[0..1] { // userId refers to the parameter, not the global this.votes.createdById == userId } } votesByDirection VoteDirection projection ProjectionWithConstant on Question { id: "Question id", title: "Question title", body: "Question body", votesByDirection("up"): { // This would work, but the information is redundant // direction: "Upvote direction", user: { userId: "Upvote user id", }, }, votesByDirection("down"): { // This would work, but the information is redundant // direction: "Upvote direction", user: { userId: "Upvote user id", }, }, } voteByUser projection ProjectionWithParameter(userId: String[1..1] userId) on Question { id : "Question id", title : "Question title", body : "Question body", voteByUser(userId): { direction: "Vote direction", // This would work, but the information is redundant /* user: { userId: "Vote user id", }, */ } } service Question { /question/{id: Long[1..1]}/{userId: String[1..1] userId} GET { format : json; multiplicity: one; criteria : this.id == id; projection : ProjectionWithParameter(userId); } } upvotes downvotes class Question systemTemporal versioned audited { // ... upvotes: Integer { count(this.votes.direction == VoteDirection.UP) } downvotes: Integer { count(this.votes.direction == VoteDirection.DOWN) } } count() sum() min() max() average()