sam-goodwin / eventual

Build scalable and durable micro-services with APIs, Messaging and Workflows
https://docs.eventual.ai
MIT License
174 stars 4 forks source link

Support single table design #361

Open sam-goodwin opened 1 year ago

sam-goodwin commented 1 year ago

We currently have the problem where an Entity is coupled to a DynamoDB Table. This puts all the burden of single table design on the user because our framework does not help with mapping pk/sk attributes for developers, so the only way currently is to use a union type and manage the pk/sk yourself.

It would be better if our framework provided a pattern for this.

I propose we decouple entity from DynamoDB and introduce a new collection primitive.

export const User = entity("User", {
  attributes: {
    userId: z.string(),
    userName: z.string()
  },
  partition: ["userId"],
});

export const Friend = entity("Friend", {
  attributes: {
    userId: z.string(),
    friendId: z.string()
  },
  partition: ["userId"],
  sort: ["friendId"]
})

export const userCollection = collection("UserService", {
  entities: [
    User,
    Friend
  ]
});

Queries are executed against the collection. We can include the data type always in the key to enable a nice polymorphic, type-narrowing experience.

await userCollection.query({
  // query all Friends
  type: "Friend",
  userId: "<user-id>",
});

Indexes are configured on the collection, but depending on the attributes specified

// create an index to be able to query users who are friends with someone
const usersWithFriend = userCollection.index("usersWithFriend", {
  // narrow only to the Friend type
  type: "Friend",
  partition: ["friendId"],
  sort: ["userId"]
});

TODO: what about indexes on more than one type?

// Option 1 - specify a list of type names
userCollection.index("usersWithFriend", {
  type: ["User", "Friend"],
});

// Option 2 - omit it to mean all types
userCollection.index("usersWithFriend", {
  // ..
});

// Option 3 - pass a reference to the entities themselves
userCollection.index("usersWithFriend", {
  type: [User, Friend],
});

TODO: how can we query across multiple data types (i.e. Joins). How will we construct the PK/SK?

// this should return User and Friend records alike
// - the 1 User with the userId
// - all of a User's Friends 
const usersOrFriends: (User | Friend) = await userCollection.query({
  userId: "userId"
})

// adding friendId back in should now only return Friends
await userCollection.query({
  userId: "userId",
  friendId: "friendId"
})