@awsless/dynamodb-server 0.1.6 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.MD +62 -61
  2. package/dist/index.js +30 -23
  3. package/package.json +1 -1
package/README.MD CHANGED
@@ -9,6 +9,7 @@ A local DynamoDB server for testing and development. Provides both a fast in-mem
9
9
  - **DynamoDB Streams** - Support for stream callbacks on item changes
10
10
  - **TTL Support** - Time-to-live expiration for items
11
11
  - **GSI/LSI Support** - Global and Local Secondary Index support
12
+ - **Multi Key for GSI Support** - Multi-key support for Global Secondary Indexes
12
13
 
13
14
  ## Setup
14
15
 
@@ -21,13 +22,13 @@ npm i @awsless/dynamodb-server
21
22
  ## Basic Usage
22
23
 
23
24
  ```ts
24
- import { DynamoDBServer } from "@awsless/dynamodb-server"
25
+ import { DynamoDBServer } from '@awsless/dynamodb-server'
25
26
 
26
27
  // Create a server with the fast in-memory engine (default)
27
28
  const server = new DynamoDBServer()
28
29
 
29
30
  // Or use the Java engine for full compatibility
30
- const server = new DynamoDBServer({ engine: "java" })
31
+ const server = new DynamoDBServer({ engine: 'java' })
31
32
 
32
33
  // Start the server
33
34
  await server.listen(8000)
@@ -49,7 +50,7 @@ await server.stop()
49
50
  The in-memory engine is optimized for speed and is perfect for unit tests. It implements the core DynamoDB operations without requiring Java.
50
51
 
51
52
  ```ts
52
- const server = new DynamoDBServer({ engine: "memory" })
53
+ const server = new DynamoDBServer({ engine: 'memory' })
53
54
  ```
54
55
 
55
56
  ### Java Engine
@@ -57,24 +58,24 @@ const server = new DynamoDBServer({ engine: "memory" })
57
58
  The Java engine uses AWS DynamoDB Local for full compatibility with DynamoDB behavior. Requires Java to be installed.
58
59
 
59
60
  ```ts
60
- const server = new DynamoDBServer({ engine: "java" })
61
+ const server = new DynamoDBServer({ engine: 'java' })
61
62
  ```
62
63
 
63
64
  ## Configuration Options
64
65
 
65
66
  ```ts
66
67
  const server = new DynamoDBServer({
67
- // Engine type: "memory" (default) or "java"
68
- engine: "memory",
68
+ // Engine type: "memory" (default) or "java"
69
+ engine: 'memory',
69
70
 
70
- // Hostname to bind to (default: "localhost")
71
- hostname: "localhost",
71
+ // Hostname to bind to (default: "localhost")
72
+ hostname: 'localhost',
72
73
 
73
- // Port to listen on (default: auto-assigned)
74
- port: 8000,
74
+ // Port to listen on (default: auto-assigned)
75
+ port: 8000,
75
76
 
76
- // AWS region (default: "us-east-1")
77
- region: "us-east-1",
77
+ // AWS region (default: "us-east-1")
78
+ region: 'us-east-1',
78
79
  })
79
80
  ```
80
81
 
@@ -83,11 +84,11 @@ const server = new DynamoDBServer({
83
84
  You can subscribe to item changes using stream callbacks:
84
85
 
85
86
  ```ts
86
- const unsubscribe = server.onStreamRecord("my-table", record => {
87
- console.log("Stream event:", record.eventName) // INSERT, MODIFY, or REMOVE
88
- console.log("Keys:", record.dynamodb.Keys)
89
- console.log("New Image:", record.dynamodb.NewImage)
90
- console.log("Old Image:", record.dynamodb.OldImage)
87
+ const unsubscribe = server.onStreamRecord('my-table', record => {
88
+ console.log('Stream event:', record.eventName) // INSERT, MODIFY, or REMOVE
89
+ console.log('Keys:', record.dynamodb.Keys)
90
+ console.log('New Image:', record.dynamodb.NewImage)
91
+ console.log('Old Image:', record.dynamodb.OldImage)
91
92
  })
92
93
 
93
94
  // Unsubscribe when done
@@ -97,50 +98,50 @@ unsubscribe()
97
98
  ## Testing with Vitest/Jest
98
99
 
99
100
  ```ts
100
- import { DynamoDBServer } from "@awsless/dynamodb-server"
101
- import { CreateTableCommand } from "@aws-sdk/client-dynamodb"
102
- import { PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb"
103
-
104
- describe("My DynamoDB Tests", () => {
105
- const server = new DynamoDBServer()
106
-
107
- beforeAll(async () => {
108
- await server.listen()
109
-
110
- // Create your tables
111
- await server.getClient().send(
112
- new CreateTableCommand({
113
- TableName: "users",
114
- KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
115
- AttributeDefinitions: [{ AttributeName: "id", AttributeType: "N" }],
116
- BillingMode: "PAY_PER_REQUEST",
117
- })
118
- )
119
- })
120
-
121
- afterAll(async () => {
122
- await server.stop()
123
- })
124
-
125
- it("should store and retrieve items", async () => {
126
- const client = server.getDocumentClient()
127
-
128
- await client.send(
129
- new PutCommand({
130
- TableName: "users",
131
- Item: { id: 1, name: "John" },
132
- })
133
- )
134
-
135
- const result = await client.send(
136
- new GetCommand({
137
- TableName: "users",
138
- Key: { id: 1 },
139
- })
140
- )
141
-
142
- expect(result.Item).toEqual({ id: 1, name: "John" })
143
- })
101
+ import { DynamoDBServer } from '@awsless/dynamodb-server'
102
+ import { CreateTableCommand } from '@aws-sdk/client-dynamodb'
103
+ import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb'
104
+
105
+ describe('My DynamoDB Tests', () => {
106
+ const server = new DynamoDBServer()
107
+
108
+ beforeAll(async () => {
109
+ await server.listen()
110
+
111
+ // Create your tables
112
+ await server.getClient().send(
113
+ new CreateTableCommand({
114
+ TableName: 'users',
115
+ KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
116
+ AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'N' }],
117
+ BillingMode: 'PAY_PER_REQUEST',
118
+ })
119
+ )
120
+ })
121
+
122
+ afterAll(async () => {
123
+ await server.stop()
124
+ })
125
+
126
+ it('should store and retrieve items', async () => {
127
+ const client = server.getDocumentClient()
128
+
129
+ await client.send(
130
+ new PutCommand({
131
+ TableName: 'users',
132
+ Item: { id: 1, name: 'John' },
133
+ })
134
+ )
135
+
136
+ const result = await client.send(
137
+ new GetCommand({
138
+ TableName: 'users',
139
+ Key: { id: 1 },
140
+ })
141
+ )
142
+
143
+ expect(result.Item).toEqual({ id: 1, name: 'John' })
144
+ })
144
145
  })
145
146
  ```
146
147
 
package/dist/index.js CHANGED
@@ -984,6 +984,26 @@ function estimateItemSize(item) {
984
984
  }
985
985
 
986
986
  // src/expressions/key-condition.ts
987
+ function stripOuterParens(expression) {
988
+ let normalized = expression.trim();
989
+ while (normalized.startsWith("(") && normalized.endsWith(")")) {
990
+ let depth = 0;
991
+ let balanced = true;
992
+ for (let index = 0; index < normalized.length - 1; index++) {
993
+ if (normalized[index] === "(") depth++;
994
+ else if (normalized[index] === ")") depth--;
995
+ if (depth === 0) {
996
+ balanced = false;
997
+ break;
998
+ }
999
+ }
1000
+ if (!balanced) {
1001
+ break;
1002
+ }
1003
+ normalized = normalized.slice(1, -1).trim();
1004
+ }
1005
+ return normalized;
1006
+ }
987
1007
  function parseKeyCondition(expression, keySchema, context) {
988
1008
  const hashKeyNames = getHashKeys(keySchema);
989
1009
  const rangeKeyNames = getRangeKeys(keySchema);
@@ -1011,29 +1031,8 @@ function parseKeyCondition(expression, keySchema, context) {
1011
1031
  }
1012
1032
  throw new ValidationException(`Invalid value reference: ${ref}`);
1013
1033
  }
1014
- function stripOuterParens(expr) {
1015
- let s = expr.trim();
1016
- while (s.startsWith("(") && s.endsWith(")")) {
1017
- let depth = 0;
1018
- let balanced = true;
1019
- for (let i = 0; i < s.length - 1; i++) {
1020
- if (s[i] === "(") depth++;
1021
- else if (s[i] === ")") depth--;
1022
- if (depth === 0) {
1023
- balanced = false;
1024
- break;
1025
- }
1026
- }
1027
- if (balanced) {
1028
- s = s.slice(1, -1).trim();
1029
- } else {
1030
- break;
1031
- }
1032
- }
1033
- return s;
1034
- }
1035
1034
  const normalizedExpression = stripOuterParens(expression);
1036
- const parts = splitKeyConditions(normalizedExpression);
1035
+ const parts = flattenKeyConditions(normalizedExpression);
1037
1036
  const hashConditions = /* @__PURE__ */ new Map();
1038
1037
  const rangeConditions = /* @__PURE__ */ new Map();
1039
1038
  for (const part of parts) {
@@ -1149,7 +1148,15 @@ function matchesKeyCondition(item, condition) {
1149
1148
  }
1150
1149
  return true;
1151
1150
  }
1152
- function splitKeyConditions(expression) {
1151
+ function flattenKeyConditions(expression) {
1152
+ const normalized = stripOuterParens(expression);
1153
+ const parts = splitTopLevelAndConditions(normalized);
1154
+ if (parts.length === 1) {
1155
+ return [normalized];
1156
+ }
1157
+ return parts.flatMap((part) => flattenKeyConditions(part));
1158
+ }
1159
+ function splitTopLevelAndConditions(expression) {
1153
1160
  const parts = [];
1154
1161
  let depth = 0;
1155
1162
  let segmentStart = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awsless/dynamodb-server",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",