Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor entities #345

Merged
merged 22 commits into from
May 12, 2023
Merged

feat: refactor entities #345

merged 22 commits into from
May 12, 2023

Conversation

thantos
Copy link
Contributor

@thantos thantos commented May 8, 2023

Entities now support a more classical entity pattern, more in line with Dynamo where the partition key and optional sort key must be provided at creation time.

The key is a composite key provided in the following ways:

  • Tuple - [partition, sort] - Delete/Get
  • Map - { [partitionField]: value, [sortField]?: value } - Delete/Get
  • In the value - { [partitionField]: value, [sortField]?: value } - Set

When a key is returned, we always use the Map format (or the in the value format for get).

Caveat 1: the schema is now required as I was struggling to get the types to work with just the type parameters.
Caveat 2: existing tables must be renamed and recreated

const myEntity = entity("myEntity", { 
   schema: { key: z.string(), value: z.number() },
   partitionKey: "key" });
const myEntity2 = entity("myEntity2", { 
   schema: { part: z.string(), key: z.string(), value: z.number() }, 
   partitionKey: "part", 
   sortKey: "key"});
// to use numbers or binary as keys, provide the type explictly
const myEntity3 = entity("myEntity3", { 
   schema: { key: z.number(), value: z.number() }, 
   partitionKey: { key: "key", type: "number" }});

const { version } = await myEntity.set({ key: "aValue", value: 1 });
await myEntity.set({ key: "aValue", value: 1 }, { expectedVersion: version  });
await myEntity.get({ key: "aValue" });
await myEntity.get(["aValue"]);
await myEntity.delete({ key: "aValue" });

await myEntity.query({ partition: "aValue" }); // without a sort key, only one value is returned.

@thantos thantos marked this pull request as draft May 8, 2023 05:52
@sam-goodwin
Copy link
Owner

Show some examples in the PR description?

apps/tests/aws-runtime/test/test-service.ts Outdated Show resolved Hide resolved
apps/tests/aws-runtime/test/test-service.ts Outdated Show resolved Hide resolved
apps/tests/aws-runtime/test/test-service.ts Outdated Show resolved Hide resolved
@thantos thantos marked this pull request as ready for review May 8, 2023 16:08
@thantos thantos requested a review from sam-goodwin May 8, 2023 16:08
@sam-goodwin
Copy link
Owner

sam-goodwin commented May 8, 2023

Not sure what's going on with the way we configure partition/sort keys. Should be an array of field names

const myEntity3 = entity("myEntity3", { 
  attributes: { 
    key: z.number(),
    value: z.number(),
    createTime: z.date()
  }, 
  partition: ["key"],
  sort: ["value", "createTime"]
});

await myEntity3.query({
  key: "value",
  value: 1,
  createdTime: {
    startsWith: "2024-01-01T00:00Z",
    between: ["2024", "2025"]
  }
});

const myIndex = myEntity3.index({
  partition: ["key"],
  sort: ["value", "createTime"],
});

@sam-goodwin
Copy link
Owner

Some help with the types for capturing order of PK/SK

type Shape = {
  [attribute in string]: z.ZodType;
};

type Attributes = z.ZodObject<any> | Shape;

type AttributeNames<Attr extends Attributes> = Attr extends z.ZodObject<infer A>
  ? keyof A
  : keyof Attr;

export interface Entity2<
  Attr extends z.ZodObject<any> | Shape,
  Partition extends readonly AttributeNames<Attr>[],
  Sort extends readonly AttributeNames<Attr>[] | undefined = undefined
> {}

export function entity2<
  const Attr extends z.ZodObject<any> | Shape,
  const Partition extends readonly AttributeNames<Attr>[],
  const Sort extends readonly AttributeNames<Attr>[] | undefined = undefined
>(input: {
  attributes: Attr;
  partition: Partition;
  sort?: Sort;
}): Entity2<Attr, Partition, Sort> {
  throw new Error();
}

const a = entity2({
  attributes: z.object({
    key: z.string(),
    id: z.string(),
  }),
  partition: ["key", "id"],
  // sort: ["key", "id"],
});

packages/@eventual/core/src/entity.ts Outdated Show resolved Hide resolved
packages/@eventual/core/src/entity.ts Outdated Show resolved Hide resolved
export interface Entity<E> extends Omit<EntitySpec, "schema" | "streams"> {
export type AnyEntity = Entity<any, string, string | undefined>;

export type EntityKeyType = string | number | EntityBinaryMember;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't suffix with Type

Comment on lines 169 to 175
export type EntityKeyField<E extends EntityValue> = {
[K in keyof E]: K extends string
? E[K] extends EntityKeyType
? K
: never
: never;
}[keyof E];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just keyof E or Extract<keyof E, string>?

: never;
}[keyof E];

export type EntityCompositeKeyFromEntity<E extends AnyEntity> =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should AnyEntity be replaced with default type arguments?

Comment on lines 263 to 264
partitionKey: EntityKeyReference<E, P>;
sortKey?: EntityKeyReference<E, S>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these not just partition: P? Types are a bit out of control.

Usually good practice to avoid overly using mapped types, especially when all you're doing is storing the data directly on the object.

streams: EntityStream<E>[];
partitionKey: EntityKeyReference<E, P>;
sortKey?: EntityKeyReference<E, S>;
schema?: ZodMappedSchema<E>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a ZodMappedSchema?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so it constructs the schema from the value type. Shouldn't we be including the zod types in everything? Go backwards from schema?

Comment on lines 357 to 359
export type ZodMappedSchema<E extends EntityValue> = {
[k in keyof E]: z.Schema<E[k]>;
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this type needed?

Comment on lines 361 to 369
export interface EntityOptions<
E extends EntityValue,
P extends EntityKeyField<E>,
S extends EntityKeyField<E> | undefined
> {
partitionKey: EntityKeyReference<E, P>;
sortKey?: EntityKeyReference<E, S>;
schema: ZodMappedSchema<E>;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is surprisingly complex.

export interface EntityOptions<
  A extends EntityAttributes | z.ZodObject<any>,
  P extends keyof E | (keyof E)[],
  S extends keyof E | (keyof E)[] | undefined = undefined
> {
  partitionKey: P;
  sortKey?: S;
  attributes: A
}

> {
partitionKey: EntityKeyReference<E, P>;
sortKey?: EntityKeyReference<E, S>;
schema: ZodMappedSchema<E>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called attributes?

*
* @default - retrieve values with no namespace.
*/
namespace?: string;
partition: E[Partition];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it only accepts a single value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have not pushed a commit since the last time we talked. Should be ready soon.

Comment on lines 13 to 32
export type EntityMethod = Exclude<
{
[k in keyof AnyEntity]: [AnyEntity[k]] extends [Function] ? k : never;
}[keyof AnyEntity],
"partitionKey" | "sortKey" | "stream" | "__entityBrand" | undefined
>;

/**
* Registers and returns functioning {@link Entity}s.
*
* Does not handle the workflow case. That is handled by the {@link entity} function in core.
*/
export interface EntityHook {
getEntity<Entity>(name: string): Promise<EntityMethods<Entity> | undefined>;
export type EntityHook = {
[K in EntityMethod]: (
entityName: string,
...args: Parameters<AnyEntity[K]>
) => ReturnType<AnyEntity[K]>;
} & {
transactWrite(items: EntityTransactItem<any>[]): Promise<void>;
}
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these types? It's hard to follow

Comment on lines 38 to 44
export type EntityQueryKey<
Attr extends EntityAttributes = EntityAttributes,
Partition extends EntityCompositeKeyPart<Attr> = EntityCompositeKeyPart<Attr>,
Sort extends EntityCompositeKeyPart<Attr> | undefined =
| EntityCompositeKeyPart<Attr>
| undefined
> = Partial<EntityCompositeKey<Attr, Partition, Sort>>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export type EntityQueryKey<
  Attr extends EntityAttributes = EntityAttributes,
  Partition extends EntityCompositeKeyPart<Attr> = EntityCompositeKeyPart<Attr>,
  Sort extends EntityCompositeKeyPart<Attr> | undefined =
    | EntityCompositeKeyPart<Attr>
    | undefined
> = {
  [pk in Partition[number]]: string;
} & (Sort extends undefined
  ? {}
  : ProgressiveQueryKey<Attr, Exclude<Sort, undefined>>);

type ProgressiveQueryKey<
  Attr extends EntityAttributes,
  Sort extends readonly (keyof Attr)[],
  Accum extends object = {}
> = Sort extends []
  ? Accum
  : Sort extends [
      infer k extends keyof Attr,
      ...infer ks extends (keyof Attr)[]
    ]
  ?
      | (Accum & {
          [sk in k]: Attr[sk];
        })
      | ProgressiveQueryKey<
          Attr,
          ks,
          Accum & {
            [sk in Sort[0]]: Attr[sk];
          }
        >
  : never;

@thantos thantos requested a review from sam-goodwin May 11, 2023 21:59
packages/@eventual/core/src/entity/entity.ts Outdated Show resolved Hide resolved
packages/@eventual/core/src/entity/entity.ts Outdated Show resolved Hide resolved
@thantos thantos merged commit b1c39b1 into main May 12, 2023
@thantos thantos deleted the sussman/feat/entity_2 branch May 12, 2023 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants