UNPKG

rotorise

Version:

Supercharge your DynamoDB with Rotorise!

256 lines (243 loc) 13.8 kB
// 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 };