@xfcfam/xf-sql 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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/index.d.ts +26 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +27 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/api/A.d.ts +17 -0
  8. package/dist/src/api/A.d.ts.map +1 -0
  9. package/dist/src/api/A.js +17 -0
  10. package/dist/src/api/A.js.map +1 -0
  11. package/dist/src/business/B.d.ts +17 -0
  12. package/dist/src/business/B.d.ts.map +1 -0
  13. package/dist/src/business/B.js +17 -0
  14. package/dist/src/business/B.js.map +1 -0
  15. package/dist/src/repository/R.d.ts +17 -0
  16. package/dist/src/repository/R.d.ts.map +1 -0
  17. package/dist/src/repository/R.js +17 -0
  18. package/dist/src/repository/R.js.map +1 -0
  19. package/dist/src/repository/base/DatabaseRepository.d.ts +184 -0
  20. package/dist/src/repository/base/DatabaseRepository.d.ts.map +1 -0
  21. package/dist/src/repository/base/DatabaseRepository.js +216 -0
  22. package/dist/src/repository/base/DatabaseRepository.js.map +1 -0
  23. package/dist/src/repository/base/TransactionalDatabaseRepository.d.ts +86 -0
  24. package/dist/src/repository/base/TransactionalDatabaseRepository.d.ts.map +1 -0
  25. package/dist/src/repository/base/TransactionalDatabaseRepository.js +104 -0
  26. package/dist/src/repository/base/TransactionalDatabaseRepository.js.map +1 -0
  27. package/dist/src/repository/general/DatabaseRepository.d.ts +192 -0
  28. package/dist/src/repository/general/DatabaseRepository.d.ts.map +1 -0
  29. package/dist/src/repository/general/DatabaseRepository.js +224 -0
  30. package/dist/src/repository/general/DatabaseRepository.js.map +1 -0
  31. package/dist/src/repository/general/TransactionalDatabaseRepository.d.ts +91 -0
  32. package/dist/src/repository/general/TransactionalDatabaseRepository.d.ts.map +1 -0
  33. package/dist/src/repository/general/TransactionalDatabaseRepository.js +114 -0
  34. package/dist/src/repository/general/TransactionalDatabaseRepository.js.map +1 -0
  35. package/dist/src/repository/structs/CheckViolationException.d.ts +19 -0
  36. package/dist/src/repository/structs/CheckViolationException.d.ts.map +1 -0
  37. package/dist/src/repository/structs/CheckViolationException.js +22 -0
  38. package/dist/src/repository/structs/CheckViolationException.js.map +1 -0
  39. package/dist/src/repository/structs/ConnectionException.d.ts +15 -0
  40. package/dist/src/repository/structs/ConnectionException.d.ts.map +1 -0
  41. package/dist/src/repository/structs/ConnectionException.js +16 -0
  42. package/dist/src/repository/structs/ConnectionException.js.map +1 -0
  43. package/dist/src/repository/structs/DatabaseException.d.ts +14 -0
  44. package/dist/src/repository/structs/DatabaseException.d.ts.map +1 -0
  45. package/dist/src/repository/structs/DatabaseException.js +15 -0
  46. package/dist/src/repository/structs/DatabaseException.js.map +1 -0
  47. package/dist/src/repository/structs/DeadlockException.d.ts +16 -0
  48. package/dist/src/repository/structs/DeadlockException.d.ts.map +1 -0
  49. package/dist/src/repository/structs/DeadlockException.js +17 -0
  50. package/dist/src/repository/structs/DeadlockException.js.map +1 -0
  51. package/dist/src/repository/structs/ForeignKeyViolationException.d.ts +20 -0
  52. package/dist/src/repository/structs/ForeignKeyViolationException.d.ts.map +1 -0
  53. package/dist/src/repository/structs/ForeignKeyViolationException.js +23 -0
  54. package/dist/src/repository/structs/ForeignKeyViolationException.js.map +1 -0
  55. package/dist/src/repository/structs/NotNullViolationException.d.ts +20 -0
  56. package/dist/src/repository/structs/NotNullViolationException.d.ts.map +1 -0
  57. package/dist/src/repository/structs/NotNullViolationException.js +23 -0
  58. package/dist/src/repository/structs/NotNullViolationException.js.map +1 -0
  59. package/dist/src/repository/structs/UniqueViolationException.d.ts +36 -0
  60. package/dist/src/repository/structs/UniqueViolationException.d.ts.map +1 -0
  61. package/dist/src/repository/structs/UniqueViolationException.js +40 -0
  62. package/dist/src/repository/structs/UniqueViolationException.js.map +1 -0
  63. package/dist/src/repository/transfers/CheckViolationException.d.ts +19 -0
  64. package/dist/src/repository/transfers/CheckViolationException.d.ts.map +1 -0
  65. package/dist/src/repository/transfers/CheckViolationException.js +22 -0
  66. package/dist/src/repository/transfers/CheckViolationException.js.map +1 -0
  67. package/dist/src/repository/transfers/ConnectionException.d.ts +15 -0
  68. package/dist/src/repository/transfers/ConnectionException.d.ts.map +1 -0
  69. package/dist/src/repository/transfers/ConnectionException.js +16 -0
  70. package/dist/src/repository/transfers/ConnectionException.js.map +1 -0
  71. package/dist/src/repository/transfers/DatabaseException.d.ts +14 -0
  72. package/dist/src/repository/transfers/DatabaseException.d.ts.map +1 -0
  73. package/dist/src/repository/transfers/DatabaseException.js +15 -0
  74. package/dist/src/repository/transfers/DatabaseException.js.map +1 -0
  75. package/dist/src/repository/transfers/DeadlockException.d.ts +16 -0
  76. package/dist/src/repository/transfers/DeadlockException.d.ts.map +1 -0
  77. package/dist/src/repository/transfers/DeadlockException.js +17 -0
  78. package/dist/src/repository/transfers/DeadlockException.js.map +1 -0
  79. package/dist/src/repository/transfers/ForeignKeyViolationException.d.ts +20 -0
  80. package/dist/src/repository/transfers/ForeignKeyViolationException.d.ts.map +1 -0
  81. package/dist/src/repository/transfers/ForeignKeyViolationException.js +23 -0
  82. package/dist/src/repository/transfers/ForeignKeyViolationException.js.map +1 -0
  83. package/dist/src/repository/transfers/NotNullViolationException.d.ts +20 -0
  84. package/dist/src/repository/transfers/NotNullViolationException.d.ts.map +1 -0
  85. package/dist/src/repository/transfers/NotNullViolationException.js +23 -0
  86. package/dist/src/repository/transfers/NotNullViolationException.js.map +1 -0
  87. package/dist/src/repository/transfers/UniqueViolationException.d.ts +36 -0
  88. package/dist/src/repository/transfers/UniqueViolationException.d.ts.map +1 -0
  89. package/dist/src/repository/transfers/UniqueViolationException.js +40 -0
  90. package/dist/src/repository/transfers/UniqueViolationException.js.map +1 -0
  91. package/package.json +52 -0
@@ -0,0 +1,216 @@
1
+ import { Repository, NotInitializedException } from '@xfarch/xf';
2
+ import { Kysely } from 'kysely';
3
+ /**
4
+ * Base Generalization for the Access Layer when the underlying
5
+ * external system is a SQL database.
6
+ *
7
+ * Encapsulates the [Kysely](https://kysely.dev) query builder behind
8
+ * a single XF-canonical class. The implementer's concrete Logical
9
+ * extends this (or a dialect-specific subclass such as
10
+ * `PostgresDatabaseRepository`) and exposes domain-meaningful methods
11
+ * that compose queries against `this.db`, a `Kysely<Schema>` bound
12
+ * to the implementer's typed schema.
13
+ *
14
+ * Concrete dialect support lives in adapter packages — install one of
15
+ * `@xfarch/xf-sql-postgres`, `@xfarch/xf-sql-mysql`, etc., or supply
16
+ * any Kysely-compatible `Dialect` directly.
17
+ *
18
+ * ──────────────────────────────────────────────────────────────────
19
+ * IMPORTANT — Required configuration for subclasses
20
+ * ──────────────────────────────────────────────────────────────────
21
+ * The underlying Kysely instance is held in a static private WeakMap
22
+ * and created inside {@link init}. If a subclass overrides `init()` /
23
+ * `terminate()`, it MUST chain through `super`:
24
+ *
25
+ * async init() { await super.init(); // own setup }
26
+ * async terminate() { // own teardown; await super.terminate() }
27
+ *
28
+ * Forgetting `super.init()` leaves `this.db` uninitialised and every
29
+ * query will throw {@link NotInitializedException}.
30
+ *
31
+ * ──────────────────────────────────────────────────────────────────
32
+ * Overridable observation hooks
33
+ * ──────────────────────────────────────────────────────────────────
34
+ * - {@link onConnected} ← after `init()` creates the Kysely instance
35
+ * - {@link onDisconnected} ← after `terminate()` destroys it
36
+ * - {@link onQuery} ← for every query Kysely executes
37
+ * - {@link onError} ← for every {@link exec}-wrapped operation that rejects
38
+ *
39
+ * Transaction hooks ({@link onTransactionStart} / Commit / Rollback)
40
+ * live on {@link TransactionalDatabaseRepository}, the subclass that
41
+ * exposes explicit transaction control.
42
+ *
43
+ * ──────────────────────────────────────────────────────────────────
44
+ * Error translation
45
+ * ──────────────────────────────────────────────────────────────────
46
+ * Dialect-specific errors (driver Error objects) are not translated
47
+ * by this class — it does not know about Postgres SQLSTATEs, MySQL
48
+ * error numbers, etc. Use a dialect adapter subclass (such as
49
+ * `PostgresDatabaseRepository`) or override {@link translateError}
50
+ * yourself to map errors to the typed Exceptions exported from this
51
+ * package (`UniqueViolationException`, etc.).
52
+ *
53
+ * @typeParam Schema Implementer-defined TypeScript interface mapping
54
+ * table names to their column shapes. See the
55
+ * Kysely docs for the conventions on
56
+ * `Generated`, `ColumnType`, etc.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { DatabaseRepository } from '@xfarch/xf-sql'
61
+ * import { PostgresDialect } from 'kysely'
62
+ * import { Pool } from 'pg'
63
+ *
64
+ * interface Schema {
65
+ * users: { id: number; name: string; email: string }
66
+ * }
67
+ *
68
+ * export class UsersDb extends DatabaseRepository<Schema> {
69
+ * constructor() {
70
+ * super({
71
+ * dialect: new PostgresDialect({
72
+ * pool: new Pool({ connectionString: process.env.DATABASE_URL }),
73
+ * }),
74
+ * })
75
+ * }
76
+ * async init() { await super.init() }
77
+ * async terminate() { await super.terminate() }
78
+ *
79
+ * getUser(id: number) {
80
+ * return this.db
81
+ * .selectFrom('users')
82
+ * .where('id', '=', id)
83
+ * .selectAll()
84
+ * .executeTakeFirstOrThrow()
85
+ * }
86
+ * }
87
+ * ```
88
+ */
89
+ export class DatabaseRepository extends Repository {
90
+ static state = new WeakMap();
91
+ /** Options provided at construction time. */
92
+ options;
93
+ constructor(options) {
94
+ super(null);
95
+ this.options = options;
96
+ }
97
+ /**
98
+ * Typed Kysely instance bound to `Schema`. Use it to compose queries:
99
+ * `this.db.selectFrom('users')…`, `this.db.insertInto('users')…`.
100
+ * Throws if {@link init} has not been called.
101
+ */
102
+ get db() {
103
+ const s = DatabaseRepository.state.get(this);
104
+ if (s === undefined) {
105
+ throw new NotInitializedException('DatabaseRepository: init() was not called (or super.init() was skipped)');
106
+ }
107
+ return s.db;
108
+ }
109
+ /**
110
+ * Subclasses that override this method MUST call `await super.init()`
111
+ * **first**; otherwise {@link db} will throw on use.
112
+ */
113
+ async init() {
114
+ const db = new Kysely({
115
+ dialect: this.options.dialect,
116
+ log: (event) => {
117
+ if (event.level === 'query') {
118
+ this.onQuery(event.query.sql, event.query.parameters);
119
+ }
120
+ else if (event.level === 'error') {
121
+ // Kysely-emitted query errors are also routed through onError
122
+ // for symmetry with exec() failures.
123
+ this.onError('query', event.error);
124
+ }
125
+ },
126
+ });
127
+ DatabaseRepository.state.set(this, { db });
128
+ await this.onConnected();
129
+ }
130
+ /**
131
+ * Subclasses that override this method MUST call `await super.terminate()`
132
+ * **last** to release the connection pool held by the Kysely instance.
133
+ */
134
+ async terminate() {
135
+ const s = DatabaseRepository.state.get(this);
136
+ if (s !== undefined) {
137
+ await s.db.destroy();
138
+ DatabaseRepository.state.delete(this);
139
+ await this.onDisconnected();
140
+ }
141
+ }
142
+ /**
143
+ * Hook for dialect-specific error translation. Default
144
+ * implementation is identity (returns the input unchanged).
145
+ *
146
+ * Dialect adapter subclasses (such as `PostgresDatabaseRepository`)
147
+ * override this to map driver errors to the typed Exceptions
148
+ * exported from this package.
149
+ *
150
+ * Called automatically by {@link exec}; not called for direct
151
+ * `this.db.…` usage, where the implementer is responsible for
152
+ * wrapping queries in `this.exec(...)`.
153
+ */
154
+ translateError(err) {
155
+ return err;
156
+ }
157
+ /**
158
+ * Execute an operation against the database, translating any
159
+ * dialect-specific errors to xf-sql Exception types via
160
+ * {@link translateError}.
161
+ *
162
+ * Use this whenever you need automatic error translation. For raw
163
+ * Kysely access without translation, use {@link db} directly.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * async createUser(input: UserInput) {
168
+ * return this.exec(() =>
169
+ * this.db.insertInto('users').values(input).returningAll().executeTakeFirstOrThrow()
170
+ * )
171
+ * }
172
+ * ```
173
+ */
174
+ async exec(op) {
175
+ try {
176
+ return await op();
177
+ }
178
+ catch (err) {
179
+ this.onError('exec', err);
180
+ throw this.translateError(err);
181
+ }
182
+ }
183
+ // ─── Overridable observation hooks ────────────────────────
184
+ /**
185
+ * Invoked after `init()` has created the Kysely instance. Default
186
+ * no-op. Override for connection-open telemetry, schema migrations,
187
+ * connection warmup, etc.
188
+ */
189
+ async onConnected() { }
190
+ /**
191
+ * Invoked after `terminate()` has destroyed the Kysely instance.
192
+ * Default no-op. Override for connection-close telemetry.
193
+ */
194
+ async onDisconnected() { }
195
+ /**
196
+ * Invoked for every query Kysely executes (both standalone and
197
+ * inside transactions). Receives the compiled SQL and the bound
198
+ * parameters. Default no-op.
199
+ *
200
+ * Use for audit logging or query profiling. The hook runs
201
+ * synchronously inside Kysely's pipeline — keep it cheap and avoid
202
+ * blocking work.
203
+ */
204
+ onQuery(_sql, _params) { }
205
+ /**
206
+ * Invoked when an `exec()`-wrapped operation rejects, or when
207
+ * Kysely emits a query-level error. The `operation` label
208
+ * distinguishes the source (`'exec'`, `'query'`, or the value
209
+ * supplied by a subclass). Default no-op.
210
+ *
211
+ * The hook fires before {@link translateError}; the (possibly
212
+ * untranslated) error is still re-thrown to the caller.
213
+ */
214
+ onError(_operation, _error) { }
215
+ }
216
+ //# sourceMappingURL=DatabaseRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DatabaseRepository.js","sourceRoot":"","sources":["../../../../src/repository/base/DatabaseRepository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import { type Transaction } from 'kysely';
2
+ import { DatabaseRepository } from './DatabaseRepository.js';
3
+ /**
4
+ * Generalization for SQL Access Layer components that need explicit
5
+ * transaction control on top of {@link DatabaseRepository}.
6
+ *
7
+ * Extends `DatabaseRepository<Schema>` and adds {@link transaction},
8
+ * a helper that runs a callback inside a Kysely transaction:
9
+ * commits on success, rolls back on throw.
10
+ *
11
+ * Error translation flows through {@link DatabaseRepository.translateError}
12
+ * — overriding it once on a dialect subclass covers both `exec` and
13
+ * `transaction`.
14
+ *
15
+ * ──────────────────────────────────────────────────────────────────
16
+ * Overridable transaction hooks
17
+ * ──────────────────────────────────────────────────────────────────
18
+ * - {@link onTransactionStart} ← before the callback runs
19
+ * - {@link onTransactionCommit} ← after the callback resolves
20
+ * - {@link onTransactionRollback} ← when the callback throws
21
+ *
22
+ * Each hook receives a `txId` — a short opaque identifier that
23
+ * correlates the three events of the same transaction. The id is
24
+ * generated per-call and has no meaning to the database (it's a
25
+ * sibling of telemetry trace ids, not of SQL `SAVEPOINT` names).
26
+ *
27
+ * @typeParam Schema See {@link DatabaseRepository}.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { TransactionalDatabaseRepository } from '@xfarch/xf-sql'
32
+ *
33
+ * export class OrdersDb extends TransactionalDatabaseRepository<Schema> {
34
+ * constructor() { super({ dialect: ... }) }
35
+ * async init() { await super.init() }
36
+ * async terminate() { await super.terminate() }
37
+ *
38
+ * async checkout(order: Order) {
39
+ * return this.transaction(async (trx) => {
40
+ * await trx.insertInto('orders').values(order).execute()
41
+ * await trx.updateTable('inventory')
42
+ * .set((eb) => ({ stock: eb('stock', '-', order.qty) }))
43
+ * .where('sku', '=', order.sku)
44
+ * .execute()
45
+ * return order.id
46
+ * })
47
+ * }
48
+ * }
49
+ * ```
50
+ */
51
+ export declare abstract class TransactionalDatabaseRepository<Schema = unknown> extends DatabaseRepository<Schema> {
52
+ /**
53
+ * Run `callback` inside a database transaction. Commits if the
54
+ * callback resolves; rolls back if it throws.
55
+ *
56
+ * The argument passed to `callback` is a `Transaction<Schema>` —
57
+ * a Kysely-typed transaction handle. Use it (not `this.db`) for
58
+ * queries inside the transaction so they participate in the same
59
+ * unit of work.
60
+ *
61
+ * Errors thrown inside the callback are translated through
62
+ * {@link DatabaseRepository.translateError} before being rethrown.
63
+ */
64
+ protected transaction<R>(callback: (trx: Transaction<Schema>) => Promise<R>): Promise<R>;
65
+ /**
66
+ * Invoked just before the transaction callback runs. Default no-op.
67
+ * `txId` is a short opaque identifier that correlates the three
68
+ * events of the same transaction.
69
+ */
70
+ protected onTransactionStart(_txId: string): void;
71
+ /**
72
+ * Invoked after the transaction callback resolves successfully and
73
+ * the commit completes. `durationMs` measures the full lifetime of
74
+ * the transaction (including commit latency). Default no-op.
75
+ */
76
+ protected onTransactionCommit(_txId: string, _durationMs: number): void;
77
+ /**
78
+ * Invoked when the transaction callback throws and the rollback
79
+ * completes. `reason` is the original error (before
80
+ * {@link DatabaseRepository.translateError} is applied to the
81
+ * rethrow). Default no-op.
82
+ */
83
+ protected onTransactionRollback(_txId: string, _reason: unknown): void;
84
+ private static makeTxId;
85
+ }
86
+ //# sourceMappingURL=TransactionalDatabaseRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransactionalDatabaseRepository.d.ts","sourceRoot":"","sources":["../../../../src/repository/base/TransactionalDatabaseRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,8BAAsB,+BAA+B,CAAC,MAAM,GAAG,OAAO,CACpE,SAAQ,kBAAkB,CAAC,MAAM,CAAC;IAElC;;;;;;;;;;;OAWG;cACa,WAAW,CAAC,CAAC,EAC3B,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACjD,OAAO,CAAC,CAAC,CAAC;IAgBb;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAEjD;;;;OAIG;IACH,SAAS,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAEvE;;;;;OAKG;IACH,SAAS,CAAC,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAItE,OAAO,CAAC,MAAM,CAAC,QAAQ;CAKxB"}
@@ -0,0 +1,104 @@
1
+ import { DatabaseRepository } from './DatabaseRepository.js';
2
+ /**
3
+ * Generalization for SQL Access Layer components that need explicit
4
+ * transaction control on top of {@link DatabaseRepository}.
5
+ *
6
+ * Extends `DatabaseRepository<Schema>` and adds {@link transaction},
7
+ * a helper that runs a callback inside a Kysely transaction:
8
+ * commits on success, rolls back on throw.
9
+ *
10
+ * Error translation flows through {@link DatabaseRepository.translateError}
11
+ * — overriding it once on a dialect subclass covers both `exec` and
12
+ * `transaction`.
13
+ *
14
+ * ──────────────────────────────────────────────────────────────────
15
+ * Overridable transaction hooks
16
+ * ──────────────────────────────────────────────────────────────────
17
+ * - {@link onTransactionStart} ← before the callback runs
18
+ * - {@link onTransactionCommit} ← after the callback resolves
19
+ * - {@link onTransactionRollback} ← when the callback throws
20
+ *
21
+ * Each hook receives a `txId` — a short opaque identifier that
22
+ * correlates the three events of the same transaction. The id is
23
+ * generated per-call and has no meaning to the database (it's a
24
+ * sibling of telemetry trace ids, not of SQL `SAVEPOINT` names).
25
+ *
26
+ * @typeParam Schema See {@link DatabaseRepository}.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { TransactionalDatabaseRepository } from '@xfarch/xf-sql'
31
+ *
32
+ * export class OrdersDb extends TransactionalDatabaseRepository<Schema> {
33
+ * constructor() { super({ dialect: ... }) }
34
+ * async init() { await super.init() }
35
+ * async terminate() { await super.terminate() }
36
+ *
37
+ * async checkout(order: Order) {
38
+ * return this.transaction(async (trx) => {
39
+ * await trx.insertInto('orders').values(order).execute()
40
+ * await trx.updateTable('inventory')
41
+ * .set((eb) => ({ stock: eb('stock', '-', order.qty) }))
42
+ * .where('sku', '=', order.sku)
43
+ * .execute()
44
+ * return order.id
45
+ * })
46
+ * }
47
+ * }
48
+ * ```
49
+ */
50
+ export class TransactionalDatabaseRepository extends DatabaseRepository {
51
+ /**
52
+ * Run `callback` inside a database transaction. Commits if the
53
+ * callback resolves; rolls back if it throws.
54
+ *
55
+ * The argument passed to `callback` is a `Transaction<Schema>` —
56
+ * a Kysely-typed transaction handle. Use it (not `this.db`) for
57
+ * queries inside the transaction so they participate in the same
58
+ * unit of work.
59
+ *
60
+ * Errors thrown inside the callback are translated through
61
+ * {@link DatabaseRepository.translateError} before being rethrown.
62
+ */
63
+ async transaction(callback) {
64
+ const txId = TransactionalDatabaseRepository.makeTxId();
65
+ const startedAt = Date.now();
66
+ this.onTransactionStart(txId);
67
+ try {
68
+ const result = await this.db.transaction().execute(callback);
69
+ this.onTransactionCommit(txId, Date.now() - startedAt);
70
+ return result;
71
+ }
72
+ catch (err) {
73
+ this.onTransactionRollback(txId, err);
74
+ throw this.translateError(err);
75
+ }
76
+ }
77
+ // ─── Overridable transaction hooks ────────────────────────
78
+ /**
79
+ * Invoked just before the transaction callback runs. Default no-op.
80
+ * `txId` is a short opaque identifier that correlates the three
81
+ * events of the same transaction.
82
+ */
83
+ onTransactionStart(_txId) { }
84
+ /**
85
+ * Invoked after the transaction callback resolves successfully and
86
+ * the commit completes. `durationMs` measures the full lifetime of
87
+ * the transaction (including commit latency). Default no-op.
88
+ */
89
+ onTransactionCommit(_txId, _durationMs) { }
90
+ /**
91
+ * Invoked when the transaction callback throws and the rollback
92
+ * completes. `reason` is the original error (before
93
+ * {@link DatabaseRepository.translateError} is applied to the
94
+ * rethrow). Default no-op.
95
+ */
96
+ onTransactionRollback(_txId, _reason) { }
97
+ // ─── Internals ────────────────────────────────────────────
98
+ static makeTxId() {
99
+ const t = Date.now().toString(36);
100
+ const r = Math.floor(Math.random() * 0x100000).toString(36).padStart(4, '0');
101
+ return `tx-${t}-${r}`;
102
+ }
103
+ }
104
+ //# sourceMappingURL=TransactionalDatabaseRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransactionalDatabaseRepository.js","sourceRoot":"","sources":["../../../../src/repository/base/TransactionalDatabaseRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,OAAgB,+BACpB,SAAQ,kBAA0B;IAElC;;;;;;;;;;;OAWG;IACO,KAAK,CAAC,WAAW,CACzB,QAAkD;QAElD,MAAM,IAAI,GAAG,+BAA+B,CAAC,QAAQ,EAAE,CAAA;QACvD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QAC5B,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC5D,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAA;YACtD,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YACrC,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IAED,6DAA6D;IAE7D;;;;OAIG;IACO,kBAAkB,CAAC,KAAa,IAAS,CAAC;IAEpD;;;;OAIG;IACO,mBAAmB,CAAC,KAAa,EAAE,WAAmB,IAAS,CAAC;IAE1E;;;;;OAKG;IACO,qBAAqB,CAAC,KAAa,EAAE,OAAgB,IAAS,CAAC;IAEzE,6DAA6D;IAErD,MAAM,CAAC,QAAQ;QACrB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QAC5E,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,CAAA;IACvB,CAAC;CACF"}
@@ -0,0 +1,192 @@
1
+ import { Repository } from '@xfcfam/xf';
2
+ import { Kysely, type Dialect } from 'kysely';
3
+ /**
4
+ * Configuration accepted by {@link DatabaseRepository}'s constructor.
5
+ *
6
+ * The `dialect` is the only required field. Dialect adapter packages
7
+ * such as `@xfcfam/xf-sql-postgres` build it for you behind a
8
+ * higher-level options shape.
9
+ */
10
+ export interface DatabaseOptions {
11
+ /** Kysely Dialect implementation (Postgres, MySQL, SQLite, …). */
12
+ dialect: Dialect;
13
+ }
14
+ /**
15
+ * Base Generalization for the Access Layer when the underlying
16
+ * external system is a SQL database.
17
+ *
18
+ * Encapsulates the [Kysely](https://kysely.dev) query builder behind
19
+ * a single XF-canonical class. The implementer's concrete Logical
20
+ * extends this (or a dialect-specific subclass such as
21
+ * `PostgresDatabaseRepository`) and exposes domain-meaningful methods
22
+ * that compose queries against `this.db`, a `Kysely<Schema>` bound
23
+ * to the implementer's typed schema.
24
+ *
25
+ * Concrete dialect support lives in adapter packages — install one of
26
+ * `@xfcfam/xf-sql-postgres`, `@xfcfam/xf-sql-mysql`, etc., or supply
27
+ * any Kysely-compatible `Dialect` directly.
28
+ *
29
+ * ──────────────────────────────────────────────────────────────────
30
+ * IMPORTANT — Required configuration for subclasses
31
+ * ──────────────────────────────────────────────────────────────────
32
+ * The underlying Kysely instance is held in a static private WeakMap
33
+ * and created inside {@link init}. If a subclass overrides `init()` /
34
+ * `terminate()`, it MUST chain through `super`:
35
+ *
36
+ * async init() { await super.init(); // own setup }
37
+ * async terminate() { // own teardown; await super.terminate() }
38
+ *
39
+ * Forgetting `super.init()` leaves `this.db` uninitialised and every
40
+ * query will throw {@link NotInitializedException}.
41
+ *
42
+ * ──────────────────────────────────────────────────────────────────
43
+ * Overridable observation hooks
44
+ * ──────────────────────────────────────────────────────────────────
45
+ * - {@link onConnected} ← after `init()` creates the Kysely instance
46
+ * - {@link onDisconnected} ← after `terminate()` destroys it
47
+ * - {@link onQuery} ← for every query Kysely executes
48
+ * - {@link onError} ← for every {@link exec}-wrapped operation that rejects
49
+ *
50
+ * Transaction hooks ({@link onTransactionStart} / Commit / Rollback)
51
+ * live on {@link TransactionalDatabaseRepository}, the subclass that
52
+ * exposes explicit transaction control.
53
+ *
54
+ * ──────────────────────────────────────────────────────────────────
55
+ * Error translation
56
+ * ──────────────────────────────────────────────────────────────────
57
+ * Dialect-specific errors (driver Error objects) are not translated
58
+ * by this class — it does not know about Postgres SQLSTATEs, MySQL
59
+ * error numbers, etc. Use a dialect adapter subclass (such as
60
+ * `PostgresDatabaseRepository`) or override {@link translateError}
61
+ * yourself to map errors to the typed Exceptions exported from this
62
+ * package (`UniqueViolationException`, etc.).
63
+ *
64
+ * @typeParam Schema Implementer-defined TypeScript interface mapping
65
+ * table names to their column shapes. See the
66
+ * Kysely docs for the conventions on
67
+ * `Generated`, `ColumnType`, etc.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { DatabaseRepository } from '@xfcfam/xf-sql'
72
+ * import { PostgresDialect } from 'kysely'
73
+ * import { Pool } from 'pg'
74
+ *
75
+ * interface Schema {
76
+ * users: { id: number; name: string; email: string }
77
+ * }
78
+ *
79
+ * export class UsersDb extends DatabaseRepository<Schema> {
80
+ * constructor() {
81
+ * super({
82
+ * dialect: new PostgresDialect({
83
+ * pool: new Pool({ connectionString: process.env.DATABASE_URL }),
84
+ * }),
85
+ * })
86
+ * }
87
+ * async init() { await super.init() }
88
+ * async terminate() { await super.terminate() }
89
+ *
90
+ * getUser(id: number) {
91
+ * return this.db
92
+ * .selectFrom('users')
93
+ * .where('id', '=', id)
94
+ * .selectAll()
95
+ * .executeTakeFirstOrThrow()
96
+ * }
97
+ * }
98
+ * ```
99
+ */
100
+ export declare abstract class DatabaseRepository<Schema = unknown> extends Repository<null> {
101
+ private static readonly state;
102
+ /** Options provided at construction time. */
103
+ protected readonly options: DatabaseOptions;
104
+ constructor(options: DatabaseOptions);
105
+ /**
106
+ * Typed Kysely instance bound to `Schema`. Use it to compose queries:
107
+ * `this.db.selectFrom('users')…`, `this.db.insertInto('users')…`.
108
+ * Throws if {@link init} has not been called.
109
+ */
110
+ protected get db(): Kysely<Schema>;
111
+ /**
112
+ * Subclasses that override this method MUST call `await super.init()`
113
+ * **first**; otherwise {@link db} will throw on use.
114
+ */
115
+ init(): Promise<void>;
116
+ /**
117
+ * Subclasses that override this method MUST call `await super.terminate()`
118
+ * **last** to release the connection pool held by the Kysely instance.
119
+ */
120
+ terminate(): Promise<void>;
121
+ /**
122
+ * Hook for dialect-specific error translation. Default
123
+ * implementation is identity (returns the input unchanged).
124
+ *
125
+ * Dialect adapter subclasses (such as `PostgresDatabaseRepository`)
126
+ * override this to map driver errors to the typed Exceptions
127
+ * exported from this package.
128
+ *
129
+ * Called automatically by {@link exec}; not called for direct
130
+ * `this.db.…` usage, where the implementer is responsible for
131
+ * wrapping queries in `this.exec(...)`.
132
+ */
133
+ protected translateError(err: unknown): unknown;
134
+ /**
135
+ * Execute an operation against the database, translating any
136
+ * dialect-specific errors to xf-sql Exception types via
137
+ * {@link translateError}.
138
+ *
139
+ * Use this whenever you need automatic error translation. For raw
140
+ * Kysely access without translation, use {@link db} directly.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * async createUser(input: UserInput) {
145
+ * return this.exec(() =>
146
+ * this.db.insertInto('users').values(input).returningAll().executeTakeFirstOrThrow()
147
+ * )
148
+ * }
149
+ * ```
150
+ */
151
+ protected exec<R>(op: () => Promise<R>): Promise<R>;
152
+ /**
153
+ * Invoked after `init()` has created the Kysely instance. Default
154
+ * no-op. Override for connection-open telemetry, schema migrations,
155
+ * connection warmup, etc.
156
+ */
157
+ protected onConnected(): Promise<void>;
158
+ /**
159
+ * Invoked after `terminate()` has destroyed the Kysely instance.
160
+ * Default no-op. Override for connection-close telemetry.
161
+ */
162
+ protected onDisconnected(): Promise<void>;
163
+ /**
164
+ * Invoked for every query Kysely executes (both standalone and
165
+ * inside transactions). Receives the compiled SQL and the bound
166
+ * parameters. Default no-op.
167
+ *
168
+ * Use for audit logging or query profiling. The hook runs
169
+ * synchronously inside Kysely's pipeline — keep it cheap and avoid
170
+ * blocking work.
171
+ */
172
+ protected onQuery(_sql: string, _params: readonly unknown[]): void;
173
+ /**
174
+ * Invoked when an `exec()`-wrapped operation rejects, or when
175
+ * Kysely emits a query-level error. The `operation` label
176
+ * distinguishes the source (`'exec'`, `'query'`, or the value
177
+ * supplied by a subclass). Default no-op.
178
+ *
179
+ * The hook fires before {@link translateError}; the (possibly
180
+ * untranslated) error is still re-thrown to the caller.
181
+ *
182
+ * The hook may be `async` — the `exec()` call site `await`s it, so
183
+ * an async override is fully observed before the error is rethrown.
184
+ * Note: the Kysely log callback (`'query'` source) is synchronous by
185
+ * Kysely's API contract and cannot `await` the hook; async overrides
186
+ * that target only that source will execute fire-and-forget from that
187
+ * path. If reliable async observability of query-level Kysely errors
188
+ * is required, wrap the hook body with its own error handling.
189
+ */
190
+ protected onError(_operation: string, _error: unknown): void | Promise<void>;
191
+ }
192
+ //# sourceMappingURL=DatabaseRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DatabaseRepository.d.ts","sourceRoot":"","sources":["../../../../src/repository/general/DatabaseRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAA;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;CACjB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,8BAAsB,kBAAkB,CAAC,MAAM,GAAG,OAAO,CAAE,SAAQ,UAAU,CAAC,IAAI,CAAC;IACjF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAEpE,6CAA6C;IAC7C,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;gBAE/B,OAAO,EAAE,eAAe;IAKpC;;;;OAIG;IACH,SAAS,KAAK,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,CAMjC;IAED;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAShC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IAI/C;;;;;;;;;;;;;;;;OAgBG;cACa,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAWzD;;;;OAIG;cACa,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAE5C;;;OAGG;cACa,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/C;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI;IAElE;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAC7E"}