@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,211 @@
|
|
|
1
|
+
import { SelectBase } from './select';
|
|
2
|
+
import { operators, type Operators, eq, and } from '../expressions/operators';
|
|
3
|
+
import { type Condition, Expression } from '../expressions/operators';
|
|
4
|
+
import type { Entity, InferSelectedModel } from '../core/table';
|
|
5
|
+
import { ItemCollectionParser } from '../core/parser';
|
|
6
|
+
import type { InternalRelationalSchema } from '../core/relations';
|
|
7
|
+
import { resolveStrategies } from '../core/strategies';
|
|
8
|
+
import { TABLE_SYMBOLS, ENTITY_SYMBOLS, mapToLogical } from "@mizzle/shared";
|
|
9
|
+
import { Column } from '../core/column';
|
|
10
|
+
import { type IMizzleClient } from '../core/client';
|
|
11
|
+
|
|
12
|
+
type WhereCallback<T extends Entity> = (
|
|
13
|
+
fields: T['_']['columns'],
|
|
14
|
+
ops: Operators
|
|
15
|
+
) => Condition;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for selecting related entities.
|
|
19
|
+
*/
|
|
20
|
+
export type IncludeOptions = boolean | {
|
|
21
|
+
where?: Condition;
|
|
22
|
+
limit?: number;
|
|
23
|
+
with?: Record<string, IncludeOptions>;
|
|
24
|
+
include?: Record<string, IncludeOptions>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for a relational query.
|
|
29
|
+
*/
|
|
30
|
+
export type RelationalQueryOptions<T extends Entity> = {
|
|
31
|
+
where?: Condition | WhereCallback<T>;
|
|
32
|
+
limit?: number;
|
|
33
|
+
orderBy?: 'asc' | 'desc';
|
|
34
|
+
/**
|
|
35
|
+
* Select relationships to include (Drizzle-style).
|
|
36
|
+
*/
|
|
37
|
+
with?: Record<string, IncludeOptions>;
|
|
38
|
+
/**
|
|
39
|
+
* Select relationships to include (Prisma-style).
|
|
40
|
+
*/
|
|
41
|
+
include?: Record<string, IncludeOptions>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type InferRelationalModel<
|
|
45
|
+
T extends Entity,
|
|
46
|
+
TOptions extends RelationalQueryOptions<T> = Record<string, never>
|
|
47
|
+
> = InferSelectedModel<T> & {
|
|
48
|
+
[K in keyof TOptions['with'] & string]: unknown;
|
|
49
|
+
} & {
|
|
50
|
+
[K in keyof TOptions['include'] & string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class RelationnalQueryBuilder<T extends Entity> {
|
|
54
|
+
constructor(
|
|
55
|
+
private client: IMizzleClient,
|
|
56
|
+
private table: T,
|
|
57
|
+
private schema?: InternalRelationalSchema,
|
|
58
|
+
private entityName?: string
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
async findFirst<TOptions extends RelationalQueryOptions<T>>(
|
|
62
|
+
options: TOptions = {} as TOptions
|
|
63
|
+
): Promise<InferRelationalModel<T, TOptions> | undefined> {
|
|
64
|
+
const results = await this.findMany({ ...options, limit: 1 });
|
|
65
|
+
return results[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async findMany<TOptions extends RelationalQueryOptions<T>>(
|
|
69
|
+
options: TOptions = {} as TOptions
|
|
70
|
+
): Promise<InferRelationalModel<T, TOptions>[]> {
|
|
71
|
+
const qb = new SelectBase<T, undefined>(this.table, this.client, undefined);
|
|
72
|
+
|
|
73
|
+
if (options.orderBy) {
|
|
74
|
+
qb.sort(options.orderBy === 'asc');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let condition: Condition | undefined;
|
|
78
|
+
|
|
79
|
+
if (options.where) {
|
|
80
|
+
const columns = (this.table._?.columns || (this.table as unknown as Record<string, Column>)) as Record<string, Column>;
|
|
81
|
+
if (typeof options.where === 'function') {
|
|
82
|
+
condition = (options.where as WhereCallback<T>)(columns as any, operators);
|
|
83
|
+
} else {
|
|
84
|
+
condition = options.where as Condition;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let results: Record<string, unknown>[];
|
|
89
|
+
|
|
90
|
+
// Single-Table Optimization (Direct PK)
|
|
91
|
+
const resolution = resolveStrategies(this.table, condition);
|
|
92
|
+
|
|
93
|
+
if (this.schema && this.entityName && (options.with || options.include) && resolution.hasPartitionKey && !resolution.indexName) {
|
|
94
|
+
const physicalTable = this.table[ENTITY_SYMBOLS.PHYSICAL_TABLE];
|
|
95
|
+
// If we are here, we should have a physical table
|
|
96
|
+
if (!physicalTable) throw new Error("Physical table not found for entity");
|
|
97
|
+
|
|
98
|
+
const pkPhysicalName = ((physicalTable as any)[TABLE_SYMBOLS.PARTITION_KEY] as Column).name;
|
|
99
|
+
|
|
100
|
+
const pkValue = resolution.keys[pkPhysicalName];
|
|
101
|
+
|
|
102
|
+
// Override where to ONLY use the PK to get related items in the same collection
|
|
103
|
+
qb.where(eq({ name: pkPhysicalName } as Column, pkValue));
|
|
104
|
+
|
|
105
|
+
const rawItems = await qb;
|
|
106
|
+
|
|
107
|
+
// Map physical attributes back to logical names for the parser
|
|
108
|
+
// We need a helper since we are not inheriting from BaseBuilder here
|
|
109
|
+
const logicalItems = rawItems.map(item => mapToLogical(this.table, item));
|
|
110
|
+
|
|
111
|
+
const parser = new ItemCollectionParser(this.schema);
|
|
112
|
+
results = parser.parse(logicalItems as any, this.entityName, (options.with || options.include) as Record<string, boolean | object>);
|
|
113
|
+
|
|
114
|
+
if (options.limit) {
|
|
115
|
+
results = results.slice(0, options.limit);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (options.limit) {
|
|
119
|
+
qb.limit(options.limit);
|
|
120
|
+
}
|
|
121
|
+
if (condition) {
|
|
122
|
+
qb.where(condition);
|
|
123
|
+
}
|
|
124
|
+
results = await qb;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Recursive relation fetching
|
|
128
|
+
if (this.schema && this.entityName && (options.with || options.include)) {
|
|
129
|
+
const relationsToFetch = (options.with || options.include) as Record<string, IncludeOptions>;
|
|
130
|
+
|
|
131
|
+
await Promise.all(results.map(async (result) => {
|
|
132
|
+
for (const [relName, relOptions] of Object.entries(relationsToFetch)) {
|
|
133
|
+
const entityConfig = this.schema!.entities[this.entityName!];
|
|
134
|
+
if (!entityConfig) continue;
|
|
135
|
+
|
|
136
|
+
const relation = entityConfig.relations[relName];
|
|
137
|
+
if (!relation) continue;
|
|
138
|
+
|
|
139
|
+
// If already populated AND no nested relations requested, skip.
|
|
140
|
+
// This preserves items from ItemCollectionParser (Single-Table optimization).
|
|
141
|
+
const isAlreadyPopulated = result[relName] !== undefined;
|
|
142
|
+
const hasNestedRelations = typeof relOptions === 'object' && (relOptions.with || relOptions.include);
|
|
143
|
+
|
|
144
|
+
if (isAlreadyPopulated && !hasNestedRelations) continue;
|
|
145
|
+
|
|
146
|
+
const targetEntity = relation.config.to;
|
|
147
|
+
const targetEntityName = Object.entries(this.schema!.entities).find(([_, m]) => m.entity === targetEntity)?.[0];
|
|
148
|
+
|
|
149
|
+
// Map result to logical names for target strategy resolution
|
|
150
|
+
const logicalValues = mapToLogical(this.table, result);
|
|
151
|
+
|
|
152
|
+
let finalLogicalValues = logicalValues;
|
|
153
|
+
if (relation.config.fields && relation.config.references) {
|
|
154
|
+
const mappedValues: Record<string, unknown> = {};
|
|
155
|
+
relation.config.fields.forEach((fieldCol, idx) => {
|
|
156
|
+
const refCol = relation.config.references![idx];
|
|
157
|
+
if (!refCol) return;
|
|
158
|
+
|
|
159
|
+
const targetLogicalEntry = Object.entries(targetEntity[ENTITY_SYMBOLS.COLUMNS] as Record<string, Column>)
|
|
160
|
+
.find(([_, c]) => c === refCol || c.name === refCol.name);
|
|
161
|
+
|
|
162
|
+
const targetLogicalName = targetLogicalEntry?.[0];
|
|
163
|
+
|
|
164
|
+
if (targetLogicalName) {
|
|
165
|
+
const sourceLogicalName = Object.entries(this.table[ENTITY_SYMBOLS.COLUMNS] as Record<string, Column>)
|
|
166
|
+
.find(([_, c]) => c === fieldCol || c.name === fieldCol.name)?.[0];
|
|
167
|
+
|
|
168
|
+
const val = sourceLogicalName ? (result[sourceLogicalName] ?? result[fieldCol.name]) : result[fieldCol.name];
|
|
169
|
+
if (val !== undefined) {
|
|
170
|
+
mappedValues[targetLogicalName] = val;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
finalLogicalValues = mappedValues;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Build keys for Query/GetItem based on parent item's data
|
|
178
|
+
const targetRes = resolveStrategies(targetEntity, undefined, finalLogicalValues);
|
|
179
|
+
if (targetRes.hasPartitionKey) {
|
|
180
|
+
const targetQb = new RelationnalQueryBuilder(this.client, targetEntity, this.schema, targetEntityName);
|
|
181
|
+
|
|
182
|
+
// Construct where clause from resolved keys using physical names directly
|
|
183
|
+
const whereParts: Expression[] = [];
|
|
184
|
+
for (const [physName, value] of Object.entries(targetRes.keys)) {
|
|
185
|
+
whereParts.push(eq({ name: physName } as Column, value));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (whereParts.length > 0) {
|
|
189
|
+
const finalWhere = whereParts.length === 1 ? whereParts[0] : and(...whereParts);
|
|
190
|
+
const nextOptions = typeof relOptions === 'object' ? relOptions : {};
|
|
191
|
+
|
|
192
|
+
if (relation.type === 'one') {
|
|
193
|
+
result[relName] = await targetQb.findFirst({ ...nextOptions, where: finalWhere });
|
|
194
|
+
} else {
|
|
195
|
+
// Overwrite for 'many' to ensure we get nested relations if requested.
|
|
196
|
+
// Optimization: only overwrite if we really need to (e.g. cross-partition).
|
|
197
|
+
result[relName] = await targetQb.findMany({ ...nextOptions, where: finalWhere });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results as InferRelationalModel<T, TOptions>[];
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GetCommand,
|
|
3
|
+
QueryCommand,
|
|
4
|
+
ScanCommand,
|
|
5
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
6
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
7
|
+
import { Column } from "../core/column";
|
|
8
|
+
import type { SelectedFields as SelectedFieldsBase } from "../core/operations";
|
|
9
|
+
import {
|
|
10
|
+
type Expression,
|
|
11
|
+
} from "../expressions/operators";
|
|
12
|
+
import { Entity, type InferSelectModel, type PhysicalTable } from "../core/table";
|
|
13
|
+
import { BaseBuilder } from "./base";
|
|
14
|
+
import type { StrategyResolution } from "../core/strategies";
|
|
15
|
+
import type { IMizzleClient } from "../core/client";
|
|
16
|
+
import { buildExpression } from "../expressions/builder";
|
|
17
|
+
|
|
18
|
+
export type SelectedFields = SelectedFieldsBase<Column, PhysicalTable>;
|
|
19
|
+
|
|
20
|
+
export class SelectBuilder<TSelection extends SelectedFields | undefined> {
|
|
21
|
+
constructor(
|
|
22
|
+
private client: IMizzleClient,
|
|
23
|
+
private fields?: TSelection,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
from<TEntity extends Entity>(entity: TEntity) {
|
|
27
|
+
return new SelectBase(entity, this.client, this.fields);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SelectBase<
|
|
32
|
+
TEntity extends Entity,
|
|
33
|
+
TSelection extends SelectedFields | undefined = undefined,
|
|
34
|
+
TResult = TSelection extends undefined ? InferSelectModel<TEntity> : Record<string, unknown>,
|
|
35
|
+
> extends BaseBuilder<TEntity, TResult[]> {
|
|
36
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "SelectBase";
|
|
37
|
+
|
|
38
|
+
private _whereClause?: Expression;
|
|
39
|
+
private _limitVal?: number;
|
|
40
|
+
private _pageSizeVal?: number;
|
|
41
|
+
private _consistentReadVal?: boolean;
|
|
42
|
+
private _sortForward: boolean = true;
|
|
43
|
+
private _forcedIndexName?: string;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
entity: TEntity,
|
|
47
|
+
client: IMizzleClient,
|
|
48
|
+
private fields?: TSelection,
|
|
49
|
+
) {
|
|
50
|
+
super(entity, client);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
where(expression: Expression): this {
|
|
54
|
+
this._whereClause = expression;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
limit(val: number): this {
|
|
59
|
+
this._limitVal = val;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pageSize(val: number): this {
|
|
64
|
+
this._pageSizeVal = val;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
consistentRead(enabled: boolean = true): this {
|
|
69
|
+
this._consistentReadVal = enabled;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sort(forward: boolean): this {
|
|
74
|
+
this._sortForward = forward;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
index(name: string): this {
|
|
79
|
+
this._forcedIndexName = name;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
iterator(): AsyncIterableIterator<TResult> {
|
|
84
|
+
const self = this;
|
|
85
|
+
return (async function* () {
|
|
86
|
+
let count = 0;
|
|
87
|
+
let lastEvaluatedKey: Record<string, any> | undefined = undefined;
|
|
88
|
+
|
|
89
|
+
do {
|
|
90
|
+
const result = await self.fetchPage(lastEvaluatedKey);
|
|
91
|
+
|
|
92
|
+
for (const item of result.items) {
|
|
93
|
+
yield item;
|
|
94
|
+
count++;
|
|
95
|
+
if (self._limitVal !== undefined && count >= self._limitVal) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
101
|
+
} while (lastEvaluatedKey);
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async fetchPage(exclusiveStartKey?: Record<string, any>): Promise<{ items: TResult[], lastEvaluatedKey?: Record<string, any> }> {
|
|
106
|
+
const resolution = this.resolveKeys(this._whereClause, undefined, this._forcedIndexName);
|
|
107
|
+
|
|
108
|
+
if (resolution.hasPartitionKey && resolution.hasSortKey && !resolution.indexName && !exclusiveStartKey) {
|
|
109
|
+
const items = await this.executeGet(resolution.keys);
|
|
110
|
+
return { items };
|
|
111
|
+
} else if (resolution.hasPartitionKey || resolution.indexName) {
|
|
112
|
+
return await this.executeQuery(resolution, exclusiveStartKey);
|
|
113
|
+
} else {
|
|
114
|
+
return await this.executeScan(exclusiveStartKey);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override async execute(): Promise<TResult[]> {
|
|
119
|
+
const { items } = await this.fetchPage();
|
|
120
|
+
return items;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async executeGet(keys: Record<string, unknown>): Promise<TResult[]> {
|
|
124
|
+
const command = new GetCommand({
|
|
125
|
+
TableName: this.tableName,
|
|
126
|
+
Key: keys,
|
|
127
|
+
ConsistentRead: this._consistentReadVal,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await this.client.send(command);
|
|
131
|
+
return result.Item ? ([this.mapToLogical(result.Item)] as TResult[]) : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async executeQuery(
|
|
135
|
+
resolution: StrategyResolution,
|
|
136
|
+
exclusiveStartKey?: Record<string, any>,
|
|
137
|
+
): Promise<{ items: TResult[], lastEvaluatedKey?: Record<string, any> }> {
|
|
138
|
+
const { expressionAttributeNames, expressionAttributeValues, addName, addValue } = this.createExpressionContext();
|
|
139
|
+
|
|
140
|
+
const keyParts: string[] = [];
|
|
141
|
+
const keyAttrNames = new Set<string>();
|
|
142
|
+
for (const [key, value] of Object.entries(resolution.keys)) {
|
|
143
|
+
keyParts.push(`${addName(key)} = ${addValue(value)}`);
|
|
144
|
+
keyAttrNames.add(key);
|
|
145
|
+
}
|
|
146
|
+
const keyConditionExpression = keyParts.join(" AND ");
|
|
147
|
+
|
|
148
|
+
// DynamoDB does NOT allow primary key attributes in FilterExpression
|
|
149
|
+
const filterExpression = this._whereClause
|
|
150
|
+
? buildExpression(this._whereClause, addName, addValue, keyAttrNames)
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
const command = new QueryCommand({
|
|
154
|
+
TableName: this.tableName,
|
|
155
|
+
IndexName: resolution.indexName,
|
|
156
|
+
KeyConditionExpression: keyConditionExpression,
|
|
157
|
+
FilterExpression: filterExpression || undefined,
|
|
158
|
+
ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : undefined,
|
|
159
|
+
ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : undefined,
|
|
160
|
+
Limit: this._pageSizeVal ?? this._limitVal,
|
|
161
|
+
ScanIndexForward: this._sortForward,
|
|
162
|
+
ConsistentRead: resolution.indexName ? undefined : this._consistentReadVal,
|
|
163
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const response = await this.client.send(command);
|
|
167
|
+
return {
|
|
168
|
+
items: (response.Items || []).map((item: any) => this.mapToLogical(item)) as TResult[],
|
|
169
|
+
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async executeScan(exclusiveStartKey?: Record<string, any>): Promise<{ items: TResult[], lastEvaluatedKey?: Record<string, any> }> {
|
|
174
|
+
const { expressionAttributeNames, expressionAttributeValues, addName, addValue } = this.createExpressionContext();
|
|
175
|
+
|
|
176
|
+
const filterExpression = this._whereClause
|
|
177
|
+
? buildExpression(this._whereClause, addName, addValue)
|
|
178
|
+
: undefined;
|
|
179
|
+
|
|
180
|
+
const command = new ScanCommand({
|
|
181
|
+
TableName: this.tableName,
|
|
182
|
+
FilterExpression: filterExpression,
|
|
183
|
+
ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : undefined,
|
|
184
|
+
ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : undefined,
|
|
185
|
+
Limit: this._pageSizeVal ?? this._limitVal,
|
|
186
|
+
ConsistentRead: this._consistentReadVal,
|
|
187
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const response = await this.client.send(command);
|
|
191
|
+
return {
|
|
192
|
+
items: (response.Items || []).map((item: any) => this.mapToLogical(item)) as TResult[],
|
|
193
|
+
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { Entity } from "../core/table";
|
|
3
|
+
import { InsertBuilder, InsertBase } from "./insert";
|
|
4
|
+
import { UpdateBuilder } from "./update";
|
|
5
|
+
import { DeleteBuilder } from "./delete";
|
|
6
|
+
import { Expression } from "../expressions/operators";
|
|
7
|
+
import { BaseBuilder } from "./base";
|
|
8
|
+
import type { IMizzleClient } from "../core/client";
|
|
9
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
10
|
+
import { buildExpression } from "../expressions/builder";
|
|
11
|
+
import { buildUpdateExpressionString } from "../expressions/update-builder";
|
|
12
|
+
import { TransactionFailedError } from "../core/errors";
|
|
13
|
+
|
|
14
|
+
export class ConditionCheckBuilder<TEntity extends Entity> extends BaseBuilder<TEntity, void> {
|
|
15
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "ConditionCheckBuilder";
|
|
16
|
+
|
|
17
|
+
private _whereClause?: Expression;
|
|
18
|
+
|
|
19
|
+
constructor(entity: TEntity, client: IMizzleClient) {
|
|
20
|
+
super(entity, client);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
where(expression: Expression): this {
|
|
24
|
+
this._whereClause = expression;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public override async execute(): Promise<void> {
|
|
29
|
+
throw new Error("ConditionCheckBuilder cannot be executed directly. Use it within a transaction.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @internal */
|
|
33
|
+
get whereClause() {
|
|
34
|
+
return this._whereClause;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
public override createExpressionContext(prefix = "") {
|
|
39
|
+
return super.createExpressionContext(prefix);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @internal */
|
|
43
|
+
public override resolveKeys(
|
|
44
|
+
whereClause?: Expression,
|
|
45
|
+
providedValues?: Record<string, unknown>,
|
|
46
|
+
) {
|
|
47
|
+
return super.resolveKeys(whereClause, providedValues);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class TransactionProxy {
|
|
52
|
+
constructor(private client: IMizzleClient) {}
|
|
53
|
+
|
|
54
|
+
insert<TEntity extends Entity>(entity: TEntity): InsertBuilder<TEntity> {
|
|
55
|
+
return new InsertBuilder(entity, this.client);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
update<TEntity extends Entity>(entity: TEntity): UpdateBuilder<TEntity> {
|
|
59
|
+
return new UpdateBuilder(entity, this.client);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
delete<TEntity extends Entity>(entity: TEntity, keys: Record<string, unknown>): DeleteBuilder<TEntity> {
|
|
63
|
+
return new DeleteBuilder(entity, this.client, keys);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
conditionCheck<TEntity extends Entity>(entity: TEntity): ConditionCheckBuilder<TEntity> {
|
|
67
|
+
return new ConditionCheckBuilder(entity, this.client);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class TransactionExecutor {
|
|
72
|
+
constructor(private client: IMizzleClient) {}
|
|
73
|
+
|
|
74
|
+
async execute(token: string, operations: any[]): Promise<void> {
|
|
75
|
+
const transactItems = operations.map(op => this.mapToTransactItem(op));
|
|
76
|
+
|
|
77
|
+
const command = new TransactWriteCommand({
|
|
78
|
+
TransactItems: transactItems,
|
|
79
|
+
ClientRequestToken: token,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await this.client.send(command);
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
if (error.name === "TransactionCanceledException" || error.__type?.includes("TransactionCanceledException")) {
|
|
86
|
+
const reasons = (error.CancellationReasons || []).map((reason: any, index: number) => ({
|
|
87
|
+
index,
|
|
88
|
+
code: reason.Code,
|
|
89
|
+
message: reason.Message,
|
|
90
|
+
item: reason.Item
|
|
91
|
+
})).filter((reason: any) => reason.code !== "None");
|
|
92
|
+
|
|
93
|
+
throw new TransactionFailedError("Transaction canceled by server.", reasons);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private mapToTransactItem(builder: any): any {
|
|
100
|
+
const kind = builder.constructor[ENTITY_SYMBOLS.ENTITY_KIND];
|
|
101
|
+
|
|
102
|
+
switch (kind) {
|
|
103
|
+
case "InsertBase":
|
|
104
|
+
return { Put: this.mapPut(builder) };
|
|
105
|
+
case "UpdateBuilder":
|
|
106
|
+
return { Update: this.mapUpdate(builder) };
|
|
107
|
+
case "DeleteBuilder":
|
|
108
|
+
return { Delete: this.mapDelete(builder) };
|
|
109
|
+
case "ConditionCheckBuilder":
|
|
110
|
+
return { ConditionCheck: this.mapConditionCheck(builder) };
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`Unsupported transaction operation: ${kind}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private mapPut(builder: InsertBase<any, any>): any {
|
|
117
|
+
const finalItem = builder.buildItem();
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
TableName: builder.tableName,
|
|
121
|
+
Item: finalItem,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private mapUpdate(builder: UpdateBuilder<any, any>): any {
|
|
126
|
+
const { expressionAttributeNames, expressionAttributeValues, addName, addValue } = builder.createExpressionContext("up_");
|
|
127
|
+
const updateExpression = buildUpdateExpressionString(builder.state, addName, addValue);
|
|
128
|
+
|
|
129
|
+
let conditionExpression: string | undefined;
|
|
130
|
+
if (builder.whereClause) {
|
|
131
|
+
conditionExpression = buildExpression(builder.whereClause, addName, addValue);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
TableName: builder.tableName,
|
|
136
|
+
Key: builder.resolveUpdateKeys(),
|
|
137
|
+
UpdateExpression: updateExpression,
|
|
138
|
+
ConditionExpression: conditionExpression,
|
|
139
|
+
ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : undefined,
|
|
140
|
+
ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private mapDelete(builder: DeleteBuilder<any, any>): any {
|
|
145
|
+
const resolution = builder.resolveKeys(undefined, builder.keys);
|
|
146
|
+
return {
|
|
147
|
+
TableName: builder.tableName,
|
|
148
|
+
Key: resolution.keys,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private mapConditionCheck(builder: ConditionCheckBuilder<any>): any {
|
|
153
|
+
const { expressionAttributeNames, expressionAttributeValues, addName, addValue } = builder.createExpressionContext("cc_");
|
|
154
|
+
|
|
155
|
+
const resolution = builder.resolveKeys(builder.whereClause);
|
|
156
|
+
|
|
157
|
+
let conditionExpression: string | undefined;
|
|
158
|
+
if (builder.whereClause) {
|
|
159
|
+
conditionExpression = buildExpression(builder.whereClause, addName, addValue);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
TableName: builder.tableName,
|
|
164
|
+
Key: resolution.keys,
|
|
165
|
+
ConditionExpression: conditionExpression,
|
|
166
|
+
ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : undefined,
|
|
167
|
+
ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|