@learncard/network-brain-service 4.0.2 → 4.0.3
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/CHANGELOG.md +14 -0
- package/dist/accesslayer/boost/read.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/profile/create.d.ts.map +1 -1
- package/dist/accesslayer/profile/read.d.ts.map +1 -1
- package/dist/accesslayer/profile/search-fields.d.ts +16 -0
- package/dist/accesslayer/profile/search-fields.d.ts.map +1 -0
- package/dist/accesslayer/profile/update.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/seed/seedSkillFrameworks.d.ts.map +1 -1
- package/package.json +4 -1
- package/project.json +18 -0
- package/scripts/backfill-profile-search.ts +79 -0
- package/scripts/seed-demo-persona.ts +6 -1
- package/src/accesslayer/boost/read.ts +171 -34
- package/src/accesslayer/boost/relationships/read.ts +163 -126
- package/src/accesslayer/profile/create.ts +2 -1
- package/src/accesslayer/profile/read.ts +23 -3
- package/src/accesslayer/profile/search-fields.ts +69 -0
- package/src/accesslayer/profile/update.ts +7 -1
- package/src/models/credential-refresh-constraints.test.ts +44 -0
- package/src/models/credential-refresh-constraints.ts +29 -3
- package/src/models/index.ts +15 -4
- package/src/seed/seedSkillFrameworks.ts +4 -1
- package/test/boost.familial-equivalence.spec.ts +482 -0
- package/test/boost.recipientsJoin.spec.ts +508 -0
- package/test/profile-search.spec.ts +307 -0
- package/vitest.boost-recipients-neo4j.config.ts +21 -0
- package/vitest.profile-search-neo4j.config.ts +20 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seedSkillFrameworks.d.ts","sourceRoot":"","sources":["../../src/seed/seedSkillFrameworks.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"seedSkillFrameworks.d.ts","sourceRoot":"","sources":["../../src/seed/seedSkillFrameworks.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAiBhC,KAAK,eAAe,GAAG;IACnB,GAAG,EAAE,CACD,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/B,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CACvE,CAAC;AAEF,KAAK,UAAU,GAAG;IACd,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC9B,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAgCF,eAAO,MAAM,gCAAgC,QAAO,MAGI,CAAC;AAoLzD,eAAO,MAAM,oCAAoC,GAC7C,QAAO,OAAO,GAAG,SAAmB,KACrC,OAAO,CAAC;IACP,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,wBAAwB,CAAC;CACvC,CA2DA,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAU,KAAK,eAAe,CAAC,KAAK,CAAC,KAAG,OAAO,CAAC,MAAM,CAGtF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,KAAK,eAAe,CAAC,KAAK,CAAC,KAAG,OAAO,CAAC,OAAO,CAElF,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAC/B,KAAK,eAAe,CAAC,KAAK,CAAC,EAC3B,WAAW,MAAM,KAClB,OAAO,CAAC,MAAM,CAwBhB,CAAC;AA6IF,eAAO,MAAM,iBAAiB,GAC1B,KAAK,eAAe,CAAC,KAAK,CAAC,EAC3B,YAAW,MAAiC,KAC7C,OAAO,CAAC,IAAI,CA2Bd,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACnC,KAAK,eAAe,CAAC,KAAK,CAAC,EAC3B,UAAS,0BAA+B,KACzC,OAAO,CAAC;IAAE,kBAAkB,EAAE,MAAM,EAAE,CAAA;CAAE,CAgG1C,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACrC,KAAK,eAAe,CAAC,KAAK,CAAC,EAC3B,UAAS,0BAA+B,KACzC,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,MAAM,EAAE,CAAA;CAAE,CAoC3D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,KAAG,MAAM,EAMzD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/network-brain-service",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.12 <25"
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
"test:coverage": "vitest run --coverage --config vitest.config.ts",
|
|
17
17
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
18
18
|
"test:inbox:e2e": "vitest run --config vitest.inbox-e2e.config.ts",
|
|
19
|
+
"test:profile-search": "vitest run --config vitest.profile-search-neo4j.config.ts",
|
|
19
20
|
"test:integration:watch": "vitest --config vitest.integration.config.ts",
|
|
20
21
|
"test:docker": "vitest run --config vitest.integration.config.ts",
|
|
22
|
+
"test:boost-recipients": "vitest run --config vitest.boost-recipients-neo4j.config.ts",
|
|
21
23
|
"seed:dev-app": "bun scripts/seed-dev-app.ts",
|
|
22
24
|
"seed:demo-persona": "bun --conditions=development scripts/seed-demo-persona.ts",
|
|
23
25
|
"skill-frameworks": "bun scripts/skill-frameworks.ts",
|
|
26
|
+
"backfill:profile-search": "bun scripts/backfill-profile-search.ts",
|
|
24
27
|
"build": "shx rm -rf dist && node bundle.mjs && tsc --p tsconfig.build.json && tsc --showConfig --p tsconfig.build.json > tsconfig.temp.json && tsc-alias --replacer ./tsc-alias-replacer.cjs -p tsconfig.temp.json && shx rm tsconfig.temp.json",
|
|
25
28
|
"typecheck": "tsc --noEmit -p tsconfig.build.json && tsc --noEmit -p tsconfig.entrypoints.json",
|
|
26
29
|
"build:docker": "shx rm -rf build && node esbuild-docker.mjs",
|
package/project.json
CHANGED
|
@@ -111,12 +111,30 @@
|
|
|
111
111
|
"script": "test:inbox:e2e"
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
+
"test:profile-search": {
|
|
115
|
+
"executor": "nx:run-script",
|
|
116
|
+
"cache": false,
|
|
117
|
+
"dependsOn": ["^build"],
|
|
118
|
+
"inputs": ["source", "tests", "{projectRoot}/vitest.profile-search-neo4j.config.ts"],
|
|
119
|
+
"options": {
|
|
120
|
+
"script": "test:profile-search"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
114
123
|
"test:integration": {
|
|
115
124
|
"executor": "nx:run-script",
|
|
116
125
|
"inputs": ["source", "tests"],
|
|
117
126
|
"options": {
|
|
118
127
|
"script": "test:integration"
|
|
119
128
|
}
|
|
129
|
+
},
|
|
130
|
+
"test:boost-recipients": {
|
|
131
|
+
"executor": "nx:run-script",
|
|
132
|
+
"cache": false,
|
|
133
|
+
"dependsOn": ["^build"],
|
|
134
|
+
"inputs": ["source", "tests", "{projectRoot}/vitest.boost-recipients-neo4j.config.ts"],
|
|
135
|
+
"options": {
|
|
136
|
+
"script": "test:boost-recipients"
|
|
137
|
+
}
|
|
120
138
|
}
|
|
121
139
|
}
|
|
122
140
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Backfills `Profile.displayNameLower`, the normalized copy of `displayName` (see
|
|
4
|
+
* `normalizeProfileSearchText`) that case-insensitive profile search reads through the
|
|
5
|
+
* `profile_displayname_lower_text_idx` TEXT index.
|
|
6
|
+
*
|
|
7
|
+
* New and updated profiles get the field on write. Run this once per environment after the
|
|
8
|
+
* release that adds or changes it, and only once no instance of the previous build is still
|
|
9
|
+
* writing profiles, so profiles written before then are findable by display name again.
|
|
10
|
+
* It's idempotent and only touches profiles whose copy is missing or out of date.
|
|
11
|
+
*
|
|
12
|
+
* Usage (from services/learn-card-network/brain-service):
|
|
13
|
+
* NEO4J_URI=... NEO4J_USERNAME=... NEO4J_PASSWORD=... \
|
|
14
|
+
* bun run backfill:profile-search -- --yes [--batch-size 1000]
|
|
15
|
+
*
|
|
16
|
+
* Without --yes it only counts the profiles that need the field and changes nothing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import neo4j from 'neo4j-driver';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
PROFILE_DISPLAY_NAME_LOWER_FIELD,
|
|
23
|
+
backfillProfileDisplayNameLower,
|
|
24
|
+
buildBackfillProfileDisplayNameLowerQuery,
|
|
25
|
+
profileDisplayNameLowerIsStale,
|
|
26
|
+
} from '../src/accesslayer/profile/search-fields';
|
|
27
|
+
|
|
28
|
+
const getArg = (name: string): string | undefined => {
|
|
29
|
+
const index = process.argv.indexOf(name);
|
|
30
|
+
return index === -1 ? undefined : process.argv[index + 1];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const main = async (): Promise<void> => {
|
|
34
|
+
const uri = process.env.NEO4J_URI;
|
|
35
|
+
const username = process.env.NEO4J_USERNAME;
|
|
36
|
+
const password = process.env.NEO4J_PASSWORD;
|
|
37
|
+
|
|
38
|
+
if (!uri || !username || !password) {
|
|
39
|
+
throw new Error('NEO4J_URI, NEO4J_USERNAME and NEO4J_PASSWORD must all be set explicitly.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const write = process.argv.includes('--yes');
|
|
43
|
+
const batchSize = Number(getArg('--batch-size') ?? 1000);
|
|
44
|
+
|
|
45
|
+
console.log(`Target: ${uri} (${write ? 'write' : 'dry run, pass --yes to write'})`);
|
|
46
|
+
|
|
47
|
+
const driver = neo4j.driver(uri, neo4j.auth.basic(username, password));
|
|
48
|
+
const session = driver.session();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (!write) {
|
|
52
|
+
const result = await session.run(
|
|
53
|
+
`MATCH (profile:Profile)
|
|
54
|
+
WHERE ${profileDisplayNameLowerIsStale('profile')}
|
|
55
|
+
RETURN count(profile) AS pending`
|
|
56
|
+
);
|
|
57
|
+
console.log(
|
|
58
|
+
`Profiles needing ${PROFILE_DISPLAY_NAME_LOWER_FIELD}:`,
|
|
59
|
+
result.records[0]?.get('pending').toString()
|
|
60
|
+
);
|
|
61
|
+
console.log(`Would run:\n${buildBackfillProfileDisplayNameLowerQuery(batchSize)}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const updated = await backfillProfileDisplayNameLower(query => session.run(query), {
|
|
66
|
+
batchSize,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(`Set ${PROFILE_DISPLAY_NAME_LOWER_FIELD} on ${updated} profiles.`);
|
|
70
|
+
} finally {
|
|
71
|
+
await session.close();
|
|
72
|
+
await driver.close();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
main().catch(error => {
|
|
77
|
+
console.error(error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
} from '@learncard/credential-library';
|
|
34
34
|
import type { UnsignedVC } from '@learncard/types';
|
|
35
35
|
import { flattenObject } from '../src/helpers/objects.helpers';
|
|
36
|
+
import {
|
|
37
|
+
PROFILE_DISPLAY_NAME_LOWER_FIELD,
|
|
38
|
+
normalizeProfileSearchText,
|
|
39
|
+
} from '../src/accesslayer/profile/search-fields';
|
|
36
40
|
|
|
37
41
|
dotenv.config({ path: fileURLToPath(new URL('../.env', import.meta.url)) });
|
|
38
42
|
|
|
@@ -237,7 +241,8 @@ const main = async (): Promise<void> => {
|
|
|
237
241
|
SET p.displayName = $displayName,
|
|
238
242
|
p.shortBio = $shortBio,
|
|
239
243
|
p.image = $image,
|
|
240
|
-
p.did = $did
|
|
244
|
+
p.did = $did,
|
|
245
|
+
p.${PROFILE_DISPLAY_NAME_LOWER_FIELD} = ${normalizeProfileSearchText('$displayName')}
|
|
241
246
|
RETURN p.did AS did`,
|
|
242
247
|
{
|
|
243
248
|
profileId: issuer.profileId,
|
|
@@ -259,6 +259,159 @@ export const countBoostSiblings = async (
|
|
|
259
259
|
return Number(result.records[0]?.get('count') ?? 0);
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Builds the Cypher (to follow `MATCH (start:Boost { id })`) that binds `boost` to every distinct
|
|
264
|
+
* member of start's family: ancestors (up to parentGenerations), descendants (up to
|
|
265
|
+
* childGenerations) and siblings (or, with includeExtendedFamily, cousins), never start itself
|
|
266
|
+
* via the sibling path.
|
|
267
|
+
*
|
|
268
|
+
* Each branch is expanded outward from the already-bound `start` node, so the cost is proportional
|
|
269
|
+
* to the size of the family. The previous form (`MATCH (boost:Boost) WHERE <pattern> OR <pattern>`)
|
|
270
|
+
* label-scanned every Boost node and evaluated the pattern predicates per node.
|
|
271
|
+
*
|
|
272
|
+
* Generations are interpolated (Cypher cannot parameterise variable-length bounds); callers must
|
|
273
|
+
* validate they are numbers. Non-finite values produce an unbounded range, matching prior behaviour.
|
|
274
|
+
*/
|
|
275
|
+
const getFamilialBoostsCypher = ({
|
|
276
|
+
parentGenerations,
|
|
277
|
+
childGenerations,
|
|
278
|
+
includeExtendedFamily,
|
|
279
|
+
}: {
|
|
280
|
+
parentGenerations: number;
|
|
281
|
+
childGenerations: number;
|
|
282
|
+
includeExtendedFamily: boolean;
|
|
283
|
+
}): string => {
|
|
284
|
+
const parentRange = Number.isFinite(parentGenerations) ? parentGenerations : '';
|
|
285
|
+
const childRange = Number.isFinite(childGenerations) ? childGenerations : '';
|
|
286
|
+
const siblingParentHops = includeExtendedFamily ? `*1..${parentRange}` : '';
|
|
287
|
+
const siblingChildHops = includeExtendedFamily ? `*1..${childRange}` : '';
|
|
288
|
+
|
|
289
|
+
return `
|
|
290
|
+
OPTIONAL MATCH (start)<-[:PARENT_OF*1..${parentRange}]-(ancestor:Boost)
|
|
291
|
+
WITH start, COLLECT(DISTINCT ancestor) AS ancestors
|
|
292
|
+
OPTIONAL MATCH (start)-[:PARENT_OF*1..${childRange}]->(descendant:Boost)
|
|
293
|
+
WITH start, ancestors, COLLECT(DISTINCT descendant) AS descendants
|
|
294
|
+
OPTIONAL MATCH (start)<-[:PARENT_OF${siblingParentHops}]-(:Boost)-[:PARENT_OF${siblingChildHops}]->(sibling:Boost)
|
|
295
|
+
WHERE sibling <> start
|
|
296
|
+
WITH ancestors, descendants, COLLECT(DISTINCT sibling) AS siblings
|
|
297
|
+
UNWIND ancestors + descendants + siblings AS boost
|
|
298
|
+
WITH DISTINCT boost`;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Above this many index candidates, `getFamilialBoostsByCandidatesCypher` falls back to expanding
|
|
303
|
+
* the family. At or below it, checking each candidate is cheaper than walking a wide family.
|
|
304
|
+
*/
|
|
305
|
+
const FAMILIAL_CANDIDATE_LIMIT = 16;
|
|
306
|
+
|
|
307
|
+
/** Boost properties with a range index (see models/index.ts) */
|
|
308
|
+
const INDEXED_BOOST_FIELDS = ['id', 'category', 'autoConnectRecipients'] as const;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Finds one top-level condition in a converted BoostQuery that Neo4j can answer from an index:
|
|
312
|
+
* plain equality or `$in` on an indexed Boost property. buildWhereClause ANDs top-level fields
|
|
313
|
+
* together, so every boost the full filter accepts also passes this condition.
|
|
314
|
+
*
|
|
315
|
+
* The value is bound as `$familialProbe`, which can't clash with buildWhereClause's `param_N`.
|
|
316
|
+
*/
|
|
317
|
+
const getIndexedFamilialProbe = (
|
|
318
|
+
query: Record<string, unknown>
|
|
319
|
+
): { predicate: string; value: unknown } | undefined => {
|
|
320
|
+
// A document-level $or makes buildWhereClause ignore the other top-level keys
|
|
321
|
+
if ('$or' in query) return undefined;
|
|
322
|
+
|
|
323
|
+
for (const field of INDEXED_BOOST_FIELDS) {
|
|
324
|
+
const value = query[field];
|
|
325
|
+
|
|
326
|
+
if (value === undefined) continue;
|
|
327
|
+
|
|
328
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
329
|
+
return { predicate: `c.${field} = $familialProbe`, value };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Mirrors buildFieldCondition: a field-level $or wins over $in, then $in wins over the rest
|
|
333
|
+
if (!('$or' in value) && '$in' in value && Array.isArray(value.$in)) {
|
|
334
|
+
return { predicate: `c.${field} IN $familialProbe`, value: value.$in };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return undefined;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Same contract as `getFamilialBoostsCypher`, for queries with an indexed filter.
|
|
343
|
+
*
|
|
344
|
+
* It first index-seeks up to FAMILIAL_CANDIDATE_LIMIT + 1 boosts that pass the indexed condition.
|
|
345
|
+
* - More than the limit: the family is expanded exactly as in `getFamilialBoostsCypher`.
|
|
346
|
+
* - Otherwise: only those candidates are checked. A candidate is an ancestor if it is in start's
|
|
347
|
+
* ancestors, a descendant if start is among its own ancestors (walked up from the candidate,
|
|
348
|
+
* which stays small even under a wide parent), and a sibling via the original pattern.
|
|
349
|
+
*
|
|
350
|
+
* With includeExtendedFamily, the cousin check intersects the candidate's ancestors with start's.
|
|
351
|
+
* That set check can also reach nodes the single-pattern form can't (it ignores relationship
|
|
352
|
+
* uniqueness across the up and down legs), but any such node is already an ancestor or
|
|
353
|
+
* descendant of start as long as every node above start is a Boost. When a non-Boost node sits
|
|
354
|
+
* above start, the original cousin pattern is used instead.
|
|
355
|
+
*/
|
|
356
|
+
const getFamilialBoostsByCandidatesCypher = ({
|
|
357
|
+
parentGenerations,
|
|
358
|
+
childGenerations,
|
|
359
|
+
includeExtendedFamily,
|
|
360
|
+
probe,
|
|
361
|
+
}: {
|
|
362
|
+
parentGenerations: number;
|
|
363
|
+
childGenerations: number;
|
|
364
|
+
includeExtendedFamily: boolean;
|
|
365
|
+
probe: string;
|
|
366
|
+
}): string => {
|
|
367
|
+
const p = Number.isFinite(parentGenerations) ? parentGenerations : '';
|
|
368
|
+
const c = Number.isFinite(childGenerations) ? childGenerations : '';
|
|
369
|
+
const up = includeExtendedFamily ? `*1..${p}` : '';
|
|
370
|
+
const down = includeExtendedFamily ? `*1..${c}` : '';
|
|
371
|
+
const k = FAMILIAL_CANDIDATE_LIMIT;
|
|
372
|
+
const sibling = includeExtendedFamily
|
|
373
|
+
? `(n=size(a) AND any(x IN bu WHERE x IN a) OR n<>size(a) AND EXISTS{(start)<-[:PARENT_OF${up}]-(:Boost)-[:PARENT_OF${down}]->(boost)})`
|
|
374
|
+
: 'EXISTS{(start)<-[:PARENT_OF]-(:Boost)-[:PARENT_OF]->(boost)}';
|
|
375
|
+
|
|
376
|
+
return `
|
|
377
|
+
CALL{MATCH(c:Boost)WHERE ${probe} WITH c LIMIT ${k + 1} RETURN collect(c)AS cs}
|
|
378
|
+
OPTIONAL MATCH(start)<-[:PARENT_OF*1..${p}]-(u)
|
|
379
|
+
WITH start,cs,collect(DISTINCT u)AS us
|
|
380
|
+
WITH start,cs,size(us)AS n,[x IN us WHERE x:Boost]AS a
|
|
381
|
+
CALL{WITH start,cs WITH start WHERE size(cs)>${k}
|
|
382
|
+
OPTIONAL MATCH(start)-[:PARENT_OF*1..${c}]->(d:Boost)RETURN collect(DISTINCT d)AS ds}
|
|
383
|
+
CALL{WITH start,cs WITH start WHERE size(cs)>${k}
|
|
384
|
+
OPTIONAL MATCH(start)<-[:PARENT_OF${up}]-(:Boost)-[:PARENT_OF${down}]->(s:Boost)WHERE s<>start
|
|
385
|
+
RETURN collect(DISTINCT s)AS ss}
|
|
386
|
+
CALL{WITH start,cs,n,a WITH * WHERE size(cs)<=${k}
|
|
387
|
+
UNWIND cs AS boost
|
|
388
|
+
OPTIONAL MATCH(boost)<-[:PARENT_OF*1..${c}]-(v)
|
|
389
|
+
WITH start,n,a,boost,collect(DISTINCT v)AS bu
|
|
390
|
+
WHERE boost IN a OR start IN bu OR boost<>start AND ${sibling}
|
|
391
|
+
RETURN collect(boost)AS hits}
|
|
392
|
+
UNWIND CASE WHEN size(cs)>${k} THEN a+ds+ss ELSE hits END AS boost
|
|
393
|
+
WITH DISTINCT boost`;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/** Picks the familial Cypher for a query and returns the extra params it needs */
|
|
397
|
+
const getFamilialCypherForQuery = (
|
|
398
|
+
query: Record<string, unknown>,
|
|
399
|
+
generations: {
|
|
400
|
+
parentGenerations: number;
|
|
401
|
+
childGenerations: number;
|
|
402
|
+
includeExtendedFamily: boolean;
|
|
403
|
+
}
|
|
404
|
+
): { cypher: string; params: Record<string, unknown> } => {
|
|
405
|
+
const probe = getIndexedFamilialProbe(query);
|
|
406
|
+
|
|
407
|
+
if (!probe) return { cypher: getFamilialBoostsCypher(generations), params: {} };
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
cypher: getFamilialBoostsByCandidatesCypher({ ...generations, probe: probe.predicate }),
|
|
411
|
+
params: { familialProbe: probe.value },
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
262
415
|
export const getFamilialBoosts = async (
|
|
263
416
|
boost: BoostInstance,
|
|
264
417
|
{
|
|
@@ -291,24 +444,16 @@ export const getFamilialBoosts = async (
|
|
|
291
444
|
convertedQuery as any
|
|
292
445
|
);
|
|
293
446
|
|
|
294
|
-
const
|
|
447
|
+
const familial = getFamilialCypherForQuery(convertedQuery, {
|
|
448
|
+
parentGenerations,
|
|
449
|
+
childGenerations,
|
|
450
|
+
includeExtendedFamily,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const _query = new QueryBuilder(new BindParam({ cursor, ...queryParams, ...familial.params }))
|
|
295
454
|
.match({ model: Boost, where: { id: boost.id }, identifier: 'start' })
|
|
296
|
-
.
|
|
297
|
-
.where(
|
|
298
|
-
`
|
|
299
|
-
((start)<-[:PARENT_OF*1..${Number.isFinite(parentGenerations) ? parentGenerations : ''}]-(boost) OR
|
|
300
|
-
(start)-[:PARENT_OF*1..${Number.isFinite(childGenerations) ? childGenerations : ''}]->(boost) OR
|
|
301
|
-
(start)<-[:PARENT_OF${
|
|
302
|
-
includeExtendedFamily
|
|
303
|
-
? `*1..${Number.isFinite(parentGenerations) ? parentGenerations : ''}`
|
|
304
|
-
: ''
|
|
305
|
-
}]-(:Boost)-[:PARENT_OF${
|
|
306
|
-
includeExtendedFamily
|
|
307
|
-
? `*1..${Number.isFinite(childGenerations) ? childGenerations : ''}`
|
|
308
|
-
: ''
|
|
309
|
-
}]->(boost) AND boost <> start)
|
|
310
|
-
AND ${whereClause}`
|
|
311
|
-
)
|
|
455
|
+
.raw(familial.cypher)
|
|
456
|
+
.where(whereClause)
|
|
312
457
|
.raw(`MATCH (boost)-[createdBy:${Boost.getRelationshipByAlias('createdBy').name}]-()`);
|
|
313
458
|
|
|
314
459
|
const query = cursor ? _query.where('createdBy.date < $cursor') : _query;
|
|
@@ -358,24 +503,16 @@ export const countFamilialBoosts = async (
|
|
|
358
503
|
convertedQuery as any
|
|
359
504
|
);
|
|
360
505
|
|
|
361
|
-
const
|
|
506
|
+
const familial = getFamilialCypherForQuery(convertedQuery, {
|
|
507
|
+
parentGenerations,
|
|
508
|
+
childGenerations,
|
|
509
|
+
includeExtendedFamily,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const query = new QueryBuilder(new BindParam({ ...queryParams, ...familial.params }))
|
|
362
513
|
.match({ model: Boost, where: { id: boost.id }, identifier: 'start' })
|
|
363
|
-
.
|
|
364
|
-
.where(
|
|
365
|
-
`
|
|
366
|
-
((start)<-[:PARENT_OF*1..${Number.isFinite(parentGenerations) ? parentGenerations : ''}]-(boost) OR
|
|
367
|
-
(start)-[:PARENT_OF*1..${Number.isFinite(childGenerations) ? childGenerations : ''}]->(boost) OR
|
|
368
|
-
(start)<-[:PARENT_OF${
|
|
369
|
-
includeExtendedFamily
|
|
370
|
-
? `*1..${Number.isFinite(parentGenerations) ? parentGenerations : ''}`
|
|
371
|
-
: ''
|
|
372
|
-
}]-(:Boost)-[:PARENT_OF${
|
|
373
|
-
includeExtendedFamily
|
|
374
|
-
? `*1..${Number.isFinite(childGenerations) ? childGenerations : ''}`
|
|
375
|
-
: ''
|
|
376
|
-
}]->(boost) AND boost <> start)
|
|
377
|
-
AND ${whereClause}`
|
|
378
|
-
);
|
|
514
|
+
.raw(familial.cypher)
|
|
515
|
+
.where(whereClause);
|
|
379
516
|
|
|
380
517
|
const result = await query.return('COUNT(DISTINCT boost) AS count').run();
|
|
381
518
|
|