mcard-js 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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/dist/__mocks__/better-sqlite3.js +20 -0
  4. package/dist/__mocks__/better-sqlite3.js.map +1 -0
  5. package/dist/config/config_constants.js +188 -0
  6. package/dist/config/config_constants.js.map +1 -0
  7. package/dist/config/env_parameters.js +62 -0
  8. package/dist/config/env_parameters.js.map +1 -0
  9. package/dist/content/model/content_type_detector.js +89 -0
  10. package/dist/content/model/content_type_detector.js.map +1 -0
  11. package/dist/core/card-collection.js +279 -0
  12. package/dist/core/card-collection.js.map +1 -0
  13. package/dist/core/event-producer.js +132 -0
  14. package/dist/core/event-producer.js.map +1 -0
  15. package/dist/core/g_time.js +201 -0
  16. package/dist/core/g_time.js.map +1 -0
  17. package/dist/core/hash/enums.js +19 -0
  18. package/dist/core/hash/enums.js.map +1 -0
  19. package/dist/core/hash/validator.js +260 -0
  20. package/dist/core/hash/validator.js.map +1 -0
  21. package/dist/core/mcard.js +205 -0
  22. package/dist/core/mcard.js.map +1 -0
  23. package/dist/engine/sqlite_engine.js +723 -0
  24. package/dist/engine/sqlite_engine.js.map +1 -0
  25. package/dist/index.js +10 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/middleware/mcardPersistenceMiddleware.js +45 -0
  28. package/dist/middleware/mcardPersistenceMiddleware.js.map +1 -0
  29. package/dist/models/database_schemas.js +31 -0
  30. package/dist/models/database_schemas.js.map +1 -0
  31. package/dist/services/logger.js +80 -0
  32. package/dist/services/logger.js.map +1 -0
  33. package/dist/services/mcardStorageService.js +36 -0
  34. package/dist/services/mcardStorageService.js.map +1 -0
  35. package/dist/utils/actionHelpers.js +25 -0
  36. package/dist/utils/actionHelpers.js.map +1 -0
  37. package/dist/utils/bufferContentHelper.js +393 -0
  38. package/dist/utils/bufferContentHelper.js.map +1 -0
  39. package/dist/utils/bufferPolyfill.js +198 -0
  40. package/dist/utils/bufferPolyfill.js.map +1 -0
  41. package/dist/utils/content-detection.js +74 -0
  42. package/dist/utils/content-detection.js.map +1 -0
  43. package/dist/utils/content-utils.js +269 -0
  44. package/dist/utils/content-utils.js.map +1 -0
  45. package/dist/utils/content_type_detector copy.js +480 -0
  46. package/dist/utils/content_type_detector copy.js.map +1 -0
  47. package/dist/utils/content_type_detector.js +480 -0
  48. package/dist/utils/content_type_detector.js.map +1 -0
  49. package/dist/utils/cryptoPolyfill.js +166 -0
  50. package/dist/utils/cryptoPolyfill.js.map +1 -0
  51. package/dist/utils/dotenv-browser.js +35 -0
  52. package/dist/utils/dotenv-browser.js.map +1 -0
  53. package/dist/utils/environmentDetector.js +93 -0
  54. package/dist/utils/environmentDetector.js.map +1 -0
  55. package/dist/utils/logWriter.js +27 -0
  56. package/dist/utils/logWriter.js.map +1 -0
  57. package/dist/utils/serviceWorkerManager.js +118 -0
  58. package/dist/utils/serviceWorkerManager.js.map +1 -0
  59. package/dist/utils/test-content-detection.js +79 -0
  60. package/dist/utils/test-content-detection.js.map +1 -0
  61. package/dist/utils/test-detection-fix.js +121 -0
  62. package/dist/utils/test-detection-fix.js.map +1 -0
  63. package/dist/utils/test-format-conversion.js +170 -0
  64. package/dist/utils/test-format-conversion.js.map +1 -0
  65. package/dist/utils/test-mov-viewer.js +57 -0
  66. package/dist/utils/test-mov-viewer.js.map +1 -0
  67. package/dist/utils/testDetection.js +21 -0
  68. package/dist/utils/testDetection.js.map +1 -0
  69. package/dist/utils/textEncoderPolyfill.js +87 -0
  70. package/dist/utils/textEncoderPolyfill.js.map +1 -0
  71. package/package.json +74 -0
  72. package/src/__mocks__/better-sqlite3.js +14 -0
  73. package/src/config/config_constants.js +227 -0
  74. package/src/config/env_parameters.js +69 -0
  75. package/src/content/model/content_type_detector.js +87 -0
  76. package/src/core/card-collection.js +300 -0
  77. package/src/core/event-producer.js +160 -0
  78. package/src/core/g_time.js +215 -0
  79. package/src/core/hash/enums.js +13 -0
  80. package/src/core/hash/validator.js +271 -0
  81. package/src/core/mcard.js +203 -0
  82. package/src/engine/sqlite_engine.js +755 -0
  83. package/src/index.js +10 -0
  84. package/src/middleware/mcardPersistenceMiddleware.js +45 -0
  85. package/src/models/database_schemas.js +26 -0
  86. package/src/services/logger.js +74 -0
  87. package/src/services/mcardStorageService.js +34 -0
  88. package/src/utils/actionHelpers.js +13 -0
  89. package/src/utils/bufferContentHelper.js +436 -0
  90. package/src/utils/bufferPolyfill.js +202 -0
  91. package/src/utils/cn.ts +6 -0
  92. package/src/utils/content-detection.js +66 -0
  93. package/src/utils/content-utils.js +250 -0
  94. package/src/utils/content_type_detector copy.js +501 -0
  95. package/src/utils/content_type_detector.js +501 -0
  96. package/src/utils/cryptoPolyfill.js +180 -0
  97. package/src/utils/dateUtils.ts +18 -0
  98. package/src/utils/dbInitializer.ts +27 -0
  99. package/src/utils/dotenv-browser.js +29 -0
  100. package/src/utils/environmentDetector.js +92 -0
  101. package/src/utils/logWriter.js +20 -0
  102. package/src/utils/serviceWorkerManager.js +122 -0
  103. package/src/utils/stateWatcher.ts +78 -0
  104. package/src/utils/storeAdapter copy.ts +157 -0
  105. package/src/utils/storeAdapter.ts +157 -0
  106. package/src/utils/test-content-detection.js +71 -0
  107. package/src/utils/test-detection-fix.js +136 -0
  108. package/src/utils/test-format-conversion.js +165 -0
  109. package/src/utils/test-mov-viewer.js +59 -0
  110. package/src/utils/testDetection.js +16 -0
  111. package/src/utils/textEncoderPolyfill.js +88 -0
@@ -0,0 +1,755 @@
1
+ import { MCardFromData } from '../core/mcard.js';
2
+ import { Page } from '../core/card-collection.js';
3
+ import { DEFAULT_PAGE_SIZE, CARDS_DB_PATH } from '../config/config_constants.js';
4
+ import { MCARD_TABLE_SCHEMA, TRIGGERS } from '../models/database_schemas.js';
5
+ import ContentTypeInterpreter from '../content/model/content_type_detector.js';
6
+ import { SafeBuffer } from '../utils/bufferPolyfill.js';
7
+ import path from 'path';
8
+ import Database from 'better-sqlite3';
9
+ import fs from 'fs';
10
+
11
+ class SQLiteConnection {
12
+ /**
13
+ * Singleton instance management
14
+ */
15
+ static _instance = null;
16
+
17
+ /**
18
+ * Get singleton instance of SQLiteConnection
19
+ * @param {string} [dbPath] - Optional path to the SQLite database file
20
+ * @returns {SQLiteConnection} Singleton instance
21
+ */
22
+ static getInstance(dbPath = null) {
23
+ if (!this._instance) {
24
+ this._instance = new SQLiteConnection(dbPath);
25
+ }
26
+ return this._instance;
27
+ }
28
+
29
+ /**
30
+ * Create a new SQLite database connection
31
+ * @param {string} [dbPath] - Optional path to the SQLite database file
32
+ * Prioritizes the provided path, then environment variable, then default config
33
+ */
34
+ constructor(dbPath = null) {
35
+ // Determine the database path in order of priority:
36
+ // 1. Explicitly provided path
37
+ // 2. Environment variable
38
+ // 3. Default configuration path
39
+ this.dbPath = dbPath ||
40
+ process.env.MCARD_DB_PATH ||
41
+ CARDS_DB_PATH;
42
+
43
+ // Ensure the path is an absolute path
44
+ this.dbPath = path.resolve(this.dbPath);
45
+
46
+ this.conn = null;
47
+ }
48
+
49
+ /**
50
+ * Establish a database connection
51
+ */
52
+ connect() {
53
+ try {
54
+ // Ensure the directory exists
55
+ const dir = path.dirname(this.dbPath);
56
+ if (!fs.existsSync(dir)) {
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ }
59
+
60
+ // Open the database connection with appropriate flags
61
+ // Don't remove the existing database file to prevent data loss
62
+ this.conn = new Database(this.dbPath, {
63
+ // Open in read-write mode, create if not exists
64
+ mode: Database.OPEN_READWRITE | Database.OPEN_CREATE,
65
+ // Disable verbose mode to reduce unnecessary logging
66
+ verbose: null
67
+ });
68
+
69
+ return this;
70
+ } catch (error) {
71
+ console.error(`Database connection error: ${error.message}`);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Set up the database, creating the file and table if they don't exist
78
+ */
79
+ setup_database() {
80
+ try {
81
+ // Ensure the connection is open
82
+ if (!this.conn) {
83
+ this.connect();
84
+ }
85
+
86
+ // Check if the tables already exist
87
+ const tableExists = this.conn.prepare(`
88
+ SELECT name FROM sqlite_master
89
+ WHERE type='table' AND name='card'
90
+ `).get();
91
+
92
+ // Only create tables if they don't exist
93
+ if (!tableExists) {
94
+ console.log('Creating new database tables...');
95
+
96
+ // Create the table using the schema
97
+ this.conn.exec(MCARD_TABLE_SCHEMA);
98
+
99
+ // Add triggers
100
+ TRIGGERS.forEach(trigger => {
101
+ try {
102
+ this.conn.exec(trigger);
103
+ } catch (triggerError) {
104
+ console.warn(`Warning during trigger creation: ${triggerError.message}`);
105
+ }
106
+ });
107
+
108
+ console.log('Database tables created successfully');
109
+ } else {
110
+ console.log('Database tables already exist, skipping creation');
111
+ }
112
+ } catch (error) {
113
+ console.error('Database setup failed:', error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Close the database connection
120
+ */
121
+ disconnect() {
122
+ try {
123
+ if (this.conn) {
124
+ this.conn.close();
125
+ this.conn = null;
126
+ }
127
+ } catch (error) {
128
+ console.warn(`Error during database disconnect: ${error.message}`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Commit the current transaction
134
+ */
135
+ commit() {
136
+ if (this.conn) {
137
+ this.conn.prepare('COMMIT').run();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Rollback the current transaction
143
+ */
144
+ rollback() {
145
+ if (this.conn) {
146
+ this.conn.prepare('ROLLBACK').run();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Add a method to execute raw queries
152
+ * @param {string} query - Raw query to execute
153
+ * @param {array} params - Parameters for the query
154
+ * @returns {array} Results of the query
155
+ */
156
+ executeQuery(query, params = []) {
157
+ if (!this.conn) {
158
+ this.connect();
159
+ }
160
+ const stmt = this.conn.prepare(query);
161
+ return stmt.all(...params);
162
+ }
163
+ }
164
+
165
+ class SQLiteEngine {
166
+ /**
167
+ * Create a new SQLite storage engine
168
+ * @param {SQLiteConnection} connection - Database connection
169
+ */
170
+ constructor(connection = null) {
171
+ this.connection = connection || SQLiteConnection.getInstance();
172
+ this.connection.connect();
173
+ this.connection.setup_database();
174
+ this.clearStmt = this.connection.conn.prepare('DELETE FROM card');
175
+ }
176
+
177
+ /**
178
+ * Destructor to ensure database connection is closed
179
+ */
180
+ destructor() {
181
+ this.connection.disconnect();
182
+ }
183
+
184
+ /**
185
+ * Symbol.dispose method for resource cleanup
186
+ */
187
+ [Symbol.dispose]() {
188
+ this.destructor();
189
+ }
190
+
191
+ /**
192
+ * Add a card to the database
193
+ * @param {MCard} card - Card to add
194
+ * @returns {string} Hash of the added card
195
+ */
196
+ add(card) {
197
+ try {
198
+ console.log('SQLiteEngine.add called with card hash:', card.hash);
199
+
200
+ // Check if the card already exists
201
+ const existingCard = this.get(card.hash);
202
+ if (existingCard) {
203
+ console.log('Card already exists with hash:', card.hash);
204
+ return card.hash;
205
+ }
206
+
207
+ // Ensure content is properly serialized for SQLite storage
208
+ let finalContent;
209
+ // Always store as string (JSON) or Buffer
210
+ if (SafeBuffer.isBuffer(card.content)) {
211
+ // Buffers can be stored directly
212
+ finalContent = card.content;
213
+ console.log('Using Buffer content directly');
214
+ } else if (typeof card.content === 'object' && card.content !== null) {
215
+ // Explicitly serialize objects
216
+ finalContent = JSON.stringify(card.content);
217
+ console.log('Serialized object content to JSON string');
218
+ } else if (typeof card.content === 'string') {
219
+ // Strings can be stored directly
220
+ finalContent = card.content;
221
+ console.log('Using string content directly');
222
+ } else {
223
+ // Convert other types to string
224
+ finalContent = String(card.content);
225
+ console.log('Converted content to string');
226
+ }
227
+
228
+ // Insert the card into the database
229
+ try {
230
+ const stmt = this.connection.conn.prepare(
231
+ 'INSERT INTO card (hash, content, g_time) VALUES (?, ?, ?)'
232
+ );
233
+
234
+ stmt.run(card.hash, finalContent, card.g_time);
235
+
236
+ console.log('Card inserted successfully with hash:', card.hash);
237
+ return card.hash;
238
+ } catch (sqlError) {
239
+ console.error('SQL error inserting card:', sqlError);
240
+ throw sqlError;
241
+ }
242
+ } catch (error) {
243
+ console.error('Error in SQLiteEngine.add:', error);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Retrieve a card by its hash
250
+ * @param {string} hashValue - Hash of the card to retrieve
251
+ * @returns {MCard|null} Retrieved card or null
252
+ */
253
+ get(hash) {
254
+ try {
255
+ console.log('SQLiteEngine.get called with hash:', hash);
256
+
257
+ // Query the database for the card
258
+ const stmt = this.connection.conn.prepare(
259
+ 'SELECT hash, content, g_time, typeof(content) as content_type FROM card WHERE hash = ?'
260
+ );
261
+
262
+ const row = stmt.get(String(hash));
263
+
264
+ if (!row) {
265
+ console.log('No card found with hash:', hash);
266
+ return null;
267
+ }
268
+
269
+ console.log(`SQLiteEngine.get - Raw content typeof:`, row.content_type);
270
+ console.log(`SQLiteEngine.get - JS typeof:`, typeof row.content);
271
+ console.log(`SQLiteEngine.get - Is Buffer:`, SafeBuffer.isBuffer(row.content));
272
+ console.log(`SQLiteEngine.get - Content length:`, row.content ? row.content.length : 0);
273
+
274
+ if (SafeBuffer.isBuffer(row.content)) {
275
+ console.log(`SQLiteEngine.get - Buffer content first 20 bytes:`, row.content.slice(0, 20).toString('hex'));
276
+ } else if (typeof row.content === 'string') {
277
+ console.log(`SQLiteEngine.get - String content first 50 chars:`, row.content.substring(0, 50));
278
+ }
279
+
280
+ // Detect content type BEFORE any transformations
281
+ const isBlob = SafeBuffer.isBuffer(row.content);
282
+ const contentForDetection = row.content;
283
+
284
+ // Detect content type from the raw content
285
+ const contentType = ContentTypeInterpreter.detectContentType(contentForDetection);
286
+
287
+ console.log(`SQLiteEngine.get - Detected contentType:`, contentType);
288
+
289
+ // Add isBlob flag to contentType object
290
+ contentType.isBlob = isBlob;
291
+
292
+ // Parse content if it's a JSON string - but only if not detected as another type
293
+ let content = row.content;
294
+ // If stored as Buffer (Node.js), convert to Uint8Array for cross-env compatibility
295
+ if (SafeBuffer.isBuffer(content)) {
296
+ // Optionally decode to string if contentType is text or JSON
297
+ if (contentType.mimeType === 'application/json' || contentType.mimeType.startsWith('text/')) {
298
+ try {
299
+ const str = SafeBuffer.toString(content);
300
+ if (contentType.mimeType === 'application/json' && (str.startsWith('{') || str.startsWith('['))) {
301
+ content = JSON.parse(str);
302
+ console.log('Parsed JSON content successfully from Buffer');
303
+ } else {
304
+ content = str;
305
+ console.log('Converted Buffer to string');
306
+ }
307
+ } catch (e) {
308
+ console.warn('Failed to decode Buffer content:', e);
309
+ }
310
+ }
311
+ // else: leave as Uint8Array/Buffer for binary data
312
+ } else if (typeof content === 'string' && contentType.mimeType === 'application/json') {
313
+ try {
314
+ // Check if the string is a JSON object
315
+ if (content.startsWith('{') || content.startsWith('[')) {
316
+ const parsed = JSON.parse(content);
317
+ content = parsed;
318
+ console.log('Parsed JSON content successfully');
319
+ }
320
+ } catch (e) {
321
+ // If parsing fails, keep the original string
322
+ console.log('Content is not a valid JSON string, keeping as-is');
323
+ }
324
+ }
325
+
326
+ console.log('Card retrieved successfully with hash:', hash);
327
+
328
+ return {
329
+ hash: row.hash,
330
+ content: content,
331
+ g_time: row.g_time,
332
+ contentType: contentType // Add content type information to the response
333
+ };
334
+ } catch (error) {
335
+ console.error('Error retrieving card:', error);
336
+ return null;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Delete a card by its hash
342
+ * @param {string} hashValue - Hash of the card to delete
343
+ * @returns {boolean} Whether deletion was successful
344
+ */
345
+ delete(hashValue) {
346
+ try {
347
+ const stmt = this.connection.conn.prepare(
348
+ 'DELETE FROM card WHERE hash = ?'
349
+ );
350
+
351
+ const result = stmt.run(String(hashValue));
352
+ return result.changes > 0;
353
+ } catch (error) {
354
+ console.error(`Error deleting card: ${error.message}`);
355
+ throw error;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Get a page of cards
361
+ * @param {number} page_number - Page number to retrieve
362
+ * @param {number} page_size - Number of items per page
363
+ * @returns {Page} Page of cards
364
+ */
365
+ get_page(page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
366
+ if (page_number < 1 || page_size < 1) {
367
+ throw new Error('Page number and size must be >= 1');
368
+ }
369
+
370
+ const offset = (page_number - 1) * page_size;
371
+
372
+ // Get total count of items
373
+ const countStmt = this.connection.conn.prepare(
374
+ 'SELECT COUNT(*) as total FROM card'
375
+ );
376
+ const { total } = countStmt.get();
377
+
378
+ // Get page of items
379
+ const stmt = this.connection.conn.prepare(`
380
+ SELECT content, g_time, hash
381
+ FROM card
382
+ ORDER BY g_time DESC
383
+ LIMIT ? OFFSET ?
384
+ `);
385
+
386
+ const rows = stmt.all(page_size, offset);
387
+
388
+ // Convert rows to cards
389
+ const items = [];
390
+ for (const row of rows) {
391
+ const [content, g_time, hash] = [row.content, row.g_time, row.hash];
392
+
393
+ // Parse content if it's a JSON string
394
+ let parsedContent = content;
395
+ if (typeof content === 'string' && (content.startsWith('{') || content.startsWith('['))) {
396
+ try {
397
+ parsedContent = JSON.parse(content);
398
+ } catch (e) {
399
+ console.warn('Failed to parse JSON content for hash:', hash);
400
+ }
401
+ }
402
+
403
+ // Create card with properly parsed content
404
+ const card = new MCardFromData(parsedContent, hash, g_time);
405
+ items.push(card);
406
+ }
407
+
408
+ // Calculate total pages
409
+ const total_pages = Math.ceil(total / page_size);
410
+
411
+ return new Page({
412
+ items,
413
+ total_items: total,
414
+ page_number,
415
+ page_size,
416
+ has_next: offset + page_size < total,
417
+ has_previous: page_number > 1,
418
+ total_pages
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Search cards by string
424
+ * @param {string} search_string - String to search for
425
+ * @param {number} page_number - Page number to retrieve
426
+ * @param {number} page_size - Number of items per page
427
+ * @returns {Page} Page of matching cards
428
+ */
429
+ search_by_string(searchString, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) {
430
+ try {
431
+ if (pageNumber < 1) {
432
+ throw new Error('Page number must be >= 1');
433
+ }
434
+ if (pageSize < 1) {
435
+ throw new Error('Page size must be >= 1');
436
+ }
437
+
438
+ const offset = (pageNumber - 1) * pageSize;
439
+ const cursor = this.connection.conn;
440
+
441
+ // First, get total count of matching items
442
+ const countStmt = cursor.prepare(`
443
+ SELECT COUNT(*) as total FROM card
444
+ WHERE
445
+ CAST(content AS TEXT) LIKE ? OR
446
+ hash LIKE ? OR
447
+ g_time LIKE ?
448
+ `);
449
+ const { total } = countStmt.get(
450
+ `%${searchString}%`,
451
+ `%${searchString}%`,
452
+ `%${searchString}%`
453
+ );
454
+
455
+ // Then, get the actual items for the current page
456
+ const stmt = cursor.prepare(`
457
+ SELECT content, g_time, hash FROM card
458
+ WHERE
459
+ CAST(content AS TEXT) LIKE ? OR
460
+ hash LIKE ? OR
461
+ g_time LIKE ?
462
+ ORDER BY g_time DESC LIMIT ? OFFSET ?
463
+ `);
464
+
465
+ const rows = stmt.all(
466
+ `%${searchString}%`,
467
+ `%${searchString}%`,
468
+ `%${searchString}%`,
469
+ pageSize,
470
+ offset
471
+ );
472
+
473
+ // Convert rows to cards
474
+ const items = [];
475
+ for (const row of rows) {
476
+ const { content, g_time, hash } = row;
477
+
478
+ // Parse content if it's a JSON string
479
+ let parsedContent = content;
480
+ if (typeof content === 'string' && (content.startsWith('{') || content.startsWith('['))) {
481
+ try {
482
+ parsedContent = JSON.parse(content);
483
+ } catch (e) {
484
+ console.warn('Failed to parse JSON content for hash:', hash);
485
+ }
486
+ }
487
+
488
+ // Create card with properly parsed content
489
+ const card = new MCardFromData(parsedContent, hash, g_time);
490
+ items.push(card);
491
+ }
492
+
493
+ // Calculate pagination flags
494
+ const has_next = total > (pageNumber * pageSize);
495
+ const has_previous = pageNumber > 1;
496
+
497
+ return new Page({
498
+ items,
499
+ total_items: total,
500
+ page_number: pageNumber,
501
+ page_size: pageSize,
502
+ has_next,
503
+ has_previous,
504
+ total_pages: Math.ceil(total / pageSize)
505
+ });
506
+ } catch (error) {
507
+ console.error(`Error searching cards: ${error.message}`);
508
+ throw error;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Search for cards by content, hash, or g_time
514
+ * @param {string} searchString - String to search for
515
+ * @param {number} pageNumber - Page number for pagination
516
+ * @param {number} pageSize - Number of items per page
517
+ * @returns {Page} Paginated search results
518
+ */
519
+ search_by_content(searchString, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) {
520
+ try {
521
+ if (pageNumber < 1) {
522
+ throw new Error('Page number must be >= 1');
523
+ }
524
+ if (pageSize < 1) {
525
+ throw new Error('Page size must be >= 1');
526
+ }
527
+
528
+ const offset = (pageNumber - 1) * pageSize;
529
+ const cursor = this.connection.conn;
530
+
531
+ // First, get total count of matching items
532
+ const countStmt = cursor.prepare(
533
+ 'SELECT COUNT(*) as total FROM card WHERE CAST(content AS TEXT) LIKE ?'
534
+ );
535
+ const { total } = countStmt.get(`%${searchString}%`);
536
+
537
+ // Then, get the actual items for the current page
538
+ const stmt = cursor.prepare(`
539
+ SELECT content, g_time, hash FROM card
540
+ WHERE CAST(content AS TEXT) LIKE ?
541
+ ORDER BY g_time DESC LIMIT ? OFFSET ?
542
+ `);
543
+
544
+ const rows = stmt.all(
545
+ `%${searchString}%`,
546
+ pageSize,
547
+ offset
548
+ );
549
+
550
+ // Convert rows to cards
551
+ const items = [];
552
+ for (const row of rows) {
553
+ const { content, g_time, hash } = row;
554
+
555
+ // Parse content if it's a JSON string
556
+ let parsedContent = content;
557
+ if (typeof content === 'string' && (content.startsWith('{') || content.startsWith('['))) {
558
+ try {
559
+ parsedContent = JSON.parse(content);
560
+ } catch (e) {
561
+ console.warn('Failed to parse JSON content for hash:', hash);
562
+ }
563
+ }
564
+
565
+ // Create card with properly parsed content
566
+ const card = new MCardFromData(parsedContent, hash, g_time);
567
+ items.push(card);
568
+ }
569
+
570
+ // Calculate pagination flags
571
+ const has_next = total > (pageNumber * pageSize);
572
+ const has_previous = pageNumber > 1;
573
+
574
+ return new Page({
575
+ items,
576
+ total_items: total,
577
+ page_number: pageNumber,
578
+ page_size: pageSize,
579
+ has_next,
580
+ has_previous,
581
+ total_pages: Math.ceil(total / pageSize)
582
+ });
583
+ } catch (error) {
584
+ console.error(`Error searching cards: ${error.message}`);
585
+ throw error;
586
+ }
587
+ }
588
+
589
+ begin() {
590
+ if (this.connection.conn) {
591
+ this.connection.conn.prepare('BEGIN TRANSACTION').run();
592
+ }
593
+ }
594
+
595
+ commit() {
596
+ if (this.connection.conn) {
597
+ this.connection.conn.prepare('COMMIT').run();
598
+ }
599
+ }
600
+
601
+ rollback() {
602
+ if (this.connection.conn) {
603
+ this.connection.conn.prepare('ROLLBACK').run();
604
+ }
605
+ }
606
+
607
+ clear() {
608
+ try {
609
+ this.begin();
610
+ this.clearStmt.run();
611
+ this.commit();
612
+ } catch (error) {
613
+ this.rollback();
614
+ throw error;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Count the total number of cards
620
+ * @returns {number} Total number of cards
621
+ */
622
+ count() {
623
+ const stmt = this.connection.conn.prepare('SELECT COUNT(*) as total FROM card');
624
+ const { total } = stmt.get();
625
+ return total;
626
+ }
627
+
628
+ /**
629
+ * Update a card's content by hash
630
+ * @param {string} hash - Hash of the card to update
631
+ * @param {any} newContent - New content for the card
632
+ * @returns {boolean} Whether the update was successful
633
+ */
634
+ update(hash, newContent) {
635
+ try {
636
+ console.log('SQLiteEngine.update called with hash:', hash);
637
+
638
+ // First verify the card exists
639
+ const existingCard = this.get(hash);
640
+ if (!existingCard) {
641
+ console.log('No card found with hash:', hash);
642
+ return false;
643
+ }
644
+
645
+ // Prepare content for storage
646
+ let finalContent;
647
+
648
+ if (typeof newContent === 'object' && newContent !== null && !SafeBuffer.isBuffer(newContent)) {
649
+ // For objects, stringify to ensure proper SQLite storage
650
+ finalContent = JSON.stringify(newContent);
651
+ console.log('Serialized object content to JSON string for update');
652
+ } else if (typeof newContent === 'string') {
653
+ // Strings can be stored directly
654
+ finalContent = newContent;
655
+ console.log('Using string content directly for update');
656
+ } else if (SafeBuffer.isBuffer(newContent)) {
657
+ // Buffers can be stored directly
658
+ finalContent = newContent;
659
+ console.log('Using Buffer content directly for update');
660
+ } else {
661
+ // Convert other types to string
662
+ finalContent = String(newContent);
663
+ console.log('Converted content to string for update');
664
+ }
665
+
666
+ // Update the card in the database
667
+ const stmt = this.connection.conn.prepare(
668
+ 'UPDATE card SET content = ? WHERE hash = ?'
669
+ );
670
+
671
+ const result = stmt.run(finalContent, String(hash));
672
+
673
+ if (result.changes > 0) {
674
+ console.log('Card updated successfully with hash:', hash);
675
+ return true;
676
+ } else {
677
+ console.log('Card update had no effect for hash:', hash);
678
+ return false;
679
+ }
680
+ } catch (error) {
681
+ console.error('Error updating card:', error);
682
+ return false;
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Get all cards
688
+ * @param {number} page_number - Page number to retrieve
689
+ * @param {number} page_size - Number of items per page
690
+ * @returns {Page} Page of all cards
691
+ */
692
+ get_all(page_number = 1, page_size = DEFAULT_PAGE_SIZE) {
693
+ if (page_number < 1) {
694
+ throw new Error("Page number must be >= 1");
695
+ }
696
+ if (page_size < 1) {
697
+ throw new Error("Page size must be >= 1");
698
+ }
699
+
700
+ const offset = (page_number - 1) * page_size;
701
+
702
+ // Get total count of items
703
+ const countStmt = this.connection.conn.prepare(
704
+ 'SELECT COUNT(*) as total FROM card'
705
+ );
706
+ const { total } = countStmt.get();
707
+ console.log('Total cards:', total);
708
+
709
+ // Get page of items
710
+ const stmt = this.connection.conn.prepare(`
711
+ SELECT content, g_time, hash
712
+ FROM card
713
+ ORDER BY g_time DESC
714
+ LIMIT ? OFFSET ?
715
+ `);
716
+
717
+ const rows = stmt.all(page_size, offset);
718
+ console.log('Rows found:', rows.length);
719
+
720
+ // Convert rows to cards
721
+ const items = [];
722
+ for (const row of rows) {
723
+ const { content, g_time, hash } = row;
724
+
725
+ // Parse content if it's a JSON string
726
+ let parsedContent = content;
727
+ if (typeof content === 'string' && (content.startsWith('{') || content.startsWith('['))) {
728
+ try {
729
+ parsedContent = JSON.parse(content);
730
+ } catch (e) {
731
+ console.warn('Failed to parse JSON content for hash:', hash);
732
+ }
733
+ }
734
+
735
+ // Create card with properly parsed content
736
+ const card = new MCardFromData(parsedContent, hash, g_time);
737
+ items.push(card);
738
+ }
739
+
740
+ // Calculate total pages
741
+ const total_pages = Math.ceil(total / page_size);
742
+
743
+ return new Page({
744
+ items,
745
+ total_items: total,
746
+ page_number,
747
+ page_size,
748
+ has_next: offset + page_size < total,
749
+ has_previous: page_number > 1,
750
+ total_pages
751
+ });
752
+ }
753
+ }
754
+
755
+ export { SQLiteEngine, SQLiteConnection };