@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# mizzle
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix: resolve schema discovery issues in monorepos and stabilize test suite.
|
|
8
|
+
|
|
9
|
+
## 1.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- Initial production release of Mizzle ORM and Mizzling CLI.
|
|
14
|
+
Includes:
|
|
15
|
+
- Fluent Query Builder (Select, Insert, Update, Delete)
|
|
16
|
+
- Native Date Support with ISO 8601 storage
|
|
17
|
+
- Relational Queries with Single-Table optimization
|
|
18
|
+
- Atomic Transactions support
|
|
19
|
+
- Migration CLI with schema discovery and push/pull
|
|
20
|
+
- High-performance tsup-based bundling
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aurios/mizzle",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "a drizzle-like orm for dynamoDB",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./columns": {
|
|
16
|
+
"types": "./dist/columns.d.ts",
|
|
17
|
+
"import": "./dist/columns.js"
|
|
18
|
+
},
|
|
19
|
+
"./table": {
|
|
20
|
+
"types": "./dist/table.d.ts",
|
|
21
|
+
"import": "./dist/table.js"
|
|
22
|
+
},
|
|
23
|
+
"./snapshot": {
|
|
24
|
+
"types": "./dist/snapshot.d.ts",
|
|
25
|
+
"import": "./dist/snapshot.js"
|
|
26
|
+
},
|
|
27
|
+
"./diff": {
|
|
28
|
+
"types": "./dist/diff.d.ts",
|
|
29
|
+
"import": "./dist/diff.js"
|
|
30
|
+
},
|
|
31
|
+
"./introspection": {
|
|
32
|
+
"types": "./dist/introspection.d.ts",
|
|
33
|
+
"import": "./dist/introspection.js"
|
|
34
|
+
},
|
|
35
|
+
"./db": {
|
|
36
|
+
"types": "./dist/db.d.ts",
|
|
37
|
+
"import": "./dist/db.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "Lucas",
|
|
42
|
+
"url": "https://github.com/realfakenerd"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"url": "https://github.com/realfakenerd/mizzle"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check": "tsc --noEmit",
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@aws-sdk/client-dynamodb": "3.962.0",
|
|
54
|
+
"@aws-sdk/lib-dynamodb": "3.962.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@mizzle/eslint-config": "workspace:*",
|
|
58
|
+
"@mizzle/shared": "workspace:*",
|
|
59
|
+
"@mizzle/tsconfig": "workspace:*",
|
|
60
|
+
"tsup": "^8.5.1"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type IMizzleClient } from "../core/client";
|
|
2
|
+
import { Entity } from "../core/table";
|
|
3
|
+
import { QueryPromise } from "./query-promise";
|
|
4
|
+
import { resolveTableName, mapToLogical } from "@mizzle/shared";
|
|
5
|
+
import { resolveStrategies, type StrategyResolution } from "../core/strategies";
|
|
6
|
+
import { type Expression } from "../expressions/operators";
|
|
7
|
+
import { ENTITY_SYMBOLS } from "@mizzle/shared";
|
|
8
|
+
|
|
9
|
+
export abstract class BaseBuilder<
|
|
10
|
+
TEntity extends Entity,
|
|
11
|
+
TResult,
|
|
12
|
+
> extends QueryPromise<TResult> {
|
|
13
|
+
constructor(
|
|
14
|
+
protected readonly entity: TEntity,
|
|
15
|
+
protected readonly client: IMizzleClient,
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get tableName(): string {
|
|
21
|
+
return resolveTableName(this.entity as any);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected get physicalTable() {
|
|
25
|
+
return this.entity[ENTITY_SYMBOLS.PHYSICAL_TABLE];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected resolveKeys(
|
|
29
|
+
whereClause?: Expression,
|
|
30
|
+
providedValues?: Record<string, unknown>,
|
|
31
|
+
indexName?: string,
|
|
32
|
+
): StrategyResolution {
|
|
33
|
+
return resolveStrategies(this.entity, whereClause, providedValues, indexName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected createExpressionContext(prefix = "") {
|
|
37
|
+
const expressionAttributeNames: Record<string, string> = {};
|
|
38
|
+
const expressionAttributeValues: Record<string, unknown> = {};
|
|
39
|
+
let nameCount = 0;
|
|
40
|
+
let valueCount = 0;
|
|
41
|
+
|
|
42
|
+
const addName = (name: string) => {
|
|
43
|
+
const segments = name.split(".");
|
|
44
|
+
const placeholders = segments.map(segment => {
|
|
45
|
+
// Check if we already mapped this name segment to avoid redundancy?
|
|
46
|
+
// Actually, simple counter is fine for now and safer against collisions.
|
|
47
|
+
const placeholder = `#${prefix}n${nameCount++}`;
|
|
48
|
+
expressionAttributeNames[placeholder] = segment;
|
|
49
|
+
return placeholder;
|
|
50
|
+
});
|
|
51
|
+
return placeholders.join(".");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const addValue = (value: unknown) => {
|
|
55
|
+
const placeholder = `:${prefix}v${valueCount++}`;
|
|
56
|
+
expressionAttributeValues[placeholder] = value;
|
|
57
|
+
return placeholder;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
expressionAttributeNames,
|
|
62
|
+
expressionAttributeValues,
|
|
63
|
+
addName,
|
|
64
|
+
addValue,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected mapToLogical(item: Record<string, unknown>): Record<string, unknown> {
|
|
69
|
+
return mapToLogical(this.entity, item);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BatchGetCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
3
|
+
import { Entity, type InferSelectModel } from "../core/table";
|
|
4
|
+
import { BaseBuilder } from "./base";
|
|
5
|
+
import type { IMizzleClient } from "../core/client";
|
|
6
|
+
|
|
7
|
+
export class BatchGetBuilder {
|
|
8
|
+
constructor(private client: IMizzleClient) {}
|
|
9
|
+
|
|
10
|
+
items<TEntity extends Entity>(entity: TEntity, keys: Partial<InferSelectModel<TEntity>>[]) {
|
|
11
|
+
return new BatchGetBase(entity, this.client, keys);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BatchGetResult<T> {
|
|
16
|
+
succeeded: T[];
|
|
17
|
+
failed: Partial<T>[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BatchGetBase<
|
|
21
|
+
TEntity extends Entity,
|
|
22
|
+
TResult = InferSelectModel<TEntity>,
|
|
23
|
+
> extends BaseBuilder<TEntity, BatchGetResult<TResult>> {
|
|
24
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "BatchGetBase";
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
entity: TEntity,
|
|
28
|
+
client: IMizzleClient,
|
|
29
|
+
private keysData: Partial<InferSelectModel<TEntity>>[],
|
|
30
|
+
) {
|
|
31
|
+
super(entity, client);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public override async execute(): Promise<BatchGetResult<TResult>> {
|
|
35
|
+
const succeeded: TResult[] = [];
|
|
36
|
+
const failed: Record<string, unknown>[] = [];
|
|
37
|
+
|
|
38
|
+
// Group keys by resolved DynamoDB keys
|
|
39
|
+
let currentKeys = this.keysData.map(k => this.resolveKeys(undefined, k as Record<string, unknown>).keys);
|
|
40
|
+
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
const maxBatchAttempts = 5; // Internal limit for unprocessed retries
|
|
43
|
+
|
|
44
|
+
while (currentKeys.length > 0 && attempts < maxBatchAttempts) {
|
|
45
|
+
attempts++;
|
|
46
|
+
|
|
47
|
+
const command = new BatchGetCommand({
|
|
48
|
+
RequestItems: {
|
|
49
|
+
[this.tableName]: {
|
|
50
|
+
Keys: currentKeys
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const response = await this.client.send(command);
|
|
56
|
+
|
|
57
|
+
if (response.Responses?.[this.tableName]) {
|
|
58
|
+
succeeded.push(...(response.Responses[this.tableName] as TResult[]));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const unprocessed = response.UnprocessedKeys?.[this.tableName]?.Keys;
|
|
62
|
+
|
|
63
|
+
if (unprocessed && unprocessed.length > 0) {
|
|
64
|
+
currentKeys = unprocessed as Record<string, unknown>[];
|
|
65
|
+
} else {
|
|
66
|
+
currentKeys = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (currentKeys.length > 0) {
|
|
71
|
+
failed.push(...currentKeys);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { succeeded, failed: failed as any[] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { BatchWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
3
|
+
import { Entity, type InferInsertModel } from "../core/table";
|
|
4
|
+
import { BaseBuilder } from "./base";
|
|
5
|
+
import type { IMizzleClient } from "../core/client";
|
|
6
|
+
import { calculateItemSize } from "../core/validation";
|
|
7
|
+
import { ItemSizeExceededError } from "../core/errors";
|
|
8
|
+
|
|
9
|
+
export type BatchWriteOperation<TEntity extends Entity> =
|
|
10
|
+
| { type: "put", item: InferInsertModel<TEntity> }
|
|
11
|
+
| { type: "delete", keys: Partial<InferInsertModel<TEntity>> };
|
|
12
|
+
|
|
13
|
+
export interface BatchWriteResult<T> {
|
|
14
|
+
succeededCount: number;
|
|
15
|
+
failed: any[]; // Operations that failed after all retries
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class BatchWriteBuilder {
|
|
19
|
+
constructor(private client: IMizzleClient) {}
|
|
20
|
+
|
|
21
|
+
operations<TEntity extends Entity>(entity: TEntity, ops: BatchWriteOperation<TEntity>[]) {
|
|
22
|
+
return new BatchWriteBase(entity, this.client, ops);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BatchWriteBase<
|
|
27
|
+
TEntity extends Entity,
|
|
28
|
+
> extends BaseBuilder<TEntity, BatchWriteResult<InferInsertModel<TEntity>>> {
|
|
29
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "BatchWriteBase";
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
entity: TEntity,
|
|
33
|
+
client: IMizzleClient,
|
|
34
|
+
private ops: BatchWriteOperation<TEntity>[],
|
|
35
|
+
) {
|
|
36
|
+
super(entity, client);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public override async execute(): Promise<BatchWriteResult<InferInsertModel<TEntity>>> {
|
|
40
|
+
let succeededCount = 0;
|
|
41
|
+
let failed: any[] = [];
|
|
42
|
+
|
|
43
|
+
const requests = this.ops.map(op => {
|
|
44
|
+
if (op.type === "put") {
|
|
45
|
+
const item = op.item as Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
// Size validation
|
|
48
|
+
const size = calculateItemSize(item);
|
|
49
|
+
if (size > 400 * 1024) {
|
|
50
|
+
throw new ItemSizeExceededError(`Item in batch exceeds the 400KB limit.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
PutRequest: {
|
|
55
|
+
Item: item
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
return {
|
|
60
|
+
DeleteRequest: {
|
|
61
|
+
Key: op.keys as Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let currentRequests = [...requests];
|
|
68
|
+
let attempts = 0;
|
|
69
|
+
const maxBatchAttempts = 5;
|
|
70
|
+
|
|
71
|
+
while (currentRequests.length > 0 && attempts < maxBatchAttempts) {
|
|
72
|
+
attempts++;
|
|
73
|
+
|
|
74
|
+
const command = new BatchWriteCommand({
|
|
75
|
+
RequestItems: {
|
|
76
|
+
[this.tableName]: currentRequests
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const response = await this.client.send(command);
|
|
81
|
+
|
|
82
|
+
const unprocessed = response.UnprocessedItems?.[this.tableName] || [];
|
|
83
|
+
succeededCount += (currentRequests.length - unprocessed.length);
|
|
84
|
+
|
|
85
|
+
if (unprocessed.length > 0) {
|
|
86
|
+
currentRequests = unprocessed;
|
|
87
|
+
} else {
|
|
88
|
+
currentRequests = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (currentRequests.length > 0) {
|
|
93
|
+
failed = currentRequests;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { succeededCount, failed };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DeleteCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { ENTITY_SYMBOLS } from "@mizzle/shared";
|
|
3
|
+
import { Entity, type InferSelectModel } from "../core/table";
|
|
4
|
+
import { BaseBuilder } from "./base";
|
|
5
|
+
import { type IMizzleClient } from "../core/client";
|
|
6
|
+
import { type Expression } from "../expressions/operators";
|
|
7
|
+
|
|
8
|
+
export class DeleteBuilder<
|
|
9
|
+
TEntity extends Entity,
|
|
10
|
+
TResult = InferSelectModel<TEntity>,
|
|
11
|
+
> extends BaseBuilder<TEntity, TResult> {
|
|
12
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "DeleteBuilder";
|
|
13
|
+
|
|
14
|
+
private _returnValues?: "NONE" | "ALL_OLD";
|
|
15
|
+
private _keys: Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
entity: TEntity,
|
|
19
|
+
client: IMizzleClient,
|
|
20
|
+
keys: Record<string, unknown>,
|
|
21
|
+
) {
|
|
22
|
+
super(entity, client);
|
|
23
|
+
this._keys = keys;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
returning(): this {
|
|
27
|
+
this._returnValues = "ALL_OLD";
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
get keys() {
|
|
33
|
+
return this._keys;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @internal */
|
|
37
|
+
public override resolveKeys(
|
|
38
|
+
whereClause?: Expression,
|
|
39
|
+
providedValues?: Record<string, unknown>,
|
|
40
|
+
) {
|
|
41
|
+
return super.resolveKeys(whereClause, providedValues);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @internal */
|
|
45
|
+
public override createExpressionContext(prefix = "") {
|
|
46
|
+
return super.createExpressionContext(prefix);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public override async execute(): Promise<TResult> {
|
|
50
|
+
const resolution = this.resolveKeys(undefined, this._keys);
|
|
51
|
+
|
|
52
|
+
const command = new DeleteCommand({
|
|
53
|
+
TableName: this.tableName,
|
|
54
|
+
Key: resolution.keys,
|
|
55
|
+
ReturnValues: this._returnValues,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const response = await this.client.send(command);
|
|
59
|
+
return response.Attributes as TResult;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { PutCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@mizzle/shared";
|
|
3
|
+
import { Entity, type InferInsertModel } from "../core/table";
|
|
4
|
+
import { BaseBuilder } from "./base";
|
|
5
|
+
import { Column } from "../core/column";
|
|
6
|
+
import type { KeyStrategy } from "../core/strategies";
|
|
7
|
+
import { type IMizzleClient } from "../core/client";
|
|
8
|
+
import { calculateItemSize } from "../core/validation";
|
|
9
|
+
import { ItemSizeExceededError } from "../core/errors";
|
|
10
|
+
|
|
11
|
+
export class InsertBuilder<TEntity extends Entity> {
|
|
12
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "InsertBuilder";
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private entity: TEntity,
|
|
16
|
+
private client: IMizzleClient,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
values(values: InferInsertModel<TEntity>): InsertBase<TEntity> {
|
|
20
|
+
return new InsertBase(this.entity, this.client, values);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class InsertBase<
|
|
25
|
+
TEntity extends Entity,
|
|
26
|
+
TResult = undefined,
|
|
27
|
+
> extends BaseBuilder<TEntity, TResult> {
|
|
28
|
+
static readonly [ENTITY_SYMBOLS.ENTITY_KIND]: string = "InsertBase";
|
|
29
|
+
|
|
30
|
+
private shouldReturnValues = false;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
entity: TEntity,
|
|
34
|
+
client: IMizzleClient,
|
|
35
|
+
private valuesData: InferInsertModel<TEntity>,
|
|
36
|
+
) {
|
|
37
|
+
super(entity, client);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @internal */
|
|
41
|
+
get values() {
|
|
42
|
+
return this.valuesData;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
returning(): InsertBase<TEntity, InferInsertModel<TEntity>> {
|
|
46
|
+
this.shouldReturnValues = true;
|
|
47
|
+
return this as unknown as InsertBase<TEntity, InferInsertModel<TEntity>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @internal */
|
|
51
|
+
buildItem(): Record<string, unknown> {
|
|
52
|
+
const itemToSave = this.processValues(this.valuesData);
|
|
53
|
+
const resolution = this.resolveKeys(undefined, itemToSave);
|
|
54
|
+
|
|
55
|
+
const finalItem: Record<string, unknown> = { ...itemToSave, ...resolution.keys };
|
|
56
|
+
|
|
57
|
+
// Also resolve GSI keys if they are defined in strategies but not in resolution.keys
|
|
58
|
+
const strategies = this.entity[ENTITY_SYMBOLS.ENTITY_STRATEGY] as unknown as Record<string, { pk: KeyStrategy, sk?: KeyStrategy }>;
|
|
59
|
+
const physicalTable = this.entity[ENTITY_SYMBOLS.PHYSICAL_TABLE];
|
|
60
|
+
const indexes = (physicalTable as any)?.[TABLE_SYMBOLS.INDEXES] || {};
|
|
61
|
+
|
|
62
|
+
for (const [indexName, strategy] of Object.entries(strategies)) {
|
|
63
|
+
if (indexName === "pk" || indexName === "sk") continue;
|
|
64
|
+
|
|
65
|
+
const indexBuilder = indexes[indexName] as { config: { pk: string; sk?: string } } | undefined;
|
|
66
|
+
if (indexBuilder) {
|
|
67
|
+
if (strategy.pk && indexBuilder.config.pk) {
|
|
68
|
+
const pkValue = this.resolveStrategyValue(strategy.pk, itemToSave);
|
|
69
|
+
if (pkValue) finalItem[indexBuilder.config.pk] = pkValue;
|
|
70
|
+
}
|
|
71
|
+
if (strategy.sk && indexBuilder.config.sk) {
|
|
72
|
+
const skValue = this.resolveStrategyValue(strategy.sk, itemToSave);
|
|
73
|
+
if (skValue) finalItem[indexBuilder.config.sk] = skValue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return finalItem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override async execute(): Promise<TResult> {
|
|
82
|
+
const finalItem = this.buildItem();
|
|
83
|
+
|
|
84
|
+
// Size validation
|
|
85
|
+
const size = calculateItemSize(finalItem);
|
|
86
|
+
if (size > 400 * 1024) {
|
|
87
|
+
throw new ItemSizeExceededError(`Item size of ${Math.round(size / 1024)}KB exceeds the 400KB limit.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const command = new PutCommand({
|
|
91
|
+
TableName: this.tableName,
|
|
92
|
+
Item: finalItem,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await this.client.send(command);
|
|
96
|
+
if (this.shouldReturnValues) return finalItem as unknown as TResult;
|
|
97
|
+
|
|
98
|
+
return undefined as unknown as TResult;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private resolveStrategyValue(strategy: KeyStrategy, availableValues: Record<string, unknown>): string | undefined {
|
|
102
|
+
if (strategy.type === "static") {
|
|
103
|
+
return strategy.segments[0] as string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const resolvedParts: string[] = [];
|
|
107
|
+
|
|
108
|
+
for (const segment of strategy.segments) {
|
|
109
|
+
if (typeof segment === "string") {
|
|
110
|
+
resolvedParts.push(segment);
|
|
111
|
+
} else {
|
|
112
|
+
const val = availableValues[segment.name];
|
|
113
|
+
if (val === undefined || val === null) return undefined;
|
|
114
|
+
resolvedParts.push(String(val));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (strategy.type === "prefix") return resolvedParts.join("");
|
|
119
|
+
if (strategy.type === "composite") return resolvedParts.join(strategy.separator || "#");
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private processValues(
|
|
124
|
+
values: InferInsertModel<TEntity>,
|
|
125
|
+
): Record<string, unknown> {
|
|
126
|
+
const item: Record<string, unknown> = { ...(values as Record<string, unknown>) };
|
|
127
|
+
const columns = this.entity[ENTITY_SYMBOLS.COLUMNS] as Record<string, Column>;
|
|
128
|
+
|
|
129
|
+
for (const key in columns) {
|
|
130
|
+
const col = columns[key];
|
|
131
|
+
if (!col) continue;
|
|
132
|
+
|
|
133
|
+
const value = item[key];
|
|
134
|
+
|
|
135
|
+
if (value === undefined) {
|
|
136
|
+
if (col.default !== undefined) item[key] = col.default;
|
|
137
|
+
else if (col.defaultFn) item[key] = col.defaultFn();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const finalValue = item[key];
|
|
141
|
+
item[key] = typeof (col as any).mapToDynamoValue === "function"
|
|
142
|
+
? (col as any).mapToDynamoValue(finalValue)
|
|
143
|
+
: finalValue;
|
|
144
|
+
|
|
145
|
+
if (["SS", "NS", "BS"].includes(col.columnType)) {
|
|
146
|
+
if (Array.isArray(finalValue)) {
|
|
147
|
+
const setVal = new Set(finalValue);
|
|
148
|
+
item[key] = setVal;
|
|
149
|
+
if (setVal.size === 0) delete item[key];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return item;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export abstract class QueryPromise<T> implements Promise<T> {
|
|
2
|
+
[Symbol.toStringTag] = "QueryPromise";
|
|
3
|
+
|
|
4
|
+
catch<TResult = never>(
|
|
5
|
+
onrejected?:
|
|
6
|
+
| ((reason: unknown) => TResult | PromiseLike<TResult>)
|
|
7
|
+
| null
|
|
8
|
+
| undefined,
|
|
9
|
+
): Promise<T | TResult> {
|
|
10
|
+
return this.then(undefined, onrejected);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
|
14
|
+
return this.then(
|
|
15
|
+
(value) => {
|
|
16
|
+
onfinally?.();
|
|
17
|
+
return value;
|
|
18
|
+
},
|
|
19
|
+
(reason) => {
|
|
20
|
+
onfinally?.();
|
|
21
|
+
throw reason;
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// oxlint-disable
|
|
27
|
+
then<TResult1 = T, TResult2 = never>(
|
|
28
|
+
onfulfilled?:
|
|
29
|
+
| ((value: T) => TResult1 | PromiseLike<TResult1>)
|
|
30
|
+
| null
|
|
31
|
+
| undefined,
|
|
32
|
+
onrejected?:
|
|
33
|
+
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
|
34
|
+
| null
|
|
35
|
+
| undefined,
|
|
36
|
+
): Promise<TResult1 | TResult2> {
|
|
37
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
abstract execute(): Promise<T>;
|
|
41
|
+
}
|