@streetjs/repository 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.
@@ -0,0 +1,72 @@
1
+ # Architecture — @streetjs/repository
2
+
3
+ ## Purpose
4
+
5
+ `@streetjs/repository` is the StreetJS data-access base layer: a generic,
6
+ strongly-typed CRUD repository over a PostgreSQL pool, plus a small atomic
7
+ "ledger" transaction helper. Domain repositories in an application extend
8
+ `StreetPostgresRepository<T>` and supply a table name and a row mapper.
9
+
10
+ ## Dependencies
11
+
12
+ ```
13
+ @streetjs/pool (PgPool — the query/transaction/stream surface)
14
+ @streetjs/postgres (PgConnection + StreetPostgresWireStream types)
15
+ ```
16
+
17
+ No cyclic dependencies. Field-level encryption is decoupled via a structural
18
+ `FieldEncryptor` interface, so this package does **not** depend on the enterprise
19
+ data-policy module — any object with `encryptEntity`/`decryptEntity` qualifies
20
+ (the framework's `FieldEncryptor` class satisfies it structurally).
21
+
22
+ ## Design
23
+
24
+ ### Typed CRUD over parameterized SQL
25
+
26
+ `StreetPostgresRepository<T>` is abstract: subclasses declare `tableName` and
27
+ implement `mapRow(row)`. Every method issues **parameterized** queries
28
+ (`$1..$N`); values are never interpolated. `create`/`update` build column and
29
+ placeholder lists from the object's own keys, skipping `undefined` values.
30
+ `findAll` clamps `limit` into `[1, 1000]` and floors `offset` at `0` to bound
31
+ result sizes.
32
+
33
+ ### Table-name safety
34
+
35
+ The only identifier that can't be parameterized is the table name. It is
36
+ validated against `^[a-zA-Z_][a-zA-Z0-9_.]*$` **lazily on first query** (the
37
+ abstract property isn't available in the base constructor), so a mis-declared
38
+ subclass fails fast with a clear error instead of emitting injectable SQL.
39
+
40
+ ### Optional transparent encryption
41
+
42
+ When a subclass sets both `encryptor` (a `FieldEncryptor`) and `encryptedEntity`
43
+ (the annotated entity class), `create`/`update` run values through
44
+ `encryptEntity` before writing and `findById`/`findAll`/`create`/`update` run
45
+ returned rows through `decryptEntity`. When either is unset, both paths are
46
+ no-ops — zero overhead for repositories that don't opt in.
47
+
48
+ ### Transactions and streaming
49
+
50
+ `withTransaction(fn)` delegates to `pool.transaction`, giving `fn` a single
51
+ connection with BEGIN/COMMIT (ROLLBACK on throw). `streamAll(sql)` delegates to
52
+ `pool.stream` for backpressured reads; parameterized streaming is not yet
53
+ supported and throws synchronously to steer callers to `pool.query`.
54
+ `LedgerTransactionService.execute(ops, onSuccess?)` runs a list of operations and
55
+ an optional success callback within one transaction.
56
+
57
+ ## Testing
58
+
59
+ The suite runs with **no live database** using a fake pool that records queries
60
+ and returns scripted results. It covers all CRUD paths (including limit/offset
61
+ clamping, undefined-field skipping, empty-update fallthrough, and the
62
+ delete/insert edge cases), lazy table-name rejection, the encrypt-on-write /
63
+ decrypt-on-read cycle via a fake `FieldEncryptor`, streaming (both the
64
+ parameterized rejection and the delegation path), `withTransaction`, and the
65
+ ledger service with and without a success callback. Coverage is 100%.
66
+
67
+ ## Non-goals
68
+
69
+ - No query builder, joins, or relations — this is a single-table CRUD base.
70
+ - No schema management (see `@streetjs/migrations`).
71
+ - No connection management (see `@streetjs/pool`).
72
+ - No encryption implementation — only the structural hook to plug one in.
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@streetjs/repository` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0]
9
+
10
+ ### Added
11
+
12
+ - Initial standalone release of the StreetJS generic repository, extracted
13
+ verbatim from the `streetjs` core (`src/database/repository.ts`).
14
+ - `StreetPostgresRepository<T>` abstract base: typed `findById`, `findAll`
15
+ (with clamped pagination), `create`, `update`, `delete`, `count`,
16
+ `withTransaction`, and `streamAll`, all over parameterized SQL with lazy
17
+ table-name validation.
18
+ - Optional transparent field-level encryption via a structural `FieldEncryptor`
19
+ hook (`encryptor` + `encryptedEntity`), decoupled from the data-policy module.
20
+ - `LedgerTransactionService` for atomic multi-operation transactions.
21
+ - Public types: `IRepository<T>`, `FieldEncryptor`.
22
+ - Runs on `@streetjs/pool` and `@streetjs/postgres`; ESM. 19 tests (no live
23
+ database) with 100% coverage and a runnable example.
24
+
25
+ [1.0.0]: https://github.com/hassanmubiru/StreetJS/releases/tag/repository-v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 street contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @streetjs/repository
2
+
3
+ The StreetJS generic PostgreSQL repository: a typed CRUD base class over
4
+ [`@streetjs/pool`](https://www.npmjs.com/package/@streetjs/pool) with safe
5
+ identifier validation, pagination, streaming, transactions, and **optional
6
+ transparent field-level encryption**. Plus `LedgerTransactionService` for
7
+ running a sequence of operations atomically. ESM, strict-TypeScript.
8
+
9
+ This is the standalone home of the repository that also backs the
10
+ `streetjs/repository` subpath. The `streetjs` framework re-exports this package,
11
+ so there is a single source of truth.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @streetjs/repository @streetjs/pool
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { StreetPostgresRepository } from '@streetjs/repository';
23
+
24
+ interface User { id: string; name: string; email: string }
25
+
26
+ class UserRepository extends StreetPostgresRepository<User> {
27
+ protected readonly tableName = 'users';
28
+ protected mapRow(row: Record<string, string | null>): User {
29
+ return { id: row.id!, name: row.name!, email: row.email! };
30
+ }
31
+ }
32
+
33
+ const users = new UserRepository(pool); // a PgPool
34
+
35
+ await users.create({ id: '1', name: 'Ada', email: 'ada@x.dev' });
36
+ await users.findById('1');
37
+ await users.findAll(20, 0); // limit clamped to [1, 1000], offset floored at 0
38
+ await users.update('1', { name: 'Ada L.' });
39
+ await users.count();
40
+ await users.delete('1');
41
+ ```
42
+
43
+ Subclasses provide a `tableName` and a `mapRow` that turns a raw row into your
44
+ entity. All queries are parameterized; the table name is validated against
45
+ `^[a-zA-Z_][a-zA-Z0-9_.]*$` on first use to prevent SQL injection through a
46
+ mis-declared subclass.
47
+
48
+ ## Transactions & streaming
49
+
50
+ ```ts
51
+ // Run arbitrary work on a single connection in a transaction:
52
+ await users.withTransaction(async (conn) => {
53
+ await conn.query('UPDATE users SET name = $1 WHERE id = $2', ['Neo', '1']);
54
+ });
55
+
56
+ // Stream a large result set with backpressure:
57
+ const stream = await users.streamAll('SELECT * FROM users');
58
+ ```
59
+
60
+ `LedgerTransactionService` runs a list of operations and an optional success
61
+ callback atomically:
62
+
63
+ ```ts
64
+ import { LedgerTransactionService } from '@streetjs/repository';
65
+
66
+ await new LedgerTransactionService(pool).execute(
67
+ [
68
+ (conn) => conn.query('INSERT INTO ledger ...'),
69
+ (conn) => conn.query('UPDATE balances ...'),
70
+ ],
71
+ async () => 'committed',
72
+ );
73
+ ```
74
+
75
+ ## Transparent field-level encryption
76
+
77
+ Set `encryptor` and `encryptedEntity` on a subclass to automatically encrypt
78
+ `@Encrypt()`-annotated fields on write and decrypt them on read. The `encryptor`
79
+ is any object satisfying the `FieldEncryptor` interface
80
+ (`encryptEntity`/`decryptEntity`) — the framework's data-policy `FieldEncryptor`
81
+ qualifies:
82
+
83
+ ```ts
84
+ class SecureUserRepo extends StreetPostgresRepository<User> {
85
+ protected readonly tableName = 'users';
86
+ protected readonly encryptor = myFieldEncryptor;
87
+ protected readonly encryptedEntity = User;
88
+ protected mapRow(row) { /* ... */ }
89
+ }
90
+ ```
91
+
92
+ ## API
93
+
94
+ | Member | Description |
95
+ | ------ | ----------- |
96
+ | `findById(id)` | Row by id, or `null`. |
97
+ | `findAll(limit?, offset?)` | Paginated rows (`ORDER BY created_at DESC`). |
98
+ | `create(data)` | Insert non-`undefined` fields, return the row. |
99
+ | `update(id, data)` | Patch fields; empty patch is a no-op read. |
100
+ | `delete(id)` | `true` if a row was deleted. |
101
+ | `count()` | Row count. |
102
+ | `withTransaction(fn)` | Run `fn(conn)` in a transaction. |
103
+ | `streamAll(sql, params?)` | Stream rows (non-parameterized only, for now). |
104
+
105
+ ## Example
106
+
107
+ A complete runnable example lives in
108
+ [`src/examples/integration.ts`](./src/examples/integration.ts):
109
+
110
+ ```bash
111
+ npm run example -w packages/repository
112
+ ```
113
+
114
+ ## License
115
+
116
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @streetjs/repository — the StreetJS generic PostgreSQL repository.
3
+ *
4
+ * `StreetPostgresRepository<T>` is a typed CRUD base class over `@streetjs/pool`
5
+ * with safe identifier validation, pagination, streaming, transactions, and
6
+ * optional transparent field-level encryption. `LedgerTransactionService` runs a
7
+ * sequence of operations atomically. Public API only.
8
+ *
9
+ * ```ts
10
+ * import { StreetPostgresRepository } from '@streetjs/repository';
11
+ *
12
+ * class UserRepository extends StreetPostgresRepository<User> {
13
+ * protected readonly tableName = 'users';
14
+ * protected mapRow(row): User { return { id: row.id!, name: row.name! }; }
15
+ * }
16
+ * const users = new UserRepository(pool);
17
+ * await users.findById('42');
18
+ * ```
19
+ *
20
+ * > This is the standalone home of the repository that also backs the
21
+ * > `streetjs/repository` subpath; the `streetjs` framework re-exports it, so
22
+ * > there is a single implementation.
23
+ */
24
+ export { StreetPostgresRepository, LedgerTransactionService, } from './repository.js';
25
+ export type { IRepository, FieldEncryptor } from './repository.js';
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @streetjs/repository — the StreetJS generic PostgreSQL repository.
3
+ *
4
+ * `StreetPostgresRepository<T>` is a typed CRUD base class over `@streetjs/pool`
5
+ * with safe identifier validation, pagination, streaming, transactions, and
6
+ * optional transparent field-level encryption. `LedgerTransactionService` runs a
7
+ * sequence of operations atomically. Public API only.
8
+ *
9
+ * ```ts
10
+ * import { StreetPostgresRepository } from '@streetjs/repository';
11
+ *
12
+ * class UserRepository extends StreetPostgresRepository<User> {
13
+ * protected readonly tableName = 'users';
14
+ * protected mapRow(row): User { return { id: row.id!, name: row.name! }; }
15
+ * }
16
+ * const users = new UserRepository(pool);
17
+ * await users.findById('42');
18
+ * ```
19
+ *
20
+ * > This is the standalone home of the repository that also backs the
21
+ * > `streetjs/repository` subpath; the `streetjs` framework re-exports it, so
22
+ * > there is a single implementation.
23
+ */
24
+ export { StreetPostgresRepository, LedgerTransactionService, } from './repository.js';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { PgPool } from '@streetjs/pool';
2
+ import type { PgConnection, StreetPostgresWireStream } from '@streetjs/postgres';
3
+ export interface IRepository<T> {
4
+ findById(id: string): Promise<T | null>;
5
+ findAll(limit: number, offset: number): Promise<T[]>;
6
+ create(data: Partial<T>): Promise<T>;
7
+ update(id: string, data: Partial<T>): Promise<T | null>;
8
+ delete(id: string): Promise<boolean>;
9
+ count(): Promise<number>;
10
+ }
11
+ /**
12
+ * Minimal structural interface for transparent field-level encryption. The
13
+ * framework's `FieldEncryptor` (in `@streetjs/*` enterprise data-policy) satisfies
14
+ * it; declaring it structurally keeps this package free of a data-policy
15
+ * dependency while preserving the optional-encryption feature.
16
+ */
17
+ export interface FieldEncryptor {
18
+ encryptEntity<T extends Record<string, unknown>>(entityClass: new (...a: never[]) => unknown, obj: T): T;
19
+ decryptEntity<T extends Record<string, unknown>>(entityClass: new (...a: never[]) => unknown, obj: T): T;
20
+ }
21
+ export declare abstract class StreetPostgresRepository<T extends object> implements IRepository<T> {
22
+ protected readonly pool: PgPool;
23
+ protected abstract readonly tableName: string;
24
+ constructor(pool: PgPool);
25
+ /** Validate tableName on first use (abstract property not available in constructor) */
26
+ private _assertSafeTableName;
27
+ protected abstract mapRow(row: Record<string, string | null>): T;
28
+ /**
29
+ * Optional transparent field-level encryption. Subclasses that set both
30
+ * `encryptor` and `encryptedEntity` get automatic AES-256-GCM encryption of
31
+ * `@Encrypt()`-annotated fields on `create()`/`update()` and decryption on
32
+ * `findById()`/`findAll()`. Defaults to undefined (no encryption).
33
+ */
34
+ protected readonly encryptor?: FieldEncryptor;
35
+ protected readonly encryptedEntity?: new (...a: never[]) => unknown;
36
+ private _encrypt;
37
+ private _decrypt;
38
+ findById(id: string): Promise<T | null>;
39
+ findAll(limit?: number, offset?: number): Promise<T[]>;
40
+ count(): Promise<number>;
41
+ create(data: Partial<T>): Promise<T>;
42
+ update(id: string, data: Partial<T>): Promise<T | null>;
43
+ delete(id: string): Promise<boolean>;
44
+ /** Execute raw SQL within a transaction */
45
+ withTransaction<R>(fn: (conn: PgConnection) => Promise<R>): Promise<R>;
46
+ /** Stream rows with backpressure.
47
+ * Finding 6 fix: accepts parameterized queries only — raw SQL without
48
+ * params is still possible but callers should always use $1..$N placeholders.
49
+ * The method signature now accepts params to discourage raw interpolation.
50
+ */
51
+ streamAll(sql: string, params?: unknown[]): Promise<StreetPostgresWireStream>;
52
+ }
53
+ export declare class LedgerTransactionService {
54
+ private readonly pool;
55
+ constructor(pool: PgPool);
56
+ execute<T>(operations: Array<(conn: PgConnection) => Promise<void>>, onSuccess?: () => Promise<T>): Promise<T | void>;
57
+ }
58
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ // src/repository.ts
2
+ // Generic repository with typed CRUD, safe identifiers, streaming, transactions,
3
+ // and optional transparent field-level encryption.
4
+ // ─── Base repository ────────────────────────────────────────────────────────────
5
+ // Finding 6 fix: safe table/schema name pattern
6
+ const SAFE_TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
7
+ export class StreetPostgresRepository {
8
+ pool;
9
+ constructor(pool) {
10
+ this.pool = pool;
11
+ // Finding 6 fix: validate tableName at construction time so a bad subclass
12
+ // fails immediately rather than silently injecting SQL at query time.
13
+ // We defer the check to the first query because abstract properties are
14
+ // not yet initialised in the base constructor — use a lazy validator instead.
15
+ }
16
+ /** Validate tableName on first use (abstract property not available in constructor) */
17
+ _assertSafeTableName() {
18
+ if (!SAFE_TABLE_NAME_RE.test(this.tableName)) {
19
+ throw new Error(`Repository tableName contains unsafe characters: "${this.tableName}". ` +
20
+ 'Only letters, digits, underscores, and dots are allowed.');
21
+ }
22
+ }
23
+ /**
24
+ * Optional transparent field-level encryption. Subclasses that set both
25
+ * `encryptor` and `encryptedEntity` get automatic AES-256-GCM encryption of
26
+ * `@Encrypt()`-annotated fields on `create()`/`update()` and decryption on
27
+ * `findById()`/`findAll()`. Defaults to undefined (no encryption).
28
+ */
29
+ encryptor;
30
+ encryptedEntity;
31
+ _encrypt(data) {
32
+ if (!this.encryptor || !this.encryptedEntity)
33
+ return data;
34
+ return this.encryptor.encryptEntity(this.encryptedEntity, data);
35
+ }
36
+ _decrypt(entity) {
37
+ if (!this.encryptor || !this.encryptedEntity)
38
+ return entity;
39
+ return this.encryptor.decryptEntity(this.encryptedEntity, entity);
40
+ }
41
+ async findById(id) {
42
+ this._assertSafeTableName();
43
+ const result = await this.pool.query(`SELECT * FROM ${this.tableName} WHERE id = $1 LIMIT 1`, [id]);
44
+ if (result.rows.length === 0)
45
+ return null;
46
+ return this._decrypt(this.mapRow(result.rows[0]));
47
+ }
48
+ async findAll(limit = 20, offset = 0) {
49
+ this._assertSafeTableName();
50
+ const safeLimit = Math.min(Math.max(1, Math.floor(limit)), 1000);
51
+ const safeOffset = Math.max(0, Math.floor(offset));
52
+ const result = await this.pool.query(`SELECT * FROM ${this.tableName} ORDER BY created_at DESC LIMIT $1 OFFSET $2`, [safeLimit, safeOffset]);
53
+ return result.rows.map((r) => this._decrypt(this.mapRow(r)));
54
+ }
55
+ async count() {
56
+ this._assertSafeTableName();
57
+ const result = await this.pool.query(`SELECT COUNT(*) AS total FROM ${this.tableName}`);
58
+ return parseInt(result.rows[0]?.['total'] ?? '0', 10);
59
+ }
60
+ async create(data) {
61
+ this._assertSafeTableName();
62
+ data = this._encrypt(data);
63
+ const keys = Object.keys(data).filter((k) => data[k] !== undefined);
64
+ const columns = keys.map((k) => `"${k}"`).join(', ');
65
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
66
+ const params = keys.map((k) => data[k]);
67
+ const result = await this.pool.query(`INSERT INTO ${this.tableName} (${columns}) VALUES (${placeholders}) RETURNING *`, params);
68
+ const row = result.rows[0];
69
+ if (!row)
70
+ throw new Error('Insert returned no rows');
71
+ return this._decrypt(this.mapRow(row));
72
+ }
73
+ async update(id, data) {
74
+ this._assertSafeTableName();
75
+ if (Object.keys(data).length === 0)
76
+ return this.findById(id);
77
+ data = this._encrypt(data);
78
+ const entries = Object.entries(data).filter(([, v]) => v !== undefined);
79
+ const setClauses = entries.map(([k], i) => `"${k}" = $${i + 1}`).join(', ');
80
+ const params = entries.map(([, v]) => v);
81
+ params.push(id); // last parameter for WHERE id = $N
82
+ const result = await this.pool.query(`UPDATE ${this.tableName} SET ${setClauses} WHERE id = $${params.length} RETURNING *`, params);
83
+ if (result.rows.length === 0)
84
+ return null;
85
+ return this._decrypt(this.mapRow(result.rows[0]));
86
+ }
87
+ async delete(id) {
88
+ this._assertSafeTableName();
89
+ const result = await this.pool.query(`DELETE FROM ${this.tableName} WHERE id = $1`, [id]);
90
+ return result.command.startsWith('DELETE') && result.rowCount > 0;
91
+ }
92
+ /** Execute raw SQL within a transaction */
93
+ async withTransaction(fn) {
94
+ return this.pool.transaction(fn);
95
+ }
96
+ /** Stream rows with backpressure.
97
+ * Finding 6 fix: accepts parameterized queries only — raw SQL without
98
+ * params is still possible but callers should always use $1..$N placeholders.
99
+ * The method signature now accepts params to discourage raw interpolation.
100
+ */
101
+ streamAll(sql, params) {
102
+ if (params && params.length > 0) {
103
+ // Note: parameterized streaming requires wire-protocol changes planned for v2.x.
104
+ // Until then, use non-parameterized SQL for streaming or pool.query() for parameterized queries.
105
+ throw new Error('streamAll does not yet support parameterized queries — use pool.query() instead');
106
+ }
107
+ return this.pool.stream(sql);
108
+ }
109
+ }
110
+ // ─── ACID ledger service ────────────────────────────────────────────────────────
111
+ export class LedgerTransactionService {
112
+ pool;
113
+ constructor(pool) {
114
+ this.pool = pool;
115
+ }
116
+ async execute(operations, onSuccess) {
117
+ return this.pool.transaction(async (conn) => {
118
+ for (const op of operations) {
119
+ await op(conn);
120
+ }
121
+ if (onSuccess)
122
+ return onSuccess();
123
+ return undefined;
124
+ });
125
+ }
126
+ }
127
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,iFAAiF;AACjF,mDAAmD;AAiCnD,mFAAmF;AAEnF,gDAAgD;AAChD,MAAM,kBAAkB,GAAG,2BAA2B,CAAC;AAEvD,MAAM,OAAgB,wBAAwB;IAKb;IAA/B,YAA+B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QACzC,2EAA2E;QAC3E,sEAAsE;QACtE,wEAAwE;QACxE,8EAA8E;IAChF,CAAC;IAED,uFAAuF;IAC/E,oBAAoB;QAC1B,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CACb,qDAAqD,IAAI,CAAC,SAAS,KAAK;gBACxE,0DAA0D,CAC3D,CAAC;QACJ,CAAC;IACH,CAAC;IAID;;;;;OAKG;IACgB,SAAS,CAAkB;IAC3B,eAAe,CAAkC;IAE5D,QAAQ,CAAC,IAAgB;QAC/B,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QAC1D,OAAO,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,EAAE,IAA+B,CAAe,CAAC;IAC3G,CAAC;IAEO,QAAQ,CAAC,MAAS;QACxB,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,eAAe;YAAE,OAAO,MAAM,CAAC;QAC5D,OAAO,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,EAAE,MAAiC,CAAM,CAAC;IACpG,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,iBAAiB,IAAI,CAAC,SAAS,wBAAwB,EACvD,CAAC,EAAE,CAAC,CACL,CAAC;QACF,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAkC,CAAC,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,EAAE,MAAM,GAAG,CAAC;QAClC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACjE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,iBAAiB,IAAI,CAAC,SAAS,8CAA8C,EAC7E,CAAC,SAAS,EAAE,UAAU,CAAC,CACxB,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAkC,CAAC,CAAC,CAAC,CAAC;IAChG,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,iCAAiC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACxF,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAgB;QAC3B,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAY,CAAC,KAAK,SAAS,CAAC,CAAC;QAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAY,CAAC,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,eAAe,IAAI,CAAC,SAAS,KAAK,OAAO,aAAa,YAAY,eAAe,EACjF,MAAM,CACP,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAoC,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,IAAgB;QACvC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC7D,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACxE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,mCAAmC;QACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,UAAU,IAAI,CAAC,SAAS,QAAQ,UAAU,gBAAgB,MAAM,CAAC,MAAM,cAAc,EACrF,MAAM,CACP,CAAC;QACF,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAkC,CAAC,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU;QACrB,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,eAAe,IAAI,CAAC,SAAS,gBAAgB,EAC7C,CAAC,EAAE,CAAC,CACL,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC;IACpE,CAAC;IAED,2CAA2C;IAC3C,KAAK,CAAC,eAAe,CAAI,EAAsC;QAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,GAAW,EAAE,MAAkB;QACvC,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,iFAAiF;YACjF,iGAAiG;YACjG,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAC;QACrG,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,mFAAmF;AAEnF,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAE7C,KAAK,CAAC,OAAO,CACX,UAAwD,EACxD,SAA4B;QAE5B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YAC1C,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;gBAC5B,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;YACD,IAAI,SAAS;gBAAE,OAAO,SAAS,EAAE,CAAC;YAClC,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@streetjs/repository",
3
+ "version": "1.0.0",
4
+ "description": "StreetJS generic PostgreSQL repository: a typed CRUD base class with safe identifier handling, pagination, streaming, transactions, and optional transparent field-level encryption. Built on @streetjs/pool.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/**/*.js",
16
+ "dist/**/*.js.map",
17
+ "dist/**/*.d.ts",
18
+ "dist/**/*.d.ts.map",
19
+ "!dist/**/*.test.js",
20
+ "!dist/**/*.test.js.map",
21
+ "!dist/**/*.test.d.ts",
22
+ "!dist/**/*.test.d.ts.map",
23
+ "!dist/tests/**",
24
+ "!dist/examples/**",
25
+ "README.md",
26
+ "ARCHITECTURE.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "c8": {
31
+ "include": ["dist/**/*.js"],
32
+ "exclude": ["dist/tests/**", "dist/examples/**"],
33
+ "reporter": ["text", "lcov"],
34
+ "all": true,
35
+ "check-coverage": true,
36
+ "lines": 90,
37
+ "functions": 90,
38
+ "branches": 90,
39
+ "statements": 90
40
+ },
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "test": "node --test dist/tests/*.test.js",
44
+ "coverage": "c8 node --test dist/tests/*.test.js",
45
+ "example": "node dist/examples/integration.js",
46
+ "lint": "tsc --noEmit",
47
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
48
+ },
49
+ "keywords": [
50
+ "streetjs",
51
+ "street-framework",
52
+ "repository",
53
+ "crud",
54
+ "postgresql",
55
+ "database",
56
+ "data-access",
57
+ "field-encryption",
58
+ "typescript",
59
+ "esm"
60
+ ],
61
+ "author": "street contributors",
62
+ "license": "MIT",
63
+ "homepage": "https://hassanmubiru.github.io/StreetJS/",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/hassanmubiru/StreetJS.git"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/hassanmubiru/StreetJS/issues"
70
+ },
71
+ "dependencies": {
72
+ "@streetjs/pool": "^1.0.0",
73
+ "@streetjs/postgres": "^1.0.0"
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^26.1.0",
77
+ "c8": "^11.0.0",
78
+ "typescript": "^6.0.3"
79
+ },
80
+ "peerDependencies": {
81
+ "typescript": ">=5.0.0"
82
+ },
83
+ "engines": {
84
+ "node": ">=22.0.0"
85
+ },
86
+ "publishConfig": {
87
+ "access": "public"
88
+ }
89
+ }