Skip to content

Instantly share code, notes, and snippets.

@andrestone
Last active March 27, 2023 07:18
Show Gist options
  • Select an option

  • Save andrestone/278ef74b3f2775a4d13abc472f3ea0b2 to your computer and use it in GitHub Desktop.

Select an option

Save andrestone/278ef74b3f2775a4d13abc472f3ea0b2 to your computer and use it in GitHub Desktop.

Revisions

  1. andrestone revised this gist Mar 27, 2023. 1 changed file with 184 additions and 162 deletions.
    346 changes: 184 additions & 162 deletions no-client.ts
    Original file line number Diff line number Diff line change
    @@ -1,129 +1,184 @@
    /* Model queries, see results, share with friends */

    import { Entity, Service } from "electrodb";
    import * as AWS from "@aws-sdk/client-dynamodb";
    import { Entity, Service } from "../../";

    const table = "your_table_name";

    /* Tasks Entity */
    const tasks = new Entity(
    {
    model: {
    entity: "tasks",
    version: "1",
    service: "taskapp"
    },
    attributes: {
    team: {
    type: "string",
    required: true
    export const configuration = {
    endpoint: "http://localhost:8000",
    region: "us-east-1"
    };

    const client = new AWS.DynamoDB(configuration);
    const dynamodb = client;

    const definition = {
    "KeySchema":[
    {
    "AttributeName":"pk",
    "KeyType":"HASH"
    },
    task: {
    type: "string",
    required: true
    {
    "AttributeName":"sk",
    "KeyType":"RANGE"
    }
    ],
    "AttributeDefinitions":[
    {
    "AttributeName":"pk",
    "AttributeType":"S"
    },
    project: {
    type: "string",
    required: true
    {
    "AttributeName":"sk",
    "AttributeType":"S"
    },
    user: {
    type: "string",
    required: true
    {
    "AttributeName":"gsi1pk",
    "AttributeType":"S"
    },
    title: {
    type: "string",
    required: true,
    {
    "AttributeName":"gsi1sk",
    "AttributeType":"S"
    },
    description: {
    type: "string"
    {
    "AttributeName":"gsi2pk",
    "AttributeType":"S"
    },
    status: {
    // use an array to type an enum
    type: ["open", "in-progress", "on-hold", "closed"] as const,
    default: "open"
    {
    "AttributeName":"gsi2sk",
    "AttributeType":"S"
    },
    points: {
    type: "number",
    {
    "AttributeName":"gsi3pk",
    "AttributeType":"S"
    },
    tags: {
    type: "set",
    items: ["green", "red", "blue", "black"]
    {
    "AttributeName":"gsi3sk",
    "AttributeType":"S"
    },
    comments: {
    type: "list",
    items: {
    type: "map",
    properties: {
    user: {
    type: "string"
    },
    body: {
    type: "string"
    }
    }
    }
    {
    "AttributeName":"gsi4pk",
    "AttributeType":"S"
    },
    closed: {
    type: "string",
    validate: /[0-9]{4}-[0-9]{2}-[0-9]{2}/,
    {
    "AttributeName":"gsi4sk",
    "AttributeType":"S"
    },
    createdAt: {
    type: "number",
    default: () => Date.now(),
    // cannot be modified after created
    readOnly: true
    {
    "AttributeName":"gsi5pk",
    "AttributeType":"S"
    },
    updatedAt: {
    type: "number",
    // watch for changes to any attribute
    watch: "*",
    // set current timestamp when updated
    set: () => Date.now(),
    readOnly: true
    {
    "AttributeName":"gsi5sk",
    "AttributeType":"S"
    }
    },
    indexes: {
    projects: {
    pk: {
    field: "pk",
    composite: ["team"]
    },
    sk: {
    field: "sk",
    // create composite keys for partial sort key queries
    composite: ["project", "task"]
    }
    ],
    "GlobalSecondaryIndexes":[
    {
    "IndexName":"gsi1pk-gsi1sk-index",
    "KeySchema":[
    {
    "AttributeName":"gsi1pk",
    "KeyType":"HASH"
    },
    {
    "AttributeName":"gsi1sk",
    "KeyType":"RANGE"
    }
    ],
    "Projection":{
    "ProjectionType":"ALL"
    }
    },
    assigned: {
    // collections allow for queries across multiple entities
    collection: "assignments",
    index: "gsi1pk-gsi1sk-index",
    pk: {
    // map to your GSI Hash/Partition key
    field: "gsi1pk",
    composite: ["user"]
    },
    sk: {
    // map to your GSI Range/Sort key
    field: "gsi1sk",
    composite: ["status"]
    }
    {
    "IndexName":"gsi2pk-gsi2sk-index",
    "KeySchema":[
    {
    "AttributeName":"gsi2pk",
    "KeyType":"HASH"
    },
    {
    "AttributeName":"gsi2sk",
    "KeyType":"RANGE"
    }
    ],
    "Projection":{
    "ProjectionType":"ALL"
    }
    },
    backlog: {
    // map to the GSI name on your DynamoDB table
    index: "gsi2pk-gsi2sk-index",
    pk: {
    field: "gsi2pk",
    composite: ["project"]
    {
    "IndexName":"gsi3pk-gsi3sk-index",
    "KeySchema":[
    {
    "AttributeName":"gsi3pk",
    "KeyType":"HASH"
    },
    {
    "AttributeName":"gsi3sk",
    "KeyType":"RANGE"
    }
    ],
    "Projection":{
    "ProjectionType":"ALL"
    }
    },
    {
    "IndexName":"gsi4pk-gsi4sk-index",
    "KeySchema":[
    {
    "AttributeName":"gsi4pk",
    "KeyType":"HASH"
    },
    {
    "AttributeName":"gsi4sk",
    "KeyType":"RANGE"
    }
    ],
    "Projection":{
    "ProjectionType":"ALL"
    }
    },
    {
    "IndexName":"gsi5pk-gsi5sk-index",
    "KeySchema":[
    {
    "AttributeName":"gsi5pk",
    "KeyType":"HASH"
    },
    {
    "AttributeName":"gsi5sk",
    "KeyType":"RANGE"
    }
    ],
    "Projection":{
    "ProjectionType":"ALL"
    }
    }
    ],
    "BillingMode":"PAY_PER_REQUEST"
    }

    export function createTableManager() {
    return {
    async exists() {
    let tables = await dynamodb.listTables({});
    return !!tables.TableNames?.includes(table);
    },
    sk: {
    field: "gsi2sk",
    composite: ["team", "closed"],
    async drop() {
    return dynamodb.deleteTable({TableName: table});
    },
    async create() {
    return dynamodb.createTable({...definition, TableName: table});
    }
    }
    }
    },
    { table }
    );
    }
    async function initializeTable() {
    const tableManager = createTableManager();
    const exists = await tableManager.exists();
    if (exists) {
    await tableManager.drop();
    }
    await tableManager.create();
    }

    /* Users Entity */
    const users = new Entity(
    @@ -161,7 +216,8 @@ const users = new Entity(
    }
    },
    manager: {
    type: "string",
    type: "set",
    items: ["frank", "jane", "joe", "sally"] as const,
    },
    firstName: {
    type: "string"
    @@ -244,65 +300,31 @@ const users = new Entity(
    { table }
    );

    const app = new Service({ users, tasks });
    new Service({ users }, { table, client, });

    /* Write queries to generate parameters on the right */

    const team = "green";
    const user = "d.huynh";
    const project = "core";
    const task = "45-6620";

    // complex objects are supported and typed in ElectroDB
    const comment = {
    user: "janet",
    body: "This seems half-baked."
    };

    // add a comment, tag, and update item's status with a condition
    tasks
    .patch({ task, project, team })
    .set({ status: "on-hold" })
    .add({ tags: ["half-baked"] })
    .append({ comments: [comment] })
    .where(( {status}, {eq} ) => eq(status, "in-progress"))
    .go();
    const run = async () => {

    const january = "2021-01";
    const july = "2021-07";
    await initializeTable();

    // sort key query conditions are first class in ElectroDB
    tasks.query
    .backlog({ project })
    .between(
    { team, closed: january },
    { team, closed: july },
    )
    .where(({title}, {contains}) => contains(title, "database"))
    .go({order: 'desc'});

    // use a collection to query more than one entity at a time
    app.collections
    .assignments({ user })
    .where(({ points }, { notExists, between }) => `
    ${notExists(points)} OR ${between(points, 1, 5)}
    `)
    .go({pages: 'all'});
    // `create` is like `put` except it uses "attribute_not_exists"
    // to ensure you do not overwrite a record that already exists
    users.create({
    team: "purple",
    user: "t.walch",
    role: "senior",
    lastName: "walch",
    firstName: "tyler",
    manager: ["jane", "frank"], // This fails if Entity is instantiated without a client
    profile: {
    bio: "makes things",
    photo: "selfie.jpg",
    location: "atlanta"
    },
    // interact with DynamoDB sets like arrays
    following: ["d.purdy"]
    }).go();
    };

    // `create` is like `put` except it uses "attribute_not_exists"
    // to ensure you do not overwrite a record that already exists
    users.create({
    team: "purple",
    user: "t.walch",
    role: "senior",
    lastName: "walch",
    firstName: "tyler",
    manager: "d.purdy",
    profile: {
    bio: "makes things",
    photo: "selfie.jpg",
    location: "atlanta"
    },
    // interact with DynamoDB sets like arrays
    following: ["d.purdy"]
    }).go();
    run();
  2. andrestone created this gist Mar 27, 2023.
    308 changes: 308 additions & 0 deletions no-client.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,308 @@
    /* Model queries, see results, share with friends */

    import { Entity, Service } from "electrodb";

    const table = "your_table_name";

    /* Tasks Entity */
    const tasks = new Entity(
    {
    model: {
    entity: "tasks",
    version: "1",
    service: "taskapp"
    },
    attributes: {
    team: {
    type: "string",
    required: true
    },
    task: {
    type: "string",
    required: true
    },
    project: {
    type: "string",
    required: true
    },
    user: {
    type: "string",
    required: true
    },
    title: {
    type: "string",
    required: true,
    },
    description: {
    type: "string"
    },
    status: {
    // use an array to type an enum
    type: ["open", "in-progress", "on-hold", "closed"] as const,
    default: "open"
    },
    points: {
    type: "number",
    },
    tags: {
    type: "set",
    items: ["green", "red", "blue", "black"]
    },
    comments: {
    type: "list",
    items: {
    type: "map",
    properties: {
    user: {
    type: "string"
    },
    body: {
    type: "string"
    }
    }
    }
    },
    closed: {
    type: "string",
    validate: /[0-9]{4}-[0-9]{2}-[0-9]{2}/,
    },
    createdAt: {
    type: "number",
    default: () => Date.now(),
    // cannot be modified after created
    readOnly: true
    },
    updatedAt: {
    type: "number",
    // watch for changes to any attribute
    watch: "*",
    // set current timestamp when updated
    set: () => Date.now(),
    readOnly: true
    }
    },
    indexes: {
    projects: {
    pk: {
    field: "pk",
    composite: ["team"]
    },
    sk: {
    field: "sk",
    // create composite keys for partial sort key queries
    composite: ["project", "task"]
    }
    },
    assigned: {
    // collections allow for queries across multiple entities
    collection: "assignments",
    index: "gsi1pk-gsi1sk-index",
    pk: {
    // map to your GSI Hash/Partition key
    field: "gsi1pk",
    composite: ["user"]
    },
    sk: {
    // map to your GSI Range/Sort key
    field: "gsi1sk",
    composite: ["status"]
    }
    },
    backlog: {
    // map to the GSI name on your DynamoDB table
    index: "gsi2pk-gsi2sk-index",
    pk: {
    field: "gsi2pk",
    composite: ["project"]
    },
    sk: {
    field: "gsi2sk",
    composite: ["team", "closed"],
    }
    }
    }
    },
    { table }
    );

    /* Users Entity */
    const users = new Entity(
    {
    model: {
    entity: "user",
    service: "taskapp",
    version: "1"
    },
    attributes: {
    team: {
    type: "string"
    },
    user: {
    type: "string"
    },
    role: {
    type: ["dev", "senior", "staff", "principal"] as const,
    set: (title: string) => {
    // save as index for comparison
    return [
    "dev",
    "senior",
    "staff",
    "principal"
    ].indexOf(title);
    },
    get: (index: number) => {
    return [
    "dev",
    "senior",
    "staff",
    "principal"
    ][index] || "other";
    }
    },
    manager: {
    type: "string",
    },
    firstName: {
    type: "string"
    },
    lastName: {
    type: "string"
    },
    fullName: {
    type: "string",
    // never set value to the database
    set: () => undefined,
    // calculate full name on retrieval
    get: (_, {firstName, lastName}) => {
    return `${firstName ?? ""} ${lastName ?? ""}`.trim();
    }
    },
    profile: {
    type: "map",
    properties: {
    photo: {
    type: "string"
    },
    bio: {
    type: "string"
    },
    location: {
    type: "string"
    }
    }
    },
    pinned: {
    type: "any"
    },
    following: {
    type: "set",
    items: "string"
    },
    followers: {
    type: "set",
    items: "string"
    },
    createdAt: {
    type: "number",
    default: () => Date.now(),
    readOnly: true
    },
    updatedAt: {
    type: "number",
    watch: "*",
    set: () => Date.now(),
    readOnly: true
    }
    },
    indexes: {
    members: {
    collection: "organization",
    pk: {
    composite: ["team"],
    field: "pk"
    },
    sk: {
    composite: ["user"],
    field: "sk"
    }
    },
    user: {
    collection: "assignments",
    index: "gsi1pk-gsi1sk-index",
    pk: {
    composite: ["user"],
    field: "gsi1pk"
    },
    sk: {
    field: "gsi1sk",
    composite: []
    }
    }
    }
    },
    { table }
    );

    const app = new Service({ users, tasks });

    /* Write queries to generate parameters on the right */

    const team = "green";
    const user = "d.huynh";
    const project = "core";
    const task = "45-6620";

    // complex objects are supported and typed in ElectroDB
    const comment = {
    user: "janet",
    body: "This seems half-baked."
    };

    // add a comment, tag, and update item's status with a condition
    tasks
    .patch({ task, project, team })
    .set({ status: "on-hold" })
    .add({ tags: ["half-baked"] })
    .append({ comments: [comment] })
    .where(( {status}, {eq} ) => eq(status, "in-progress"))
    .go();

    const january = "2021-01";
    const july = "2021-07";

    // sort key query conditions are first class in ElectroDB
    tasks.query
    .backlog({ project })
    .between(
    { team, closed: january },
    { team, closed: july },
    )
    .where(({title}, {contains}) => contains(title, "database"))
    .go({order: 'desc'});

    // use a collection to query more than one entity at a time
    app.collections
    .assignments({ user })
    .where(({ points }, { notExists, between }) => `
    ${notExists(points)} OR ${between(points, 1, 5)}
    `)
    .go({pages: 'all'});

    // `create` is like `put` except it uses "attribute_not_exists"
    // to ensure you do not overwrite a record that already exists
    users.create({
    team: "purple",
    user: "t.walch",
    role: "senior",
    lastName: "walch",
    firstName: "tyler",
    manager: "d.purdy",
    profile: {
    bio: "makes things",
    photo: "selfie.jpg",
    location: "atlanta"
    },
    // interact with DynamoDB sets like arrays
    following: ["d.purdy"]
    }).go();