@better-media/adapter-db 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,610 @@
1
+ 'use strict';
2
+
3
+ var core = require('@better-media/core');
4
+ var kysely = require('kysely');
5
+ var mongodbAdapter = require('@better-media/mongodb-adapter');
6
+
7
+ // src/adapters/memory/memory-db.adapter.ts
8
+ var MemoryDbAdapter = class {
9
+ store = /* @__PURE__ */ new Map();
10
+ schema;
11
+ hooks;
12
+ constructor(options) {
13
+ this.schema = options?.schema;
14
+ this.hooks = options?.hooks;
15
+ }
16
+ getTable(model) {
17
+ let table = this.store.get(model);
18
+ if (!table) {
19
+ table = /* @__PURE__ */ new Map();
20
+ this.store.set(model, table);
21
+ }
22
+ return table;
23
+ }
24
+ getModelFields(model) {
25
+ return this.schema?.[model]?.fields ?? {};
26
+ }
27
+ getModelDefinition(model) {
28
+ return this.schema?.[model];
29
+ }
30
+ getHookContext(model, trx) {
31
+ return {
32
+ model,
33
+ adapter: this,
34
+ transaction: trx
35
+ };
36
+ }
37
+ matchCondition(record, condition) {
38
+ const recordValue = record[condition.field];
39
+ const targetValue = condition.value;
40
+ switch (condition.operator) {
41
+ case "!=":
42
+ return recordValue !== targetValue;
43
+ case "<":
44
+ return Number(recordValue) < Number(targetValue);
45
+ case "<=":
46
+ return Number(recordValue) <= Number(targetValue);
47
+ case ">":
48
+ return Number(recordValue) > Number(targetValue);
49
+ case ">=":
50
+ return Number(recordValue) >= Number(targetValue);
51
+ case "in":
52
+ return Array.isArray(targetValue) && targetValue.includes(recordValue);
53
+ case "not_in":
54
+ return Array.isArray(targetValue) && !targetValue.includes(recordValue);
55
+ case "contains":
56
+ return typeof recordValue === "string" && recordValue.includes(String(targetValue));
57
+ case "starts_with":
58
+ return String(recordValue).toLowerCase().startsWith(String(targetValue).toLowerCase());
59
+ case "ends_with":
60
+ return String(recordValue).toLowerCase().endsWith(String(targetValue).toLowerCase());
61
+ case "like":
62
+ return typeof recordValue === "string" && recordValue.toLowerCase().includes(String(targetValue).toLowerCase());
63
+ case "=":
64
+ default:
65
+ return recordValue === targetValue;
66
+ }
67
+ }
68
+ matchesWhere(record, where, model, options) {
69
+ const definition = model ? this.getModelDefinition(model) : void 0;
70
+ if (definition?.softDelete && !options?.withDeleted) {
71
+ if (record.deletedAt !== null && record.deletedAt !== void 0) return false;
72
+ }
73
+ if (!where || where.length === 0) return true;
74
+ let isMatch = true;
75
+ for (let i = 0; i < where.length; i++) {
76
+ const condition = where[i];
77
+ const connector = i > 0 ? where[i - 1]?.connector ?? "AND" : "AND";
78
+ const conditionMatches = this.matchCondition(record, condition);
79
+ if (connector === "OR") {
80
+ isMatch = isMatch || conditionMatches;
81
+ } else {
82
+ isMatch = isMatch && conditionMatches;
83
+ }
84
+ }
85
+ return isMatch;
86
+ }
87
+ async create(options) {
88
+ const table = this.getTable(options.model);
89
+ const fields = this.getModelFields(options.model);
90
+ const context = this.getHookContext(options.model);
91
+ let dataToInsert = options.data;
92
+ dataToInsert = await core.runHooks.beforeCreate(this.hooks, dataToInsert, context);
93
+ if (!dataToInsert.id) {
94
+ throw new Error("MemoryDbAdapter requires 'id' in data for create operations");
95
+ }
96
+ const serializedData = core.serializeData(fields, dataToInsert);
97
+ const clonedData = JSON.parse(JSON.stringify(serializedData));
98
+ table.set(String(clonedData.id), clonedData);
99
+ const resultRecord = core.deserializeData(fields, clonedData);
100
+ await core.runHooks.afterCreate(this.hooks, resultRecord, context);
101
+ return resultRecord;
102
+ }
103
+ populateRecord(record, model, populate) {
104
+ const result = JSON.parse(JSON.stringify(record));
105
+ const sortedPopulate = [...populate].sort((a, b) => a.split(".").length - b.split(".").length);
106
+ for (const path of sortedPopulate) {
107
+ const parts = path.split(".");
108
+ let currentObj = result;
109
+ let currentModel = model;
110
+ for (let i = 0; i < parts.length; i++) {
111
+ const part = parts[i];
112
+ const fieldDef = this.schema?.[currentModel]?.fields[part];
113
+ if (fieldDef?.references) {
114
+ const relatedTable = this.getTable(fieldDef.references.model);
115
+ const localValue = currentObj[part];
116
+ if (localValue === void 0 || localValue === null) break;
117
+ const relatedId = typeof localValue === "object" ? localValue.id : localValue;
118
+ const relatedRecord = relatedTable.get(String(relatedId));
119
+ if (relatedRecord) {
120
+ currentObj[part] = JSON.parse(JSON.stringify(relatedRecord));
121
+ currentModel = fieldDef.references.model;
122
+ currentObj = currentObj[part];
123
+ } else {
124
+ break;
125
+ }
126
+ } else {
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ return result;
132
+ }
133
+ async findOne(options) {
134
+ const table = this.getTable(options.model);
135
+ const fields = this.getModelFields(options.model);
136
+ for (const record of table.values()) {
137
+ if (this.matchesWhere(record, options.where, options.model, {
138
+ withDeleted: options.withDeleted
139
+ })) {
140
+ let result = record;
141
+ if (options.populate) {
142
+ result = this.populateRecord(record, options.model, options.populate);
143
+ }
144
+ return core.deserializeData(fields, JSON.parse(JSON.stringify(result)));
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+ async findMany(options) {
150
+ const table = this.getTable(options.model);
151
+ let results = [];
152
+ for (const record of table.values()) {
153
+ if (this.matchesWhere(record, options.where, options.model, {
154
+ withDeleted: options.withDeleted
155
+ })) {
156
+ results.push(JSON.parse(JSON.stringify(record)));
157
+ }
158
+ }
159
+ if (options.sortBy) {
160
+ const { field, direction } = options.sortBy;
161
+ results.sort((a, b) => {
162
+ const valA = a[field];
163
+ const valB = b[field];
164
+ if (valA < valB) return direction === "asc" ? -1 : 1;
165
+ if (valA > valB) return direction === "asc" ? 1 : -1;
166
+ return 0;
167
+ });
168
+ }
169
+ if (options.offset) results = results.slice(options.offset);
170
+ if (options.limit) results = results.slice(0, options.limit);
171
+ return results;
172
+ }
173
+ async update(options) {
174
+ const table = this.getTable(options.model);
175
+ const fields = this.getModelFields(options.model);
176
+ const context = this.getHookContext(options.model);
177
+ const target = await this.findOne({ model: options.model, where: options.where });
178
+ if (!target) return null;
179
+ let updatedData = { ...target, ...options.update };
180
+ updatedData = await core.runHooks.beforeUpdate(this.hooks, updatedData, context);
181
+ const serializedUpdate = core.serializeData(fields, updatedData);
182
+ const mergedData = { ...target, ...serializedUpdate };
183
+ const clonedData = JSON.parse(JSON.stringify(mergedData));
184
+ table.set(String(target.id), clonedData);
185
+ const resultRecord = core.deserializeData(fields, clonedData);
186
+ await core.runHooks.afterUpdate(this.hooks, resultRecord, context);
187
+ return resultRecord;
188
+ }
189
+ async updateMany(options) {
190
+ const targets = await this.findMany({ model: options.model, where: options.where });
191
+ for (const target of targets) {
192
+ await this.update({
193
+ model: options.model,
194
+ where: [{ field: "id", value: target.id }],
195
+ update: options.update
196
+ });
197
+ }
198
+ return targets.length;
199
+ }
200
+ async delete(options) {
201
+ const context = this.getHookContext(options.model);
202
+ const definition = this.getModelDefinition(options.model);
203
+ await core.runHooks.beforeDelete(this.hooks, options.where, context);
204
+ if (definition?.softDelete) {
205
+ await this.updateMany({
206
+ model: options.model,
207
+ where: options.where,
208
+ update: { deletedAt: /* @__PURE__ */ new Date() }
209
+ });
210
+ } else {
211
+ const table = this.getTable(options.model);
212
+ const targets = await this.findMany({ model: options.model, where: options.where });
213
+ for (const target of targets) {
214
+ table.delete(String(target.id));
215
+ }
216
+ }
217
+ await core.runHooks.afterDelete(this.hooks, options.where, context);
218
+ }
219
+ async deleteMany(options) {
220
+ const targets = await this.findMany({ model: options.model, where: options.where });
221
+ await this.delete(options);
222
+ return targets.length;
223
+ }
224
+ async count(options) {
225
+ const results = await this.findMany({ ...options, limit: void 0, offset: void 0 });
226
+ return results.length;
227
+ }
228
+ clear() {
229
+ this.store.clear();
230
+ }
231
+ async raw(query) {
232
+ if (query === "clear") {
233
+ this.clear();
234
+ return true;
235
+ }
236
+ throw new Error("MemoryDbAdapter only supports 'clear' as raw query.");
237
+ }
238
+ async transaction(callback) {
239
+ return await callback(this);
240
+ }
241
+ /** @internal Used by runMigrations — not part of the public DatabaseAdapter contract. */
242
+ async __getMetadata() {
243
+ const metadata = [];
244
+ for (const [tableName, table] of this.store.entries()) {
245
+ const columns = /* @__PURE__ */ new Set();
246
+ for (const record of table.values()) {
247
+ Object.keys(record).forEach((k) => columns.add(k));
248
+ }
249
+ metadata.push({
250
+ name: tableName,
251
+ columns: Array.from(columns).map((name) => ({
252
+ name,
253
+ dataType: "text",
254
+ isNullable: true
255
+ }))
256
+ });
257
+ }
258
+ return metadata;
259
+ }
260
+ /** @internal Used by runMigrations — not part of the public DatabaseAdapter contract. */
261
+ async __executeMigration(operation) {
262
+ if (operation.type === "createTable") {
263
+ this.getTable(operation.table);
264
+ }
265
+ }
266
+ };
267
+ var KyselyDbAdapter = class _KyselyDbAdapter {
268
+ db;
269
+ config;
270
+ schema;
271
+ hooks;
272
+ constructor(db, options) {
273
+ this.db = db;
274
+ this.config = options.config;
275
+ this.schema = options.schema;
276
+ this.hooks = options.hooks;
277
+ }
278
+ getModelFields(model) {
279
+ return this.schema[model]?.fields ?? {};
280
+ }
281
+ getModelDefinition(model) {
282
+ return this.schema[model];
283
+ }
284
+ getHookContext(model, trx) {
285
+ return {
286
+ model,
287
+ adapter: this,
288
+ transaction: trx
289
+ };
290
+ }
291
+ applyWhere(qb, where, model, options) {
292
+ let currentQb = qb;
293
+ const definition = model ? this.getModelDefinition(model) : void 0;
294
+ if (definition?.softDelete && !options?.withDeleted) {
295
+ currentQb = currentQb.where("deletedAt", "is", null);
296
+ }
297
+ if (!where || where.length === 0) return currentQb;
298
+ for (let i = 0; i < where.length; i++) {
299
+ const condition = where[i];
300
+ if (!condition) continue;
301
+ const connector = i > 0 ? where[i - 1]?.connector ?? "AND" : "AND";
302
+ const field = condition.field;
303
+ let operator = condition.operator ?? "=";
304
+ let value = condition.value;
305
+ if (operator === "contains") {
306
+ operator = "like";
307
+ value = `%${value}%`;
308
+ } else if (operator === "starts_with") {
309
+ operator = "like";
310
+ value = `${value}%`;
311
+ } else if (operator === "ends_with") {
312
+ operator = "like";
313
+ value = `%${value}`;
314
+ } else if (operator === "not_in") {
315
+ operator = "not in";
316
+ }
317
+ const method = connector === "OR" ? "orWhere" : "where";
318
+ currentQb = currentQb[method](field, operator, value);
319
+ }
320
+ return currentQb;
321
+ }
322
+ async create(options) {
323
+ const fields = this.getModelFields(options.model);
324
+ const context = this.getHookContext(options.model);
325
+ let dataToInsert = options.data;
326
+ dataToInsert = await core.runHooks.beforeCreate(this.hooks, dataToInsert, context);
327
+ const serializedData = core.serializeData(fields, dataToInsert);
328
+ let resultRecord;
329
+ if (this.config.provider === "sqlite") {
330
+ await this.db.insertInto(options.model).values(serializedData).execute();
331
+ const refetched = await this.db.selectFrom(options.model).selectAll().where("id", "=", serializedData.id).executeTakeFirst();
332
+ resultRecord = core.deserializeData(
333
+ fields,
334
+ refetched || serializedData
335
+ );
336
+ } else {
337
+ const result = await this.db.insertInto(options.model).values(serializedData).returningAll().executeTakeFirst() || serializedData;
338
+ resultRecord = core.deserializeData(fields, result);
339
+ }
340
+ await core.runHooks.afterCreate(this.hooks, resultRecord, context);
341
+ return resultRecord;
342
+ }
343
+ async findOne(options) {
344
+ const fields = this.getModelFields(options.model);
345
+ let qb = this.db.selectFrom(options.model);
346
+ if (options.select) {
347
+ qb = qb.select(options.select);
348
+ } else {
349
+ qb = qb.selectAll();
350
+ }
351
+ qb = this.applyWhere(qb, options.where, options.model, { withDeleted: options.withDeleted });
352
+ if (options.populate) {
353
+ for (const relation of options.populate) {
354
+ const fieldDef = this.schema[options.model]?.fields[relation];
355
+ if (fieldDef?.references) {
356
+ qb = qb.leftJoin(
357
+ fieldDef.references.model,
358
+ `${options.model}.${relation}`,
359
+ `${fieldDef.references.model}.${fieldDef.references.field}`
360
+ );
361
+ }
362
+ }
363
+ }
364
+ const result = await qb.executeTakeFirst();
365
+ if (!result) return null;
366
+ return core.deserializeData(fields, result);
367
+ }
368
+ async findMany(options) {
369
+ const fields = this.getModelFields(options.model);
370
+ let qb = this.db.selectFrom(options.model);
371
+ if (options.select) {
372
+ qb = qb.select(options.select);
373
+ } else {
374
+ qb = qb.selectAll();
375
+ }
376
+ qb = this.applyWhere(qb, options.where, options.model, { withDeleted: options.withDeleted });
377
+ if (options.sortBy) {
378
+ qb = qb.orderBy(options.sortBy.field, options.sortBy.direction);
379
+ }
380
+ if (options.limit !== void 0) qb = qb.limit(options.limit);
381
+ if (options.offset !== void 0) qb = qb.offset(options.offset);
382
+ const results = await qb.execute();
383
+ return results.map((row) => core.deserializeData(fields, row));
384
+ }
385
+ async update(options) {
386
+ const fields = this.getModelFields(options.model);
387
+ const context = this.getHookContext(options.model);
388
+ const target = await this.findOne({ model: options.model, where: options.where });
389
+ if (!target) return null;
390
+ let updatedData = { ...target, ...options.update };
391
+ updatedData = await core.runHooks.beforeUpdate(this.hooks, updatedData, context);
392
+ const updatePayload = core.serializeData(fields, options.update);
393
+ let qb = this.db.updateTable(options.model).set(updatePayload);
394
+ qb = this.applyWhere(qb, options.where, options.model);
395
+ let resultRecord;
396
+ if (this.config.provider === "sqlite") {
397
+ await qb.execute();
398
+ resultRecord = core.deserializeData(fields, core.serializeData(fields, updatedData));
399
+ } else {
400
+ const result = await qb.returningAll().executeTakeFirst();
401
+ resultRecord = core.deserializeData(
402
+ fields,
403
+ result || core.serializeData(fields, updatedData)
404
+ );
405
+ }
406
+ await core.runHooks.afterUpdate(this.hooks, resultRecord, context);
407
+ return resultRecord;
408
+ }
409
+ async updateMany(options) {
410
+ const fields = this.getModelFields(options.model);
411
+ const updatePayload = core.serializeData(fields, options.update);
412
+ let qb = this.db.updateTable(options.model).set(updatePayload);
413
+ qb = this.applyWhere(
414
+ qb,
415
+ options.where,
416
+ options.model
417
+ );
418
+ const results = await qb.execute();
419
+ return Number(results[0]?.numUpdatedRows || 0);
420
+ }
421
+ async delete(options) {
422
+ const context = this.getHookContext(options.model);
423
+ const definition = this.getModelDefinition(options.model);
424
+ await core.runHooks.beforeDelete(this.hooks, options.where, context);
425
+ if (definition?.softDelete) {
426
+ await this.updateMany({
427
+ model: options.model,
428
+ where: options.where,
429
+ update: { deletedAt: /* @__PURE__ */ new Date() }
430
+ });
431
+ } else {
432
+ let qb = this.db.deleteFrom(options.model);
433
+ qb = this.applyWhere(qb, options.where, options.model);
434
+ await qb.execute();
435
+ }
436
+ await core.runHooks.afterDelete(this.hooks, options.where, context);
437
+ }
438
+ async deleteMany(options) {
439
+ const definition = this.getModelDefinition(options.model);
440
+ if (definition?.softDelete) {
441
+ return await this.updateMany({
442
+ model: options.model,
443
+ where: options.where,
444
+ update: { deletedAt: /* @__PURE__ */ new Date() }
445
+ });
446
+ }
447
+ let qb = this.db.deleteFrom(options.model);
448
+ qb = this.applyWhere(qb, options.where, options.model);
449
+ const results = await qb.execute();
450
+ return Number(results[0]?.numDeletedRows || 0);
451
+ }
452
+ async count(options) {
453
+ let qb = this.db.selectFrom(options.model);
454
+ qb = this.applyWhere(qb, options.where, options.model);
455
+ const { count } = this.db.fn;
456
+ qb = qb.select(count("id").as("c"));
457
+ const result = await qb.executeTakeFirst();
458
+ return Number(result?.c || 0);
459
+ }
460
+ async raw(query, params) {
461
+ const result = await kysely.sql.raw(query, params).execute(this.db);
462
+ return result.rows;
463
+ }
464
+ async transaction(callback) {
465
+ return await this.db.transaction().execute(async (trx) => {
466
+ const trxAdapter = new _KyselyDbAdapter(trx, {
467
+ config: this.config,
468
+ schema: this.schema,
469
+ hooks: this.hooks
470
+ });
471
+ return await callback(trxAdapter);
472
+ });
473
+ }
474
+ /** @internal Used by runMigrations — not part of the public DatabaseAdapter contract. */
475
+ async __getMetadata() {
476
+ const tables = await this.db.introspection.getTables();
477
+ const dialect = this.__getDialect();
478
+ let filtered = tables;
479
+ if (dialect === "postgres") {
480
+ const currentSchema = await this.getPostgresSchema();
481
+ filtered = tables.filter((table) => {
482
+ const schema = table.schema;
483
+ return !schema || schema === currentSchema;
484
+ });
485
+ }
486
+ return filtered.map((table) => ({
487
+ name: table.name,
488
+ columns: table.columns.map((col) => ({
489
+ name: col.name,
490
+ dataType: col.dataType,
491
+ isNullable: col.isNullable ?? true
492
+ }))
493
+ }));
494
+ }
495
+ async getPostgresSchema() {
496
+ try {
497
+ const result = await kysely.sql`SHOW search_path`.execute(this.db);
498
+ const searchPath = result.rows[0]?.search_path ?? result.rows[0]?.searchPath;
499
+ if (!searchPath) return "public";
500
+ const schemas = searchPath.split(",").map((s) => s.trim()).map((s) => s.replace(/^["']|["']$/g, "")).filter((s) => !s.startsWith("$") && !s.startsWith("\\$"));
501
+ return schemas[0] || "public";
502
+ } catch {
503
+ return "public";
504
+ }
505
+ }
506
+ /** @internal Used by runMigrations to auto-detect the SQL dialect. */
507
+ __getDialect() {
508
+ switch (this.config.provider) {
509
+ case "pg":
510
+ return "postgres";
511
+ case "mysql":
512
+ return "mysql";
513
+ case "sqlite":
514
+ return "sqlite";
515
+ default:
516
+ return "postgres";
517
+ }
518
+ }
519
+ /** @internal Used by runMigrations — not part of the public DatabaseAdapter contract. */
520
+ async __executeMigration(operation) {
521
+ const dialect = this.__getDialect();
522
+ if (operation.type === "createTable") {
523
+ let builder = this.db.schema.createTable(operation.table).ifNotExists();
524
+ for (const [name, field] of Object.entries(operation.definition.fields)) {
525
+ const type = core.getColumnType(field, dialect);
526
+ builder = builder.addColumn(name, type, (col) => {
527
+ if (field.primaryKey) col = col.primaryKey();
528
+ if (field.required) col = col.notNull();
529
+ if (field.unique) col = col.unique();
530
+ if (field.references) {
531
+ col = col.references(`${field.references.model}.${field.references.field}`).onDelete(field.references.onDelete || "cascade");
532
+ }
533
+ return col;
534
+ });
535
+ }
536
+ await builder.execute();
537
+ if (operation.definition.indexes) {
538
+ for (const index of operation.definition.indexes) {
539
+ const indexName = `idx_${operation.table}_${index.fields.join("_")}`;
540
+ let indexBuilder = this.db.schema.createIndex(indexName).on(operation.table).columns(index.fields);
541
+ if (index.unique) {
542
+ indexBuilder = indexBuilder.unique();
543
+ }
544
+ await indexBuilder.execute();
545
+ }
546
+ }
547
+ } else if (operation.type === "addColumn") {
548
+ const type = core.getColumnType(operation.definition, dialect);
549
+ await this.db.schema.alterTable(operation.table).addColumn(operation.field, type, (col) => {
550
+ if (operation.definition.required) col = col.notNull();
551
+ if (operation.definition.unique) col = col.unique();
552
+ if (operation.definition.references) {
553
+ col = col.references(
554
+ `${operation.definition.references.model}.${operation.definition.references.field}`
555
+ ).onDelete(operation.definition.references.onDelete || "cascade");
556
+ }
557
+ return col;
558
+ }).execute();
559
+ } else if (operation.type === "createIndex") {
560
+ let builder = this.db.schema.createIndex(operation.name).on(operation.table).columns(operation.fields);
561
+ if (operation.unique) {
562
+ builder = builder.unique();
563
+ }
564
+ await builder.execute();
565
+ }
566
+ }
567
+ };
568
+
569
+ // src/index.ts
570
+ function memoryDatabase() {
571
+ const adapter = new MemoryDbAdapter();
572
+ return new Proxy(adapter, {
573
+ get(target, prop, receiver) {
574
+ if (prop === "get") {
575
+ return async (key) => {
576
+ const res = await adapter.findOne({
577
+ model: "legacy",
578
+ where: [{ field: "id", value: key }]
579
+ });
580
+ return res;
581
+ };
582
+ }
583
+ if (prop === "put") {
584
+ return async (key, data) => {
585
+ await adapter.create({ model: "legacy", data: { id: key, ...data } });
586
+ };
587
+ }
588
+ if (prop === "delete") {
589
+ return async (key) => {
590
+ await adapter.delete({ model: "legacy", where: [{ field: "id", value: key }] });
591
+ };
592
+ }
593
+ return Reflect.get(target, prop, receiver);
594
+ }
595
+ });
596
+ }
597
+
598
+ Object.defineProperty(exports, "MongoDbAdapter", {
599
+ enumerable: true,
600
+ get: function () { return mongodbAdapter.MongoDbAdapter; }
601
+ });
602
+ Object.defineProperty(exports, "mongodbAdapter", {
603
+ enumerable: true,
604
+ get: function () { return mongodbAdapter.mongodbAdapter; }
605
+ });
606
+ exports.KyselyDbAdapter = KyselyDbAdapter;
607
+ exports.MemoryDbAdapter = MemoryDbAdapter;
608
+ exports.memoryDatabase = memoryDatabase;
609
+ //# sourceMappingURL=index.js.map
610
+ //# sourceMappingURL=index.js.map