rotorise 0.2.4 → 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 +28 -35
- package/dist/Rotorise.cjs.map +1 -1
- package/dist/Rotorise.d.cts +125 -70
- package/dist/Rotorise.d.ts +125 -70
- package/dist/Rotorise.js +26 -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/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Rotorise
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe DynamoDB composite key management for TypeScript.
|
|
4
4
|
|
|
5
|
-
DynamoDB offers incredible flexibility, but managing advanced patterns and techniques can be a challenge.
|
|
5
|
+
DynamoDB offers incredible flexibility, but managing advanced patterns and techniques can be a challenge. Rotorise simplifies complex operations by providing abstractions for key definitions, composite key constructors, partial composite keys, and advanced sort key usage in queries. It integrates seamlessly with [Brushless](https://github.com/josher8a/Brushless) for a frictionless and performant DynamoDB experience.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -10,6 +10,190 @@ DynamoDB offers incredible flexibility, but managing advanced patterns and techn
|
|
|
10
10
|
npm install rotorise
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Define your entity type and schema. Rotorise infers the exact key string types.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { tableEntry } from 'rotorise'
|
|
19
|
+
|
|
20
|
+
type User = {
|
|
21
|
+
orgId: string
|
|
22
|
+
id: string
|
|
23
|
+
role: 'admin' | 'user' | 'guest'
|
|
24
|
+
email: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const UserTable = tableEntry<User>()({
|
|
28
|
+
PK: ['orgId', 'id'],
|
|
29
|
+
SK: ['role'],
|
|
30
|
+
GSI1PK: ['role'],
|
|
31
|
+
GSI1SK: 'email',
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `tableEntry<Entity>()(schema, separator?)`
|
|
38
|
+
|
|
39
|
+
Entry point for defining a DynamoDB table schema. Returns an object with the methods below.
|
|
40
|
+
|
|
41
|
+
The double-call `<Entity>()(schema)` is required because TypeScript does not support partial type parameter inference — `Entity` is explicit while `Schema` is inferred from arguments.
|
|
42
|
+
|
|
43
|
+
The optional `separator` defaults to `'#'`.
|
|
44
|
+
|
|
45
|
+
### `.key(keyName, attributes, config?)`
|
|
46
|
+
|
|
47
|
+
Builds a specific key value from the given attributes.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
UserTable.key('PK', { orgId: 'acme', id: '123' })
|
|
51
|
+
// => 'ORGID#acme#ID#123'
|
|
52
|
+
|
|
53
|
+
UserTable.key('GSI1SK', { email: 'a@b.com' })
|
|
54
|
+
// => 'a@b.com'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`config` options:**
|
|
58
|
+
|
|
59
|
+
- **`depth`** — Limit composite key to the first N components. Useful for `begins_with` queries.
|
|
60
|
+
- **`allowPartial`** — When `true`, stops building the key when an attribute is missing instead of throwing. Returns a union of all valid partial prefixes at the type level.
|
|
61
|
+
- **`enforceBoundary`** — When `true`, appends a trailing separator if the key is partial. Ensures a `begins_with` query doesn't match unintended prefixes.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
UserTable.key('PK', { orgId: 'acme' }, { allowPartial: true })
|
|
65
|
+
// => 'ORGID#acme'
|
|
66
|
+
// Type: 'ORGID#acme' | `ORGID#${string}#ID#${string}`
|
|
67
|
+
|
|
68
|
+
UserTable.key('PK', { orgId: 'acme', id: '1' }, { depth: 1 })
|
|
69
|
+
// => 'ORGID#acme'
|
|
70
|
+
|
|
71
|
+
UserTable.key('PK', { orgId: 'acme', id: '1' }, { depth: 1, enforceBoundary: true })
|
|
72
|
+
// => 'ORGID#acme#'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `.toEntry(item)`
|
|
76
|
+
|
|
77
|
+
Converts a raw entity into a complete DynamoDB item with all keys computed.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const item = UserTable.toEntry({
|
|
81
|
+
orgId: 'acme',
|
|
82
|
+
id: '123',
|
|
83
|
+
role: 'admin',
|
|
84
|
+
email: 'a@b.com',
|
|
85
|
+
})
|
|
86
|
+
// => { orgId: 'acme', id: '123', role: 'admin', email: 'a@b.com',
|
|
87
|
+
// PK: 'ORGID#acme#ID#123', SK: 'ROLE#admin',
|
|
88
|
+
// GSI1PK: 'ROLE#admin', GSI1SK: 'a@b.com' }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rejects excess properties at the type level.
|
|
92
|
+
|
|
93
|
+
### `.fromEntry(entry)`
|
|
94
|
+
|
|
95
|
+
Strips computed keys from a table entry, returning the raw entity.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const user = UserTable.fromEntry(item)
|
|
99
|
+
// => { orgId: 'acme', id: '123', role: 'admin', email: 'a@b.com' }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `.infer`
|
|
103
|
+
|
|
104
|
+
Zero-runtime inference helper. Use with `typeof` to get the full table entry type.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
type UserEntry = typeof UserTable.infer
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `.path()`
|
|
111
|
+
|
|
112
|
+
Creates a proxy that builds DynamoDB expression paths as strings.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
UserTable.path().email.toString() // => 'email'
|
|
116
|
+
UserTable.path().PK.toString() // => 'PK'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Advanced Features
|
|
120
|
+
|
|
121
|
+
### Transforms
|
|
122
|
+
|
|
123
|
+
Override how a field maps to its key segment using a transform function.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const Table = tableEntry<User>()({
|
|
127
|
+
PK: [
|
|
128
|
+
['orgId', (id: string) => ({ tag: 'ORG', value: id })],
|
|
129
|
+
['id', (id: string) => ({ tag: 'USER', value: id })],
|
|
130
|
+
],
|
|
131
|
+
SK: ['role'],
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
Table.key('PK', { orgId: 'acme', id: '123' })
|
|
135
|
+
// => 'ORG#acme#USER#123'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A transform returns either:
|
|
139
|
+
- A `joinable` value (the field name uppercased becomes the tag)
|
|
140
|
+
- `{ value }` (no tag segment emitted)
|
|
141
|
+
- `{ tag, value }` (custom tag)
|
|
142
|
+
|
|
143
|
+
### Discriminated Schemas
|
|
144
|
+
|
|
145
|
+
When your table stores a union of entity types, use a discriminator to define per-variant key specs.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
type Item =
|
|
149
|
+
| { kind: 'order'; orderId: string; userId: string }
|
|
150
|
+
| { kind: 'refund'; refundId: string; orderId: string }
|
|
151
|
+
|
|
152
|
+
const ItemTable = tableEntry<Item>()({
|
|
153
|
+
PK: {
|
|
154
|
+
discriminator: 'kind',
|
|
155
|
+
spec: {
|
|
156
|
+
order: ['userId', 'orderId'],
|
|
157
|
+
refund: ['orderId', 'refundId'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
SK: ['kind'],
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
ItemTable.key('PK', { kind: 'order', userId: 'u1', orderId: 'o1' })
|
|
164
|
+
// => 'USERID#u1#ORDERID#o1'
|
|
165
|
+
|
|
166
|
+
ItemTable.key('PK', { kind: 'refund', orderId: 'o1', refundId: 'r1' })
|
|
167
|
+
// => 'ORDERID#o1#REFUNDID#r1'
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Set a discriminated spec value to `null` to produce `undefined` (useful for GSIs that don't apply to all variants).
|
|
171
|
+
|
|
172
|
+
### Custom Separator
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const Table = tableEntry<User>()(schema, '-')
|
|
176
|
+
// Keys use '-' instead of '#': 'ORGID-acme-ID-123'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Error Handling
|
|
180
|
+
|
|
181
|
+
All runtime errors throw `RotoriseError` (exported), so you can distinguish library errors from your own.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { RotoriseError } from 'rotorise'
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
Table.key('PK', { /* missing required attrs */ })
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e instanceof RotoriseError) { /* ... */ }
|
|
190
|
+
}
|
|
191
|
+
```
|
|
13
192
|
|
|
14
193
|
## Contributing
|
|
15
|
-
|
|
194
|
+
|
|
195
|
+
Open an [issue](https://github.com/josher8a/Rotorise/issues) or a PR. We are open to any kind of contribution and feedback.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
[Apache-2.0](LICENSE)
|
package/dist/Rotorise.cjs
CHANGED
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key2 of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
14
|
-
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
1
|
+
'use strict';
|
|
19
2
|
|
|
20
3
|
// src/Rotorise.ts
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
var RotoriseError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RotoriseError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
26
10
|
var chainableNoOpProxy = new Proxy(() => chainableNoOpProxy, {
|
|
27
11
|
get: () => chainableNoOpProxy
|
|
28
12
|
});
|
|
@@ -44,7 +28,7 @@ var createPathProxy = (path = "") => {
|
|
|
44
28
|
var key = () => (schema, separator = "#") => (key2, attributes, config) => {
|
|
45
29
|
const case_ = schema[key2];
|
|
46
30
|
if (case_ === void 0) {
|
|
47
|
-
throw new
|
|
31
|
+
throw new RotoriseError(`Key ${key2.toString()} not found in schema`);
|
|
48
32
|
}
|
|
49
33
|
let structure;
|
|
50
34
|
if (Array.isArray(case_)) {
|
|
@@ -52,13 +36,13 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
|
|
|
52
36
|
} else if (typeof case_ === "object") {
|
|
53
37
|
const discriminator = attributes[case_.discriminator];
|
|
54
38
|
if (discriminator === void 0) {
|
|
55
|
-
throw new
|
|
39
|
+
throw new RotoriseError(
|
|
56
40
|
`Discriminator ${case_.discriminator.toString()} not found in ${JSON.stringify(attributes)}`
|
|
57
41
|
);
|
|
58
42
|
}
|
|
59
43
|
const val = case_.spec[discriminator];
|
|
60
44
|
if (val === void 0) {
|
|
61
|
-
throw new
|
|
45
|
+
throw new RotoriseError(
|
|
62
46
|
`Discriminator value ${discriminator?.toString()} not found in ${JSON.stringify(attributes)}`
|
|
63
47
|
);
|
|
64
48
|
}
|
|
@@ -74,6 +58,7 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
|
|
|
74
58
|
if (value == null) return void 0;
|
|
75
59
|
return value;
|
|
76
60
|
}
|
|
61
|
+
const fullLength = structure.length;
|
|
77
62
|
if (config?.depth !== void 0) {
|
|
78
63
|
structure = structure.slice(0, config.depth);
|
|
79
64
|
}
|
|
@@ -97,17 +82,21 @@ var key = () => (schema, separator = "#") => (key2, attributes, config) => {
|
|
|
97
82
|
} else if (config?.allowPartial) {
|
|
98
83
|
break;
|
|
99
84
|
} else {
|
|
100
|
-
throw new
|
|
85
|
+
throw new RotoriseError(
|
|
101
86
|
`buildCompositeKey: Attribute ${key3.toString()} not found in ${JSON.stringify(attributes)}`
|
|
102
87
|
);
|
|
103
88
|
}
|
|
104
89
|
}
|
|
90
|
+
if (config?.enforceBoundary && fullLength * 2 > composite.length) {
|
|
91
|
+
composite.push("");
|
|
92
|
+
}
|
|
105
93
|
return composite.join(separator);
|
|
106
94
|
};
|
|
107
95
|
var toEntry = () => (schema, separator = "#") => (item) => {
|
|
108
96
|
const entry = { ...item };
|
|
97
|
+
const buildKey = key()(schema, separator);
|
|
109
98
|
for (const key_ in schema) {
|
|
110
|
-
const val =
|
|
99
|
+
const val = buildKey(key_, item);
|
|
111
100
|
if (val !== void 0) {
|
|
112
101
|
entry[key_] = val;
|
|
113
102
|
}
|
|
@@ -121,17 +110,21 @@ var fromEntry = () => (schema) => (entry) => {
|
|
|
121
110
|
}
|
|
122
111
|
return item;
|
|
123
112
|
};
|
|
124
|
-
var tableEntry = () => (schema, separator
|
|
113
|
+
var tableEntry = () => (schema, ...[separator]) => {
|
|
114
|
+
const sep = separator ?? "#";
|
|
115
|
+
if (sep === "" || typeof sep !== "string") {
|
|
116
|
+
throw new RotoriseError("Separator must not be an empty string");
|
|
117
|
+
}
|
|
125
118
|
return {
|
|
126
|
-
toEntry: toEntry()(schema,
|
|
119
|
+
toEntry: toEntry()(schema, sep),
|
|
127
120
|
fromEntry: fromEntry()(schema),
|
|
128
|
-
key: key()(schema,
|
|
121
|
+
key: key()(schema, sep),
|
|
129
122
|
infer: chainableNoOpProxy,
|
|
130
123
|
path: () => createPathProxy()
|
|
131
124
|
};
|
|
132
125
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
|
|
127
|
+
exports.RotoriseError = RotoriseError;
|
|
128
|
+
exports.tableEntry = tableEntry;
|
|
129
|
+
//# sourceMappingURL=Rotorise.cjs.map
|
|
137
130
|
//# sourceMappingURL=Rotorise.cjs.map
|
package/dist/Rotorise.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Rotorise.ts"],"sourcesContent":["import type {\n DistributiveOmit,\n DistributivePick,\n Exact,\n evaluate,\n MergeIntersectionObject,\n NonEmptyArray,\n Replace,\n SliceFromStart,\n ValueOf,\n} from './utils'\n\nexport type CompositeKeyParamsImpl<\n Entity,\n InputSpec extends InputSpecShape,\n skip extends number = 1,\n> = Entity extends unknown\n ? evaluate<\n Pick<\n Entity,\n extractHeadOrPass<\n SliceFromStart<\n InputSpec,\n number extends skip ? 1 : skip\n >[number]\n > &\n keyof Entity\n > &\n Partial<\n Pick<\n Entity,\n extractHeadOrPass<InputSpec[number]> & keyof Entity\n >\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 ? Join<\n CompositeKeyRec<\n Entity,\n number extends Deep ? Spec : SliceFromStart<Spec, Deep>\n >,\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\ntype joinablePair = [joinable, joinable]\n\ntype Join<\n Pairs,\n Separator extends string,\n KeepIntermediate extends boolean = false,\n Acc extends string = '',\n AllAcc extends string = never,\n> = Pairs extends [infer Head extends joinablePair, ...infer Tail]\n ? Join<\n Tail,\n Separator,\n KeepIntermediate,\n Acc extends ''\n ? [Head[0]] extends [never]\n ? `${Head[1]}`\n : `${Head[0]}${Separator}${Head[1]}`\n : [Head[0]] extends [never]\n ? `${Acc}${Separator}${Head[1]}`\n : `${Acc}${Separator}${Head[0]}${Separator}${Head[1]}`,\n KeepIntermediate extends true\n ? AllAcc | (Acc extends '' ? never : Acc)\n : never\n >\n : AllAcc | Acc\n\ntype ExtractHelper<Key, Value> = Value extends joinable\n ? [Key, Value]\n : Value extends {\n tag: infer Tag extends string\n value: infer Value extends joinable\n }\n ? [Tag, Value]\n : Value extends {\n tag?: undefined\n value: infer Value extends joinable\n }\n ? [never, Value]\n : never\n\ntype ExtractPair<Entity, Spec> = Spec extends [\n infer Key extends string,\n // biome-ignore lint/suspicious/noExplicitAny: <explanation>\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 CompositeKeyRec<\n Entity,\n Spec,\n Acc extends joinablePair[] = [],\n KeysCache extends string = keyof Entity & string,\n> = Spec extends [infer Head, ...infer Tail]\n ? CompositeKeyRec<\n Entity,\n Tail,\n [...Acc, ExtractPair<Entity, Head>],\n KeysCache\n >\n : 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: ggg\n ([PropertyKey, (key: any) => unknown, ...unknown[]] | PropertyKey)[]\nexport type TransformShape =\n | {\n tag?: string\n value: joinable\n }\n | joinable\n\ntype TableEntryImpl<\n Entity,\n Schema,\n Separator extends string = '#',\n> = Entity extends unknown\n ? {\n [Key in keyof Schema]: Schema[Key] extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<\n Schema[Key]['spec'],\n ValueOf<Entity, Schema[Key]['discriminator']>\n >,\n Separator\n >\n : ProcessKey<Entity, Schema[Key], Separator>\n } & Entity\n : never\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\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 { depth?: number; allowPartial?: boolean },\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 Error(`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 Error(\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 Error(\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 if (config?.depth !== undefined) {\n structure = structure.slice(0, config.depth) as never\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 Error(\n `buildCompositeKey: Attribute ${key.toString()} not found in ${JSON.stringify(attributes)}`,\n )\n }\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\n for (const key_ in schema) {\n const val = key<Entity>()(schema, separator)(key_, item)\n if (val !== undefined) {\n entry[key_] = val satisfies string as never\n }\n }\n // console.log({ entry })\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 // console.log({ item })\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 : never\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}\n\n// Pre-compute discriminated variant types\ntype VariantType<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> = VariantType<\n ProcessSpecType<\n VariantType<Entity, K, V>,\n Spec['spec'][V & keyof Spec['spec']],\n Config\n >,\n K,\n V\n>\n\n// Optimized attribute processing\ntype OptimizedAttributes<\n Entity,\n Spec,\n Config extends SpecConfigShape,\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\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\n ? NullAs\n : never\n\ntype OptimizedBuildedKey<\n Entity,\n Spec,\n Separator extends string,\n Config extends SpecConfigShape,\n Attributes,\n> = Entity extends unknown\n ? Spec extends DiscriminatedSchemaShape\n ? ProcessKey<\n Entity,\n ValueOf<Spec['spec'], ValueOf<Entity, Spec['discriminator']>>,\n Separator,\n undefined,\n Config,\n Attributes\n >\n : ProcessKey<Entity, Spec, Separator, undefined, Config, Attributes>\n : never\n\ntype TableEntryDefinition<Entity, Schema, Separator extends string> = {\n toEntry: <const ExactEntity extends Exact<Entity, ExactEntity>>(\n item: ExactEntity,\n ) => TableEntryImpl<ExactEntity, Schema, Separator>\n fromEntry: <const Entry extends TableEntryImpl<Entity, Schema, Separator>>(\n entry: Entry,\n ) => DistributiveOmit<Entry, keyof Schema>\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] // exclude undefined param\n ? { depth?: undefined; allowPartial?: undefined }\n : Config,\n >(\n key: Key,\n attributes: Attributes,\n config?: Config,\n ) => OptimizedBuildedKey<Attributes, Spec, Separator, Config_, Attributes>\n infer: TableEntryImpl<Entity, Schema, Separator>\n path: () => TableEntryImpl<Entity, Schema, Separator>\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 = '#' as Separator,\n ): TableEntryDefinition<Entity, Schema, Separator> => {\n return {\n toEntry: toEntry()(schema as never, separator) as never,\n fromEntry: fromEntry()(schema as never) as never,\n key: key()(schema as never, separator) as never,\n infer: chainableNoOpProxy as never,\n path: () => createPathProxy() as never,\n }\n }\n"],"mappings":"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","names":["key"]}
|
|
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"]}
|