rotorise
Version:
Supercharge your DynamoDB with Rotorise!
256 lines (243 loc) • 13.8 kB
text/typescript
// Lighter Exact implementation — catches excess top-level properties only.
// Does not recursively check nested objects/arrays. This is intentional:
// DynamoDB entity shapes are flat at the key level.
// Uses DistributiveKeyof so that union-typed Shape (discriminated unions)
// exposes keys from ALL variants, not just the common keys.
type DistributiveKeyof<T> = T extends unknown ? keyof T : never
type Exact<Shape, Candidate> = Candidate & {
[K in keyof Candidate]: K extends DistributiveKeyof<Shape>
? Candidate[K]
: never
}
type ValueOf<
ObjectType,
ValueType = keyof ObjectType,
> = ValueType extends keyof ObjectType ? ObjectType[ValueType] : never
/**
* Force an operation like `{ a: 0 } & { b: 1 }` to be computed so that it displays `{ a: 0; b: 1 }`.
* This version is distributive, meaning it will preserve union types while flattening each member.
*/
type show<T> = T extends unknown
? { [K in keyof T]: T[K] } & unknown
: never
type DistributivePick<T, K> = T extends unknown
? K extends keyof T
? Pick<T, K>
: never
: never
type DistributiveOmit<T, K extends keyof T> = T extends unknown
? Omit<T, K>
: never
type SliceFromStart<
T,
End extends number,
Acc extends unknown[] = [],
> = End extends 0
? []
: End extends 1
? T extends [infer Head, ...unknown[]]
? [Head]
: []
: T extends unknown[]
? Acc['length'] extends End
? Acc
: T extends [infer Head, ...infer Tail]
? SliceFromStart<Tail, End, [...Acc, Head]>
: Acc
: never
type MergeIntersectionObject<T, Keys = keyof T> = {
[K in Keys]: T[K]
}
type NonEmptyArray<T> = [T, ...T[]]
type Replace<T, U, V> = T extends U ? V : T
/**
* Represents a type-level error message. Used to provide helpful feedback in the IDE.
*/
declare const errorMessage: unique symbol
type ErrorMessage<T extends string> = {
readonly [errorMessage]: T
}
type TransformOverride<Spec extends InputSpecShape, K, Fallback, Matched = Extract<Spec[number], [K, (...args: any[]) => any, ...any[]]>> = [Matched] extends [never] ? Fallback : (Matched extends [any, (x: infer P) => any, ...any[]] ? (x: P) => void : never) extends (x: infer I) => void ? I : Fallback;
type CompositeKeyParamsImpl<Entity, InputSpec extends InputSpecShape, skip extends number = 1> = Entity extends unknown ? show<{
[K in extractHeadOrPass<SliceFromStart<InputSpec, number extends skip ? 1 : skip>[number]> & keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>;
} & {
[K in extractHeadOrPass<InputSpec[number]> & keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>;
}> : never;
type CompositeKeyParams<Entity extends Record<string, unknown>, FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[], skip extends number = 1> = CompositeKeyParamsImpl<Entity, FullSpec, skip>;
type CompositeKeyBuilderImpl<Entity, Spec, Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = Entity extends unknown ? CompositeKeyStringBuilder<Entity, [
Deep
] extends [never] ? Spec : number extends Deep ? Spec : SliceFromStart<Spec, Deep>, Separator, boolean extends isPartial ? false : isPartial> : never;
type CompositeKeyBuilder<Entity extends Record<string, unknown>, Spec extends InputSpec<MergeIntersectionObject<Entity>>[], Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = CompositeKeyBuilderImpl<Entity, Spec, Separator, Deep, isPartial>;
type joinable = string | number | bigint | boolean | null | undefined;
type ExtractHelper<Key, Value> = Value extends object ? Value extends {
tag: infer Tag extends string;
value: infer Value extends joinable;
} ? [Tag, Value] : Value extends {
value: infer Value extends joinable;
} ? [never, Value] : never : [Key, Value];
type ExtractPair<Entity, Spec> = Spec extends [
infer Key extends string,
(...key: any[]) => infer Value,
...unknown[]
] ? ExtractHelper<Uppercase<Key>, Value> : Spec extends keyof Entity & string ? [Uppercase<Spec>, Entity[Spec] & joinable] : never;
type CompositeKeyStringBuilder<Entity, Spec, Separator extends string, KeepIntermediate extends boolean, Acc extends string = '', AllAcc extends string = never> = Spec extends [infer Head, ...infer Tail] ? ExtractPair<Entity, Head> extends [
infer Key extends joinable,
infer Value extends joinable
] ? CompositeKeyStringBuilder<Entity, Tail, Separator, KeepIntermediate, Acc extends '' ? [Key] extends [never] ? `${Value}` : `${Key}${Separator}${Value}` : [Key] extends [never] ? `${Acc}${Separator}${Value}` : `${Acc}${Separator}${Key}${Separator}${Value}`, KeepIntermediate extends true ? AllAcc | (Acc extends '' ? never : Acc) : never> : never : AllAcc | Acc;
type DiscriminatedSchemaShape = {
discriminator: PropertyKey;
spec: {
[k in PropertyKey]: unknown;
};
};
type InputSpecShape = ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[];
type TransformShape = {
tag?: string;
value: joinable;
} | joinable;
type ComputeTableKeyType<Entity, Spec, Separator extends string, NullAs extends never | undefined = never> = Spec extends InputSpecShape ? CompositeKeyBuilderImpl<Entity, Spec, Separator, number, false> : Spec extends keyof Entity ? Replace<Entity[Spec], null, undefined> : Spec extends null ? NullAs : never;
type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity extends unknown ? show<{
readonly [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape ? ComputeTableKeyType<Entity, ValueOf<Schema[Key]['spec'], ValueOf<Entity, Schema[Key]['discriminator']>>, Separator> : Schema[Key] extends keyof Entity | InputSpecShape | null ? ComputeTableKeyType<Entity, Schema[Key], Separator> : ErrorMessage<'Invalid schema definition'>;
} & Entity> : never;
/**
* Represents a complete DynamoDB table entry, combining the original entity
* with its computed internal and global keys.
*
* @template Entity The base entity type.
* @template Schema The schema defining the table keys.
* @template Separator The string used to join composite key components (default: '#').
*/
type TableEntry<Entity extends Record<string, unknown>, Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = '#'> = TableEntryImpl<Entity, Schema, Separator>;
type DefaultOf<T> = T extends Record<string, unknown> ? Partial<T> : T;
type InputSpec<E> = {
[key in keyof E]: (undefined extends E[key] ? [
key,
(key: Exclude<E[key], undefined>) => TransformShape,
DefaultOf<Exclude<E[key], undefined>>
] : [key, (key: E[key]) => TransformShape]) | (undefined extends E[key] ? never : null extends E[key] ? never : key);
}[keyof E];
type ValidateInputSpec<T> = {
[I in keyof T]: T[I] extends readonly [
unknown,
(arg: infer P) => any,
unknown
] ? [T[I][0], T[I][1], P] : T[I];
};
type Tuple3 = {
length: 3;
};
type NeedsValidation<V> = V extends readonly unknown[] ? Extract<V[number], Tuple3> : V extends {
spec: infer S;
} ? NeedsValidation<S[keyof S]> : never;
type ValidateSchema<Schema> = [NeedsValidation<Schema[keyof Schema]>] extends [
never
] ? unknown : {
[K in keyof Schema]: Schema[K] extends {
discriminator: unknown;
spec: infer Spec;
} ? {
discriminator: Schema[K]['discriminator'];
spec: {
[SV in keyof Spec]: ValidateInputSpec<Spec[SV]>;
};
} : ValidateInputSpec<Schema[K]>;
};
type extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T;
type FullKeySpecSimple<Entity> = NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>> | (keyof Entity & string) | null;
type DiscriminatedSchema<Entity, E> = {
[key in keyof E]: E[key] extends PropertyKey ? {
discriminator: key;
spec: {
[val in E[key]]: FullKeySpecSimple<Extract<Entity, {
[k in key]: val;
}>>;
};
} : never;
}[keyof E];
type FullKeySpec<Entity> = FullKeySpecSimple<Entity> | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>;
declare class RotoriseError extends Error {
constructor(message: string);
}
type ProcessSpecType<Entity, Spec, Config extends SpecConfigShape> = Spec extends string ? DistributivePick<Entity, Spec> : Spec extends InputSpecShape ? CompositeKeyParamsImpl<Entity, Spec, Config['allowPartial'] extends true ? 1 : Extract<Config['depth'], number>> : Spec extends null | undefined ? unknown : ErrorMessage<'Invalid Spec: Expected string, InputSpecShape, null or undefined'>;
type SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape;
type SpecConfigShape = {
depth?: number;
allowPartial?: boolean;
enforceBoundary?: boolean;
};
type ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
Entity
] extends [never] ? never : Extract<Entity, {
[k in K]: V;
}>;
type TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
Entity
] extends [never] ? {
[k in K]: V;
} : Entity & {
[k in K]: V;
};
type ProcessVariant<Entity, K extends PropertyKey, V extends PropertyKey, Spec extends DiscriminatedSchemaShape, Config extends SpecConfigShape, VariantSpec = Spec['spec'][V & keyof Spec['spec']]> = TagVariant<VariantSpec extends null | undefined ? unknown : ProcessSpecType<ExtractVariant<Entity, K, V>, VariantSpec, Config>, K, V>;
type OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<Spec extends DiscriminatedSchemaShape ? {
[K in Spec['discriminator']]: {
[V in keyof Spec['spec']]: ProcessVariant<Entity, K, V, Spec, Config>;
}[keyof Spec['spec']];
}[Spec['discriminator']] : ProcessSpecType<Entity, Spec, Config>>;
type ProcessKey<Entity, Spec, Separator extends string, NullAs extends never | undefined = never, Config extends SpecConfigShape = SpecConfigShape, Attributes = Pick<Entity, Spec & keyof Entity>> = [Entity] extends [never] ? never : Spec extends keyof Entity ? Replace<ValueOf<Attributes>, null, undefined> : Spec extends InputSpecShape ? CompositeKeyBuilderImpl<Entity, Spec, Separator, Exclude<Config['depth'], undefined>, Exclude<Config['allowPartial'], undefined>> : Spec extends null | undefined ? NullAs : ErrorMessage<'Invalid Spec'>;
type OptimizedBuiltKey<Entity, Spec, Separator extends string, Config extends SpecConfigShape, Attributes> = Entity extends unknown ? show<Spec extends DiscriminatedSchemaShape ? ProcessKey<Entity, ValueOf<Spec['spec'], ValueOf<Entity, Spec['discriminator']>>, Separator, undefined, Config, Attributes> : ProcessKey<Entity, Spec, Separator, undefined, Config, Attributes>> : never;
type TableEntryDefinition<Entity, Schema, Separator extends string> = {
/**
* Converts a raw entity into a complete table entry with all keys computed.
* Use this when preparing items for insertion into DynamoDB.
*/
toEntry: <const ExactEntity>(item: Exact<Entity, ExactEntity>) => TableEntryImpl<ExactEntity, Schema, Separator>;
/**
* Extracts the raw entity from a table entry by removing all computed keys.
* Use this when processing items retrieved from DynamoDB.
*/
fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(entry: Entry) => DistributiveOmit<Entry, keyof Schema>;
/**
* Generates a specific key for the given entity attributes.
* Supports partial keys and depth limiting for query operations.
*
* @param key The name of the key to generate (e.g., 'PK', 'GSIPK').
* @param attributes the object containing the values needed to build the key.
* @param config Optional configuration for partial keys or depth limiting.
*/
key: <const Key extends keyof Schema, const Config extends SpecConfig<Spec>, const Attributes extends OptimizedAttributes<Entity, Spec, Config_>, Spec = Schema[Key], Config_ extends SpecConfigShape = [SpecConfigShape] extends [Config] ? {
depth?: undefined;
allowPartial?: undefined;
enforceBoundary?: boolean;
} : Config>(key: Key, attributes: Attributes, config?: Config) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>;
/**
* A zero-runtime inference helper. Use this with `typeof` to get the
* total type of a table entry.
*/
infer: TableEntryImpl<Entity, Schema, Separator>;
/**
* Creates a proxy to generate property paths as strings.
* Useful for building UpdateExpressions or ProjectionExpressions.
*
* @example
* table.path().data.nested.property.toString() // returns "data.nested.property"
*/
path: () => TableEntryImpl<Entity, Schema, Separator>;
};
/**
* Entry point for defining a DynamoDB table schema with Rotorise.
*
* @template Entity The base entity type that this table represents.
* @returns A builder function that accepts the schema and an optional separator.
*
* Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.
*
* @example
* const userTable = tableEntry<User>()({
* PK: ["orgId", "id"],
* SK: "role"
* })
*/
declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema & ValidateSchema<Schema>, ...[separator]: [Separator] extends [
''
] ? [ErrorMessage<'Separator must not be an empty string'>] : [separator?: Separator]) => TableEntryDefinition<Entity, Schema, Separator>;
export { type CompositeKeyBuilder, type CompositeKeyParams, type CompositeKeyParamsImpl, RotoriseError, type TableEntry, type TransformShape, tableEntry };