@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.cjs
CHANGED
|
@@ -6214,40 +6214,80 @@ function restrictedRecord(record4, target) {
|
|
|
6214
6214
|
|
|
6215
6215
|
// src/worker-routes-network-records-search.ts
|
|
6216
6216
|
var import_crm9 = require("@odla-ai/crm");
|
|
6217
|
-
var
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6217
|
+
var SEARCH_BATCH_SIZE = 1e3;
|
|
6218
|
+
var MAX_READER_SEARCH_POPULATION = 1e4;
|
|
6219
|
+
var ReaderSearchPopulationError = class extends Error {
|
|
6220
|
+
count;
|
|
6221
|
+
constructor(count) {
|
|
6222
|
+
super(`reader-visible search population exceeds ${MAX_READER_SEARCH_POPULATION} records`);
|
|
6223
|
+
this.name = "ReaderSearchPopulationError";
|
|
6224
|
+
this.count = count;
|
|
6225
|
+
}
|
|
6226
|
+
};
|
|
6227
|
+
function folded(value) {
|
|
6228
|
+
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();
|
|
6229
|
+
}
|
|
6230
|
+
function terms(value) {
|
|
6231
|
+
return folded(value).split(/\s+/u).filter(Boolean);
|
|
6232
|
+
}
|
|
6233
|
+
function matchesReaderIdentity(record4, search, includeEmail) {
|
|
6234
|
+
const query = terms(search);
|
|
6235
|
+
if (!query.length) return true;
|
|
6236
|
+
const name = folded(record4.name);
|
|
6237
|
+
const visible = [name, ...name.split(/\s+/u).filter(Boolean)];
|
|
6238
|
+
if (includeEmail && record4.primaryEmail) visible.push(folded(record4.primaryEmail));
|
|
6239
|
+
return query.every((term) => visible.some((candidate) => candidate.includes(term)));
|
|
6240
|
+
}
|
|
6241
|
+
function baseWhere(type, stage) {
|
|
6242
|
+
return {
|
|
6243
|
+
and: [
|
|
6244
|
+
{ type },
|
|
6245
|
+
{ archived: false },
|
|
6246
|
+
...stage ? [{ stage }] : []
|
|
6247
|
+
]
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
async function listVisibleSearchPopulation(db, where, window, count) {
|
|
6251
|
+
if (count > MAX_READER_SEARCH_POPULATION) throw new ReaderSearchPopulationError(count);
|
|
6252
|
+
const offsets = Array.from(
|
|
6253
|
+
{ length: Math.ceil(count / SEARCH_BATCH_SIZE) },
|
|
6254
|
+
(_, index2) => index2 * SEARCH_BATCH_SIZE
|
|
6255
|
+
);
|
|
6256
|
+
const pages = await Promise.all(offsets.map((offset) => db.query({
|
|
6257
|
+
[import_crm9.CRM_NS.record]: { $: {
|
|
6258
|
+
where,
|
|
6259
|
+
order: { [window.sort.field]: window.sort.dir },
|
|
6260
|
+
limit: Math.min(SEARCH_BATCH_SIZE, count - offset),
|
|
6261
|
+
offset
|
|
6262
|
+
} }
|
|
6263
|
+
})));
|
|
6264
|
+
return pages.flatMap((page) => (page[import_crm9.CRM_NS.record] ?? []).map(import_crm9.rowToRecord));
|
|
6221
6265
|
}
|
|
6222
6266
|
async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
6223
|
-
const
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
];
|
|
6267
|
+
const where = baseWhere(type, window.stage);
|
|
6268
|
+
const crmDb = db;
|
|
6269
|
+
const aggregate = await crmDb.aggregate(import_crm9.CRM_NS.record, { count: true }, { where });
|
|
6270
|
+
const candidateCount = aggregate.count ?? 0;
|
|
6228
6271
|
if (window.search) {
|
|
6229
|
-
const
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6272
|
+
const population = await listVisibleSearchPopulation(crmDb, where, window, candidateCount);
|
|
6273
|
+
const includeEmail = Boolean(emailField && allowed2.includes(emailField));
|
|
6274
|
+
const matches = population.filter((record4) => matchesReaderIdentity(record4, window.search, includeEmail));
|
|
6275
|
+
return {
|
|
6276
|
+
records: matches.slice(window.offset, window.offset + window.limit),
|
|
6277
|
+
total: matches.length
|
|
6278
|
+
};
|
|
6234
6279
|
}
|
|
6235
|
-
const
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
offset: window.offset
|
|
6244
|
-
} }
|
|
6245
|
-
}),
|
|
6246
|
-
crmDb.aggregate(import_crm9.CRM_NS.record, { count: true }, { where })
|
|
6247
|
-
]);
|
|
6280
|
+
const result = await crmDb.query({
|
|
6281
|
+
[import_crm9.CRM_NS.record]: { $: {
|
|
6282
|
+
where,
|
|
6283
|
+
order: { [window.sort.field]: window.sort.dir },
|
|
6284
|
+
limit: window.limit,
|
|
6285
|
+
offset: window.offset
|
|
6286
|
+
} }
|
|
6287
|
+
});
|
|
6248
6288
|
return {
|
|
6249
6289
|
records: (result[import_crm9.CRM_NS.record] ?? []).map(import_crm9.rowToRecord),
|
|
6250
|
-
total:
|
|
6290
|
+
total: candidateCount
|
|
6251
6291
|
};
|
|
6252
6292
|
}
|
|
6253
6293
|
|
|
@@ -6406,13 +6446,25 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
6406
6446
|
if (window.stage && !stages.some((stage) => stage.id === window.stage)) {
|
|
6407
6447
|
return json({ error: "stage is not defined for this record type" }, 400);
|
|
6408
6448
|
}
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6449
|
+
let page;
|
|
6450
|
+
try {
|
|
6451
|
+
page = await listReaderRecords(
|
|
6452
|
+
db,
|
|
6453
|
+
type,
|
|
6454
|
+
allowed2,
|
|
6455
|
+
typeDef.emailField,
|
|
6456
|
+
window
|
|
6457
|
+
);
|
|
6458
|
+
} catch (error) {
|
|
6459
|
+
if (error instanceof ReaderSearchPopulationError) {
|
|
6460
|
+
return json({
|
|
6461
|
+
error: "directory search requires a narrower stage filter",
|
|
6462
|
+
code: "search_population_too_large",
|
|
6463
|
+
maximum: 1e4
|
|
6464
|
+
}, 422);
|
|
6465
|
+
}
|
|
6466
|
+
throw error;
|
|
6467
|
+
}
|
|
6416
6468
|
const response = {
|
|
6417
6469
|
version: 1,
|
|
6418
6470
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
@@ -6961,6 +7013,9 @@ async function networkSourceKey(sourceSystem, sourceRecordId) {
|
|
|
6961
7013
|
async function networkChapterLinkId(personId, chapterId, chapterPersonId) {
|
|
6962
7014
|
return `chapter_person_${await sha256(`${personId}\0${chapterId}\0${chapterPersonId}`)}`;
|
|
6963
7015
|
}
|
|
7016
|
+
async function networkChapterPersonSourceKey(chapterId, chapterPersonId) {
|
|
7017
|
+
return `chapter_source_${await sha256(`${chapterId}\0${chapterPersonId}`)}`;
|
|
7018
|
+
}
|
|
6964
7019
|
function projectNetworkContactPolicy(policy, links) {
|
|
6965
7020
|
return links.map((link) => ({ ...link, contactPolicy: { ...policy } }));
|
|
6966
7021
|
}
|
|
@@ -7222,10 +7277,11 @@ var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
|
7222
7277
|
function redactedName(displayName) {
|
|
7223
7278
|
return displayName.split(/\s+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}\u2026`).join(" ");
|
|
7224
7279
|
}
|
|
7225
|
-
var redactedLink = (link) => ({
|
|
7280
|
+
var redactedLink = async (link) => ({
|
|
7226
7281
|
id: link.id,
|
|
7227
7282
|
personId: link.personId,
|
|
7228
7283
|
chapterId: link.chapterId,
|
|
7284
|
+
sourceKey: await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId),
|
|
7229
7285
|
state: link.state,
|
|
7230
7286
|
createdAt: link.createdAt
|
|
7231
7287
|
});
|
|
@@ -7389,8 +7445,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7389
7445
|
}
|
|
7390
7446
|
}
|
|
7391
7447
|
const state = await loadNetworkPersonRegistry(db);
|
|
7392
|
-
const persons = state.persons.map((person) => {
|
|
7448
|
+
const persons = await Promise.all(state.persons.map(async (person) => {
|
|
7393
7449
|
const links = state.links.filter((link) => link.personId === person.id);
|
|
7450
|
+
const safeLinks = await Promise.all(links.map(redactedLink));
|
|
7394
7451
|
const canonicalId = canonicalNetworkPersonId(person.id, state.persons);
|
|
7395
7452
|
const canonical2 = state.persons.find((candidate) => candidate.id === canonicalId) ?? person;
|
|
7396
7453
|
return {
|
|
@@ -7402,16 +7459,20 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
7402
7459
|
revision: person.revision,
|
|
7403
7460
|
contactPolicy: safePolicy(person.contactPolicy),
|
|
7404
7461
|
identityClaims: state.claims.filter((claim) => claim.personId === person.id).map(({ id, kind, hint, verifiedAt }) => ({ id, kind, hint: hint ?? null, verifiedAt })),
|
|
7405
|
-
chapterLinks:
|
|
7406
|
-
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy,
|
|
7462
|
+
chapterLinks: safeLinks,
|
|
7463
|
+
chapterProjections: projectNetworkContactPolicy(canonical2.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safePolicy(contactPolicy) }))
|
|
7407
7464
|
};
|
|
7408
|
-
});
|
|
7465
|
+
}));
|
|
7409
7466
|
const aliases = state.aliases.map(safeAlias);
|
|
7410
7467
|
return json({
|
|
7411
7468
|
version: 1,
|
|
7412
7469
|
mode: "shadow",
|
|
7413
7470
|
writable: true,
|
|
7414
7471
|
authoritativeForChapterOperation: false,
|
|
7472
|
+
// loadNetworkPersonRegistry deliberately bounds each collection at 1,000.
|
|
7473
|
+
// Exactly 1,000 links may mean there are more, so fail closed rather than
|
|
7474
|
+
// letting a consumer mistake an omitted explicit link for "source only".
|
|
7475
|
+
chapterLinkProjectionComplete: state.links.length < 1e3,
|
|
7415
7476
|
configuredChapterIds: ctx.chapter.networkPersonRegistry.chapterIds,
|
|
7416
7477
|
persons,
|
|
7417
7478
|
aliases,
|