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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Rotorise
2
2
 
3
- Supercharge your DynamoDB with DynamoDB Rotorise!
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. [Rotorise](https://github.com/josher8a/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 the [Brushless](https://github.com/josher8a/Brushless) for a frictionless and performant DynamoDB experience.
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
- Open an [issue](https://github.com/josher8a/Rotorise/issues) or a PR. We are open to any kind of contribution and feedback.
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
- "use strict";
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 Rotorise_exports = {};
22
- __export(Rotorise_exports, {
23
- tableEntry: () => tableEntry
24
- });
25
- module.exports = __toCommonJS(Rotorise_exports);
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 Error(`Key ${key2.toString()} not found in schema`);
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 Error(
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 Error(
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 Error(
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 = key()(schema, separator)(key_, item);
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, separator),
119
+ toEntry: toEntry()(schema, sep),
127
120
  fromEntry: fromEntry()(schema),
128
- key: key()(schema, separator),
121
+ key: key()(schema, sep),
129
122
  infer: chainableNoOpProxy,
130
123
  path: () => createPathProxy()
131
124
  };
132
125
  };
133
- // Annotate the CommonJS export names for ESM import in node:
134
- 0 && (module.exports = {
135
- tableEntry
136
- });
126
+
127
+ exports.RotoriseError = RotoriseError;
128
+ exports.tableEntry = tableEntry;
129
+ //# sourceMappingURL=Rotorise.cjs.map
137
130
  //# sourceMappingURL=Rotorise.cjs.map
@@ -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"]}