Version control
In this example we will be modeling a version control system similar to GitHub
Table Definition
Setup
This file is referenced by multiple entities below
./types.ts
/* istanbul ignore file */
import { IssueCommentIds, PullRequestCommentIds } from "./index";
export const StatusTypes = ["Open", "Closed"] as const;
export type Status = (typeof StatusTypes)[number];
export function toStatusCode(status: unknown) {
for (let index in StatusTypes) {
if (StatusTypes[index] === status) {
return `${index}`;
}
}
}
export function toStatusString(code: unknown) {
for (let index in StatusTypes) {
if (`${index}` === code) {
return StatusTypes[index];
}
}
}
export const PullRequestTicket = "PullRequest";
export const IssueTicket = "Issue";
export const IsNotTicket = "";
export const TicketTypes = [
IssueTicket,
PullRequestTicket,
IsNotTicket,
] as const;
export const NotYetViewed = "#";
export type SubscriptionTypes = (typeof TicketTypes)[number];
export function isIssueCommentIds(comment: any): comment is IssueCommentIds {
return comment.issueNumber !== undefined && comment.username !== undefined;
}
export function isPullRequestCommentIds(
comment: any,
): comment is PullRequestCommentIds {
return (
comment.pullRequestNumber !== undefined && comment.username !== undefined
);
}
Entities
Issues
import { Entity } from "electrodb";
import moment from "moment";
import {
TicketTypes,
IssueTicket,
StatusTypes,
toStatusString,
toStatusCode,
} from "./types";
export const issues = new Entity({
model: {
entity: "issues",
service: "versioncontrol",
version: "1",
},
attributes: {
issueNumber: {
type: "string",
},
repoName: {
type: "string",
},
repoOwner: {
type: "string",
},
username: {
type: "string",
},
ticketType: {
type: TicketTypes,
set: () => IssueTicket,
readOnly: true,
},
ticketNumber: {
type: "string",
readOnly: true,
watch: ["issueNumber"],
set: (_, { issueNumber }) => issueNumber,
},
status: {
type: StatusTypes,
default: "Open",
set: (val) => toStatusCode(val),
get: (val) => toStatusString(val),
},
subject: {
type: "string",
},
body: {
type: "string",
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
issue: {
collection: "issueReview",
pk: {
composite: ["repoOwner", "repoName", "issueNumber"],
field: "pk",
},
sk: {
composite: [],
field: "sk",
},
},
created: {
collection: ["owned", "managed"],
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
composite: ["username"],
},
sk: {
field: "gsi1sk",
composite: ["status", "createdAt"],
},
},
todos: {
collection: "activity",
index: "gsi2pk-gsi2sk-index",
pk: {
composite: ["repoOwner", "repoName"],
field: "gsi2pk",
},
sk: {
composite: ["status", "createdAt"],
field: "gsi2sk",
},
},
_: {
collection: "subscribers",
index: "gsi4pk-gsi4sk-index",
pk: {
composite: ["repoOwner", "repoName", "ticketNumber"],
field: "gsi4pk",
},
sk: {
composite: [],
field: "gsi4sk",
},
},
},
});
IssueComments
import { Entity } from "electrodb";
import moment from "moment";
import {
TicketTypes,
IssueTicket,
StatusTypes,
toStatusString,
toStatusCode,
} from "./types";
export const issueComments = new Entity({
model: {
entity: "issueComment",
service: "versioncontrol",
version: "1",
},
attributes: {
issueNumber: {
type: "string",
},
commentId: {
type: "string",
},
username: {
type: "string",
},
replyTo: {
type: "string",
},
replyViewed: {
type: "string",
default: NotYetViewed,
get: (replyViewed) => {
if (replyViewed !== NotYetViewed) {
return replyViewed;
}
},
set: (replyViewed) => {
if (replyViewed === undefined) {
return NotYetViewed;
}
return replyViewed;
},
},
repoName: {
type: "string",
},
repoOwner: {
type: "string",
},
body: {
type: "string",
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
comments: {
collection: "issueReview",
pk: {
composite: ["repoOwner", "repoName", "issueNumber"],
field: "pk",
},
sk: {
composite: ["commentId"],
field: "sk",
},
},
created: {
collection: "conversations",
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
composite: ["username"],
},
sk: {
field: "gsi1sk",
composite: ["repoOwner", "repoName", "issueNumber"],
},
},
replies: {
collection: "inbox",
index: "gsi2pk-gsi2sk-index",
pk: {
composite: ["replyTo"],
field: "gsi2pk",
},
sk: {
composite: ["createdAt", "replyViewed"],
field: "gsi2sk",
},
},
},
});
PullRequests
import { Entity } from "electrodb";
import moment from "moment";
import {
NotYetViewed,
TicketTypes,
PullRequestTicket,
StatusTypes,
toStatusString,
toStatusCode,
} from "./types";
export const pullRequests = new Entity({
model: {
entity: "pullRequest",
service: "versioncontrol",
version: "1",
},
attributes: {
pullRequestNumber: {
type: "string",
required: true,
},
repoName: {
type: "string",
required: true,
},
repoOwner: {
type: "string",
required: true,
},
username: {
type: "string",
required: true,
},
ticketType: {
type: TicketTypes,
default: () => PullRequestTicket,
set: () => PullRequestTicket,
readOnly: true,
},
ticketNumber: {
type: "string",
readOnly: true,
watch: ["pullRequestNumber"],
set: (_, { issueNumber }) => issueNumber,
},
status: {
type: StatusTypes,
default: "Open",
set: (val) => toStatusCode(val),
get: (val) => toStatusString(val),
},
reviewers: {
type: "list",
items: {
type: "map",
properties: {
username: {
type: "string",
required: true,
},
approved: {
type: "boolean",
required: true,
},
createdAt: {
type: "string",
default: () => moment.utc().format(),
readOnly: true,
},
},
},
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
pullRequest: {
collection: "PRReview",
pk: {
composite: ["repoOwner", "repoName", "pullRequestNumber"],
field: "pk",
},
sk: {
composite: [],
field: "sk",
},
},
created: {
collection: ["owned", "managed"],
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
composite: ["username"],
},
sk: {
field: "gsi1sk",
composite: ["status", "createdAt"],
},
},
enhancements: {
collection: "activity",
index: "gsi2pk-gsi2sk-index",
pk: {
field: "gsi2pk",
composite: ["repoOwner", "repoName"],
},
sk: {
field: "gsi2sk",
composite: ["status", "createdAt"],
},
},
_: {
collection: "subscribers",
index: "gsi4pk-gsi4sk-index",
pk: {
composite: ["repoOwner", "repoName", "ticketNumber"],
field: "gsi4pk",
},
sk: {
composite: [],
field: "gsi4sk",
},
},
},
});
PullRequestComments
import { Entity } from "electrodb";
import moment from "moment";
import {
NotYetViewed,
TicketTypes,
PullRequestTicket,
StatusTypes,
toStatusString,
toStatusCode,
} from "./types";
export const pullRequestComments = new Entity({
model: {
entity: "pullRequestComment",
service: "versioncontrol",
version: "1",
},
attributes: {
repoName: {
type: "string",
},
username: {
type: "string",
},
repoOwner: {
type: "string",
},
pullRequestNumber: {
type: "string",
},
commentId: {
type: "string",
},
replyTo: {
type: "string",
},
replyViewed: {
type: "string",
default: NotYetViewed,
get: (replyViewed) => {
if (replyViewed !== NotYetViewed) {
return replyViewed;
}
},
set: (replyViewed) => {
if (replyViewed === undefined) {
return NotYetViewed;
}
return replyViewed;
},
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
comments: {
collection: "PRReview",
pk: {
composite: ["repoOwner", "repoName", "pullRequestNumber"],
field: "pk",
},
sk: {
composite: ["commentId"],
field: "sk",
},
},
created: {
collection: "conversations",
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
composite: ["username"],
},
sk: {
field: "gsi1sk",
composite: ["repoOwner", "repoName"],
},
},
replies: {
collection: "inbox",
index: "gsi2pk-gsi2sk-index",
pk: {
composite: ["replyTo"],
field: "gsi2pk",
},
sk: {
composite: ["createdAt", "replyViewed"],
field: "gsi2sk",
},
},
},
});
Repositories
import { Entity } from "electrodb";
import moment from "moment";
export const licenses = [
"afl-3.0",
"apache-2.0",
"artistic-2.0",
"bsl-1.0",
"bsd-2-clause",
"bsd-3-clause",
"bsd-3-clause-clear",
"cc",
"cc0-1.0",
"cc-by-4.0",
"cc-by-sa-4.0",
"wtfpl",
"ecl-2.0",
"epl-1.0",
"epl-2.0",
"eupl-1.1",
"agpl-3.0",
"gpl",
"gpl-2.0",
"gpl-3.0",
"lgpl",
"lgpl-2.1",
"lgpl-3.0",
"isc",
"lppl-1.3c",
"ms-pl",
"mit",
"mpl-2.0",
"osl-3.0",
"postgresql",
"ofl-1.1",
"ncsa",
"unlicense",
"zlib",
] as const;
export const repositories = new Entity({
model: {
entity: "repositories",
service: "versioncontrol",
version: "1",
},
attributes: {
repoName: {
type: "string",
},
repoOwner: {
type: "string",
},
about: {
type: "string",
},
username: {
type: "string",
readOnly: true,
watch: ["repoOwner"],
set: (_, { repoOwner }) => repoOwner,
},
description: {
type: "string",
},
isPrivate: {
type: "boolean",
},
license: {
type: licenses,
},
defaultBranch: {
type: "string",
default: "main",
},
topics: {
type: "set",
items: "string",
},
followers: {
type: "set",
items: "string",
},
stars: {
type: "set",
items: "string",
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
repositories: {
collection: "alerts",
pk: {
composite: ["repoOwner"],
field: "pk",
},
sk: {
composite: ["repoName"],
field: "sk",
},
},
created: {
collection: "owned",
index: "gsi1pk-gsi1sk-index",
pk: {
composite: ["username"],
field: "gsi1pk",
},
sk: {
composite: ["isPrivate", "createdAt"],
field: "gsi1sk",
},
},
},
});
Subscriptions
import { Entity } from "electrodb";
import moment from "moment";
import { TicketTypes, IsNotTicket } from "./types";
const RepositorySubscription = "#";
export const subscriptions = new Entity({
model: {
entity: "subscription",
service: "versioncontrol",
version: "1",
},
attributes: {
repoName: {
type: "string",
required: true,
},
repoOwner: {
type: "string",
required: true,
},
username: {
type: "string",
required: true,
},
ticketNumber: {
type: "string",
default: () => IsNotTicket,
set: (ticketNumber) => {
if (ticketNumber === IsNotTicket) {
return RepositorySubscription;
} else {
return ticketNumber;
}
},
get: (ticketNumber) => {
if (ticketNumber === RepositorySubscription) {
return IsNotTicket;
} else {
return ticketNumber;
}
},
},
ticketType: {
type: TicketTypes,
default: () => IsNotTicket,
set: (ticketType) => {
if (ticketType === IsNotTicket) {
return RepositorySubscription;
} else {
return ticketType;
}
},
get: (ticketType) => {
if (ticketType === RepositorySubscription) {
return IsNotTicket;
} else {
return ticketType;
}
},
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
repository: {
pk: {
composite: ["repoOwner", "repoName"],
field: "pk",
},
sk: {
composite: ["username", "ticketType", "ticketNumber"],
field: "sk",
},
},
user: {
collection: "watching",
index: "gsi3pk-gsi3sk-index",
pk: {
composite: ["username"],
field: "gsi3pk",
},
sk: {
composite: ["ticketType", "ticketNumber"],
field: "gsi3sk",
},
},
tickets: {
collection: "subscribers",
index: "gsi4pk-gsi4sk-index",
pk: {
composite: ["repoOwner", "repoName", "ticketNumber"],
field: "gsi4pk",
},
sk: {
composite: ["ticketType", "username"],
field: "gsi4sk",
},
},
},
});
Users
import { Entity } from "electrodb";
import moment from "moment";
export const users = new Entity({
model: {
entity: "user",
service: "versioncontrol",
version: "1",
},
attributes: {
username: {
type: "string",
},
fullName: {
type: "string",
},
photo: {
type: "string",
},
bio: {
type: "string",
},
location: {
type: "string",
},
pinned: {
type: "any",
},
following: {
type: "set",
items: "string",
},
followers: {
type: "set",
items: "string",
},
createdAt: {
type: "string",
set: () => moment.utc().format(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => moment.utc().format(),
readOnly: true,
},
},
indexes: {
user: {
collection: "overview",
pk: {
composite: ["username"],
field: "pk",
},
sk: {
composite: [],
field: "sk",
},
},
_: {
collection: "owned",
index: "gsi1pk-gsi1sk-index",
pk: {
composite: ["username"],
field: "gsi1pk",
},
sk: {
field: "gsi1sk",
composite: [],
},
},
subscriptions: {
collection: "watching",
index: "gsi3pk-gsi3sk-index",
pk: {
composite: ["username"],
field: "gsi3pk",
},
sk: {
composite: [],
field: "gsi3sk",
},
},
},
});
Service
import { Service } from "electrodb";
export const table = "electro";
export const store = new Service(
{
users,
issues,
repositories,
pullRequests,
subscriptions,
issueComments,
pullRequestComments,
},
{ table },
);
Access Patterns
Get public repositories by username
export async function getPublicRepository(username: string) {
return store.entities.repositories.query.created({
username,
isPrivate: false,
});
}
Get pull request and associated comments
Note that newest comments will be returned first
export async function reviewPullRequest(options: {
pr: PullRequestIds;
cursor?: string;
}) {
const { pr, cursor } = options;
return store.collections.PRReview(pr).go({ cursor, order: "desc" });
}
Get issue and associated comments
Note that newest comments will be returned first
export async function reviewIssue(options: {
issue: IssueIds;
cursor?: string;
}) {
const { issue, cursor } = options;
return store.collections.issueReview(issue).go({ cursor, order: "desc" });
}
Get pull requests created by user
export async function getUserPullRequests(options: {
username: string;
status?: Status;
cursor?: string;
}) {
const { username, status, cursor } = options;
return store.entities.pullRequests.query
.created({ username, status })
.go({ cursor, order: "desc" });
}
Close pull request
Note that this can only be performed by user who opened PR or the repo owner
export async function closePullRequest(user: string, pr: PullRequestIds) {
return store.entities.pullRequests
.update(pr)
.set({ status: "Closed" })
.where(
({ username, repoOwner }, { eq }) => `
${eq(username, user)} OR ${eq(repoOwner, user)}
`,
)
.go();
}
Get all user info, repos, pull requests, and issues in one query
export async function getFirstPageLoad(username: string) {
const results: OwnedItems = {
issues: [],
pullRequests: [],
repositories: [],
users: [],
};
let next = null;
do {
const { cursor, data } = await store.collections.owned({ username }).go();
results.issues = results.issues.concat(data.issues);
results.pullRequests = results.pullRequests.concat(data.pullRequests);
results.repositories = results.repositories.concat(data.repositories);
results.users = results.users.concat(data.users);
next = cursor;
} while (next !== null);
return results;
}
Get subscriptions for a given repository, pull request, or issue.
export async function getSubscribed(
repoOwner: string,
repoName: string,
ticketNumber: string = IsNotTicket,
) {
return store.collections
.subscribers({ repoOwner, repoName, ticketNumber })
.go();
}
Get unread comment replies
const MinDate = "0000-00-00";
const MaxDate = "9999-99-99";
export async function getUnreadComments(user: string) {
const start = {
createdAt: MinDate,
replyViewed: NotYetViewed,
};
const end = {
createdAt: MaxDate,
replyViewed: NotYetViewed,
};
let [issues, pullRequests] = await Promise.all([
store.entities.issueComments.query
.replies({ replyTo: user })
.between(start, end)
.go(),
store.entities.pullRequestComments.query
.replies({ replyTo: user })
.between(start, end)
.go(),
]);
return {
issues,
pullRequests,
};
}
Mark comment reply as read
Note that this can only be done by the user who was being replied to
export async function readReply(
user: string,
comment: IssueCommentIds,
): Promise<boolean>;
export async function readReply(
user: string,
comment: PullRequestCommentIds,
): Promise<boolean>;
export async function readReply(user: string, comment: any): Promise<boolean> {
const replyViewed = moment.utc().format();
if (isIssueCommentIds(comment)) {
return await store.entities.issueComments
.patch(comment)
.set({ replyViewed })
.where(({ replyTo }, { eq }) => eq(replyTo, user))
.go()
.then(() => true)
.catch(() => false);
} else if (isPullRequestCommentIds(comment)) {
return await store.entities.pullRequestComments
.patch(comment)
.set({ replyViewed })
.where(({ replyTo }, { eq }) => eq(replyTo, user))
.go()
.then(() => true)
.catch(() => false);
} else {
return false;
}
}
Approve pull request
export async function approvePullRequest(
repoOwner: string,
repoName: string,
pullRequestNumber: string,
username: string,
) {
const pullRequest = await store.entities.pullRequests
.get({ repoOwner, repoName, pullRequestNumber })
.go();
if (!pullRequest.data || !pullRequest.data.reviewers) {
return false;
}
let index: number = -1;
for (let i = 0; i < pullRequest.data.reviewers.length; i++) {
const reviewer = pullRequest.data.reviewers[i];
if (reviewer.username === username) {
index = i;
}
}
if (index === -1) {
return false;
}
return store.entities.pullRequests
.update({ repoOwner, repoName, pullRequestNumber })
.data(({ reviewers }, { set }) => {
set(reviewers[index].approved, true);
})
.where(
({ reviewers }, { eq }) => `
${eq(reviewers[index].username, username)};
`,
)
.go()
.then(() => true)
.catch(() => false);
}
Follow repository
export async function followRepository(
repoOwner: string,
repoName: string,
follower: string,
) {
await store.entities.repositories
.update({ repoOwner, repoName })
.add({ followers: [follower] })
.go();
}