@hawiah/core 1.0.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/dist/Hawiah.js ADDED
@@ -0,0 +1,709 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Hawiah = void 0;
7
+ const dataloader_1 = __importDefault(require("dataloader"));
8
+ /**
9
+ * Hawiah: A lightweight, schema-less database abstraction layer.
10
+ * Designed to be friendly and easy to use.
11
+ */
12
+ class Hawiah {
13
+ driver;
14
+ isConnected = false;
15
+ relations = new Map();
16
+ loaders = new Map();
17
+ /**
18
+ * Creates a new Hawiah instance.
19
+ * @param options - Configuration options
20
+ * @param options.driver - The database driver to use
21
+ */
22
+ constructor(options) {
23
+ this.driver = options.driver;
24
+ }
25
+ /**
26
+ * Connects to the database.
27
+ */
28
+ async connect() {
29
+ if (this.isConnected) {
30
+ return;
31
+ }
32
+ await this.driver.connect();
33
+ this.isConnected = true;
34
+ }
35
+ /**
36
+ * Disconnects from the database.
37
+ */
38
+ async disconnect() {
39
+ if (!this.isConnected) {
40
+ return;
41
+ }
42
+ await this.driver.disconnect();
43
+ this.isConnected = false;
44
+ }
45
+ /**
46
+ * Adds a new record to the database.
47
+ * @param data - The data to add
48
+ * @returns The added record
49
+ */
50
+ async insert(data) {
51
+ this.ensureConnected();
52
+ return await this.driver.set(data);
53
+ }
54
+ /**
55
+ * Inserts multiple records into the database.
56
+ * @param dataArray - Array of data to insert
57
+ * @returns Array of inserted records
58
+ */
59
+ async insertMany(dataArray) {
60
+ this.ensureConnected();
61
+ const results = [];
62
+ for (const data of dataArray) {
63
+ const result = await this.driver.set(data);
64
+ results.push(result);
65
+ }
66
+ return results;
67
+ }
68
+ /**
69
+ * Gets records matching the query.
70
+ * @param query - The filter condition (default: all)
71
+ * @param limit - Optional maximum number of records to return
72
+ * @returns Array of matching records
73
+ */
74
+ async get(query = {}, limit) {
75
+ this.ensureConnected();
76
+ const results = await this.driver.get(query);
77
+ if (limit && limit > 0) {
78
+ return results.slice(0, limit);
79
+ }
80
+ return results;
81
+ }
82
+ /**
83
+ * Gets a single record matching the query.
84
+ * @param query - The filter condition
85
+ * @returns The first matching record or null
86
+ */
87
+ async getOne(query) {
88
+ this.ensureConnected();
89
+ return await this.driver.getOne(query);
90
+ }
91
+ /**
92
+ * Gets all records in the database.
93
+ * @returns Array of all records
94
+ */
95
+ async getAll() {
96
+ this.ensureConnected();
97
+ return await this.driver.get({});
98
+ }
99
+ /**
100
+ * Gets records matching any of the provided queries.
101
+ * @param queries - Array of filter conditions
102
+ * @returns Combined array of matching records
103
+ */
104
+ async getMany(queries) {
105
+ this.ensureConnected();
106
+ const results = [];
107
+ for (const query of queries) {
108
+ const data = await this.driver.get(query);
109
+ results.push(...data);
110
+ }
111
+ return results;
112
+ }
113
+ /**
114
+ * Updates records matching the query.
115
+ * @param query - The filter condition
116
+ * @param data - The data to update
117
+ * @returns Number of updated records
118
+ */
119
+ async update(query, data) {
120
+ this.ensureConnected();
121
+ return await this.driver.update(query, data);
122
+ }
123
+ /**
124
+ * Updates a single record matching the query.
125
+ * @param query - The filter condition
126
+ * @param data - The data to update
127
+ * @returns True if a record was updated
128
+ */
129
+ async updateOne(query, data) {
130
+ this.ensureConnected();
131
+ const count = await this.driver.update(query, data);
132
+ return count > 0;
133
+ }
134
+ /**
135
+ * Saves a record (adds if new, updates if exists).
136
+ * @param query - The filter condition to check existence
137
+ * @param data - The data to add or update
138
+ * @returns The saved record
139
+ */
140
+ async save(query, data) {
141
+ this.ensureConnected();
142
+ const existing = await this.driver.getOne(query);
143
+ if (existing) {
144
+ await this.driver.update(query, data);
145
+ return { ...existing, ...data };
146
+ }
147
+ else {
148
+ return await this.driver.set({ ...query, ...data });
149
+ }
150
+ }
151
+ /**
152
+ * Removes records matching the query.
153
+ * @param query - The filter condition
154
+ * @returns Number of removed records
155
+ */
156
+ async remove(query) {
157
+ this.ensureConnected();
158
+ return await this.driver.delete(query);
159
+ }
160
+ /**
161
+ * Removes a single record matching the query.
162
+ * @param query - The filter condition
163
+ * @returns True if a record was removed
164
+ */
165
+ async removeOne(query) {
166
+ this.ensureConnected();
167
+ const count = await this.driver.delete(query);
168
+ return count > 0;
169
+ }
170
+ /**
171
+ * Clears all records from the database.
172
+ * @returns Number of removed records
173
+ */
174
+ async clear() {
175
+ this.ensureConnected();
176
+ return await this.driver.delete({});
177
+ }
178
+ /**
179
+ * Checks if records exist matching the query.
180
+ * @param query - The filter condition
181
+ * @returns True if exists
182
+ */
183
+ async has(query) {
184
+ this.ensureConnected();
185
+ return await this.driver.exists(query);
186
+ }
187
+ /**
188
+ * Counts records matching the query.
189
+ * @param query - The filter condition
190
+ * @returns Count of records
191
+ */
192
+ async count(query = {}) {
193
+ this.ensureConnected();
194
+ return await this.driver.count(query);
195
+ }
196
+ /**
197
+ * Counts records where a field matches a value.
198
+ * @param field - The field name
199
+ * @param value - The value to match
200
+ * @returns Count of records
201
+ */
202
+ async countBy(field, value) {
203
+ this.ensureConnected();
204
+ const query = { [field]: value };
205
+ return await this.driver.count(query);
206
+ }
207
+ /**
208
+ * Gets a record by its ID.
209
+ * @param id - The record ID to search for
210
+ * @returns The matching record or null if not found
211
+ */
212
+ async getById(id) {
213
+ this.ensureConnected();
214
+ return await this.driver.getOne({ _id: id });
215
+ }
216
+ /**
217
+ * Updates a record by its ID.
218
+ * @param id - The record ID to update
219
+ * @param data - The data to update
220
+ * @returns True if the record was updated
221
+ */
222
+ async updateById(id, data) {
223
+ this.ensureConnected();
224
+ const count = await this.driver.update({ _id: id }, data);
225
+ return count > 0;
226
+ }
227
+ /**
228
+ * Removes a record by its ID.
229
+ * @param id - The record ID to remove
230
+ * @returns True if the record was removed
231
+ */
232
+ async removeById(id) {
233
+ this.ensureConnected();
234
+ const count = await this.driver.delete({ _id: id });
235
+ return count > 0;
236
+ }
237
+ /**
238
+ * Checks if a record exists by its ID.
239
+ * @param id - The record ID to check
240
+ * @returns True if the record exists
241
+ */
242
+ async hasId(id) {
243
+ this.ensureConnected();
244
+ return await this.driver.exists({ _id: id });
245
+ }
246
+ /**
247
+ * Gets records where a field matches a value.
248
+ * @param field - The field name to match
249
+ * @param value - The value to match
250
+ * @returns Array of matching records
251
+ */
252
+ async getBy(field, value) {
253
+ this.ensureConnected();
254
+ const query = { [field]: value };
255
+ return await this.driver.get(query);
256
+ }
257
+ /**
258
+ * Checks if a record exists where a field matches a value.
259
+ * @param field - The field name to check
260
+ * @param value - The value to match
261
+ * @returns True if a matching record exists
262
+ */
263
+ async hasBy(field, value) {
264
+ this.ensureConnected();
265
+ const query = { [field]: value };
266
+ return await this.driver.exists(query);
267
+ }
268
+ /**
269
+ * Sorts the results based on a field.
270
+ * @param query - The filter condition
271
+ * @param field - The field to sort by
272
+ * @param direction - 'asc' or 'desc'
273
+ * @returns Sorted array of records
274
+ */
275
+ async sort(query, field, direction = 'asc') {
276
+ this.ensureConnected();
277
+ const results = await this.driver.get(query);
278
+ return results.sort((a, b) => {
279
+ if (a[field] < b[field])
280
+ return direction === 'asc' ? -1 : 1;
281
+ if (a[field] > b[field])
282
+ return direction === 'asc' ? 1 : -1;
283
+ return 0;
284
+ });
285
+ }
286
+ /**
287
+ * Selects specific fields from the results.
288
+ * @param query - The filter condition
289
+ * @param fields - Array of field names to select
290
+ * @returns Array of records with only selected fields
291
+ */
292
+ async select(query, fields) {
293
+ this.ensureConnected();
294
+ const results = await this.driver.get(query);
295
+ return results.map(record => {
296
+ const selected = {};
297
+ fields.forEach(field => {
298
+ if (field in record) {
299
+ selected[field] = record[field];
300
+ }
301
+ });
302
+ return selected;
303
+ });
304
+ }
305
+ /**
306
+ * Retrieves unique values for a specific field.
307
+ * @param field - The field to get unique values for
308
+ * @param query - Optional filter condition
309
+ * @returns Array of unique values
310
+ */
311
+ async unique(field, query = {}) {
312
+ this.ensureConnected();
313
+ const results = await this.driver.get(query);
314
+ const values = results.map(record => record[field]);
315
+ return [...new Set(values)];
316
+ }
317
+ /**
318
+ * Groups records by a specific field.
319
+ * @param field - The field to group by
320
+ * @param query - Optional filter condition
321
+ * @returns Object where keys are group values and values are arrays of records
322
+ */
323
+ async group(field, query = {}) {
324
+ this.ensureConnected();
325
+ const results = await this.driver.get(query);
326
+ return results.reduce((groups, record) => {
327
+ const key = String(record[field]);
328
+ if (!groups[key]) {
329
+ groups[key] = [];
330
+ }
331
+ groups[key].push(record);
332
+ return groups;
333
+ }, {});
334
+ }
335
+ /**
336
+ * Paginates results.
337
+ * @param query - The filter condition
338
+ * @param page - Page number (1-based)
339
+ * @param pageSize - Records per page
340
+ * @returns Pagination result object
341
+ */
342
+ async paginate(query, page = 1, pageSize = 10) {
343
+ this.ensureConnected();
344
+ const allResults = await this.driver.get(query);
345
+ const total = allResults.length;
346
+ const totalPages = Math.ceil(total / pageSize);
347
+ const startIndex = (page - 1) * pageSize;
348
+ const endIndex = startIndex + pageSize;
349
+ const data = allResults.slice(startIndex, endIndex);
350
+ return {
351
+ data,
352
+ page,
353
+ pageSize,
354
+ total,
355
+ totalPages
356
+ };
357
+ }
358
+ /**
359
+ * Calculates the sum of a numeric field.
360
+ * @param field - The field to sum
361
+ * @param query - Optional filter condition
362
+ * @returns The sum of all values in the field
363
+ */
364
+ async sum(field, query = {}) {
365
+ this.ensureConnected();
366
+ const results = await this.driver.get(query);
367
+ return results.reduce((sum, record) => sum + (Number(record[field]) || 0), 0);
368
+ }
369
+ /**
370
+ * Increments a numeric field by a specified amount.
371
+ * @param query - The filter condition to find the record
372
+ * @param field - The field to increment
373
+ * @param amount - The amount to increment by (default: 1)
374
+ * @returns The new value after incrementing
375
+ * @throws Error if record is not found
376
+ */
377
+ async increment(query, field, amount = 1) {
378
+ this.ensureConnected();
379
+ const record = await this.driver.getOne(query);
380
+ if (!record) {
381
+ throw new Error('Record not found');
382
+ }
383
+ const currentValue = Number(record[field]) || 0;
384
+ const newValue = currentValue + amount;
385
+ await this.driver.update(query, { [field]: newValue });
386
+ return newValue;
387
+ }
388
+ /**
389
+ * Decrements a numeric field by a specified amount.
390
+ * @param query - The filter condition to find the record
391
+ * @param field - The field to decrement
392
+ * @param amount - The amount to decrement by (default: 1)
393
+ * @returns The new value after decrementing
394
+ * @throws Error if record is not found
395
+ */
396
+ async decrement(query, field, amount = 1) {
397
+ return await this.increment(query, field, -amount);
398
+ }
399
+ /**
400
+ * Pushes a value to the end of an array field.
401
+ * @param query - The filter condition to find records
402
+ * @param field - The array field to push to
403
+ * @param value - The value to push
404
+ * @returns Number of records updated
405
+ */
406
+ async push(query, field, value) {
407
+ this.ensureConnected();
408
+ const records = await this.driver.get(query);
409
+ let count = 0;
410
+ for (const record of records) {
411
+ if (!Array.isArray(record[field])) {
412
+ record[field] = [];
413
+ }
414
+ record[field].push(value);
415
+ await this.driver.update(query, record);
416
+ count++;
417
+ }
418
+ return count;
419
+ }
420
+ /**
421
+ * Removes all occurrences of a value from an array field.
422
+ * @param query - The filter condition to find records
423
+ * @param field - The array field to pull from
424
+ * @param value - The value to remove
425
+ * @returns Number of records updated
426
+ */
427
+ async pull(query, field, value) {
428
+ this.ensureConnected();
429
+ const records = await this.driver.get(query);
430
+ let count = 0;
431
+ for (const record of records) {
432
+ if (Array.isArray(record[field])) {
433
+ const initialLength = record[field].length;
434
+ record[field] = record[field].filter((item) => JSON.stringify(item) !== JSON.stringify(value));
435
+ if (record[field].length !== initialLength) {
436
+ await this.driver.update(query, record);
437
+ count++;
438
+ }
439
+ }
440
+ }
441
+ return count;
442
+ }
443
+ /**
444
+ * Removes the first element from an array field.
445
+ * @param query - The filter condition to find records
446
+ * @param field - The array field to shift
447
+ * @returns Number of records updated
448
+ */
449
+ async shift(query, field) {
450
+ this.ensureConnected();
451
+ const records = await this.driver.get(query);
452
+ let count = 0;
453
+ for (const record of records) {
454
+ if (Array.isArray(record[field]) && record[field].length > 0) {
455
+ record[field].shift();
456
+ await this.driver.update(query, record);
457
+ count++;
458
+ }
459
+ }
460
+ return count;
461
+ }
462
+ /**
463
+ * Adds a value to the beginning of an array field.
464
+ * @param query - The filter condition to find records
465
+ * @param field - The array field to unshift to
466
+ * @param value - The value to add
467
+ * @returns Number of records updated
468
+ */
469
+ async unshift(query, field, value) {
470
+ this.ensureConnected();
471
+ const records = await this.driver.get(query);
472
+ let count = 0;
473
+ for (const record of records) {
474
+ if (!Array.isArray(record[field])) {
475
+ record[field] = [];
476
+ }
477
+ record[field].unshift(value);
478
+ await this.driver.update(query, record);
479
+ count++;
480
+ }
481
+ return count;
482
+ }
483
+ /**
484
+ * Removes the last element from an array field.
485
+ * @param query - The filter condition to find records
486
+ * @param field - The array field to pop from
487
+ * @returns Number of records updated
488
+ */
489
+ async pop(query, field) {
490
+ this.ensureConnected();
491
+ const records = await this.driver.get(query);
492
+ let count = 0;
493
+ for (const record of records) {
494
+ if (Array.isArray(record[field]) && record[field].length > 0) {
495
+ record[field].pop();
496
+ await this.driver.update(query, record);
497
+ count++;
498
+ }
499
+ }
500
+ return count;
501
+ }
502
+ /**
503
+ * Removes a field from matching records.
504
+ * @param query - The filter condition to find records
505
+ * @param field - The field name to remove
506
+ * @returns Number of records updated
507
+ */
508
+ async unset(query, field) {
509
+ this.ensureConnected();
510
+ const records = await this.driver.get(query);
511
+ let count = 0;
512
+ for (const record of records) {
513
+ if (field in record) {
514
+ delete record[field];
515
+ await this.driver.update(query, record);
516
+ count++;
517
+ }
518
+ }
519
+ return count;
520
+ }
521
+ /**
522
+ * Renames a field in matching records.
523
+ * @param query - The filter condition to find records
524
+ * @param oldField - The current field name
525
+ * @param newField - The new field name
526
+ * @returns Number of records updated
527
+ */
528
+ async rename(query, oldField, newField) {
529
+ this.ensureConnected();
530
+ const records = await this.driver.get(query);
531
+ let count = 0;
532
+ for (const record of records) {
533
+ if (oldField in record) {
534
+ record[newField] = record[oldField];
535
+ delete record[oldField];
536
+ await this.driver.update(query, record);
537
+ count++;
538
+ }
539
+ }
540
+ return count;
541
+ }
542
+ /**
543
+ * Gets the first record in the database.
544
+ * @returns The first record or null if empty
545
+ */
546
+ async first() {
547
+ this.ensureConnected();
548
+ const results = await this.driver.get({});
549
+ return results.length > 0 ? results[0] : null;
550
+ }
551
+ /**
552
+ * Gets the last record in the database.
553
+ * @returns The last record or null if empty
554
+ */
555
+ async last() {
556
+ this.ensureConnected();
557
+ const results = await this.driver.get({});
558
+ return results.length > 0 ? results[results.length - 1] : null;
559
+ }
560
+ /**
561
+ * Checks if the database is empty.
562
+ * @returns True if no records exist
563
+ */
564
+ async isEmpty() {
565
+ this.ensureConnected();
566
+ const count = await this.driver.count({});
567
+ return count === 0;
568
+ }
569
+ /**
570
+ * Gets random records from the database.
571
+ * @param sampleSize - Number of random records to return (default: 1)
572
+ * @returns Array of random records
573
+ */
574
+ async random(sampleSize = 1) {
575
+ this.ensureConnected();
576
+ const results = await this.driver.get({});
577
+ const shuffled = results.sort(() => 0.5 - Math.random());
578
+ return shuffled.slice(0, sampleSize);
579
+ }
580
+ /**
581
+ * Ensures the database is connected before executing operations.
582
+ * @throws Error if database is not connected
583
+ * @private
584
+ */
585
+ ensureConnected() {
586
+ if (!this.isConnected) {
587
+ throw new Error('Database not connected. Call connect() first.');
588
+ }
589
+ }
590
+ /**
591
+ * Gets the underlying database driver.
592
+ * @returns The database driver instance
593
+ */
594
+ getDriver() {
595
+ return this.driver;
596
+ }
597
+ /**
598
+ * Checks if the database connection is active.
599
+ * @returns True if connected
600
+ */
601
+ isActive() {
602
+ return this.isConnected;
603
+ }
604
+ /**
605
+ * Define a relationship with another Hawiah instance
606
+ * @param name - Relationship name
607
+ * @param target - Target Hawiah instance
608
+ * @param localKey - Local field name
609
+ * @param foreignKey - Foreign field name
610
+ * @param type - Relationship type ('one' or 'many')
611
+ */
612
+ relation(name, target, localKey, foreignKey, type = 'many') {
613
+ this.relations.set(name, { target, localKey, foreignKey, type });
614
+ return this;
615
+ }
616
+ /**
617
+ * Get records with populated relationships
618
+ * @param query - Query filter
619
+ * @param relations - Relationship names to populate
620
+ */
621
+ async getWith(query = {}, ...relations) {
622
+ this.ensureConnected();
623
+ const records = await this.driver.get(query);
624
+ if (relations.length === 0 || records.length === 0) {
625
+ return records;
626
+ }
627
+ for (const relationName of relations) {
628
+ await this.loadRelation(records, relationName);
629
+ }
630
+ return records;
631
+ }
632
+ /**
633
+ * Get one record with populated relationships
634
+ * @param query - Query filter
635
+ * @param relations - Relationship names to populate
636
+ */
637
+ async getOneWith(query, ...relations) {
638
+ this.ensureConnected();
639
+ const record = await this.driver.getOne(query);
640
+ if (!record || relations.length === 0) {
641
+ return record;
642
+ }
643
+ for (const relationName of relations) {
644
+ await this.loadRelation([record], relationName);
645
+ }
646
+ return record;
647
+ }
648
+ /**
649
+ * Load a relationship for records using DataLoader batching
650
+ */
651
+ async loadRelation(records, relationName) {
652
+ const relation = this.relations.get(relationName);
653
+ if (!relation) {
654
+ throw new Error(`Relation "${relationName}" not defined`);
655
+ }
656
+ const { target, localKey, foreignKey, type } = relation;
657
+ let loader = this.loaders.get(relationName);
658
+ if (!loader) {
659
+ loader = new dataloader_1.default(async (keys) => {
660
+ const allRecords = await target.get({});
661
+ if (type === 'one') {
662
+ const map = new Map();
663
+ allRecords.forEach(record => {
664
+ if (keys.includes(record[foreignKey])) {
665
+ map.set(record[foreignKey], record);
666
+ }
667
+ });
668
+ return keys.map(key => map.get(key) || null);
669
+ }
670
+ else {
671
+ const map = new Map();
672
+ allRecords.forEach(record => {
673
+ if (keys.includes(record[foreignKey])) {
674
+ if (!map.has(record[foreignKey])) {
675
+ map.set(record[foreignKey], []);
676
+ }
677
+ map.get(record[foreignKey]).push(record);
678
+ }
679
+ });
680
+ return keys.map(key => map.get(key) || []);
681
+ }
682
+ }, { cache: true });
683
+ this.loaders.set(relationName, loader);
684
+ }
685
+ const keys = records.map(r => r[localKey]).filter(k => k != null);
686
+ if (keys.length === 0)
687
+ return;
688
+ const results = await loader.loadMany(keys);
689
+ let resultIndex = 0;
690
+ records.forEach(record => {
691
+ if (record[localKey] != null) {
692
+ record[relationName] = results[resultIndex];
693
+ resultIndex++;
694
+ }
695
+ else {
696
+ record[relationName] = type === 'many' ? [] : null;
697
+ }
698
+ });
699
+ }
700
+ /**
701
+ * Clear relationship cache
702
+ */
703
+ clearCache() {
704
+ this.loaders.forEach(loader => loader.clearAll());
705
+ this.loaders.clear();
706
+ }
707
+ }
708
+ exports.Hawiah = Hawiah;
709
+ //# sourceMappingURL=Hawiah.js.map