Version control
Building a version control database
In this example we will be modeling a version control system similar to GitHub
Table Definition
Example Setup
Table Definition
{ "TableName": "electro", "KeySchema": [ { "AttributeName": "pk", "KeyType": "HASH" }, { "AttributeName": "sk", "KeyType": "RANGE" } ], "AttributeDefinitions": [ { "AttributeName": "pk", "AttributeType": "S" }, { "AttributeName": "sk", "AttributeType": "S" }, { "AttributeName": "gsi1pk", "AttributeType": "S" }, { "AttributeName": "gsi1sk", "AttributeType": "S" } ], "GlobalSecondaryIndexes": [ { "IndexName": "gsi1pk-gsi1sk-index", "KeySchema": [ { "AttributeName": "gsi1pk", "KeyType": "HASH" }, { "AttributeName": "gsi1sk", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" } } ], "BillingMode": "PAY_PER_REQUEST" }
Setup
This file is referenced by multiple entities below
./types.ts
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 type IssueIds = {
repoOwner: string;
repoName: string;
issueNumber: string;
};
export type PullRequestIds = {
repoOwner: string;
repoName: string;
pullRequestNumber: string;
};
export type IssueCommentIds = IssueIds & {
commentId: string;
username: string;
};
export type PullRequestCommentIds = PullRequestIds & {
commentId: string;
username: string;
};
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 {
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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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 { NotYetViewed } 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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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: ["updatedAt", "replyViewed"],
field: "gsi2sk",
},
},
},
});PullRequests
import { Entity, CustomAttributeType } from "electrodb";
import {
TicketTypes,
PullRequestTicket,
StatusTypes,
toStatusString,
toStatusCode,
} from "./types";
export type PullRequestReviewer = {
approved: boolean;
updatedAt: string;
}
export type PullRequestReviews = Record<string, PullRequestReviewer>;
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: (_, { pullRequestNumber }) => pullRequestNumber,
},
status: {
type: StatusTypes,
default: "Open",
set: (val) => toStatusCode(val),
get: (val) => toStatusString(val),
},
reviewers: {
type: CustomAttributeType<PullRequestReviews>('any'),
},
createdAt: {
type: "string",
set: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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 { NotYetViewed } 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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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: ["updatedAt", "replyViewed"],
field: "gsi2sk",
},
},
},
});Repositories
import { Entity } from "electrodb";
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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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 { 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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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";
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: () => new Date().toISOString(),
readOnly: true,
},
updatedAt: {
type: "string",
watch: "*",
set: () => new Date().toISOString(),
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
The entities.ts, service.ts, types.ts, and Table Definition tabs show the setup this example runs against, and the generated parameters below are produced live by executing the example.
import { store } from "./service";
export async function getPublicRepository(username: string) {
return store.entities.repositories.query.created({
username,
isPrivate: false,
}).go();
}
await getPublicRepository("tywalch");Generated DynamoDB Parameters
Generating parameters…
Get pull request and associated comments
Note that newest comments will be returned first
import { store } from "./service";
import type { PullRequestIds } from "./types";
export async function reviewPullRequest(
pr: PullRequestIds,
cursor?: string
) {
return store.collections.PRReview(pr).go({ cursor, order: "desc" });
}
await reviewPullRequest({
repoOwner: "tywalch",
repoName: "electrodb",
pullRequestNumber: "414"
});Generated DynamoDB Parameters
Generating parameters…
Get issue and associated comments
Note that newest comments will be returned first
import { store } from "./service";
import type { IssueIds } from "./types";
export async function reviewIssue(
issue: IssueIds,
cursor?: string
) {
return store.collections.issueReview(issue).go({ cursor, order: "desc" });
}
await reviewIssue({ repoOwner: "tywalch", repoName: "electrodb", issueNumber: "287" });Generated DynamoDB Parameters
Generating parameters…
Get pull requests created by user
import { store } from "./service";
import type { Status } from "./types";
export async function getUserPullRequests(
username: string,
status?: Status,
cursor?: string,
) {
return store.entities.pullRequests.query
.created({ username, status })
.go({ cursor, order: "desc" });
}
await getUserPullRequests("tywalch", "Open");Generated DynamoDB Parameters
Generating parameters…
Approve pull request
Note that this can only be performed by an existing reviewer on the pull request — the where condition asserts the reviewer exists, and returnOnConditionCheckFailure reports whether the approval was applied
import { store } from "./service";
export async function approvePullRequest(
repoOwner: string,
repoName: string,
pullRequestNumber: string,
username: string,
) {
return store.entities.pullRequests
.patch({ repoOwner, repoName, pullRequestNumber })
.data(({ reviewers }, { set }) =>
set(reviewers[username], {
approved: true,
updatedAt: new Date().toISOString(),
})
)
.where(({ reviewers }, { exists }) =>
exists(reviewers[username])
)
.go({ returnOnConditionCheckFailure: true })
.then(({ rejected }) => !rejected);
}
await approvePullRequest("tywalch", "electrodb", "414", "sparky");Generated DynamoDB Parameters
Generating parameters…
Get all user info, repos, pull requests, and issues in one query
import type { EntityItem } from "electrodb";
import { issues, pullRequests, repositories, users } from "./entities";
import { store } from "./service";
type OwnedItems = {
issues: EntityItem<typeof issues>[];
pullRequests: EntityItem<typeof pullRequests>[];
repositories: EntityItem<typeof repositories>[];
users: EntityItem<typeof users>[];
};
export async function getOwnedItems(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;
}
await getOwnedItems("tywalch");Generated DynamoDB Parameters
Generating parameters…
Get subscriptions for a given repository, pull request, or issue.
import { store } from "./service";
import { IsNotTicket } from "./types";
export async function getSubscribed(
repoOwner: string,
repoName: string,
ticketNumber: string = IsNotTicket,
) {
return store.collections
.subscribers({ repoOwner, repoName, ticketNumber })
.go();
}
await getSubscribed("tywalch", "electrodb");Generated DynamoDB Parameters
Generating parameters…
Get unread comment replies
import { store } from "./service";
import { NotYetViewed } from "./types";
export async function getUnreadComments(user: string, start: string, end: string) {
const first = { updatedAt: start, replyViewed: NotYetViewed };
const last = { updatedAt: end, replyViewed: NotYetViewed };
const [issues, pullRequests] = await Promise.all([
store.entities.issueComments.query
.replies({ replyTo: user })
.between(first, last)
.go(),
store.entities.pullRequestComments.query
.replies({ replyTo: user })
.between(first, last)
.go(),
]);
return {
issues,
pullRequests,
};
}
await getUnreadComments("tywalch", "0000-00-00", "9999-99-99");Generated DynamoDB Parameters
Generating parameters…
Mark comment reply as read
Note that this can only be done by the user who was being replied to
import { store } from "./service";
import {
isIssueCommentIds,
isPullRequestCommentIds,
type IssueCommentIds,
type PullRequestCommentIds,
} from "./types";
export async function markAsRead(user: string, comment: IssueCommentIds): Promise<boolean>;
export async function markAsRead(user: string, comment: PullRequestCommentIds): Promise<boolean>;
export async function markAsRead(user: string, comment: any): Promise<boolean> {
const replyViewed = new Date().toISOString();
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;
}
}
await markAsRead("tywalch", {
repoOwner: "tywalch",
repoName: "electrodb",
issueNumber: "287",
commentId: "comment-001",
username: "sparky",
});Generated DynamoDB Parameters
Generating parameters…
Follow repository
import { store } from "./service";
export async function followRepository(
repoOwner: string,
repoName: string,
follower: string,
) {
await store.entities.repositories
.update({ repoOwner, repoName })
.add({ followers: [follower] })
.go();
}
await followRepository("tywalch", "electrodb", "sparky");Generated DynamoDB Parameters
Generating parameters…