rotorise 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/Rotorise.ts"],"names":["key"],"mappings":"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","file":"Rotorise.cjs","sourcesContent":["import type {\n DistributiveOmit,\n DistributivePick,\n ErrorMessage,\n Exact,\n MergeIntersectionObject,\n NonEmptyArray,\n Replace,\n SliceFromStart,\n show,\n ValueOf,\n} from './utils'\n\n// When a spec item has a transform function, use the transform's parameter type\n// instead of the entity's property type. This allows callers to pass only the\n// fields the transform actually needs (e.g. Pick<Obj, 'id'> instead of Obj).\ntype TransformOverride<\n Spec extends InputSpecShape,\n K,\n Fallback,\n Matched = Extract<Spec[number], [K, (...args: any[]) => any, ...any[]]>,\n> = [Matched] extends [never]\n ? Fallback\n : // Contravariant inference: when the same key appears multiple times with\n // different transforms (e.g. Pick<Obj,'id'> and Pick<Obj,'name'>),\n // this intersects the parameter types rather than unioning them.\n (\n Matched extends [any, (x: infer P) => any, ...any[]] // biome-ignore lint/suspicious/noExplicitAny: inference\n ? (x: P) => void\n : never\n ) extends (x: infer I) => void\n ? I\n : Fallback\n\nexport type CompositeKeyParamsImpl<\n Entity,\n InputSpec extends InputSpecShape,\n skip extends number = 1,\n> = Entity extends unknown\n ? show<\n {\n [K in extractHeadOrPass<\n SliceFromStart<\n InputSpec,\n number extends skip ? 1 : skip\n >[number]\n > &\n keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>\n } & {\n [K in extractHeadOrPass<InputSpec[number]> &\n keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>\n }\n >\n : never\n\nexport type CompositeKeyParams<\n Entity extends Record<string, unknown>,\n FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[],\n skip extends number = 1,\n> = CompositeKeyParamsImpl<Entity, FullSpec, skip>\n\ntype CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = Entity extends unknown\n ? CompositeKeyStringBuilder<\n Entity,\n [Deep] extends [never]\n ? Spec\n : number extends Deep\n ? Spec\n : SliceFromStart<Spec, Deep>,\n Separator,\n boolean extends isPartial ? false : isPartial\n >\n : never\n\nexport type CompositeKeyBuilder<\n Entity extends Record<string, unknown>,\n Spec extends InputSpec<MergeIntersectionObject<Entity>>[],\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = CompositeKeyBuilderImpl<Entity, Spec, Separator, Deep, isPartial>\n\ntype joinable = string | number | bigint | boolean | null | undefined\n\ntype ExtractHelper<Key, Value> = Value extends object\n ? Value extends {\n tag: infer Tag extends string\n value: infer Value extends joinable\n }\n ? [Tag, Value]\n : Value extends {\n value: infer Value extends joinable\n }\n ? [never, Value]\n : never\n : [Key, Value]\n\ntype ExtractPair<Entity, Spec> = Spec extends [\n infer Key extends string,\n // biome-ignore lint/suspicious/noExplicitAny: required for generic transform inference\n (...key: any[]) => infer Value,\n ...unknown[],\n]\n ? ExtractHelper<Uppercase<Key>, Value>\n : Spec extends keyof Entity & string\n ? [Uppercase<Spec>, Entity[Spec] & joinable]\n : never\n\ntype CompositeKeyStringBuilder<\n Entity,\n Spec,\n Separator extends string,\n KeepIntermediate extends boolean,\n Acc extends string = '',\n AllAcc extends string = never,\n> = Spec extends [infer Head, ...infer Tail]\n ? ExtractPair<Entity, Head> extends [\n infer Key extends joinable,\n infer Value extends joinable,\n ]\n ? CompositeKeyStringBuilder<\n Entity,\n Tail,\n Separator,\n KeepIntermediate,\n Acc extends ''\n ? [Key] extends [never]\n ? `${Value}`\n : `${Key}${Separator}${Value}`\n : [Key] extends [never]\n ? `${Acc}${Separator}${Value}`\n : `${Acc}${Separator}${Key}${Separator}${Value}`,\n KeepIntermediate extends true\n ? AllAcc | (Acc extends '' ? never : Acc)\n : never\n >\n : never\n : AllAcc | Acc\n\ntype DiscriminatedSchemaShape = {\n discriminator: PropertyKey\n spec: {\n [k in PropertyKey]: unknown\n }\n}\n\ntype InputSpecShape =\n // biome-ignore lint/suspicious/noExplicitAny: key type is erased at runtime, any is needed for structural matching\n ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[]\n\nexport type TransformShape =\n | {\n tag?: string\n value: joinable\n }\n | joinable\n\ntype ComputeTableKeyType<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n> = Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<Entity, Spec, Separator, number, false>\n : Spec extends keyof Entity\n ? Replace<Entity[Spec], null, undefined>\n : Spec extends null\n ? NullAs\n : never\n\ntype TableEntryImpl<\n Entity,\n Schema,\n Separator extends string = '#',\n> = Entity extends unknown\n ? show<\n {\n readonly [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape\n ? ComputeTableKeyType<\n Entity,\n ValueOf<\n Schema[Key]['spec'],\n ValueOf<Entity, Schema[Key]['discriminator']>\n >,\n Separator\n >\n : Schema[Key] extends keyof Entity | InputSpecShape | null\n ? ComputeTableKeyType<Entity, Schema[Key], Separator>\n : ErrorMessage<'Invalid schema definition'>\n } & Entity\n >\n : never\n\n/**\n * Represents a complete DynamoDB table entry, combining the original entity\n * with its computed internal and global keys.\n *\n * @template Entity The base entity type.\n * @template Schema The schema defining the table keys.\n * @template Separator The string used to join composite key components (default: '#').\n */\nexport type TableEntry<\n Entity extends Record<string, unknown>,\n Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n> = TableEntryImpl<Entity, Schema, Separator>\n\ntype InputSpec<E> = {\n [key in keyof E]:\n | (undefined extends E[key]\n ? [\n key,\n (key: Exclude<E[key], undefined>) => TransformShape,\n Exclude<E[key], undefined>,\n ]\n : [key, (key: Exclude<E[key], undefined>) => TransformShape])\n | (undefined extends E[key] ? never : null extends E[key] ? never : key)\n}[keyof E]\n\ntype extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T\n\ntype FullKeySpecSimple<Entity> =\n | NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>>\n | (keyof Entity & string)\n | null\n\ntype FullKeySpecSimpleShape = InputSpecShape | string | null\n\ntype DiscriminatedSchema<Entity, E> = {\n [key in keyof E]: E[key] extends PropertyKey\n ? {\n discriminator: key\n spec: {\n [val in E[key]]: FullKeySpecSimple<\n Extract<\n Entity,\n {\n [k in key]: val\n }\n >\n >\n }\n }\n : never\n}[keyof E]\n\ntype FullKeySpec<Entity> =\n | FullKeySpecSimple<Entity>\n | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>\n\ntype FullKeySpecShape = FullKeySpecSimpleShape | DiscriminatedSchemaShape\n\nexport class RotoriseError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'RotoriseError'\n }\n}\n\n// Runtime implementation uses `as never` casts because the generic types are\n// too complex for TS to verify at the value level. Type correctness is enforced\n// by the type-level types (CompositeKeyStringBuilder, TableEntryImpl, etc.) and\n// validated by the attest-based test suite.\n\nconst chainableNoOpProxy: unknown = new Proxy(() => chainableNoOpProxy, {\n get: () => chainableNoOpProxy,\n})\n\nconst createPathProxy = <T>(path = ''): T => {\n return new Proxy(() => {}, {\n get: (_target, prop) => {\n if (typeof prop === 'string') {\n if (prop === 'toString') {\n return () => path\n }\n\n return createPathProxy(\n path === ''\n ? prop\n : !Number.isNaN(Number.parseInt(prop))\n ? `${path}[${prop}]`\n : `${path}.${prop}`,\n )\n }\n },\n }) as T\n}\n\nconst key =\n <const Entity>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <\n const Key extends keyof Schema,\n const Config extends {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n },\n const Attributes extends Partial<Entity>,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ): string | undefined => {\n const case_ = schema[key]\n\n if (case_ === undefined) {\n throw new RotoriseError(`Key ${key.toString()} not found in schema`)\n }\n let structure: InputSpec<MergeIntersectionObject<Entity>>[]\n\n if (Array.isArray(case_)) {\n structure = case_\n } else if (typeof case_ === 'object') {\n const discriminator =\n attributes[case_.discriminator as keyof Attributes]\n if (discriminator === undefined) {\n throw new RotoriseError(\n `Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n const val = case_.spec[discriminator as keyof typeof case_.spec]\n if (val === undefined) {\n throw new RotoriseError(\n `Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n if (val === null) {\n return undefined\n }\n\n if (!Array.isArray(val)) {\n return attributes[val as keyof Attributes] as never\n }\n\n structure = val\n } else {\n const value = attributes[case_ as keyof Attributes]\n if (value == null) return undefined as never\n\n return value as never\n }\n\n const fullLength = structure.length\n\n if (config?.depth !== undefined) {\n structure = structure.slice(0, config.depth) as typeof structure\n }\n const composite: joinable[] = []\n\n for (const keySpec of structure) {\n const [key, transform, Default] = Array.isArray(keySpec)\n ? keySpec\n : [keySpec]\n\n const value = attributes[key as keyof Attributes] ?? Default\n\n if (transform && value !== undefined) {\n const transformed = transform(value as never)\n if (typeof transformed === 'object' && transformed !== null) {\n if (transformed.tag !== undefined)\n composite.push(transformed.tag)\n composite.push(transformed.value)\n } else {\n composite.push(key.toString().toUpperCase())\n composite.push(transformed)\n }\n } else if (value !== undefined && value !== null && value !== '') {\n composite.push(key.toString().toUpperCase())\n composite.push(value as joinable)\n } else if (config?.allowPartial) {\n break\n } else {\n throw new RotoriseError(\n `buildCompositeKey: Attribute ${key.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n }\n\n // Each spec element produces 2 segments (KEY, value). If fewer segments\n // were emitted than expected (partial key), append a trailing separator.\n if (config?.enforceBoundary && fullLength * 2 > composite.length) {\n composite.push('')\n }\n\n return composite.join(separator) as never\n }\n\nconst toEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <const ExactEntity extends Entity>(\n item: ExactEntity,\n ): ExactEntity extends infer E extends Entity\n ? TableEntryImpl<E, Schema, Separator>\n : never => {\n const entry = { ...item }\n const buildKey = key<Entity>()(schema, separator)\n\n for (const key_ in schema) {\n const val = buildKey(key_, item)\n if (val !== undefined) {\n entry[key_] = val satisfies string as never\n }\n }\n return entry as never\n }\n\nconst fromEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpecShape>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ) =>\n <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ): DistributiveOmit<Entry, keyof Schema> => {\n const item = { ...entry }\n\n for (const key_ in schema) {\n delete item[key_]\n }\n return item as never\n }\n\ntype ProcessSpecType<\n Entity,\n Spec,\n Config extends SpecConfigShape,\n> = Spec extends string\n ? DistributivePick<Entity, Spec>\n : Spec extends InputSpecShape\n ? CompositeKeyParamsImpl<\n Entity,\n Spec,\n Config['allowPartial'] extends true\n ? 1\n : Extract<Config['depth'], number>\n >\n : Spec extends null | undefined\n ? unknown\n : ErrorMessage<'Invalid Spec: Expected string, InputSpecShape, null or undefined'>\n\n// Cache commonly used conditional types\ntype SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape\n\ntype SpecConfigShape = {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n}\n\n// Pre-compute discriminated variant types\ntype ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? never\n : Extract<Entity, { [k in K]: V }>\n\ntype TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? { [k in K]: V }\n : Entity & { [k in K]: V }\n\n// Flatten nested type computation\ntype ProcessVariant<\n Entity,\n K extends PropertyKey,\n V extends PropertyKey,\n Spec extends DiscriminatedSchemaShape,\n Config extends SpecConfigShape,\n VariantSpec = Spec['spec'][V & keyof Spec['spec']],\n> = TagVariant<\n VariantSpec extends null | undefined\n ? unknown\n : ProcessSpecType<ExtractVariant<Entity, K, V>, VariantSpec, Config>,\n K,\n V\n>\n\n// Optimized attribute processing\ntype OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<\n Spec extends DiscriminatedSchemaShape\n ? {\n [K in Spec['discriminator']]: {\n [V in keyof Spec['spec']]: ProcessVariant<\n Entity,\n K,\n V,\n Spec,\n Config\n >\n }[keyof Spec['spec']]\n }[Spec['discriminator']]\n : ProcessSpecType<Entity, Spec, Config>\n>\n\ntype ProcessKey<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n Config extends SpecConfigShape = SpecConfigShape,\n Attributes = Pick<Entity, Spec & keyof Entity>,\n> = [Entity] extends [never]\n ? never\n : Spec extends keyof Entity\n ? Replace<ValueOf<Attributes>, null, undefined>\n : Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator,\n Exclude<Config['depth'], undefined>,\n Exclude<Config['allowPartial'], undefined>\n >\n : Spec extends null | undefined\n ? NullAs\n : ErrorMessage<'Invalid Spec'>\n\ntype OptimizedBuiltKey<\n Entity,\n Spec,\n Separator extends string,\n Config extends SpecConfigShape,\n Attributes,\n> = Entity extends unknown\n ? show<\n Spec extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<\n Spec['spec'],\n ValueOf<Entity, Spec['discriminator']>\n >,\n Separator,\n undefined,\n Config,\n Attributes\n >\n : ProcessKey<\n Entity,\n Spec,\n Separator,\n undefined,\n Config,\n Attributes\n >\n >\n : never\n\ntype TableEntryDefinition<Entity, Schema, Separator extends string> = {\n /**\n * Converts a raw entity into a complete table entry with all keys computed.\n * Use this when preparing items for insertion into DynamoDB.\n */\n toEntry: <const ExactEntity>(\n item: Exact<Entity, ExactEntity>,\n ) => TableEntryImpl<ExactEntity, Schema, Separator>\n\n /**\n * Extracts the raw entity from a table entry by removing all computed keys.\n * Use this when processing items retrieved from DynamoDB.\n */\n fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ) => DistributiveOmit<Entry, keyof Schema>\n\n /**\n * Generates a specific key for the given entity attributes.\n * Supports partial keys and depth limiting for query operations.\n *\n * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').\n * @param attributes the object containing the values needed to build the key.\n * @param config Optional configuration for partial keys or depth limiting.\n */\n key: <\n const Key extends keyof Schema,\n const Config extends SpecConfig<Spec>,\n const Attributes extends OptimizedAttributes<Entity, Spec, Config_>,\n Spec = Schema[Key],\n Config_ extends SpecConfigShape = [SpecConfigShape] extends [Config]\n ? {\n depth?: undefined\n allowPartial?: undefined\n enforceBoundary?: boolean\n }\n : Config,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>\n\n /**\n * A zero-runtime inference helper. Use this with `typeof` to get the\n * total type of a table entry.\n */\n infer: TableEntryImpl<Entity, Schema, Separator>\n\n /**\n * Creates a proxy to generate property paths as strings.\n * Useful for building UpdateExpressions or ProjectionExpressions.\n *\n * @example\n * table.path().data.nested.property.toString() // returns \"data.nested.property\"\n */\n path: () => TableEntryImpl<Entity, Schema, Separator>\n}\n\n/**\n * Entry point for defining a DynamoDB table schema with Rotorise.\n *\n * @template Entity The base entity type that this table represents.\n * @returns A builder function that accepts the schema and an optional separator.\n *\n * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.\n *\n * @example\n * const userTable = tableEntry<User>()({\n * PK: [\"orgId\", \"id\"],\n * SK: \"role\"\n * })\n */\nexport const tableEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ...[separator]: [Separator] extends ['']\n ? [ErrorMessage<'Separator must not be an empty string'>]\n : [separator?: Separator]\n ): TableEntryDefinition<Entity, Schema, Separator> => {\n const sep = separator ?? ('#' as Separator)\n if (sep === '' || typeof sep !== 'string') {\n throw new RotoriseError('Separator must not be an empty string')\n }\n return {\n toEntry: toEntry()(schema as never, sep) as never,\n fromEntry: fromEntry()(schema as never) as never,\n key: key()(schema as never, sep) as never,\n infer: chainableNoOpProxy as never,\n path: () => createPathProxy() as never,\n }\n }\n"]}
1
+ {"version":3,"sources":["../src/Rotorise.ts"],"names":["key"],"mappings":"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","file":"Rotorise.cjs","sourcesContent":["import type {\n DistributiveOmit,\n DistributivePick,\n ErrorMessage,\n Exact,\n MergeIntersectionObject,\n NonEmptyArray,\n Replace,\n SliceFromStart,\n show,\n ValueOf,\n} from './utils'\n\n// When a spec item has a transform function, use the transform's parameter type\n// instead of the entity's property type. This allows callers to pass only the\n// fields the transform actually needs (e.g. Pick<Obj, 'id'> instead of Obj).\ntype TransformOverride<\n Spec extends InputSpecShape,\n K,\n Fallback,\n Matched = Extract<Spec[number], [K, (...args: any[]) => any, ...any[]]>,\n> = [Matched] extends [never]\n ? Fallback\n : // Contravariant inference: when the same key appears multiple times with\n // different transforms (e.g. Pick<Obj,'id'> and Pick<Obj,'name'>),\n // this intersects the parameter types rather than unioning them.\n (\n Matched extends [any, (x: infer P) => any, ...any[]] // biome-ignore lint/suspicious/noExplicitAny: inference\n ? (x: P) => void\n : never\n ) extends (x: infer I) => void\n ? I\n : Fallback\n\nexport type CompositeKeyParamsImpl<\n Entity,\n InputSpec extends InputSpecShape,\n skip extends number = 1,\n> = Entity extends unknown\n ? show<\n {\n [K in extractHeadOrPass<\n SliceFromStart<\n InputSpec,\n number extends skip ? 1 : skip\n >[number]\n > &\n keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>\n } & {\n [K in extractHeadOrPass<InputSpec[number]> &\n keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>\n }\n >\n : never\n\nexport type CompositeKeyParams<\n Entity extends Record<string, unknown>,\n FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[],\n skip extends number = 1,\n> = CompositeKeyParamsImpl<Entity, FullSpec, skip>\n\ntype CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = Entity extends unknown\n ? CompositeKeyStringBuilder<\n Entity,\n [Deep] extends [never]\n ? Spec\n : number extends Deep\n ? Spec\n : SliceFromStart<Spec, Deep>,\n Separator,\n boolean extends isPartial ? false : isPartial\n >\n : never\n\nexport type CompositeKeyBuilder<\n Entity extends Record<string, unknown>,\n Spec extends InputSpec<MergeIntersectionObject<Entity>>[],\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = CompositeKeyBuilderImpl<Entity, Spec, Separator, Deep, isPartial>\n\ntype joinable = string | number | bigint | boolean | null | undefined\n\ntype ExtractHelper<Key, Value> = Value extends object\n ? Value extends {\n tag: infer Tag extends string\n value: infer Value extends joinable\n }\n ? [Tag, Value]\n : Value extends {\n value: infer Value extends joinable\n }\n ? [never, Value]\n : never\n : [Key, Value]\n\ntype ExtractPair<Entity, Spec> = Spec extends [\n infer Key extends string,\n // biome-ignore lint/suspicious/noExplicitAny: required for generic transform inference\n (...key: any[]) => infer Value,\n ...unknown[],\n]\n ? ExtractHelper<Uppercase<Key>, Value>\n : Spec extends keyof Entity & string\n ? [Uppercase<Spec>, Entity[Spec] & joinable]\n : never\n\ntype CompositeKeyStringBuilder<\n Entity,\n Spec,\n Separator extends string,\n KeepIntermediate extends boolean,\n Acc extends string = '',\n AllAcc extends string = never,\n> = Spec extends [infer Head, ...infer Tail]\n ? ExtractPair<Entity, Head> extends [\n infer Key extends joinable,\n infer Value extends joinable,\n ]\n ? CompositeKeyStringBuilder<\n Entity,\n Tail,\n Separator,\n KeepIntermediate,\n Acc extends ''\n ? [Key] extends [never]\n ? `${Value}`\n : `${Key}${Separator}${Value}`\n : [Key] extends [never]\n ? `${Acc}${Separator}${Value}`\n : `${Acc}${Separator}${Key}${Separator}${Value}`,\n KeepIntermediate extends true\n ? AllAcc | (Acc extends '' ? never : Acc)\n : never\n >\n : never\n : AllAcc | Acc\n\ntype DiscriminatedSchemaShape = {\n discriminator: PropertyKey\n spec: {\n [k in PropertyKey]: unknown\n }\n}\n\ntype InputSpecShape =\n // biome-ignore lint/suspicious/noExplicitAny: key type is erased at runtime, any is needed for structural matching\n ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[]\n\nexport type TransformShape =\n | {\n tag?: string\n value: joinable\n }\n | joinable\n\ntype ComputeTableKeyType<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n> = Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<Entity, Spec, Separator, number, false>\n : Spec extends keyof Entity\n ? Replace<Entity[Spec], null, undefined>\n : Spec extends null\n ? NullAs\n : never\n\ntype TableEntryImpl<\n Entity,\n Schema,\n Separator extends string = '#',\n> = Entity extends unknown\n ? show<\n {\n readonly [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape\n ? ComputeTableKeyType<\n Entity,\n ValueOf<\n Schema[Key]['spec'],\n ValueOf<Entity, Schema[Key]['discriminator']>\n >,\n Separator\n >\n : Schema[Key] extends keyof Entity | InputSpecShape | null\n ? ComputeTableKeyType<Entity, Schema[Key], Separator>\n : ErrorMessage<'Invalid schema definition'>\n } & Entity\n >\n : never\n\n/**\n * Represents a complete DynamoDB table entry, combining the original entity\n * with its computed internal and global keys.\n *\n * @template Entity The base entity type.\n * @template Schema The schema defining the table keys.\n * @template Separator The string used to join composite key components (default: '#').\n */\nexport type TableEntry<\n Entity extends Record<string, unknown>,\n Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n> = TableEntryImpl<Entity, Schema, Separator>\n\n// Partial<T> for objects so narrowed transform defaults pass the InputSpec constraint.\ntype DefaultOf<T> = T extends Record<string, unknown> ? Partial<T> : T\n\ntype InputSpec<E> = {\n [key in keyof E]:\n | (undefined extends E[key]\n ? [\n key,\n (key: Exclude<E[key], undefined>) => TransformShape,\n DefaultOf<Exclude<E[key], undefined>>,\n ]\n : [key, (key: E[key]) => TransformShape])\n | (undefined extends E[key] ? never : null extends E[key] ? never : key)\n}[keyof E]\n\n// --- 3-tuple default validation ---\n// InputSpec uses DefaultOf so narrowed defaults pass the constraint.\n// ValidateSchema mirrors the schema replacing each 3-tuple default slot with\n// the transform's param type. The `Schema & ValidateSchema<Schema>` intersection\n// then rejects defaults that don't match the transform param (e.g. `{}`).\n\n// biome-ignore lint/suspicious/noExplicitAny: structural matching\ntype ValidateInputSpec<T> = {\n [I in keyof T]: T[I] extends readonly [\n unknown,\n (arg: infer P) => any,\n unknown,\n ]\n ? [T[I][0], T[I][1], P]\n : T[I]\n}\n\ntype Tuple3 = { length: 3 }\n\n// True if any spec entry in V is a 3-tuple. Recurses into discriminated specs.\ntype NeedsValidation<V> = V extends readonly unknown[]\n ? Extract<V[number], Tuple3>\n : V extends { spec: infer S }\n ? NeedsValidation<S[keyof S]>\n : never\n\n// Short-circuits to `unknown` when schema has no 3-tuples (zero overhead).\ntype ValidateSchema<Schema> = [NeedsValidation<Schema[keyof Schema]>] extends [\n never,\n]\n ? unknown\n : {\n [K in keyof Schema]: Schema[K] extends {\n discriminator: unknown\n spec: infer Spec\n }\n ? {\n discriminator: Schema[K]['discriminator']\n spec: {\n [SV in keyof Spec]: ValidateInputSpec<Spec[SV]>\n }\n }\n : ValidateInputSpec<Schema[K]>\n }\n\ntype extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T\n\ntype FullKeySpecSimple<Entity> =\n | NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>>\n | (keyof Entity & string)\n | null\n\ntype FullKeySpecSimpleShape = InputSpecShape | string | null\n\ntype DiscriminatedSchema<Entity, E> = {\n [key in keyof E]: E[key] extends PropertyKey\n ? {\n discriminator: key\n spec: {\n [val in E[key]]: FullKeySpecSimple<\n Extract<\n Entity,\n {\n [k in key]: val\n }\n >\n >\n }\n }\n : never\n}[keyof E]\n\ntype FullKeySpec<Entity> =\n | FullKeySpecSimple<Entity>\n | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>\n\ntype FullKeySpecShape = FullKeySpecSimpleShape | DiscriminatedSchemaShape\n\nexport class RotoriseError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'RotoriseError'\n }\n}\n\n// Runtime implementation uses `as never` casts because the generic types are\n// too complex for TS to verify at the value level. Type correctness is enforced\n// by the type-level types (CompositeKeyStringBuilder, TableEntryImpl, etc.) and\n// validated by the attest-based test suite.\n\nconst chainableNoOpProxy: unknown = new Proxy(() => chainableNoOpProxy, {\n get: () => chainableNoOpProxy,\n})\n\nconst createPathProxy = <T>(path = ''): T => {\n return new Proxy(() => {}, {\n get: (_target, prop) => {\n if (typeof prop === 'string') {\n if (prop === 'toString') {\n return () => path\n }\n\n return createPathProxy(\n path === ''\n ? prop\n : !Number.isNaN(Number.parseInt(prop))\n ? `${path}[${prop}]`\n : `${path}.${prop}`,\n )\n }\n },\n }) as T\n}\n\nconst key =\n <const Entity>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <\n const Key extends keyof Schema,\n const Config extends {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n },\n const Attributes extends Partial<Entity>,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ): string | undefined => {\n const case_ = schema[key]\n\n if (case_ === undefined) {\n throw new RotoriseError(`Key ${key.toString()} not found in schema`)\n }\n let structure: InputSpec<MergeIntersectionObject<Entity>>[]\n\n if (Array.isArray(case_)) {\n structure = case_\n } else if (typeof case_ === 'object') {\n const discriminator =\n attributes[case_.discriminator as keyof Attributes]\n if (discriminator === undefined) {\n throw new RotoriseError(\n `Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n const val = case_.spec[discriminator as keyof typeof case_.spec]\n if (val === undefined) {\n throw new RotoriseError(\n `Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n if (val === null) {\n return undefined\n }\n\n if (!Array.isArray(val)) {\n return attributes[val as keyof Attributes] as never\n }\n\n structure = val\n } else {\n const value = attributes[case_ as keyof Attributes]\n if (value == null) return undefined as never\n\n return value as never\n }\n\n const fullLength = structure.length\n\n if (config?.depth !== undefined) {\n structure = structure.slice(0, config.depth) as typeof structure\n }\n const composite: joinable[] = []\n\n for (const keySpec of structure) {\n const [key, transform, Default] = Array.isArray(keySpec)\n ? keySpec\n : [keySpec]\n\n const value = attributes[key as keyof Attributes] ?? Default\n\n if (transform && value !== undefined) {\n const transformed = transform(value as never)\n if (typeof transformed === 'object' && transformed !== null) {\n if (transformed.tag !== undefined)\n composite.push(transformed.tag)\n composite.push(transformed.value)\n } else {\n composite.push(key.toString().toUpperCase())\n composite.push(transformed)\n }\n } else if (value !== undefined && value !== null && value !== '') {\n composite.push(key.toString().toUpperCase())\n composite.push(value as joinable)\n } else if (config?.allowPartial) {\n break\n } else {\n throw new RotoriseError(\n `buildCompositeKey: Attribute ${key.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n }\n\n // Each spec element produces 2 segments (KEY, value). If fewer segments\n // were emitted than expected (partial key), append a trailing separator.\n if (config?.enforceBoundary && fullLength * 2 > composite.length) {\n composite.push('')\n }\n\n return composite.join(separator) as never\n }\n\nconst toEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <const ExactEntity extends Entity>(\n item: ExactEntity,\n ): ExactEntity extends infer E extends Entity\n ? TableEntryImpl<E, Schema, Separator>\n : never => {\n const entry = { ...item }\n const buildKey = key<Entity>()(schema, separator)\n\n for (const key_ in schema) {\n const val = buildKey(key_, item)\n if (val !== undefined) {\n entry[key_] = val satisfies string as never\n }\n }\n return entry as never\n }\n\nconst fromEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpecShape>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ) =>\n <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ): DistributiveOmit<Entry, keyof Schema> => {\n const item = { ...entry }\n\n for (const key_ in schema) {\n delete item[key_]\n }\n return item as never\n }\n\ntype ProcessSpecType<\n Entity,\n Spec,\n Config extends SpecConfigShape,\n> = Spec extends string\n ? DistributivePick<Entity, Spec>\n : Spec extends InputSpecShape\n ? CompositeKeyParamsImpl<\n Entity,\n Spec,\n Config['allowPartial'] extends true\n ? 1\n : Extract<Config['depth'], number>\n >\n : Spec extends null | undefined\n ? unknown\n : ErrorMessage<'Invalid Spec: Expected string, InputSpecShape, null or undefined'>\n\n// Cache commonly used conditional types\ntype SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape\n\ntype SpecConfigShape = {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n}\n\n// Pre-compute discriminated variant types\ntype ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? never\n : Extract<Entity, { [k in K]: V }>\n\ntype TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? { [k in K]: V }\n : Entity & { [k in K]: V }\n\n// Flatten nested type computation\ntype ProcessVariant<\n Entity,\n K extends PropertyKey,\n V extends PropertyKey,\n Spec extends DiscriminatedSchemaShape,\n Config extends SpecConfigShape,\n VariantSpec = Spec['spec'][V & keyof Spec['spec']],\n> = TagVariant<\n VariantSpec extends null | undefined\n ? unknown\n : ProcessSpecType<ExtractVariant<Entity, K, V>, VariantSpec, Config>,\n K,\n V\n>\n\n// Optimized attribute processing\ntype OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<\n Spec extends DiscriminatedSchemaShape\n ? {\n [K in Spec['discriminator']]: {\n [V in keyof Spec['spec']]: ProcessVariant<\n Entity,\n K,\n V,\n Spec,\n Config\n >\n }[keyof Spec['spec']]\n }[Spec['discriminator']]\n : ProcessSpecType<Entity, Spec, Config>\n>\n\ntype ProcessKey<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n Config extends SpecConfigShape = SpecConfigShape,\n Attributes = Pick<Entity, Spec & keyof Entity>,\n> = [Entity] extends [never]\n ? never\n : Spec extends keyof Entity\n ? Replace<ValueOf<Attributes>, null, undefined>\n : Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator,\n Exclude<Config['depth'], undefined>,\n Exclude<Config['allowPartial'], undefined>\n >\n : Spec extends null | undefined\n ? NullAs\n : ErrorMessage<'Invalid Spec'>\n\ntype OptimizedBuiltKey<\n Entity,\n Spec,\n Separator extends string,\n Config extends SpecConfigShape,\n Attributes,\n> = Entity extends unknown\n ? show<\n Spec extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<\n Spec['spec'],\n ValueOf<Entity, Spec['discriminator']>\n >,\n Separator,\n undefined,\n Config,\n Attributes\n >\n : ProcessKey<\n Entity,\n Spec,\n Separator,\n undefined,\n Config,\n Attributes\n >\n >\n : never\n\ntype TableEntryDefinition<Entity, Schema, Separator extends string> = {\n /**\n * Converts a raw entity into a complete table entry with all keys computed.\n * Use this when preparing items for insertion into DynamoDB.\n */\n toEntry: <const ExactEntity>(\n item: Exact<Entity, ExactEntity>,\n ) => TableEntryImpl<ExactEntity, Schema, Separator>\n\n /**\n * Extracts the raw entity from a table entry by removing all computed keys.\n * Use this when processing items retrieved from DynamoDB.\n */\n fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ) => DistributiveOmit<Entry, keyof Schema>\n\n /**\n * Generates a specific key for the given entity attributes.\n * Supports partial keys and depth limiting for query operations.\n *\n * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').\n * @param attributes the object containing the values needed to build the key.\n * @param config Optional configuration for partial keys or depth limiting.\n */\n key: <\n const Key extends keyof Schema,\n const Config extends SpecConfig<Spec>,\n const Attributes extends OptimizedAttributes<Entity, Spec, Config_>,\n Spec = Schema[Key],\n Config_ extends SpecConfigShape = [SpecConfigShape] extends [Config]\n ? {\n depth?: undefined\n allowPartial?: undefined\n enforceBoundary?: boolean\n }\n : Config,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>\n\n /**\n * A zero-runtime inference helper. Use this with `typeof` to get the\n * total type of a table entry.\n */\n infer: TableEntryImpl<Entity, Schema, Separator>\n\n /**\n * Creates a proxy to generate property paths as strings.\n * Useful for building UpdateExpressions or ProjectionExpressions.\n *\n * @example\n * table.path().data.nested.property.toString() // returns \"data.nested.property\"\n */\n path: () => TableEntryImpl<Entity, Schema, Separator>\n}\n\n/**\n * Entry point for defining a DynamoDB table schema with Rotorise.\n *\n * @template Entity The base entity type that this table represents.\n * @returns A builder function that accepts the schema and an optional separator.\n *\n * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.\n *\n * @example\n * const userTable = tableEntry<User>()({\n * PK: [\"orgId\", \"id\"],\n * SK: \"role\"\n * })\n */\nexport const tableEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n >(\n schema: Schema & ValidateSchema<Schema>,\n ...[separator]: [Separator] extends ['']\n ? [ErrorMessage<'Separator must not be an empty string'>]\n : [separator?: Separator]\n ): TableEntryDefinition<Entity, Schema, Separator> => {\n const sep = separator ?? ('#' as Separator)\n if (sep === '' || typeof sep !== 'string') {\n throw new RotoriseError('Separator must not be an empty string')\n }\n return {\n toEntry: toEntry()(schema as never, sep) as never,\n fromEntry: fromEntry()(schema as never) as never,\n key: key()(schema as never, sep) as never,\n infer: chainableNoOpProxy as never,\n path: () => createPathProxy() as never,\n }\n }\n"]}
@@ -119,13 +119,40 @@ type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity ext
119
119
  * @template Separator The string used to join composite key components (default: '#').
120
120
  */
121
121
  type TableEntry<Entity extends Record<string, unknown>, Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = '#'> = TableEntryImpl<Entity, Schema, Separator>;
122
+ type DefaultOf<T> = T extends Record<string, unknown> ? Partial<T> : T;
122
123
  type InputSpec<E> = {
123
124
  [key in keyof E]: (undefined extends E[key] ? [
124
125
  key,
125
126
  (key: Exclude<E[key], undefined>) => TransformShape,
126
- Exclude<E[key], undefined>
127
- ] : [key, (key: Exclude<E[key], undefined>) => TransformShape]) | (undefined extends E[key] ? never : null extends E[key] ? never : key);
127
+ DefaultOf<Exclude<E[key], undefined>>
128
+ ] : [key, (key: E[key]) => TransformShape]) | (undefined extends E[key] ? never : null extends E[key] ? never : key);
128
129
  }[keyof E];
130
+ type ValidateInputSpec<T> = {
131
+ [I in keyof T]: T[I] extends readonly [
132
+ unknown,
133
+ (arg: infer P) => any,
134
+ unknown
135
+ ] ? [T[I][0], T[I][1], P] : T[I];
136
+ };
137
+ type Tuple3 = {
138
+ length: 3;
139
+ };
140
+ type NeedsValidation<V> = V extends readonly unknown[] ? Extract<V[number], Tuple3> : V extends {
141
+ spec: infer S;
142
+ } ? NeedsValidation<S[keyof S]> : never;
143
+ type ValidateSchema<Schema> = [NeedsValidation<Schema[keyof Schema]>] extends [
144
+ never
145
+ ] ? unknown : {
146
+ [K in keyof Schema]: Schema[K] extends {
147
+ discriminator: unknown;
148
+ spec: infer Spec;
149
+ } ? {
150
+ discriminator: Schema[K]['discriminator'];
151
+ spec: {
152
+ [SV in keyof Spec]: ValidateInputSpec<Spec[SV]>;
153
+ };
154
+ } : ValidateInputSpec<Schema[K]>;
155
+ };
129
156
  type extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T;
130
157
  type FullKeySpecSimple<Entity> = NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>> | (keyof Entity & string) | null;
131
158
  type DiscriminatedSchema<Entity, E> = {
@@ -221,7 +248,7 @@ type TableEntryDefinition<Entity, Schema, Separator extends string> = {
221
248
  * SK: "role"
222
249
  * })
223
250
  */
224
- declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, ...[separator]: [Separator] extends [
251
+ 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 [
225
252
  ''
226
253
  ] ? [ErrorMessage<'Separator must not be an empty string'>] : [separator?: Separator]) => TableEntryDefinition<Entity, Schema, Separator>;
227
254
 
@@ -119,13 +119,40 @@ type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity ext
119
119
  * @template Separator The string used to join composite key components (default: '#').
120
120
  */
121
121
  type TableEntry<Entity extends Record<string, unknown>, Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = '#'> = TableEntryImpl<Entity, Schema, Separator>;
122
+ type DefaultOf<T> = T extends Record<string, unknown> ? Partial<T> : T;
122
123
  type InputSpec<E> = {
123
124
  [key in keyof E]: (undefined extends E[key] ? [
124
125
  key,
125
126
  (key: Exclude<E[key], undefined>) => TransformShape,
126
- Exclude<E[key], undefined>
127
- ] : [key, (key: Exclude<E[key], undefined>) => TransformShape]) | (undefined extends E[key] ? never : null extends E[key] ? never : key);
127
+ DefaultOf<Exclude<E[key], undefined>>
128
+ ] : [key, (key: E[key]) => TransformShape]) | (undefined extends E[key] ? never : null extends E[key] ? never : key);
128
129
  }[keyof E];
130
+ type ValidateInputSpec<T> = {
131
+ [I in keyof T]: T[I] extends readonly [
132
+ unknown,
133
+ (arg: infer P) => any,
134
+ unknown
135
+ ] ? [T[I][0], T[I][1], P] : T[I];
136
+ };
137
+ type Tuple3 = {
138
+ length: 3;
139
+ };
140
+ type NeedsValidation<V> = V extends readonly unknown[] ? Extract<V[number], Tuple3> : V extends {
141
+ spec: infer S;
142
+ } ? NeedsValidation<S[keyof S]> : never;
143
+ type ValidateSchema<Schema> = [NeedsValidation<Schema[keyof Schema]>] extends [
144
+ never
145
+ ] ? unknown : {
146
+ [K in keyof Schema]: Schema[K] extends {
147
+ discriminator: unknown;
148
+ spec: infer Spec;
149
+ } ? {
150
+ discriminator: Schema[K]['discriminator'];
151
+ spec: {
152
+ [SV in keyof Spec]: ValidateInputSpec<Spec[SV]>;
153
+ };
154
+ } : ValidateInputSpec<Schema[K]>;
155
+ };
129
156
  type extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T;
130
157
  type FullKeySpecSimple<Entity> = NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>> | (keyof Entity & string) | null;
131
158
  type DiscriminatedSchema<Entity, E> = {
@@ -221,7 +248,7 @@ type TableEntryDefinition<Entity, Schema, Separator extends string> = {
221
248
  * SK: "role"
222
249
  * })
223
250
  */
224
- declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, ...[separator]: [Separator] extends [
251
+ 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 [
225
252
  ''
226
253
  ] ? [ErrorMessage<'Separator must not be an empty string'>] : [separator?: Separator]) => TableEntryDefinition<Entity, Schema, Separator>;
227
254
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/Rotorise.ts"],"names":["key"],"mappings":"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","file":"Rotorise.js","sourcesContent":["import type {\n DistributiveOmit,\n DistributivePick,\n ErrorMessage,\n Exact,\n MergeIntersectionObject,\n NonEmptyArray,\n Replace,\n SliceFromStart,\n show,\n ValueOf,\n} from './utils'\n\n// When a spec item has a transform function, use the transform's parameter type\n// instead of the entity's property type. This allows callers to pass only the\n// fields the transform actually needs (e.g. Pick<Obj, 'id'> instead of Obj).\ntype TransformOverride<\n Spec extends InputSpecShape,\n K,\n Fallback,\n Matched = Extract<Spec[number], [K, (...args: any[]) => any, ...any[]]>,\n> = [Matched] extends [never]\n ? Fallback\n : // Contravariant inference: when the same key appears multiple times with\n // different transforms (e.g. Pick<Obj,'id'> and Pick<Obj,'name'>),\n // this intersects the parameter types rather than unioning them.\n (\n Matched extends [any, (x: infer P) => any, ...any[]] // biome-ignore lint/suspicious/noExplicitAny: inference\n ? (x: P) => void\n : never\n ) extends (x: infer I) => void\n ? I\n : Fallback\n\nexport type CompositeKeyParamsImpl<\n Entity,\n InputSpec extends InputSpecShape,\n skip extends number = 1,\n> = Entity extends unknown\n ? show<\n {\n [K in extractHeadOrPass<\n SliceFromStart<\n InputSpec,\n number extends skip ? 1 : skip\n >[number]\n > &\n keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>\n } & {\n [K in extractHeadOrPass<InputSpec[number]> &\n keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>\n }\n >\n : never\n\nexport type CompositeKeyParams<\n Entity extends Record<string, unknown>,\n FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[],\n skip extends number = 1,\n> = CompositeKeyParamsImpl<Entity, FullSpec, skip>\n\ntype CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = Entity extends unknown\n ? CompositeKeyStringBuilder<\n Entity,\n [Deep] extends [never]\n ? Spec\n : number extends Deep\n ? Spec\n : SliceFromStart<Spec, Deep>,\n Separator,\n boolean extends isPartial ? false : isPartial\n >\n : never\n\nexport type CompositeKeyBuilder<\n Entity extends Record<string, unknown>,\n Spec extends InputSpec<MergeIntersectionObject<Entity>>[],\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = CompositeKeyBuilderImpl<Entity, Spec, Separator, Deep, isPartial>\n\ntype joinable = string | number | bigint | boolean | null | undefined\n\ntype ExtractHelper<Key, Value> = Value extends object\n ? Value extends {\n tag: infer Tag extends string\n value: infer Value extends joinable\n }\n ? [Tag, Value]\n : Value extends {\n value: infer Value extends joinable\n }\n ? [never, Value]\n : never\n : [Key, Value]\n\ntype ExtractPair<Entity, Spec> = Spec extends [\n infer Key extends string,\n // biome-ignore lint/suspicious/noExplicitAny: required for generic transform inference\n (...key: any[]) => infer Value,\n ...unknown[],\n]\n ? ExtractHelper<Uppercase<Key>, Value>\n : Spec extends keyof Entity & string\n ? [Uppercase<Spec>, Entity[Spec] & joinable]\n : never\n\ntype CompositeKeyStringBuilder<\n Entity,\n Spec,\n Separator extends string,\n KeepIntermediate extends boolean,\n Acc extends string = '',\n AllAcc extends string = never,\n> = Spec extends [infer Head, ...infer Tail]\n ? ExtractPair<Entity, Head> extends [\n infer Key extends joinable,\n infer Value extends joinable,\n ]\n ? CompositeKeyStringBuilder<\n Entity,\n Tail,\n Separator,\n KeepIntermediate,\n Acc extends ''\n ? [Key] extends [never]\n ? `${Value}`\n : `${Key}${Separator}${Value}`\n : [Key] extends [never]\n ? `${Acc}${Separator}${Value}`\n : `${Acc}${Separator}${Key}${Separator}${Value}`,\n KeepIntermediate extends true\n ? AllAcc | (Acc extends '' ? never : Acc)\n : never\n >\n : never\n : AllAcc | Acc\n\ntype DiscriminatedSchemaShape = {\n discriminator: PropertyKey\n spec: {\n [k in PropertyKey]: unknown\n }\n}\n\ntype InputSpecShape =\n // biome-ignore lint/suspicious/noExplicitAny: key type is erased at runtime, any is needed for structural matching\n ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[]\n\nexport type TransformShape =\n | {\n tag?: string\n value: joinable\n }\n | joinable\n\ntype ComputeTableKeyType<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n> = Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<Entity, Spec, Separator, number, false>\n : Spec extends keyof Entity\n ? Replace<Entity[Spec], null, undefined>\n : Spec extends null\n ? NullAs\n : never\n\ntype TableEntryImpl<\n Entity,\n Schema,\n Separator extends string = '#',\n> = Entity extends unknown\n ? show<\n {\n readonly [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape\n ? ComputeTableKeyType<\n Entity,\n ValueOf<\n Schema[Key]['spec'],\n ValueOf<Entity, Schema[Key]['discriminator']>\n >,\n Separator\n >\n : Schema[Key] extends keyof Entity | InputSpecShape | null\n ? ComputeTableKeyType<Entity, Schema[Key], Separator>\n : ErrorMessage<'Invalid schema definition'>\n } & Entity\n >\n : never\n\n/**\n * Represents a complete DynamoDB table entry, combining the original entity\n * with its computed internal and global keys.\n *\n * @template Entity The base entity type.\n * @template Schema The schema defining the table keys.\n * @template Separator The string used to join composite key components (default: '#').\n */\nexport type TableEntry<\n Entity extends Record<string, unknown>,\n Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n> = TableEntryImpl<Entity, Schema, Separator>\n\ntype InputSpec<E> = {\n [key in keyof E]:\n | (undefined extends E[key]\n ? [\n key,\n (key: Exclude<E[key], undefined>) => TransformShape,\n Exclude<E[key], undefined>,\n ]\n : [key, (key: Exclude<E[key], undefined>) => TransformShape])\n | (undefined extends E[key] ? never : null extends E[key] ? never : key)\n}[keyof E]\n\ntype extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T\n\ntype FullKeySpecSimple<Entity> =\n | NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>>\n | (keyof Entity & string)\n | null\n\ntype FullKeySpecSimpleShape = InputSpecShape | string | null\n\ntype DiscriminatedSchema<Entity, E> = {\n [key in keyof E]: E[key] extends PropertyKey\n ? {\n discriminator: key\n spec: {\n [val in E[key]]: FullKeySpecSimple<\n Extract<\n Entity,\n {\n [k in key]: val\n }\n >\n >\n }\n }\n : never\n}[keyof E]\n\ntype FullKeySpec<Entity> =\n | FullKeySpecSimple<Entity>\n | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>\n\ntype FullKeySpecShape = FullKeySpecSimpleShape | DiscriminatedSchemaShape\n\nexport class RotoriseError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'RotoriseError'\n }\n}\n\n// Runtime implementation uses `as never` casts because the generic types are\n// too complex for TS to verify at the value level. Type correctness is enforced\n// by the type-level types (CompositeKeyStringBuilder, TableEntryImpl, etc.) and\n// validated by the attest-based test suite.\n\nconst chainableNoOpProxy: unknown = new Proxy(() => chainableNoOpProxy, {\n get: () => chainableNoOpProxy,\n})\n\nconst createPathProxy = <T>(path = ''): T => {\n return new Proxy(() => {}, {\n get: (_target, prop) => {\n if (typeof prop === 'string') {\n if (prop === 'toString') {\n return () => path\n }\n\n return createPathProxy(\n path === ''\n ? prop\n : !Number.isNaN(Number.parseInt(prop))\n ? `${path}[${prop}]`\n : `${path}.${prop}`,\n )\n }\n },\n }) as T\n}\n\nconst key =\n <const Entity>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <\n const Key extends keyof Schema,\n const Config extends {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n },\n const Attributes extends Partial<Entity>,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ): string | undefined => {\n const case_ = schema[key]\n\n if (case_ === undefined) {\n throw new RotoriseError(`Key ${key.toString()} not found in schema`)\n }\n let structure: InputSpec<MergeIntersectionObject<Entity>>[]\n\n if (Array.isArray(case_)) {\n structure = case_\n } else if (typeof case_ === 'object') {\n const discriminator =\n attributes[case_.discriminator as keyof Attributes]\n if (discriminator === undefined) {\n throw new RotoriseError(\n `Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n const val = case_.spec[discriminator as keyof typeof case_.spec]\n if (val === undefined) {\n throw new RotoriseError(\n `Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n if (val === null) {\n return undefined\n }\n\n if (!Array.isArray(val)) {\n return attributes[val as keyof Attributes] as never\n }\n\n structure = val\n } else {\n const value = attributes[case_ as keyof Attributes]\n if (value == null) return undefined as never\n\n return value as never\n }\n\n const fullLength = structure.length\n\n if (config?.depth !== undefined) {\n structure = structure.slice(0, config.depth) as typeof structure\n }\n const composite: joinable[] = []\n\n for (const keySpec of structure) {\n const [key, transform, Default] = Array.isArray(keySpec)\n ? keySpec\n : [keySpec]\n\n const value = attributes[key as keyof Attributes] ?? Default\n\n if (transform && value !== undefined) {\n const transformed = transform(value as never)\n if (typeof transformed === 'object' && transformed !== null) {\n if (transformed.tag !== undefined)\n composite.push(transformed.tag)\n composite.push(transformed.value)\n } else {\n composite.push(key.toString().toUpperCase())\n composite.push(transformed)\n }\n } else if (value !== undefined && value !== null && value !== '') {\n composite.push(key.toString().toUpperCase())\n composite.push(value as joinable)\n } else if (config?.allowPartial) {\n break\n } else {\n throw new RotoriseError(\n `buildCompositeKey: Attribute ${key.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n }\n\n // Each spec element produces 2 segments (KEY, value). If fewer segments\n // were emitted than expected (partial key), append a trailing separator.\n if (config?.enforceBoundary && fullLength * 2 > composite.length) {\n composite.push('')\n }\n\n return composite.join(separator) as never\n }\n\nconst toEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <const ExactEntity extends Entity>(\n item: ExactEntity,\n ): ExactEntity extends infer E extends Entity\n ? TableEntryImpl<E, Schema, Separator>\n : never => {\n const entry = { ...item }\n const buildKey = key<Entity>()(schema, separator)\n\n for (const key_ in schema) {\n const val = buildKey(key_, item)\n if (val !== undefined) {\n entry[key_] = val satisfies string as never\n }\n }\n return entry as never\n }\n\nconst fromEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpecShape>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ) =>\n <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ): DistributiveOmit<Entry, keyof Schema> => {\n const item = { ...entry }\n\n for (const key_ in schema) {\n delete item[key_]\n }\n return item as never\n }\n\ntype ProcessSpecType<\n Entity,\n Spec,\n Config extends SpecConfigShape,\n> = Spec extends string\n ? DistributivePick<Entity, Spec>\n : Spec extends InputSpecShape\n ? CompositeKeyParamsImpl<\n Entity,\n Spec,\n Config['allowPartial'] extends true\n ? 1\n : Extract<Config['depth'], number>\n >\n : Spec extends null | undefined\n ? unknown\n : ErrorMessage<'Invalid Spec: Expected string, InputSpecShape, null or undefined'>\n\n// Cache commonly used conditional types\ntype SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape\n\ntype SpecConfigShape = {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n}\n\n// Pre-compute discriminated variant types\ntype ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? never\n : Extract<Entity, { [k in K]: V }>\n\ntype TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? { [k in K]: V }\n : Entity & { [k in K]: V }\n\n// Flatten nested type computation\ntype ProcessVariant<\n Entity,\n K extends PropertyKey,\n V extends PropertyKey,\n Spec extends DiscriminatedSchemaShape,\n Config extends SpecConfigShape,\n VariantSpec = Spec['spec'][V & keyof Spec['spec']],\n> = TagVariant<\n VariantSpec extends null | undefined\n ? unknown\n : ProcessSpecType<ExtractVariant<Entity, K, V>, VariantSpec, Config>,\n K,\n V\n>\n\n// Optimized attribute processing\ntype OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<\n Spec extends DiscriminatedSchemaShape\n ? {\n [K in Spec['discriminator']]: {\n [V in keyof Spec['spec']]: ProcessVariant<\n Entity,\n K,\n V,\n Spec,\n Config\n >\n }[keyof Spec['spec']]\n }[Spec['discriminator']]\n : ProcessSpecType<Entity, Spec, Config>\n>\n\ntype ProcessKey<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n Config extends SpecConfigShape = SpecConfigShape,\n Attributes = Pick<Entity, Spec & keyof Entity>,\n> = [Entity] extends [never]\n ? never\n : Spec extends keyof Entity\n ? Replace<ValueOf<Attributes>, null, undefined>\n : Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator,\n Exclude<Config['depth'], undefined>,\n Exclude<Config['allowPartial'], undefined>\n >\n : Spec extends null | undefined\n ? NullAs\n : ErrorMessage<'Invalid Spec'>\n\ntype OptimizedBuiltKey<\n Entity,\n Spec,\n Separator extends string,\n Config extends SpecConfigShape,\n Attributes,\n> = Entity extends unknown\n ? show<\n Spec extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<\n Spec['spec'],\n ValueOf<Entity, Spec['discriminator']>\n >,\n Separator,\n undefined,\n Config,\n Attributes\n >\n : ProcessKey<\n Entity,\n Spec,\n Separator,\n undefined,\n Config,\n Attributes\n >\n >\n : never\n\ntype TableEntryDefinition<Entity, Schema, Separator extends string> = {\n /**\n * Converts a raw entity into a complete table entry with all keys computed.\n * Use this when preparing items for insertion into DynamoDB.\n */\n toEntry: <const ExactEntity>(\n item: Exact<Entity, ExactEntity>,\n ) => TableEntryImpl<ExactEntity, Schema, Separator>\n\n /**\n * Extracts the raw entity from a table entry by removing all computed keys.\n * Use this when processing items retrieved from DynamoDB.\n */\n fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ) => DistributiveOmit<Entry, keyof Schema>\n\n /**\n * Generates a specific key for the given entity attributes.\n * Supports partial keys and depth limiting for query operations.\n *\n * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').\n * @param attributes the object containing the values needed to build the key.\n * @param config Optional configuration for partial keys or depth limiting.\n */\n key: <\n const Key extends keyof Schema,\n const Config extends SpecConfig<Spec>,\n const Attributes extends OptimizedAttributes<Entity, Spec, Config_>,\n Spec = Schema[Key],\n Config_ extends SpecConfigShape = [SpecConfigShape] extends [Config]\n ? {\n depth?: undefined\n allowPartial?: undefined\n enforceBoundary?: boolean\n }\n : Config,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>\n\n /**\n * A zero-runtime inference helper. Use this with `typeof` to get the\n * total type of a table entry.\n */\n infer: TableEntryImpl<Entity, Schema, Separator>\n\n /**\n * Creates a proxy to generate property paths as strings.\n * Useful for building UpdateExpressions or ProjectionExpressions.\n *\n * @example\n * table.path().data.nested.property.toString() // returns \"data.nested.property\"\n */\n path: () => TableEntryImpl<Entity, Schema, Separator>\n}\n\n/**\n * Entry point for defining a DynamoDB table schema with Rotorise.\n *\n * @template Entity The base entity type that this table represents.\n * @returns A builder function that accepts the schema and an optional separator.\n *\n * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.\n *\n * @example\n * const userTable = tableEntry<User>()({\n * PK: [\"orgId\", \"id\"],\n * SK: \"role\"\n * })\n */\nexport const tableEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ...[separator]: [Separator] extends ['']\n ? [ErrorMessage<'Separator must not be an empty string'>]\n : [separator?: Separator]\n ): TableEntryDefinition<Entity, Schema, Separator> => {\n const sep = separator ?? ('#' as Separator)\n if (sep === '' || typeof sep !== 'string') {\n throw new RotoriseError('Separator must not be an empty string')\n }\n return {\n toEntry: toEntry()(schema as never, sep) as never,\n fromEntry: fromEntry()(schema as never) as never,\n key: key()(schema as never, sep) as never,\n infer: chainableNoOpProxy as never,\n path: () => createPathProxy() as never,\n }\n }\n"]}
1
+ {"version":3,"sources":["../src/Rotorise.ts"],"names":["key"],"mappings":"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","file":"Rotorise.js","sourcesContent":["import type {\n DistributiveOmit,\n DistributivePick,\n ErrorMessage,\n Exact,\n MergeIntersectionObject,\n NonEmptyArray,\n Replace,\n SliceFromStart,\n show,\n ValueOf,\n} from './utils'\n\n// When a spec item has a transform function, use the transform's parameter type\n// instead of the entity's property type. This allows callers to pass only the\n// fields the transform actually needs (e.g. Pick<Obj, 'id'> instead of Obj).\ntype TransformOverride<\n Spec extends InputSpecShape,\n K,\n Fallback,\n Matched = Extract<Spec[number], [K, (...args: any[]) => any, ...any[]]>,\n> = [Matched] extends [never]\n ? Fallback\n : // Contravariant inference: when the same key appears multiple times with\n // different transforms (e.g. Pick<Obj,'id'> and Pick<Obj,'name'>),\n // this intersects the parameter types rather than unioning them.\n (\n Matched extends [any, (x: infer P) => any, ...any[]] // biome-ignore lint/suspicious/noExplicitAny: inference\n ? (x: P) => void\n : never\n ) extends (x: infer I) => void\n ? I\n : Fallback\n\nexport type CompositeKeyParamsImpl<\n Entity,\n InputSpec extends InputSpecShape,\n skip extends number = 1,\n> = Entity extends unknown\n ? show<\n {\n [K in extractHeadOrPass<\n SliceFromStart<\n InputSpec,\n number extends skip ? 1 : skip\n >[number]\n > &\n keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>\n } & {\n [K in extractHeadOrPass<InputSpec[number]> &\n keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>\n }\n >\n : never\n\nexport type CompositeKeyParams<\n Entity extends Record<string, unknown>,\n FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[],\n skip extends number = 1,\n> = CompositeKeyParamsImpl<Entity, FullSpec, skip>\n\ntype CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = Entity extends unknown\n ? CompositeKeyStringBuilder<\n Entity,\n [Deep] extends [never]\n ? Spec\n : number extends Deep\n ? Spec\n : SliceFromStart<Spec, Deep>,\n Separator,\n boolean extends isPartial ? false : isPartial\n >\n : never\n\nexport type CompositeKeyBuilder<\n Entity extends Record<string, unknown>,\n Spec extends InputSpec<MergeIntersectionObject<Entity>>[],\n Separator extends string = '#',\n Deep extends number = number,\n isPartial extends boolean = false,\n> = CompositeKeyBuilderImpl<Entity, Spec, Separator, Deep, isPartial>\n\ntype joinable = string | number | bigint | boolean | null | undefined\n\ntype ExtractHelper<Key, Value> = Value extends object\n ? Value extends {\n tag: infer Tag extends string\n value: infer Value extends joinable\n }\n ? [Tag, Value]\n : Value extends {\n value: infer Value extends joinable\n }\n ? [never, Value]\n : never\n : [Key, Value]\n\ntype ExtractPair<Entity, Spec> = Spec extends [\n infer Key extends string,\n // biome-ignore lint/suspicious/noExplicitAny: required for generic transform inference\n (...key: any[]) => infer Value,\n ...unknown[],\n]\n ? ExtractHelper<Uppercase<Key>, Value>\n : Spec extends keyof Entity & string\n ? [Uppercase<Spec>, Entity[Spec] & joinable]\n : never\n\ntype CompositeKeyStringBuilder<\n Entity,\n Spec,\n Separator extends string,\n KeepIntermediate extends boolean,\n Acc extends string = '',\n AllAcc extends string = never,\n> = Spec extends [infer Head, ...infer Tail]\n ? ExtractPair<Entity, Head> extends [\n infer Key extends joinable,\n infer Value extends joinable,\n ]\n ? CompositeKeyStringBuilder<\n Entity,\n Tail,\n Separator,\n KeepIntermediate,\n Acc extends ''\n ? [Key] extends [never]\n ? `${Value}`\n : `${Key}${Separator}${Value}`\n : [Key] extends [never]\n ? `${Acc}${Separator}${Value}`\n : `${Acc}${Separator}${Key}${Separator}${Value}`,\n KeepIntermediate extends true\n ? AllAcc | (Acc extends '' ? never : Acc)\n : never\n >\n : never\n : AllAcc | Acc\n\ntype DiscriminatedSchemaShape = {\n discriminator: PropertyKey\n spec: {\n [k in PropertyKey]: unknown\n }\n}\n\ntype InputSpecShape =\n // biome-ignore lint/suspicious/noExplicitAny: key type is erased at runtime, any is needed for structural matching\n ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[]\n\nexport type TransformShape =\n | {\n tag?: string\n value: joinable\n }\n | joinable\n\ntype ComputeTableKeyType<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n> = Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<Entity, Spec, Separator, number, false>\n : Spec extends keyof Entity\n ? Replace<Entity[Spec], null, undefined>\n : Spec extends null\n ? NullAs\n : never\n\ntype TableEntryImpl<\n Entity,\n Schema,\n Separator extends string = '#',\n> = Entity extends unknown\n ? show<\n {\n readonly [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape\n ? ComputeTableKeyType<\n Entity,\n ValueOf<\n Schema[Key]['spec'],\n ValueOf<Entity, Schema[Key]['discriminator']>\n >,\n Separator\n >\n : Schema[Key] extends keyof Entity | InputSpecShape | null\n ? ComputeTableKeyType<Entity, Schema[Key], Separator>\n : ErrorMessage<'Invalid schema definition'>\n } & Entity\n >\n : never\n\n/**\n * Represents a complete DynamoDB table entry, combining the original entity\n * with its computed internal and global keys.\n *\n * @template Entity The base entity type.\n * @template Schema The schema defining the table keys.\n * @template Separator The string used to join composite key components (default: '#').\n */\nexport type TableEntry<\n Entity extends Record<string, unknown>,\n Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n> = TableEntryImpl<Entity, Schema, Separator>\n\n// Partial<T> for objects so narrowed transform defaults pass the InputSpec constraint.\ntype DefaultOf<T> = T extends Record<string, unknown> ? Partial<T> : T\n\ntype InputSpec<E> = {\n [key in keyof E]:\n | (undefined extends E[key]\n ? [\n key,\n (key: Exclude<E[key], undefined>) => TransformShape,\n DefaultOf<Exclude<E[key], undefined>>,\n ]\n : [key, (key: E[key]) => TransformShape])\n | (undefined extends E[key] ? never : null extends E[key] ? never : key)\n}[keyof E]\n\n// --- 3-tuple default validation ---\n// InputSpec uses DefaultOf so narrowed defaults pass the constraint.\n// ValidateSchema mirrors the schema replacing each 3-tuple default slot with\n// the transform's param type. The `Schema & ValidateSchema<Schema>` intersection\n// then rejects defaults that don't match the transform param (e.g. `{}`).\n\n// biome-ignore lint/suspicious/noExplicitAny: structural matching\ntype ValidateInputSpec<T> = {\n [I in keyof T]: T[I] extends readonly [\n unknown,\n (arg: infer P) => any,\n unknown,\n ]\n ? [T[I][0], T[I][1], P]\n : T[I]\n}\n\ntype Tuple3 = { length: 3 }\n\n// True if any spec entry in V is a 3-tuple. Recurses into discriminated specs.\ntype NeedsValidation<V> = V extends readonly unknown[]\n ? Extract<V[number], Tuple3>\n : V extends { spec: infer S }\n ? NeedsValidation<S[keyof S]>\n : never\n\n// Short-circuits to `unknown` when schema has no 3-tuples (zero overhead).\ntype ValidateSchema<Schema> = [NeedsValidation<Schema[keyof Schema]>] extends [\n never,\n]\n ? unknown\n : {\n [K in keyof Schema]: Schema[K] extends {\n discriminator: unknown\n spec: infer Spec\n }\n ? {\n discriminator: Schema[K]['discriminator']\n spec: {\n [SV in keyof Spec]: ValidateInputSpec<Spec[SV]>\n }\n }\n : ValidateInputSpec<Schema[K]>\n }\n\ntype extractHeadOrPass<T> = T extends readonly unknown[] ? T[0] : T\n\ntype FullKeySpecSimple<Entity> =\n | NonEmptyArray<InputSpec<MergeIntersectionObject<Entity>>>\n | (keyof Entity & string)\n | null\n\ntype FullKeySpecSimpleShape = InputSpecShape | string | null\n\ntype DiscriminatedSchema<Entity, E> = {\n [key in keyof E]: E[key] extends PropertyKey\n ? {\n discriminator: key\n spec: {\n [val in E[key]]: FullKeySpecSimple<\n Extract<\n Entity,\n {\n [k in key]: val\n }\n >\n >\n }\n }\n : never\n}[keyof E]\n\ntype FullKeySpec<Entity> =\n | FullKeySpecSimple<Entity>\n | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>\n\ntype FullKeySpecShape = FullKeySpecSimpleShape | DiscriminatedSchemaShape\n\nexport class RotoriseError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'RotoriseError'\n }\n}\n\n// Runtime implementation uses `as never` casts because the generic types are\n// too complex for TS to verify at the value level. Type correctness is enforced\n// by the type-level types (CompositeKeyStringBuilder, TableEntryImpl, etc.) and\n// validated by the attest-based test suite.\n\nconst chainableNoOpProxy: unknown = new Proxy(() => chainableNoOpProxy, {\n get: () => chainableNoOpProxy,\n})\n\nconst createPathProxy = <T>(path = ''): T => {\n return new Proxy(() => {}, {\n get: (_target, prop) => {\n if (typeof prop === 'string') {\n if (prop === 'toString') {\n return () => path\n }\n\n return createPathProxy(\n path === ''\n ? prop\n : !Number.isNaN(Number.parseInt(prop))\n ? `${path}[${prop}]`\n : `${path}.${prop}`,\n )\n }\n },\n }) as T\n}\n\nconst key =\n <const Entity>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <\n const Key extends keyof Schema,\n const Config extends {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n },\n const Attributes extends Partial<Entity>,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ): string | undefined => {\n const case_ = schema[key]\n\n if (case_ === undefined) {\n throw new RotoriseError(`Key ${key.toString()} not found in schema`)\n }\n let structure: InputSpec<MergeIntersectionObject<Entity>>[]\n\n if (Array.isArray(case_)) {\n structure = case_\n } else if (typeof case_ === 'object') {\n const discriminator =\n attributes[case_.discriminator as keyof Attributes]\n if (discriminator === undefined) {\n throw new RotoriseError(\n `Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n const val = case_.spec[discriminator as keyof typeof case_.spec]\n if (val === undefined) {\n throw new RotoriseError(\n `Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n if (val === null) {\n return undefined\n }\n\n if (!Array.isArray(val)) {\n return attributes[val as keyof Attributes] as never\n }\n\n structure = val\n } else {\n const value = attributes[case_ as keyof Attributes]\n if (value == null) return undefined as never\n\n return value as never\n }\n\n const fullLength = structure.length\n\n if (config?.depth !== undefined) {\n structure = structure.slice(0, config.depth) as typeof structure\n }\n const composite: joinable[] = []\n\n for (const keySpec of structure) {\n const [key, transform, Default] = Array.isArray(keySpec)\n ? keySpec\n : [keySpec]\n\n const value = attributes[key as keyof Attributes] ?? Default\n\n if (transform && value !== undefined) {\n const transformed = transform(value as never)\n if (typeof transformed === 'object' && transformed !== null) {\n if (transformed.tag !== undefined)\n composite.push(transformed.tag)\n composite.push(transformed.value)\n } else {\n composite.push(key.toString().toUpperCase())\n composite.push(transformed)\n }\n } else if (value !== undefined && value !== null && value !== '') {\n composite.push(key.toString().toUpperCase())\n composite.push(value as joinable)\n } else if (config?.allowPartial) {\n break\n } else {\n throw new RotoriseError(\n `buildCompositeKey: Attribute ${key.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\n }\n\n // Each spec element produces 2 segments (KEY, value). If fewer segments\n // were emitted than expected (partial key), append a trailing separator.\n if (config?.enforceBoundary && fullLength * 2 > composite.length) {\n composite.push('')\n }\n\n return composite.join(separator) as never\n }\n\nconst toEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<\n string,\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n | {\n discriminator: keyof Entity\n spec: {\n [val in string]:\n | InputSpec<MergeIntersectionObject<Entity>>[]\n | keyof Entity\n }\n }\n >,\n Separator extends string = '#',\n >(\n schema: Schema,\n separator: Separator = '#' as Separator,\n ) =>\n <const ExactEntity extends Entity>(\n item: ExactEntity,\n ): ExactEntity extends infer E extends Entity\n ? TableEntryImpl<E, Schema, Separator>\n : never => {\n const entry = { ...item }\n const buildKey = key<Entity>()(schema, separator)\n\n for (const key_ in schema) {\n const val = buildKey(key_, item)\n if (val !== undefined) {\n entry[key_] = val satisfies string as never\n }\n }\n return entry as never\n }\n\nconst fromEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpecShape>,\n Separator extends string = '#',\n >(\n schema: Schema,\n ) =>\n <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ): DistributiveOmit<Entry, keyof Schema> => {\n const item = { ...entry }\n\n for (const key_ in schema) {\n delete item[key_]\n }\n return item as never\n }\n\ntype ProcessSpecType<\n Entity,\n Spec,\n Config extends SpecConfigShape,\n> = Spec extends string\n ? DistributivePick<Entity, Spec>\n : Spec extends InputSpecShape\n ? CompositeKeyParamsImpl<\n Entity,\n Spec,\n Config['allowPartial'] extends true\n ? 1\n : Extract<Config['depth'], number>\n >\n : Spec extends null | undefined\n ? unknown\n : ErrorMessage<'Invalid Spec: Expected string, InputSpecShape, null or undefined'>\n\n// Cache commonly used conditional types\ntype SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape\n\ntype SpecConfigShape = {\n depth?: number\n allowPartial?: boolean\n enforceBoundary?: boolean\n}\n\n// Pre-compute discriminated variant types\ntype ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? never\n : Extract<Entity, { [k in K]: V }>\n\ntype TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [\n Entity,\n] extends [never]\n ? { [k in K]: V }\n : Entity & { [k in K]: V }\n\n// Flatten nested type computation\ntype ProcessVariant<\n Entity,\n K extends PropertyKey,\n V extends PropertyKey,\n Spec extends DiscriminatedSchemaShape,\n Config extends SpecConfigShape,\n VariantSpec = Spec['spec'][V & keyof Spec['spec']],\n> = TagVariant<\n VariantSpec extends null | undefined\n ? unknown\n : ProcessSpecType<ExtractVariant<Entity, K, V>, VariantSpec, Config>,\n K,\n V\n>\n\n// Optimized attribute processing\ntype OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<\n Spec extends DiscriminatedSchemaShape\n ? {\n [K in Spec['discriminator']]: {\n [V in keyof Spec['spec']]: ProcessVariant<\n Entity,\n K,\n V,\n Spec,\n Config\n >\n }[keyof Spec['spec']]\n }[Spec['discriminator']]\n : ProcessSpecType<Entity, Spec, Config>\n>\n\ntype ProcessKey<\n Entity,\n Spec,\n Separator extends string,\n NullAs extends never | undefined = never,\n Config extends SpecConfigShape = SpecConfigShape,\n Attributes = Pick<Entity, Spec & keyof Entity>,\n> = [Entity] extends [never]\n ? never\n : Spec extends keyof Entity\n ? Replace<ValueOf<Attributes>, null, undefined>\n : Spec extends InputSpecShape\n ? CompositeKeyBuilderImpl<\n Entity,\n Spec,\n Separator,\n Exclude<Config['depth'], undefined>,\n Exclude<Config['allowPartial'], undefined>\n >\n : Spec extends null | undefined\n ? NullAs\n : ErrorMessage<'Invalid Spec'>\n\ntype OptimizedBuiltKey<\n Entity,\n Spec,\n Separator extends string,\n Config extends SpecConfigShape,\n Attributes,\n> = Entity extends unknown\n ? show<\n Spec extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<\n Spec['spec'],\n ValueOf<Entity, Spec['discriminator']>\n >,\n Separator,\n undefined,\n Config,\n Attributes\n >\n : ProcessKey<\n Entity,\n Spec,\n Separator,\n undefined,\n Config,\n Attributes\n >\n >\n : never\n\ntype TableEntryDefinition<Entity, Schema, Separator extends string> = {\n /**\n * Converts a raw entity into a complete table entry with all keys computed.\n * Use this when preparing items for insertion into DynamoDB.\n */\n toEntry: <const ExactEntity>(\n item: Exact<Entity, ExactEntity>,\n ) => TableEntryImpl<ExactEntity, Schema, Separator>\n\n /**\n * Extracts the raw entity from a table entry by removing all computed keys.\n * Use this when processing items retrieved from DynamoDB.\n */\n fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ) => DistributiveOmit<Entry, keyof Schema>\n\n /**\n * Generates a specific key for the given entity attributes.\n * Supports partial keys and depth limiting for query operations.\n *\n * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').\n * @param attributes the object containing the values needed to build the key.\n * @param config Optional configuration for partial keys or depth limiting.\n */\n key: <\n const Key extends keyof Schema,\n const Config extends SpecConfig<Spec>,\n const Attributes extends OptimizedAttributes<Entity, Spec, Config_>,\n Spec = Schema[Key],\n Config_ extends SpecConfigShape = [SpecConfigShape] extends [Config]\n ? {\n depth?: undefined\n allowPartial?: undefined\n enforceBoundary?: boolean\n }\n : Config,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>\n\n /**\n * A zero-runtime inference helper. Use this with `typeof` to get the\n * total type of a table entry.\n */\n infer: TableEntryImpl<Entity, Schema, Separator>\n\n /**\n * Creates a proxy to generate property paths as strings.\n * Useful for building UpdateExpressions or ProjectionExpressions.\n *\n * @example\n * table.path().data.nested.property.toString() // returns \"data.nested.property\"\n */\n path: () => TableEntryImpl<Entity, Schema, Separator>\n}\n\n/**\n * Entry point for defining a DynamoDB table schema with Rotorise.\n *\n * @template Entity The base entity type that this table represents.\n * @returns A builder function that accepts the schema and an optional separator.\n *\n * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.\n *\n * @example\n * const userTable = tableEntry<User>()({\n * PK: [\"orgId\", \"id\"],\n * SK: \"role\"\n * })\n */\nexport const tableEntry =\n <const Entity extends Record<string, unknown>>() =>\n <\n const Schema extends Record<string, FullKeySpec<Entity>>,\n Separator extends string = '#',\n >(\n schema: Schema & ValidateSchema<Schema>,\n ...[separator]: [Separator] extends ['']\n ? [ErrorMessage<'Separator must not be an empty string'>]\n : [separator?: Separator]\n ): TableEntryDefinition<Entity, Schema, Separator> => {\n const sep = separator ?? ('#' as Separator)\n if (sep === '' || typeof sep !== 'string') {\n throw new RotoriseError('Separator must not be an empty string')\n }\n return {\n toEntry: toEntry()(schema as never, sep) as never,\n fromEntry: fromEntry()(schema as never) as never,\n key: key()(schema as never, sep) as never,\n infer: chainableNoOpProxy as never,\n path: () => createPathProxy() as never,\n }\n }\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rotorise",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Supercharge your DynamoDB with Rotorise!",
5
5
  "main": "dist/Rotorise.cjs",
6
6
  "types": "dist/Rotorise.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@ark/attest": "^0.34.0",
50
50
  "@biomejs/biome": "2.0.0",
51
51
  "@types/node": "^18.19.34",
52
- "tsup": "^8.1.0",
52
+ "tsup": "^8.5.1",
53
53
  "typescript": "^5.4.5",
54
54
  "vitest": "^2.1.8"
55
55
  }