On this page

On this page

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.

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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.

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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

Edit in Playground ↗
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…