@aurios/mizzle 1.0.0
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/CHANGELOG.md +20 -0
- package/package.json +62 -0
- package/src/builders/base.ts +71 -0
- package/src/builders/batch-get.ts +76 -0
- package/src/builders/batch-write.ts +98 -0
- package/src/builders/delete.ts +61 -0
- package/src/builders/insert.ts +156 -0
- package/src/builders/query-promise.ts +41 -0
- package/src/builders/relational-builder.ts +211 -0
- package/src/builders/select.ts +196 -0
- package/src/builders/transaction.ts +170 -0
- package/src/builders/update.ts +155 -0
- package/src/columns/binary-set.ts +49 -0
- package/src/columns/binary.ts +48 -0
- package/src/columns/boolean.ts +45 -0
- package/src/columns/date.ts +83 -0
- package/src/columns/index.ts +44 -0
- package/src/columns/json.ts +62 -0
- package/src/columns/list.ts +47 -0
- package/src/columns/map.ts +46 -0
- package/src/columns/number-set.ts +49 -0
- package/src/columns/number.ts +57 -0
- package/src/columns/string-set.ts +59 -0
- package/src/columns/string.ts +64 -0
- package/src/columns/uuid.ts +51 -0
- package/src/core/client.ts +18 -0
- package/src/core/column-builder.ts +272 -0
- package/src/core/column.ts +167 -0
- package/src/core/diff.ts +50 -0
- package/src/core/errors.ts +34 -0
- package/src/core/introspection.ts +73 -0
- package/src/core/operations.ts +34 -0
- package/src/core/parser.ts +109 -0
- package/src/core/relations.ts +149 -0
- package/src/core/retry.ts +70 -0
- package/src/core/snapshot.ts +192 -0
- package/src/core/strategies.ts +271 -0
- package/src/core/table.ts +260 -0
- package/src/core/validation.ts +75 -0
- package/src/db.ts +199 -0
- package/src/expressions/actions.ts +66 -0
- package/src/expressions/builder.ts +77 -0
- package/src/expressions/operators.ts +108 -0
- package/src/expressions/update-builder.ts +98 -0
- package/src/index.ts +20 -0
- package/src/indexes.ts +19 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +20 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
2
|
+
import { Column } from "./column";
|
|
3
|
+
import {
|
|
4
|
+
BinaryExpression,
|
|
5
|
+
LogicalExpression,
|
|
6
|
+
type Expression,
|
|
7
|
+
} from "../expressions/operators";
|
|
8
|
+
import { Entity } from "./table";
|
|
9
|
+
|
|
10
|
+
export type KeyStrategyType = "static" | "prefix" | "composite";
|
|
11
|
+
|
|
12
|
+
export interface KeyStrategy {
|
|
13
|
+
type: KeyStrategyType;
|
|
14
|
+
segments: (string | Column)[];
|
|
15
|
+
separator?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SK = "METADATA"
|
|
20
|
+
*/
|
|
21
|
+
export function staticKey(value: string): KeyStrategy {
|
|
22
|
+
return { type: "static", segments: [value] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* PK = "USER#" + id
|
|
27
|
+
*/
|
|
28
|
+
export function prefixKey(prefix: string, column: Column): KeyStrategy {
|
|
29
|
+
return { type: "prefix", segments: [prefix, column] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* SK = "ORDER#" + date + "#STATUS#" + status
|
|
34
|
+
*/
|
|
35
|
+
export function compositeKey(
|
|
36
|
+
separator: string,
|
|
37
|
+
...segments: (string | Column)[]
|
|
38
|
+
): KeyStrategy {
|
|
39
|
+
return { type: "composite", separator, segments };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extracValuesFromExpression(
|
|
43
|
+
expression: Expression,
|
|
44
|
+
acc: Record<string, unknown>
|
|
45
|
+
): void {
|
|
46
|
+
if (expression instanceof BinaryExpression) {
|
|
47
|
+
if (expression.operator === "=") {
|
|
48
|
+
acc[expression.column.name] = expression.value;
|
|
49
|
+
}
|
|
50
|
+
} else if (expression instanceof LogicalExpression) {
|
|
51
|
+
if (expression.operator === "AND") {
|
|
52
|
+
for (const condition of expression.conditions) {
|
|
53
|
+
extracValuesFromExpression(condition, acc);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveKeyStrategy(
|
|
60
|
+
strategy: KeyStrategy,
|
|
61
|
+
availableValues: Record<string, unknown>,
|
|
62
|
+
): string | undefined {
|
|
63
|
+
if (strategy.type === "static") {
|
|
64
|
+
return strategy.segments[0] as string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (strategy.segments.length === 0) return undefined;
|
|
68
|
+
|
|
69
|
+
if (strategy.type === "prefix" && strategy.segments.length === 2 && typeof strategy.segments[0] === "string" && strategy.segments[1] instanceof Column) {
|
|
70
|
+
const prefix = strategy.segments[0];
|
|
71
|
+
const col = strategy.segments[1];
|
|
72
|
+
const val = availableValues[col.name];
|
|
73
|
+
if (val === undefined || val === null) return undefined;
|
|
74
|
+
const strVal = String(val);
|
|
75
|
+
if (strVal.startsWith(prefix)) return strVal;
|
|
76
|
+
return prefix + strVal;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const resolvedParts: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const segment of strategy.segments) {
|
|
82
|
+
if (typeof segment === "string") {
|
|
83
|
+
resolvedParts.push(segment);
|
|
84
|
+
} else if (segment instanceof Column) {
|
|
85
|
+
const val = availableValues[segment.name];
|
|
86
|
+
|
|
87
|
+
if (val === undefined || val === null) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resolvedParts.push(String(val));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (strategy.type === "prefix") {
|
|
96
|
+
return resolvedParts.join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (strategy.type === "composite") {
|
|
100
|
+
return resolvedParts.join(strategy.separator || "#");
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface StrategyResolution {
|
|
106
|
+
keys: Record<string, unknown>;
|
|
107
|
+
hasPartitionKey: boolean;
|
|
108
|
+
hasSortKey: boolean;
|
|
109
|
+
indexName?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveStrategies(
|
|
113
|
+
entity: Entity,
|
|
114
|
+
whereClause?: Expression,
|
|
115
|
+
providedValues?: Record<string, unknown>,
|
|
116
|
+
forcedIndexName?: string
|
|
117
|
+
): StrategyResolution {
|
|
118
|
+
const strategies = entity[ENTITY_SYMBOLS.ENTITY_STRATEGY] as unknown as Record<string, any>;
|
|
119
|
+
const physicalTable = entity[ENTITY_SYMBOLS.PHYSICAL_TABLE] as any;
|
|
120
|
+
|
|
121
|
+
const pkCol = physicalTable[TABLE_SYMBOLS.PARTITION_KEY] as Column;
|
|
122
|
+
const skCol = physicalTable[TABLE_SYMBOLS.SORT_KEY] as Column | undefined;
|
|
123
|
+
|
|
124
|
+
const availableValues: Record<string, unknown> = {};
|
|
125
|
+
const columns = entity[ENTITY_SYMBOLS.COLUMNS] as Record<string, Column>;
|
|
126
|
+
|
|
127
|
+
if (providedValues) {
|
|
128
|
+
for (const [key, val] of Object.entries(providedValues)) {
|
|
129
|
+
if (columns[key]) {
|
|
130
|
+
availableValues[key] = val;
|
|
131
|
+
} else {
|
|
132
|
+
const logicalEntry = Object.entries(columns).find(([_, c]) => c.name === key);
|
|
133
|
+
if (logicalEntry) {
|
|
134
|
+
availableValues[logicalEntry[0]] = val;
|
|
135
|
+
} else {
|
|
136
|
+
availableValues[key] = val;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (whereClause) {
|
|
143
|
+
extracValuesFromExpression(whereClause, availableValues);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result: StrategyResolution = {
|
|
147
|
+
keys: {},
|
|
148
|
+
hasPartitionKey: false,
|
|
149
|
+
hasSortKey: false,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (forcedIndexName) {
|
|
153
|
+
const indexes = physicalTable[TABLE_SYMBOLS.INDEXES];
|
|
154
|
+
const indexBuilder = indexes?.[forcedIndexName] as { config: { pk: string; sk?: string } } | undefined;
|
|
155
|
+
const indexStrategy = strategies[forcedIndexName];
|
|
156
|
+
|
|
157
|
+
if (indexBuilder && indexStrategy) {
|
|
158
|
+
// Check if availableValues already contains the PHYSICAL key
|
|
159
|
+
if (availableValues[indexBuilder.config.pk] !== undefined) {
|
|
160
|
+
result.indexName = forcedIndexName;
|
|
161
|
+
result.keys[indexBuilder.config.pk] = availableValues[indexBuilder.config.pk];
|
|
162
|
+
result.hasPartitionKey = true;
|
|
163
|
+
if (indexBuilder.config.sk && availableValues[indexBuilder.config.sk] !== undefined) {
|
|
164
|
+
result.keys[indexBuilder.config.sk] = availableValues[indexBuilder.config.sk];
|
|
165
|
+
result.hasSortKey = true;
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const indexPkValue = resolveKeyStrategy(indexStrategy.pk, availableValues);
|
|
171
|
+
if (indexPkValue) {
|
|
172
|
+
result.indexName = forcedIndexName;
|
|
173
|
+
result.keys[indexBuilder.config.pk] = indexPkValue;
|
|
174
|
+
result.hasPartitionKey = true;
|
|
175
|
+
|
|
176
|
+
const indexSkValue = indexStrategy.sk ? resolveKeyStrategy(indexStrategy.sk, availableValues) : undefined;
|
|
177
|
+
if (indexSkValue && indexBuilder.config.sk) {
|
|
178
|
+
result.keys[indexBuilder.config.sk] = indexSkValue;
|
|
179
|
+
result.hasSortKey = true;
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for PHYSICAL keys directly first
|
|
187
|
+
if (availableValues[pkCol.name] !== undefined) {
|
|
188
|
+
result.keys[pkCol.name] = availableValues[pkCol.name];
|
|
189
|
+
result.hasPartitionKey = true;
|
|
190
|
+
if (skCol && availableValues[skCol.name] !== undefined) {
|
|
191
|
+
result.keys[skCol.name] = availableValues[skCol.name];
|
|
192
|
+
result.hasSortKey = true;
|
|
193
|
+
} else if (!skCol) {
|
|
194
|
+
result.hasSortKey = true;
|
|
195
|
+
}
|
|
196
|
+
if (result.hasPartitionKey) return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (strategies.pk) {
|
|
200
|
+
const pkValue = resolveKeyStrategy(strategies.pk, availableValues);
|
|
201
|
+
if (pkValue) {
|
|
202
|
+
result.keys[pkCol.name] = pkValue;
|
|
203
|
+
result.hasPartitionKey = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (strategies.sk) {
|
|
208
|
+
const skValue = resolveKeyStrategy(strategies.sk, availableValues);
|
|
209
|
+
if (skValue) {
|
|
210
|
+
if (skCol) {
|
|
211
|
+
result.keys[skCol.name] = skValue;
|
|
212
|
+
result.hasSortKey = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
result.hasSortKey = skCol ? false : true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!result.hasPartitionKey) {
|
|
220
|
+
const indexes = physicalTable[TABLE_SYMBOLS.INDEXES];
|
|
221
|
+
if (indexes) {
|
|
222
|
+
for (const [indexName, indexBuilderBase] of Object.entries(indexes)) {
|
|
223
|
+
const indexBuilder = indexBuilderBase as { config: { pk: string; sk?: string } };
|
|
224
|
+
const indexStrategy = strategies[indexName];
|
|
225
|
+
if (!indexStrategy) continue;
|
|
226
|
+
|
|
227
|
+
if (availableValues[indexBuilder.config.pk] !== undefined) {
|
|
228
|
+
result.indexName = indexName;
|
|
229
|
+
result.keys = {};
|
|
230
|
+
result.keys[indexBuilder.config.pk] = availableValues[indexBuilder.config.pk];
|
|
231
|
+
result.hasPartitionKey = true;
|
|
232
|
+
if (indexBuilder.config.sk && availableValues[indexBuilder.config.sk] !== undefined) {
|
|
233
|
+
result.keys[indexBuilder.config.sk] = availableValues[indexBuilder.config.sk];
|
|
234
|
+
result.hasSortKey = true;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (indexStrategy.pk && indexBuilder.config.pk) {
|
|
240
|
+
const indexPkValue = resolveKeyStrategy(
|
|
241
|
+
indexStrategy.pk,
|
|
242
|
+
availableValues,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (indexPkValue) {
|
|
246
|
+
result.indexName = indexName;
|
|
247
|
+
|
|
248
|
+
result.keys = {};
|
|
249
|
+
result.keys[indexBuilder.config.pk] = indexPkValue;
|
|
250
|
+
result.hasPartitionKey = true;
|
|
251
|
+
|
|
252
|
+
if (indexStrategy.sk && indexBuilder.config.sk) {
|
|
253
|
+
const indexSkValue = resolveKeyStrategy(
|
|
254
|
+
indexStrategy.sk,
|
|
255
|
+
availableValues,
|
|
256
|
+
);
|
|
257
|
+
if (indexSkValue) {
|
|
258
|
+
result.keys[indexBuilder.config.sk] =
|
|
259
|
+
indexSkValue;
|
|
260
|
+
result.hasSortKey = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { ENTITY_SYMBOLS, INFER_MODE, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
2
|
+
import type { Simplify, Update } from "@mizzle/shared";
|
|
3
|
+
import { Column, type GetColumnData } from "./column";
|
|
4
|
+
import type {
|
|
5
|
+
BuildColumns,
|
|
6
|
+
ColumnBuider,
|
|
7
|
+
ColumnBuilderBase,
|
|
8
|
+
} from "./column-builder";
|
|
9
|
+
import { getColumnBuilders, type ColumnsBuilder } from "../columns";
|
|
10
|
+
import type { IndexBuilder } from "../indexes";
|
|
11
|
+
import type { OpitionalKeyOnly, RequiredKeyOnly } from "./operations";
|
|
12
|
+
import type { KeyStrategy } from "./strategies";
|
|
13
|
+
|
|
14
|
+
type IndexStrategyConfig<TIndex extends IndexBuilder> =
|
|
15
|
+
TIndex["type"] extends "lsi"
|
|
16
|
+
? { sk: KeyStrategy | Column }
|
|
17
|
+
: {
|
|
18
|
+
pk: KeyStrategy | Column;
|
|
19
|
+
sk?: KeyStrategy | Column;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type IndexesStrategy<
|
|
23
|
+
TIndexes extends Record<string, IndexBuilder> | undefined,
|
|
24
|
+
> =
|
|
25
|
+
TIndexes extends Record<string, IndexBuilder>
|
|
26
|
+
? { [K in keyof TIndexes]?: IndexStrategyConfig<TIndexes[K]> }
|
|
27
|
+
: object;
|
|
28
|
+
|
|
29
|
+
export type StrategyCallback<
|
|
30
|
+
TColumns extends Record<string, Column>,
|
|
31
|
+
TPhysicalConfig extends PhysicalTableConfig,
|
|
32
|
+
> = (columns: TColumns) => {
|
|
33
|
+
[K in keyof TPhysicalConfig as K extends "pk" | "sk"
|
|
34
|
+
? K
|
|
35
|
+
: never]: KeyStrategy | Column;
|
|
36
|
+
} & IndexesStrategy<TPhysicalConfig["indexes"]>;
|
|
37
|
+
|
|
38
|
+
export interface EntityConfig<TColumn extends Column = Column> {
|
|
39
|
+
name: string;
|
|
40
|
+
table: PhysicalTable;
|
|
41
|
+
columns: Record<string, TColumn>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PhysicalTableConfig {
|
|
45
|
+
pk: ColumnBuilderBase;
|
|
46
|
+
sk?: ColumnBuilderBase;
|
|
47
|
+
indexes?: Record<string, IndexBuilder>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class PhysicalTable<
|
|
51
|
+
T extends PhysicalTableConfig = PhysicalTableConfig,
|
|
52
|
+
> {
|
|
53
|
+
declare readonly _: {
|
|
54
|
+
config: T;
|
|
55
|
+
name: string;
|
|
56
|
+
partitionKey: Column;
|
|
57
|
+
sortKey?: Column;
|
|
58
|
+
indexes: T["indexes"];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** @internal */
|
|
62
|
+
[TABLE_SYMBOLS.TABLE_NAME]: string = "";
|
|
63
|
+
|
|
64
|
+
/** @internal */
|
|
65
|
+
[TABLE_SYMBOLS.INDEXES]: T["indexes"] = undefined;
|
|
66
|
+
|
|
67
|
+
/** @internal */
|
|
68
|
+
[TABLE_SYMBOLS.PARTITION_KEY]: Column = {} as Column;
|
|
69
|
+
|
|
70
|
+
/** @internal */
|
|
71
|
+
[TABLE_SYMBOLS.SORT_KEY]?: Column = undefined;
|
|
72
|
+
|
|
73
|
+
static readonly Symbol = TABLE_SYMBOLS;
|
|
74
|
+
|
|
75
|
+
constructor(name: string, config: T) {
|
|
76
|
+
this[TABLE_SYMBOLS.TABLE_NAME] = name;
|
|
77
|
+
this[TABLE_SYMBOLS.PARTITION_KEY] = (config.pk as ColumnBuider).build(
|
|
78
|
+
this as unknown as AnyTable,
|
|
79
|
+
);
|
|
80
|
+
this[TABLE_SYMBOLS.SORT_KEY] = config.sk
|
|
81
|
+
? (config.sk as ColumnBuider).build(this as unknown as AnyTable)
|
|
82
|
+
: undefined;
|
|
83
|
+
this[TABLE_SYMBOLS.INDEXES] = config.indexes;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class Entity<T extends EntityConfig = EntityConfig> {
|
|
88
|
+
declare readonly _: {
|
|
89
|
+
readonly config: T;
|
|
90
|
+
readonly name: T["name"];
|
|
91
|
+
readonly table: T["table"];
|
|
92
|
+
readonly columns: T["columns"];
|
|
93
|
+
readonly strategies: Record<string, KeyStrategy>;
|
|
94
|
+
readonly inferSelect: InferSelectModel<Entity<T>>;
|
|
95
|
+
readonly inferInsert: InferInsertModel<Entity<T>>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
declare readonly $inferSelect: InferSelectModel<Entity<T>>;
|
|
99
|
+
declare readonly $inferInsert: InferInsertModel<Entity<T>>;
|
|
100
|
+
|
|
101
|
+
/** @internal */
|
|
102
|
+
[ENTITY_SYMBOLS.ENTITY_NAME]: string = "";
|
|
103
|
+
|
|
104
|
+
/** @internal */
|
|
105
|
+
[ENTITY_SYMBOLS.PHYSICAL_TABLE]: T["table"] = {} as T["table"];
|
|
106
|
+
|
|
107
|
+
/** @internal */
|
|
108
|
+
[ENTITY_SYMBOLS.COLUMNS]: T["columns"] = {} as T["columns"];
|
|
109
|
+
|
|
110
|
+
[ENTITY_SYMBOLS.ENTITY_STRATEGY]: Record<string, KeyStrategy> = {};
|
|
111
|
+
|
|
112
|
+
static readonly Symbol = ENTITY_SYMBOLS;
|
|
113
|
+
|
|
114
|
+
constructor(
|
|
115
|
+
name: T["name"],
|
|
116
|
+
table: T["table"],
|
|
117
|
+
columns: T["columns"],
|
|
118
|
+
strategies: Record<string, KeyStrategy>,
|
|
119
|
+
) {
|
|
120
|
+
this[ENTITY_SYMBOLS.ENTITY_NAME] = name;
|
|
121
|
+
this[ENTITY_SYMBOLS.PHYSICAL_TABLE] = table;
|
|
122
|
+
this[ENTITY_SYMBOLS.COLUMNS] = columns;
|
|
123
|
+
this[ENTITY_SYMBOLS.ENTITY_STRATEGY] = strategies;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type MapColumnName<
|
|
128
|
+
TName extends string,
|
|
129
|
+
TColumn extends Column,
|
|
130
|
+
TDBColumnNames extends boolean,
|
|
131
|
+
> = TDBColumnNames extends true ? TColumn["_"]["name"] : TName;
|
|
132
|
+
|
|
133
|
+
export type InferModelFromColumns<
|
|
134
|
+
TColumns extends Record<string, Column>,
|
|
135
|
+
TInferMode extends "select" | "insert" = "select",
|
|
136
|
+
TConfig extends { dbColumnNames: boolean; override?: boolean } = {
|
|
137
|
+
dbColumnNames: false;
|
|
138
|
+
override: false;
|
|
139
|
+
},
|
|
140
|
+
> = Simplify<
|
|
141
|
+
{
|
|
142
|
+
[Key in keyof TColumns & string as RequiredKeyOnly<
|
|
143
|
+
MapColumnName<Key, TColumns[Key], TConfig["dbColumnNames"]>,
|
|
144
|
+
TColumns[Key],
|
|
145
|
+
TInferMode
|
|
146
|
+
>]: GetColumnData<TColumns[Key], typeof INFER_MODE.QUERY>;
|
|
147
|
+
} & {
|
|
148
|
+
[Key in keyof TColumns & string as OpitionalKeyOnly<
|
|
149
|
+
MapColumnName<Key, TColumns[Key], TConfig["dbColumnNames"]>,
|
|
150
|
+
TColumns[Key],
|
|
151
|
+
TInferMode
|
|
152
|
+
>]?: GetColumnData<TColumns[Key], typeof INFER_MODE.QUERY> | undefined;
|
|
153
|
+
}
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
export type InferSelectModel<
|
|
157
|
+
TTable extends Entity,
|
|
158
|
+
TConfig extends { dbColumnNames: boolean } = { dbColumnNames: false },
|
|
159
|
+
> = InferModelFromColumns<TTable["_"]["columns"], "select", TConfig>;
|
|
160
|
+
|
|
161
|
+
export type InferInsertModel<
|
|
162
|
+
TTable extends Entity,
|
|
163
|
+
TConfig extends { dbColumnNames: boolean; override?: boolean } = {
|
|
164
|
+
dbColumnNames: false;
|
|
165
|
+
override: false;
|
|
166
|
+
},
|
|
167
|
+
> = InferModelFromColumns<TTable["_"]["columns"], "insert", TConfig>;
|
|
168
|
+
|
|
169
|
+
export type InferSelectedModel<
|
|
170
|
+
TTable extends Entity,
|
|
171
|
+
TConfig extends { dbColumnNames: boolean } = { dbColumnNames: false },
|
|
172
|
+
> = InferSelectModel<TTable, TConfig>;
|
|
173
|
+
|
|
174
|
+
export type TableDefinition<T extends EntityConfig = EntityConfig> = Entity<T> & {
|
|
175
|
+
columns: T["columns"];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type AtomicValues<T extends Entity> = Partial<InferInsertModel<T>>;
|
|
179
|
+
|
|
180
|
+
export type EntityWithColumns<T extends EntityConfig> = Entity<T> & {
|
|
181
|
+
[Key in keyof T["columns"]]: T["columns"][Key];
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export type UpdateTableConfig<
|
|
185
|
+
T extends PhysicalTableConfig,
|
|
186
|
+
TUpdate extends Partial<PhysicalTableConfig>,
|
|
187
|
+
> = Required<Update<T, TUpdate>>;
|
|
188
|
+
|
|
189
|
+
export type AnyTable<TPartial extends Partial<PhysicalTableConfig> = object> =
|
|
190
|
+
PhysicalTable<UpdateTableConfig<PhysicalTableConfig, TPartial>>;
|
|
191
|
+
|
|
192
|
+
export function dynamoEntity<
|
|
193
|
+
TName extends string,
|
|
194
|
+
TTable extends PhysicalTable,
|
|
195
|
+
TColumnsMap extends Record<string, ColumnBuilderBase>,
|
|
196
|
+
>(
|
|
197
|
+
table: TTable,
|
|
198
|
+
name: TName,
|
|
199
|
+
columns: TColumnsMap | ((columnsTypes: ColumnsBuilder) => TColumnsMap),
|
|
200
|
+
strategies?: StrategyCallback<
|
|
201
|
+
BuildColumns<TName, TColumnsMap>,
|
|
202
|
+
TTable["_"]["config"]
|
|
203
|
+
>,
|
|
204
|
+
): EntityWithColumns<{
|
|
205
|
+
name: TName;
|
|
206
|
+
table: TTable;
|
|
207
|
+
columns: BuildColumns<TName, TColumnsMap>;
|
|
208
|
+
}> {
|
|
209
|
+
const parsedColumns: TColumnsMap =
|
|
210
|
+
typeof columns === "function" ? columns(getColumnBuilders()) : columns;
|
|
211
|
+
|
|
212
|
+
const builtColumns = Object.fromEntries(
|
|
213
|
+
Object.entries(parsedColumns).map(([name, colBuilderBase]) => {
|
|
214
|
+
const colBuilder = colBuilderBase as ColumnBuider;
|
|
215
|
+
colBuilder.setName(name);
|
|
216
|
+
const column = colBuilder.build({} as unknown as AnyTable);
|
|
217
|
+
return [name, column];
|
|
218
|
+
}),
|
|
219
|
+
) as BuildColumns<TName, TColumnsMap>;
|
|
220
|
+
|
|
221
|
+
const definedStrategies = strategies ? strategies(builtColumns) : {};
|
|
222
|
+
|
|
223
|
+
const normalizedStrategies: Record<string, any> = {};
|
|
224
|
+
for (const [key, val] of Object.entries(definedStrategies)) {
|
|
225
|
+
if (val instanceof Column) {
|
|
226
|
+
normalizedStrategies[key] = { type: "prefix", segments: ["", val] };
|
|
227
|
+
} else if (val && typeof val === "object" && !("type" in val && "segments" in val)) {
|
|
228
|
+
// It's an index strategy object { pk, sk }
|
|
229
|
+
const indexStrategy: any = { ...val };
|
|
230
|
+
if (indexStrategy.pk instanceof Column) {
|
|
231
|
+
indexStrategy.pk = { type: "prefix", segments: ["", indexStrategy.pk] };
|
|
232
|
+
}
|
|
233
|
+
if (indexStrategy.sk instanceof Column) {
|
|
234
|
+
indexStrategy.sk = { type: "prefix", segments: ["", indexStrategy.sk] };
|
|
235
|
+
}
|
|
236
|
+
normalizedStrategies[key] = indexStrategy;
|
|
237
|
+
} else {
|
|
238
|
+
normalizedStrategies[key] = val;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rawEntity = new Entity(name, table, {}, normalizedStrategies);
|
|
243
|
+
|
|
244
|
+
rawEntity[ENTITY_SYMBOLS.COLUMNS] = builtColumns;
|
|
245
|
+
|
|
246
|
+
const entity = Object.assign(rawEntity, builtColumns);
|
|
247
|
+
|
|
248
|
+
return entity as unknown as EntityWithColumns<{
|
|
249
|
+
name: TName;
|
|
250
|
+
table: TTable;
|
|
251
|
+
columns: BuildColumns<TName, TColumnsMap>;
|
|
252
|
+
}>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function dynamoTable<
|
|
256
|
+
TTableName extends string,
|
|
257
|
+
TConfig extends PhysicalTableConfig,
|
|
258
|
+
>(name: TTableName, config: TConfig) {
|
|
259
|
+
return new PhysicalTable(name, config);
|
|
260
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimating the byte size of a DynamoDB item.
|
|
3
|
+
* Reference: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/CapacityUnitCalculations.html
|
|
4
|
+
*/
|
|
5
|
+
export function calculateItemSize(item: Record<string, unknown>): number {
|
|
6
|
+
let size = 0;
|
|
7
|
+
|
|
8
|
+
for (const [key, value] of Object.entries(item)) {
|
|
9
|
+
// Attribute name size
|
|
10
|
+
size += Buffer.byteLength(key, "utf8");
|
|
11
|
+
|
|
12
|
+
size += calculateValueSize(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return size;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function calculateValueSize(value: unknown): number {
|
|
19
|
+
if (value === null || value === undefined) {
|
|
20
|
+
return 1; // NULL
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return Buffer.byteLength(value, "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value === "number") {
|
|
28
|
+
// Numbers are up to 38 significant digits.
|
|
29
|
+
// In DynamoDB, they are stored as variable length.
|
|
30
|
+
// Rule of thumb: length of string representation + extra byte.
|
|
31
|
+
// AWS Doc: "Numbers are stored in a compressed format... up to 21 bytes."
|
|
32
|
+
// We'll use a conservative estimate.
|
|
33
|
+
return 21;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof value === "boolean") {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
41
|
+
return value.byteLength;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
// List: 3 bytes overhead + sum of elements
|
|
46
|
+
let listSize = 3;
|
|
47
|
+
for (const item of value) {
|
|
48
|
+
listSize += calculateValueSize(item);
|
|
49
|
+
}
|
|
50
|
+
return listSize;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (value instanceof Set) {
|
|
54
|
+
// Set: overhead? usually treated like List or similar.
|
|
55
|
+
// AWS says: "size of all the elements plus the size of the attribute name."
|
|
56
|
+
// We'll estimate it like a List for safety.
|
|
57
|
+
let setSize = 3;
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
setSize += calculateValueSize(item);
|
|
60
|
+
}
|
|
61
|
+
return setSize;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof value === "object") {
|
|
65
|
+
// Map: 3 bytes overhead + attribute names + attribute values
|
|
66
|
+
let mapSize = 3;
|
|
67
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
68
|
+
mapSize += Buffer.byteLength(k, "utf8");
|
|
69
|
+
mapSize += calculateValueSize(v);
|
|
70
|
+
}
|
|
71
|
+
return mapSize;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 0;
|
|
75
|
+
}
|