rotorise 0.2.5 → 0.3.3

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,44 +1,22 @@
1
- type KeysOfUnion<ObjectType> = ObjectType extends unknown
2
- ? keyof ObjectType
3
- : never
4
- type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <
5
- G,
6
- >() => G extends U ? 1 : 2
7
- ? true
8
- : false
9
-
10
- type ArrayElement<T> = T extends readonly unknown[] ? T[0] : never
11
-
12
- type ExactObject<ParameterType, InputType> = {
13
- [Key in keyof ParameterType]: Exact<
14
- ParameterType[Key],
15
- Key extends keyof InputType ? InputType[Key] : never
16
- >
17
- } & Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>
18
-
19
- type Exact<ParameterType, InputType> = IsEqual<
20
- ParameterType,
21
- InputType
22
- > extends true
23
- ? ParameterType
24
- : // Convert union of array to array of union: A[] & B[] => (A & B)[]
25
- ParameterType extends unknown[]
26
- ? Array<Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>>
27
- : // In TypeScript, Array is a subtype of ReadonlyArray, so always test Array before ReadonlyArray.
28
- ParameterType extends readonly unknown[]
29
- ? ReadonlyArray<
30
- Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>
31
- >
32
- : ParameterType extends object
33
- ? ExactObject<ParameterType, InputType>
34
- : ParameterType
1
+ // Lighter Exact implementation catches excess top-level properties only.
2
+ // Does not recursively check nested objects/arrays. This is intentional:
3
+ // DynamoDB entity shapes are flat at the key level.
4
+ type Exact<Shape, Candidate> = Candidate & {
5
+ [K in keyof Candidate]: K extends keyof Shape ? Candidate[K] : never
6
+ }
35
7
 
36
8
  type ValueOf<
37
9
  ObjectType,
38
10
  ValueType = keyof ObjectType,
39
11
  > = ValueType extends keyof ObjectType ? ObjectType[ValueType] : never
40
12
 
41
- type evaluate<T> = { [K in keyof T]: T[K] } & unknown
13
+ /**
14
+ * Force an operation like `{ a: 0 } & { b: 1 }` to be computed so that it displays `{ a: 0; b: 1 }`.
15
+ * This version is distributive, meaning it will preserve union types while flattening each member.
16
+ */
17
+ type show<T> = T extends unknown
18
+ ? { [K in keyof T]: T[K] } & unknown
19
+ : never
42
20
 
43
21
  type DistributivePick<T, K> = T extends unknown
44
22
  ? K extends keyof T
@@ -54,48 +32,63 @@ type SliceFromStart<
54
32
  T,
55
33
  End extends number,
56
34
  Acc extends unknown[] = [],
57
- > = T extends unknown[]
58
- ? Acc['length'] extends End
59
- ? Acc
60
- : T extends [infer Head, ...infer Tail]
61
- ? SliceFromStart<Tail, End, [...Acc, Head]>
62
- : Acc
63
- : never
35
+ > = End extends 0
36
+ ? []
37
+ : End extends 1
38
+ ? T extends [infer Head, ...unknown[]]
39
+ ? [Head]
40
+ : []
41
+ : T extends unknown[]
42
+ ? Acc['length'] extends End
43
+ ? Acc
44
+ : T extends [infer Head, ...infer Tail]
45
+ ? SliceFromStart<Tail, End, [...Acc, Head]>
46
+ : Acc
47
+ : never
64
48
 
65
49
  type MergeIntersectionObject<T, Keys = keyof T> = {
66
50
  [K in Keys]: T[K]
67
51
  }
68
- // type t = MergeIntersectionObject<
69
- // { a: 'a1'; b: 1n; extra: 'extra' } | { a: 'a2'; b: 2 }
70
- // >
71
52
 
72
53
  type NonEmptyArray<T> = [T, ...T[]]
73
54
 
74
55
  type Replace<T, U, V> = T extends U ? V : T
75
56
 
76
- type CompositeKeyParamsImpl<Entity, InputSpec extends InputSpecShape, skip extends number = 1> = Entity extends unknown ? evaluate<Pick<Entity, extractHeadOrPass<SliceFromStart<InputSpec, number extends skip ? 1 : skip>[number]> & keyof Entity> & Partial<Pick<Entity, extractHeadOrPass<InputSpec[number]> & keyof Entity>>> : never;
57
+ /**
58
+ * Represents a type-level error message. Used to provide helpful feedback in the IDE.
59
+ */
60
+ declare const errorMessage: unique symbol
61
+ type ErrorMessage<T extends string> = {
62
+ readonly [errorMessage]: T
63
+ }
64
+
65
+ 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;
66
+ type CompositeKeyParamsImpl<Entity, InputSpec extends InputSpecShape, skip extends number = 1> = Entity extends unknown ? show<{
67
+ [K in extractHeadOrPass<SliceFromStart<InputSpec, number extends skip ? 1 : skip>[number]> & keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>;
68
+ } & {
69
+ [K in extractHeadOrPass<InputSpec[number]> & keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>;
70
+ }> : never;
77
71
  type CompositeKeyParams<Entity extends Record<string, unknown>, FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[], skip extends number = 1> = CompositeKeyParamsImpl<Entity, FullSpec, skip>;
78
- type CompositeKeyBuilderImpl<Entity, Spec, Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = Entity extends unknown ? Join<CompositeKeyRec<Entity, number extends Deep ? Spec : SliceFromStart<Spec, Deep>>, Separator, boolean extends isPartial ? false : isPartial> : never;
72
+ type CompositeKeyBuilderImpl<Entity, Spec, Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = Entity extends unknown ? CompositeKeyStringBuilder<Entity, [
73
+ Deep
74
+ ] extends [never] ? Spec : number extends Deep ? Spec : SliceFromStart<Spec, Deep>, Separator, boolean extends isPartial ? false : isPartial> : never;
79
75
  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>;
80
76
  type joinable = string | number | bigint | boolean | null | undefined;
81
- type joinablePair = [joinable, joinable];
82
- type Join<Pairs, Separator extends string, KeepIntermediate extends boolean = false, Acc extends string = '', AllAcc extends string = never> = Pairs extends [infer Head extends joinablePair, ...infer Tail] ? Join<Tail, Separator, KeepIntermediate, Acc extends '' ? [Head[0]] extends [never] ? `${Head[1]}` : `${Head[0]}${Separator}${Head[1]}` : [Head[0]] extends [never] ? `${Acc}${Separator}${Head[1]}` : `${Acc}${Separator}${Head[0]}${Separator}${Head[1]}`, KeepIntermediate extends true ? AllAcc | (Acc extends '' ? never : Acc) : never> : AllAcc | Acc;
83
- type ExtractHelper<Key, Value> = Value extends joinable ? [Key, Value] : Value extends {
77
+ type ExtractHelper<Key, Value> = Value extends object ? Value extends {
84
78
  tag: infer Tag extends string;
85
79
  value: infer Value extends joinable;
86
80
  } ? [Tag, Value] : Value extends {
87
- tag?: undefined;
88
81
  value: infer Value extends joinable;
89
- } ? [never, Value] : never;
82
+ } ? [never, Value] : never : [Key, Value];
90
83
  type ExtractPair<Entity, Spec> = Spec extends [
91
84
  infer Key extends string,
92
85
  (...key: any[]) => infer Value,
93
86
  ...unknown[]
94
87
  ] ? ExtractHelper<Uppercase<Key>, Value> : Spec extends keyof Entity & string ? [Uppercase<Spec>, Entity[Spec] & joinable] : never;
95
- type CompositeKeyRec<Entity, Spec, Acc extends joinablePair[] = [], KeysCache extends string = keyof Entity & string> = Spec extends [infer Head, ...infer Tail] ? CompositeKeyRec<Entity, Tail, [
96
- ...Acc,
97
- ExtractPair<Entity, Head>
98
- ], KeysCache> : Acc;
88
+ 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 [
89
+ infer Key extends joinable,
90
+ infer Value extends joinable
91
+ ] ? 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;
99
92
  type DiscriminatedSchemaShape = {
100
93
  discriminator: PropertyKey;
101
94
  spec: {
@@ -107,9 +100,18 @@ type TransformShape = {
107
100
  tag?: string;
108
101
  value: joinable;
109
102
  } | joinable;
110
- type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity extends unknown ? {
111
- [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape ? ProcessKey<Entity, ValueOf<Schema[Key]['spec'], ValueOf<Entity, Schema[Key]['discriminator']>>, Separator> : ProcessKey<Entity, Schema[Key], Separator>;
112
- } & Entity : never;
103
+ 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;
104
+ type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity extends unknown ? show<{
105
+ 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'>;
106
+ } & Entity> : never;
107
+ /**
108
+ * Represents a complete DynamoDB table entry, combining the original entity
109
+ * with its computed internal and global keys.
110
+ *
111
+ * @template Entity The base entity type.
112
+ * @template Schema The schema defining the table keys.
113
+ * @template Separator The string used to join composite key components (default: '#').
114
+ */
113
115
  type TableEntry<Entity extends Record<string, unknown>, Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = '#'> = TableEntryImpl<Entity, Schema, Separator>;
114
116
  type InputSpec<E> = {
115
117
  [key in keyof E]: (undefined extends E[key] ? [
@@ -131,39 +133,90 @@ type DiscriminatedSchema<Entity, E> = {
131
133
  } : never;
132
134
  }[keyof E];
133
135
  type FullKeySpec<Entity> = FullKeySpecSimple<Entity> | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>;
134
- 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>> : never;
136
+ declare class RotoriseError extends Error {
137
+ constructor(message: string);
138
+ }
139
+ 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'>;
135
140
  type SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape;
136
141
  type SpecConfigShape = {
137
142
  depth?: number;
138
143
  allowPartial?: boolean;
139
144
  enforceBoundary?: boolean;
140
145
  };
141
- type VariantType<Entity, K extends PropertyKey, V extends PropertyKey> = [
146
+ type ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
147
+ Entity
148
+ ] extends [never] ? never : Extract<Entity, {
149
+ [k in K]: V;
150
+ }>;
151
+ type TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
142
152
  Entity
143
153
  ] extends [never] ? {
144
154
  [k in K]: V;
145
155
  } : Entity & {
146
156
  [k in K]: V;
147
157
  };
148
- type ProcessVariant<Entity, K extends PropertyKey, V extends PropertyKey, Spec extends DiscriminatedSchemaShape, Config extends SpecConfigShape> = VariantType<ProcessSpecType<VariantType<Entity, K, V>, Spec['spec'][V & keyof Spec['spec']], Config>, K, V>;
149
- type OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = Spec extends DiscriminatedSchemaShape ? {
158
+ 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>;
159
+ type OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<Spec extends DiscriminatedSchemaShape ? {
150
160
  [K in Spec['discriminator']]: {
151
161
  [V in keyof Spec['spec']]: ProcessVariant<Entity, K, V, Spec, Config>;
152
162
  }[keyof Spec['spec']];
153
- }[Spec['discriminator']] : ProcessSpecType<Entity, Spec, Config>;
154
- 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 ? NullAs : never;
155
- type OptimizedBuildedKey<Entity, Spec, Separator extends string, Config extends SpecConfigShape, Attributes> = Entity extends unknown ? Spec extends DiscriminatedSchemaShape ? ProcessKey<Entity, ValueOf<Spec['spec'], ValueOf<Entity, Spec['discriminator']>>, Separator, undefined, Config, Attributes> : ProcessKey<Entity, Spec, Separator, undefined, Config, Attributes> : never;
163
+ }[Spec['discriminator']] : ProcessSpecType<Entity, Spec, Config>>;
164
+ 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'>;
165
+ 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;
156
166
  type TableEntryDefinition<Entity, Schema, Separator extends string> = {
157
- toEntry: <const ExactEntity extends Exact<Entity, ExactEntity>>(item: ExactEntity) => TableEntryImpl<ExactEntity, Schema, Separator>;
167
+ /**
168
+ * Converts a raw entity into a complete table entry with all keys computed.
169
+ * Use this when preparing items for insertion into DynamoDB.
170
+ */
171
+ toEntry: <const ExactEntity>(item: Exact<Entity, ExactEntity>) => TableEntryImpl<ExactEntity, Schema, Separator>;
172
+ /**
173
+ * Extracts the raw entity from a table entry by removing all computed keys.
174
+ * Use this when processing items retrieved from DynamoDB.
175
+ */
158
176
  fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(entry: Entry) => DistributiveOmit<Entry, keyof Schema>;
177
+ /**
178
+ * Generates a specific key for the given entity attributes.
179
+ * Supports partial keys and depth limiting for query operations.
180
+ *
181
+ * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').
182
+ * @param attributes the object containing the values needed to build the key.
183
+ * @param config Optional configuration for partial keys or depth limiting.
184
+ */
159
185
  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] ? {
160
186
  depth?: undefined;
161
187
  allowPartial?: undefined;
162
188
  enforceBoundary?: boolean;
163
- } : Config>(key: Key, attributes: Attributes, config?: Config) => OptimizedBuildedKey<Attributes, Spec, Separator, Config_, Attributes>;
189
+ } : Config>(key: Key, attributes: Attributes, config?: Config) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>;
190
+ /**
191
+ * A zero-runtime inference helper. Use this with `typeof` to get the
192
+ * total type of a table entry.
193
+ */
164
194
  infer: TableEntryImpl<Entity, Schema, Separator>;
195
+ /**
196
+ * Creates a proxy to generate property paths as strings.
197
+ * Useful for building UpdateExpressions or ProjectionExpressions.
198
+ *
199
+ * @example
200
+ * table.path().data.nested.property.toString() // returns "data.nested.property"
201
+ */
165
202
  path: () => TableEntryImpl<Entity, Schema, Separator>;
166
203
  };
167
- declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, separator?: Separator) => TableEntryDefinition<Entity, Schema, Separator>;
204
+ /**
205
+ * Entry point for defining a DynamoDB table schema with Rotorise.
206
+ *
207
+ * @template Entity The base entity type that this table represents.
208
+ * @returns A builder function that accepts the schema and an optional separator.
209
+ *
210
+ * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.
211
+ *
212
+ * @example
213
+ * const userTable = tableEntry<User>()({
214
+ * PK: ["orgId", "id"],
215
+ * SK: "role"
216
+ * })
217
+ */
218
+ declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, ...[separator]: [Separator] extends [
219
+ ''
220
+ ] ? [ErrorMessage<'Separator must not be an empty string'>] : [separator?: Separator]) => TableEntryDefinition<Entity, Schema, Separator>;
168
221
 
169
- export { type CompositeKeyBuilder, type CompositeKeyParams, type CompositeKeyParamsImpl, type TableEntry, type TransformShape, tableEntry };
222
+ export { type CompositeKeyBuilder, type CompositeKeyParams, type CompositeKeyParamsImpl, RotoriseError, type TableEntry, type TransformShape, tableEntry };
@@ -1,44 +1,22 @@
1
- type KeysOfUnion<ObjectType> = ObjectType extends unknown
2
- ? keyof ObjectType
3
- : never
4
- type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <
5
- G,
6
- >() => G extends U ? 1 : 2
7
- ? true
8
- : false
9
-
10
- type ArrayElement<T> = T extends readonly unknown[] ? T[0] : never
11
-
12
- type ExactObject<ParameterType, InputType> = {
13
- [Key in keyof ParameterType]: Exact<
14
- ParameterType[Key],
15
- Key extends keyof InputType ? InputType[Key] : never
16
- >
17
- } & Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>
18
-
19
- type Exact<ParameterType, InputType> = IsEqual<
20
- ParameterType,
21
- InputType
22
- > extends true
23
- ? ParameterType
24
- : // Convert union of array to array of union: A[] & B[] => (A & B)[]
25
- ParameterType extends unknown[]
26
- ? Array<Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>>
27
- : // In TypeScript, Array is a subtype of ReadonlyArray, so always test Array before ReadonlyArray.
28
- ParameterType extends readonly unknown[]
29
- ? ReadonlyArray<
30
- Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>
31
- >
32
- : ParameterType extends object
33
- ? ExactObject<ParameterType, InputType>
34
- : ParameterType
1
+ // Lighter Exact implementation catches excess top-level properties only.
2
+ // Does not recursively check nested objects/arrays. This is intentional:
3
+ // DynamoDB entity shapes are flat at the key level.
4
+ type Exact<Shape, Candidate> = Candidate & {
5
+ [K in keyof Candidate]: K extends keyof Shape ? Candidate[K] : never
6
+ }
35
7
 
36
8
  type ValueOf<
37
9
  ObjectType,
38
10
  ValueType = keyof ObjectType,
39
11
  > = ValueType extends keyof ObjectType ? ObjectType[ValueType] : never
40
12
 
41
- type evaluate<T> = { [K in keyof T]: T[K] } & unknown
13
+ /**
14
+ * Force an operation like `{ a: 0 } & { b: 1 }` to be computed so that it displays `{ a: 0; b: 1 }`.
15
+ * This version is distributive, meaning it will preserve union types while flattening each member.
16
+ */
17
+ type show<T> = T extends unknown
18
+ ? { [K in keyof T]: T[K] } & unknown
19
+ : never
42
20
 
43
21
  type DistributivePick<T, K> = T extends unknown
44
22
  ? K extends keyof T
@@ -54,48 +32,63 @@ type SliceFromStart<
54
32
  T,
55
33
  End extends number,
56
34
  Acc extends unknown[] = [],
57
- > = T extends unknown[]
58
- ? Acc['length'] extends End
59
- ? Acc
60
- : T extends [infer Head, ...infer Tail]
61
- ? SliceFromStart<Tail, End, [...Acc, Head]>
62
- : Acc
63
- : never
35
+ > = End extends 0
36
+ ? []
37
+ : End extends 1
38
+ ? T extends [infer Head, ...unknown[]]
39
+ ? [Head]
40
+ : []
41
+ : T extends unknown[]
42
+ ? Acc['length'] extends End
43
+ ? Acc
44
+ : T extends [infer Head, ...infer Tail]
45
+ ? SliceFromStart<Tail, End, [...Acc, Head]>
46
+ : Acc
47
+ : never
64
48
 
65
49
  type MergeIntersectionObject<T, Keys = keyof T> = {
66
50
  [K in Keys]: T[K]
67
51
  }
68
- // type t = MergeIntersectionObject<
69
- // { a: 'a1'; b: 1n; extra: 'extra' } | { a: 'a2'; b: 2 }
70
- // >
71
52
 
72
53
  type NonEmptyArray<T> = [T, ...T[]]
73
54
 
74
55
  type Replace<T, U, V> = T extends U ? V : T
75
56
 
76
- type CompositeKeyParamsImpl<Entity, InputSpec extends InputSpecShape, skip extends number = 1> = Entity extends unknown ? evaluate<Pick<Entity, extractHeadOrPass<SliceFromStart<InputSpec, number extends skip ? 1 : skip>[number]> & keyof Entity> & Partial<Pick<Entity, extractHeadOrPass<InputSpec[number]> & keyof Entity>>> : never;
57
+ /**
58
+ * Represents a type-level error message. Used to provide helpful feedback in the IDE.
59
+ */
60
+ declare const errorMessage: unique symbol
61
+ type ErrorMessage<T extends string> = {
62
+ readonly [errorMessage]: T
63
+ }
64
+
65
+ 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;
66
+ type CompositeKeyParamsImpl<Entity, InputSpec extends InputSpecShape, skip extends number = 1> = Entity extends unknown ? show<{
67
+ [K in extractHeadOrPass<SliceFromStart<InputSpec, number extends skip ? 1 : skip>[number]> & keyof Entity]: TransformOverride<InputSpec, K, Entity[K]>;
68
+ } & {
69
+ [K in extractHeadOrPass<InputSpec[number]> & keyof Entity]?: TransformOverride<InputSpec, K, Entity[K]>;
70
+ }> : never;
77
71
  type CompositeKeyParams<Entity extends Record<string, unknown>, FullSpec extends InputSpec<MergeIntersectionObject<Entity>>[], skip extends number = 1> = CompositeKeyParamsImpl<Entity, FullSpec, skip>;
78
- type CompositeKeyBuilderImpl<Entity, Spec, Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = Entity extends unknown ? Join<CompositeKeyRec<Entity, number extends Deep ? Spec : SliceFromStart<Spec, Deep>>, Separator, boolean extends isPartial ? false : isPartial> : never;
72
+ type CompositeKeyBuilderImpl<Entity, Spec, Separator extends string = '#', Deep extends number = number, isPartial extends boolean = false> = Entity extends unknown ? CompositeKeyStringBuilder<Entity, [
73
+ Deep
74
+ ] extends [never] ? Spec : number extends Deep ? Spec : SliceFromStart<Spec, Deep>, Separator, boolean extends isPartial ? false : isPartial> : never;
79
75
  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>;
80
76
  type joinable = string | number | bigint | boolean | null | undefined;
81
- type joinablePair = [joinable, joinable];
82
- type Join<Pairs, Separator extends string, KeepIntermediate extends boolean = false, Acc extends string = '', AllAcc extends string = never> = Pairs extends [infer Head extends joinablePair, ...infer Tail] ? Join<Tail, Separator, KeepIntermediate, Acc extends '' ? [Head[0]] extends [never] ? `${Head[1]}` : `${Head[0]}${Separator}${Head[1]}` : [Head[0]] extends [never] ? `${Acc}${Separator}${Head[1]}` : `${Acc}${Separator}${Head[0]}${Separator}${Head[1]}`, KeepIntermediate extends true ? AllAcc | (Acc extends '' ? never : Acc) : never> : AllAcc | Acc;
83
- type ExtractHelper<Key, Value> = Value extends joinable ? [Key, Value] : Value extends {
77
+ type ExtractHelper<Key, Value> = Value extends object ? Value extends {
84
78
  tag: infer Tag extends string;
85
79
  value: infer Value extends joinable;
86
80
  } ? [Tag, Value] : Value extends {
87
- tag?: undefined;
88
81
  value: infer Value extends joinable;
89
- } ? [never, Value] : never;
82
+ } ? [never, Value] : never : [Key, Value];
90
83
  type ExtractPair<Entity, Spec> = Spec extends [
91
84
  infer Key extends string,
92
85
  (...key: any[]) => infer Value,
93
86
  ...unknown[]
94
87
  ] ? ExtractHelper<Uppercase<Key>, Value> : Spec extends keyof Entity & string ? [Uppercase<Spec>, Entity[Spec] & joinable] : never;
95
- type CompositeKeyRec<Entity, Spec, Acc extends joinablePair[] = [], KeysCache extends string = keyof Entity & string> = Spec extends [infer Head, ...infer Tail] ? CompositeKeyRec<Entity, Tail, [
96
- ...Acc,
97
- ExtractPair<Entity, Head>
98
- ], KeysCache> : Acc;
88
+ 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 [
89
+ infer Key extends joinable,
90
+ infer Value extends joinable
91
+ ] ? 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;
99
92
  type DiscriminatedSchemaShape = {
100
93
  discriminator: PropertyKey;
101
94
  spec: {
@@ -107,9 +100,18 @@ type TransformShape = {
107
100
  tag?: string;
108
101
  value: joinable;
109
102
  } | joinable;
110
- type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity extends unknown ? {
111
- [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape ? ProcessKey<Entity, ValueOf<Schema[Key]['spec'], ValueOf<Entity, Schema[Key]['discriminator']>>, Separator> : ProcessKey<Entity, Schema[Key], Separator>;
112
- } & Entity : never;
103
+ 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;
104
+ type TableEntryImpl<Entity, Schema, Separator extends string = '#'> = Entity extends unknown ? show<{
105
+ 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'>;
106
+ } & Entity> : never;
107
+ /**
108
+ * Represents a complete DynamoDB table entry, combining the original entity
109
+ * with its computed internal and global keys.
110
+ *
111
+ * @template Entity The base entity type.
112
+ * @template Schema The schema defining the table keys.
113
+ * @template Separator The string used to join composite key components (default: '#').
114
+ */
113
115
  type TableEntry<Entity extends Record<string, unknown>, Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = '#'> = TableEntryImpl<Entity, Schema, Separator>;
114
116
  type InputSpec<E> = {
115
117
  [key in keyof E]: (undefined extends E[key] ? [
@@ -131,39 +133,90 @@ type DiscriminatedSchema<Entity, E> = {
131
133
  } : never;
132
134
  }[keyof E];
133
135
  type FullKeySpec<Entity> = FullKeySpecSimple<Entity> | DiscriminatedSchema<Entity, MergeIntersectionObject<Entity>>;
134
- 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>> : never;
136
+ declare class RotoriseError extends Error {
137
+ constructor(message: string);
138
+ }
139
+ 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'>;
135
140
  type SpecConfig<Spec> = Spec extends string ? never : SpecConfigShape;
136
141
  type SpecConfigShape = {
137
142
  depth?: number;
138
143
  allowPartial?: boolean;
139
144
  enforceBoundary?: boolean;
140
145
  };
141
- type VariantType<Entity, K extends PropertyKey, V extends PropertyKey> = [
146
+ type ExtractVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
147
+ Entity
148
+ ] extends [never] ? never : Extract<Entity, {
149
+ [k in K]: V;
150
+ }>;
151
+ type TagVariant<Entity, K extends PropertyKey, V extends PropertyKey> = [
142
152
  Entity
143
153
  ] extends [never] ? {
144
154
  [k in K]: V;
145
155
  } : Entity & {
146
156
  [k in K]: V;
147
157
  };
148
- type ProcessVariant<Entity, K extends PropertyKey, V extends PropertyKey, Spec extends DiscriminatedSchemaShape, Config extends SpecConfigShape> = VariantType<ProcessSpecType<VariantType<Entity, K, V>, Spec['spec'][V & keyof Spec['spec']], Config>, K, V>;
149
- type OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = Spec extends DiscriminatedSchemaShape ? {
158
+ 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>;
159
+ type OptimizedAttributes<Entity, Spec, Config extends SpecConfigShape> = show<Spec extends DiscriminatedSchemaShape ? {
150
160
  [K in Spec['discriminator']]: {
151
161
  [V in keyof Spec['spec']]: ProcessVariant<Entity, K, V, Spec, Config>;
152
162
  }[keyof Spec['spec']];
153
- }[Spec['discriminator']] : ProcessSpecType<Entity, Spec, Config>;
154
- 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 ? NullAs : never;
155
- type OptimizedBuildedKey<Entity, Spec, Separator extends string, Config extends SpecConfigShape, Attributes> = Entity extends unknown ? Spec extends DiscriminatedSchemaShape ? ProcessKey<Entity, ValueOf<Spec['spec'], ValueOf<Entity, Spec['discriminator']>>, Separator, undefined, Config, Attributes> : ProcessKey<Entity, Spec, Separator, undefined, Config, Attributes> : never;
163
+ }[Spec['discriminator']] : ProcessSpecType<Entity, Spec, Config>>;
164
+ 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'>;
165
+ 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;
156
166
  type TableEntryDefinition<Entity, Schema, Separator extends string> = {
157
- toEntry: <const ExactEntity extends Exact<Entity, ExactEntity>>(item: ExactEntity) => TableEntryImpl<ExactEntity, Schema, Separator>;
167
+ /**
168
+ * Converts a raw entity into a complete table entry with all keys computed.
169
+ * Use this when preparing items for insertion into DynamoDB.
170
+ */
171
+ toEntry: <const ExactEntity>(item: Exact<Entity, ExactEntity>) => TableEntryImpl<ExactEntity, Schema, Separator>;
172
+ /**
173
+ * Extracts the raw entity from a table entry by removing all computed keys.
174
+ * Use this when processing items retrieved from DynamoDB.
175
+ */
158
176
  fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(entry: Entry) => DistributiveOmit<Entry, keyof Schema>;
177
+ /**
178
+ * Generates a specific key for the given entity attributes.
179
+ * Supports partial keys and depth limiting for query operations.
180
+ *
181
+ * @param key The name of the key to generate (e.g., 'PK', 'GSIPK').
182
+ * @param attributes the object containing the values needed to build the key.
183
+ * @param config Optional configuration for partial keys or depth limiting.
184
+ */
159
185
  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] ? {
160
186
  depth?: undefined;
161
187
  allowPartial?: undefined;
162
188
  enforceBoundary?: boolean;
163
- } : Config>(key: Key, attributes: Attributes, config?: Config) => OptimizedBuildedKey<Attributes, Spec, Separator, Config_, Attributes>;
189
+ } : Config>(key: Key, attributes: Attributes, config?: Config) => OptimizedBuiltKey<Attributes, Spec, Separator, Config_, Attributes>;
190
+ /**
191
+ * A zero-runtime inference helper. Use this with `typeof` to get the
192
+ * total type of a table entry.
193
+ */
164
194
  infer: TableEntryImpl<Entity, Schema, Separator>;
195
+ /**
196
+ * Creates a proxy to generate property paths as strings.
197
+ * Useful for building UpdateExpressions or ProjectionExpressions.
198
+ *
199
+ * @example
200
+ * table.path().data.nested.property.toString() // returns "data.nested.property"
201
+ */
165
202
  path: () => TableEntryImpl<Entity, Schema, Separator>;
166
203
  };
167
- declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, separator?: Separator) => TableEntryDefinition<Entity, Schema, Separator>;
204
+ /**
205
+ * Entry point for defining a DynamoDB table schema with Rotorise.
206
+ *
207
+ * @template Entity The base entity type that this table represents.
208
+ * @returns A builder function that accepts the schema and an optional separator.
209
+ *
210
+ * Note: the double-call `<Entity>()(schema)` is required for partial type parameter inference.
211
+ *
212
+ * @example
213
+ * const userTable = tableEntry<User>()({
214
+ * PK: ["orgId", "id"],
215
+ * SK: "role"
216
+ * })
217
+ */
218
+ declare const tableEntry: <const Entity extends Record<string, unknown>>() => <const Schema extends Record<string, FullKeySpec<Entity>>, Separator extends string = "#">(schema: Schema, ...[separator]: [Separator] extends [
219
+ ''
220
+ ] ? [ErrorMessage<'Separator must not be an empty string'>] : [separator?: Separator]) => TableEntryDefinition<Entity, Schema, Separator>;
168
221
 
169
- export { type CompositeKeyBuilder, type CompositeKeyParams, type CompositeKeyParamsImpl, type TableEntry, type TransformShape, tableEntry };
222
+ export { type CompositeKeyBuilder, type CompositeKeyParams, type CompositeKeyParamsImpl, RotoriseError, type TableEntry, type TransformShape, tableEntry };
package/dist/Rotorise.js CHANGED
@@ -1,4 +1,10 @@
1
1
  // src/Rotorise.ts
2
+ var RotoriseError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "RotoriseError";
6
+ }
7
+ };
2
8
  var chainableNoOpProxy = new Proxy(() => chainableNoOpProxy, {
3
9
  get: () => chainableNoOpProxy
4
10
  });
@@ -20,7 +26,7 @@ var createPathProxy = (path = "") => {
20
26
  var key = () => (schema, separator = "#") => (key2, attributes, config) => {
21
27
  const case_ = schema[key2];
22
28
  if (case_ === void 0) {
23
- throw new Error(`Key ${key2.toString()} not found in schema`);
29
+ throw new RotoriseError(`Key ${key2.toString()} not found in schema`);
24
30
  }
25
31
  let structure;
26
32
  if (Array.isArray(case_)) {
@@ -28,13 +34,13 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
28
34
  } else if (typeof case_ === "object") {
29
35
  const discriminator = attributes[case_.discriminator];
30
36
  if (discriminator === void 0) {
31
- throw new Error(
37
+ throw new RotoriseError(
32
38
  `Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`
33
39
  );
34
40
  }
35
41
  const val = case_.spec[discriminator];
36
42
  if (val === void 0) {
37
- throw new Error(
43
+ throw new RotoriseError(
38
44
  `Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`
39
45
  );
40
46
  }
@@ -74,7 +80,7 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
74
80
  } else if (config?.allowPartial) {
75
81
  break;
76
82
  } else {
77
- throw new Error(
83
+ throw new RotoriseError(
78
84
  `buildCompositeKey: Attribute ${key3.toString()} not found in ${JSON.stringify(attributes)}`
79
85
  );
80
86
  }
@@ -86,8 +92,9 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
86
92
  };
87
93
  var toEntry = () => (schema, separator = "#") => (item) => {
88
94
  const entry = { ...item };
95
+ const buildKey = key()(schema, separator);
89
96
  for (const key_ in schema) {
90
- const val = key()(schema, separator)(key_, item);
97
+ const val = buildKey(key_, item);
91
98
  if (val !== void 0) {
92
99
  entry[key_] = val;
93
100
  }
@@ -101,16 +108,20 @@ var fromEntry = () => (schema) => (entry) => {
101
108
  }
102
109
  return item;
103
110
  };
104
- var tableEntry = () => (schema, separator = "#") => {
111
+ var tableEntry = () => (schema, ...[separator]) => {
112
+ const sep = separator ?? "#";
113
+ if (sep === "" || typeof sep !== "string") {
114
+ throw new RotoriseError("Separator must not be an empty string");
115
+ }
105
116
  return {
106
- toEntry: toEntry()(schema, separator),
117
+ toEntry: toEntry()(schema, sep),
107
118
  fromEntry: fromEntry()(schema),
108
- key: key()(schema, separator),
119
+ key: key()(schema, sep),
109
120
  infer: chainableNoOpProxy,
110
121
  path: () => createPathProxy()
111
122
  };
112
123
  };
113
- export {
114
- tableEntry
115
- };
124
+
125
+ export { RotoriseError, tableEntry };
126
+ //# sourceMappingURL=Rotorise.js.map
116
127
  //# sourceMappingURL=Rotorise.js.map