dynoquery 0.1.11 → 0.1.14

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
@@ -11,119 +11,172 @@ npm install dynoquery
11
11
  ## Features
12
12
 
13
13
  - Basic CRUD operations (create, get, update, delete)
14
- - Optimized for **Single-Table Design**
15
- - Query and Scan support
14
+ - Optimized for **Single-Table Design** (Partitions and GSIs)
15
+ - Automatic result mapping to models
16
+ - Built-in caching for partition instances
16
17
  - TypeScript support
17
18
 
18
- ## Usage
19
+ ## Quick Start (Basic Usage)
20
+
21
+ First, initialize the client with your table name.
19
22
 
20
23
  ```typescript
21
24
  import { DynoQuery } from 'dynoquery';
22
25
 
23
26
  const db = new DynoQuery({
24
27
  region: 'us-east-1',
25
- tableName: 'MyTable', // Define default table for single-table structure
26
- pkName: 'PK', // Optional: Custom attribute name for Partition Key (default: 'PK')
27
- skName: 'SK', // Optional: Custom attribute name for Sort Key (default: 'SK')
28
- pkPrefix: 'TENANT#A#', // Optional: Global prefix for all partitions (useful for multitenancy)
29
- // optional endpoint for local development
30
- // endpoint: 'http://localhost:8000'
28
+ tableName: 'MyTable'
29
+ });
30
+ ```
31
+
32
+ ### 1. Working with Partitions (Models)
33
+
34
+ Define your models to handle different types of data within your single table.
35
+
36
+ ```typescript
37
+ const db = new DynoQuery({
38
+ region: 'us-east-1',
39
+ tableName: 'MyTable',
31
40
  models: {
32
- User: { pkPrefix: 'USER#' }, // TENANT#A#USER#
41
+ User: { pkPrefix: 'USER#' }, // Resulting PK: USER#<id>
42
+ Product: { pkPrefix: 'PROD#' }
43
+ }
44
+ });
45
+
46
+ async function userExample() {
47
+ const john = db.User('john@example.com');
48
+
49
+ // Create an item (SK: PROFILE)
50
+ await john.create('PROFILE', { name: 'John Doe', email: 'john@example.com' });
51
+
52
+ // Get an item (uses cache if already loaded)
53
+ const profile = await john.get('PROFILE');
54
+ console.log(profile.name); // 'John Doe'
55
+
56
+ // Update an item (partial update)
57
+ await john.update('PROFILE', { theme: 'dark' });
58
+
59
+ // Delete an item
60
+ await john.delete('PROFILE');
61
+
62
+ // Fetch all items in this partition
63
+ const allData = await john.getAll();
64
+ }
65
+ ```
66
+
67
+ ### 2. Global Secondary Indexes (findBy)
68
+
69
+ Use `findBy` to query your GSIs easily.
70
+
71
+ ```typescript
72
+ const db = new DynoQuery({
73
+ region: 'us-east-1',
74
+ tableName: 'MyTable',
75
+ models: {
76
+ Product: { pkPrefix: 'PROD#' }
33
77
  },
34
78
  findBy: {
35
- // TENANT#A#CAT#
36
79
  Category: { indexName: 'GSI1', pkPrefix: 'CAT#' }, // pkName defaults to GSI1PK, skName defaults to GSI1SK
37
- Date: { indexName: 'GSI2', pkPrefix: 'DATE#' }
38
80
  }
39
81
  });
40
82
 
41
- async function example() {
42
- // Use registered partition
43
- // Resulting PK: TENANT#A#USER#john@example.com
44
- const john = db.User('john@example.com');
83
+ async function indexExample() {
84
+ // Query by Category PK: CAT#ELECTRONICS
85
+ const electronics = db.findByCategory('ELECTRONICS');
45
86
 
46
- // Use registered index
47
- // Resulting GSI1PK: TENANT#A#CAT#1
48
- const categories = db.findByCategory('1');
49
- const items = await categories.get('100');
50
- const allItems = await categories.getAll();
87
+ // Get all items in this category
88
+ const items = await electronics.getAll();
51
89
 
52
- // Index results are automatically mapped to models based on PK prefix
90
+ // If results match a registered model prefix, they are automatically mapped
53
91
  items.forEach(item => {
54
- if (item.__model === 'User') {
55
- console.log('Found user:', item.name);
56
- // You can also get a Partition instance for this item
57
- const userPartition = item.getPartition();
92
+ if (item.__model === 'Product') {
93
+ const productPartition = item.getPartition(); // Returns a Partition instance
58
94
  }
59
95
  });
60
-
61
- // Load all data for this partition (optional, but good for multiple reads)
62
- const allJohnData = await john.getAll();
63
-
64
- // john.get() loads data immediately (using cache if loaded)
65
- const userMetadata = await john.get('METADATA');
66
- console.log(userMetadata);
96
+ }
97
+ ```
67
98
 
68
- // Create an item through partition
69
- await john.create('PROFILE', { name: 'John Doe', email: 'john@example.com' });
70
-
71
- // Resulting GSI1PK: TENANT#A#CAT#USER
72
- const cat = db.findByCategory('USER', '1');
73
- const date = db.findByDate('2026-10-11', '2');
74
- console.log(cat.getSkValue()); // '1'
75
- await john.create('PROFILE', {
76
- name: 'John Doe',
77
- email: 'john@example.com',
78
- }, [cat, date]);
79
-
80
- // Update the item (updates both DB and partition cache)
81
- await john.update('PROFILE', { theme: 'dark' });
99
+ ### 3. Creating Items with GSI Support
82
100
 
83
- // Delete an item
84
- await john.delete('METADATA');
101
+ You can pass index queries directly to `create()` to automatically populate GSI attributes.
85
102
 
86
- // Advanced Partition usage (Subclassing)
87
- class UserPartition extends Partition {
88
- constructor(db: DynoQuery, email: string) {
89
- super(db, { pkPrefix: 'USER#' }, email);
90
- }
103
+ ```typescript
104
+ const electronics = db.findByCategory('ELECTRONICS', 'RANK#1');
105
+
106
+ // This will automatically set GSI1PK='CAT#ELECTRONICS' and GSI1SK='RANK#1'
107
+ await db.Product('p123').create('INFO', {
108
+ name: 'Gaming Mouse',
109
+ price: 50
110
+ }, [electronics]);
111
+ ```
112
+
113
+ ## Optional Configuration Parameters
114
+
115
+ | Parameter | Type | Default | Description |
116
+ | :--- | :--- | :--- | :--- |
117
+ | `pkName` | `string` | `'PK'` | Custom attribute name for Partition Key. |
118
+ | `skName` | `string` | `'SK'` | Custom attribute name for Sort Key. |
119
+ | `pkPrefix` | `string` | `''` | Global prefix for all partitions (useful for multitenancy, e.g., `TENANT#A#`). |
120
+ | `endpoint` | `string` | - | Optional endpoint for local development (e.g., `http://localhost:8000`). |
121
+ | `credentials` | `object` | - | Custom AWS credentials (`{ accessKeyId, secretAccessKey, sessionToken? }`). |
122
+
123
+ ### Example with Optional Parameters
124
+
125
+ ```typescript
126
+ const db = new DynoQuery({
127
+ region: 'us-east-1',
128
+ tableName: 'MyTable',
129
+ pkName: 'PartitionKey', // Custom PK name
130
+ skName: 'SortKey', // Custom SK name
131
+ pkPrefix: 'TENANT#A#', // Global prefix for multitenancy
132
+ endpoint: 'http://localhost:8000', // For local DynamoDB
133
+ credentials: {
134
+ accessKeyId: 'MY_ACCESS_KEY',
135
+ secretAccessKey: 'MY_SECRET_KEY'
91
136
  }
137
+ });
138
+ ```
139
+
140
+ ## Advanced Usage
141
+
142
+ ### Pagination
143
+
144
+ Both `Partition.getAll()` and `IndexQuery.getAll()` support pagination.
145
+
146
+ ```typescript
147
+ const index = db.findByCategory('ELECTRONICS');
148
+ const items = await index.getAll({ limit: 10 });
92
149
 
93
- const user2 = new UserPartition(db, 'jane@example.com');
94
- const data2 = await user2.get('METADATA');
95
- console.log(data2);
150
+ const token = index.getLastEvaluatedKey();
151
+ if (token) {
152
+ // Fetch next page
153
+ const nextItems = await index.getAll({ limit: 10, exclusiveStartKey: token });
96
154
  }
97
155
  ```
98
156
 
99
157
  ## API Reference
100
158
 
101
159
  ### DynoQuery
102
- The main client for interacting with DynamoDB.
103
- - `create(params)`: Put an item.
104
- - `get(params)`: Get an item.
105
- - `update(params)`: Update an item.
106
- - `delete(params)`: Delete an item.
107
- - `query(params)`: Query items.
108
- - `scan(params)`: Scan items.
160
+ - `create(params)`: Low-level PutCommand wrapper.
161
+ - `get(params)`: Low-level GetCommand wrapper.
162
+ - `update(params)`: Low-level UpdateCommand wrapper.
163
+ - `delete(params)`: Low-level DeleteCommand wrapper.
164
+ - `query(params)`: Low-level QueryCommand wrapper.
165
+ - `scan(params)`: Low-level ScanCommand wrapper.
109
166
 
110
167
  ### Partition
111
- A way to manage data within a specific partition.
112
- - `getPkValue()`: Returns the generated partition key value.
113
- - `get(sk)`: Fetches data for a specific sort key (returns a Promise).
114
- - `getAll()`: Fetches all items in the partition and caches them. Returns the items.
115
- - `create(sk, data, indices?)`: Creates an item in the partition. If `indices` (array of `IndexQuery`) are provided, it automatically adds the index PK and SK to the item.
116
- - `update(sk, data)`: Updates an existing item (partial update).
168
+ - `get(sk)`: Fetches data for a specific SK (returns a Promise).
169
+ - `getAll(options?)`: Fetches items in the partition. Options: `{ limit, exclusiveStartKey }`.
170
+ - `create(sk, data, indices?)`: Creates an item. `indices` is an array of `IndexQuery` for GSI population.
171
+ - `update(sk, data)`: Partial update of an item.
117
172
  - `delete(sk)`: Deletes an item.
118
173
  - `deleteAll()`: Deletes all items in the partition.
174
+ - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
119
175
 
120
176
  ### IndexQuery
121
- A way to query Global Secondary Indexes.
122
- - `getPkValue()`: Returns the generated partition key value for this index.
123
- - `getSkValue()`: Returns the sort key value if it was provided when calling the index query method.
124
- - `get(skValue | options)`: Query items in the index. Supports `skValue` (string) for `begins_with` search, or an options object with `skValue`, `limit`, and `scanIndexForward`. If `skValue` was provided when the `IndexQuery` was created, it will be used as the default if no `skValue` is passed here.
125
- - `getAll()`: Fetches all items in the index for the given partition key. If `skValue` was provided when the `IndexQuery` was created, it will filter by it using `begins_with`.
126
- - Automatically identifies the model name in results using `__model` (based on registered models) and provides `getPartition()` helper.
177
+ - `get(skValue?)`: Get a single item from the index.
178
+ - `getAll(options?)`: Query index. Options: `{ limit, scanIndexForward, exclusiveStartKey, skValue }`.
179
+ - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
127
180
 
128
181
  ## License
129
182
 
@@ -16,16 +16,22 @@ export declare class IndexQuery {
16
16
  protected skName: string;
17
17
  protected pkValue: string;
18
18
  protected skValue?: string;
19
+ protected lastEvaluatedKey: any;
19
20
  constructor(db: DynoQuery, config: IndexQueryConfig);
20
- get<T = any>(skValueOrOptions?: string | {
21
- skValue?: string;
21
+ getAll<T = any>(options?: {
22
22
  limit?: number;
23
23
  scanIndexForward?: boolean;
24
+ exclusiveStartKey?: any;
25
+ skValue?: string;
26
+ filterExpression?: string;
27
+ expressionAttributeNames?: Record<string, string>;
28
+ expressionAttributeValues?: Record<string, any>;
24
29
  }): Promise<T[]>;
25
- getAll<T = any>(): Promise<T[]>;
30
+ get<T = any>(skValue?: string): Promise<T | null>;
26
31
  getPkValue(): string;
27
32
  getPkName(): string;
28
33
  getSkName(): string;
29
34
  getSkValue(): string | undefined;
35
+ getLastEvaluatedKey(): any;
30
36
  private mapItemToModel;
31
37
  }
@@ -13,6 +13,7 @@ exports.IndexQuery = void 0;
13
13
  const partition_1 = require("./partition");
14
14
  class IndexQuery {
15
15
  constructor(db, config) {
16
+ this.lastEvaluatedKey = null;
16
17
  this.db = db;
17
18
  this.tableName = config.tableName || db.getTableName() || "";
18
19
  this.indexName = config.indexName;
@@ -30,46 +31,38 @@ class IndexQuery {
30
31
  throw new Error("TableName must be provided in IndexQueryConfig or DynoQueryConfig");
31
32
  }
32
33
  }
33
- get(skValueOrOptions) {
34
+ getAll(options) {
34
35
  return __awaiter(this, void 0, void 0, function* () {
35
- let options = {};
36
- if (typeof skValueOrOptions === 'string') {
37
- options.skValue = skValueOrOptions;
38
- }
39
- else if (typeof skValueOrOptions === 'object') {
40
- options = skValueOrOptions;
41
- }
42
- else if (this.skValue) {
43
- options.skValue = this.skValue;
44
- }
36
+ const finalSkValue = (options === null || options === void 0 ? void 0 : options.skValue) || this.skValue;
45
37
  let keyCondition = "#pk = :pk";
46
- const expressionAttributeNames = {
47
- "#pk": this.pkName,
48
- };
49
- const expressionAttributeValues = {
50
- ":pk": this.pkValue,
51
- };
52
- if (options.skValue) {
38
+ const expressionAttributeNames = Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.expressionAttributeNames);
39
+ const expressionAttributeValues = Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.expressionAttributeValues);
40
+ if (finalSkValue) {
53
41
  keyCondition += " AND begins_with(#sk, :sk)";
54
42
  expressionAttributeNames["#sk"] = this.skName;
55
- expressionAttributeValues[":sk"] = options.skValue;
43
+ expressionAttributeValues[":sk"] = finalSkValue;
56
44
  }
57
45
  const response = yield this.db.query({
58
46
  TableName: this.tableName,
59
47
  IndexName: this.indexName,
60
48
  KeyConditionExpression: keyCondition,
49
+ FilterExpression: options === null || options === void 0 ? void 0 : options.filterExpression,
61
50
  ExpressionAttributeNames: expressionAttributeNames,
62
51
  ExpressionAttributeValues: expressionAttributeValues,
63
- Limit: options.limit,
64
- ScanIndexForward: options.scanIndexForward,
52
+ Limit: options === null || options === void 0 ? void 0 : options.limit,
53
+ ScanIndexForward: options === null || options === void 0 ? void 0 : options.scanIndexForward,
54
+ ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.exclusiveStartKey,
65
55
  });
66
56
  const items = (response.Items || []);
67
- return items.map(item => this.mapItemToModel(item));
57
+ const mappedItems = items.map(item => this.mapItemToModel(item));
58
+ this.lastEvaluatedKey = response.LastEvaluatedKey || null;
59
+ return mappedItems;
68
60
  });
69
61
  }
70
- getAll() {
62
+ get(skValue) {
71
63
  return __awaiter(this, void 0, void 0, function* () {
72
- return this.get();
64
+ const items = yield this.getAll({ limit: 1, skValue });
65
+ return items.length > 0 ? items[0] : null;
73
66
  });
74
67
  }
75
68
  getPkValue() {
@@ -84,6 +77,9 @@ class IndexQuery {
84
77
  getSkValue() {
85
78
  return this.skValue;
86
79
  }
80
+ getLastEvaluatedKey() {
81
+ return this.lastEvaluatedKey;
82
+ }
87
83
  mapItemToModel(item) {
88
84
  const pkName = this.db.getPkName();
89
85
  const pkValue = item[pkName];
@@ -13,12 +13,19 @@ export declare class Partition {
13
13
  protected skName: string;
14
14
  protected cache: Record<string, any>;
15
15
  protected isLoaded: boolean;
16
+ protected lastEvaluatedKey: any;
16
17
  constructor(db: DynoQuery, config: PartitionConfig, id?: string);
17
18
  /**
18
19
  * Fetches all items in the partition and caches them.
19
20
  * Returns the data and caches it.
20
21
  */
21
- getAll<T = any>(): Promise<T[]>;
22
+ getAll<T = any>(options?: {
23
+ limit?: number;
24
+ exclusiveStartKey?: any;
25
+ filterExpression?: string;
26
+ expressionAttributeNames?: Record<string, string>;
27
+ expressionAttributeValues?: Record<string, any>;
28
+ }): Promise<T[]>;
22
29
  /**
23
30
  * Create an item in this partition.
24
31
  */
@@ -38,6 +45,7 @@ export declare class Partition {
38
45
  */
39
46
  get<T = any>(sk: string): Promise<T | null>;
40
47
  getPkValue(): string;
48
+ getLastEvaluatedKey(): any;
41
49
  /**
42
50
  * Delete all data in this partition.
43
51
  */
package/dist/partition.js CHANGED
@@ -14,6 +14,7 @@ class Partition {
14
14
  constructor(db, config, id) {
15
15
  this.cache = {};
16
16
  this.isLoaded = false;
17
+ this.lastEvaluatedKey = null;
17
18
  this.db = db;
18
19
  this.tableName = config.tableName || db.getTableName();
19
20
  this.pkName = db.getPkName();
@@ -54,17 +55,16 @@ class Partition {
54
55
  * Fetches all items in the partition and caches them.
55
56
  * Returns the data and caches it.
56
57
  */
57
- getAll() {
58
+ getAll(options) {
58
59
  return __awaiter(this, void 0, void 0, function* () {
59
60
  const response = yield this.db.query({
60
61
  TableName: this.tableName,
61
62
  KeyConditionExpression: "#pk = :pk",
62
- ExpressionAttributeNames: {
63
- "#pk": this.pkName,
64
- },
65
- ExpressionAttributeValues: {
66
- ":pk": this.pkValue,
67
- },
63
+ FilterExpression: options === null || options === void 0 ? void 0 : options.filterExpression,
64
+ ExpressionAttributeNames: Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.expressionAttributeNames),
65
+ ExpressionAttributeValues: Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.expressionAttributeValues),
66
+ Limit: options === null || options === void 0 ? void 0 : options.limit,
67
+ ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.exclusiveStartKey,
68
68
  });
69
69
  const items = (response.Items || []);
70
70
  items.forEach((item) => {
@@ -72,7 +72,10 @@ class Partition {
72
72
  this.cache[item[this.skName]] = item;
73
73
  }
74
74
  });
75
- this.isLoaded = true;
75
+ if (!(options === null || options === void 0 ? void 0 : options.exclusiveStartKey) && !response.LastEvaluatedKey) {
76
+ this.isLoaded = true;
77
+ }
78
+ this.lastEvaluatedKey = response.LastEvaluatedKey || null;
76
79
  return items;
77
80
  });
78
81
  }
@@ -152,6 +155,9 @@ class Partition {
152
155
  getPkValue() {
153
156
  return this.pkValue;
154
157
  }
158
+ getLastEvaluatedKey() {
159
+ return this.lastEvaluatedKey;
160
+ }
155
161
  /**
156
162
  * Delete all data in this partition.
157
163
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynoquery",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/devspikejs/dynoquery.git"