@odla-ai/chapter 0.47.2 → 0.47.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.
@@ -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 MAX_SAFE_LIKE_BYTES = 50;
6218
- function safePrefix(value) {
6219
- const prefixed = `${value}%`;
6220
- return value.includes("%") || value.includes("_") || new TextEncoder().encode(prefixed).byteLength > MAX_SAFE_LIKE_BYTES ? value : { $like: prefixed };
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 clauses = [
6224
- { type },
6225
- { archived: false },
6226
- ...window.stage ? [{ stage: window.stage }] : []
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 visible = [{ name: safePrefix(window.search) }];
6230
- if (emailField && allowed2.includes(emailField)) {
6231
- visible.push({ primaryEmail: safePrefix(window.search.toLowerCase()) });
6232
- }
6233
- clauses.push({ or: visible });
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 where = { and: clauses };
6236
- const crmDb = db;
6237
- const [result, aggregate] = await Promise.all([
6238
- crmDb.query({
6239
- [import_crm9.CRM_NS.record]: { $: {
6240
- where,
6241
- order: { [window.sort.field]: window.sort.dir },
6242
- limit: window.limit,
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: aggregate.count ?? 0
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
- const page = await listReaderRecords(
6410
- db,
6411
- type,
6412
- allowed2,
6413
- typeDef.emailField,
6414
- window
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 },