@odla-ai/chapter 0.47.0 → 0.47.2

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.
@@ -6117,7 +6117,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
6117
6117
  };
6118
6118
 
6119
6119
  // src/worker-routes-network-records-follower.ts
6120
- var import_crm9 = require("@odla-ai/crm");
6120
+ var import_crm10 = require("@odla-ai/crm");
6121
6121
  var import_db9 = require("@odla-ai/db");
6122
6122
 
6123
6123
  // src/worker-routes-network-records-common.ts
@@ -6125,9 +6125,39 @@ var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
6125
6125
  var MAX_PAGE = 50;
6126
6126
  var MAX_OFFSET = 1e4;
6127
6127
  var MAX_SEARCH = 200;
6128
+ var MAX_STAGE = 100;
6128
6129
  var MAX_BODY = 32768;
6129
6130
  var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
6130
6131
  var MUTATION_ID2 = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
6132
+ var NETWORK_RECORD_SORTS = [
6133
+ { id: "updated_desc", label: "Recently updated" },
6134
+ { id: "updated_asc", label: "Least recently updated" },
6135
+ { id: "name_asc", label: "Name A\u2013Z" },
6136
+ { id: "name_desc", label: "Name Z\u2013A" }
6137
+ ];
6138
+ var NETWORK_RECORD_SORT_COLUMNS = {
6139
+ updated_desc: { field: "updatedAt", dir: "desc" },
6140
+ updated_asc: { field: "updatedAt", dir: "asc" },
6141
+ name_asc: { field: "name", dir: "asc" },
6142
+ name_desc: { field: "name", dir: "desc" }
6143
+ };
6144
+ function networkRecordWindow(params) {
6145
+ const limit = integer(params.get("limit"), 25, 1, MAX_PAGE);
6146
+ const offset = integer(params.get("offset"), 0, 0, MAX_OFFSET);
6147
+ const search = (params.get("search") ?? "").trim();
6148
+ const stage = (params.get("stage") ?? "").trim();
6149
+ const requestedSort = (params.get("sort") ?? "updated_desc").trim();
6150
+ const sortId = NETWORK_RECORD_SORTS.some((candidate) => candidate.id === requestedSort) ? requestedSort : null;
6151
+ if (limit == null || offset == null || search.length > MAX_SEARCH || stage.length > 0 && (stage.length > MAX_STAGE || /[\u0000-\u001f\u007f]/.test(stage)) || !sortId) return null;
6152
+ return {
6153
+ limit,
6154
+ offset,
6155
+ ...search ? { search } : {},
6156
+ ...stage ? { stage } : {},
6157
+ sortId,
6158
+ sort: NETWORK_RECORD_SORT_COLUMNS[sortId]
6159
+ };
6160
+ }
6131
6161
  function bodyTooLarge(body) {
6132
6162
  return new TextEncoder().encode(body).byteLength > MAX_BODY;
6133
6163
  }
@@ -6182,6 +6212,45 @@ function restrictedRecord(record4, target) {
6182
6212
  return { ...record4, ...edit ? { edit } : { edit: void 0 } };
6183
6213
  }
6184
6214
 
6215
+ // src/worker-routes-network-records-search.ts
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 };
6221
+ }
6222
+ async function listReaderRecords(db, type, allowed2, emailField, window) {
6223
+ const clauses = [
6224
+ { type },
6225
+ { archived: false },
6226
+ ...window.stage ? [{ stage: window.stage }] : []
6227
+ ];
6228
+ 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 });
6234
+ }
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
+ ]);
6248
+ return {
6249
+ records: (result[import_crm9.CRM_NS.record] ?? []).map(import_crm9.rowToRecord),
6250
+ total: aggregate.count ?? 0
6251
+ };
6252
+ }
6253
+
6185
6254
  // src/worker-routes-network-records-follower.ts
6186
6255
  var NETWORK_MUTATION_META = "chapterNetworkMutation";
6187
6256
  function mutationFingerprint(input) {
@@ -6201,16 +6270,16 @@ function mutationError(error) {
6201
6270
  if (error?.code === "transact_guard_failed") {
6202
6271
  return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
6203
6272
  }
6204
- if (error instanceof import_crm9.CrmInputError) {
6273
+ if (error instanceof import_crm10.CrmInputError) {
6205
6274
  return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
6206
6275
  }
6207
6276
  return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
6208
6277
  }
6209
6278
  async function priorMutation(db, id) {
6210
6279
  const result = await db.query({
6211
- [import_crm9.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
6280
+ [import_crm10.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
6212
6281
  });
6213
- const row = (result[import_crm9.CRM_NS.activity] ?? [])[0];
6282
+ const row = (result[import_crm10.CRM_NS.activity] ?? [])[0];
6214
6283
  const meta = row?.meta;
6215
6284
  const receipt = meta?.[NETWORK_MUTATION_META];
6216
6285
  return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
@@ -6264,7 +6333,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6264
6333
  if (prior.fingerprint !== fingerprint) {
6265
6334
  return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
6266
6335
  }
6267
- const replayed = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6336
+ const replayed = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6268
6337
  if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
6269
6338
  const result = {
6270
6339
  version: 1,
@@ -6274,7 +6343,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6274
6343
  };
6275
6344
  return json(result);
6276
6345
  }
6277
- const record4 = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6346
+ const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6278
6347
  if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
6279
6348
  if (record4.updatedAt !== input.expectedRevision) {
6280
6349
  return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
@@ -6295,7 +6364,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6295
6364
  };
6296
6365
  try {
6297
6366
  const deps2 = { crm: ctx.chapter.crm, db };
6298
- const updated = hasFields ? await (0, import_crm9.updateRecord)(deps2, {
6367
+ const updated = hasFields ? await (0, import_crm10.updateRecord)(deps2, {
6299
6368
  id: record4.id,
6300
6369
  input: fields,
6301
6370
  mutationId: activityId,
@@ -6306,7 +6375,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6306
6375
  authorId: `network:${verified.sender}`,
6307
6376
  meta: auditMeta
6308
6377
  }
6309
- }) : await (0, import_crm9.setStage)(deps2, {
6378
+ }) : await (0, import_crm10.setStage)(deps2, {
6310
6379
  id: record4.id,
6311
6380
  to: input.stage,
6312
6381
  authorId: `network:${verified.sender}`,
@@ -6330,21 +6399,19 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6330
6399
  const type = url.searchParams.get("type") ?? "";
6331
6400
  const allowed2 = reader.fields[type];
6332
6401
  if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
6333
- const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
6334
- const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
6335
- const search = (url.searchParams.get("search") ?? "").trim();
6336
- if (limit == null || offset == null || search.length > MAX_SEARCH) {
6337
- return json({ error: "invalid pagination or search parameters" }, 400);
6338
- }
6339
- const page = await (0, import_crm9.listRecords)(
6340
- { crm: ctx.chapter.crm, db },
6341
- {
6342
- type,
6343
- limit,
6344
- offset,
6345
- ...search ? { search } : {},
6346
- sort: { field: "updatedAt", dir: "desc" }
6347
- }
6402
+ const window = networkRecordWindow(url.searchParams);
6403
+ if (!window) return json({ error: "invalid record query parameters" }, 400);
6404
+ const typeDef = ctx.chapter.crm.type(type);
6405
+ const stages = typeDef.pipeline?.stages ?? [];
6406
+ if (window.stage && !stages.some((stage) => stage.id === window.stage)) {
6407
+ return json({ error: "stage is not defined for this record type" }, 400);
6408
+ }
6409
+ const page = await listReaderRecords(
6410
+ db,
6411
+ type,
6412
+ allowed2,
6413
+ typeDef.emailField,
6414
+ window
6348
6415
  );
6349
6416
  const response = {
6350
6417
  version: 1,
@@ -6352,8 +6419,16 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
6352
6419
  type,
6353
6420
  records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
6354
6421
  total: page.total,
6355
- limit: page.limit,
6356
- offset: page.offset
6422
+ limit: window.limit,
6423
+ offset: window.offset,
6424
+ filters: {
6425
+ stages: stages.map((stage) => ({ id: stage.id, label: stage.label ?? stage.id })),
6426
+ sorts: [...NETWORK_RECORD_SORTS]
6427
+ },
6428
+ appliedQuery: {
6429
+ ...window.stage ? { stage: window.stage } : {},
6430
+ sort: window.sortId
6431
+ }
6357
6432
  };
6358
6433
  return json(response);
6359
6434
  };
@@ -6386,7 +6461,8 @@ async function readTargetRecords(db, env, ctx, target, query) {
6386
6461
  const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
6387
6462
  return {
6388
6463
  ok: false,
6389
- error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
6464
+ error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`),
6465
+ ...response.status === 400 ? { status: 400 } : {}
6390
6466
  };
6391
6467
  }
6392
6468
  return {
@@ -6477,16 +6553,22 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
6477
6553
  const type = url.searchParams.get("type") ?? "";
6478
6554
  const target = targetFor2(ctx, targetId);
6479
6555
  if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
6480
- const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
6481
- const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
6482
- const search = (url.searchParams.get("search") ?? "").trim();
6483
- if (limit == null || offset == null || search.length > MAX_SEARCH) {
6484
- return json({ error: "invalid pagination or search parameters" }, 400);
6485
- }
6486
- const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
6487
- if (search) query.set("search", search);
6556
+ const window = networkRecordWindow(url.searchParams);
6557
+ if (!window) return json({ error: "invalid record query parameters" }, 400);
6558
+ const query = new URLSearchParams({
6559
+ type,
6560
+ limit: String(window.limit),
6561
+ offset: String(window.offset),
6562
+ sort: window.sortId
6563
+ });
6564
+ if (window.search) query.set("search", window.search);
6565
+ if (window.stage) query.set("stage", window.stage);
6488
6566
  const result = await readTargetRecords(got.db, env, ctx, target, query);
6489
- return result.ok ? json(result.page) : json({ error: result.error }, 502);
6567
+ if (!result.ok) return json({ error: result.error }, result.status ?? 502);
6568
+ if ((window.stage || url.searchParams.has("sort")) && (!result.page.appliedQuery || result.page.appliedQuery.sort !== window.sortId || result.page.appliedQuery.stage !== window.stage)) {
6569
+ return json({ error: "follower did not apply the requested record query" }, 502);
6570
+ }
6571
+ return json(result.page);
6490
6572
  };
6491
6573
 
6492
6574
  // src/worker-routes-network-notes.ts
@@ -6574,7 +6656,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
6574
6656
  };
6575
6657
 
6576
6658
  // src/worker-routes-network-shared-notes.ts
6577
- var import_crm10 = require("@odla-ai/crm");
6659
+ var import_crm11 = require("@odla-ai/crm");
6578
6660
  var import_db11 = require("@odla-ai/db");
6579
6661
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
6580
6662
  var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
@@ -6604,7 +6686,7 @@ function sharedNote(activity, sourceId) {
6604
6686
  }
6605
6687
  async function listSharedNotes(db, sourceId, recordId) {
6606
6688
  const result = await db.query({
6607
- [import_crm10.CRM_NS.activity]: {
6689
+ [import_crm11.CRM_NS.activity]: {
6608
6690
  $: {
6609
6691
  where: {
6610
6692
  and: [
@@ -6617,7 +6699,7 @@ async function listSharedNotes(db, sourceId, recordId) {
6617
6699
  }
6618
6700
  }
6619
6701
  });
6620
- return (result[import_crm10.CRM_NS.activity] ?? []).flatMap((activity) => {
6702
+ return (result[import_crm11.CRM_NS.activity] ?? []).flatMap((activity) => {
6621
6703
  const note = sharedNote(activity, sourceId);
6622
6704
  return note ? [note] : [];
6623
6705
  });
@@ -6645,7 +6727,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
6645
6727
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
6646
6728
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
6647
6729
  }
6648
- const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
6730
+ const record4 = await (0, import_crm11.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
6649
6731
  if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
6650
6732
  if (req.method === "GET") {
6651
6733
  return json({
@@ -6667,7 +6749,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
6667
6749
  }
6668
6750
  const createdAt = Date.now();
6669
6751
  const id = `network:${verified.sender}:${mutationId}`;
6670
- const result = await (0, import_crm10.addActivity)(
6752
+ const result = await (0, import_crm11.addActivity)(
6671
6753
  {
6672
6754
  crm: ctx.chapter.crm,
6673
6755
  db,
@@ -8240,7 +8322,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
8240
8322
  };
8241
8323
 
8242
8324
  // src/worker-routes-formation.ts
8243
- var import_crm11 = require("@odla-ai/crm");
8325
+ var import_crm12 = require("@odla-ai/crm");
8244
8326
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
8245
8327
  var handleFormation = async (req, url, env, ctx) => {
8246
8328
  const formation = ctx.chapter.formation;
@@ -8292,7 +8374,7 @@ var handleFormation = async (req, url, env, ctx) => {
8292
8374
  const db = ctx.makeDb(env);
8293
8375
  const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
8294
8376
  try {
8295
- const created = await (0, import_crm11.createRecord)(
8377
+ const created = await (0, import_crm12.createRecord)(
8296
8378
  {
8297
8379
  crm: ctx.chapter.crm,
8298
8380
  db,