@woltz/rich-domain-drizzle 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/cjs/batch-executor.d.ts +30 -0
- package/dist/cjs/batch-executor.d.ts.map +1 -0
- package/dist/cjs/batch-executor.js +201 -0
- package/dist/cjs/batch-executor.js.map +1 -0
- package/dist/cjs/errors.d.ts +31 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +84 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.d.ts +8 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +34 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mappers/to-domain.d.ts +11 -0
- package/dist/cjs/mappers/to-domain.d.ts.map +1 -0
- package/dist/cjs/mappers/to-domain.js +15 -0
- package/dist/cjs/mappers/to-domain.js.map +1 -0
- package/dist/cjs/mappers/to-persistence.d.ts +47 -0
- package/dist/cjs/mappers/to-persistence.d.ts.map +1 -0
- package/dist/cjs/mappers/to-persistence.js +69 -0
- package/dist/cjs/mappers/to-persistence.js.map +1 -0
- package/dist/cjs/query-builder.d.ts +24 -0
- package/dist/cjs/query-builder.d.ts.map +1 -0
- package/dist/cjs/query-builder.js +146 -0
- package/dist/cjs/query-builder.js.map +1 -0
- package/dist/cjs/repository.d.ts +46 -0
- package/dist/cjs/repository.d.ts.map +1 -0
- package/dist/cjs/repository.js +192 -0
- package/dist/cjs/repository.js.map +1 -0
- package/dist/cjs/unit-of-work.d.ts +49 -0
- package/dist/cjs/unit-of-work.d.ts.map +1 -0
- package/dist/cjs/unit-of-work.js +94 -0
- package/dist/cjs/unit-of-work.js.map +1 -0
- package/dist/esm/batch-executor.d.ts +30 -0
- package/dist/esm/batch-executor.d.ts.map +1 -0
- package/dist/esm/batch-executor.js +196 -0
- package/dist/esm/batch-executor.js.map +1 -0
- package/dist/esm/errors.d.ts +31 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +75 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mappers/to-domain.d.ts +11 -0
- package/dist/esm/mappers/to-domain.d.ts.map +1 -0
- package/dist/esm/mappers/to-domain.js +11 -0
- package/dist/esm/mappers/to-domain.js.map +1 -0
- package/dist/esm/mappers/to-persistence.d.ts +47 -0
- package/dist/esm/mappers/to-persistence.d.ts.map +1 -0
- package/dist/esm/mappers/to-persistence.js +65 -0
- package/dist/esm/mappers/to-persistence.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/query-builder.d.ts +24 -0
- package/dist/esm/query-builder.d.ts.map +1 -0
- package/dist/esm/query-builder.js +142 -0
- package/dist/esm/query-builder.js.map +1 -0
- package/dist/esm/repository.d.ts +46 -0
- package/dist/esm/repository.d.ts.map +1 -0
- package/dist/esm/repository.js +188 -0
- package/dist/esm/repository.js.map +1 -0
- package/dist/esm/unit-of-work.d.ts +49 -0
- package/dist/esm/unit-of-work.d.ts.map +1 -0
- package/dist/esm/unit-of-work.js +87 -0
- package/dist/esm/unit-of-work.js.map +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/tsconfig.types.tsbuildinfo +1 -0
- package/dist/types/batch-executor.d.ts +30 -0
- package/dist/types/batch-executor.d.ts.map +1 -0
- package/dist/types/errors.d.ts +31 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/mappers/to-domain.d.ts +11 -0
- package/dist/types/mappers/to-domain.d.ts.map +1 -0
- package/dist/types/mappers/to-persistence.d.ts +47 -0
- package/dist/types/mappers/to-persistence.d.ts.map +1 -0
- package/dist/types/query-builder.d.ts +24 -0
- package/dist/types/query-builder.d.ts.map +1 -0
- package/dist/types/repository.d.ts +46 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/unit-of-work.d.ts +49 -0
- package/dist/types/unit-of-work.d.ts.map +1 -0
- package/package.json +69 -0
- package/src/batch-executor.ts +317 -0
- package/src/errors.ts +78 -0
- package/src/index.ts +37 -0
- package/src/mappers/to-domain.ts +13 -0
- package/src/mappers/to-persistence.ts +101 -0
- package/src/query-builder.ts +217 -0
- package/src/repository.ts +252 -0
- package/src/unit-of-work.ts +123 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Aggregate, Repository, Mapper, Criteria, PaginatedResult } from "@woltz/rich-domain";
|
|
2
|
+
import { DrizzleClient, DrizzleUnitOfWork } from "./unit-of-work";
|
|
3
|
+
import { DrizzleToPersistence } from "./mappers/to-persistence";
|
|
4
|
+
import { SearchableField } from "./query-builder";
|
|
5
|
+
export interface DrizzleRepositoryConfig<TDomain, TPersistence> {
|
|
6
|
+
db: DrizzleClient;
|
|
7
|
+
table: any;
|
|
8
|
+
toDomainMapper: Mapper<TPersistence, TDomain>;
|
|
9
|
+
toPersistenceMapper: DrizzleToPersistence<TDomain>;
|
|
10
|
+
uow: DrizzleUnitOfWork;
|
|
11
|
+
}
|
|
12
|
+
export declare abstract class DrizzleRepository<TDomain extends Aggregate<any>, TPersistence> extends Repository<TDomain> {
|
|
13
|
+
protected readonly db: DrizzleClient;
|
|
14
|
+
protected readonly table: any;
|
|
15
|
+
protected readonly toDomainMapper: Mapper<TPersistence, TDomain>;
|
|
16
|
+
protected readonly toPersistenceMapper: DrizzleToPersistence<TDomain>;
|
|
17
|
+
protected readonly uow: DrizzleUnitOfWork;
|
|
18
|
+
constructor(config: DrizzleRepositoryConfig<TDomain, TPersistence>);
|
|
19
|
+
/**
|
|
20
|
+
* Returns tx from UOWStorage if inside a transaction, otherwise the raw db.
|
|
21
|
+
*/
|
|
22
|
+
protected get context(): DrizzleClient;
|
|
23
|
+
/**
|
|
24
|
+
* The table name string used by EntitySchemaRegistry and db.query accessor.
|
|
25
|
+
*/
|
|
26
|
+
protected abstract get model(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Search conditions for full-text search via Criteria.search().
|
|
29
|
+
*/
|
|
30
|
+
protected abstract getSearchableFields(): SearchableField<TPersistence>[];
|
|
31
|
+
/**
|
|
32
|
+
* Relations to include when fetching (for Drizzle relational query API).
|
|
33
|
+
*/
|
|
34
|
+
protected getDefaultRelations(): Record<string, any>;
|
|
35
|
+
find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
|
|
36
|
+
findById(id: string): Promise<TDomain | null>;
|
|
37
|
+
findManyByIds(ids: string[]): Promise<TDomain[]>;
|
|
38
|
+
count(criteria?: Criteria<TDomain>): Promise<number>;
|
|
39
|
+
exists(id: string): Promise<boolean>;
|
|
40
|
+
save(entity: TDomain): Promise<void>;
|
|
41
|
+
delete(entity: TDomain): Promise<void>;
|
|
42
|
+
deleteById(id: string): Promise<void>;
|
|
43
|
+
transaction<T>(work: () => Promise<T>): Promise<T>;
|
|
44
|
+
private markArrayOfAggregateWithClean;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/repository.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,QAAQ,EACR,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAc,MAAM,gBAAgB,CAAC;AAE9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAuB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,MAAM,WAAW,uBAAuB,CAAC,OAAO,EAAE,YAAY;IAC5D,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,EAAE,GAAG,CAAC;IACX,cAAc,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC9C,mBAAmB,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACnD,GAAG,EAAE,iBAAiB,CAAC;CACxB;AAED,8BAAsB,iBAAiB,CACrC,OAAO,SAAS,SAAS,CAAC,GAAG,CAAC,EAC9B,YAAY,CACZ,SAAQ,UAAU,CAAC,OAAO,CAAC;IAC3B,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IACrC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC;IAC9B,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,mBAAmB,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACtE,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC;gBAE9B,MAAM,EAAE,uBAAuB,CAAC,OAAO,EAAE,YAAY,CAAC;IASlE;;OAEG;IACH,SAAS,KAAK,OAAO,IAAI,aAAa,CAGrC;IAED;;OAEG;IACH,SAAS,CAAC,QAAQ,KAAK,KAAK,IAAI,MAAM,CAAC;IAEvC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,mBAAmB,IAAI,eAAe,CAAC,YAAY,CAAC,EAAE;IAEzE;;OAEG;IACH,SAAS,CAAC,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAI9C,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAqDpE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IA8B7C,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IA4BhD,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBpD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQpC,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrC,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxD,OAAO,CAAC,6BAA6B;CAOtC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Drizzle database instance type.
|
|
4
|
+
* Generic to support pg, mysql, sqlite, and other drivers.
|
|
5
|
+
*/
|
|
6
|
+
export type DrizzleClient = {
|
|
7
|
+
transaction: <T>(fn: (tx: any) => Promise<T>) => Promise<T>;
|
|
8
|
+
query: Record<string, any>;
|
|
9
|
+
select: (...args: any[]) => any;
|
|
10
|
+
insert: (table: any) => any;
|
|
11
|
+
update: (table: any) => any;
|
|
12
|
+
delete: (table: any) => any;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
};
|
|
15
|
+
export type DrizzleTransactionClient = DrizzleClient;
|
|
16
|
+
export declare class DrizzleTransactionContext {
|
|
17
|
+
readonly client: DrizzleTransactionClient;
|
|
18
|
+
constructor(client: DrizzleTransactionClient);
|
|
19
|
+
}
|
|
20
|
+
export declare const UOWStorage: AsyncLocalStorage<{
|
|
21
|
+
ctx: DrizzleTransactionContext | null;
|
|
22
|
+
}>;
|
|
23
|
+
export declare class DrizzleUnitOfWork {
|
|
24
|
+
private readonly db;
|
|
25
|
+
constructor(db: DrizzleClient);
|
|
26
|
+
/**
|
|
27
|
+
* Get current transaction context (if any).
|
|
28
|
+
*/
|
|
29
|
+
getCurrentContext(): DrizzleTransactionContext | null;
|
|
30
|
+
/**
|
|
31
|
+
* Check if currently inside a transaction.
|
|
32
|
+
*/
|
|
33
|
+
isInTransaction(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Execute work inside a transaction.
|
|
36
|
+
* If already in a transaction, reuses the existing context (idempotent nesting).
|
|
37
|
+
*/
|
|
38
|
+
transaction<T>(work: () => Promise<T>): Promise<T>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decorator that wraps a method in a transaction.
|
|
42
|
+
* If already inside a transaction, reuses the existing one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function Transactional(inputUow?: DrizzleUnitOfWork): MethodDecorator;
|
|
45
|
+
/**
|
|
46
|
+
* Helper to get current transaction client from anywhere.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getCurrentDrizzleContext(): DrizzleTransactionClient | null;
|
|
49
|
+
//# sourceMappingURL=unit-of-work.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../src/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,aAAa,CAAC;AAErD,qBAAa,yBAAyB;aACR,MAAM,EAAE,wBAAwB;gBAAhC,MAAM,EAAE,wBAAwB;CAC7D;AAED,eAAO,MAAM,UAAU;SAChB,yBAAyB,GAAG,IAAI;EACnC,CAAC;AAEL,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,aAAa;IAE9C;;OAEG;IACH,iBAAiB,IAAI,yBAAyB,GAAG,IAAI;IAIrD;;OAEG;IACH,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACG,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAazD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,eAAe,CA2B3E;AAwBD;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,wBAAwB,GAAG,IAAI,CAE1E"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@woltz/rich-domain-drizzle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drizzle integration for Rich Domain Library",
|
|
5
|
+
"homepage": "https://woltz.mintlify.app",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"require": "./dist/cjs/index.js",
|
|
13
|
+
"import": "./dist/esm/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"author": "Tarcisio Andrade",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/tarcisioandrade/rich-domain/issues"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/tarcisioandrade/rich-domain.git",
|
|
27
|
+
"directory": "packages/rich-domain-drizzle"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"**/package.json"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "npm run build:cjs && npm run build:esm && npm run build:types && npm run postbuild",
|
|
36
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
37
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
38
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
39
|
+
"postbuild": "node postbuild.js",
|
|
40
|
+
"test": "echo 'No tests yet'",
|
|
41
|
+
"test:watch": "echo 'No tests yet'",
|
|
42
|
+
"check": "tsc -b --noEmit",
|
|
43
|
+
"coverage": "echo 'No coverage yet'",
|
|
44
|
+
"lint": "echo 'No linting yet'",
|
|
45
|
+
"clean": "rm -rf dist coverage node_modules *.tsbuildinfo",
|
|
46
|
+
"prepublishOnly": "npm run build",
|
|
47
|
+
"release": "standard-version",
|
|
48
|
+
"release:minor": "standard-version --release-as minor",
|
|
49
|
+
"release:major": "standard-version --release-as major",
|
|
50
|
+
"release:patch": "standard-version --release-as patch"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"ddd",
|
|
54
|
+
"domain-driven-design",
|
|
55
|
+
"drizzle",
|
|
56
|
+
"orm",
|
|
57
|
+
"repository",
|
|
58
|
+
"unit-of-work",
|
|
59
|
+
"typescript"
|
|
60
|
+
],
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@woltz/rich-domain": "^1.8.6",
|
|
63
|
+
"drizzle-orm": ">=0.45.2",
|
|
64
|
+
"typescript": ">=4.7.0"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22.12.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { eq, inArray, and } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
AggregateChanges,
|
|
4
|
+
EntitySchemaRegistry,
|
|
5
|
+
BatchDeleteOperation,
|
|
6
|
+
BatchCreateOperation,
|
|
7
|
+
BatchUpdateOperation,
|
|
8
|
+
BatchCreateItem,
|
|
9
|
+
} from "@woltz/rich-domain";
|
|
10
|
+
import { DrizzleClient } from "./unit-of-work";
|
|
11
|
+
import {
|
|
12
|
+
TableNotFoundError,
|
|
13
|
+
BatchOperationError,
|
|
14
|
+
MissingJunctionConfigError,
|
|
15
|
+
} from "./errors";
|
|
16
|
+
|
|
17
|
+
export interface DrizzleBatchExecutorConfig {
|
|
18
|
+
registry: EntitySchemaRegistry;
|
|
19
|
+
db: DrizzleClient;
|
|
20
|
+
tableMap: Map<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DrizzleBatchExecutor {
|
|
24
|
+
constructor(private readonly config: DrizzleBatchExecutorConfig) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute all batch operations in order:
|
|
28
|
+
* 1. Deletes (leaf → root, sorted by depth DESC)
|
|
29
|
+
* 2. Creates (root → leaf, sorted by depth ASC)
|
|
30
|
+
* 3. Updates (any order)
|
|
31
|
+
*/
|
|
32
|
+
async execute(changes: AggregateChanges): Promise<void> {
|
|
33
|
+
if (changes.isEmpty()) return;
|
|
34
|
+
|
|
35
|
+
const batch = changes.toBatchOperations();
|
|
36
|
+
|
|
37
|
+
await this.executeDeletes(batch.deletes);
|
|
38
|
+
await this.executeCreates(batch.creates);
|
|
39
|
+
await this.executeUpdates(batch.updates);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async executeDeletes(
|
|
43
|
+
deletes: Array<BatchDeleteOperation>
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const sorted = [...deletes].sort((a, b) => b.depth - a.depth);
|
|
46
|
+
|
|
47
|
+
for (const del of sorted) {
|
|
48
|
+
const { entity, ids, relationField, parentEntity, parentId } = del;
|
|
49
|
+
|
|
50
|
+
if (relationField && parentEntity) {
|
|
51
|
+
this.config.registry.validateRelationField(parentEntity, relationField);
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
this.config.registry.isReferenceCollection(
|
|
55
|
+
parentEntity,
|
|
56
|
+
relationField
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
await this.executeJunctionDelete(
|
|
60
|
+
parentEntity,
|
|
61
|
+
relationField,
|
|
62
|
+
parentId,
|
|
63
|
+
ids
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
await this.executeDeleteOwned(entity, ids);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
await this.executeDeleteOwned(entity, ids);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async executeDeleteOwned(
|
|
75
|
+
entity: string,
|
|
76
|
+
ids: string[]
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (ids.length === 0) return;
|
|
79
|
+
|
|
80
|
+
const table = this.getTable(entity);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await this.config.db.delete(table).where(inArray(table.id, ids));
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
throw new BatchOperationError(
|
|
86
|
+
"delete",
|
|
87
|
+
entity,
|
|
88
|
+
error.message || "Unknown error during deletion",
|
|
89
|
+
error
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async executeJunctionDelete(
|
|
95
|
+
parentEntity: string,
|
|
96
|
+
relationField: string,
|
|
97
|
+
parentId: string | undefined,
|
|
98
|
+
ids: string[]
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
if (ids.length === 0 || !parentId) return;
|
|
101
|
+
|
|
102
|
+
const junction = this.config.registry.getJunctionConfig(
|
|
103
|
+
parentEntity,
|
|
104
|
+
relationField
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!junction) {
|
|
108
|
+
throw new MissingJunctionConfigError(parentEntity, relationField);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const junctionTable = this.getJunctionTable(junction.table);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await this.config.db
|
|
115
|
+
.delete(junctionTable)
|
|
116
|
+
.where(
|
|
117
|
+
and(
|
|
118
|
+
eq(junctionTable[junction.sourceKey], parentId),
|
|
119
|
+
inArray(junctionTable[junction.targetKey], ids)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
throw new BatchOperationError(
|
|
124
|
+
"disconnect",
|
|
125
|
+
junction.table,
|
|
126
|
+
error.message || "Failed to delete from junction table",
|
|
127
|
+
error
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async executeCreates(
|
|
133
|
+
creates: Array<BatchCreateOperation>
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const sorted = [...creates].sort((a, b) => a.depth - b.depth);
|
|
136
|
+
|
|
137
|
+
for (const create of sorted) {
|
|
138
|
+
const { entity, items, relationField, parentEntity } = create;
|
|
139
|
+
|
|
140
|
+
if (relationField && parentEntity) {
|
|
141
|
+
this.config.registry.validateRelationField(parentEntity, relationField);
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
this.config.registry.isReferenceCollection(
|
|
145
|
+
parentEntity,
|
|
146
|
+
relationField
|
|
147
|
+
)
|
|
148
|
+
) {
|
|
149
|
+
await this.executeJunctionCreate(parentEntity, relationField, items);
|
|
150
|
+
} else {
|
|
151
|
+
await this.executeCreateOwned(entity, items);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
await this.executeCreateOwned(entity, items);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async executeCreateOwned(
|
|
160
|
+
entity: string,
|
|
161
|
+
items: Array<BatchCreateItem>
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
if (items.length === 0) return;
|
|
164
|
+
|
|
165
|
+
const table = this.getTable(entity);
|
|
166
|
+
|
|
167
|
+
const records = items.map((item) => {
|
|
168
|
+
const entityData = this.config.registry.mapEntity(entity, item.data);
|
|
169
|
+
const fk = item.parentId
|
|
170
|
+
? this.config.registry.getParentFk(entity, item.parentId)
|
|
171
|
+
: null;
|
|
172
|
+
|
|
173
|
+
return { ...entityData, ...fk };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await this.config.db.insert(table).values(records);
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
throw new BatchOperationError(
|
|
180
|
+
"create",
|
|
181
|
+
entity,
|
|
182
|
+
error.message || "Unknown error during creation",
|
|
183
|
+
error
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async executeJunctionCreate(
|
|
189
|
+
parentEntity: string,
|
|
190
|
+
relationField: string,
|
|
191
|
+
items: Array<BatchCreateItem>
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (items.length === 0) return;
|
|
194
|
+
|
|
195
|
+
const junction = this.config.registry.getJunctionConfig(
|
|
196
|
+
parentEntity,
|
|
197
|
+
relationField
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!junction) {
|
|
201
|
+
throw new MissingJunctionConfigError(parentEntity, relationField);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const junctionTable = this.getJunctionTable(junction.table);
|
|
205
|
+
|
|
206
|
+
const grouped = new Map<string, string[]>();
|
|
207
|
+
for (const item of items) {
|
|
208
|
+
const parentId = item.parentId;
|
|
209
|
+
if (!parentId) continue;
|
|
210
|
+
const entityId = this.extractId(item.data);
|
|
211
|
+
if (!entityId) continue;
|
|
212
|
+
|
|
213
|
+
if (!grouped.has(parentId)) {
|
|
214
|
+
grouped.set(parentId, []);
|
|
215
|
+
}
|
|
216
|
+
grouped.get(parentId)!.push(entityId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const records: Record<string, string>[] = [];
|
|
220
|
+
for (const [parentId, targetIds] of grouped) {
|
|
221
|
+
for (const targetId of targetIds) {
|
|
222
|
+
records.push({
|
|
223
|
+
[junction.sourceKey]: parentId,
|
|
224
|
+
[junction.targetKey]: targetId,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (records.length === 0) return;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await this.config.db
|
|
233
|
+
.insert(junctionTable)
|
|
234
|
+
.values(records)
|
|
235
|
+
.onConflictDoNothing();
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
throw new BatchOperationError(
|
|
238
|
+
"connect",
|
|
239
|
+
junction.table,
|
|
240
|
+
error.message || "Failed to insert into junction table",
|
|
241
|
+
error
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async executeUpdates(
|
|
247
|
+
updates: Array<BatchUpdateOperation>
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
for (const upd of updates) {
|
|
250
|
+
const table = this.getTable(upd.entity);
|
|
251
|
+
|
|
252
|
+
for (const item of upd.items) {
|
|
253
|
+
const mappedFields = this.config.registry.mapFields(
|
|
254
|
+
upd.entity,
|
|
255
|
+
item.changedFields
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (Object.keys(mappedFields).length > 0) {
|
|
259
|
+
try {
|
|
260
|
+
await this.config.db
|
|
261
|
+
.update(table)
|
|
262
|
+
.set(mappedFields)
|
|
263
|
+
.where(eq(table.id, item.id));
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
throw new BatchOperationError(
|
|
266
|
+
"update",
|
|
267
|
+
upd.entity,
|
|
268
|
+
error.message || "Failed to update entity",
|
|
269
|
+
error
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getTable(entityName: string): any {
|
|
278
|
+
const table = this.config.tableMap.get(entityName);
|
|
279
|
+
if (!table) {
|
|
280
|
+
throw new TableNotFoundError(
|
|
281
|
+
entityName,
|
|
282
|
+
Array.from(this.config.tableMap.keys())
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return table;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private getJunctionTable(tableName: string): any {
|
|
289
|
+
const table = this.config.tableMap.get(tableName);
|
|
290
|
+
if (!table) {
|
|
291
|
+
throw new TableNotFoundError(
|
|
292
|
+
tableName,
|
|
293
|
+
Array.from(this.config.tableMap.keys())
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return table;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private extractId(data: any): string | undefined {
|
|
300
|
+
if (!data) return undefined;
|
|
301
|
+
if (data.id?.value !== undefined && data.id?.value !== null) {
|
|
302
|
+
return String(data.id.value);
|
|
303
|
+
}
|
|
304
|
+
if (typeof data.id === "string") return data.id;
|
|
305
|
+
if (typeof data.id === "number") return String(data.id);
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function executeBatch(
|
|
311
|
+
db: DrizzleClient,
|
|
312
|
+
changes: AggregateChanges,
|
|
313
|
+
config: Omit<DrizzleBatchExecutorConfig, "db">
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const executor = new DrizzleBatchExecutor({ ...config, db });
|
|
316
|
+
await executor.execute(changes);
|
|
317
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export class DrizzleAdapterError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(`[DrizzleAdapter] ${message}`);
|
|
4
|
+
this.name = "DrizzleAdapterError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class TableNotFoundError extends DrizzleAdapterError {
|
|
9
|
+
constructor(
|
|
10
|
+
public readonly entityName: string,
|
|
11
|
+
public readonly availableTables: string[]
|
|
12
|
+
) {
|
|
13
|
+
super(
|
|
14
|
+
`Table for entity "${entityName}" not found in tableMap. ` +
|
|
15
|
+
`Available: ${availableTables.join(", ")}`
|
|
16
|
+
);
|
|
17
|
+
this.name = "TableNotFoundError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class NoRecordsAffectedError extends DrizzleAdapterError {
|
|
22
|
+
constructor(
|
|
23
|
+
public readonly operation: string,
|
|
24
|
+
public readonly entity: string,
|
|
25
|
+
public readonly id: string,
|
|
26
|
+
public readonly cause?: Error
|
|
27
|
+
) {
|
|
28
|
+
super(`${operation} on ${entity} (id: ${id}) affected 0 records`);
|
|
29
|
+
this.name = "NoRecordsAffectedError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class BatchOperationError extends DrizzleAdapterError {
|
|
34
|
+
constructor(
|
|
35
|
+
public readonly operation: string,
|
|
36
|
+
public readonly entity: string,
|
|
37
|
+
message: string,
|
|
38
|
+
public readonly cause?: Error
|
|
39
|
+
) {
|
|
40
|
+
super(`Batch ${operation} on ${entity} failed: ${message}`);
|
|
41
|
+
this.name = "BatchOperationError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class DrizzleRepositoryError extends DrizzleAdapterError {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public readonly cause?: Error
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "DrizzleRepositoryError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class MissingJunctionConfigError extends DrizzleAdapterError {
|
|
56
|
+
constructor(
|
|
57
|
+
public readonly parentEntity: string,
|
|
58
|
+
public readonly collectionField: string
|
|
59
|
+
) {
|
|
60
|
+
super(
|
|
61
|
+
`Collection "${collectionField}" on entity "${parentEntity}" is of type "reference" but has no junction configured. ` +
|
|
62
|
+
`Drizzle does not manage junction tables automatically — you must provide the junction config. ` +
|
|
63
|
+
`Example:\n` +
|
|
64
|
+
` collections: {\n` +
|
|
65
|
+
` ${collectionField}: {\n` +
|
|
66
|
+
` type: "reference",\n` +
|
|
67
|
+
` entity: "TargetEntity",\n` +
|
|
68
|
+
` junction: {\n` +
|
|
69
|
+
` table: "${parentEntity.toLowerCase()}_${collectionField}",\n` +
|
|
70
|
+
` sourceKey: "${parentEntity.toLowerCase()}Id",\n` +
|
|
71
|
+
` targetKey: "targetEntityId",\n` +
|
|
72
|
+
` },\n` +
|
|
73
|
+
` },\n` +
|
|
74
|
+
` }`
|
|
75
|
+
);
|
|
76
|
+
this.name = "MissingJunctionConfigError";
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Unit of Work
|
|
2
|
+
export {
|
|
3
|
+
DrizzleUnitOfWork,
|
|
4
|
+
DrizzleTransactionContext,
|
|
5
|
+
UOWStorage,
|
|
6
|
+
Transactional,
|
|
7
|
+
getCurrentDrizzleContext,
|
|
8
|
+
type DrizzleClient,
|
|
9
|
+
type DrizzleTransactionClient,
|
|
10
|
+
} from "./unit-of-work";
|
|
11
|
+
|
|
12
|
+
// Repository
|
|
13
|
+
export { DrizzleRepository, type DrizzleRepositoryConfig } from "./repository";
|
|
14
|
+
|
|
15
|
+
// Mappers
|
|
16
|
+
export { DrizzleToPersistence } from "./mappers/to-persistence";
|
|
17
|
+
export { DrizzleToDomain } from "./mappers/to-domain";
|
|
18
|
+
|
|
19
|
+
// Batch Executor
|
|
20
|
+
export {
|
|
21
|
+
DrizzleBatchExecutor,
|
|
22
|
+
executeBatch,
|
|
23
|
+
type DrizzleBatchExecutorConfig,
|
|
24
|
+
} from "./batch-executor";
|
|
25
|
+
|
|
26
|
+
// Query Builder
|
|
27
|
+
export { DrizzleQueryBuilder, type SearchableField } from "./query-builder";
|
|
28
|
+
|
|
29
|
+
// Errors
|
|
30
|
+
export {
|
|
31
|
+
DrizzleAdapterError,
|
|
32
|
+
TableNotFoundError,
|
|
33
|
+
NoRecordsAffectedError,
|
|
34
|
+
BatchOperationError,
|
|
35
|
+
DrizzleRepositoryError,
|
|
36
|
+
MissingJunctionConfigError,
|
|
37
|
+
} from "./errors";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Mapper } from "@woltz/rich-domain";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for mapping Drizzle query results to domain entities.
|
|
5
|
+
* Subclass and implement build() to transform DB records to domain aggregates.
|
|
6
|
+
*
|
|
7
|
+
* This is identical to a plain Mapper<TPersistence, TDomain>.
|
|
8
|
+
* Provided for naming consistency with the adapter pattern.
|
|
9
|
+
*/
|
|
10
|
+
export abstract class DrizzleToDomain<TPersistence, TDomain> extends Mapper<
|
|
11
|
+
TPersistence,
|
|
12
|
+
TDomain
|
|
13
|
+
> {}
|