mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(group): persist manifest grpc and lib links through bridge
This commit is contained in:
parent
fd2a4d52ed
commit
fa80ce6a25
4 changed files with 239 additions and 15 deletions
|
|
@ -191,24 +191,50 @@ export async function writeBridge(groupDir: string, input: WriteBridgeInput): Pr
|
|||
// Use repo-scoped matching: find FROM node by (repo, role=consumer) and TO by (repo, role=provider)
|
||||
// with symbolRef matching, because link.contractId is the consumer's ID which may differ
|
||||
// from the provider's contractId (e.g. wildcard consumer vs method-level provider).
|
||||
const findContractNode = async (
|
||||
repo: string,
|
||||
role: 'consumer' | 'provider',
|
||||
symbolUid: string,
|
||||
filePath: string,
|
||||
symbolName: string,
|
||||
): Promise<string | null> => {
|
||||
if (symbolUid) {
|
||||
const uidRows = await queryBridge<{ id: string }>(
|
||||
handle,
|
||||
`MATCH (c:Contract) WHERE c.repo = $repo AND c.role = $role
|
||||
AND c.symbolUid = $symbolUid RETURN c.id AS id LIMIT 1`,
|
||||
{ repo, role, symbolUid },
|
||||
);
|
||||
if (uidRows.length > 0) return uidRows[0].id;
|
||||
}
|
||||
|
||||
const refRows = await queryBridge<{ id: string }>(
|
||||
handle,
|
||||
`MATCH (c:Contract) WHERE c.repo = $repo AND c.role = $role
|
||||
AND c.filePath = $filePath AND c.symbolName = $symbolName
|
||||
RETURN c.id AS id LIMIT 1`,
|
||||
{ repo, role, filePath, symbolName },
|
||||
);
|
||||
if (refRows.length > 0) return refRows[0].id;
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const link of input.crossLinks) {
|
||||
// Find from-node: match by repo + role + symbolRef (consumer side)
|
||||
const fromRows = await queryBridge<{ id: string }>(
|
||||
handle,
|
||||
`MATCH (c:Contract) WHERE c.repo = $repo AND c.role = 'consumer'
|
||||
AND c.filePath = $fp AND c.symbolName = $sn RETURN c.id AS id LIMIT 1`,
|
||||
{ repo: link.from.repo, fp: link.from.symbolRef.filePath, sn: link.from.symbolRef.name },
|
||||
const fromId = await findContractNode(
|
||||
link.from.repo,
|
||||
'consumer',
|
||||
link.from.symbolUid,
|
||||
link.from.symbolRef.filePath,
|
||||
link.from.symbolRef.name,
|
||||
);
|
||||
// Find to-node: match by repo + role + symbolRef (provider side)
|
||||
const toRows = await queryBridge<{ id: string }>(
|
||||
handle,
|
||||
`MATCH (c:Contract) WHERE c.repo = $repo AND c.role = 'provider'
|
||||
AND c.filePath = $fp AND c.symbolName = $sn RETURN c.id AS id LIMIT 1`,
|
||||
{ repo: link.to.repo, fp: link.to.symbolRef.filePath, sn: link.to.symbolRef.name },
|
||||
const toId = await findContractNode(
|
||||
link.to.repo,
|
||||
'provider',
|
||||
link.to.symbolUid,
|
||||
link.to.symbolRef.filePath,
|
||||
link.to.symbolRef.name,
|
||||
);
|
||||
if (!fromRows.length || !toRows.length) continue;
|
||||
const fromId = fromRows[0].id;
|
||||
const toId = toRows[0].id;
|
||||
if (!fromId || !toId) continue;
|
||||
await queryBridge(
|
||||
handle,
|
||||
`
|
||||
|
|
|
|||
|
|
@ -89,6 +89,28 @@ export class ManifestExtractor {
|
|||
LIMIT 1`,
|
||||
{ contract: link.contract },
|
||||
);
|
||||
} else if (link.type === 'grpc') {
|
||||
const [serviceName, methodName = ''] = link.contract.split('/');
|
||||
rows = await executor(
|
||||
`MATCH (n)
|
||||
WHERE n.name CONTAINS $serviceName
|
||||
OR n.name CONTAINS $methodName
|
||||
OR n.filePath ENDS WITH '.proto'
|
||||
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
||||
LIMIT 1`,
|
||||
{ contract: link.contract, serviceName, methodName },
|
||||
);
|
||||
} else if (link.type === 'lib') {
|
||||
const packageName = link.contract.split('/').pop() ?? link.contract;
|
||||
rows = await executor(
|
||||
`MATCH (n)
|
||||
WHERE n.name = $contract
|
||||
OR n.name CONTAINS $packageName
|
||||
OR n.filePath CONTAINS $packageName
|
||||
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
||||
LIMIT 1`,
|
||||
{ contract: link.contract, packageName },
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,4 +121,70 @@ describe('bridge-db edge cases', () => {
|
|||
expect(contractRows).toHaveLength(2);
|
||||
await closeBridgeDb(handle!);
|
||||
});
|
||||
|
||||
it('test_writeBridge_manifest_grpc_link_with_symbol_uids_persists_queryable_contract_edge', async () => {
|
||||
const provider = makeContract({
|
||||
contractId: 'grpc::auth.AuthService/Login',
|
||||
type: 'grpc',
|
||||
role: 'provider',
|
||||
repo: 'platform/auth',
|
||||
symbolUid: 'uid-auth-login',
|
||||
symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
|
||||
symbolName: 'auth.AuthService/Login',
|
||||
});
|
||||
const consumer = makeContract({
|
||||
contractId: 'grpc::auth.AuthService/Login',
|
||||
type: 'grpc',
|
||||
role: 'consumer',
|
||||
repo: 'platform/orders',
|
||||
symbolUid: 'uid-orders-client',
|
||||
symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
|
||||
symbolName: 'auth.AuthService/Login',
|
||||
});
|
||||
const link: CrossLink = {
|
||||
from: {
|
||||
repo: 'platform/orders',
|
||||
symbolUid: 'uid-orders-client',
|
||||
symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
|
||||
},
|
||||
to: {
|
||||
repo: 'platform/auth',
|
||||
symbolUid: 'uid-auth-login',
|
||||
symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
|
||||
},
|
||||
type: 'grpc',
|
||||
contractId: 'grpc::auth.AuthService/Login',
|
||||
matchType: 'manifest',
|
||||
confidence: 1.0,
|
||||
};
|
||||
|
||||
await writeBridge(tmpDir, {
|
||||
contracts: [provider, consumer],
|
||||
crossLinks: [link],
|
||||
repoSnapshots: {},
|
||||
missingRepos: [],
|
||||
});
|
||||
|
||||
const handle = await openBridgeDbReadOnly(tmpDir);
|
||||
expect(handle).not.toBeNull();
|
||||
const rows = await queryBridge<{
|
||||
contractId: string;
|
||||
matchType: string;
|
||||
fromRepo: string;
|
||||
toRepo: string;
|
||||
}>(
|
||||
handle!,
|
||||
`MATCH (a:Contract)-[l:ContractLink]->(b:Contract)
|
||||
RETURN l.contractId AS contractId, l.matchType AS matchType, l.fromRepo AS fromRepo, l.toRepo AS toRepo`,
|
||||
);
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
contractId: 'grpc::auth.AuthService/Login',
|
||||
matchType: 'manifest',
|
||||
fromRepo: 'platform/orders',
|
||||
toRepo: 'platform/auth',
|
||||
},
|
||||
]);
|
||||
await closeBridgeDb(handle!);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -60,6 +60,116 @@ describe('ManifestExtractor', () => {
|
|||
expect(result.crossLinks[0].to.repo).toBe('sales/crm/backend');
|
||||
});
|
||||
|
||||
it('resolves grpc manifest links to concrete provider and consumer symbols', async () => {
|
||||
const links: GroupManifestLink[] = [
|
||||
{
|
||||
from: 'platform/orders',
|
||||
to: 'platform/auth',
|
||||
type: 'grpc',
|
||||
contract: 'auth.AuthService/Login',
|
||||
role: 'consumer',
|
||||
},
|
||||
];
|
||||
|
||||
const dbExecutors = new Map<
|
||||
string,
|
||||
(cypher: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>[]>
|
||||
>([
|
||||
[
|
||||
'platform/auth',
|
||||
async (_cypher, params) => {
|
||||
if (params?.contract !== 'auth.AuthService/Login') return [];
|
||||
return [
|
||||
{
|
||||
uid: 'uid-auth-login',
|
||||
name: 'Login',
|
||||
filePath: 'src/auth.proto',
|
||||
},
|
||||
];
|
||||
},
|
||||
],
|
||||
[
|
||||
'platform/orders',
|
||||
async (_cypher, params) => {
|
||||
if (params?.contract !== 'auth.AuthService/Login') return [];
|
||||
return [
|
||||
{
|
||||
uid: 'uid-orders-client',
|
||||
name: 'AuthServiceClient',
|
||||
filePath: 'src/client.ts',
|
||||
},
|
||||
];
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = await extractor.extractFromManifest(links, dbExecutors);
|
||||
|
||||
const provider = result.contracts.find((c) => c.role === 'provider');
|
||||
const consumer = result.contracts.find((c) => c.role === 'consumer');
|
||||
|
||||
expect(provider?.symbolUid).toBe('uid-auth-login');
|
||||
expect(provider?.symbolRef.filePath).toBe('src/auth.proto');
|
||||
expect(consumer?.symbolUid).toBe('uid-orders-client');
|
||||
expect(consumer?.symbolRef.filePath).toBe('src/client.ts');
|
||||
expect(result.crossLinks[0].to.symbolRef.filePath).toBe('src/auth.proto');
|
||||
expect(result.crossLinks[0].from.symbolRef.filePath).toBe('src/client.ts');
|
||||
});
|
||||
|
||||
it('resolves lib manifest links to concrete provider and consumer symbols', async () => {
|
||||
const links: GroupManifestLink[] = [
|
||||
{
|
||||
from: 'platform/web',
|
||||
to: 'platform/shared-lib',
|
||||
type: 'lib',
|
||||
contract: '@platform/contracts',
|
||||
role: 'consumer',
|
||||
},
|
||||
];
|
||||
|
||||
const dbExecutors = new Map<
|
||||
string,
|
||||
(cypher: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>[]>
|
||||
>([
|
||||
[
|
||||
'platform/shared-lib',
|
||||
async (_cypher, params) => {
|
||||
if (params?.contract !== '@platform/contracts') return [];
|
||||
return [
|
||||
{
|
||||
uid: 'uid-lib',
|
||||
name: '@platform/contracts',
|
||||
filePath: 'src/index.ts',
|
||||
},
|
||||
];
|
||||
},
|
||||
],
|
||||
[
|
||||
'platform/web',
|
||||
async (_cypher, params) => {
|
||||
if (params?.contract !== '@platform/contracts') return [];
|
||||
return [
|
||||
{
|
||||
uid: 'uid-importer',
|
||||
name: 'contractsClient',
|
||||
filePath: 'src/app.ts',
|
||||
},
|
||||
];
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = await extractor.extractFromManifest(links, dbExecutors);
|
||||
|
||||
const provider = result.contracts.find((c) => c.role === 'provider');
|
||||
const consumer = result.contracts.find((c) => c.role === 'consumer');
|
||||
|
||||
expect(provider?.symbolUid).toBe('uid-lib');
|
||||
expect(consumer?.symbolUid).toBe('uid-importer');
|
||||
expect(result.crossLinks[0].to.symbolUid).toBe('uid-lib');
|
||||
expect(result.crossLinks[0].from.symbolUid).toBe('uid-importer');
|
||||
});
|
||||
|
||||
it('returns empty for no links', async () => {
|
||||
const result = await extractor.extractFromManifest([]);
|
||||
expect(result.contracts).toHaveLength(0);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue