From 514bfca049d7ad04483253500434c337a1577078 Mon Sep 17 00:00:00 2001 From: mararok <5163714+Mararok@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:56:10 +0200 Subject: [PATCH] feat: add AOT domain discovery --- jest.config.ts | 8 +- src/Compiler/Jest/HcJestTransformer.ts | 108 +++++++-- .../Feature/AbstractFeatureTsTransformer.ts | 17 ++ .../FeatureInfraDomainTsTransformer.ts | 69 ++++++ .../Feature/FeatureModuleTsTransformer.ts | 64 ++++++ .../Feature/FeatureTsTransformer.ts | 89 ++++++++ .../{ => Transformer/Feature}/discoverer.ts | 4 +- .../Transformer/FeatureModuleTsTransformer.ts | 142 ------------ .../Transformer/ModuleClassTsTransformer.ts | 187 +++++++++++++++ .../Transformer/TsTransformerHelper.ts | 58 ++++- src/Compiler/transformer.ts | 19 +- src/HcModule.ts | 11 +- .../Domain/Generic/DomainInfraModuleHelper.ts | 55 +++++ .../Generic/EntityRepositoryDecorator.ts | 9 +- .../Domain/Generic/HcDomainInfraModule.ts | 8 + .../Domain/Generic/HcInfraDomainModule.ts | 88 -------- .../Manager/EntityRepositoryManager.ts | 7 +- .../Manager/EntityRepositoryManagerCommon.ts | 3 +- .../InfraAggregateRootRepositoryManager.ts | 9 +- .../Persistence/Domain/Generic/index.ts | 2 +- ...Module.ts => HcMemoryDomainInfraModule.ts} | 2 +- .../Domain/Memory/MemoryEntityPersister.ts | 2 + .../Persistence/Domain/Memory/index.ts | 2 +- src/Util/Feature/FeatureModuleDiscoverer.ts | 212 ++++++++++++++++++ src/Util/Feature/FeatureModuleMeta.ts | 106 +++++++++ src/Util/FeatureModuleDiscoverer.ts | 201 ----------------- src/Util/index.ts | 2 + .../src/Book/Application/Book/index.ts | 2 - .../libs/test-lib/src/Book/BookModule.ts | 7 +- .../test-lib/src/Book/Domain/Book/Book.ts | 11 +- .../test-lib/src/Book/Domain/Book/BookCopy.ts | 14 ++ .../src/Book/Domain/Book/BookRepository.ts | 6 + .../src/Book/Domain/Book/Shared/BookCopyId.ts | 4 + .../src/Book/Domain/Book/Shared/BookId.ts | 4 + .../src/Book/Domain/Book/Shared/index.ts | 2 + .../src/Book/Domain/BookDomainErrors.ts | 9 +- .../libs/test-lib/src/Book/Domain/index.ts | 3 +- ...astructureModule.ts => BookInfraModule.ts} | 9 +- .../Domain/Book/InfraBookCopyRepository.ts | 5 + .../Domain/Book/InfraBookRepository.ts | 6 + .../Domain/BookDomainInfraModule.ts | 14 ++ .../Account/Book/MemoryBookRepository.ts | 5 - .../Domain/Memory/Account/index.ts | 1 - .../Infrastructure/Domain/Memory/index.ts | 1 - test/helper/libs/test-lib/src/Book/index.ts | 6 +- .../helper/libs/test-lib/src/TestLibModule.ts | 3 +- test/helper/libs/test-lib/src/index.ts | 7 + test/helper/src/Test/Domain/Author/Author.ts | 17 -- .../Test/Domain/Author/AuthorRepository.ts | 4 - test/helper/src/Test/Domain/Author/Book.ts | 17 -- test/helper/src/Test/Domain/Author/index.ts | 3 - .../src/Test/Domain/TestDomainErrors.ts | 10 - test/helper/src/Test/Domain/index.ts | 2 - .../Account/Author/MemoryAuthorRepository.ts | 6 - .../Account/Author/MemoryBookRepository.ts | 5 - .../Persistence/Memory/Account/index.ts | 1 - .../Persistence/Memory/index.ts | 1 - .../Infrastructure/PrivateTestInfraModule.ts | 8 - test/helper/src/Test/index.ts | 1 - ...r.test.ts => FeatureTsTransformer.test.ts} | 21 +- .../__snapshots__/FeatureTsTransformer.js | 24 ++ ...snap => FeatureTsTransformer.test.ts.snap} | 10 +- .../Memory/MemoryDomainPersistance.test.ts | 59 +++-- .../Domain/Entity/EntityCollection.test.ts | 28 +-- .../AggregateRootRepositoryProxy.test.ts | 11 +- .../FeatureModuleDiscoverer.test.ts | 2 +- .../FeatureModuleDiscoverer.test.ts.snap | 25 ++- tsconfig.build.json | 4 + tsconfig.json | 5 +- 69 files changed, 1194 insertions(+), 673 deletions(-) create mode 100644 src/Compiler/Transformer/Feature/AbstractFeatureTsTransformer.ts create mode 100644 src/Compiler/Transformer/Feature/FeatureInfraDomainTsTransformer.ts create mode 100644 src/Compiler/Transformer/Feature/FeatureModuleTsTransformer.ts create mode 100644 src/Compiler/Transformer/Feature/FeatureTsTransformer.ts rename src/Compiler/{ => Transformer/Feature}/discoverer.ts (66%) delete mode 100644 src/Compiler/Transformer/FeatureModuleTsTransformer.ts create mode 100644 src/Compiler/Transformer/ModuleClassTsTransformer.ts create mode 100644 src/Infrastructure/Persistence/Domain/Generic/DomainInfraModuleHelper.ts create mode 100644 src/Infrastructure/Persistence/Domain/Generic/HcDomainInfraModule.ts delete mode 100644 src/Infrastructure/Persistence/Domain/Generic/HcInfraDomainModule.ts rename src/Infrastructure/Persistence/Domain/Memory/{HcInfraMemoryDomainModule.ts => HcMemoryDomainInfraModule.ts} (83%) create mode 100644 src/Util/Feature/FeatureModuleDiscoverer.ts create mode 100644 src/Util/Feature/FeatureModuleMeta.ts delete mode 100644 src/Util/FeatureModuleDiscoverer.ts create mode 100644 test/helper/libs/test-lib/src/Book/Domain/Book/BookCopy.ts create mode 100644 test/helper/libs/test-lib/src/Book/Domain/Book/BookRepository.ts create mode 100644 test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookCopyId.ts create mode 100644 test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookId.ts create mode 100644 test/helper/libs/test-lib/src/Book/Domain/Book/Shared/index.ts rename test/helper/libs/test-lib/src/Book/Infrastructure/{BookInfrastructureModule.ts => BookInfraModule.ts} (62%) create mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookCopyRepository.ts create mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookRepository.ts create mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/BookDomainInfraModule.ts delete mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/Book/MemoryBookRepository.ts delete mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/index.ts delete mode 100644 test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/index.ts create mode 100644 test/helper/libs/test-lib/src/index.ts delete mode 100644 test/helper/src/Test/Domain/Author/Author.ts delete mode 100644 test/helper/src/Test/Domain/Author/AuthorRepository.ts delete mode 100644 test/helper/src/Test/Domain/Author/Book.ts delete mode 100644 test/helper/src/Test/Domain/Author/index.ts delete mode 100644 test/helper/src/Test/Domain/TestDomainErrors.ts delete mode 100644 test/helper/src/Test/Domain/index.ts delete mode 100644 test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryAuthorRepository.ts delete mode 100644 test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryBookRepository.ts delete mode 100644 test/helper/src/Test/Infrastructure/Persistence/Memory/Account/index.ts delete mode 100644 test/helper/src/Test/Infrastructure/Persistence/Memory/index.ts delete mode 100644 test/helper/src/Test/Infrastructure/PrivateTestInfraModule.ts delete mode 100644 test/helper/src/Test/index.ts rename test/integration/Compiler/Transformer/{FeatureModuleTsTransformer.test.ts => FeatureTsTransformer.test.ts} (57%) create mode 100644 test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.js rename test/integration/Compiler/Transformer/__snapshots__/{FeatureModuleTsTransformer.test.ts.snap => FeatureTsTransformer.test.ts.snap} (68%) rename test/unit/Util/{ => Feature}/FeatureModuleDiscoverer.test.ts (81%) rename test/unit/Util/{ => Feature}/__snapshots__/FeatureModuleDiscoverer.test.ts.snap (64%) diff --git a/jest.config.ts b/jest.config.ts index cd3c0a6..8fe5177 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -8,15 +8,13 @@ const jestConfig: JestConfigWithTsJest = { preset: "ts-jest", runner: "groups", roots: [__dirname], - modulePaths: [__dirname], + modulePaths: [compilerOptions.baseUrl], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "" }), transform: { '^.+\\.ts?$': ['/lib/Compiler/Jest', { rootDir, - tsconfig: 'tsconfig.json', - astTransformers: { - before: [{ path: './lib/Compiler/transformer', options: { sourceRoot: rootDir + "/src" } }] - } + tsconfig: compilerOptions, + diagnostics: false, }] }, testMatch: ["/test/**/*.test.ts"], diff --git a/src/Compiler/Jest/HcJestTransformer.ts b/src/Compiler/Jest/HcJestTransformer.ts index 30d6401..ca114a5 100644 --- a/src/Compiler/Jest/HcJestTransformer.ts +++ b/src/Compiler/Jest/HcJestTransformer.ts @@ -1,37 +1,52 @@ -import { FeatureModuleDiscoverer } from '../../Util/FeatureModuleDiscoverer'; +import { FeatureModuleDiscoverer } from '../../Util/Feature/FeatureModuleDiscoverer'; import type { AsyncTransformer, TransformedSource } from '@jest/transform'; import { hash } from 'node:crypto'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; import { TsJestTransformer, type TsJestTransformerOptions, type TsJestTransformOptions } from 'ts-jest'; +import { TsTransfromerHelper } from '../Transformer/TsTransformerHelper'; +import ts from 'typescript'; +import { FeatureTsTransformer } from '../Transformer/Feature/FeatureTsTransformer'; +import { FsHelper } from '@/Util/Filesystem/FsHelper'; -export type HcJestTransformerOptions = TsJestTransformerOptions & { rootDir: string; }; +export type HcJestTransformerOptions = TsJestTransformerOptions & { rootDir: string; tmpDir: string; }; export const HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH = '@hexancore/core/compiler/transformer'; export class HcJestTransformer implements AsyncTransformer { private sourceRoot!: string; + private compilerOptions: any; private tsJestTransformer: TsJestTransformer; - private featureModuleDiscoveryHashMap: Map; + private featureTsTransformer!: FeatureTsTransformer; + private featuresHashMap: Map; + + private tmpDir!: string; protected constructor(options: HcJestTransformerOptions) { this.processOptions(options); this.tsJestTransformer = new TsJestTransformer(options); - this.featureModuleDiscoveryHashMap = new Map(); + this.featuresHashMap = new Map(); } private processOptions(options: HcJestTransformerOptions) { + this.compilerOptions = options.tsconfig; + options.rootDir = options.rootDir.replaceAll("\\", "/"); this.sourceRoot = options.rootDir + "/src"; options.tsconfig = options.tsconfig ?? `${options.rootDir}/tsconfig.test.json`; - options.astTransformers = options.astTransformers ?? ({}); - options.astTransformers.before = options.astTransformers.before ?? []; - - if (!options.astTransformers.before.find((t) => typeof t !== 'string' && (t.path === HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH || t.path === './lib/Compiler/transformer'))) { - options.astTransformers.before.push({ - path: HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH, - options: { - sourceRoot: this.sourceRoot - } - }); + } + + private setupTransformedTmpDir(options: TsJestTransformOptions): void { + if (this.tmpDir) { + return; + } + + const projectHash = hash('md5', this.sourceRoot); + + this.tmpDir = options.config.cacheDirectory + '/hcjest-' + projectHash; + this.tmpDir = FsHelper.normalizePathSep(this.tmpDir); + if (!existsSync(this.tmpDir)) { + mkdirSync(this.tmpDir, { recursive: true }); } } @@ -45,8 +60,10 @@ export class HcJestTransformer implements AsyncTransformer { - return this.tsJestTransformer.processAsync(sourceText, sourcePath, options as any) as any; + sourcePath = FsHelper.normalizePathSep(sourcePath); + const featureName = this.extractFeatureNameFromPath(sourcePath); + if (!featureName || !this.featureTsTransformer.supports(sourcePath, featureName)) { + return this.tsJestTransformer.processAsync(sourceText, sourcePath, options); + } + + return this.processFeatureSourceFile(featureName, sourceText, sourcePath, options); + } + + private processFeatureSourceFile(featureName: string, sourceText: string, sourcePath: string, options: TsJestTransformOptions): TransformedSource { + this.setupTransformedTmpDir(options); + + const inSourceFile = ts.createSourceFile( + sourcePath, + sourceText, + this.compilerOptions.target ?? ts.ScriptTarget.Latest + ); + + const transformed = ts.transform(inSourceFile, [(context: ts.TransformationContext) => (source) => this.featureTsTransformer.transform(source, context)], this.compilerOptions); + const outSourceFile = transformed.transformed[0]; + + const printed = TsTransfromerHelper.printFile(outSourceFile); + const tmpPath = this.tmpDir + '/' + featureName + '-' + hash('md5', sourcePath, 'hex').substring(0, 8) + '-' + path.basename(sourcePath); + writeFileSync(tmpPath, printed); + + const outTranspile = ts.transpileModule(printed, { + compilerOptions: this.compilerOptions, + fileName: tmpPath + }); + + const sourceMap = JSON.parse(outTranspile.sourceMapText!); + sourceMap.file = tmpPath; + sourceMap.sources = [tmpPath]; + + return { + code: outTranspile.outputText, + map: sourceMap + }; } public getCacheKey(sourceText: string, sourcePath: string, transformOptions: TsJestTransformOptions): string { @@ -80,13 +140,17 @@ export class HcJestTransformer implements AsyncTransformer ts.factory.createIdentifier(r.name)))), + ts.factory.createPropertyAssignment("domainErrors", ts.factory.createIdentifier(domainErrorsClassName)), + ]); + + const createMeta = TsTransfromerHelper.createConstStatement("HcDomainInfraModuleMetaExtra", ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(classIdentifier, methodIdentifier), + undefined, + [optionsObject] + )); + + return [ + TsTransfromerHelper.createConstStatement("HcDomainInfraAggrgateRootRepositories", + ts.factory.createArrayLiteralExpression(repos.map((r) => ts.factory.createIdentifier(r))) + ), + createMeta + ]; + }, + + extraMetaProvider() { + return ts.factory.createIdentifier("HcDomainInfraModuleMetaExtra"); + }, + source, + context + }); + } + + public supports(sourcefilePath: string, feature: FeatureModuleMeta): boolean { + return sourcefilePath.endsWith("DomainInfraModule.ts"); + } +} \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/FeatureModuleTsTransformer.ts b/src/Compiler/Transformer/Feature/FeatureModuleTsTransformer.ts new file mode 100644 index 0000000..0ad0323 --- /dev/null +++ b/src/Compiler/Transformer/Feature/FeatureModuleTsTransformer.ts @@ -0,0 +1,64 @@ +import ts from 'typescript'; +import type { FeatureApplicationMessageMeta, FeatureModuleMeta } from "../../../Util/Feature/FeatureModuleMeta"; +import { AbstractFeatureTsTransformer } from "./AbstractFeatureTsTransformer"; +import type { ProviderModuleMetaTransformDef } from '../ModuleClassTsTransformer'; + +/** + * Adding automatic injection of message handlers, services, infra module to `[Feature]Module` source. + * Less write, more fun ! + */ +export class FeatureModuleTsTransformer extends AbstractFeatureTsTransformer { + + public supports(sourcefilePath: string, feature: FeatureModuleMeta): boolean { + return sourcefilePath.endsWith(feature.name + "Module.ts"); + } + + public transform(feature: FeatureModuleMeta, source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile { + + const messageHandlersProviders: ProviderModuleMetaTransformDef[] = []; + messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.commands)); + messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.queries)); + messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.events)); + + return this.moduleClassTransformer.transform({ + imports: [], + meta: { + imports: [], + providers: [...messageHandlersProviders, ...this.createServiceProviders(feature)], + }, + source, + context + }); + } + + private createMessageHandlerProviders(messages: FeatureApplicationMessageMeta[]): ProviderModuleMetaTransformDef[] { + const providers: ProviderModuleMetaTransformDef[] = []; + for (const m of messages) { + const importPath = `./${m.path}/${m.handlerClassName}`; + providers.push({ + addToExports: false, + name: m.handlerClassName, + importFrom: importPath + }); + } + + return providers; + } + + private createServiceProviders(feature: FeatureModuleMeta): ProviderModuleMetaTransformDef[] { + const providers: ProviderModuleMetaTransformDef[] = []; + for (const s of feature.application.services) { + if (!s.isInjectable) { + continue; + } + + providers.push({ + addToExports: false, + name: s.className, + importFrom: `./${s.path}` + }); + } + + return providers; + } +} \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts b/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts new file mode 100644 index 0000000..44513e3 --- /dev/null +++ b/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts @@ -0,0 +1,89 @@ +import { FsHelper } from "@/Util/Filesystem/FsHelper"; +import { execFileSync } from "node:child_process"; +import ts from 'typescript'; +import type { FeatureModuleMap, FeatureModuleMeta } from "../../../Util/Feature/FeatureModuleMeta"; +import { TsTransfromerHelper, type ImportFromMapper } from "../TsTransformerHelper"; +import type { AbstractFeatureTsTransformer } from "./AbstractFeatureTsTransformer"; +import { FeatureInfraDomainModuleTsTransformer } from "./FeatureInfraDomainTsTransformer"; +import { FeatureModuleTsTransformer } from "./FeatureModuleTsTransformer"; +import path from "node:path"; +import { FeatureModuleDiscoverer } from "@/Util/Feature/FeatureModuleDiscoverer"; + +/** + * Adding automatic injection of message handlers, services, infra module to `[Feature]Module` source. + * Less write, more fun ! + */ +export class FeatureTsTransformer { + private transformers: AbstractFeatureTsTransformer[]; + + public constructor( + private sourceRoot: string, + private features: FeatureModuleMap, + importFromMapper: ImportFromMapper, + needFixImportAccess = true, + ) { + + this.transformers = [ + new FeatureInfraDomainModuleTsTransformer(importFromMapper, needFixImportAccess), + new FeatureModuleTsTransformer(importFromMapper, needFixImportAccess), + ]; + } + + public static create(sourceRoot: string, compilerDir?: string, features?: FeatureModuleMap, needFixImportAccess = true): FeatureTsTransformer { + sourceRoot = FsHelper.normalizePathSep(sourceRoot); + const importFromMapper = TsTransfromerHelper.createImportFromMapper(sourceRoot); + compilerDir = compilerDir ?? FsHelper.normalizePathSep(path.dirname(path.dirname(__dirname))); + features = features ?? this.loadFeaturesMapSync(sourceRoot, compilerDir); + return new this(sourceRoot, features, importFromMapper, needFixImportAccess); + } + + private static loadFeaturesMapSync(sourceRoot: string, compilerDir: string): Map { + // TS transformer can't use async, but we need async code and execSync allows run async code :) Hack of day ! + const output = execFileSync('node', [`${compilerDir}/Transformer/Feature/discoverer.js`, sourceRoot], { + windowsHide: true, + shell: false, + timeout: 60*1000 + }); + const features = JSON.parse(output.toString()); + return new Map(features); + } + + public transform(source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile { + const sourceFilePath = FsHelper.normalizePathSep(source.fileName); + const feature = this.getFeatureOfPath(sourceFilePath); + if (!feature) { + return source; + } + + const sourceFilePathWithoutRoot = sourceFilePath.substring(this.sourceRoot.length + 1); + for (const t of this.transformers) { + if (t.supports(sourceFilePathWithoutRoot, feature)) { + const transformed = t.transform(feature, source, context); + return transformed; + } + } + + return source; + } + + private getFeatureOfPath(sourceFilePath: string): FeatureModuleMeta | null { + const featureName = FeatureModuleDiscoverer.extractFeatureNameFromPath(this.sourceRoot, sourceFilePath); + if (!featureName) { + return null; + } + + return this.features.get(featureName) as FeatureModuleMeta; + } + + public supports(sourceFilePath: string, featureName: string): boolean { + const feature = this.features.get(featureName)!; + for (const t of this.transformers) { + if (t.supports(sourceFilePath, feature)) { + return true; + } + } + + return false; + + } +} \ No newline at end of file diff --git a/src/Compiler/discoverer.ts b/src/Compiler/Transformer/Feature/discoverer.ts similarity index 66% rename from src/Compiler/discoverer.ts rename to src/Compiler/Transformer/Feature/discoverer.ts index 99e7a73..f2435e5 100644 --- a/src/Compiler/discoverer.ts +++ b/src/Compiler/Transformer/Feature/discoverer.ts @@ -1,5 +1,5 @@ -import { FeatureModuleDiscoverer } from "../Util/FeatureModuleDiscoverer"; - +import { FeatureModuleDiscoverer } from "../../../Util/Feature/FeatureModuleDiscoverer"; +// node ./lib/Compiler/Transformer/Feature/discoverer.js ./test/helper/libs/test-lib/src async function run() { if (process.argv.length < 3) { throw new Error("Wrong execute, not given sourceRoot argument"); diff --git a/src/Compiler/Transformer/FeatureModuleTsTransformer.ts b/src/Compiler/Transformer/FeatureModuleTsTransformer.ts deleted file mode 100644 index cdb720f..0000000 --- a/src/Compiler/Transformer/FeatureModuleTsTransformer.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { execFileSync, execSync } from "node:child_process"; -import ts from 'typescript'; -import { type FeatureApplicationMessageInfo, type FeatureModuleDiscovery } from "../../Util/FeatureModuleDiscoverer"; -import { TsTransfromerHelper } from "./TsTransformerHelper"; - -/** - * Adding automatic injection of message handlers, services, infra module to `[Feature]Module` source. - * Less write, more fun ! - */ -export class FeatureModuleTsTransformer { - public constructor(private features: Map) { - } - - public static create(sourceRoot: string, compilerDir: string): FeatureModuleTsTransformer { - const features = this.loadFeaturesMapSync(sourceRoot, compilerDir); - return new this(features); - } - - private static loadFeaturesMapSync(sourceRoot: string, compilerDir: string): Map { - // TS transformer can't use async, but we need async code and execSync allows run async code :) Hack of day ! - const output = execFileSync('node', [`${compilerDir}/discoverer.js`, sourceRoot]); - const features = JSON.parse(output.toString()); - return new Map(features); - } - - public transform(featureName: string, source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile { - const feature = this.features.get(featureName); - if (!feature) { - throw new Error(`No found feature module info, name: ${featureName}`); - } - - let imported = false; - const currentImports = new Set(); - let importMap: Map; - - const visitor: ts.Visitor = (node) => { - if (ts.isImportDeclaration(node)) { - currentImports.add(TsTransfromerHelper.extractModuleSpecifierFromImportDeclaration(node)); - return node; - } - - if (!ts.isImportDeclaration(node) && !imported) { - imported = true; - importMap = this.createImportDeclarations(feature, currentImports); - if (ts.isClassDeclaration(node)) { - node = ts.visitEachChild(node, (n) => this.transformModuleDecorator(n, feature, importMap, context), context); - } - return [...importMap.values(), node]; - } - - if (ts.isClassDeclaration(node)) { - node = ts.visitEachChild(node, (n) => this.transformModuleDecorator(n, feature, importMap, context), context); - } - - return node; - - }; - - return ts.factory.updateSourceFile(source, ts.visitNodes( - source.statements, - visitor, - (node): node is ts.Statement => ts.isStatement(node)) - ); - } - - private createImportDeclarations(feature: FeatureModuleDiscovery, currentImports: Set) { - const importMap: Map = new Map(); - - this.addMessageHandlerImportDeclarations(feature.application.commands, currentImports, importMap); - this.addMessageHandlerImportDeclarations(feature.application.queries, currentImports, importMap); - this.addMessageHandlerImportDeclarations(feature.application.events, currentImports, importMap); - this.addServiceImportDeclarations(feature, currentImports, importMap); - this.addInfrastructureModuleImportDeclaration(feature, importMap); - - return importMap; - } - - private addMessageHandlerImportDeclarations(messages: FeatureApplicationMessageInfo[], currentImports: Set, importMap: Map) { - for (const m of messages) { - const importPath = `./${m.path}/${m.handlerClassName}`; - if (currentImports.has(importPath)) { - continue; - } - - importMap.set(m.handlerClassName, this.createImportDeclaration(m.handlerClassName, importPath)); - } - } - - private addServiceImportDeclarations(feature: FeatureModuleDiscovery, currentImports: Set, importMap: Map): void { - for (const s of feature.application.services) { - if (s.isInjectable) { - const importPath = `./${s.path}`; - if (currentImports.has(importPath)) { - continue; - } - - importMap.set(s.className, this.createImportDeclaration(s.className, importPath)); - } - } - } - private addInfrastructureModuleImportDeclaration(feature: FeatureModuleDiscovery, importMap: Map): void { - const infrastructureModuleImportDec = this.createImportDeclaration(feature.infrastructure.module.className, `./${feature.infrastructure.module.path}`); - importMap.set(feature.infrastructure.module.className, infrastructureModuleImportDec); - } - - private createImportDeclaration(importName: string, importPath: string): ts.ImportDeclaration { - const namedImports = ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(importName)) - ]); - const importFrom = ts.factory.createStringLiteral(importPath); - return ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause(false, undefined, namedImports), - importFrom - ); - } - - private transformModuleDecorator( - node: ts.Node, - feature: FeatureModuleDiscovery, - importMap: Map, - context: ts.TransformationContext - ) { - return TsTransfromerHelper.transformModuleDecoratorExpression(node, () => { - const imports = [ - feature.infrastructure.module.className - ]; - - const providers = [ - ...feature.application.commands.map((m) => m.handlerClassName), - ...feature.application.queries.map((m) => m.handlerClassName), - ...feature.application.events.map((m) => m.handlerClassName), - ...feature.application.services.filter((s) => s.isInjectable).map((s) => s.className) - ]; - const classIdentifierMapper = (className) => TsTransfromerHelper.createNamedImportAccess(importMap.get(className)!, className, context); - return { - imports: imports.map(classIdentifierMapper), - providers: providers.map(classIdentifierMapper), - }; - }); - } -} \ No newline at end of file diff --git a/src/Compiler/Transformer/ModuleClassTsTransformer.ts b/src/Compiler/Transformer/ModuleClassTsTransformer.ts new file mode 100644 index 0000000..ea08031 --- /dev/null +++ b/src/Compiler/Transformer/ModuleClassTsTransformer.ts @@ -0,0 +1,187 @@ +import ts from "typescript"; +import { TsTransfromerHelper, type ImportedIdentifierMapper, type ImportFromMapper } from "./TsTransformerHelper"; + +export type ImportDeclarationMap = Map; + +export interface AddImportTransformDef { + name: string; + importModuleSpecifier: string; +} + +export interface ImportModuleMetaTransformDef { + name: string; + importModuleSpecifier: string; + addToExports: boolean; +} + +export interface ProviderModuleMetaTransformDef { + name: string; + importFrom: string; + expression?: ts.Expression; + addToExports: boolean; +} + +export interface CreateImportDeclarationsOutput { + declarations: ImportDeclarationMap; + newDefImports: ImportModuleMetaTransformDef[]; + newDefProviders: ProviderModuleMetaTransformDef[]; + providerStatements: ts.VariableStatement[]; + extraMetaProvider?: ts.Identifier; +} + +export interface ModuleClassTsTransformOptions { + imports?: AddImportTransformDef[], + meta?: { + imports: ImportModuleMetaTransformDef[]; + providers: ProviderModuleMetaTransformDef[]; + }; + extraStatementProvider?(importedIdentifierMapper: ImportedIdentifierMapper): ts.Statement[]; + extraMetaProvider?(): ts.Identifier; + source: ts.SourceFile; + context: ts.TransformationContext; +} + +export class ModuleClassTsTransformer { + + protected importedIdentifierMapper: (importDec: ts.ImportDeclaration, name: string, context: ts.TransformationContext) => ts.PropertyAccessChain | ts.Identifier; + + public constructor(protected importFromMapper: ImportFromMapper, protected needFixImportAccess: boolean) { + this.importedIdentifierMapper = this.needFixImportAccess ? (importDec, name, context) => + TsTransfromerHelper.createNamedImportAccess(importDec, name, context) + : (_importDec, name, _context) => ts.factory.createIdentifier(name); + + } + + public transform(options: ModuleClassTsTransformOptions): ts.SourceFile { + let imported = false; + const currentImports = new Set(); + let importDeclarations: CreateImportDeclarationsOutput; + + const visitor: ts.Visitor = (node) => { + if (ts.isImportDeclaration(node)) { + currentImports.add(TsTransfromerHelper.extractModuleSpecifierFromImportDeclaration(node)); + return node; + } + + if (!imported) { + imported = true; + importDeclarations = this.createImportDeclarations(options, currentImports); + + if (ts.isClassDeclaration(node)) { + node = ts.visitEachChild(node, (childNode) => this.transformModuleDecorator(childNode, importDeclarations, options.context), options.context); + } + const importedIdentifierMapper = (className: string) => + this.importedIdentifierMapper(importDeclarations.declarations.get(className)!, className, options.context); + + let extraStatements: ts.Statement[] = []; + if (options.extraStatementProvider) { + extraStatements = options.extraStatementProvider(importedIdentifierMapper); + } + + return [...importDeclarations.declarations.values(), ...extraStatements, ...importDeclarations.providerStatements, node]; + } + + if (ts.isClassDeclaration(node)) { + node = ts.visitEachChild(node, (childNode) => this.transformModuleDecorator(childNode, importDeclarations, options.context), options.context); + } + + return node; + + }; + + return ts.factory.updateSourceFile(options.source, ts.visitNodes( + options.source.statements, + visitor, + (node): node is ts.Statement => ts.isStatement(node)) + ); + } + + private createImportDeclarations(options: ModuleClassTsTransformOptions, currentImports: Set): CreateImportDeclarationsOutput { + + const declarations: ImportDeclarationMap = new Map(); + const newMetaImports: ImportModuleMetaTransformDef[] = []; + const newMetaProviders: ProviderModuleMetaTransformDef[] = []; + const providerStatements: ts.VariableStatement[] = []; + + if (options.imports) { + for (const i of options.imports) { + const importModuleSpecifier = this.importFromMapper(i.importModuleSpecifier); + if (currentImports.has(importModuleSpecifier)) { + continue; + } + + declarations.set(i.name, TsTransfromerHelper.createImportDeclaration(i.name, importModuleSpecifier)); + } + + } + + if (options.meta) { + for (const d of options.meta.imports) { + const importModuleSpecifier = this.importFromMapper(d.importModuleSpecifier); + if (currentImports.has(importModuleSpecifier)) { + continue; + } + + declarations.set(d.name, TsTransfromerHelper.createImportDeclaration(d.name, importModuleSpecifier)); + newMetaImports.push(d); + } + + for (const d of options.meta.providers) { + const importModuleSpecifier = this.importFromMapper(d.importFrom); + if (currentImports.has(importModuleSpecifier)) { + continue; + } + + declarations.set(d.name, TsTransfromerHelper.createImportDeclaration(d.name, importModuleSpecifier)); + newMetaProviders.push(d); + if (d.expression) { + providerStatements.push(this.createProviderDeclarationStatement(d)); + } + } + } + + return { + declarations, + providerStatements, + newDefImports: newMetaImports, + newDefProviders: newMetaProviders, + extraMetaProvider: options.extraMetaProvider ? options.extraMetaProvider() : undefined + }; + } + + private transformModuleDecorator( + node: ts.Node, + importDeclarations: CreateImportDeclarationsOutput, + context: ts.TransformationContext + ) { + return TsTransfromerHelper.transformModuleDecoratorExpression(node, () => { + const imports = importDeclarations.newDefImports.map((i) => i.name); + + const classIdentifierMapper = (className: string) => + this.importedIdentifierMapper(importDeclarations.declarations.get(className)!, className, context); + + const providers: any[] = importDeclarations.newDefProviders.map(p => p.name).map(classIdentifierMapper); + const exports: any[] = importDeclarations.newDefProviders.filter((p) => p.addToExports).map(p => p.name).map(classIdentifierMapper); + + if (importDeclarations.extraMetaProvider) { + providers.push(this.getArrayPropertyAsSpread(importDeclarations.extraMetaProvider, 'providers')); + exports.push(this.getArrayPropertyAsSpread(importDeclarations.extraMetaProvider, 'exports')); + } + return { + imports: imports.map(classIdentifierMapper), + providers, + exports + }; + }); + } + + private createProviderDeclarationStatement(def: ProviderModuleMetaTransformDef): ts.VariableStatement { + const varName = def.name + "Provider"; + return TsTransfromerHelper.createConstStatement(varName, def.expression!); + } + + private getArrayPropertyAsSpread(identifier: ts.Identifier, property: string) { + return ts.factory.createSpreadElement( + ts.factory.createPropertyAccessChain(identifier, undefined, property)); + } +} \ No newline at end of file diff --git a/src/Compiler/Transformer/TsTransformerHelper.ts b/src/Compiler/Transformer/TsTransformerHelper.ts index 60c8617..ac7f55e 100644 --- a/src/Compiler/Transformer/TsTransformerHelper.ts +++ b/src/Compiler/Transformer/TsTransformerHelper.ts @@ -1,8 +1,14 @@ import { Module } from "@nestjs/common"; -import ts, { type TransformationContext } from "typescript"; +import ts from "typescript"; + +export type ImportFromMapper = (importFrom: string) => string; + +export type ImportedIdentifierMapper = (identifier: string) => ts.PropertyAccessChain | ts.Identifier; export class TsTransfromerHelper { + private static printer: ts.Printer; + public static getLastImportIndex(source: ts.SourceFile): number { let lastIndex: number = 0; @@ -38,9 +44,9 @@ export class TsTransfromerHelper { } public static transformModuleDecoratorExpression(node: ts.Node, moduleMetaProvider: () => ({ - providers?: ts.Expression[], + providers?: any[], imports?: ts.Expression[], - exports?: ts.Expression[], + exports?: any[], })): ts.Node { return TsTransfromerHelper.transformDecoratorCallOfType(node, Module, (decorator, decoratorExpression) => { const decoratorArguments = decoratorExpression.arguments; @@ -96,10 +102,9 @@ export class TsTransfromerHelper { return (node.moduleSpecifier as ts.StringLiteral).text; } - public static createNamedImportAccess(importDec: ts.ImportDeclaration, name: string, context: TransformationContext): ts.PropertyAccessChain | ts.Identifier { + public static createNamedImportAccess(importDec: ts.ImportDeclaration, name: string, context: ts.TransformationContext): ts.PropertyAccessChain | ts.Identifier { const opts = context.getCompilerOptions(); - - if (opts.module === ts.ModuleKind.Node16) { + if (opts.module === ts.ModuleKind.Node16 || opts.module === ts.ModuleKind.CommonJS) { return ts.factory.createPropertyAccessChain(ts.factory.getGeneratedNameForNode(importDec), undefined, name); } @@ -107,4 +112,45 @@ export class TsTransfromerHelper { } + public static createImportDeclaration(importName: string | string[], importPath: string): ts.ImportDeclaration { + importName = Array.isArray(importName) ? importName : [importName]; + + const namedImports = ts.factory.createNamedImports(importName.map(n => + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(n))) + ); + + const importFrom = ts.factory.createStringLiteral(importPath); + + return ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, undefined, namedImports), + importFrom + ); + } + + public static createConstStatement(name: string, initializer: ts.Expression, type?: ts.TypeNode): ts.VariableStatement { + return ts.factory.createVariableStatement([], ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration(name, undefined, type, initializer)], + ts.NodeFlags.Const, + )); + } + + public static createImportFromMapper(sourceRoot: string): ImportFromMapper { + const isInternalCoreTesting = sourceRoot.includes("/test/helper/libs"); + return isInternalCoreTesting ? (importFrom: string) => { + return importFrom.replace('@hexancore/core', '@');//FsHelper.normalizePathSep(process.cwd() + "/lib/index.js")); + } : (importFrom: string) => importFrom; + } + + public static printFile(source: ts.SourceFile): string { + if (!this.printer) { + this.printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + removeComments: false, + omitTrailingSemicolon: true, + }); + } + + return this.printer.printFile(source); + } } \ No newline at end of file diff --git a/src/Compiler/transformer.ts b/src/Compiler/transformer.ts index 7429713..8d7ec6d 100644 --- a/src/Compiler/transformer.ts +++ b/src/Compiler/transformer.ts @@ -1,17 +1,18 @@ -import { FeatureModuleDiscoverer } from '../Util/FeatureModuleDiscoverer'; -import ts from 'typescript'; -import { FeatureModuleTsTransformer } from './Transformer/FeatureModuleTsTransformer'; import { readFileSync } from 'fs'; import path from 'path'; +import ts from 'typescript'; import { FsHelper } from '../Util/Filesystem/FsHelper'; +import { FeatureTsTransformer } from './Transformer/Feature/FeatureTsTransformer'; let transformerProgramContext: { sourceRoot: string, transformPredicate: (sourceFile: ts.SourceFile) => boolean; - featureModuleTransformer: FeatureModuleTsTransformer; + featureTransformer: FeatureTsTransformer; }; -let pluginConfigContext: { sourceRoot?: string; }; +let pluginConfigContext: { + sourceRoot?: string; +}; function initTransformerContext(context: ts.TransformationContext) { if (!pluginConfigContext.sourceRoot) { @@ -19,11 +20,10 @@ function initTransformerContext(context: ts.TransformationContext) { } const sourceRoot = pluginConfigContext.sourceRoot = FsHelper.normalizePathSep(pluginConfigContext.sourceRoot); - const compilerDir = FsHelper.normalizePathSep(__dirname); transformerProgramContext = { sourceRoot, transformPredicate: (source: ts.SourceFile) => FsHelper.normalizePathSep(source.fileName).startsWith(sourceRoot), - featureModuleTransformer: FeatureModuleTsTransformer.create(sourceRoot, compilerDir) + featureTransformer: FeatureTsTransformer.create(sourceRoot) }; } @@ -37,10 +37,7 @@ export const factory = (_program: ts.Program, pluginConfig: any) => { return (source: ts.SourceFile): ts.SourceFile => { if (transformerProgramContext.transformPredicate(source)) { - const featureModuleName = FeatureModuleDiscoverer.extractFeatureNameFromPath(transformerProgramContext.sourceRoot, source.fileName); - if (featureModuleName) { - return transformerProgramContext.featureModuleTransformer.transform(featureModuleName, source, context); - } + return transformerProgramContext.featureTransformer.transform(source, context); } return source; diff --git a/src/HcModule.ts b/src/HcModule.ts index 074fdea..95b9d53 100644 --- a/src/HcModule.ts +++ b/src/HcModule.ts @@ -2,12 +2,12 @@ import { ConfigurableModuleBuilder, Global, Module, Provider, type DynamicModule import { AccountId, CurrentTime, LogicError } from '@hexancore/common'; import { HcAppConfigModule } from './Infrastructure/Config/HcAppConfigModule'; import { HcApplicationModule } from './Application'; -import { EntityPersisterFactoryManager } from './Infrastructure'; import { ClsModule } from 'nestjs-cls'; import { nanoid } from 'nanoid'; import { AccountContext } from './Infrastructure/Account/AccountContext'; import { ClsAccountContext } from './Infrastructure/Account/ClsAccountContext'; import { SimpleAccountContext } from './Infrastructure/Account/SimpleAccountContext'; +import { HcDomainInfraModule } from './Infrastructure/Persistence/Domain/Generic/HcDomainInfraModule'; export interface HcModuleOptions { } @@ -51,9 +51,9 @@ function createAccountContextProvider(options: HcModuleExtras): Provider { export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = new ConfigurableModuleBuilder() .setClassMethodName('forRoot') .setExtras({ cls: false, accountContext: undefined }, (def, extras) => { - def.imports = def.imports ? def.imports : []; + def.imports = def.imports ? def.imports : [HcDomainInfraModule]; def.providers = def.providers ? def.providers : []; - def.exports = def.exports ? def.exports : []; + def.exports = def.exports ? def.exports : [HcDomainInfraModule]; if (extras.cls) { def.imports.push(createClsModule()); @@ -72,12 +72,11 @@ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = n @Global() @Module({ - imports: [HcAppConfigModule, HcApplicationModule], + imports: [HcAppConfigModule, HcApplicationModule, HcDomainInfraModule], providers: [ CurrentTimeProvider, - EntityPersisterFactoryManager, ], - exports: [CurrentTime, EntityPersisterFactoryManager], + exports: [CurrentTime, HcDomainInfraModule], }) export class HcModule extends ConfigurableModuleClass { public static forRoot(options?: typeof OPTIONS_TYPE): DynamicModule { diff --git a/src/Infrastructure/Persistence/Domain/Generic/DomainInfraModuleHelper.ts b/src/Infrastructure/Persistence/Domain/Generic/DomainInfraModuleHelper.ts new file mode 100644 index 0000000..205a688 --- /dev/null +++ b/src/Infrastructure/Persistence/Domain/Generic/DomainInfraModuleHelper.ts @@ -0,0 +1,55 @@ +import type { DomainErrors } from "@hexancore/common"; +import { AggregateRootRepositoryManager, getAggregateRootRepositoryToken, type AggregateRootConstructor } from "../../../../Domain"; +import type { ModuleMetadata, Provider } from "@nestjs/common"; +import { InfraAggregateRootRepositoryManager } from "./Manager/InfraAggregateRootRepositoryManager"; +import { EntityPersisterFactoryManager } from "./Persister"; +import { AggregateRootRepositoryProxyHandler } from "./AggregateRootRepositoryProxyHandler"; + +const AGGREGATE_ROOT_REPOSITORY_PROVIDER_INJECT = [AggregateRootRepositoryManager]; +class AggregateRootRepositoryProvider { + public provide: string; + public inject: any[]; + public useFactory: (manager: AggregateRootRepositoryManager) => AggregateRootRepositoryProxyHandler; + + public constructor(root: AggregateRootConstructor) { + this.provide = getAggregateRootRepositoryToken(root); + this.inject = AGGREGATE_ROOT_REPOSITORY_PROVIDER_INJECT; + this.useFactory = (manager: AggregateRootRepositoryManager) => { + return AggregateRootRepositoryProxyHandler.create(manager, root); + }; + } +} + +export class DomainInfraModuleHelper { + public static createMeta(options: { + featureName: string, + domainErrors: DomainErrors, + aggregateRootCtrs: AggregateRootConstructor[]; + }): Required> { + const aggregateRootRepositoryProviders: Provider[] = options.aggregateRootCtrs.map( + r => new AggregateRootRepositoryProvider(r) + ); + + return { + providers: [ + AggregateRootRepositoryManager, + this.createInfraAggregateRootRepositoryManagerProvider(options.featureName, options.domainErrors), + ...aggregateRootRepositoryProviders + ], + exports: [ + AggregateRootRepositoryManager, + ...aggregateRootRepositoryProviders + ] + }; + } + + private static createInfraAggregateRootRepositoryManagerProvider(featureName: string, domainErrors: DomainErrors): Provider { + return { + provide: InfraAggregateRootRepositoryManager, + inject: [EntityPersisterFactoryManager], + useFactory: async (factoryManager: EntityPersisterFactoryManager) => { + return new InfraAggregateRootRepositoryManager(featureName, factoryManager, domainErrors); + } + }; + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Domain/Generic/EntityRepositoryDecorator.ts b/src/Infrastructure/Persistence/Domain/Generic/EntityRepositoryDecorator.ts index 95d1ef1..3eedd05 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/EntityRepositoryDecorator.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/EntityRepositoryDecorator.ts @@ -10,9 +10,8 @@ export type EntityRepositoryConstructor( entity: EntityConstructor, @@ -29,8 +28,8 @@ export type AggregateRootRepositoryConstructor( aggregateRoot: AggregateRootConstructor, diff --git a/src/Infrastructure/Persistence/Domain/Generic/HcDomainInfraModule.ts b/src/Infrastructure/Persistence/Domain/Generic/HcDomainInfraModule.ts new file mode 100644 index 0000000..a458259 --- /dev/null +++ b/src/Infrastructure/Persistence/Domain/Generic/HcDomainInfraModule.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { EntityPersisterFactoryManager } from './Persister/EntityPersisterFactoryManager'; + +@Module({ + providers: [EntityPersisterFactoryManager], + exports: [EntityPersisterFactoryManager], +}) +export class HcDomainInfraModule { } diff --git a/src/Infrastructure/Persistence/Domain/Generic/HcInfraDomainModule.ts b/src/Infrastructure/Persistence/Domain/Generic/HcInfraDomainModule.ts deleted file mode 100644 index 935ce56..0000000 --- a/src/Infrastructure/Persistence/Domain/Generic/HcInfraDomainModule.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { AggregateRootRepositoryManager, getAggregateRootRepositoryToken } from '../../../../Domain'; -import { HcAppModuleMeta } from '@/Util/ModuleHelper'; -import { ConfigurableModuleBuilder, DynamicModule, Module, ModuleMetadata } from '@nestjs/common'; -import { InfraAggregateRootRepositoryManager } from './Manager'; -import { EntityPersisterFactoryManager } from './Persister'; -import { DomainErrors, LogicError } from '@hexancore/common'; -import { glob } from 'glob'; -import path from 'path'; -import { AggregateRootRepositoryProxyHandler } from './AggregateRootRepositoryProxyHandler'; - -export interface HcInfraDomainModuleOptions { - moduleInfraDir: string; -} - -export type HcInfraDomainModuleExtra = Pick; - -async function importDomainErrors(moduleMeta: HcAppModuleMeta): Promise> { - const domainErrorsConst = moduleMeta.name + 'DomainErrors'; - const domainErrorsImportPath = moduleMeta.getDomainPath(domainErrorsConst); - const domainErrors = (await import(domainErrorsImportPath))[domainErrorsConst]; - if (!domainErrors) { - throw new LogicError( - `Not found DomainErrors for module: '+moduleMeta.name+' , ${domainErrorsImportPath} need export const ${domainErrorsConst} = DomainErrors(...)`, - ); - } - - return domainErrors; -} - -async function importRepositoriesImpl(moduleInfraDir: string) { - moduleInfraDir = moduleInfraDir.split(path.sep).join(path.posix.sep); - const pattern = 'Persistence/**/*Repository.{ts,js}'; - const files = await glob(pattern, { absolute: true, magicalBraces: true, cwd: moduleInfraDir }); - const imports = files.map((file) => import(path.resolve(file))); - await Promise.all(imports); -} - -const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder() - .setExtras( - { - imports: [], - providers: [], - exports: [], - }, - (definition, extras) => ({ - ...definition, - ...extras, - }), - ) - .build(); - -@Module({}) -export class HcInfraDomainModule extends ConfigurableModuleClass { - - public static async forFeature(options: typeof OPTIONS_TYPE): Promise { - const moduleMeta = HcAppModuleMeta.fromPath(options.moduleInfraDir); - const m = super.register(options); - - await importRepositoriesImpl(options.moduleInfraDir); - - m.providers!.push(AggregateRootRepositoryManager); - m.providers!.push({ - provide: InfraAggregateRootRepositoryManager, - inject: [EntityPersisterFactoryManager], - useFactory: async (factoryManager: EntityPersisterFactoryManager) => { - const domainErrors = await importDomainErrors(moduleMeta); - return new InfraAggregateRootRepositoryManager(moduleMeta, factoryManager, domainErrors); - }, - }); - m.exports!.push(AggregateRootRepositoryManager); - - - const aggregateRootMetas = InfraAggregateRootRepositoryManager.getModuleAggregateRootMetas(moduleMeta.name); - for (const meta of aggregateRootMetas) { - const provider = { - provide: getAggregateRootRepositoryToken(meta.entityClass), - useFactory: (manager: AggregateRootRepositoryManager) => { - return AggregateRootRepositoryProxyHandler.create(manager, meta.entityClass); - }, - inject: [AggregateRootRepositoryManager] - }; - m.providers?.push(provider); - m.exports?.push(provider); - } - - return m; - } -} diff --git a/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManager.ts b/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManager.ts index 5300595..6ba1204 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManager.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManager.ts @@ -5,6 +5,9 @@ import { EntityRepositoryConstructor } from '../EntityRepositoryDecorator'; import { EntityRepositoryManagerCommon } from './EntityRepositoryManagerCommon'; import { DOMAIN_ERRORS_PROPERTY } from '../AbstractEntityRepositoryCommon'; +/** + * @internal + */ export class EntityRepositoryManager extends EntityRepositoryManagerCommon< AnyEntityRepository, ConstructorParameters['length'], @@ -28,8 +31,8 @@ export class EntityRepositoryManager extends EntityRepositoryManagerCommon< protected getRepositoryConstructor(entityClass: EntityConstructor): new (...args: any[]) => any { const meta: EntityMeta = entityClass[ENTITY_META_PROPERTY]; - if (meta.module != this.module.name) { - throw new LogicError('Getting repository for other module entity: ' + meta.fullname); + if (meta.module != this.featureName) { + throw new LogicError(`Getting entity repository from other Feature: ${meta.fullname} in ${this.featureName}`); } const ctr = EntityRepositoryManager.__repositoryConstructors.get(meta.fullname); diff --git a/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManagerCommon.ts b/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManagerCommon.ts index 59bda7e..befa4dc 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManagerCommon.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/Manager/EntityRepositoryManagerCommon.ts @@ -2,7 +2,6 @@ import { DomainErrors, LogicError, TupleTail } from '@hexancore/common'; import { AbstractEntityRepositoryCommon } from '../AbstractEntityRepositoryCommon'; import { IEntityPersisterFactory } from '../Persister/IEntityPersisterFactory'; import { AbstractEntityCommon, ENTITY_COMMON_META } from '@/Domain'; -import { HcAppModuleMeta } from '@/Util/ModuleHelper'; export abstract class EntityRepositoryManagerCommon< R extends AbstractEntityRepositoryCommon, @@ -15,7 +14,7 @@ export abstract class EntityRepositoryManagerCommon< protected baseArgs: any[]; public constructor( - public readonly module: HcAppModuleMeta, + public readonly featureName: string, protected persisterFactory: IEntityPersisterFactory, protected errors: DomainErrors, ) { diff --git a/src/Infrastructure/Persistence/Domain/Generic/Manager/InfraAggregateRootRepositoryManager.ts b/src/Infrastructure/Persistence/Domain/Generic/Manager/InfraAggregateRootRepositoryManager.ts index a3de946..f35350d 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/Manager/InfraAggregateRootRepositoryManager.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/Manager/InfraAggregateRootRepositoryManager.ts @@ -6,7 +6,6 @@ import { IEntityPersisterFactory } from '../Persister'; import { EntityRepositoryManager } from './EntityRepositoryManager'; import { EntityRepositoryManagerCommon } from './EntityRepositoryManagerCommon'; import { DOMAIN_ERRORS_PROPERTY } from '../AbstractEntityRepositoryCommon'; -import { HcAppModuleMeta } from '@/Util/ModuleHelper'; export class InfraAggregateRootRepositoryManager extends EntityRepositoryManagerCommon< AnyAggregateRootRepository, @@ -21,18 +20,18 @@ export class InfraAggregateRootRepositoryManager extends EntityRepositoryManager protected entityRepositoryManager: EntityRepositoryManager; public constructor( - module: HcAppModuleMeta, + featureName: string, persisterFactory: IEntityPersisterFactory, errors: DomainErrors ) { - super(module, persisterFactory, errors); - this.entityRepositoryManager = new EntityRepositoryManager(module, persisterFactory, errors); + super(featureName, persisterFactory, errors); + this.entityRepositoryManager = new EntityRepositoryManager(featureName, persisterFactory, errors); this.baseArgs = [this.persisterFactory, this.entityRepositoryManager]; } protected getRepositoryConstructor(entityClass: AggregateRootConstructor): new (...args: any[]) => any { const meta: AggregateRootMeta = entityClass[ENTITY_META_PROPERTY]; - if (meta.module != this.module.name) { + if (meta.module != this.featureName) { throw new LogicError('Getting repository for other module aggregate root: ' + meta.fullname); } diff --git a/src/Infrastructure/Persistence/Domain/Generic/index.ts b/src/Infrastructure/Persistence/Domain/Generic/index.ts index d757bce..f7e642e 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/index.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/index.ts @@ -9,4 +9,4 @@ export * from './AggregateRootRepositoryProxyHandler'; export * from './Persister'; export * from './Manager'; -export * from './HcInfraDomainModule'; +export * from './DomainInfraModuleHelper'; \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Domain/Memory/HcInfraMemoryDomainModule.ts b/src/Infrastructure/Persistence/Domain/Memory/HcMemoryDomainInfraModule.ts similarity index 83% rename from src/Infrastructure/Persistence/Domain/Memory/HcInfraMemoryDomainModule.ts rename to src/Infrastructure/Persistence/Domain/Memory/HcMemoryDomainInfraModule.ts index 55114f2..c039421 100644 --- a/src/Infrastructure/Persistence/Domain/Memory/HcInfraMemoryDomainModule.ts +++ b/src/Infrastructure/Persistence/Domain/Memory/HcMemoryDomainInfraModule.ts @@ -5,4 +5,4 @@ import { MemoryEntityPersisterFactory } from './MemoryEntityPersisterFactory'; providers: [MemoryEntityPersisterFactory], exports: [MemoryEntityPersisterFactory], }) -export class HcInfraMemoryDomainModule {} +export class HcMemoryDomainInfraModule { } diff --git a/src/Infrastructure/Persistence/Domain/Memory/MemoryEntityPersister.ts b/src/Infrastructure/Persistence/Domain/Memory/MemoryEntityPersister.ts index d0e0226..cb9d18a 100644 --- a/src/Infrastructure/Persistence/Domain/Memory/MemoryEntityPersister.ts +++ b/src/Infrastructure/Persistence/Domain/Memory/MemoryEntityPersister.ts @@ -2,6 +2,8 @@ import { AbstractEntityCommon, EntityMetaCommon, EntityIdTypeOf } from '@/Domain import { AR, AbstractValueObject, AsyncResult, CurrentTime, OK, OKA, wrapToArray } from '@hexancore/common'; import { AbstractEntityPersister, AbstractEntityRepositoryCommon } from '../Generic'; +export const MEMORY_PERSISTER_TYPE = "memory"; + export class MemoryEntityPersister, M extends EntityMetaCommon> extends AbstractEntityPersister { protected persisted: T[]; diff --git a/src/Infrastructure/Persistence/Domain/Memory/index.ts b/src/Infrastructure/Persistence/Domain/Memory/index.ts index 03aaa73..9c452c6 100644 --- a/src/Infrastructure/Persistence/Domain/Memory/index.ts +++ b/src/Infrastructure/Persistence/Domain/Memory/index.ts @@ -5,4 +5,4 @@ export * from './MemoryAggregateRootRepository'; export * from './MemoryEntityRepository'; export * from './MemoryEntityCollectionQueries'; -export * from './HcInfraMemoryDomainModule'; +export * from './HcMemoryDomainInfraModule'; diff --git a/src/Util/Feature/FeatureModuleDiscoverer.ts b/src/Util/Feature/FeatureModuleDiscoverer.ts new file mode 100644 index 0000000..4ef7daf --- /dev/null +++ b/src/Util/Feature/FeatureModuleDiscoverer.ts @@ -0,0 +1,212 @@ +import { fdir, type PathsOutput } from "fdir"; +import { pathExists } from 'fs-extra'; +import { readFile } from 'node:fs/promises'; +import path from "node:path"; +import { PerformanceHelper } from "../Performance/PerformanceHelper"; +import { FeatureModuleMeta, type ClassMeta, type FeatureAggregateRootMeta, type FeatureApplicationMessageMeta, type FeatureApplicationMeta, type FeatureApplicationServiceMeta, type FeatureDomainMeta, type FeatureEntityMeta, type FeatureInfrastructureMeta } from "./FeatureModuleMeta"; +import { FsHelper } from "../Filesystem/FsHelper"; + +export const CORE_FEATURE_NAME = "Core"; + +export class FeatureModuleDiscoverer { + + public constructor(private sourceRoot: string) { + this.sourceRoot = FsHelper.normalizePathSep(this.sourceRoot); + } + + public static extractFeatureNameFromPath(sourceRoot: string, sourcePath: string): string | null { + if (!sourcePath.startsWith(sourceRoot)) { + return null; + } + + const withoutSourceRootParts = sourcePath.substring(sourceRoot.length + 1).split("/", 2); + if (withoutSourceRootParts.length < 2) { + return null; + } + + const featureName = withoutSourceRootParts[0]; + return featureName === CORE_FEATURE_NAME ? null : featureName; + } + + public async discoverAll(): Promise> { + const featureDirs: PathsOutput = await FeatureModuleDiscoverer.crawlDirOnly(this.sourceRoot); + const features = new Map(); + for (const featureDir of featureDirs) { + const featureName = path.basename(featureDir); + if (featureName === CORE_FEATURE_NAME) { + continue; + } + + const discovery = await this.discover(featureName, featureDir); + features.set(featureName, discovery); + } + + return features; + } + + private async discover(featureName: string, featureDir: string): Promise { + featureDir = featureDir.endsWith("/") ? featureDir.substring(0, featureDir.length - 1) : featureDir; + + return PerformanceHelper.measureFunction("hc.core.feature_module_discovery" + featureName, async () => { + return FeatureModuleMeta.create({ + name: featureName, + application: await this.discoverFeatureApplication(featureDir), + domain: await this.discoverFeatureDomain(featureDir), + infrastructure: await this.discoverFeatureInfrastructure(featureName) + }); + }); + } + + private async discoverFeatureApplication(featureDir: string): Promise { + const contextDirs: PathsOutput = await FeatureModuleDiscoverer.crawlDirOnly(path.join(featureDir, "Application")); + + const commands: FeatureApplicationMessageMeta[] = []; + const queries: FeatureApplicationMessageMeta[] = []; + const events: FeatureApplicationMessageMeta[] = []; + const services: FeatureApplicationServiceMeta[] = []; + + for (const contextDir of contextDirs) { + const contextName = path.basename(contextDir); + commands.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, "Command"))); + queries.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, "Query"))); + events.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, "Event"))); + services.push(...(await this.discoverServices(contextName, contextDir, featureDir))); + } + + return { + commands, + queries, + events, + services, + }; + } + + private async discoverFeatureDomain(featureDir: string): Promise { + return { + aggregateRoots: await this.discoverAggregateRoots(featureDir), + }; + } + + private async discoverAggregateRoots(featureDir: string): Promise { + const dirs: PathsOutput = await FeatureModuleDiscoverer.crawlDirOnly(path.join(featureDir, "Domain")); + + const roots: FeatureAggregateRootMeta[] = []; + for (const rootDir of dirs) { + const name = path.basename(rootDir); + // Domain/Service is dir with Domain Services working on multiple aggregate roots + if (name === "Service") { + continue; + } + + roots.push({ + name, + repositoryName: name + "Repository", + infraRepositoryName: "Infra" + name + "Repository", + entities: await this.discoverAggregateRootEntities(rootDir) + }); + } + + return roots; + } + + private async discoverAggregateRootEntities(rootDir: string): Promise { + const filePaths: PathsOutput = await FeatureModuleDiscoverer.createDirClawler() + .withFullPaths() + .withMaxDepth(0) + .crawl(path.join(rootDir)).withPromise(); + + const entities: FeatureEntityMeta[] = []; + for (const filePath of filePaths) { + const isEntity = (await readFile(filePath)).indexOf("AbstractEntity", 0, 'utf8') !== -1; + if (!isEntity) { + continue; + } + + const name = path.basename(filePath, ".ts"); + entities.push({ + name, + repositoryName: name + "Repository", + infraRepositoryName: "Infra" + name + "Repository", + }); + } + + return entities; + } + + private async discoverMessageHandlers(context: string, contextDir: string, featureDir: string, messageKind: 'Command' | 'Query' | 'Event'): Promise { + const dir = contextDir + `${messageKind}`; + if (!(await pathExists(dir))) { + return []; + } + + const dirIt = (new fdir()).withPathSeparator("/").onlyDirs().withMaxDepth(1).withFullPaths(); + const messagePaths = await dirIt.crawl(dir).withPromise(); + messagePaths.shift(); + const messages: FeatureApplicationMessageMeta[] = []; + for (const messagePath of messagePaths) { + const name = path.basename(messagePath); + messages.push({ + context: context, + name: path.basename(messagePath), + className: `${context}${name}${messageKind}`, + handlerClassName: `${context}${name}${messageKind}Handler`, + path: messagePath.substring(featureDir.length + 1, messagePath.length - 1), + + }); + } + + return messages; + } + + private async discoverServices(context: string, contextDir: string, featureDir: string): Promise { + const dir = contextDir + `Service`; + if (!(await pathExists(dir))) { + return []; + } + + const dirIt = (new fdir()).withPathSeparator("/").withFullPaths(); + dirIt.filter((path) => !path.endsWith("index.ts")); + + const filePaths = await dirIt.crawl(dir).withPromise(); + const classes: FeatureApplicationServiceMeta[] = []; + for (const filePath of filePaths) { + const isInjectable = (await readFile(filePath)).indexOf("@Injectable(", 0, 'utf8') !== -1; + classes.push({ + className: path.basename(filePath, ".ts"), + context: context, + isInjectable, + path: filePath.substring(featureDir.length + 1, filePath.length - 3), + }); + } + + return classes; + } + + private async discoverFeatureInfrastructure(featureName: string): Promise { + return { + module: this.discoverInfrastructureModule(featureName), + }; + } + + private discoverInfrastructureModule(featureName: string): ClassMeta { + const className = `${featureName}InfraModule`; + const filePath = `Infrastructure/${className}`; + return { className, path: filePath }; + } + + private static createDirClawler(): fdir { + return (new fdir()).withPathSeparator("/"); + } + + private static async crawlDirOnly(dir: string): Promise { + const crawler = this.createDirClawler() + .onlyDirs() + .withFullPaths() + .withMaxDepth(1); + + const dirs = await crawler.crawl(dir).withPromise(); + dirs.shift(); + return dirs; + } + +} \ No newline at end of file diff --git a/src/Util/Feature/FeatureModuleMeta.ts b/src/Util/Feature/FeatureModuleMeta.ts new file mode 100644 index 0000000..a5ec15d --- /dev/null +++ b/src/Util/Feature/FeatureModuleMeta.ts @@ -0,0 +1,106 @@ +import { hash } from "node:crypto"; + +export interface ClassMeta { + className: string; + path: string; +} + +export interface FeatureApplicationServiceMeta { + className: string; + /** + * Name of context where service belongs. + */ + context: string; + /** + * Indicate is marked with `@Injectable` + */ + isInjectable: boolean; + + /** + * Relative to feature root. + */ + path: string; +} + +export interface FeatureApplicationMessageMeta { + context: string; + className: string, + handlerClassName: string, + name: string; + path: string; +} + +export interface FeatureApplicationMeta { + commands: FeatureApplicationMessageMeta[]; + queries: FeatureApplicationMessageMeta[]; + events: FeatureApplicationMessageMeta[]; + services: FeatureApplicationServiceMeta[]; +} + +export interface FeatureAggregateRootMeta { + name: string; + repositoryName: string; + infraRepositoryName: string; + entities: FeatureEntityMeta[]; +} + +export interface FeatureEntityMeta { + name: string; + repositoryName: string; + infraRepositoryName: string; +} + +export interface FeatureDomainMeta { + aggregateRoots: FeatureAggregateRootMeta[]; +} + +export interface FeatureInfrastructureMeta { + module: ClassMeta; +} + +export type FeatureModuleMap = Map; + +export class FeatureModuleMeta { + public readonly name!: string; + public readonly cacheKey!: string; + + public readonly application!: FeatureApplicationMeta; + public readonly domain!: FeatureDomainMeta; + public readonly infrastructure!: FeatureInfrastructureMeta; + + private constructor(props: Omit & { cacheKey?: string; }) { + Object.assign(this, props); + if (!this.cacheKey) { + this.cacheKey = FeatureModuleMeta.calcCacheKey(this); + } + } + + public static create(props: Omit & { cacheKey?: string; }): FeatureModuleMeta { + return new this(props); + } + + private static calcCacheKey(meta: Omit): string { + let data = ""; + for (const m of meta.application.commands) { + data += m.context + m.name; + } + + for (const m of meta.application.queries) { + data += m.context + m.name; + } + + for (const m of meta.application.events) { + data += m.context + m.name; + } + + for (const m of meta.application.services) { + data += m.path; + } + + for (const r of meta.domain.aggregateRoots) { + data += r.name; + } + + return hash("sha1", data, "hex"); + } +} \ No newline at end of file diff --git a/src/Util/FeatureModuleDiscoverer.ts b/src/Util/FeatureModuleDiscoverer.ts deleted file mode 100644 index 5c83493..0000000 --- a/src/Util/FeatureModuleDiscoverer.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { fdir, type PathsOutput } from "fdir"; -import { pathExists } from 'fs-extra'; -import path from "node:path"; -import { PerformanceHelper } from "./Performance/PerformanceHelper"; -import { hash } from "node:crypto"; -import { readFile } from 'node:fs/promises'; - -export interface ClassDiscovery { - className: string; - path: string; -} - -export interface FeatureApplicationServiceInfo { - className: string; - /** - * Name of context where service belongs. - */ - context: string; - /** - * Indicate is marked with `@Injectable` - */ - isInjectable: boolean; - - /** - * Relative to feature root. - */ - path: string; -} - -export interface FeatureApplicationMessageInfo { - context: string; - className: string, - handlerClassName: string, - name: string; - path: string; -} - -export interface FeatureModuleDiscovery { - application: { - commands: FeatureApplicationMessageInfo[]; - queries: FeatureApplicationMessageInfo[]; - events: FeatureApplicationMessageInfo[]; - services: FeatureApplicationServiceInfo[]; - }; - - infrastructure: { - module: ClassDiscovery; - }; - - cacheKey: string; -} - -export class FeatureModuleDiscoverer { - public constructor(private sourceRoot: string) { - this.sourceRoot.replaceAll("\\", "/"); - } - - public async discoverAll(): Promise> { - const featureDirs: PathsOutput = await (new fdir()) - .onlyDirs() - .withMaxDepth(1) - .withPathSeparator("/") - .crawl(this.sourceRoot) - .withPromise(); - - featureDirs.shift(); - const features = new Map(); - for (const featureDir of featureDirs) { - const discovery = await FeatureModuleDiscoverer.discover(featureDir); - features.set(path.basename(featureDir), discovery); - } - - return features; - } - - public static async discover(featureDir: string): Promise { - const featureName = path.basename(featureDir); - featureDir = featureDir.replaceAll("\\", "/"); - featureDir = featureDir.endsWith("/") ? featureDir.substring(0, featureDir.length - 1) : featureDir; - return PerformanceHelper.measureFunction("hc.core.feature_module_discovery" + featureName, async () => { - const contextDirs: PathsOutput = await (new fdir()) - .onlyDirs() - .withMaxDepth(1) - .withPathSeparator("/") - .crawl(path.join(featureDir, "Application")) - .withPromise(); - - const commands: FeatureApplicationMessageInfo[] = []; - const queries: FeatureApplicationMessageInfo[] = []; - const events: FeatureApplicationMessageInfo[] = []; - const services: FeatureApplicationServiceInfo[] = []; - - for (const contextDir of contextDirs) { - const contextName = path.basename(contextDir); - commands.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, 'Command'))); - queries.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, 'Query'))); - events.push(...(await this.discoverMessageHandlers(contextName, contextDir, featureDir, 'Event'))); - services.push(...(await this.discoverServices(contextName, contextDir, featureDir))); - } - - const discovery = { - application: { - commands, - queries, - events, - services, - }, - infrastructure: { - module: this.discoverInfrastructureModule(featureName) - } - }; - - this.calcCacheHashKey(discovery); - return discovery as FeatureModuleDiscovery; - }); - } - - public static extractFeatureNameFromPath(sourceRoot: string, sourcePath: string): string | null { - if (sourcePath.startsWith(sourceRoot) && sourcePath.endsWith("Module.ts")) { - const withoutSourceRoot = sourcePath.substring(sourceRoot.length + 1); - const withoutSourceRootSplit = withoutSourceRoot.split("/"); - return withoutSourceRootSplit.length === 2 ? withoutSourceRootSplit[0] : null; - } - - return null; - } - - private static async discoverMessageHandlers(context: string, contextDir: string, featureDir: string, messageType: 'Command' | 'Query' | 'Event'): Promise { - const dir = contextDir + `${messageType}`; - if (!(await pathExists(dir))) { - return []; - } - - const dirIt = (new fdir()).withPathSeparator("/").onlyDirs().withMaxDepth(1).withFullPaths(); - const messagePaths = await dirIt.crawl(dir).withPromise(); - messagePaths.shift(); - const messages: FeatureApplicationMessageInfo[] = []; - for (const messagePath of messagePaths) { - const name = path.basename(messagePath); - messages.push({ - context: context, - name: path.basename(messagePath), - className: `${context}${name}${messageType}`, - handlerClassName: `${context}${name}${messageType}Handler`, - path: messagePath.substring(featureDir.length + 1, messagePath.length - 1), - - }); - } - - return messages; - } - - private static async discoverServices(context: string, contextDir: string, featureDir: string): Promise { - const dir = contextDir + `Service`; - if (!(await pathExists(dir))) { - return []; - } - - const dirIt = (new fdir()).withPathSeparator("/").withFullPaths(); - dirIt.filter((path) => !path.endsWith("index.ts")); - - const filePaths = await dirIt.crawl(dir).withPromise(); - const classes: FeatureApplicationServiceInfo[] = []; - for (const filePath of filePaths) { - const isInjectable = (await readFile(filePath)).indexOf("@Injectable(", 0, 'utf8') !== -1; - classes.push({ - className: path.basename(filePath, ".ts"), - context: context, - isInjectable, - path: filePath.substring(featureDir.length + 1, filePath.length - 3), - }); - } - - return classes; - } - - private static discoverInfrastructureModule(featureName: string): ClassDiscovery { - const className = `${featureName}InfrastructureModule`; - const filePath = `Infrastructure/${className}`; - return { className, path: filePath }; - } - - private static calcCacheHashKey(discovery: Omit) { - let hashSource = ""; - for (const c of discovery.application.commands) { - hashSource += c.context + c.name; - } - for (const c of discovery.application.queries) { - hashSource += c.context + c.name; - } - for (const c of discovery.application.events) { - hashSource += c.context + c.name; - } - for (const c of discovery.application.services) { - hashSource += c.path; - } - - discovery["cacheKey"] = hash("sha1", hashSource, "hex"); - } - -} \ No newline at end of file diff --git a/src/Util/index.ts b/src/Util/index.ts index d755796..ef5e95d 100644 --- a/src/Util/index.ts +++ b/src/Util/index.ts @@ -1,3 +1,5 @@ export * from './Log'; +export * from './Filesystem/FsHelper'; export * from './Filesystem/FileTypeReadable'; +export * from './Performance/PerformanceHelper'; diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/index.ts b/test/helper/libs/test-lib/src/Book/Application/Book/index.ts index 5b5d5be..7f8f0ed 100644 --- a/test/helper/libs/test-lib/src/Book/Application/Book/index.ts +++ b/test/helper/libs/test-lib/src/Book/Application/Book/index.ts @@ -1,7 +1,5 @@ // Don't edit - file is updated automatic from hcli. -export * from './Service'; - // __COMMAND__ export * from './Command/Create/BookCreateCommand'; diff --git a/test/helper/libs/test-lib/src/Book/BookModule.ts b/test/helper/libs/test-lib/src/Book/BookModule.ts index a2b248c..9957dbe 100644 --- a/test/helper/libs/test-lib/src/Book/BookModule.ts +++ b/test/helper/libs/test-lib/src/Book/BookModule.ts @@ -1,9 +1,14 @@ import { Module } from "@nestjs/common"; +import { BookInfraModule } from "./Infrastructure/BookInfraModule"; // TEST TRANSFORMING IGNORE COMMENT export const SOME_ASSIGMENT_NOISE = 10; -@Module({}) + + +@Module({ + imports: [BookInfraModule] +}) export class BookModule { } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Book.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Book.ts index e0c35b2..f5e755a 100644 --- a/test/helper/libs/test-lib/src/Book/Domain/Book/Book.ts +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Book.ts @@ -1,12 +1,13 @@ -import { AggregateRoot, AbstractAggregateRoot } from '@'; -import { UIntValue, ValueObject } from '@hexancore/common'; - -@ValueObject('Test') -export class BookId extends UIntValue { } +import { AggregateRoot, AbstractAggregateRoot, EntityCollection, type IEntityCollection } from '@'; +import type { BookId } from './Shared/BookId'; +import { BookCopy } from './BookCopy'; @AggregateRoot() export class Book extends AbstractAggregateRoot { + @EntityCollection(BookCopy) + public declare readonly copies: IEntityCollection; + public constructor(public title: string) { super(); return this.proxify(); diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/BookCopy.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/BookCopy.ts new file mode 100644 index 0000000..b80f63e --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/BookCopy.ts @@ -0,0 +1,14 @@ +import { AbstractEntity, Entity } from '@'; +import type { Book } from './Book'; +import type { BookCopyId } from './Shared/BookCopyId'; + +@Entity() +export class BookCopy extends AbstractEntity { + + public readonly bookId?: BookCopy; + + public constructor() { + super(); + return this.proxify(); + } +} diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/BookRepository.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/BookRepository.ts new file mode 100644 index 0000000..829bf47 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/BookRepository.ts @@ -0,0 +1,6 @@ +import type { IAggregateRootRepository } from "@"; +import type { Book } from "./Book"; + +export interface BookRepository extends IAggregateRootRepository { + +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookCopyId.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookCopyId.ts new file mode 100644 index 0000000..cd6ae9a --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookCopyId.ts @@ -0,0 +1,4 @@ +import { UIntValue, ValueObject } from "@hexancore/common"; + +@ValueObject('Book') +export class BookCopyId extends UIntValue { } diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookId.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookId.ts new file mode 100644 index 0000000..b252036 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/BookId.ts @@ -0,0 +1,4 @@ +import { UIntValue, ValueObject } from "@hexancore/common"; + +@ValueObject('Book') +export class BookId extends UIntValue { } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/index.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/index.ts new file mode 100644 index 0000000..16e6b34 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/index.ts @@ -0,0 +1,2 @@ +export * from "./BookId"; +export * from "./BookCopyId"; \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/BookDomainErrors.ts b/test/helper/libs/test-lib/src/Book/Domain/BookDomainErrors.ts index 4ca279c..dcb0344 100644 --- a/test/helper/libs/test-lib/src/Book/Domain/BookDomainErrors.ts +++ b/test/helper/libs/test-lib/src/Book/Domain/BookDomainErrors.ts @@ -1,8 +1,5 @@ import { DefineDomainErrors, standard_entity_errors } from "@hexancore/common"; -export const BookDomainErrors = DefineDomainErrors( - 'Book', - new (class { - public entity_book: standard_entity_errors = 'not_found'; - })(), -); \ No newline at end of file +export const BookDomainErrors = DefineDomainErrors('Book', new (class { + public entity_book: standard_entity_errors = 'not_found'; +})()); \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/index.ts b/test/helper/libs/test-lib/src/Book/Domain/index.ts index 4b83c9f..25ee7af 100644 --- a/test/helper/libs/test-lib/src/Book/Domain/index.ts +++ b/test/helper/libs/test-lib/src/Book/Domain/index.ts @@ -1 +1,2 @@ -export * from './BookDomainErrors'; +export * from "./Book/Shared"; +export * from "./BookDomainErrors"; \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/BookInfrastructureModule.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/BookInfraModule.ts similarity index 62% rename from test/helper/libs/test-lib/src/Book/Infrastructure/BookInfrastructureModule.ts rename to test/helper/libs/test-lib/src/Book/Infrastructure/BookInfraModule.ts index c4224f0..7086786 100644 --- a/test/helper/libs/test-lib/src/Book/Infrastructure/BookInfrastructureModule.ts +++ b/test/helper/libs/test-lib/src/Book/Infrastructure/BookInfraModule.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { BookInfraPort } from '../Application/Book/Service/BookInfraPort'; import { BookInfraAdapter } from './Service/Book/BookInfraAdapter'; +import { BookDomainInfraModule } from './Domain/BookDomainInfraModule'; export const BookInfraPortProvider = { provide: BookInfraPort, @@ -8,10 +9,12 @@ export const BookInfraPortProvider = { }; @Module({ - imports: [], + imports: [BookDomainInfraModule], providers: [ BookInfraPortProvider ], - exports: [BookInfraPortProvider] + exports: [BookInfraPortProvider, BookDomainInfraModule] }) -export class BookInfrastructureModule { } +export class BookInfraModule { } + + diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookCopyRepository.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookCopyRepository.ts new file mode 100644 index 0000000..b84571c --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookCopyRepository.ts @@ -0,0 +1,5 @@ +import { EntityRepository, MEMORY_PERSISTER_TYPE, MemoryEntityRepository } from "@"; +import { BookCopy } from "../../../Domain/Book/BookCopy"; + +@EntityRepository(BookCopy, MEMORY_PERSISTER_TYPE) +export class InfraBookCopyRepository extends MemoryEntityRepository { } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookRepository.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookRepository.ts new file mode 100644 index 0000000..ae175e7 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Book/InfraBookRepository.ts @@ -0,0 +1,6 @@ +import { AggregateRootRepository, MEMORY_PERSISTER_TYPE, MemoryAggregateRootRepository } from "@"; +import { Book } from "@test/libs/test-lib/src/Book/Domain/Book/Book"; +import type { BookRepository } from "../../../Domain/Book/BookRepository"; + +@AggregateRootRepository(Book, MEMORY_PERSISTER_TYPE) +export class InfraBookRepository extends MemoryAggregateRootRepository implements BookRepository { } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/BookDomainInfraModule.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/BookDomainInfraModule.ts new file mode 100644 index 0000000..e5d5681 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/BookDomainInfraModule.ts @@ -0,0 +1,14 @@ +import { Module, type Provider } from '@nestjs/common'; +import {HcAppModuleMeta} from "@/Util/ModuleHelper"; +HcAppModuleMeta; // testing transforms compilerOptions.paths + +const FakeProvider: Provider = { + provide: "FAKE_BOOK_DOMAIN_INFRA", + useValue: "fake" +}; + +@Module({ + providers: [FakeProvider], + exports: [FakeProvider] +}) +export class BookDomainInfraModule { } diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/Book/MemoryBookRepository.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/Book/MemoryBookRepository.ts deleted file mode 100644 index db4d7d8..0000000 --- a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/Book/MemoryBookRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AggregateRootRepository, MemoryAggregateRootRepository } from "@"; -import { Book } from "@test/libs/test-lib/src/Book/Domain/Book/Book"; - -@AggregateRootRepository(Book, 'memory') -export class MemoryBookRepository extends MemoryAggregateRootRepository { } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/index.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/index.ts deleted file mode 100644 index 4e64d8c..0000000 --- a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/Account/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Book/MemoryBookRepository'; \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/index.ts b/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/index.ts deleted file mode 100644 index 0792df6..0000000 --- a/test/helper/libs/test-lib/src/Book/Infrastructure/Domain/Memory/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Account'; \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/index.ts b/test/helper/libs/test-lib/src/Book/index.ts index c6d33f9..d6986ad 100644 --- a/test/helper/libs/test-lib/src/Book/index.ts +++ b/test/helper/libs/test-lib/src/Book/index.ts @@ -1,3 +1,7 @@ +// feature index contains all public elements + +// Aplication - only Command/Query export * from './Application'; -export * from './BookModule'; + +// Domain SharedKernel export * from './Domain'; \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/TestLibModule.ts b/test/helper/libs/test-lib/src/TestLibModule.ts index 84ee405..7034e65 100644 --- a/test/helper/libs/test-lib/src/TestLibModule.ts +++ b/test/helper/libs/test-lib/src/TestLibModule.ts @@ -2,7 +2,8 @@ import { Module } from "@nestjs/common"; import { BookModule } from "./Book/BookModule"; @Module({ - imports: [BookModule] + imports: [BookModule], + exports: [BookModule] }) export class TestLibModule { diff --git a/test/helper/libs/test-lib/src/index.ts b/test/helper/libs/test-lib/src/index.ts new file mode 100644 index 0000000..5351a6c --- /dev/null +++ b/test/helper/libs/test-lib/src/index.ts @@ -0,0 +1,7 @@ +// Lib index contains all public stuff + +// Every feature public elements +export * from "./Book"; + +// Main lib module +export * from "./TestLibModule"; \ No newline at end of file diff --git a/test/helper/src/Test/Domain/Author/Author.ts b/test/helper/src/Test/Domain/Author/Author.ts deleted file mode 100644 index 80c9fb6..0000000 --- a/test/helper/src/Test/Domain/Author/Author.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AbstractAggregateRoot, AggregateRoot, EntityCollection, IEntityCollection } from '@'; -import { UIntValue, ValueObject } from '@hexancore/common'; -import { Book } from './Book'; - -@ValueObject('Test') -export class AuthorId extends UIntValue { } - -@AggregateRoot() -export class Author extends AbstractAggregateRoot { - @EntityCollection(Book) - public declare readonly books: IEntityCollection; - - public constructor(public name: string) { - super(); - return this.proxify(); - } -} diff --git a/test/helper/src/Test/Domain/Author/AuthorRepository.ts b/test/helper/src/Test/Domain/Author/AuthorRepository.ts deleted file mode 100644 index de3c148..0000000 --- a/test/helper/src/Test/Domain/Author/AuthorRepository.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { IAggregateRootRepository } from "@"; -import type { Author } from "./Author"; - -export interface AuthorRepository extends IAggregateRootRepository { } \ No newline at end of file diff --git a/test/helper/src/Test/Domain/Author/Book.ts b/test/helper/src/Test/Domain/Author/Book.ts deleted file mode 100644 index ef6b05c..0000000 --- a/test/helper/src/Test/Domain/Author/Book.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Entity, AbstractEntity } from '@'; -import { UIntValue, ValueObject } from '@hexancore/common'; -import { Author, AuthorId } from './Author'; - -@ValueObject('Test') -export class BookId extends UIntValue { } - -@Entity() -export class Book extends AbstractEntity { - - public readonly authorId?: AuthorId; - - public constructor(public name: string) { - super(); - return this.proxify(); - } -} diff --git a/test/helper/src/Test/Domain/Author/index.ts b/test/helper/src/Test/Domain/Author/index.ts deleted file mode 100644 index 2127f54..0000000 --- a/test/helper/src/Test/Domain/Author/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Author'; -export * from './AuthorRepository'; -export * from './Book'; diff --git a/test/helper/src/Test/Domain/TestDomainErrors.ts b/test/helper/src/Test/Domain/TestDomainErrors.ts deleted file mode 100644 index b65e67f..0000000 --- a/test/helper/src/Test/Domain/TestDomainErrors.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DefineDomainErrors, standard_entity_errors } from "@hexancore/common"; - -export const TestDomainErrors = DefineDomainErrors( - 'Test', - new (class { - public entity_book: standard_entity_errors = 'not_found'; - public entity_author: standard_entity_errors | 'custom_1' = 'not_found'; - public other_error = ''; - })(), -); \ No newline at end of file diff --git a/test/helper/src/Test/Domain/index.ts b/test/helper/src/Test/Domain/index.ts deleted file mode 100644 index 75c3862..0000000 --- a/test/helper/src/Test/Domain/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Author'; -export * from './TestDomainErrors'; \ No newline at end of file diff --git a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryAuthorRepository.ts b/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryAuthorRepository.ts deleted file mode 100644 index 6db352d..0000000 --- a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryAuthorRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AggregateRootRepository, MemoryAggregateRootRepository } from "@"; -import { Author } from "@test/src/Test/Domain"; -import type { AuthorRepository } from "@test/src/Test/Domain/Author/AuthorRepository"; - -@AggregateRootRepository(Author, 'memory') -export class MemoryAuthorRepository extends MemoryAggregateRootRepository implements AuthorRepository { } \ No newline at end of file diff --git a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryBookRepository.ts b/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryBookRepository.ts deleted file mode 100644 index 94d12fe..0000000 --- a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/Author/MemoryBookRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, MemoryEntityRepository } from "@"; -import { Book } from "@test/src/Test/Domain"; - -@EntityRepository(Book, 'memory') -export class MemoryBookRepository extends MemoryEntityRepository { } \ No newline at end of file diff --git a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/index.ts b/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/index.ts deleted file mode 100644 index 9cd4144..0000000 --- a/test/helper/src/Test/Infrastructure/Persistence/Memory/Account/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Author'; \ No newline at end of file diff --git a/test/helper/src/Test/Infrastructure/Persistence/Memory/index.ts b/test/helper/src/Test/Infrastructure/Persistence/Memory/index.ts deleted file mode 100644 index 0792df6..0000000 --- a/test/helper/src/Test/Infrastructure/Persistence/Memory/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Account'; \ No newline at end of file diff --git a/test/helper/src/Test/Infrastructure/PrivateTestInfraModule.ts b/test/helper/src/Test/Infrastructure/PrivateTestInfraModule.ts deleted file mode 100644 index 5ded724..0000000 --- a/test/helper/src/Test/Infrastructure/PrivateTestInfraModule.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { HcInfraDomainModule } from '@'; -import { Module } from '@nestjs/common'; - -@Module({ - imports: [HcInfraDomainModule.forFeature({ moduleInfraDir: __dirname })], - exports: [HcInfraDomainModule] -}) -export class PrivateTestInfraModule {} diff --git a/test/helper/src/Test/index.ts b/test/helper/src/Test/index.ts deleted file mode 100644 index ad06f68..0000000 --- a/test/helper/src/Test/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const TEST_MODULE_DIR = __dirname; \ No newline at end of file diff --git a/test/integration/Compiler/Transformer/FeatureModuleTsTransformer.test.ts b/test/integration/Compiler/Transformer/FeatureTsTransformer.test.ts similarity index 57% rename from test/integration/Compiler/Transformer/FeatureModuleTsTransformer.test.ts rename to test/integration/Compiler/Transformer/FeatureTsTransformer.test.ts index 94e20a1..e476311 100644 --- a/test/integration/Compiler/Transformer/FeatureModuleTsTransformer.test.ts +++ b/test/integration/Compiler/Transformer/FeatureTsTransformer.test.ts @@ -3,28 +3,33 @@ */ import { TsTransformerTestHelper } from "@/Compiler/Test/TsTransformerTestHelper"; -import { FeatureModuleTsTransformer } from "@/Compiler/Transformer/FeatureModuleTsTransformer"; +import { FeatureModuleTsTransformer } from "@/Compiler/Transformer/Feature/FeatureModuleTsTransformer"; +import { FeatureTsTransformer } from "@/Compiler/Transformer/Feature/FeatureTsTransformer"; import { readFileSync } from "fs"; import ts from "typescript"; describe(FeatureModuleTsTransformer.constructor.name, () => { const helper = TsTransformerTestHelper.createFromTsConfig(process.cwd() + "/tsconfig.json"); - let transformer: FeatureModuleTsTransformer; + let transformer: FeatureTsTransformer; beforeAll(() => { const sourceRoot = process.cwd() + "/test/helper/libs/test-lib/src"; const compilerRoot = process.cwd() + "/lib/Compiler"; - transformer = FeatureModuleTsTransformer.create(sourceRoot, compilerRoot); + transformer = FeatureTsTransformer.create(sourceRoot, compilerRoot); }); test("transform", () => { const codeFilePath = process.cwd() + "/test/helper/libs/test-lib/src/Book/BookModule.ts"; const sourceFile = helper.createSourceFileFromExisting(codeFilePath); - const tsConfig = JSON.parse(readFileSync(process.cwd() +"/tsconfig.json").toString()); - - const out = ts.transpileModule(sourceFile.text, {compilerOptions: tsConfig.compilerOptions, transformers: { - before: [(context) => (sourceFile: ts.SourceFile) => transformer.transform("Book", sourceFile, context)] - }}); + const tsConfig = JSON.parse(readFileSync(process.cwd() + "/tsconfig.json").toString()); + + const out = ts.transpileModule(sourceFile.text, { + compilerOptions: tsConfig.compilerOptions, + fileName: codeFilePath, + transformers: { + before: [(context) => (sourceFile: ts.SourceFile) => transformer.transform(sourceFile, context)] + } + }); expect(out.outputText).toMatchSnapshot(); }); diff --git a/test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.js b/test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.js new file mode 100644 index 0000000..1acd7fc --- /dev/null +++ b/test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.js @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BookModule = exports.SOME_ASSIGMENT_NOISE = void 0; +const tslib_1 = require("tslib"); +const common_1 = require("@nestjs/common"); +const BookInfraModule_1 = require("./Infrastructure/BookInfraModule"); +const BookCreateCommandHandler_1 = require("./Application/Book/Command/Create/BookCreateCommandHandler"); +const BookGetByIdQueryHandler_1 = require("./Application/Book/Query/GetById/BookGetByIdQueryHandler"); +const BookService_1 = require("./Application/Book/Service/BookService"); +// TEST TRANSFORMING IGNORE COMMENT +exports.SOME_ASSIGMENT_NOISE = 10; +let BookModule = class BookModule { +}; +exports.BookModule = BookModule; +exports.BookModule = BookModule = tslib_1.__decorate([ + (0, common_1.Module)({ + imports: [BookInfraModule_1.BookInfraModule], + providers: [BookCreateCommandHandler_1.BookCreateCommandHandler, BookGetByIdQueryHandler_1.BookGetByIdQueryHandler, BookService_1.BookService] + }) +], BookModule); +//# sourceMappingURL=BookModule.js.map" + diff --git a/test/integration/Compiler/Transformer/__snapshots__/FeatureModuleTsTransformer.test.ts.snap b/test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.test.ts.snap similarity index 68% rename from test/integration/Compiler/Transformer/__snapshots__/FeatureModuleTsTransformer.test.ts.snap rename to test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.test.ts.snap index 7381e56..11a73a9 100644 --- a/test/integration/Compiler/Transformer/__snapshots__/FeatureModuleTsTransformer.test.ts.snap +++ b/test/integration/Compiler/Transformer/__snapshots__/FeatureTsTransformer.test.ts.snap @@ -6,17 +6,21 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.BookModule = exports.SOME_ASSIGMENT_NOISE = void 0; const tslib_1 = require("tslib"); const common_1 = require("@nestjs/common"); +const BookInfraModule_1 = require("./Infrastructure/BookInfraModule"); const BookCreateCommandHandler_1 = require("./Application/Book/Command/Create/BookCreateCommandHandler"); const BookGetByIdQueryHandler_1 = require("./Application/Book/Query/GetById/BookGetByIdQueryHandler"); const BookService_1 = require("./Application/Book/Service/BookService"); -const BookInfrastructureModule_1 = require("./Infrastructure/BookInfrastructureModule"); // TEST TRANSFORMING IGNORE COMMENT exports.SOME_ASSIGMENT_NOISE = 10; let BookModule = class BookModule { }; exports.BookModule = BookModule; exports.BookModule = BookModule = tslib_1.__decorate([ - (0, common_1.Module)({ imports: [BookInfrastructureModule_1.BookInfrastructureModule], providers: [BookCreateCommandHandler_1.BookCreateCommandHandler, BookGetByIdQueryHandler_1.BookGetByIdQueryHandler, BookService_1.BookService] }) + (0, common_1.Module)({ + imports: [BookInfraModule_1.BookInfraModule], + providers: [BookCreateCommandHandler_1.BookCreateCommandHandler, BookGetByIdQueryHandler_1.BookGetByIdQueryHandler, BookService_1.BookService], + exports: [] + }) ], BookModule); -" +//# sourceMappingURL=BookModule.js.map" `; diff --git a/test/integration/Infrastructure/Persistence/Domain/Memory/MemoryDomainPersistance.test.ts b/test/integration/Infrastructure/Persistence/Domain/Memory/MemoryDomainPersistance.test.ts index 6ec980f..a8aac7a 100644 --- a/test/integration/Infrastructure/Persistence/Domain/Memory/MemoryDomainPersistance.test.ts +++ b/test/integration/Infrastructure/Persistence/Domain/Memory/MemoryDomainPersistance.test.ts @@ -2,35 +2,35 @@ * @group integration */ -import { HcInfraMemoryDomainModule, HcModule, InjectAggregateRootRepository } from '@'; -import { OK } from '@hexancore/common'; +import { HcMemoryDomainInfraModule, HcModule, InjectAggregateRootRepository } from '@'; import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Author, AuthorId, Book, BookId, type AuthorRepository } from '@test/src/Test/Domain'; -import { TestDomainErrors } from '@test/src/Test/Domain/TestDomainErrors'; -import { PrivateTestInfraModule } from '@test/src/Test/Infrastructure/PrivateTestInfraModule'; +import { BookCopyId, BookDomainErrors, BookId } from '@test/libs/test-lib/src'; +import { Book } from '@test/libs/test-lib/src/Book/Domain/Book/Book'; +import { BookCopy } from '@test/libs/test-lib/src/Book/Domain/Book/BookCopy'; +import type { BookRepository } from '@test/libs/test-lib/src/Book/Domain/Book/BookRepository'; +import { BookDomainInfraModule } from '@test/libs/test-lib/src/Book/Infrastructure/Domain/BookDomainInfraModule'; @Injectable() -class AuthorService { - public constructor(@InjectAggregateRootRepository(Author) public repository: AuthorRepository) { } +class Book2Service { + public constructor(@InjectAggregateRootRepository(Book) public repository: BookRepository) { } } describe('MemoryAggregateRootRepository', () => { let module: TestingModule; - let authorRepository: AuthorRepository; + let bookRepository: BookRepository; beforeEach(async () => { module = await Test.createTestingModule({ imports: [ HcModule.forRoot(), - HcInfraMemoryDomainModule, - PrivateTestInfraModule, - + HcMemoryDomainInfraModule, + BookDomainInfraModule ], - providers: [AuthorService] + providers: [Book2Service] }).compile(); - authorRepository = module.get(AuthorService).repository; + bookRepository = module.get(Book2Service).repository; }); afterEach(async () => { @@ -40,38 +40,37 @@ describe('MemoryAggregateRootRepository', () => { }); test('persist()', async () => { - const author = new Author('test_author'); - author.id = AuthorId.cs(1); - - const book = new Book('test_book'); + const book = new Book('test_author'); book.id = BookId.cs(1); - author.books.add(book); - const rp = await authorRepository.persist(author); - expect(rp).toEqual(OK(true)); + const bookCopy = new BookCopy(); + bookCopy.id = BookCopyId.cs(1); + book.copies.add(bookCopy); - const r = await authorRepository.getAllAsArray(); + const rp = await bookRepository.persist(book); + expect(rp).toMatchSuccessResult(true); + + const r = await bookRepository.getAllAsArray(); expect(r.isSuccess()).toBeTruthy(); - expect(r.v[0].id).toEqual(author.id); + expect(r.v[0].id).toEqual(book.id); - const abr = await r.v[0].books.getAllAsArray(); + const abr = await r.v[0].copies.getAllAsArray(); expect(abr.isSuccess()).toBeTruthy(); const ab = abr.v; expect(ab.length).toBe(1); - expect(ab[0]).toEqual(book); - - const currentBookById = await r.v[0].books.getById(ab[0].id); + expect(ab[0]).toEqual(bookCopy); - expect(currentBookById).toEqual(OK(book)); + const currentBookById = await r.v[0].copies.getById(ab[0].id); + expect(currentBookById).toMatchSuccessResult(bookCopy); }); test('getById() when not exists', async () => { - const id = AuthorId.cs(1); - const current = await authorRepository.getById(id); + const id = BookId.cs(1); + const current = await bookRepository.getById(id); expect(current).toMatchAppError({ - type: TestDomainErrors.entity.author.t('not_found'), + type: BookDomainErrors.entity.book.t('not_found'), code: 404, data: { searchCriteria: { diff --git a/test/unit/Domain/Entity/EntityCollection.test.ts b/test/unit/Domain/Entity/EntityCollection.test.ts index a3f6924..08a37e4 100644 --- a/test/unit/Domain/Entity/EntityCollection.test.ts +++ b/test/unit/Domain/Entity/EntityCollection.test.ts @@ -1,32 +1,24 @@ + /** * @group unit/core */ -import { Author, AuthorId } from '@test/src/Test/Domain/Author'; -import { Book } from '@test/src/Test/Domain/Author/Book'; +import { Book } from '@test/libs/test-lib/src/Book/Domain/Book/Book'; +import { BookCopy } from '@test/libs/test-lib/src/Book/Domain/Book/BookCopy'; + import { AGGREGATE_ROOT_META, EntityCollectionMeta } from '../../../../src'; describe('EntityCollection', () => { test('meta properties', () => { - expect(AGGREGATE_ROOT_META(Author).collections).toEqual(new Map([[Book.name, new EntityCollectionMeta(Book, 'books')]])); + expect(AGGREGATE_ROOT_META(Book).collections).toEqual(new Map([[BookCopy.name, new EntityCollectionMeta(BookCopy, 'copies')]])); }); - test('add when author id undefined', () => { - const author = new Author('test'); - + test('add', () => { const book = new Book('test'); - author.books.add(book); + const bookCopy = new BookCopy(); - expect(book.authorId).toBe(author.id); - expect(author.books.waitingAdd).toEqual([book]); - }); - - test('add when author has id', () => { - const author = new Author('test'); - author.id = AuthorId.c(1).v; - - const book = new Book('test'); - author.books.add(book); + book.copies.add(bookCopy); - expect(author.books.waitingAdd).toEqual([book]); + expect(bookCopy.bookId).toBeUndefined(); + expect(book.copies.waitingAdd).toEqual([bookCopy]); }); }); diff --git a/test/unit/Infrastructure/Persistence/Domain/AggregateRootRepositoryProxy.test.ts b/test/unit/Infrastructure/Persistence/Domain/AggregateRootRepositoryProxy.test.ts index db43c13..f4757a6 100644 --- a/test/unit/Infrastructure/Persistence/Domain/AggregateRootRepositoryProxy.test.ts +++ b/test/unit/Infrastructure/Persistence/Domain/AggregateRootRepositoryProxy.test.ts @@ -1,17 +1,18 @@ import { AggregateRootRepositoryManager, AggregateRootRepositoryProxyHandler } from "@"; import { OKA } from "@hexancore/common"; import { mock, type M } from "@hexancore/mocker"; -import { Author, type AuthorRepository } from "@test/src/Test/Domain"; +import { Book } from "@test/libs/test-lib/src/Book/Domain/Book/Book"; +import type { BookRepository } from "@test/libs/test-lib/src/Book/Domain/Book/BookRepository"; describe('AggregateRootRepositoryProxy', () => { - test("df", async () => { + test("create", async () => { const manager: M = mock(); - const repository: M = mock(); + const repository: M = mock(); repository.expects('getAll').andReturn(OKA(10 as any)); - manager.expects("get", Author).andReturn(repository.i); + manager.expects("get", Book).andReturn(repository.i); - const p = AggregateRootRepositoryProxyHandler.create(manager.i, Author) as unknown as AuthorRepository; + const p = AggregateRootRepositoryProxyHandler.create(manager.i, Book) as unknown as BookRepository; expect(await p.getAll()).toMatchSuccessResult(10); }); diff --git a/test/unit/Util/FeatureModuleDiscoverer.test.ts b/test/unit/Util/Feature/FeatureModuleDiscoverer.test.ts similarity index 81% rename from test/unit/Util/FeatureModuleDiscoverer.test.ts rename to test/unit/Util/Feature/FeatureModuleDiscoverer.test.ts index 81fd212..ca8ba90 100644 --- a/test/unit/Util/FeatureModuleDiscoverer.test.ts +++ b/test/unit/Util/Feature/FeatureModuleDiscoverer.test.ts @@ -2,7 +2,7 @@ * @group unit */ -import { FeatureModuleDiscoverer } from "@/Util/FeatureModuleDiscoverer"; +import { FeatureModuleDiscoverer } from "@/Util/Feature/FeatureModuleDiscoverer"; import { LIBS_DIRNAME } from "@test/libs"; describe(FeatureModuleDiscoverer.constructor.name, () => { diff --git a/test/unit/Util/__snapshots__/FeatureModuleDiscoverer.test.ts.snap b/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap similarity index 64% rename from test/unit/Util/__snapshots__/FeatureModuleDiscoverer.test.ts.snap rename to test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap index 84c576f..4255485 100644 --- a/test/unit/Util/__snapshots__/FeatureModuleDiscoverer.test.ts.snap +++ b/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap @@ -2,7 +2,7 @@ exports[`Function discoverAll 1`] = ` Map { - "Book" => { + "Book" => FeatureModuleMeta { "application": { "commands": [ { @@ -38,13 +38,30 @@ Map { }, ], }, - "cacheKey": "7a7e06e234e8d33002e7d6d539fdbe522521291c", + "cacheKey": "1261b3da77abfb8027ef4327b11cd95dc26847c0", + "domain": { + "aggregateRoots": [ + { + "entities": [ + { + "infraRepositoryName": "InfraBookCopyRepository", + "name": "BookCopy", + "repositoryName": "BookCopyRepository", + }, + ], + "infraRepositoryName": "InfraBookRepository", + "name": "Book", + "repositoryName": "BookRepository", + }, + ], + }, "infrastructure": { "module": { - "className": "BookInfrastructureModule", - "path": "Infrastructure/BookInfrastructureModule", + "className": "BookInfraModule", + "path": "Infrastructure/BookInfraModule", }, }, + "name": "Book", }, } `; diff --git a/tsconfig.build.json b/tsconfig.build.json index 8c53e32..32f2f4e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,9 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "declarationMap": false, + "sourceMap": false, + }, "files": [], "include": ["src/**/*"] diff --git a/tsconfig.json b/tsconfig.json index 75ea4ad..0a4109d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "noImplicitAny": false, "importHelpers": true, "declaration": true, - "declarationMap": false, + "sourceMap": true, "removeComments": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, @@ -42,7 +42,8 @@ ], "include": [ "src/**/*.ts", - "test/**/*" ], + "test/helper/*" + ], "typedocOptions": { "mode": "file", "out": "./docs",