Skip to content
45 changes: 36 additions & 9 deletions infrastructure/evault-core/src/core/db/db.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,36 @@ export class DbService {
{ id, ontology: meta.ontology, acl, eName },
);

// Deduplicate envelopes — if multiple Envelope nodes share the
// same ontology (field name), keep the first and delete the rest.
// This prevents non-deterministic reads where collect(e) returns
// duplicates in undefined order and reduce picks the wrong one.
const seen = new Map<string, string>(); // ontology → kept envelope id
const dupsToDelete: string[] = [];
for (const env of existing.envelopes) {
if (seen.has(env.ontology)) {
dupsToDelete.push(env.id);
} else {
seen.set(env.ontology, env.id);
}
}
if (dupsToDelete.length > 0) {
console.warn(
`[eVault] Cleaning ${dupsToDelete.length} duplicate envelope(s) for MetaEnvelope ${id}`,
);
for (const dupId of dupsToDelete) {
await this.runQueryInternal(
`MATCH (e:Envelope { id: $envelopeId }) DETACH DELETE e`,
{ envelopeId: dupId },
);
}
// Remove deleted dupes from the existing list so the update
// loop below doesn't try to reference them.
existing.envelopes = existing.envelopes.filter(
(e) => !dupsToDelete.includes(e.id),
);
}

const createdEnvelopes: Envelope<T[keyof T]>[] = [];
let counter = 0;

Expand Down Expand Up @@ -601,21 +631,18 @@ export class DbService {
valueType,
});
} else {
// Create new envelope
// Create new envelope — use MERGE on the relationship
// + ontology to prevent duplicate Envelopes if two
// concurrent updates race.
const envW3id = await new W3IDBuilder().build();
const envelopeId = envW3id.id;

await this.runQueryInternal(
`
MATCH (m:MetaEnvelope { id: $metaId, eName: $eName })
CREATE (${alias}:Envelope {
id: $${alias}_id,
ontology: $${alias}_ontology,
value: $${alias}_value,
valueType: $${alias}_type
})
WITH m, ${alias}
MERGE (m)-[:LINKS_TO]->(${alias})
MERGE (m)-[:LINKS_TO]->(${alias}:Envelope { ontology: $${alias}_ontology })
ON CREATE SET ${alias}.id = $${alias}_id, ${alias}.value = $${alias}_value, ${alias}.valueType = $${alias}_type
ON MATCH SET ${alias}.value = $${alias}_value, ${alias}.valueType = $${alias}_type
`,
{
metaId: id,
Expand Down
22 changes: 18 additions & 4 deletions infrastructure/evault-core/src/core/protocol/graphql-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,12 +374,19 @@ export class GraphQLServer {
context.eName,
);

// Build the full metaEnvelope response
// Build parsed from actual written envelopes, not input
const parsedFromEnvelopes = result.envelopes.reduce(
(acc: Record<string, any>, env: any) => {
acc[env.ontology] = env.value;
return acc;
},
{},
);
const metaEnvelope = {
id: result.metaEnvelope.id,
ontology: result.metaEnvelope.ontology,
envelopes: result.envelopes,
parsed: input.payload,
parsed: parsedFromEnvelopes,
};

// Deliver webhooks for create operation
Expand Down Expand Up @@ -508,12 +515,19 @@ export class GraphQLServer {
context.eName,
);

// Build the full metaEnvelope response
// Build parsed from actual written envelopes, not input
const parsedFromEnvelopes = result.envelopes.reduce(
(acc: Record<string, any>, env: any) => {
acc[env.ontology] = env.value;
return acc;
},
{},
);
const metaEnvelope = {
id: result.metaEnvelope.id,
ontology: result.metaEnvelope.ontology,
envelopes: result.envelopes,
parsed: input.payload,
parsed: parsedFromEnvelopes,
};

// Deliver webhooks for update operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,25 @@ export class DiscoveryController {
try {
const { q, page, limit, sortBy } = req.query;

if (!q) {
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
res.setHeader("Pragma", "no-cache");
return res
.status(400)
.json({ error: 'Query parameter "q" is required' });
}

const pageNum = Math.max(1, parseInt(page as string) || 1);
const limitNum = Math.min(
100,
Math.max(1, parseInt(limit as string) || 10),
Math.max(1, parseInt(limit as string) || 12),
);

const results = await this.userSearchService.searchUsers(
q as string,
pageNum,
limitNum,
(sortBy as string) || "relevance",
);
const query = ((q as string) ?? "").trim();

const results = query
? await this.userSearchService.searchUsers(
query,
pageNum,
limitNum,
(sortBy as string) || "relevance",
)
: await this.userSearchService.listPublicUsers(
pageNum,
limitNum,
);

res.setHeader(
"Cache-Control",
Expand Down
Loading
Loading