dynoquery 0.1.0 → 0.1.2

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
@@ -1,6 +1,6 @@
1
1
  # DynoQuery
2
2
 
3
- A lightweight wrapper for Amazon DynamoDB using the AWS SDK v3.
3
+ A lightweight wrapper for Amazon DynamoDB using the AWS SDK v3, specifically designed for **Single-Table Design** patterns.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,6 +11,7 @@ npm install dynoquery
11
11
  ## Features
12
12
 
13
13
  - Basic CRUD operations (create, get, update, delete)
14
+ - Optimized for **Single-Table Design**
14
15
  - Model-based approach for easy data management
15
16
  - Query and Scan support
16
17
  - Batch operations (batchGet, batchWrite)
@@ -24,11 +25,16 @@ import { DynoQuery } from 'dynoquery';
24
25
  const db = new DynoQuery({
25
26
  region: 'us-east-1',
26
27
  tableName: 'MyTable', // Define default table for single-table structure
28
+ pkName: 'PK', // Optional: Custom attribute name for Partition Key (default: 'PK')
29
+ skName: 'SK', // Optional: Custom attribute name for Sort Key (default: 'SK')
27
30
  pkPrefix: 'TENANT#A#', // Optional: Global prefix for all partitions (useful for multitenancy)
28
31
  // optional endpoint for local development
29
32
  // endpoint: 'http://localhost:8000'
30
33
  partitions: {
31
34
  User: { pkPrefix: 'USER#' },
35
+ },
36
+ indexes: {
37
+ ByCategory: { indexName: 'GSI1', pkName: 'GSI1PK', skName: 'GSI1SK', pkPrefix: 'CAT#' }
32
38
  }
33
39
  });
34
40
 
@@ -37,8 +43,23 @@ async function example() {
37
43
  // Resulting PK: TENANT#A#USER#john@example.com
38
44
  const john = db.User('john@example.com');
39
45
 
46
+ // Use registered index
47
+ // Resulting GSI1PK: TENANT#A#CAT#1
48
+ const categories = db.ByCategory('1');
49
+ const items = await categories.get('100');
50
+ const allItems = await categories.getAll();
51
+
52
+ // Index results are automatically mapped to models based on PK prefix
53
+ 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();
58
+ }
59
+ });
60
+
40
61
  // Load all data for this partition (optional, but good for multiple reads)
41
- await john.loadAll();
62
+ const allJohnData = await john.getAll();
42
63
 
43
64
  // john.get() loads data immediately (using cache if loaded)
44
65
  const userMetadata = await john.get('METADATA');
@@ -90,11 +111,17 @@ A model-based abstraction for a specific data type.
90
111
  ### Partition
91
112
  A way to manage models and data within a specific partition.
92
113
  - `get(sk)`: Fetches data for a specific sort key (returns a Promise).
93
- - `loadAll()`: Fetches all items in the partition and caches them.
114
+ - `getAll()`: Fetches all items in the partition and caches them. Returns the items.
94
115
  - `create(sk, data)`: Creates an item in the partition and returns its `Model`.
95
116
  - `model(sk)`: Get a `Model` instance for a specific sort key.
96
117
  - `deleteAll()`: Deletes all items in the partition.
97
118
 
119
+ ### IndexQuery
120
+ A way to query Global Secondary Indexes.
121
+ - `get(skValue | options)`: Query items in the index. Supports `skValue` (string) for `begins_with` search, or an options object with `skValue`, `limit`, and `scanIndexForward`.
122
+ - `getAll()`: Fetches all items in the index for the given partition key.
123
+ - Automatically identifies models in results using `__model` and provides `getPartition()` helper.
124
+
98
125
  ## License
99
126
 
100
127
  MIT
@@ -0,0 +1,25 @@
1
+ import { DynoQuery } from "./index";
2
+ export interface IndexQueryConfig {
3
+ tableName?: string;
4
+ indexName: string;
5
+ pkName?: string;
6
+ skName?: string;
7
+ pkPrefix?: string;
8
+ pkValue: string;
9
+ }
10
+ export declare class IndexQuery {
11
+ protected db: DynoQuery;
12
+ protected tableName: string;
13
+ protected indexName: string;
14
+ protected pkName: string;
15
+ protected skName: string;
16
+ protected pkValue: string;
17
+ constructor(db: DynoQuery, config: IndexQueryConfig);
18
+ get<T = any>(skValueOrOptions?: string | {
19
+ skValue?: string;
20
+ limit?: number;
21
+ scanIndexForward?: boolean;
22
+ }): Promise<T[]>;
23
+ getAll<T = any>(): Promise<T[]>;
24
+ private mapItemToModel;
25
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.IndexQuery = void 0;
13
+ const partition_1 = require("./partition");
14
+ class IndexQuery {
15
+ constructor(db, config) {
16
+ this.db = db;
17
+ this.tableName = config.tableName || db.getTableName() || "";
18
+ this.indexName = config.indexName;
19
+ this.pkName = config.pkName || "GSI1PK";
20
+ this.skName = config.skName || "GSI1SK";
21
+ const globalPrefix = db.getPkPrefix();
22
+ const indexPrefix = config.pkPrefix || "";
23
+ let finalPrefix = indexPrefix;
24
+ if (globalPrefix && !indexPrefix.startsWith(globalPrefix)) {
25
+ finalPrefix = globalPrefix + indexPrefix;
26
+ }
27
+ this.pkValue = `${finalPrefix}${config.pkValue}`;
28
+ if (!this.tableName) {
29
+ throw new Error("TableName must be provided in IndexQueryConfig or DynoQueryConfig");
30
+ }
31
+ }
32
+ get(skValueOrOptions) {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ let options = {};
35
+ if (typeof skValueOrOptions === 'string') {
36
+ options.skValue = skValueOrOptions;
37
+ }
38
+ else if (typeof skValueOrOptions === 'object') {
39
+ options = skValueOrOptions;
40
+ }
41
+ let keyCondition = "#pk = :pk";
42
+ const expressionAttributeNames = {
43
+ "#pk": this.pkName,
44
+ };
45
+ const expressionAttributeValues = {
46
+ ":pk": this.pkValue,
47
+ };
48
+ if (options.skValue) {
49
+ keyCondition += " AND begins_with(#sk, :sk)";
50
+ expressionAttributeNames["#sk"] = this.skName;
51
+ expressionAttributeValues[":sk"] = options.skValue;
52
+ }
53
+ const response = yield this.db.query({
54
+ TableName: this.tableName,
55
+ IndexName: this.indexName,
56
+ KeyConditionExpression: keyCondition,
57
+ ExpressionAttributeNames: expressionAttributeNames,
58
+ ExpressionAttributeValues: expressionAttributeValues,
59
+ Limit: options.limit,
60
+ ScanIndexForward: options.scanIndexForward,
61
+ });
62
+ const items = (response.Items || []);
63
+ return items.map(item => this.mapItemToModel(item));
64
+ });
65
+ }
66
+ getAll() {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ return this.get();
69
+ });
70
+ }
71
+ mapItemToModel(item) {
72
+ const pkName = this.db.getPkName();
73
+ const pkValue = item[pkName];
74
+ if (!pkValue)
75
+ return item;
76
+ const registeredPartitions = this.db.getRegisteredPartitions();
77
+ const globalPrefix = this.db.getPkPrefix();
78
+ for (const [name, def] of Object.entries(registeredPartitions)) {
79
+ const fullPrefix = globalPrefix + def.pkPrefix;
80
+ if (pkValue.startsWith(fullPrefix)) {
81
+ // Find the ID by removing the prefix
82
+ const id = pkValue.substring(fullPrefix.length);
83
+ // Return a Partition instance and attach the data to its cache.
84
+ const partition = new partition_1.Partition(this.db, { pkPrefix: fullPrefix }, id);
85
+ // Pre-fill the cache if we have the SK
86
+ const skName = this.db.getSkName();
87
+ if (item[skName]) {
88
+ partition["cache"][item[skName]] = item;
89
+ }
90
+ // Add a property __model to the item if it matches.
91
+ item.__model = name;
92
+ // Also provide a way to get the partition instance from the item
93
+ item.getPartition = () => partition;
94
+ return item;
95
+ }
96
+ }
97
+ return item;
98
+ }
99
+ }
100
+ exports.IndexQuery = IndexQuery;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { PutCommandInput, GetCommandInput, UpdateCommandInput, DeleteCommandInput, QueryCommandInput, ScanCommandInput, BatchGetCommandInput, BatchWriteCommandInput } from "@aws-sdk/lib-dynamodb";
2
2
  export interface DynoQueryConfig {
3
3
  tableName?: string;
4
+ pkName?: string;
5
+ skName?: string;
4
6
  region?: string;
5
7
  endpoint?: string;
6
8
  pkPrefix?: string;
@@ -12,12 +14,21 @@ export interface DynoQueryConfig {
12
14
  partitions?: Record<string, {
13
15
  pkPrefix: string;
14
16
  }>;
17
+ indexes?: Record<string, {
18
+ indexName: string;
19
+ pkName?: string;
20
+ skName?: string;
21
+ pkPrefix?: string;
22
+ }>;
15
23
  }
16
24
  export declare class DynoQuery {
17
25
  private client;
18
26
  private docClient;
19
27
  private defaultTableName?;
20
28
  private globalPkPrefix;
29
+ private pkName;
30
+ private skName;
31
+ private registeredPartitions;
21
32
  [key: string]: any;
22
33
  constructor(config?: DynoQueryConfig);
23
34
  /**
@@ -54,6 +65,12 @@ export declare class DynoQuery {
54
65
  batchWrite(params: BatchWriteCommandInput): Promise<import("@aws-sdk/lib-dynamodb").BatchWriteCommandOutput>;
55
66
  getTableName(): string | undefined;
56
67
  getPkPrefix(): string;
68
+ getPkName(): string;
69
+ getSkName(): string;
70
+ getRegisteredPartitions(): Record<string, {
71
+ pkPrefix: string;
72
+ }>;
57
73
  }
58
74
  export * from "./model";
59
75
  export * from "./partition";
76
+ export * from "./index-query";
package/dist/index.js CHANGED
@@ -29,11 +29,15 @@ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
29
29
  const partition_1 = require("./partition");
30
30
  class DynoQuery {
31
31
  constructor(config = {}) {
32
+ this.registeredPartitions = {};
32
33
  const clientConfig = Object.assign({}, config);
33
34
  // Remove properties that are not part of DynamoDBClientConfig
34
35
  delete clientConfig.tableName;
35
36
  delete clientConfig.pkPrefix;
36
37
  delete clientConfig.partitions;
38
+ delete clientConfig.indexes;
39
+ delete clientConfig.pkName;
40
+ delete clientConfig.skName;
37
41
  this.client = new client_dynamodb_1.DynamoDBClient(clientConfig);
38
42
  this.docClient = lib_dynamodb_1.DynamoDBDocumentClient.from(this.client, {
39
43
  marshallOptions: {
@@ -42,13 +46,30 @@ class DynoQuery {
42
46
  });
43
47
  this.defaultTableName = config.tableName;
44
48
  this.globalPkPrefix = config.pkPrefix || "";
49
+ this.pkName = config.pkName || "PK";
50
+ this.skName = config.skName || "SK";
45
51
  if (config.partitions) {
52
+ this.registeredPartitions = config.partitions;
46
53
  Object.entries(config.partitions).forEach(([name, def]) => {
47
54
  this[name] = (id) => {
48
55
  return new partition_1.Partition(this, { pkPrefix: this.globalPkPrefix + def.pkPrefix }, id);
49
56
  };
50
57
  });
51
58
  }
59
+ if (config.indexes) {
60
+ const { IndexQuery } = require("./index-query");
61
+ Object.entries(config.indexes).forEach(([name, def]) => {
62
+ this[name] = (id) => {
63
+ return new IndexQuery(this, {
64
+ indexName: def.indexName,
65
+ pkName: def.pkName,
66
+ skName: def.skName,
67
+ pkPrefix: def.pkPrefix,
68
+ pkValue: id
69
+ });
70
+ };
71
+ });
72
+ }
52
73
  }
53
74
  /**
54
75
  * Create or replace an item in the table.
@@ -129,9 +150,9 @@ class DynoQuery {
129
150
  return __awaiter(this, void 0, void 0, function* () {
130
151
  if (this.defaultTableName && params.RequestItems) {
131
152
  // Note: Batch operations are a bit trickier because TableName is a key in RequestItems
132
- // This wrapper doesn't automatically add it to RequestItems yet,
133
- // but let's see if we should handle it.
134
- // For now, let's keep it as is or add it if RequestItems is empty?
153
+ // This wrapper doesn't automatically add it to RequestItems yet,
154
+ // but let's see if we should handle it.
155
+ // For now, let's keep it as is or add it if RequestItems is empty?
135
156
  // Usually BatchGetCommandInput is complex.
136
157
  }
137
158
  const command = new lib_dynamodb_1.BatchGetCommand(params);
@@ -153,7 +174,17 @@ class DynoQuery {
153
174
  getPkPrefix() {
154
175
  return this.globalPkPrefix;
155
176
  }
177
+ getPkName() {
178
+ return this.pkName;
179
+ }
180
+ getSkName() {
181
+ return this.skName;
182
+ }
183
+ getRegisteredPartitions() {
184
+ return this.registeredPartitions;
185
+ }
156
186
  }
157
187
  exports.DynoQuery = DynoQuery;
158
188
  __exportStar(require("./model"), exports);
159
189
  __exportStar(require("./partition"), exports);
190
+ __exportStar(require("./index-query"), exports);
package/dist/model.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare class Model<T = any> {
10
10
  protected tableName?: string;
11
11
  protected pkPrefix: string;
12
12
  protected skValue: string;
13
+ protected pkName: string;
14
+ protected skName: string;
13
15
  protected onUpdate?: (sk: any, data: any) => void;
14
16
  constructor(db: DynoQuery, config: ModelConfig<T>);
15
17
  protected getTableName(): string;
package/dist/model.js CHANGED
@@ -16,6 +16,8 @@ class Model {
16
16
  this.tableName = config.tableName || db.getTableName();
17
17
  this.pkPrefix = config.pkPrefix;
18
18
  this.skValue = config.skValue;
19
+ this.pkName = db.getPkName();
20
+ this.skName = db.getSkName();
19
21
  this.onUpdate = config.onUpdate;
20
22
  if (!this.tableName) {
21
23
  throw new Error("TableName must be provided in ModelConfig or DynoQueryConfig");
@@ -36,8 +38,8 @@ class Model {
36
38
  const response = yield this.db.get({
37
39
  TableName: this.getTableName(),
38
40
  Key: {
39
- PK: this.getPK(id),
40
- SK: this.skValue,
41
+ [this.pkName]: this.getPK(id),
42
+ [this.skName]: this.skValue,
41
43
  },
42
44
  });
43
45
  return response.Item || null;
@@ -48,7 +50,7 @@ class Model {
48
50
  */
49
51
  save(data_1) {
50
52
  return __awaiter(this, arguments, void 0, function* (data, id = "") {
51
- const item = Object.assign({ PK: this.getPK(id), SK: this.skValue }, data);
53
+ const item = Object.assign({ [this.pkName]: this.getPK(id), [this.skName]: this.skValue }, data);
52
54
  yield this.db.create({
53
55
  TableName: this.getTableName(),
54
56
  Item: item,
@@ -70,8 +72,8 @@ class Model {
70
72
  const response = yield this.db.get({
71
73
  TableName: this.getTableName(),
72
74
  Key: {
73
- PK: this.getPK(id),
74
- SK: this.skValue,
75
+ [this.pkName]: this.getPK(id),
76
+ [this.skName]: this.skValue,
75
77
  },
76
78
  });
77
79
  const current = response.Item || {};
@@ -87,8 +89,8 @@ class Model {
87
89
  yield this.db.delete({
88
90
  TableName: this.getTableName(),
89
91
  Key: {
90
- PK: this.getPK(id),
91
- SK: this.skValue,
92
+ [this.pkName]: this.getPK(id),
93
+ [this.skName]: this.skValue,
92
94
  },
93
95
  });
94
96
  if (this.onUpdate) {
@@ -9,13 +9,16 @@ export declare class Partition {
9
9
  protected db: DynoQuery;
10
10
  protected tableName?: string;
11
11
  protected pk: string;
12
+ protected pkName: string;
13
+ protected skName: string;
12
14
  protected cache: Record<string, any>;
13
15
  protected isLoaded: boolean;
14
16
  constructor(db: DynoQuery, config: PartitionConfig, id?: string);
15
17
  /**
16
- * Load all data for this partition key.
18
+ * Fetches all items in the partition and caches them.
19
+ * Returns the data and caches it.
17
20
  */
18
- loadAll(): Promise<this>;
21
+ getAll<T = any>(): Promise<T[]>;
19
22
  /**
20
23
  * Get a model instance for a specific SK within this partition.
21
24
  */
package/dist/partition.js CHANGED
@@ -17,6 +17,8 @@ class Partition {
17
17
  this.isLoaded = false;
18
18
  this.db = db;
19
19
  this.tableName = config.tableName || db.getTableName();
20
+ this.pkName = db.getPkName();
21
+ this.skName = db.getSkName();
20
22
  if (config.pk) {
21
23
  this.pk = config.pk;
22
24
  }
@@ -50,26 +52,29 @@ class Partition {
50
52
  }
51
53
  }
52
54
  /**
53
- * Load all data for this partition key.
55
+ * Fetches all items in the partition and caches them.
56
+ * Returns the data and caches it.
54
57
  */
55
- loadAll() {
58
+ getAll() {
56
59
  return __awaiter(this, void 0, void 0, function* () {
57
60
  const response = yield this.db.query({
58
61
  TableName: this.tableName,
59
- KeyConditionExpression: "PK = :pk",
62
+ KeyConditionExpression: "#pk = :pk",
63
+ ExpressionAttributeNames: {
64
+ "#pk": this.pkName,
65
+ },
60
66
  ExpressionAttributeValues: {
61
67
  ":pk": this.pk,
62
68
  },
63
69
  });
64
- if (response.Items) {
65
- response.Items.forEach((item) => {
66
- if (item.SK) {
67
- this.cache[item.SK] = item;
68
- }
69
- });
70
- }
70
+ const items = (response.Items || []);
71
+ items.forEach((item) => {
72
+ if (item[this.skName]) {
73
+ this.cache[item[this.skName]] = item;
74
+ }
75
+ });
71
76
  this.isLoaded = true;
72
- return this;
77
+ return items;
73
78
  });
74
79
  }
75
80
  /**
@@ -132,7 +137,10 @@ class Partition {
132
137
  return __awaiter(this, void 0, void 0, function* () {
133
138
  const response = yield this.db.query({
134
139
  TableName: this.tableName,
135
- KeyConditionExpression: "PK = :pk",
140
+ KeyConditionExpression: "#pk = :pk",
141
+ ExpressionAttributeNames: {
142
+ "#pk": this.pkName,
143
+ },
136
144
  ExpressionAttributeValues: {
137
145
  ":pk": this.pk,
138
146
  },
@@ -148,8 +156,8 @@ class Partition {
148
156
  const deleteRequests = chunk.map((item) => ({
149
157
  DeleteRequest: {
150
158
  Key: {
151
- PK: item.PK,
152
- SK: item.SK,
159
+ [this.pkName]: item[this.pkName],
160
+ [this.skName]: item[this.skName],
153
161
  },
154
162
  },
155
163
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynoquery",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/devspikejs/dynoquery.git"