refactor: 迁移博客镜像与资产模块
This commit is contained in:
parent
51f05acf55
commit
679aabb40d
@ -857,23 +857,23 @@ Actual: Admin repo remained clean after Task 2.2 verification, so Batch 2 create
|
||||
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\wordpress\**`
|
||||
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\asset\**`
|
||||
|
||||
- [ ] **Step 1: Write RED tests for public Blog behavior**
|
||||
- [x] **Step 1: Write RED tests for public Blog behavior**
|
||||
|
||||
Cover article list, article detail, term relation, and theme profile.
|
||||
|
||||
- [ ] **Step 2: Write RED tests for WordPress mapping**
|
||||
- [x] **Step 2: Write RED tests for WordPress mapping**
|
||||
|
||||
Cover remote ID to local post/term mapping and sync job state.
|
||||
|
||||
- [ ] **Step 3: Write RED tests for Asset ownership**
|
||||
- [x] **Step 3: Write RED tests for Asset ownership**
|
||||
|
||||
Cover object owner module, MIME metadata, reference, and access grant.
|
||||
|
||||
- [ ] **Step 4: Implement modules**
|
||||
- [x] **Step 4: Implement modules**
|
||||
|
||||
Keep route compatibility for public Blog endpoints and Admin-facing endpoints. Move MinIO ownership out of legacy `src/minio` into `src/modules/asset`.
|
||||
|
||||
- [ ] **Step 5: Verify**
|
||||
- [x] **Step 5: Verify**
|
||||
|
||||
Run:
|
||||
|
||||
@ -884,6 +884,8 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Actual note: Batch 3 moved API ownership to transitional `BlogContentModule`, `WordpressMirrorModule`, and `AssetModule` boundaries while retaining legacy business internals behind imported modules for compatibility. Contract tests now parse `sql/refactor-v3/00-full-schema.sql` through `test/helpers/sql-schema.helper.ts` so domain expectations catch SQL schema drift instead of only repeating local constants. Route compatibility and duplicate-controller safety are covered by module graph tests, with `QqbotModule` mocked in metadata-only specs to avoid unrelated BangDream/NapCat side effects.
|
||||
|
||||
### Task 3.2: Sync Admin Blog/WordPress/Asset pages
|
||||
|
||||
**Files:**
|
||||
@ -893,15 +895,15 @@ Expected: PASS.
|
||||
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\blog\**`
|
||||
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\router\routes\modules\blog.ts`
|
||||
|
||||
- [ ] **Step 1: Update callers**
|
||||
- [x] **Step 1: Update callers**
|
||||
|
||||
Use string IDs and explicit response types.
|
||||
|
||||
- [ ] **Step 2: Keep pages work-focused**
|
||||
- [x] **Step 2: Keep pages work-focused**
|
||||
|
||||
Use KtTable or existing forms; avoid landing-page or card-heavy redesign.
|
||||
|
||||
- [ ] **Step 3: Verify**
|
||||
- [x] **Step 3: Verify**
|
||||
|
||||
Run:
|
||||
|
||||
@ -911,6 +913,8 @@ pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typec
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Actual note: Admin Blog/WordPress ID-facing types were aligned to string IDs, `api/blog/asset.ts` was added against the existing `/minio/*` compatibility routes, and the Blog router/page layout was left unchanged because Batch 3 did not introduce a new visible page structure.
|
||||
|
||||
### Task 3.3: Commit Batch 3
|
||||
|
||||
Run:
|
||||
|
||||
@ -6,7 +6,6 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MinioModule } from 'nestjs-minio-client';
|
||||
import { MinioClientModule } from './minio/minio.module';
|
||||
import {
|
||||
ApiRequestLogInterceptor,
|
||||
ApiExceptionFilter,
|
||||
@ -15,8 +14,9 @@ import {
|
||||
SaveBodyInterceptor,
|
||||
} from './common';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { BlogModule } from './blog/blog.module';
|
||||
import { WordpressModule } from './wordpress/wordpress.module';
|
||||
import { AssetModule } from './modules/asset/asset.module';
|
||||
import { BlogContentModule } from './modules/blog/blog-content.module';
|
||||
import { WordpressMirrorModule } from './modules/wordpress/wordpress-mirror.module';
|
||||
import { QqbotModule } from './qqbot/qqbot.module';
|
||||
import { RuntimeModule } from './runtime';
|
||||
|
||||
@ -63,12 +63,12 @@ import { RuntimeModule } from './runtime';
|
||||
},
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
MinioClientModule,
|
||||
CommonModule,
|
||||
RuntimeModule,
|
||||
AdminModule,
|
||||
BlogModule,
|
||||
WordpressModule,
|
||||
BlogContentModule,
|
||||
WordpressMirrorModule,
|
||||
AssetModule,
|
||||
QqbotModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@ -15,7 +15,7 @@ import { AdminUserManageController } from '@/admin/user/admin-user-manage.contro
|
||||
import { AdminUserController } from '@/admin/user/admin-user.controller';
|
||||
import { AdminUser } from '@/admin/user/admin-user.entity';
|
||||
import { AdminUserService } from '@/admin/user/admin-user.service';
|
||||
import { WordpressModule } from '@/wordpress/wordpress.module';
|
||||
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
|
||||
|
||||
export const ADMIN_IDENTITY_CONTROLLERS = [
|
||||
AdminAuthController,
|
||||
@ -37,7 +37,7 @@ export const ADMIN_IDENTITY_PROVIDERS = [
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AdminUser, AdminRole, AdminMenu, AdminDept]),
|
||||
AdminAuthGuardModule,
|
||||
WordpressModule,
|
||||
WordpressMirrorModule,
|
||||
],
|
||||
controllers: ADMIN_IDENTITY_CONTROLLERS,
|
||||
providers: ADMIN_IDENTITY_PROVIDERS,
|
||||
|
||||
@ -14,7 +14,7 @@ import { AdminTimezoneController } from '@/admin/timezone/admin-timezone.control
|
||||
import { AdminTimezoneService } from '@/admin/timezone/admin-timezone.service';
|
||||
import { AdminUser } from '@/admin/user/admin-user.entity';
|
||||
import { AdminExampleController } from '@/admin/example/admin-example.controller';
|
||||
import { MinioClientModule } from '@/minio/minio.module';
|
||||
import { AssetModule } from '@/modules/asset/asset.module';
|
||||
|
||||
export const ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS = [
|
||||
ComponentController,
|
||||
@ -45,7 +45,7 @@ export const ADMIN_PLATFORM_CONFIG_PROVIDERS = [
|
||||
AdminAuthGuardModule,
|
||||
DictModule,
|
||||
NoticeModule,
|
||||
MinioClientModule,
|
||||
AssetModule,
|
||||
],
|
||||
controllers: ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS,
|
||||
providers: ADMIN_PLATFORM_CONFIG_PROVIDERS,
|
||||
|
||||
47
src/modules/asset/asset.module.ts
Normal file
47
src/modules/asset/asset.module.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MinioClientController } from '@/minio/minio.controller';
|
||||
import { MinioClientModule } from '@/minio/minio.module';
|
||||
import { MinioClientService } from '@/minio/minio.service';
|
||||
|
||||
export const ASSET_CONTROLLERS = [MinioClientController];
|
||||
|
||||
export const ASSET_PROVIDERS = [MinioClientService];
|
||||
|
||||
export const ASSET_DOMAIN_CONTRACT = {
|
||||
tables: [
|
||||
'asset_bucket',
|
||||
'asset_object',
|
||||
'asset_reference',
|
||||
'asset_access_grant',
|
||||
],
|
||||
objectOwnership: {
|
||||
objectTable: 'asset_object',
|
||||
bucketTable: 'asset_bucket',
|
||||
ownerModuleField: 'source_module',
|
||||
objectKeyField: 'object_key',
|
||||
},
|
||||
mimeMetadata: {
|
||||
objectTable: 'asset_object',
|
||||
mimeField: 'mime_type',
|
||||
sizeField: 'size_bytes',
|
||||
metadataField: 'metadata_json',
|
||||
},
|
||||
reference: {
|
||||
table: 'asset_reference',
|
||||
objectKey: 'object_id',
|
||||
ownerFields: ['owner_module', 'owner_type', 'owner_id'],
|
||||
},
|
||||
accessGrant: {
|
||||
table: 'asset_access_grant',
|
||||
objectKey: 'object_id',
|
||||
tokenField: 'grant_token',
|
||||
expiresAtField: 'expires_at',
|
||||
statusField: 'status',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Module({
|
||||
imports: [MinioClientModule],
|
||||
exports: [MinioClientModule],
|
||||
})
|
||||
export class AssetModule {}
|
||||
66
src/modules/blog/blog-content.module.ts
Normal file
66
src/modules/blog/blog-content.module.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BlogArticleController } from '@/blog/blog-article.controller';
|
||||
import { BlogArticleService } from '@/blog/blog-article.service';
|
||||
import { BlogModule } from '@/blog/blog.module';
|
||||
import { BlogTermController } from '@/blog/blog-term.controller';
|
||||
import { BlogTermService } from '@/blog/blog-term.service';
|
||||
import { BlogThemeConfigController } from '@/blog/blog-theme-config.controller';
|
||||
import { BlogThemeConfigService } from '@/blog/blog-theme-config.service';
|
||||
|
||||
export const BLOG_CONTENT_CONTROLLERS = [
|
||||
BlogArticleController,
|
||||
BlogTermController,
|
||||
BlogThemeConfigController,
|
||||
];
|
||||
|
||||
export const BLOG_CONTENT_PROVIDERS = [
|
||||
BlogArticleService,
|
||||
BlogTermService,
|
||||
BlogThemeConfigService,
|
||||
];
|
||||
|
||||
export const BLOG_CONTENT_DOMAIN_CONTRACT = {
|
||||
tables: [
|
||||
'blog_post',
|
||||
'blog_taxonomy',
|
||||
'blog_term',
|
||||
'blog_post_term',
|
||||
'blog_theme_profile',
|
||||
'blog_import_job',
|
||||
],
|
||||
publicArticleList: {
|
||||
route: 'GET /blog/article/public/list',
|
||||
sourceTable: 'blog_post',
|
||||
statusField: 'status',
|
||||
publishTimeField: 'publish_time',
|
||||
},
|
||||
publicArticleDetail: {
|
||||
route: 'GET /blog/article/public/detail',
|
||||
sourceTable: 'blog_post',
|
||||
lookupFields: ['id', 'slug'],
|
||||
},
|
||||
termRelation: {
|
||||
relationTable: 'blog_post_term',
|
||||
postKey: 'post_id',
|
||||
termKey: 'term_id',
|
||||
taxonomyTables: ['blog_taxonomy', 'blog_term'],
|
||||
},
|
||||
themeProfile: {
|
||||
table: 'blog_theme_profile',
|
||||
profileKey: 'profile_key',
|
||||
configField: 'config_json',
|
||||
enabledField: 'enabled',
|
||||
},
|
||||
importJob: {
|
||||
table: 'blog_import_job',
|
||||
sourceField: 'source_key',
|
||||
statusField: 'status',
|
||||
summaryField: 'summary_json',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Module({
|
||||
imports: [BlogModule],
|
||||
exports: [BlogModule],
|
||||
})
|
||||
export class BlogContentModule {}
|
||||
59
src/modules/wordpress/wordpress-mirror.module.ts
Normal file
59
src/modules/wordpress/wordpress-mirror.module.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WordpressArticleController } from '@/wordpress/wordpress-article.controller';
|
||||
import { WordpressAuthController } from '@/wordpress/wordpress-auth.controller';
|
||||
import { WordpressCategoryController } from '@/wordpress/wordpress-category.controller';
|
||||
import { WordpressModule } from '@/wordpress/wordpress.module';
|
||||
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||
import { WordpressTagController } from '@/wordpress/wordpress-tag.controller';
|
||||
import { WordpressThemeController } from '@/wordpress/wordpress-theme.controller';
|
||||
|
||||
export const WORDPRESS_MIRROR_CONTROLLERS = [
|
||||
WordpressAuthController,
|
||||
WordpressArticleController,
|
||||
WordpressTagController,
|
||||
WordpressCategoryController,
|
||||
WordpressThemeController,
|
||||
];
|
||||
|
||||
export const WORDPRESS_MIRROR_PROVIDERS = [WordpressService];
|
||||
|
||||
export const WORDPRESS_MIRROR_DOMAIN_CONTRACT = {
|
||||
tables: [
|
||||
'wordpress_site',
|
||||
'wordpress_auth_session',
|
||||
'wordpress_remote_post',
|
||||
'wordpress_remote_term',
|
||||
'wordpress_sync_job',
|
||||
'wordpress_sync_mapping',
|
||||
],
|
||||
remotePostMapping: {
|
||||
remoteTable: 'wordpress_remote_post',
|
||||
mappingTable: 'wordpress_sync_mapping',
|
||||
remoteType: 'post',
|
||||
localType: 'blog_post',
|
||||
remoteKeys: ['site_id', 'remote_id'],
|
||||
localKey: 'local_id',
|
||||
},
|
||||
remoteTermMapping: {
|
||||
remoteTable: 'wordpress_remote_term',
|
||||
mappingTable: 'wordpress_sync_mapping',
|
||||
remoteType: 'term',
|
||||
localType: 'blog_term',
|
||||
remoteKeys: ['site_id', 'taxonomy_key', 'remote_id'],
|
||||
localKey: 'local_id',
|
||||
},
|
||||
syncJob: {
|
||||
table: 'wordpress_sync_job',
|
||||
jobTypeField: 'job_type',
|
||||
statusField: 'status',
|
||||
startedAtField: 'started_at',
|
||||
finishedAtField: 'finished_at',
|
||||
summaryField: 'summary_json',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Module({
|
||||
imports: [WordpressModule],
|
||||
exports: [WordpressModule],
|
||||
})
|
||||
export class WordpressMirrorModule {}
|
||||
68
test/helpers/sql-schema.helper.ts
Normal file
68
test/helpers/sql-schema.helper.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const createTableRegex =
|
||||
/CREATE TABLE IF NOT EXISTS\s+([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)\s*ENGINE=/gi;
|
||||
|
||||
const nonColumnTokens = new Set([
|
||||
'CONSTRAINT',
|
||||
'FOREIGN',
|
||||
'KEY',
|
||||
'PRIMARY',
|
||||
'UNIQUE',
|
||||
]);
|
||||
|
||||
export type SqlSchemaContract = {
|
||||
hasTable(tableName: string): boolean;
|
||||
expectTableColumns(tableName: string, columns: readonly string[]): void;
|
||||
};
|
||||
|
||||
const parseColumnNames = (tableBlock: string) => {
|
||||
return tableBlock
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim().replace(/,$/, ''))
|
||||
.flatMap((line) => {
|
||||
const match = line.match(/^`?([A-Za-z_][A-Za-z0-9_]*)`?\s+/);
|
||||
|
||||
if (!match || nonColumnTokens.has(match[1].toUpperCase())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [match[1]];
|
||||
});
|
||||
};
|
||||
|
||||
const parseSchema = (sql: string) => {
|
||||
const tables = new Map<string, Set<string>>();
|
||||
|
||||
for (const match of sql.matchAll(createTableRegex)) {
|
||||
const [, tableName, tableBlock] = match;
|
||||
tables.set(tableName, new Set(parseColumnNames(tableBlock)));
|
||||
}
|
||||
|
||||
return tables;
|
||||
};
|
||||
|
||||
export const readRefactorV3SqlSchema = (): SqlSchemaContract => {
|
||||
const schemaPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'sql',
|
||||
'refactor-v3',
|
||||
'00-full-schema.sql',
|
||||
);
|
||||
const tables = parseSchema(fs.readFileSync(schemaPath, 'utf8'));
|
||||
|
||||
return {
|
||||
hasTable: (tableName) => tables.has(tableName),
|
||||
expectTableColumns: (tableName, columns) => {
|
||||
const tableColumns = tables.get(tableName);
|
||||
|
||||
expect(tableColumns).toBeDefined();
|
||||
expect(Array.from(tableColumns || [])).toEqual(
|
||||
expect.arrayContaining([...columns]),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
164
test/modules/asset/asset-module-contract.spec.ts
Normal file
164
test/modules/asset/asset-module-contract.spec.ts
Normal file
@ -0,0 +1,164 @@
|
||||
jest.mock('../../../src/qqbot/qqbot.module', () => ({
|
||||
QqbotModule: class QqbotModule {},
|
||||
}));
|
||||
|
||||
import { MODULE_METADATA } from '@nestjs/common/constants';
|
||||
import { AppModule } from '../../../src/app.module';
|
||||
import { MinioClientController } from '../../../src/minio/minio.controller';
|
||||
import { MinioClientModule } from '../../../src/minio/minio.module';
|
||||
import { MinioClientService } from '../../../src/minio/minio.service';
|
||||
import {
|
||||
ASSET_CONTROLLERS,
|
||||
ASSET_DOMAIN_CONTRACT,
|
||||
ASSET_PROVIDERS,
|
||||
AssetModule,
|
||||
} from '../../../src/modules/asset/asset.module';
|
||||
import { AdminPlatformConfigModule } from '../../../src/modules/admin/platform-config/admin-platform-config.module';
|
||||
import {
|
||||
collectControllerRoutes,
|
||||
routeKey,
|
||||
} from '../../helpers/controller-route.helper';
|
||||
import { readRefactorV3SqlSchema } from '../../helpers/sql-schema.helper';
|
||||
|
||||
const getModuleMetadata = <T>(moduleClass: unknown, key: string): T[] => {
|
||||
return Reflect.getMetadata(key, moduleClass) || [];
|
||||
};
|
||||
|
||||
const expectControllersNotRegisteredDirectly = (
|
||||
moduleClass: unknown,
|
||||
controllers: unknown[],
|
||||
) => {
|
||||
const directControllers = getModuleMetadata(
|
||||
moduleClass,
|
||||
MODULE_METADATA.CONTROLLERS,
|
||||
);
|
||||
|
||||
for (const controller of controllers) {
|
||||
expect(directControllers).not.toContain(controller);
|
||||
}
|
||||
};
|
||||
|
||||
describe('Asset module contract', () => {
|
||||
const schema = readRefactorV3SqlSchema();
|
||||
|
||||
it('keeps MinIO-backed asset routes compatible through the Asset boundary', () => {
|
||||
const routes = collectControllerRoutes(ASSET_CONTROLLERS);
|
||||
|
||||
expect(routes.map(routeKey)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'GET /minio/check',
|
||||
'POST /minio/bucket',
|
||||
'POST /minio/upload',
|
||||
'GET /minio/list',
|
||||
'GET /minio/url',
|
||||
'GET /minio/resource-proxy',
|
||||
'GET /minio/download',
|
||||
'DELETE /minio/remove',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes MinIO through the Asset module boundary without duplicate direct controllers', () => {
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).toEqual(
|
||||
expect.arrayContaining([AssetModule]),
|
||||
);
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).not.toEqual(
|
||||
expect.arrayContaining([MinioClientModule]),
|
||||
);
|
||||
expect(
|
||||
getModuleMetadata(AdminPlatformConfigModule, MODULE_METADATA.IMPORTS),
|
||||
).toEqual(expect.arrayContaining([AssetModule]));
|
||||
expect(
|
||||
getModuleMetadata(AdminPlatformConfigModule, MODULE_METADATA.IMPORTS),
|
||||
).not.toEqual(expect.arrayContaining([MinioClientModule]));
|
||||
|
||||
expect(getModuleMetadata(AssetModule, MODULE_METADATA.IMPORTS)).toEqual(
|
||||
expect.arrayContaining([MinioClientModule]),
|
||||
);
|
||||
expect(getModuleMetadata(AssetModule, MODULE_METADATA.EXPORTS)).toEqual(
|
||||
expect.arrayContaining([MinioClientModule]),
|
||||
);
|
||||
expectControllersNotRegisteredDirectly(AssetModule, ASSET_CONTROLLERS);
|
||||
|
||||
expect(
|
||||
getModuleMetadata(MinioClientModule, MODULE_METADATA.CONTROLLERS),
|
||||
).toEqual(expect.arrayContaining([MinioClientController]));
|
||||
expect(
|
||||
getModuleMetadata(MinioClientModule, MODULE_METADATA.PROVIDERS),
|
||||
).toEqual(expect.arrayContaining([MinioClientService]));
|
||||
expect(ASSET_CONTROLLERS).toEqual(
|
||||
expect.arrayContaining([MinioClientController]),
|
||||
);
|
||||
expect(ASSET_PROVIDERS).toEqual(
|
||||
expect.arrayContaining([MinioClientService]),
|
||||
);
|
||||
});
|
||||
|
||||
it('matches the real Batch 3 Asset SQL schema and boundary contract', () => {
|
||||
expect(ASSET_DOMAIN_CONTRACT.tables).toEqual([
|
||||
'asset_bucket',
|
||||
'asset_object',
|
||||
'asset_reference',
|
||||
'asset_access_grant',
|
||||
]);
|
||||
for (const table of ASSET_DOMAIN_CONTRACT.tables) {
|
||||
expect(schema.hasTable(table)).toBe(true);
|
||||
}
|
||||
|
||||
schema.expectTableColumns('asset_bucket', [
|
||||
'id',
|
||||
'bucket_key',
|
||||
'bucket_name',
|
||||
'provider',
|
||||
'status',
|
||||
]);
|
||||
schema.expectTableColumns('asset_object', [
|
||||
'id',
|
||||
'bucket_id',
|
||||
'object_key',
|
||||
'source_module',
|
||||
'mime_type',
|
||||
'size_bytes',
|
||||
'metadata_json',
|
||||
]);
|
||||
schema.expectTableColumns('asset_reference', [
|
||||
'id',
|
||||
'object_id',
|
||||
'owner_module',
|
||||
'owner_type',
|
||||
'owner_id',
|
||||
]);
|
||||
schema.expectTableColumns('asset_access_grant', [
|
||||
'id',
|
||||
'object_id',
|
||||
'grant_token',
|
||||
'expires_at',
|
||||
'status',
|
||||
]);
|
||||
|
||||
expect(ASSET_DOMAIN_CONTRACT.objectOwnership).toEqual({
|
||||
objectTable: 'asset_object',
|
||||
bucketTable: 'asset_bucket',
|
||||
ownerModuleField: 'source_module',
|
||||
objectKeyField: 'object_key',
|
||||
});
|
||||
expect(ASSET_DOMAIN_CONTRACT.mimeMetadata).toEqual({
|
||||
objectTable: 'asset_object',
|
||||
mimeField: 'mime_type',
|
||||
sizeField: 'size_bytes',
|
||||
metadataField: 'metadata_json',
|
||||
});
|
||||
expect(ASSET_DOMAIN_CONTRACT.reference).toEqual({
|
||||
table: 'asset_reference',
|
||||
objectKey: 'object_id',
|
||||
ownerFields: ['owner_module', 'owner_type', 'owner_id'],
|
||||
});
|
||||
expect(ASSET_DOMAIN_CONTRACT.accessGrant).toEqual({
|
||||
table: 'asset_access_grant',
|
||||
objectKey: 'object_id',
|
||||
tokenField: 'grant_token',
|
||||
expiresAtField: 'expires_at',
|
||||
statusField: 'status',
|
||||
});
|
||||
});
|
||||
});
|
||||
204
test/modules/blog/blog-module-contract.spec.ts
Normal file
204
test/modules/blog/blog-module-contract.spec.ts
Normal file
@ -0,0 +1,204 @@
|
||||
jest.mock('../../../src/qqbot/qqbot.module', () => ({
|
||||
QqbotModule: class QqbotModule {},
|
||||
}));
|
||||
|
||||
import { MODULE_METADATA } from '@nestjs/common/constants';
|
||||
import { AppModule } from '../../../src/app.module';
|
||||
import { BlogArticleController } from '../../../src/blog/blog-article.controller';
|
||||
import { BlogArticleService } from '../../../src/blog/blog-article.service';
|
||||
import { BlogModule } from '../../../src/blog/blog.module';
|
||||
import { BlogTermController } from '../../../src/blog/blog-term.controller';
|
||||
import { BlogTermService } from '../../../src/blog/blog-term.service';
|
||||
import { BlogThemeConfigController } from '../../../src/blog/blog-theme-config.controller';
|
||||
import { BlogThemeConfigService } from '../../../src/blog/blog-theme-config.service';
|
||||
import {
|
||||
BLOG_CONTENT_CONTROLLERS,
|
||||
BLOG_CONTENT_DOMAIN_CONTRACT,
|
||||
BLOG_CONTENT_PROVIDERS,
|
||||
BlogContentModule,
|
||||
} from '../../../src/modules/blog/blog-content.module';
|
||||
import {
|
||||
collectControllerRoutes,
|
||||
routeKey,
|
||||
} from '../../helpers/controller-route.helper';
|
||||
import { readRefactorV3SqlSchema } from '../../helpers/sql-schema.helper';
|
||||
|
||||
const getModuleMetadata = <T>(moduleClass: unknown, key: string): T[] => {
|
||||
return Reflect.getMetadata(key, moduleClass) || [];
|
||||
};
|
||||
|
||||
const expectControllersNotRegisteredDirectly = (
|
||||
moduleClass: unknown,
|
||||
controllers: unknown[],
|
||||
) => {
|
||||
const directControllers = getModuleMetadata(
|
||||
moduleClass,
|
||||
MODULE_METADATA.CONTROLLERS,
|
||||
);
|
||||
|
||||
for (const controller of controllers) {
|
||||
expect(directControllers).not.toContain(controller);
|
||||
}
|
||||
};
|
||||
|
||||
describe('Blog content module contract', () => {
|
||||
const schema = readRefactorV3SqlSchema();
|
||||
|
||||
it('keeps public and Admin-facing Blog routes compatible', () => {
|
||||
const routes = collectControllerRoutes(BLOG_CONTENT_CONTROLLERS);
|
||||
|
||||
expect(routes.map(routeKey)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'GET /blog/article/public/list',
|
||||
'GET /blog/article/public/detail',
|
||||
'GET /blog/article/list',
|
||||
'GET /blog/article/detail',
|
||||
'POST /blog/article/save',
|
||||
'POST /blog/article/update',
|
||||
'POST /blog/article/remove',
|
||||
'GET /blog/article/category-options',
|
||||
'GET /blog/article/tag-options',
|
||||
'POST /blog/article/import-wordpress',
|
||||
'GET /blog/category/list',
|
||||
'GET /blog/category/detail',
|
||||
'POST /blog/category/save',
|
||||
'POST /blog/category/update',
|
||||
'POST /blog/category/remove',
|
||||
'GET /blog/tag/list',
|
||||
'GET /blog/tag/detail',
|
||||
'POST /blog/tag/save',
|
||||
'POST /blog/tag/update',
|
||||
'POST /blog/tag/remove',
|
||||
'GET /blog/term/options',
|
||||
'GET /blog/theme/config',
|
||||
'POST /blog/theme/save',
|
||||
'POST /blog/theme/import-wordpress',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes Blog through the new module boundary without duplicate direct controllers', () => {
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).toEqual(
|
||||
expect.arrayContaining([BlogContentModule]),
|
||||
);
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).not.toEqual(
|
||||
expect.arrayContaining([BlogModule]),
|
||||
);
|
||||
|
||||
expect(
|
||||
getModuleMetadata(BlogContentModule, MODULE_METADATA.IMPORTS),
|
||||
).toEqual(expect.arrayContaining([BlogModule]));
|
||||
expect(
|
||||
getModuleMetadata(BlogContentModule, MODULE_METADATA.EXPORTS),
|
||||
).toEqual(expect.arrayContaining([BlogModule]));
|
||||
expectControllersNotRegisteredDirectly(
|
||||
BlogContentModule,
|
||||
BLOG_CONTENT_CONTROLLERS,
|
||||
);
|
||||
|
||||
expect(getModuleMetadata(BlogModule, MODULE_METADATA.CONTROLLERS)).toEqual(
|
||||
expect.arrayContaining([
|
||||
BlogArticleController,
|
||||
BlogTermController,
|
||||
BlogThemeConfigController,
|
||||
]),
|
||||
);
|
||||
expect(getModuleMetadata(BlogModule, MODULE_METADATA.PROVIDERS)).toEqual(
|
||||
expect.arrayContaining([
|
||||
BlogArticleService,
|
||||
BlogTermService,
|
||||
BlogThemeConfigService,
|
||||
]),
|
||||
);
|
||||
expect(BLOG_CONTENT_CONTROLLERS).toEqual(
|
||||
expect.arrayContaining([
|
||||
BlogArticleController,
|
||||
BlogTermController,
|
||||
BlogThemeConfigController,
|
||||
]),
|
||||
);
|
||||
expect(BLOG_CONTENT_PROVIDERS).toEqual(
|
||||
expect.arrayContaining([
|
||||
BlogArticleService,
|
||||
BlogTermService,
|
||||
BlogThemeConfigService,
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('matches the real Batch 3 Blog content SQL schema and public surface contract', () => {
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.tables).toEqual([
|
||||
'blog_post',
|
||||
'blog_taxonomy',
|
||||
'blog_term',
|
||||
'blog_post_term',
|
||||
'blog_theme_profile',
|
||||
'blog_import_job',
|
||||
]);
|
||||
for (const table of BLOG_CONTENT_DOMAIN_CONTRACT.tables) {
|
||||
expect(schema.hasTable(table)).toBe(true);
|
||||
}
|
||||
|
||||
schema.expectTableColumns('blog_post', [
|
||||
'id',
|
||||
'slug',
|
||||
'title',
|
||||
'status',
|
||||
'publish_time',
|
||||
]);
|
||||
schema.expectTableColumns('blog_taxonomy', [
|
||||
'id',
|
||||
'taxonomy_key',
|
||||
'taxonomy_name',
|
||||
]);
|
||||
schema.expectTableColumns('blog_term', [
|
||||
'id',
|
||||
'taxonomy_id',
|
||||
'slug',
|
||||
'term_name',
|
||||
]);
|
||||
schema.expectTableColumns('blog_post_term', ['id', 'post_id', 'term_id']);
|
||||
schema.expectTableColumns('blog_theme_profile', [
|
||||
'id',
|
||||
'profile_key',
|
||||
'config_json',
|
||||
'enabled',
|
||||
]);
|
||||
schema.expectTableColumns('blog_import_job', [
|
||||
'id',
|
||||
'source_key',
|
||||
'status',
|
||||
'summary_json',
|
||||
]);
|
||||
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.publicArticleList).toEqual({
|
||||
route: 'GET /blog/article/public/list',
|
||||
sourceTable: 'blog_post',
|
||||
statusField: 'status',
|
||||
publishTimeField: 'publish_time',
|
||||
});
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.publicArticleDetail).toEqual({
|
||||
route: 'GET /blog/article/public/detail',
|
||||
sourceTable: 'blog_post',
|
||||
lookupFields: ['id', 'slug'],
|
||||
});
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.termRelation).toEqual({
|
||||
relationTable: 'blog_post_term',
|
||||
postKey: 'post_id',
|
||||
termKey: 'term_id',
|
||||
taxonomyTables: ['blog_taxonomy', 'blog_term'],
|
||||
});
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.themeProfile).toEqual({
|
||||
table: 'blog_theme_profile',
|
||||
profileKey: 'profile_key',
|
||||
configField: 'config_json',
|
||||
enabledField: 'enabled',
|
||||
});
|
||||
expect(BLOG_CONTENT_DOMAIN_CONTRACT.importJob).toEqual({
|
||||
table: 'blog_import_job',
|
||||
sourceField: 'source_key',
|
||||
statusField: 'status',
|
||||
summaryField: 'summary_json',
|
||||
});
|
||||
});
|
||||
});
|
||||
216
test/modules/wordpress/wordpress-module-contract.spec.ts
Normal file
216
test/modules/wordpress/wordpress-module-contract.spec.ts
Normal file
@ -0,0 +1,216 @@
|
||||
jest.mock('../../../src/qqbot/qqbot.module', () => ({
|
||||
QqbotModule: class QqbotModule {},
|
||||
}));
|
||||
|
||||
import { MODULE_METADATA } from '@nestjs/common/constants';
|
||||
import { AppModule } from '../../../src/app.module';
|
||||
import { AdminIdentityModule } from '../../../src/modules/admin/identity/admin-identity.module';
|
||||
import {
|
||||
WORDPRESS_MIRROR_CONTROLLERS,
|
||||
WORDPRESS_MIRROR_DOMAIN_CONTRACT,
|
||||
WORDPRESS_MIRROR_PROVIDERS,
|
||||
WordpressMirrorModule,
|
||||
} from '../../../src/modules/wordpress/wordpress-mirror.module';
|
||||
import { WordpressArticleController } from '../../../src/wordpress/wordpress-article.controller';
|
||||
import { WordpressAuthController } from '../../../src/wordpress/wordpress-auth.controller';
|
||||
import { WordpressCategoryController } from '../../../src/wordpress/wordpress-category.controller';
|
||||
import { WordpressModule } from '../../../src/wordpress/wordpress.module';
|
||||
import { WordpressService } from '../../../src/wordpress/wordpress.service';
|
||||
import { WordpressTagController } from '../../../src/wordpress/wordpress-tag.controller';
|
||||
import { WordpressThemeController } from '../../../src/wordpress/wordpress-theme.controller';
|
||||
import {
|
||||
collectControllerRoutes,
|
||||
routeKey,
|
||||
} from '../../helpers/controller-route.helper';
|
||||
import { readRefactorV3SqlSchema } from '../../helpers/sql-schema.helper';
|
||||
|
||||
const getModuleMetadata = <T>(moduleClass: unknown, key: string): T[] => {
|
||||
return Reflect.getMetadata(key, moduleClass) || [];
|
||||
};
|
||||
|
||||
const expectControllersNotRegisteredDirectly = (
|
||||
moduleClass: unknown,
|
||||
controllers: unknown[],
|
||||
) => {
|
||||
const directControllers = getModuleMetadata(
|
||||
moduleClass,
|
||||
MODULE_METADATA.CONTROLLERS,
|
||||
);
|
||||
|
||||
for (const controller of controllers) {
|
||||
expect(directControllers).not.toContain(controller);
|
||||
}
|
||||
};
|
||||
|
||||
describe('WordPress mirror module contract', () => {
|
||||
const schema = readRefactorV3SqlSchema();
|
||||
|
||||
it('keeps public and Admin-facing WordPress routes compatible', () => {
|
||||
const routes = collectControllerRoutes(WORDPRESS_MIRROR_CONTROLLERS);
|
||||
|
||||
expect(routes.map(routeKey)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'POST /wordpress/auth/login',
|
||||
'POST /wordpress/auth/logout',
|
||||
'GET /wordpress/auth/check',
|
||||
'GET /wordpress/article/public/list',
|
||||
'GET /wordpress/article/public/detail',
|
||||
'GET /wordpress/article/list',
|
||||
'GET /wordpress/article/detail',
|
||||
'POST /wordpress/article/save',
|
||||
'POST /wordpress/article/update',
|
||||
'POST /wordpress/article/remove',
|
||||
'GET /wordpress/tag/list',
|
||||
'GET /wordpress/tag/detail',
|
||||
'POST /wordpress/tag/save',
|
||||
'POST /wordpress/tag/update',
|
||||
'POST /wordpress/tag/remove',
|
||||
'GET /wordpress/category/list',
|
||||
'GET /wordpress/category/detail',
|
||||
'POST /wordpress/category/save',
|
||||
'POST /wordpress/category/update',
|
||||
'POST /wordpress/category/remove',
|
||||
'GET /wordpress/theme/config',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes WordPress through the new module boundary without duplicate direct controllers', () => {
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).toEqual(
|
||||
expect.arrayContaining([WordpressMirrorModule]),
|
||||
);
|
||||
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).not.toEqual(
|
||||
expect.arrayContaining([WordpressModule]),
|
||||
);
|
||||
expect(
|
||||
getModuleMetadata(AdminIdentityModule, MODULE_METADATA.IMPORTS),
|
||||
).toEqual(expect.arrayContaining([WordpressMirrorModule]));
|
||||
expect(
|
||||
getModuleMetadata(AdminIdentityModule, MODULE_METADATA.IMPORTS),
|
||||
).not.toEqual(expect.arrayContaining([WordpressModule]));
|
||||
|
||||
expect(
|
||||
getModuleMetadata(WordpressMirrorModule, MODULE_METADATA.IMPORTS),
|
||||
).toEqual(expect.arrayContaining([WordpressModule]));
|
||||
expect(
|
||||
getModuleMetadata(WordpressMirrorModule, MODULE_METADATA.EXPORTS),
|
||||
).toEqual(expect.arrayContaining([WordpressModule]));
|
||||
expectControllersNotRegisteredDirectly(
|
||||
WordpressMirrorModule,
|
||||
WORDPRESS_MIRROR_CONTROLLERS,
|
||||
);
|
||||
|
||||
expect(
|
||||
getModuleMetadata(WordpressModule, MODULE_METADATA.CONTROLLERS),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
WordpressAuthController,
|
||||
WordpressArticleController,
|
||||
WordpressTagController,
|
||||
WordpressCategoryController,
|
||||
WordpressThemeController,
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
getModuleMetadata(WordpressModule, MODULE_METADATA.PROVIDERS),
|
||||
).toEqual(expect.arrayContaining([WordpressService]));
|
||||
expect(WORDPRESS_MIRROR_CONTROLLERS).toEqual(
|
||||
expect.arrayContaining([
|
||||
WordpressAuthController,
|
||||
WordpressArticleController,
|
||||
WordpressTagController,
|
||||
WordpressCategoryController,
|
||||
WordpressThemeController,
|
||||
]),
|
||||
);
|
||||
expect(WORDPRESS_MIRROR_PROVIDERS).toEqual(
|
||||
expect.arrayContaining([WordpressService]),
|
||||
);
|
||||
});
|
||||
|
||||
it('matches the real Batch 3 WordPress mirror SQL schema and sync job contract', () => {
|
||||
expect(WORDPRESS_MIRROR_DOMAIN_CONTRACT.tables).toEqual([
|
||||
'wordpress_site',
|
||||
'wordpress_auth_session',
|
||||
'wordpress_remote_post',
|
||||
'wordpress_remote_term',
|
||||
'wordpress_sync_job',
|
||||
'wordpress_sync_mapping',
|
||||
]);
|
||||
for (const table of WORDPRESS_MIRROR_DOMAIN_CONTRACT.tables) {
|
||||
expect(schema.hasTable(table)).toBe(true);
|
||||
}
|
||||
|
||||
schema.expectTableColumns('wordpress_site', [
|
||||
'id',
|
||||
'site_key',
|
||||
'base_url',
|
||||
'status',
|
||||
]);
|
||||
schema.expectTableColumns('wordpress_auth_session', [
|
||||
'id',
|
||||
'site_id',
|
||||
'status',
|
||||
'expires_at',
|
||||
'safe_summary',
|
||||
]);
|
||||
schema.expectTableColumns('wordpress_remote_post', [
|
||||
'id',
|
||||
'site_id',
|
||||
'remote_id',
|
||||
'slug',
|
||||
'status',
|
||||
'raw_payload',
|
||||
]);
|
||||
schema.expectTableColumns('wordpress_remote_term', [
|
||||
'id',
|
||||
'site_id',
|
||||
'remote_id',
|
||||
'taxonomy_key',
|
||||
'slug',
|
||||
'raw_payload',
|
||||
]);
|
||||
schema.expectTableColumns('wordpress_sync_job', [
|
||||
'id',
|
||||
'site_id',
|
||||
'job_type',
|
||||
'status',
|
||||
'started_at',
|
||||
'finished_at',
|
||||
'summary_json',
|
||||
]);
|
||||
schema.expectTableColumns('wordpress_sync_mapping', [
|
||||
'id',
|
||||
'site_id',
|
||||
'remote_type',
|
||||
'remote_id',
|
||||
'local_type',
|
||||
'local_id',
|
||||
]);
|
||||
|
||||
expect(WORDPRESS_MIRROR_DOMAIN_CONTRACT.remotePostMapping).toEqual({
|
||||
remoteTable: 'wordpress_remote_post',
|
||||
mappingTable: 'wordpress_sync_mapping',
|
||||
remoteType: 'post',
|
||||
localType: 'blog_post',
|
||||
remoteKeys: ['site_id', 'remote_id'],
|
||||
localKey: 'local_id',
|
||||
});
|
||||
expect(WORDPRESS_MIRROR_DOMAIN_CONTRACT.remoteTermMapping).toEqual({
|
||||
remoteTable: 'wordpress_remote_term',
|
||||
mappingTable: 'wordpress_sync_mapping',
|
||||
remoteType: 'term',
|
||||
localType: 'blog_term',
|
||||
remoteKeys: ['site_id', 'taxonomy_key', 'remote_id'],
|
||||
localKey: 'local_id',
|
||||
});
|
||||
expect(WORDPRESS_MIRROR_DOMAIN_CONTRACT.syncJob).toEqual({
|
||||
table: 'wordpress_sync_job',
|
||||
jobTypeField: 'job_type',
|
||||
statusField: 'status',
|
||||
startedAtField: 'started_at',
|
||||
finishedAtField: 'finished_at',
|
||||
summaryField: 'summary_json',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user