@solidxai/core 0.1.10-beta.24 → 0.1.10-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/action-metadata.controller.d.ts +1 -20
- package/dist/controllers/action-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/agent-event.controller.d.ts +1 -20
- package/dist/controllers/agent-event.controller.d.ts.map +1 -1
- package/dist/controllers/agent-session.controller.d.ts +1 -20
- package/dist/controllers/agent-session.controller.d.ts.map +1 -1
- package/dist/controllers/ai-interaction.controller.d.ts +1 -20
- package/dist/controllers/ai-interaction.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message-details.controller.d.ts +1 -20
- package/dist/controllers/chatter-message-details.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message.controller.d.ts +1 -20
- package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
- package/dist/controllers/dashboard-user-layout.controller.d.ts +1 -20
- package/dist/controllers/dashboard-user-layout.controller.d.ts.map +1 -1
- package/dist/controllers/email-template.controller.d.ts +1 -20
- package/dist/controllers/email-template.controller.d.ts.map +1 -1
- package/dist/controllers/export-template.controller.d.ts +1 -20
- package/dist/controllers/export-template.controller.d.ts.map +1 -1
- package/dist/controllers/export-transaction.controller.d.ts +1 -20
- package/dist/controllers/export-transaction.controller.d.ts.map +1 -1
- package/dist/controllers/import-transaction-error-log.controller.d.ts +1 -20
- package/dist/controllers/import-transaction-error-log.controller.d.ts.map +1 -1
- package/dist/controllers/import-transaction.controller.d.ts +1 -20
- package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
- package/dist/controllers/list-of-values.controller.d.ts +1 -20
- package/dist/controllers/list-of-values.controller.d.ts.map +1 -1
- package/dist/controllers/locale.controller.d.ts +1 -20
- package/dist/controllers/locale.controller.d.ts.map +1 -1
- package/dist/controllers/mcp-audit-log.controller.d.ts +1 -20
- package/dist/controllers/mcp-audit-log.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.d.ts +1 -20
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/menu-item-metadata.controller.d.ts +1 -20
- package/dist/controllers/menu-item-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/model-metadata.controller.d.ts +10 -30
- package/dist/controllers/model-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/model-metadata.controller.js +2 -2
- package/dist/controllers/model-sequence.controller.d.ts +1 -20
- package/dist/controllers/model-sequence.controller.d.ts.map +1 -1
- package/dist/controllers/module-metadata-explorer.controller.d.ts +171 -0
- package/dist/controllers/module-metadata-explorer.controller.d.ts.map +1 -0
- package/dist/controllers/module-metadata-explorer.controller.js +143 -0
- package/dist/controllers/module-metadata-explorer.controller.js.map +1 -0
- package/dist/controllers/module-metadata.controller.d.ts +5 -0
- package/dist/controllers/module-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/module-metadata.controller.js +12 -0
- package/dist/controllers/module-metadata.controller.js.map +1 -1
- package/dist/controllers/module-package.controller.d.ts +622 -0
- package/dist/controllers/module-package.controller.d.ts.map +1 -0
- package/dist/controllers/module-package.controller.js +150 -0
- package/dist/controllers/module-package.controller.js.map +1 -0
- package/dist/controllers/mq-message-queue.controller.d.ts +1 -20
- package/dist/controllers/mq-message-queue.controller.d.ts.map +1 -1
- package/dist/controllers/mq-message.controller.d.ts +1 -20
- package/dist/controllers/mq-message.controller.d.ts.map +1 -1
- package/dist/controllers/permission-metadata.controller.d.ts +1 -20
- package/dist/controllers/permission-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/role-metadata.controller.d.ts +1 -20
- package/dist/controllers/role-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/saved-filters.controller.d.ts +1 -20
- package/dist/controllers/saved-filters.controller.d.ts.map +1 -1
- package/dist/controllers/scheduled-job.controller.d.ts +1 -20
- package/dist/controllers/scheduled-job.controller.d.ts.map +1 -1
- package/dist/controllers/security-rule.controller.d.ts +1 -20
- package/dist/controllers/security-rule.controller.d.ts.map +1 -1
- package/dist/controllers/sms-template.controller.d.ts +1 -20
- package/dist/controllers/sms-template.controller.d.ts.map +1 -1
- package/dist/controllers/user-activity-history.controller.d.ts +1 -20
- package/dist/controllers/user-activity-history.controller.d.ts.map +1 -1
- package/dist/controllers/user-view-metadata.controller.d.ts +1 -20
- package/dist/controllers/user-view-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/user.controller.d.ts +1 -20
- package/dist/controllers/user.controller.d.ts.map +1 -1
- package/dist/controllers/view-metadata.controller.d.ts +1 -20
- package/dist/controllers/view-metadata.controller.d.ts.map +1 -1
- package/dist/dtos/confirm-module-package-import.dto.d.ts +4 -0
- package/dist/dtos/confirm-module-package-import.dto.d.ts.map +1 -0
- package/dist/dtos/confirm-module-package-import.dto.js +34 -0
- package/dist/dtos/confirm-module-package-import.dto.js.map +1 -0
- package/dist/dtos/metadata-explorer-references-query.dto.d.ts +10 -0
- package/dist/dtos/metadata-explorer-references-query.dto.d.ts.map +1 -0
- package/dist/dtos/metadata-explorer-references-query.dto.js +69 -0
- package/dist/dtos/metadata-explorer-references-query.dto.js.map +1 -0
- package/dist/dtos/metadata-explorer-search-query.dto.d.ts +8 -0
- package/dist/dtos/metadata-explorer-search-query.dto.d.ts.map +1 -0
- package/dist/dtos/metadata-explorer-search-query.dto.js +55 -0
- package/dist/dtos/metadata-explorer-search-query.dto.js.map +1 -0
- package/dist/dtos/metadata-explorer-write.dto.d.ts +4 -0
- package/dist/dtos/metadata-explorer-write.dto.d.ts.map +1 -0
- package/dist/dtos/metadata-explorer-write.dto.js +30 -0
- package/dist/dtos/metadata-explorer-write.dto.js.map +1 -0
- package/dist/dtos/run-module-package-build.dto.d.ts +5 -0
- package/dist/dtos/run-module-package-build.dto.d.ts.map +1 -0
- package/dist/dtos/run-module-package-build.dto.js +38 -0
- package/dist/dtos/run-module-package-build.dto.js.map +1 -0
- package/dist/dtos/run-module-package-seed.dto.d.ts +4 -0
- package/dist/dtos/run-module-package-seed.dto.d.ts.map +1 -0
- package/dist/dtos/run-module-package-seed.dto.js +34 -0
- package/dist/dtos/run-module-package-seed.dto.js.map +1 -0
- package/dist/helpers/command.service.js +1 -1
- package/dist/helpers/command.service.js.map +1 -1
- package/dist/helpers/module-metadata-helper.service.d.ts +2 -0
- package/dist/helpers/module-metadata-helper.service.d.ts.map +1 -1
- package/dist/helpers/module-metadata-helper.service.js +14 -12
- package/dist/helpers/module-metadata-helper.service.js.map +1 -1
- package/dist/helpers/module.helper.d.ts.map +1 -1
- package/dist/helpers/module.helper.js +11 -6
- package/dist/helpers/module.helper.js.map +1 -1
- package/dist/helpers/typeorm-migration-helpers.d.ts +12 -0
- package/dist/helpers/typeorm-migration-helpers.d.ts.map +1 -0
- package/dist/helpers/typeorm-migration-helpers.js +119 -0
- package/dist/helpers/typeorm-migration-helpers.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +1 -0
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +57 -38
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/module-test-data.service.js +1 -1
- package/dist/seeders/module-test-data.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +2364 -1106
- package/dist/services/crud-helper.service.d.ts +18 -0
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +67 -1
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts +1 -21
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +23 -87
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/genai/ingest-metadata.service.js +2 -2
- package/dist/services/genai/ingest-metadata.service.js.map +1 -1
- package/dist/services/media.service.d.ts +1 -20
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +31 -2
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js +5 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts +15 -30
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +249 -29
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/module-metadata-explorer.service.d.ts +161 -0
- package/dist/services/module-metadata-explorer.service.d.ts.map +1 -0
- package/dist/services/module-metadata-explorer.service.js +485 -0
- package/dist/services/module-metadata-explorer.service.js.map +1 -0
- package/dist/services/module-metadata.service.d.ts +7 -0
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +121 -2
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/module-package.service.d.ts +266 -0
- package/dist/services/module-package.service.d.ts.map +1 -0
- package/dist/services/module-package.service.js +980 -0
- package/dist/services/module-package.service.js.map +1 -0
- package/dist/services/settings/default-settings-provider.service.d.ts +18 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +10 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +10 -2
- package/dist/solid-core.module.js.map +1 -1
- package/dist-tests/api/authenticate.spec.js +119 -0
- package/dist-tests/api/authenticate.spec.js.map +1 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
- package/dist-tests/api/ping.spec.js +21 -0
- package/dist-tests/api/ping.spec.js.map +1 -0
- package/dist-tests/helpers/auth.js +41 -0
- package/dist-tests/helpers/auth.js.map +1 -0
- package/dist-tests/helpers/env.js +11 -0
- package/dist-tests/helpers/env.js.map +1 -0
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
- package/docs/grouping-enhancements.md +89 -0
- package/docs/java-spring/README.md +3 -0
- package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
- package/docs/module-package-import-handoff.md +691 -0
- package/docs/seed-changes.md +65 -0
- package/docs/test-data-workflow.md +200 -0
- package/docs/type-declaration-import-issue.md +24 -0
- package/package.json +2 -1
- package/src/controllers/module-metadata-explorer.controller.ts +83 -0
- package/src/controllers/module-metadata.controller.ts +6 -0
- package/src/controllers/module-package.controller.ts +102 -0
- package/src/dtos/confirm-module-package-import.dto.ts +12 -0
- package/src/dtos/metadata-explorer-references-query.dto.ts +39 -0
- package/src/dtos/metadata-explorer-search-query.dto.ts +28 -0
- package/src/dtos/metadata-explorer-write.dto.ts +11 -0
- package/src/dtos/run-module-package-build.dto.ts +14 -0
- package/src/dtos/run-module-package-seed.dto.ts +12 -0
- package/src/helpers/command.service.ts +1 -1
- package/src/helpers/module-metadata-helper.service.ts +19 -21
- package/src/helpers/module.helper.ts +12 -9
- package/src/helpers/typeorm-migration-helpers.ts +163 -0
- package/src/index.ts +8 -0
- package/src/seeders/module-metadata-seeder.service.ts +63 -38
- package/src/seeders/module-test-data.service.ts +1 -1
- package/src/seeders/seed-data/solid-core-metadata.json +2365 -1107
- package/src/services/crud-helper.service.ts +79 -1
- package/src/services/crud.service.ts +26 -119
- package/src/services/dashboard-providers/README.md +1 -1
- package/src/services/genai/ingest-metadata.service.ts +3 -3
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +37 -2
- package/src/services/mediaStorageProviders/file-storage-provider.ts +5 -1
- package/src/services/model-metadata.service.ts +302 -98
- package/src/services/module-metadata-explorer.service.ts +670 -0
- package/src/services/module-metadata.service.ts +136 -10
- package/src/services/module-package.service.ts +1171 -0
- package/src/services/settings/default-settings-provider.service.ts +10 -0
- package/src/solid-core.module.ts +10 -2
- package/.claude/settings.local.json +0 -15
- package/CLAUDE.md +0 -26
- package/dist/services/database/database-bootstrap.service.d.ts +0 -12
- package/dist/services/database/database-bootstrap.service.d.ts.map +0 -1
- package/dist/services/database/database-bootstrap.service.js +0 -115
- package/dist/services/database/database-bootstrap.service.js.map +0 -1
- package/sql/default/mariadb/proc_CleanupModelMetadata.sql +0 -153
- package/sql/default/mariadb/proc_CleanupModuleMetadata.sql +0 -56
- package/sql/default/mssql/proc_CleanupModuleMetadata.sql +0 -78
- package/sql/default/mysql/proc_CleanupModelMetadata.sql +0 -153
- package/sql/default/mysql/proc_CleanupModuleMetadata.sql +0 -56
- package/sql/default/postgres/proc_CleanupModelMetadata.sql +0 -148
- package/sql/default/postgres/proc_CleanupModuleMetadata.sql +0 -50
- package/sql/default/postgres/scratchpad.sql.txt +0 -12
- package/src/services/1.js +0 -6
- package/src/services/database/database-bootstrap.service.ts +0 -91
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
|
|
5
|
+
import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
|
|
6
|
+
import { MetadataExplorerReferencesQueryDto } from '../dtos/metadata-explorer-references-query.dto';
|
|
7
|
+
import { MetadataExplorerSearchQueryDto } from '../dtos/metadata-explorer-search-query.dto';
|
|
8
|
+
import { MetadataExplorerWriteDto } from '../dtos/metadata-explorer-write.dto';
|
|
9
|
+
|
|
10
|
+
type MetadataExplorerSectionType = 'object' | 'array';
|
|
11
|
+
|
|
12
|
+
type MetadataExplorerSectionDefinition = {
|
|
13
|
+
key: string;
|
|
14
|
+
title: string;
|
|
15
|
+
jsonPath: string;
|
|
16
|
+
type: MetadataExplorerSectionType;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MetadataValidationIssue = {
|
|
21
|
+
path: string;
|
|
22
|
+
message: string;
|
|
23
|
+
severity: 'error' | 'warning';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type MetadataSearchMatch = {
|
|
27
|
+
path: string;
|
|
28
|
+
sectionKey: string | null;
|
|
29
|
+
matchType: 'key' | 'value';
|
|
30
|
+
valueType: string;
|
|
31
|
+
preview: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SECTION_DEFINITIONS: MetadataExplorerSectionDefinition[] = [
|
|
35
|
+
{ key: 'module', title: 'Module', jsonPath: 'moduleMetadata', type: 'object', description: 'Top-level module metadata excluding nested models.' },
|
|
36
|
+
{ key: 'models', title: 'Models', jsonPath: 'moduleMetadata.models', type: 'array', description: 'Model metadata definitions for the module.' },
|
|
37
|
+
{ key: 'roles', title: 'Roles', jsonPath: 'roles', type: 'array', description: 'Role metadata definitions.' },
|
|
38
|
+
{ key: 'users', title: 'Users', jsonPath: 'users', type: 'array', description: 'Seed users and user-role assignments.' },
|
|
39
|
+
{ key: 'permissions', title: 'Permissions', jsonPath: 'permissions', type: 'array', description: 'Permission declarations and explicit additions.' },
|
|
40
|
+
{ key: 'actions', title: 'Actions', jsonPath: 'actions', type: 'array', description: 'Action metadata definitions.' },
|
|
41
|
+
{ key: 'menus', title: 'Menus', jsonPath: 'menus', type: 'array', description: 'Menu item metadata definitions.' },
|
|
42
|
+
{ key: 'views', title: 'Views', jsonPath: 'views', type: 'array', description: 'List, form, kanban, tree, and related view metadata.' },
|
|
43
|
+
{ key: 'emailTemplates', title: 'Email Templates', jsonPath: 'emailTemplates', type: 'array', description: 'Email template metadata.' },
|
|
44
|
+
{ key: 'smsTemplates', title: 'SMS Templates', jsonPath: 'smsTemplates', type: 'array', description: 'SMS template metadata.' },
|
|
45
|
+
{ key: 'mediaStorageProviders', title: 'Media Storage Providers', jsonPath: 'mediaStorageProviders', type: 'array', description: 'Configured media storage providers.' },
|
|
46
|
+
{ key: 'savedFilters', title: 'Saved Filters', jsonPath: 'savedFilters', type: 'array', description: 'Saved filter definitions.' },
|
|
47
|
+
{ key: 'securityRules', title: 'Security Rules', jsonPath: 'securityRules', type: 'array', description: 'Security rule definitions.' },
|
|
48
|
+
{ key: 'listOfValues', title: 'List Of Values', jsonPath: 'listOfValues', type: 'array', description: 'List of values entries.' },
|
|
49
|
+
{ key: 'dashboards', title: 'Dashboards', jsonPath: 'dashboards', type: 'array', description: 'Dashboard metadata definitions.' },
|
|
50
|
+
{ key: 'testing', title: 'Testing', jsonPath: 'testing', type: 'object', description: 'Testing roles, users, specs, and scenarios.' },
|
|
51
|
+
{ key: 'scheduledJobs', title: 'Scheduled Jobs', jsonPath: 'scheduledJobs', type: 'array', description: 'Scheduled job metadata definitions.' },
|
|
52
|
+
{ key: 'modelSequences', title: 'Model Sequences', jsonPath: 'modelSequences', type: 'array', description: 'Model sequence definitions.' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
@Injectable()
|
|
56
|
+
export class ModuleMetadataExplorerService {
|
|
57
|
+
private readonly logger = new Logger(ModuleMetadataExplorerService.name);
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
|
|
61
|
+
) { }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Input: module name string.
|
|
65
|
+
* Output: explorer manifest containing file details, section summaries, and document validation state.
|
|
66
|
+
* Logic: loads the module metadata JSON, computes section-level summary information, and returns
|
|
67
|
+
* the high-level descriptor the explorer UI can use to render navigation and status badges.
|
|
68
|
+
*/
|
|
69
|
+
async getManifest(moduleName: string) {
|
|
70
|
+
const { document, filePath, stats } = await this.loadMetadataDocument(moduleName);
|
|
71
|
+
const validation = this.validateDocumentValue(document);
|
|
72
|
+
|
|
73
|
+
const sections = SECTION_DEFINITIONS.map((definition) => {
|
|
74
|
+
const value = this.getValueAtPath(document, definition.jsonPath);
|
|
75
|
+
return {
|
|
76
|
+
...definition,
|
|
77
|
+
exists: value !== undefined,
|
|
78
|
+
itemCount: Array.isArray(value) ? value.length : undefined,
|
|
79
|
+
nodeCount: this.countNodes(value),
|
|
80
|
+
preview: this.buildPreview(value, 96),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
moduleName,
|
|
86
|
+
filePath,
|
|
87
|
+
lastModifiedAt: stats.mtime.toISOString(),
|
|
88
|
+
sections,
|
|
89
|
+
validation,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Input: module name string.
|
|
95
|
+
* Output: full metadata document payload with file metadata and validation results.
|
|
96
|
+
* Logic: reads the resolved module metadata JSON from disk and returns it as the canonical
|
|
97
|
+
* document payload for full-document editing scenarios.
|
|
98
|
+
*/
|
|
99
|
+
async getDocument(moduleName: string) {
|
|
100
|
+
const { document, filePath, stats } = await this.loadMetadataDocument(moduleName);
|
|
101
|
+
return {
|
|
102
|
+
moduleName,
|
|
103
|
+
filePath,
|
|
104
|
+
lastModifiedAt: stats.mtime.toISOString(),
|
|
105
|
+
value: document,
|
|
106
|
+
validation: this.validateDocumentValue(document),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Input: module name string and a DTO containing the replacement JSON document in `value`.
|
|
112
|
+
* Output: refreshed full document payload after a successful write.
|
|
113
|
+
* Logic: validates the incoming JSON document at a coarse structural level, writes it back to
|
|
114
|
+
* the resolved metadata file, and then reloads the saved document to return the persisted state.
|
|
115
|
+
*/
|
|
116
|
+
@DisallowInProduction()
|
|
117
|
+
async updateDocument(moduleName: string, dto: MetadataExplorerWriteDto) {
|
|
118
|
+
this.assertValueProvided(dto?.value);
|
|
119
|
+
const validation = this.validateDocumentValue(dto.value);
|
|
120
|
+
if (!validation.valid) {
|
|
121
|
+
throw new BadRequestException(validation);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
|
|
125
|
+
await this.writeMetadataDocument(filePath, dto.value);
|
|
126
|
+
return this.getDocument(moduleName);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Input: module name string and an optional DTO containing a document candidate in `value`.
|
|
131
|
+
* Output: validation result object with `valid` flag and issue list.
|
|
132
|
+
* Logic: validates either the supplied document candidate or the currently persisted metadata
|
|
133
|
+
* file without mutating anything, so the UI can perform pre-save checks.
|
|
134
|
+
*/
|
|
135
|
+
async validateDocument(moduleName: string, dto?: MetadataExplorerWriteDto) {
|
|
136
|
+
const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
|
|
137
|
+
const value = dto && Object.prototype.hasOwnProperty.call(dto, 'value')
|
|
138
|
+
? dto.value
|
|
139
|
+
: (await this.loadMetadataDocument(moduleName)).document;
|
|
140
|
+
return {
|
|
141
|
+
moduleName,
|
|
142
|
+
filePath,
|
|
143
|
+
...this.validateDocumentValue(value),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Input: module name string and a section key such as `models` or `views`.
|
|
149
|
+
* Output: section payload containing metadata about the section, its JSON value, and validation.
|
|
150
|
+
* Logic: resolves the section definition, extracts that fragment from the full metadata document,
|
|
151
|
+
* and returns the section in a UI-friendly envelope.
|
|
152
|
+
*/
|
|
153
|
+
async getSection(moduleName: string, sectionKey: string) {
|
|
154
|
+
const definition = this.getSectionDefinition(sectionKey);
|
|
155
|
+
const { document, filePath, stats } = await this.loadMetadataDocument(moduleName);
|
|
156
|
+
const value = this.getValueAtPath(document, definition.jsonPath);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
moduleName,
|
|
160
|
+
filePath,
|
|
161
|
+
lastModifiedAt: stats.mtime.toISOString(),
|
|
162
|
+
section: this.buildSectionResponse(definition, value),
|
|
163
|
+
validation: this.validateSectionValue(definition, value),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Input: module name string, section key, and a DTO containing the replacement section JSON in `value`.
|
|
169
|
+
* Output: refreshed section payload after the section is written back into the document.
|
|
170
|
+
* Logic: validates the incoming section shape, merges it into the current document at the
|
|
171
|
+
* configured JSON path, validates the resulting document, writes the file, and reloads the section.
|
|
172
|
+
*/
|
|
173
|
+
@DisallowInProduction()
|
|
174
|
+
async updateSection(moduleName: string, sectionKey: string, dto: MetadataExplorerWriteDto) {
|
|
175
|
+
this.assertValueProvided(dto?.value);
|
|
176
|
+
const definition = this.getSectionDefinition(sectionKey);
|
|
177
|
+
const sectionValidation = this.validateSectionValue(definition, dto.value);
|
|
178
|
+
if (!sectionValidation.valid) {
|
|
179
|
+
throw new BadRequestException(sectionValidation);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { document, filePath } = await this.loadMetadataDocument(moduleName);
|
|
183
|
+
this.setValueAtPath(document, definition.jsonPath, dto.value);
|
|
184
|
+
|
|
185
|
+
const documentValidation = this.validateDocumentValue(document);
|
|
186
|
+
if (!documentValidation.valid) {
|
|
187
|
+
throw new BadRequestException(documentValidation);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.writeMetadataDocument(filePath, document);
|
|
191
|
+
return this.getSection(moduleName, sectionKey);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Input: module name string, section key, and an optional DTO containing a section candidate in `value`.
|
|
196
|
+
* Output: validation result scoped to that section.
|
|
197
|
+
* Logic: validates either the supplied section candidate or the persisted section value against
|
|
198
|
+
* the configured section definition without performing any writes.
|
|
199
|
+
*/
|
|
200
|
+
async validateSection(moduleName: string, sectionKey: string, dto?: MetadataExplorerWriteDto) {
|
|
201
|
+
const definition = this.getSectionDefinition(sectionKey);
|
|
202
|
+
const value = dto && Object.prototype.hasOwnProperty.call(dto, 'value')
|
|
203
|
+
? dto.value
|
|
204
|
+
: this.getValueAtPath((await this.loadMetadataDocument(moduleName)).document, definition.jsonPath);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
moduleName,
|
|
208
|
+
sectionKey,
|
|
209
|
+
section: {
|
|
210
|
+
key: definition.key,
|
|
211
|
+
title: definition.title,
|
|
212
|
+
jsonPath: definition.jsonPath,
|
|
213
|
+
type: definition.type,
|
|
214
|
+
},
|
|
215
|
+
...this.validateSectionValue(definition, value),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Input: module name string plus search query DTO with query text, optional section scope,
|
|
221
|
+
* exact-match toggle, preview length, limit, and offset.
|
|
222
|
+
* Output: paginated list of key/value matches with JSON paths and section attribution.
|
|
223
|
+
* Logic: loads the document, optionally narrows the search root to a specific section, and then
|
|
224
|
+
* recursively scans keys and primitive values for textual matches.
|
|
225
|
+
*/
|
|
226
|
+
async search(moduleName: string, query: MetadataExplorerSearchQueryDto) {
|
|
227
|
+
const needle = `${query?.query ?? ''}`.trim();
|
|
228
|
+
if (!needle) {
|
|
229
|
+
throw new BadRequestException('Search query is required.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { document } = await this.loadMetadataDocument(moduleName);
|
|
233
|
+
const targetSection = query?.sectionKey ? this.getSectionDefinition(query.sectionKey) : null;
|
|
234
|
+
const searchRoot = targetSection ? this.getValueAtPath(document, targetSection.jsonPath) : document;
|
|
235
|
+
const rootPath = targetSection ? targetSection.jsonPath : '';
|
|
236
|
+
const limit = query?.limit ?? 10;
|
|
237
|
+
const offset = query?.offset ?? 0;
|
|
238
|
+
const matches = this.collectMatches(searchRoot, rootPath, {
|
|
239
|
+
needle,
|
|
240
|
+
exact: query?.exact ?? false,
|
|
241
|
+
limit,
|
|
242
|
+
offset,
|
|
243
|
+
previewLength: query?.previewLength ?? 120,
|
|
244
|
+
matchKeys: true,
|
|
245
|
+
matchValues: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
moduleName,
|
|
250
|
+
query: needle,
|
|
251
|
+
sectionKey: targetSection?.key ?? null,
|
|
252
|
+
meta: {
|
|
253
|
+
totalMatches: matches.total,
|
|
254
|
+
returned: matches.items.length,
|
|
255
|
+
limit,
|
|
256
|
+
offset,
|
|
257
|
+
},
|
|
258
|
+
matches: matches.items,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Input: module name string plus reference-query DTO with needle, optional section scope,
|
|
264
|
+
* match strategy flags, exclusion path, limit, and offset.
|
|
265
|
+
* Output: paginated list of reference-like matches across keys and/or values.
|
|
266
|
+
* Logic: performs the same recursive traversal engine as search, but with defaults tuned for
|
|
267
|
+
* exact reference lookups and optional exclusion of the source path being inspected.
|
|
268
|
+
*/
|
|
269
|
+
async findReferences(moduleName: string, query: MetadataExplorerReferencesQueryDto) {
|
|
270
|
+
const needle = `${query?.needle ?? ''}`.trim();
|
|
271
|
+
if (!needle) {
|
|
272
|
+
throw new BadRequestException('Reference needle is required.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { document } = await this.loadMetadataDocument(moduleName);
|
|
276
|
+
const targetSection = query?.sectionKey ? this.getSectionDefinition(query.sectionKey) : null;
|
|
277
|
+
const searchRoot = targetSection ? this.getValueAtPath(document, targetSection.jsonPath) : document;
|
|
278
|
+
const rootPath = targetSection ? targetSection.jsonPath : '';
|
|
279
|
+
const limit = query?.limit ?? 10;
|
|
280
|
+
const offset = query?.offset ?? 0;
|
|
281
|
+
const matches = this.collectMatches(searchRoot, rootPath, {
|
|
282
|
+
needle,
|
|
283
|
+
exact: query?.exact ?? true,
|
|
284
|
+
limit,
|
|
285
|
+
offset,
|
|
286
|
+
previewLength: 140,
|
|
287
|
+
matchKeys: query?.matchKeys ?? true,
|
|
288
|
+
matchValues: query?.matchValues ?? true,
|
|
289
|
+
excludePath: query?.excludePath,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
moduleName,
|
|
294
|
+
needle,
|
|
295
|
+
sectionKey: targetSection?.key ?? null,
|
|
296
|
+
meta: {
|
|
297
|
+
totalMatches: matches.total,
|
|
298
|
+
returned: matches.items.length,
|
|
299
|
+
limit,
|
|
300
|
+
offset,
|
|
301
|
+
},
|
|
302
|
+
matches: matches.items,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Input: module name string.
|
|
308
|
+
* Output: parsed JSON document, resolved file path, and file stats.
|
|
309
|
+
* Logic: resolves the module metadata file location, reads it from disk, parses JSON, and maps
|
|
310
|
+
* common file and syntax errors into explorer-specific HTTP exceptions.
|
|
311
|
+
*/
|
|
312
|
+
private async loadMetadataDocument(moduleName: string) {
|
|
313
|
+
const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
|
|
314
|
+
if (!filePath) {
|
|
315
|
+
throw new NotFoundException(`Unable to resolve metadata file for module: ${moduleName}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const [raw, stats] = await Promise.all([
|
|
320
|
+
fs.readFile(filePath, 'utf-8'),
|
|
321
|
+
fs.stat(filePath),
|
|
322
|
+
]);
|
|
323
|
+
const document = JSON.parse(raw);
|
|
324
|
+
return { document, filePath, stats };
|
|
325
|
+
} catch (error: any) {
|
|
326
|
+
if (error?.code === 'ENOENT') {
|
|
327
|
+
throw new NotFoundException(`Metadata file not found for module: ${moduleName}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (error instanceof SyntaxError) {
|
|
331
|
+
throw new BadRequestException(`Metadata file for module ${moduleName} contains invalid JSON.`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.logger.error(`Failed to load metadata for module ${moduleName}`, error);
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Input: absolute metadata file path and the JSON document value to persist.
|
|
341
|
+
* Output: none.
|
|
342
|
+
* Logic: ensures the target directory exists and writes a formatted JSON file with a trailing
|
|
343
|
+
* newline so the explorer remains the canonical persistence path for metadata edits.
|
|
344
|
+
*/
|
|
345
|
+
private async writeMetadataDocument(filePath: string, document: any) {
|
|
346
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
347
|
+
await fs.writeFile(filePath, `${JSON.stringify(document, null, 2)}\n`, 'utf-8');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Input: section key string.
|
|
352
|
+
* Output: matching section definition object.
|
|
353
|
+
* Logic: looks up the fixed explorer section registry and throws a 404 when the caller asks for
|
|
354
|
+
* a section the explorer does not expose.
|
|
355
|
+
*/
|
|
356
|
+
private getSectionDefinition(sectionKey: string): MetadataExplorerSectionDefinition {
|
|
357
|
+
const definition = SECTION_DEFINITIONS.find((item) => item.key === sectionKey);
|
|
358
|
+
if (!definition) {
|
|
359
|
+
throw new NotFoundException(`Metadata explorer section not found: ${sectionKey}`);
|
|
360
|
+
}
|
|
361
|
+
return definition;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Input: section definition and extracted section JSON value.
|
|
366
|
+
* Output: normalized section response payload for the API.
|
|
367
|
+
* Logic: enriches the raw section value with descriptive metadata, existence flags, counts, and
|
|
368
|
+
* the original JSON so the UI can render a section panel without extra computation.
|
|
369
|
+
*/
|
|
370
|
+
private buildSectionResponse(definition: MetadataExplorerSectionDefinition, value: any) {
|
|
371
|
+
return {
|
|
372
|
+
key: definition.key,
|
|
373
|
+
title: definition.title,
|
|
374
|
+
jsonPath: definition.jsonPath,
|
|
375
|
+
type: definition.type,
|
|
376
|
+
description: definition.description,
|
|
377
|
+
exists: value !== undefined,
|
|
378
|
+
itemCount: Array.isArray(value) ? value.length : undefined,
|
|
379
|
+
nodeCount: this.countNodes(value),
|
|
380
|
+
value,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Input: candidate full metadata document.
|
|
386
|
+
* Output: validation result containing a boolean validity flag and issue list.
|
|
387
|
+
* Logic: performs coarse structural checks for the overall document contract and also validates
|
|
388
|
+
* each known explorer section against its expected container type.
|
|
389
|
+
*/
|
|
390
|
+
private validateDocumentValue(document: any) {
|
|
391
|
+
const issues: MetadataValidationIssue[] = [];
|
|
392
|
+
|
|
393
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
394
|
+
issues.push({
|
|
395
|
+
path: '',
|
|
396
|
+
message: 'Metadata document must be a JSON object.',
|
|
397
|
+
severity: 'error',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const moduleMetadata = document?.moduleMetadata;
|
|
402
|
+
if (!moduleMetadata || typeof moduleMetadata !== 'object' || Array.isArray(moduleMetadata)) {
|
|
403
|
+
issues.push({
|
|
404
|
+
path: 'moduleMetadata',
|
|
405
|
+
message: 'moduleMetadata must be present as an object.',
|
|
406
|
+
severity: 'error',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (moduleMetadata && !Array.isArray(moduleMetadata.models)) {
|
|
411
|
+
issues.push({
|
|
412
|
+
path: 'moduleMetadata.models',
|
|
413
|
+
message: 'moduleMetadata.models must be an array.',
|
|
414
|
+
severity: 'error',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const definition of SECTION_DEFINITIONS) {
|
|
419
|
+
const sectionValue = this.getValueAtPath(document, definition.jsonPath);
|
|
420
|
+
const sectionValidation = this.validateSectionValue(definition, sectionValue, true);
|
|
421
|
+
issues.push(...sectionValidation.issues);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
426
|
+
issues,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Input: section definition, candidate section value, and optional allow-undefined toggle.
|
|
432
|
+
* Output: validation result for that section.
|
|
433
|
+
* Logic: checks whether a section is present when required and whether it matches the configured
|
|
434
|
+
* array or object shape expected by the explorer contract.
|
|
435
|
+
*/
|
|
436
|
+
private validateSectionValue(
|
|
437
|
+
definition: MetadataExplorerSectionDefinition,
|
|
438
|
+
value: any,
|
|
439
|
+
allowUndefined = false,
|
|
440
|
+
) {
|
|
441
|
+
const issues: MetadataValidationIssue[] = [];
|
|
442
|
+
|
|
443
|
+
if (value === undefined) {
|
|
444
|
+
if (!allowUndefined) {
|
|
445
|
+
issues.push({
|
|
446
|
+
path: definition.jsonPath,
|
|
447
|
+
message: `${definition.title} is missing from the metadata document.`,
|
|
448
|
+
severity: 'error',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
453
|
+
issues,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const isArray = Array.isArray(value);
|
|
458
|
+
if (definition.type === 'array' && !isArray) {
|
|
459
|
+
issues.push({
|
|
460
|
+
path: definition.jsonPath,
|
|
461
|
+
message: `${definition.title} must be an array.`,
|
|
462
|
+
severity: 'error',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (definition.type === 'object' && (value === null || typeof value !== 'object' || isArray)) {
|
|
467
|
+
issues.push({
|
|
468
|
+
path: definition.jsonPath,
|
|
469
|
+
message: `${definition.title} must be an object.`,
|
|
470
|
+
severity: 'error',
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
476
|
+
issues,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Input: source object and dot-delimited JSON path.
|
|
482
|
+
* Output: value found at that path, or undefined if any segment is missing.
|
|
483
|
+
* Logic: walks the object graph one segment at a time and safely stops when an intermediate node
|
|
484
|
+
* does not exist.
|
|
485
|
+
*/
|
|
486
|
+
private getValueAtPath(source: any, jsonPath: string): any {
|
|
487
|
+
if (!jsonPath) return source;
|
|
488
|
+
return jsonPath.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), source);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Input: target object, dot-delimited JSON path, and replacement value.
|
|
493
|
+
* Output: none.
|
|
494
|
+
* Logic: creates missing object containers along the path as needed and then assigns the final
|
|
495
|
+
* value at the terminal segment.
|
|
496
|
+
*/
|
|
497
|
+
private setValueAtPath(target: any, jsonPath: string, value: any) {
|
|
498
|
+
const parts = jsonPath.split('.');
|
|
499
|
+
let current = target;
|
|
500
|
+
|
|
501
|
+
for (let index = 0; index < parts.length - 1; index++) {
|
|
502
|
+
const part = parts[index];
|
|
503
|
+
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
|
504
|
+
current[part] = {};
|
|
505
|
+
}
|
|
506
|
+
current = current[part];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
current[parts[parts.length - 1]] = value;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Input: any JSON value.
|
|
514
|
+
* Output: integer node count representing the size of that subtree.
|
|
515
|
+
* Logic: recursively counts container nodes and primitive leaves so the explorer can show rough
|
|
516
|
+
* section complexity metrics in manifests and section responses.
|
|
517
|
+
*/
|
|
518
|
+
private countNodes(value: any): number {
|
|
519
|
+
if (value === undefined || value === null) return 0;
|
|
520
|
+
if (Array.isArray(value)) {
|
|
521
|
+
return value.reduce<number>((sum, item) => sum + this.countNodes(item), 1);
|
|
522
|
+
}
|
|
523
|
+
if (typeof value === 'object') {
|
|
524
|
+
return Object.values(value).reduce<number>((sum, item) => sum + this.countNodes(item), 1);
|
|
525
|
+
}
|
|
526
|
+
return 1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Input: any JSON value and a maximum preview length.
|
|
531
|
+
* Output: string preview suitable for manifest/search display.
|
|
532
|
+
* Logic: converts the value to a string representation and truncates it to a bounded length for
|
|
533
|
+
* compact display in explorer summaries.
|
|
534
|
+
*/
|
|
535
|
+
private buildPreview(value: any, maxLength: number): string {
|
|
536
|
+
if (value === undefined) return '';
|
|
537
|
+
|
|
538
|
+
const normalized = typeof value === 'string'
|
|
539
|
+
? value
|
|
540
|
+
: JSON.stringify(value);
|
|
541
|
+
|
|
542
|
+
if (!normalized) return '';
|
|
543
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Input: search root value, its root JSON path, and traversal options.
|
|
548
|
+
* Output: total match count plus the paginated slice of matches requested by the caller.
|
|
549
|
+
* Logic: recursively traverses arrays, objects, and primitives, recording key and/or value
|
|
550
|
+
* matches together with their JSON paths and preview text.
|
|
551
|
+
*/
|
|
552
|
+
private collectMatches(
|
|
553
|
+
root: any,
|
|
554
|
+
rootPath: string,
|
|
555
|
+
opts: {
|
|
556
|
+
needle: string;
|
|
557
|
+
exact: boolean;
|
|
558
|
+
limit: number;
|
|
559
|
+
offset: number;
|
|
560
|
+
previewLength: number;
|
|
561
|
+
matchKeys: boolean;
|
|
562
|
+
matchValues: boolean;
|
|
563
|
+
excludePath?: string;
|
|
564
|
+
},
|
|
565
|
+
) {
|
|
566
|
+
const results: MetadataSearchMatch[] = [];
|
|
567
|
+
const normalizedNeedle = opts.exact ? opts.needle : opts.needle.toLowerCase();
|
|
568
|
+
|
|
569
|
+
const visit = (value: any, currentPath: string) => {
|
|
570
|
+
if (value === undefined) return;
|
|
571
|
+
|
|
572
|
+
if (Array.isArray(value)) {
|
|
573
|
+
value.forEach((item, index) => {
|
|
574
|
+
visit(item, this.joinPath(currentPath, `${index}`));
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (value && typeof value === 'object') {
|
|
580
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
581
|
+
const childPath = this.joinPath(currentPath, key);
|
|
582
|
+
|
|
583
|
+
if (opts.matchKeys && this.matchesNeedle(key, normalizedNeedle, opts.exact) && childPath !== opts.excludePath) {
|
|
584
|
+
results.push({
|
|
585
|
+
path: childPath,
|
|
586
|
+
sectionKey: this.resolveSectionKey(childPath),
|
|
587
|
+
matchType: 'key',
|
|
588
|
+
valueType: 'key',
|
|
589
|
+
preview: this.buildPreview(child, opts.previewLength),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
visit(child, childPath);
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (!opts.matchValues) return;
|
|
599
|
+
const comparable = value == null ? '' : `${value}`;
|
|
600
|
+
if (this.matchesNeedle(comparable, normalizedNeedle, opts.exact) && currentPath !== opts.excludePath) {
|
|
601
|
+
results.push({
|
|
602
|
+
path: currentPath,
|
|
603
|
+
sectionKey: this.resolveSectionKey(currentPath),
|
|
604
|
+
matchType: 'value',
|
|
605
|
+
valueType: typeof value,
|
|
606
|
+
preview: this.buildPreview(value, opts.previewLength),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
visit(root, rootPath);
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
total: results.length,
|
|
615
|
+
items: results.slice(opts.offset, opts.offset + opts.limit),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Input: candidate string value, normalized search needle, and exact-match flag.
|
|
621
|
+
* Output: boolean indicating whether the value matches the search.
|
|
622
|
+
* Logic: performs either exact comparison or case-insensitive containment matching, depending on
|
|
623
|
+
* the caller's intent.
|
|
624
|
+
*/
|
|
625
|
+
private matchesNeedle(value: string, normalizedNeedle: string, exact: boolean): boolean {
|
|
626
|
+
if (exact) {
|
|
627
|
+
return value === normalizedNeedle;
|
|
628
|
+
}
|
|
629
|
+
return value.toLowerCase().includes(normalizedNeedle);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Input: JSON path string from a match result.
|
|
634
|
+
* Output: matching explorer section key or null when the path is outside known sections.
|
|
635
|
+
* Logic: finds the deepest section definition whose JSON path prefixes the supplied match path so
|
|
636
|
+
* search results can be attributed back to a sidebar section.
|
|
637
|
+
*/
|
|
638
|
+
private resolveSectionKey(jsonPath: string): string | null {
|
|
639
|
+
const sorted = [...SECTION_DEFINITIONS].sort((a, b) => b.jsonPath.length - a.jsonPath.length);
|
|
640
|
+
const matched = sorted.find((section) =>
|
|
641
|
+
jsonPath === section.jsonPath
|
|
642
|
+
|| jsonPath.startsWith(`${section.jsonPath}.`)
|
|
643
|
+
|| jsonPath.startsWith(`${section.jsonPath}[`)
|
|
644
|
+
);
|
|
645
|
+
return matched?.key ?? null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Input: current base JSON path and next property/index segment.
|
|
650
|
+
* Output: composed JSON path string.
|
|
651
|
+
* Logic: appends either dot notation or array index notation so traversal results use a stable,
|
|
652
|
+
* readable path format.
|
|
653
|
+
*/
|
|
654
|
+
private joinPath(base: string, next: string): string {
|
|
655
|
+
if (!base) return /^\d+$/.test(next) ? `[${next}]` : next;
|
|
656
|
+
return /^\d+$/.test(next) ? `${base}[${next}]` : `${base}.${next}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Input: arbitrary candidate value.
|
|
661
|
+
* Output: none; throws when the value is missing.
|
|
662
|
+
* Logic: enforces that write and validation endpoints receive an explicit JSON payload instead of
|
|
663
|
+
* silently treating an omitted body as intentional input.
|
|
664
|
+
*/
|
|
665
|
+
private assertValueProvided(value: any) {
|
|
666
|
+
if (value === undefined) {
|
|
667
|
+
throw new BadRequestException('A JSON value payload is required.');
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|