@odla-ai/chapter 0.47.2 → 0.47.4
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/worker/index.cjs +101 -40
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +101 -40
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -6190,40 +6190,80 @@ function restrictedRecord(record4, target) {
|
|
|
6190
6190
|
|
|
6191
6191
|
// src/worker-routes-network-records-search.ts
|
|
6192
6192
|
import { CRM_NS, rowToRecord } from "@odla-ai/crm";
|
|
6193
|
-
var
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6193
|
+
var SEARCH_BATCH_SIZE = 1e3;
|
|
6194
|
+
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
6195
|
+
var ReaderSearchPopulationError = class extends Error {
|
|
6196
|
+
count;
|
|
6197
|
+
constructor(count) {
|
|
6198
|
+
super(`reader-visible search population exceeds ${MAX_READER_SEARCH_POPULATION} records`);
|
|
6199
|
+
this.name = "ReaderSearchPopulationError";
|
|
6200
|
+
this.count = count;
|
|
6201
|
+
}
|
|
6202
|
+
};
|
|
6203
|
+
function folded(value) {
|
|
6204
|
+
return value.replace(/[øØ]/gu, "o").replace(/[łŁ]/gu, "l").replace(/[đĐðÐ]/gu, "d").replace(/[æÆ]/gu, "ae").replace(/[œŒ]/gu, "oe").replace(/[þÞ]/gu, "th").replace(/ß/gu, "ss").normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}@._+-]+/gu, " ").trim();
|
|
6205
|
+
}
|
|
6206
|
+
function terms(value) {
|
|
6207
|
+
return folded(value).split(/\s+/u).filter(Boolean);
|
|
6208
|
+
}
|
|
6209
|
+
function matchesReaderIdentity(record4, search, includeEmail) {
|
|
6210
|
+
const query = terms(search);
|
|
6211
|
+
if (!query.length) return true;
|
|
6212
|
+
const name = folded(record4.name);
|
|
6213
|
+
const visible = [name, ...name.split(/\s+/u).filter(Boolean)];
|
|
6214
|
+
if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
|
|
6215
|
+
return query.every((term) => visible.some((candidate) => candidate.includes(term)));
|
|
6216
|
+
}
|
|
6217
|
+
function baseWhere(type, stage) {
|
|
6218
|
+
return {
|
|
6219
|
+
and: [
|
|
6220
|
+
{ type },
|
|
6221
|
+
{ archived: false },
|
|
6222
|
+
...stage ? [{ stage }] : []
|
|
6223
|
+
]
|
|
6224
|
+
};
|
|
6225
|
+
}
|
|
6226
|
+
async function listVisibleSearchPopulation(db, where, window, count) {
|
|
6227
|
+
if (count > MAX_READER_SEARCH_POPULATION) throw new ReaderSearchPopulationError(count);
|
|
6228
|
+
const offsets = Array.from(
|
|
6229
|
+
{ length: Math.ceil(count / SEARCH_BATCH_SIZE) },
|
|
6230
|
+
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
6231
|
+
);
|
|
6232
|
+
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
6233
|
+
[CRM_NS.record]: { $: {
|
|
6234
|
+
where,
|
|
6235
|
+
order: { [window.sort.field]: window.sort.dir },
|
|
6236
|
+
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
6237
|
+
offset
|
|
6238
|
+
} }
|
|
6239
|
+
})));
|
|
6240
|
+
return pages.flatMap((page) => (page[CRM_NS.record] ?? []).map(rowToRecord));
|
|
6197
6241
|
}
|
|
6198
6242
|
async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
6199
|
-
const
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
];
|
|
6243
|
+
const where = baseWhere(type, window.stage);
|
|
6244
|
+
const crmDb = db;
|
|
6245
|
+
const aggregate = await crmDb.aggregate(CRM_NS.record, { count: true }, { where });
|
|
6246
|
+
const candidateCount = aggregate.count ?? 0;
|
|
6204
6247
|
if (window.search) {
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6248
|
+
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
6249
|
+
const includeEmail = Boolean(emailField && allowed2.includes(emailField));
|
|
6250
|
+
const matches = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
|
|
6251
|
+
return {
|
|
6252
|
+
records: matches.slice(window.offset, window.offset + window.limit),
|
|
6253
|
+
total: matches.length
|
|
6254
|
+
};
|
|
6210
6255
|
}
|
|
6211
|
-
const
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
offset: window.offset
|
|
6220
|
-
} }
|
|
6221
|
-
}),
|
|
6222
|
-
crmDb.aggregate(CRM_NS.record, { count: true }, { where })
|
|
6223
|
-
]);
|
|
6256
|
+
const result = await crmDb.query({
|
|
6257
|
+
[CRM_NS.record]: { $: {
|
|
6258
|
+
where,
|
|
6259
|
+
order: { [window.sort.field]: window.sort.dir },
|
|
6260
|
+
limit: window.limit,
|
|
6261
|
+
offset: window.offset
|
|
6262
|
+
} }
|
|
6263
|
+
});
|
|
6224
6264
|
return {
|
|
6225
6265
|
records: (result[CRM_NS.record] ?? []).map(rowToRecord),
|
|
6226
|
-
total:
|
|
6266
|
+
total: candidateCount
|
|
6227
6267
|
};
|
|
6228
6268
|
}
|
|
6229
6269
|
|
|
@@ -6382,13 +6422,25 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6382
6422
|
if (window.stage && !stages.some((stage) => stage.id === window.stage)) {
|
|
6383
6423
|
return json({ error: "stage is not defined for this record type" }, 400);
|
|
6384
6424
|
}
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6425
|
+
let page;
|
|
6426
|
+
try {
|
|
6427
|
+
page = await listReaderRecords(
|
|
6428
|
+
db,
|
|
6429
|
+
type,
|
|
6430
|
+
allowed2,
|
|
6431
|
+
typeDef.emailField,
|
|
6432
|
+
window
|
|
6433
|
+
);
|
|
6434
|
+
} catch (error) {
|
|
6435
|
+
if (error instanceof ReaderSearchPopulationError) {
|
|
6436
|
+
return json({
|
|
6437
|
+
error: "directory search requires a narrower stage filter",
|
|
6438
|
+
code: "search_population_too_large",
|
|
6439
|
+
maximum: 1e4
|
|
6440
|
+
}, 422);
|
|
6441
|
+
}
|
|
6442
|
+
throw error;
|
|
6443
|
+
}
|
|
6392
6444
|
const response = {
|
|
6393
6445
|
version: 1,
|
|
6394
6446
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
@@ -6941,6 +6993,9 @@ async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
|
6941
6993
|
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6942
6994
|
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6943
6995
|
}
|
|
6996
|
+
async function networkChapterPersonSourceKey(chapterId, chapterPersonId) {
|
|
6997
|
+
return `chapter_source_${await sha256(`${chapterId}\0${chapterPersonId}`)}`;
|
|
6998
|
+
}
|
|
6944
6999
|
function projectNetworkContactPolicy(policy, links) {
|
|
6945
7000
|
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6946
7001
|
}
|
|
@@ -7202,10 +7257,11 @@ var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
|
7202
7257
|
function redactedName(displayName) {
|
|
7203
7258
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7204
7259
|
}
|
|
7205
|
-
var redactedLink = (link) => ({
|
|
7260
|
+
var redactedLink = async (link) => ({
|
|
7206
7261
|
id: link.id,
|
|
7207
7262
|
personId: link.personId,
|
|
7208
7263
|
chapterId: link.chapterId,
|
|
7264
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId),
|
|
7209
7265
|
state: link.state,
|
|
7210
7266
|
createdAt: link.createdAt
|
|
7211
7267
|
});
|
|
@@ -7369,8 +7425,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7369
7425
|
}
|
|
7370
7426
|
}
|
|
7371
7427
|
const state = await loadNetworkPersonRegistry(db);
|
|
7372
|
-
const persons = state.persons.map((person) => {
|
|
7428
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7373
7429
|
const links = state.links.filter((link) => link.personId === person.id);
|
|
7430
|
+
const safeLinks = await Promise.all(links.map(redactedLink));
|
|
7374
7431
|
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7375
7432
|
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7376
7433
|
return {
|
|
@@ -7382,16 +7439,20 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7382
7439
|
revision: person.revision,
|
|
7383
7440
|
contactPolicy: safePolicy(person.contactPolicy),
|
|
7384
7441
|
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7385
|
-
chapterLinks:
|
|
7386
|
-
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy,
|
|
7442
|
+
chapterLinks: safeLinks,
|
|
7443
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7387
7444
|
};
|
|
7388
|
-
});
|
|
7445
|
+
}));
|
|
7389
7446
|
const aliases = state.aliases.map(safeAlias);
|
|
7390
7447
|
return json({
|
|
7391
7448
|
version: 1,
|
|
7392
7449
|
mode: "shadow",
|
|
7393
7450
|
writable: true,
|
|
7394
7451
|
authoritativeForChapterOperation: false,
|
|
7452
|
+
// loadNetworkPersonRegistry deliberately bounds each collection at 1,000.
|
|
7453
|
+
// Exactly 1,000 links may mean there are more, so fail closed rather than
|
|
7454
|
+
// letting a consumer mistake an omitted explicit link for "source only".
|
|
7455
|
+
chapterLinkProjectionComplete: state.links.length < 1e3,
|
|
7395
7456
|
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7396
7457
|
persons,
|
|
7397
7458
|
aliases,
|