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.
- package/README.md +187 -3
- package/dist/Rotorise.cjs +24 -35
- package/dist/Rotorise.cjs.map +1 -1
- package/dist/Rotorise.d.cts +123 -70
- package/dist/Rotorise.d.ts +123 -70
- package/dist/Rotorise.js +22 -11
- package/dist/Rotorise.js.map +1 -1
- package/package.json +7 -4
- package/.attest/assertions/typescript.json +0 -122
- package/setupVitest.ts +0 -4
- package/vitest.config.ts +0 -7
package/dist/Rotorise.d.cts
CHANGED
|
@@ -1,44 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
> =
|
|
58
|
-
?
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
?
|
|
62
|
-
:
|
|
63
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
],
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 :
|
|
155
|
-
type
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -1,44 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
> =
|
|
58
|
-
?
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
?
|
|
62
|
-
:
|
|
63
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
],
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 :
|
|
155
|
-
type
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
117
|
+
toEntry: toEntry()(schema, sep),
|
|
107
118
|
fromEntry: fromEntry()(schema),
|
|
108
|
-
key: key()(schema,
|
|
119
|
+
key: key()(schema, sep),
|
|
109
120
|
infer: chainableNoOpProxy,
|
|
110
121
|
path: () => createPathProxy()
|
|
111
122
|
};
|
|
112
123
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
|
|
125
|
+
export { RotoriseError, tableEntry };
|
|
126
|
+
//# sourceMappingURL=Rotorise.js.map
|
|
116
127
|
//# sourceMappingURL=Rotorise.js.map
|