Skip to content

Commit b7b8c3d

Browse files
Copilothotlong
andcommitted
feat(metadata): add dependency validation to publishPackage - check referenced items exist in package or are already published
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 1b502b5 commit b7b8c3d

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

packages/metadata/src/metadata-manager.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ export class MetadataManager implements IMetadataService {
375375
// Validation pass
376376
if (shouldValidate) {
377377
const validationErrors: Array<{ type: string; name: string; message: string }> = [];
378+
379+
// Schema validation
378380
for (const item of packageItems) {
379381
const result = await this.validate(item.type, item.data);
380382
if (!result.valid && result.errors) {
@@ -387,6 +389,36 @@ export class MetadataManager implements IMetadataService {
387389
}
388390
}
389391
}
392+
393+
// Dependency validation: referenced items must be in the same package or already published
394+
const packageItemKeys = new Set(packageItems.map(i => `${i.type}:${i.name}`));
395+
for (const item of packageItems) {
396+
const deps = await this.getDependencies(item.type, item.name);
397+
for (const dep of deps) {
398+
const depKey = `${dep.targetType}:${dep.targetName}`;
399+
// Skip if the dependency is within this package
400+
if (packageItemKeys.has(depKey)) continue;
401+
// Check if the dependency exists and has been published
402+
const depItem = await this.get(dep.targetType, dep.targetName);
403+
if (!depItem) {
404+
validationErrors.push({
405+
type: item.type,
406+
name: item.name,
407+
message: `Dependency '${dep.targetType}:${dep.targetName}' not found`,
408+
});
409+
} else {
410+
const depMeta = depItem as any;
411+
if (depMeta.publishedDefinition === undefined && depMeta.state !== 'active') {
412+
validationErrors.push({
413+
type: item.type,
414+
name: item.name,
415+
message: `Dependency '${dep.targetType}:${dep.targetName}' is not published`,
416+
});
417+
}
418+
}
419+
}
420+
}
421+
390422
if (validationErrors.length > 0) {
391423
return {
392424
success: false,

packages/metadata/src/metadata-service.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,78 @@ describe('MetadataManager — IMetadataService Contract', () => {
772772
expect(result.success).toBe(true);
773773
expect(result.itemsPublished).toBe(1);
774774
});
775+
776+
it('should fail when dependency is not found or not published', async () => {
777+
await manager.register('view', 'opp_list', {
778+
name: 'opp_list', label: 'Opp List', packageId: 'com.acme.dep',
779+
metadata: { columns: ['name'] },
780+
});
781+
782+
// Register a dependency pointing to a non-existent item
783+
manager.addDependency({
784+
sourceType: 'view',
785+
sourceName: 'opp_list',
786+
targetType: 'object',
787+
targetName: 'opportunity',
788+
kind: 'reference',
789+
});
790+
791+
const result = await manager.publishPackage('com.acme.dep', { validate: true });
792+
expect(result.success).toBe(false);
793+
expect(result.validationErrors).toBeDefined();
794+
expect(result.validationErrors!.some(e => e.message.includes('opportunity'))).toBe(true);
795+
});
796+
797+
it('should pass dependency check when target is in the same package', async () => {
798+
await manager.register('object', 'project', {
799+
name: 'project', label: 'Project', packageId: 'com.acme.same',
800+
metadata: { fields: ['name'] },
801+
});
802+
await manager.register('view', 'project_list', {
803+
name: 'project_list', label: 'Project List', packageId: 'com.acme.same',
804+
metadata: { columns: ['name'] },
805+
});
806+
807+
// Dependency within the same package
808+
manager.addDependency({
809+
sourceType: 'view',
810+
sourceName: 'project_list',
811+
targetType: 'object',
812+
targetName: 'project',
813+
kind: 'reference',
814+
});
815+
816+
const result = await manager.publishPackage('com.acme.same', { validate: true });
817+
expect(result.success).toBe(true);
818+
expect(result.itemsPublished).toBe(2);
819+
});
820+
821+
it('should pass dependency check when target is already published', async () => {
822+
// Pre-existing published object (different package)
823+
await manager.register('object', 'account', {
824+
name: 'account', label: 'Account', packageId: 'com.acme.core',
825+
publishedDefinition: { fields: ['name'] },
826+
state: 'active',
827+
});
828+
829+
// View in a different package references the published object
830+
await manager.register('view', 'account_list', {
831+
name: 'account_list', label: 'Account List', packageId: 'com.acme.views',
832+
metadata: { columns: ['name'] },
833+
});
834+
835+
manager.addDependency({
836+
sourceType: 'view',
837+
sourceName: 'account_list',
838+
targetType: 'object',
839+
targetName: 'account',
840+
kind: 'reference',
841+
});
842+
843+
const result = await manager.publishPackage('com.acme.views', { validate: true });
844+
expect(result.success).toBe(true);
845+
expect(result.itemsPublished).toBe(1);
846+
});
775847
});
776848

777849
describe('revertPackage', () => {

0 commit comments

Comments
 (0)