@rempays/shared-core 1.0.2-beta.10 → 1.0.2-beta.12

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.
@@ -1,7 +1,4 @@
1
- /**
2
- * Cliente genérico de DynamoDB
3
- * Wrapper type-safe sobre AWS SDK
4
- */
1
+ import type { PaginatedResult } from './modules/shared-types.js';
5
2
  export interface DynamoDBConfig {
6
3
  region?: string;
7
4
  endpoint?: string;
@@ -25,6 +22,7 @@ export interface QueryOptions {
25
22
  expressionAttributeValues?: Record<string, any>;
26
23
  limit?: number;
27
24
  scanIndexForward?: boolean;
25
+ lastEvaluatedKey?: Record<string, any>;
28
26
  }
29
27
  export declare class DynamoDBClientWrapper {
30
28
  private client;
@@ -46,9 +44,33 @@ export declare class DynamoDBClientWrapper {
46
44
  */
47
45
  delete(tableName: string, key: Record<string, any>): Promise<void>;
48
46
  /**
49
- * Query con índice o clave primaria
47
+ * Query con índice o clave primaria (con paginación)
50
48
  */
51
- query<T extends Record<string, any>>(tableName: string, options: QueryOptions): Promise<T[]>;
49
+ query<T extends Record<string, any>>(tableName: string, options: QueryOptions): Promise<PaginatedResult<T>>;
50
+ /**
51
+ * Query simple por PK (Partition Key)
52
+ */
53
+ queryByPK<T extends Record<string, any>>(tableName: string, pkName: string, pkValue: any, options?: {
54
+ limit?: number;
55
+ scanIndexForward?: boolean;
56
+ lastEvaluatedKey?: Record<string, any>;
57
+ }): Promise<PaginatedResult<T>>;
58
+ /**
59
+ * Query por PK y SK (Sort Key) con operador
60
+ */
61
+ queryByPKAndSK<T extends Record<string, any>>(tableName: string, pkName: string, pkValue: any, skName: string, skOperator: '=' | '<' | '<=' | '>' | '>=' | 'BETWEEN' | 'begins_with', skValue: any, skValue2?: any, // Para BETWEEN
62
+ options?: {
63
+ limit?: number;
64
+ scanIndexForward?: boolean;
65
+ lastEvaluatedKey?: Record<string, any>;
66
+ indexName?: string;
67
+ }): Promise<PaginatedResult<T>>;
68
+ /**
69
+ * Query todos los items por PK (sin límite, con paginación automática)
70
+ */
71
+ queryAllByPK<T extends Record<string, any>>(tableName: string, pkName: string, pkValue: any, options?: {
72
+ scanIndexForward?: boolean;
73
+ }): Promise<T[]>;
52
74
  /**
53
75
  * Scan completo de la tabla (usar con cuidado)
54
76
  */
@@ -64,4 +86,32 @@ export declare class DynamoDBClientWrapper {
64
86
  * Batch get (obtener múltiples items)
65
87
  */
66
88
  batchGet<T extends Record<string, any>>(tableName: string, keys: Array<Record<string, any>>): Promise<T[]>;
89
+ /**
90
+ * Verifica si un item existe
91
+ */
92
+ exists(tableName: string, key: Record<string, any>): Promise<boolean>;
93
+ /**
94
+ * Incrementa un atributo numérico
95
+ */
96
+ increment<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string, incrementBy?: number): Promise<T>;
97
+ /**
98
+ * Decrementa un atributo numérico
99
+ */
100
+ decrement<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string, decrementBy?: number): Promise<T>;
101
+ /**
102
+ * Agrega items a una lista (array)
103
+ */
104
+ appendToList<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string, values: any[]): Promise<T>;
105
+ /**
106
+ * Agrega items a un Set
107
+ */
108
+ addToSet<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string, values: any[]): Promise<T>;
109
+ /**
110
+ * Elimina items de un Set
111
+ */
112
+ removeFromSet<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string, values: any[]): Promise<T>;
113
+ /**
114
+ * Elimina un atributo
115
+ */
116
+ removeAttribute<T extends Record<string, any>>(tableName: string, key: Record<string, any>, attributeName: string): Promise<T>;
67
117
  }
@@ -1,8 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * Cliente genérico de DynamoDB
4
- * Wrapper type-safe sobre AWS SDK
5
- */
6
2
  Object.defineProperty(exports, "__esModule", { value: true });
7
3
  exports.DynamoDBClientWrapper = void 0;
8
4
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
@@ -80,7 +76,7 @@ class DynamoDBClientWrapper {
80
76
  await this.client.send(command);
81
77
  }
82
78
  /**
83
- * Query con índice o clave primaria
79
+ * Query con índice o clave primaria (con paginación)
84
80
  */
85
81
  async query(tableName, options) {
86
82
  const command = new client_dynamodb_1.QueryCommand({
@@ -100,12 +96,87 @@ class DynamoDBClientWrapper {
100
96
  ...(options.scanIndexForward !== undefined && {
101
97
  ScanIndexForward: options.scanIndexForward,
102
98
  }),
99
+ ...(options.lastEvaluatedKey && {
100
+ ExclusiveStartKey: (0, util_dynamodb_1.marshall)(options.lastEvaluatedKey),
101
+ }),
103
102
  });
104
103
  const response = await this.client.send(command);
105
- if (!response.Items || response.Items.length === 0) {
106
- return [];
104
+ const items = response.Items?.map((item) => (0, util_dynamodb_1.unmarshall)(item)) || [];
105
+ const lastEvaluatedKey = response.LastEvaluatedKey
106
+ ? (0, util_dynamodb_1.unmarshall)(response.LastEvaluatedKey)
107
+ : undefined;
108
+ return {
109
+ items,
110
+ lastEvaluatedKey,
111
+ hasMore: !!lastEvaluatedKey,
112
+ count: items.length,
113
+ };
114
+ }
115
+ /**
116
+ * Query simple por PK (Partition Key)
117
+ */
118
+ async queryByPK(tableName, pkName, pkValue, options) {
119
+ return this.query(tableName, {
120
+ keyConditionExpression: `#pk = :pk`,
121
+ expressionAttributeNames: { '#pk': pkName },
122
+ expressionAttributeValues: { ':pk': pkValue },
123
+ ...options,
124
+ });
125
+ }
126
+ /**
127
+ * Query por PK y SK (Sort Key) con operador
128
+ */
129
+ async queryByPKAndSK(tableName, pkName, pkValue, skName, skOperator, skValue, skValue2, // Para BETWEEN
130
+ options) {
131
+ let keyConditionExpression;
132
+ let expressionAttributeValues;
133
+ if (skOperator === 'BETWEEN' && skValue2 !== undefined) {
134
+ keyConditionExpression = `#pk = :pk AND #sk BETWEEN :sk1 AND :sk2`;
135
+ expressionAttributeValues = {
136
+ ':pk': pkValue,
137
+ ':sk1': skValue,
138
+ ':sk2': skValue2,
139
+ };
107
140
  }
108
- return response.Items.map((item) => (0, util_dynamodb_1.unmarshall)(item));
141
+ else if (skOperator === 'begins_with') {
142
+ keyConditionExpression = `#pk = :pk AND begins_with(#sk, :sk)`;
143
+ expressionAttributeValues = {
144
+ ':pk': pkValue,
145
+ ':sk': skValue,
146
+ };
147
+ }
148
+ else {
149
+ keyConditionExpression = `#pk = :pk AND #sk ${skOperator} :sk`;
150
+ expressionAttributeValues = {
151
+ ':pk': pkValue,
152
+ ':sk': skValue,
153
+ };
154
+ }
155
+ return this.query(tableName, {
156
+ keyConditionExpression,
157
+ expressionAttributeNames: {
158
+ '#pk': pkName,
159
+ '#sk': skName,
160
+ },
161
+ expressionAttributeValues,
162
+ ...options,
163
+ });
164
+ }
165
+ /**
166
+ * Query todos los items por PK (sin límite, con paginación automática)
167
+ */
168
+ async queryAllByPK(tableName, pkName, pkValue, options) {
169
+ const allItems = [];
170
+ let lastEvaluatedKey;
171
+ do {
172
+ const result = await this.queryByPK(tableName, pkName, pkValue, {
173
+ ...options,
174
+ lastEvaluatedKey,
175
+ });
176
+ allItems.push(...result.items);
177
+ lastEvaluatedKey = result.lastEvaluatedKey;
178
+ } while (lastEvaluatedKey);
179
+ return allItems;
109
180
  }
110
181
  /**
111
182
  * Scan completo de la tabla (usar con cuidado)
@@ -166,5 +237,87 @@ class DynamoDBClientWrapper {
166
237
  }
167
238
  return response.Responses[tableName].map((item) => (0, util_dynamodb_1.unmarshall)(item));
168
239
  }
240
+ /**
241
+ * Verifica si un item existe
242
+ */
243
+ async exists(tableName, key) {
244
+ const item = await this.get(tableName, key);
245
+ return item !== null;
246
+ }
247
+ /**
248
+ * Incrementa un atributo numérico
249
+ */
250
+ async increment(tableName, key, attributeName, incrementBy = 1) {
251
+ return this.update(tableName, key, {
252
+ updateExpression: `SET #attr = if_not_exists(#attr, :zero) + :inc`,
253
+ expressionAttributeNames: {
254
+ '#attr': attributeName,
255
+ },
256
+ expressionAttributeValues: {
257
+ ':zero': 0,
258
+ ':inc': incrementBy,
259
+ },
260
+ });
261
+ }
262
+ /**
263
+ * Decrementa un atributo numérico
264
+ */
265
+ async decrement(tableName, key, attributeName, decrementBy = 1) {
266
+ return this.increment(tableName, key, attributeName, -decrementBy);
267
+ }
268
+ /**
269
+ * Agrega items a una lista (array)
270
+ */
271
+ async appendToList(tableName, key, attributeName, values) {
272
+ return this.update(tableName, key, {
273
+ updateExpression: `SET #attr = list_append(if_not_exists(#attr, :empty), :vals)`,
274
+ expressionAttributeNames: {
275
+ '#attr': attributeName,
276
+ },
277
+ expressionAttributeValues: {
278
+ ':empty': [],
279
+ ':vals': values,
280
+ },
281
+ });
282
+ }
283
+ /**
284
+ * Agrega items a un Set
285
+ */
286
+ async addToSet(tableName, key, attributeName, values) {
287
+ return this.update(tableName, key, {
288
+ updateExpression: `ADD #attr :vals`,
289
+ expressionAttributeNames: {
290
+ '#attr': attributeName,
291
+ },
292
+ expressionAttributeValues: {
293
+ ':vals': new Set(values),
294
+ },
295
+ });
296
+ }
297
+ /**
298
+ * Elimina items de un Set
299
+ */
300
+ async removeFromSet(tableName, key, attributeName, values) {
301
+ return this.update(tableName, key, {
302
+ updateExpression: `DELETE #attr :vals`,
303
+ expressionAttributeNames: {
304
+ '#attr': attributeName,
305
+ },
306
+ expressionAttributeValues: {
307
+ ':vals': new Set(values),
308
+ },
309
+ });
310
+ }
311
+ /**
312
+ * Elimina un atributo
313
+ */
314
+ async removeAttribute(tableName, key, attributeName) {
315
+ return this.update(tableName, key, {
316
+ updateExpression: `REMOVE #attr`,
317
+ expressionAttributeNames: {
318
+ '#attr': attributeName,
319
+ },
320
+ });
321
+ }
169
322
  }
170
323
  exports.DynamoDBClientWrapper = DynamoDBClientWrapper;
@@ -46,12 +46,9 @@ export declare class ChatService {
46
46
  listChatsByBusiness(businessId: string, status: string, params?: PaginationParams): Promise<PaginatedResult<Chat>>;
47
47
  /**
48
48
  * Lista todos los chats de un business (todos los status)
49
+ * Nota: Hace múltiples queries, una por cada status común
49
50
  */
50
51
  listAllChatsByBusiness(businessId: string, params?: PaginationParams): Promise<PaginatedResult<Chat>>;
51
- /**
52
- * Incrementa el contador de mensajes no leídos
53
- */
54
- incrementUnreadCount(chatId: string): Promise<void>;
55
52
  /**
56
53
  * Marca todos los mensajes como leídos
57
54
  */
@@ -242,48 +242,33 @@ class ChatService {
242
242
  }
243
243
  /**
244
244
  * Lista todos los chats de un business (todos los status)
245
+ * Nota: Hace múltiples queries, una por cada status común
245
246
  */
246
247
  async listAllChatsByBusiness(businessId, params = {}) {
247
- const { limit = 20, lastEvaluatedKey } = params;
248
- const result = await this.client.send(new client_dynamodb_1.QueryCommand({
249
- TableName: this.tableName,
250
- IndexName: 'GSI1',
251
- KeyConditionExpression: 'begins_with(GSI1PK, :prefix)',
252
- ExpressionAttributeValues: (0, util_dynamodb_1.marshall)({
253
- ':prefix': `BUSINESS#${businessId}#`,
254
- }),
255
- ScanIndexForward: false,
256
- Limit: limit,
257
- ExclusiveStartKey: lastEvaluatedKey ? (0, util_dynamodb_1.marshall)(lastEvaluatedKey) : undefined,
258
- }));
259
- const items = result.Items?.map((item) => {
260
- const data = (0, util_dynamodb_1.unmarshall)(item);
261
- const { PK, SK, GSI1PK, GSI1SK, ...chat } = data;
262
- return chat;
263
- }) || [];
248
+ const { limit = 20 } = params;
249
+ const statuses = ['active', 'closed', 'pending', 'archived'];
250
+ const allChats = [];
251
+ // Query para cada status
252
+ for (const status of statuses) {
253
+ const result = await this.listChatsByBusiness(businessId, status, {
254
+ limit: Math.ceil(limit / statuses.length),
255
+ });
256
+ allChats.push(...result.items);
257
+ // Si ya tenemos suficientes, parar
258
+ if (allChats.length >= limit) {
259
+ break;
260
+ }
261
+ }
262
+ // Ordenar por lastMessageTimestamp descendente
263
+ allChats.sort((a, b) => b.lastMessageTimestamp - a.lastMessageTimestamp);
264
+ // Limitar resultados
265
+ const items = allChats.slice(0, limit);
264
266
  return {
265
267
  items,
266
- lastEvaluatedKey: result.LastEvaluatedKey ? (0, util_dynamodb_1.unmarshall)(result.LastEvaluatedKey) : undefined,
267
- hasMore: !!result.LastEvaluatedKey,
268
+ lastEvaluatedKey: undefined,
269
+ hasMore: allChats.length > limit,
268
270
  };
269
271
  }
270
- /**
271
- * Incrementa el contador de mensajes no leídos
272
- */
273
- async incrementUnreadCount(chatId) {
274
- await this.client.send(new client_dynamodb_1.UpdateItemCommand({
275
- TableName: this.tableName,
276
- Key: (0, util_dynamodb_1.marshall)({
277
- PK: `CHAT#${chatId}`,
278
- SK: 'METADATA',
279
- }),
280
- UpdateExpression: 'SET unreadCount = if_not_exists(unreadCount, :zero) + :inc',
281
- ExpressionAttributeValues: (0, util_dynamodb_1.marshall)({
282
- ':inc': 1,
283
- ':zero': 0,
284
- }),
285
- }));
286
- }
287
272
  /**
288
273
  * Marca todos los mensajes como leídos
289
274
  */
@@ -6,4 +6,5 @@ export interface PaginatedResult<T> {
6
6
  items: T[];
7
7
  lastEvaluatedKey?: Record<string, any>;
8
8
  hasMore: boolean;
9
+ count?: number;
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rempays/shared-core",
3
- "version": "1.0.2-beta.10",
3
+ "version": "1.0.2-beta.12",
4
4
  "description": "Core utilities layer for RemPays platform with AWS services integration (Cognito, S3, Secrets Manager, Textract, Facebook API, DynamoDB)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,7 +38,8 @@
38
38
  "build": "tsc",
39
39
  "build:watch": "tsc --watch",
40
40
  "prepublishOnly": "npm run build",
41
- "dev": "tsc --watch"
41
+ "dev": "tsc --watch",
42
+ "test:chat": "tsx examples/test-chat-service.ts"
42
43
  },
43
44
  "dependencies": {
44
45
  "@aws-sdk/client-cognito-identity-provider": "^3.454.0",
@@ -50,6 +51,8 @@
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^20.11.0",
54
+ "dotenv": "^17.3.1",
55
+ "tsx": "^4.7.0",
53
56
  "typescript": "^5.3.0"
54
57
  },
55
58
  "files": [