@aglyn/aglyn 1.0.0-beta.198 → 1.0.0-beta.200

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.
Files changed (113) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-search.d.ts +39 -0
  3. package/src/lib/app-utils/activity-search.js +85 -0
  4. package/src/lib/app-utils/activity-search.js.map +1 -0
  5. package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
  6. package/src/lib/app-utils/admin-audit-index.js +177 -0
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -0
  8. package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
  9. package/src/lib/app-utils/artifact-list-keys.js +144 -0
  10. package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
  11. package/src/lib/app-utils/content-query-fields.d.ts +69 -0
  12. package/src/lib/app-utils/content-query-fields.js +79 -0
  13. package/src/lib/app-utils/content-query-fields.js.map +1 -0
  14. package/src/lib/app-utils/crm.d.ts +151 -1
  15. package/src/lib/app-utils/crm.js +388 -1
  16. package/src/lib/app-utils/crm.js.map +1 -1
  17. package/src/lib/app-utils/dataset-models.d.ts +75 -24
  18. package/src/lib/app-utils/dataset-models.js +107 -40
  19. package/src/lib/app-utils/dataset-models.js.map +1 -1
  20. package/src/lib/app-utils/docs-index.generated.js +40 -40
  21. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  22. package/src/lib/app-utils/email-search.d.ts +36 -0
  23. package/src/lib/app-utils/email-search.js +53 -0
  24. package/src/lib/app-utils/email-search.js.map +1 -0
  25. package/src/lib/app-utils/forms.d.ts +83 -3
  26. package/src/lib/app-utils/forms.js +67 -0
  27. package/src/lib/app-utils/forms.js.map +1 -1
  28. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
  29. package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
  30. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
  31. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  32. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  33. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  35. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  36. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  38. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  39. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  41. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  42. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  44. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  45. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  47. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  48. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  50. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  51. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  53. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  54. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  56. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  57. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  59. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  60. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  62. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  63. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  65. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  66. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  68. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  69. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  71. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  72. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  73. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  74. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  75. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  76. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  77. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  78. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  79. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  80. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  81. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  82. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  83. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  84. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  85. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  86. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  87. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  88. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  89. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  90. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  91. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  92. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  93. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  94. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  95. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  96. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  97. package/src/lib/app-utils/media-metadata.d.ts +79 -0
  98. package/src/lib/app-utils/media-metadata.js +74 -0
  99. package/src/lib/app-utils/media-metadata.js.map +1 -1
  100. package/src/lib/app-utils/message-search.d.ts +40 -0
  101. package/src/lib/app-utils/message-search.js +168 -0
  102. package/src/lib/app-utils/message-search.js.map +1 -0
  103. package/src/lib/app-utils/name-search.d.ts +95 -0
  104. package/src/lib/app-utils/name-search.js +108 -0
  105. package/src/lib/app-utils/name-search.js.map +1 -1
  106. package/src/lib/app-utils/server.d.ts +2 -0
  107. package/src/lib/app-utils/server.js +2 -0
  108. package/src/lib/app-utils/server.js.map +1 -1
  109. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  110. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  111. package/src/server.d.ts +1 -0
  112. package/src/server.js +7 -0
  113. package/src/server.js.map +1 -1
@@ -45,7 +45,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
45
45
  */ import { normalizeAddress, normalizePhone } from "../foundation/index.js";
46
46
  import { consentGroupScope } from "./consent-groups.js";
47
47
  import { CONTACT_LIFECYCLE_STAGES, isContactLifecycleStage } from "./crm-kinds.js";
48
- import { CONTACT_FACETS_FIELD, CONTACT_SOURCE_LABELS, normalizeContactEmail, readContactFacet } from "./contacts.js";
48
+ import { CONTACT_ALTERNATE_EMAILS_FIELD, CONTACT_FACETS_FIELD, CONTACT_SOURCE_LABELS, normalizeContactEmail, readContactFacet } from "./contacts.js";
49
+ import { isEmailStateStatus } from "./email-state.js";
50
+ import { NAME_TOKEN_MAX_PREFIX, nameSearchKey, SCOPED_SEARCH_JOIN, scopedSearchTokens } from "./name-search.js";
49
51
  import { MAX_SCOPE_HOSTS, ORG_SCOPE_TOKEN } from "./scope-tokens.js";
50
52
  // The fixed vocabularies and their guards live in a leaf module; every name
51
53
  // stays importable from here.
@@ -2173,5 +2175,390 @@ const CRM_VIEW_SOURCE_FIELD = CRM_CONTACT_VIEW_FIELDS.source;
2173
2175
  ...new Set(value.map((id)=>String(id != null ? id : '').trim()).filter((id)=>id.length > 0 && id.length <= 200))
2174
2176
  ].slice(0, CRM_MEDIA_IDS_MAX);
2175
2177
  }
2178
+ /*==========================================
2179
+ * WHAT A CRM LIST QUERIES BY (AGL-3321).
2180
+ *
2181
+ * Every filter and every search word on the Leads, Contacts, Companies,
2182
+ * Deals and Tasks lists is a predicate on the list's Firestore query, and a
2183
+ * query can only ask about a field a WRITER stored in the shape the query
2184
+ * asks for. Three kinds of field exist for that and for nothing else:
2185
+ *
2186
+ * `searchTokens` word prefixes of what the list's search box reads
2187
+ * (a lead's name, address, company, title and tags;
2188
+ * a deal's title), for `array-contains` on one typed
2189
+ * word — see `nameSearchTokens`.
2190
+ * `scopedSearchTokens` the same tokens behind each of the record's
2191
+ * `visibleTo` tokens (`host:abc~acme`). A list under
2192
+ * a site already spends its one array clause on
2193
+ * `visibleTo array-contains-any`, so its search asks
2194
+ * `array-contains-any` over the site's tokens joined
2195
+ * to the word — one clause answering both.
2196
+ * keys a normalized scalar where the stored value cannot
2197
+ * be asked as it is: a lead's status (absent on a
2198
+ * capture), its lead source (compared the way the
2199
+ * picklist compares labels), a verdict on the
2200
+ * address (`none` when there is none) and a contact's
2201
+ * per-holder facet values (`facetKeys`).
2202
+ *
2203
+ * All of them are DERIVED: a pure function of the document, computed here
2204
+ * and nowhere else, and written by every path that writes one of its inputs
2205
+ * — see `CRM_LIST_FIELD_INPUTS` — so a record a list cannot find is a writer
2206
+ * that forgot, never a second formula that disagrees. A server writer that
2207
+ * holds the whole document spreads `crmListFields`; one that patched a field
2208
+ * restamps the record from what was stored (`restampCrmListFields` in the
2209
+ * admin library); the one-time backfill (`backfill-crm-list-fields.mjs`)
2210
+ * restates them for a plain Node script and is held to the same worked
2211
+ * examples (`tools/scripts/lib/org-record-list-fields.fixtures.json`), which this
2212
+ * library's spec asserts too.
2213
+ *=========================================*/ /** The search box's word-prefix tokens, on every CRM record a list searches. */ export const CRM_SEARCH_TOKENS_FIELD = 'searchTokens';
2214
+ /** The same tokens behind each `visibleTo` token — see the block header. */ export const CRM_SCOPED_SEARCH_TOKENS_FIELD = 'scopedSearchTokens';
2215
+ /** What joins a scope token to a search token — the platform's one join (`SCOPED_SEARCH_JOIN`). */ export const CRM_SCOPED_SEARCH_JOIN = SCOPED_SEARCH_JOIN;
2216
+ /**
2217
+ * The most search tokens one record keeps. A name, an address, a company
2218
+ * and a title are a few dozen prefixes; the cap is what keeps a record with
2219
+ * twenty long tags from spending hundreds of index entries on them.
2220
+ */ export const CRM_SEARCH_TOKENS_MAX = 200;
2221
+ const SEARCH_KEY = nameSearchKey;
2222
+ /**
2223
+ * The word-prefix tokens of several values, merged: every prefix (up to
2224
+ * twelve characters) of every whitespace-separated word, lower-cased —
2225
+ * exactly `nameSearchTokens` per value — plus, for an email address, the
2226
+ * words it is made of, so `acme` finds `dana@acme.com` and `dana` does too.
2227
+ */ export function crmSearchTokens(values) {
2228
+ const tokens = new Set();
2229
+ const addWord = (word)=>{
2230
+ const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX);
2231
+ for(let end = 1; end <= capped.length; end += 1){
2232
+ if (tokens.size >= CRM_SEARCH_TOKENS_MAX) return;
2233
+ tokens.add(capped.slice(0, end));
2234
+ }
2235
+ };
2236
+ const addText = (text)=>{
2237
+ const key = SEARCH_KEY(text);
2238
+ if (!key) return;
2239
+ for (const word of key.split(' ')){
2240
+ if (!word) continue;
2241
+ addWord(word);
2242
+ // An address, a domain or a hyphenated name is also its parts.
2243
+ if (/[@.+_-]/.test(word)) {
2244
+ for (const part of word.split(/[@.+_-]+/))if (part) addWord(part);
2245
+ }
2246
+ }
2247
+ };
2248
+ for (const value of values){
2249
+ if (Array.isArray(value)) {
2250
+ for (const entry of value)if (typeof entry === 'string') addText(entry);
2251
+ } else if (typeof value === 'string') {
2252
+ addText(value);
2253
+ }
2254
+ }
2255
+ return [
2256
+ ...tokens
2257
+ ];
2258
+ }
2259
+ /**
2260
+ * The two search fields for a record whose search reads `values`: the
2261
+ * tokens, and the same tokens behind each of its scope tokens through the
2262
+ * platform's one scoped writer (`scopedSearchTokens`).
2263
+ */ export function crmSearchFields(visibleTo, values) {
2264
+ const searchTokens = crmSearchTokens(values);
2265
+ return {
2266
+ searchTokens,
2267
+ scopedSearchTokens: scopedSearchTokens(visibleTo, searchTokens)
2268
+ };
2269
+ }
2270
+ /**
2271
+ * The address verdict as a list asks it (AGL-3245): the state's status, or
2272
+ * `none` when nothing is known — stored rather than left absent, because a
2273
+ * query cannot find a field's absence and "Nothing known" is a filter.
2274
+ */ export const CRM_EMAIL_STATUS_FIELD = 'emailStatus';
2275
+ export const CRM_EMAIL_STATUS_NONE = 'none';
2276
+ export function crmEmailStatusKey(record) {
2277
+ const raw = record == null ? void 0 : record['emailState'];
2278
+ const status = raw && typeof raw === 'object' ? raw['status'] : undefined;
2279
+ // A status outside the vocabulary reads as nothing known, as `readEmailState` reads it.
2280
+ return isEmailStateStatus(status) ? status : CRM_EMAIL_STATUS_NONE;
2281
+ }
2282
+ /** A lead's lead source as the list compares it: the picklist's key, `null` for none. */ export const CRM_LEAD_SOURCE_KEY_FIELD = 'leadSourceKey';
2283
+ export function crmLeadSourceKey(value) {
2284
+ const key = SEARCH_KEY(normalizeCrmPicklistLabel(value));
2285
+ return key || null;
2286
+ }
2287
+ /** What a lead's search box reads (AGL-3246). */ export const CRM_LEAD_SEARCH_SOURCES = [
2288
+ 'name',
2289
+ 'email',
2290
+ 'company',
2291
+ 'jobTitle',
2292
+ 'tags'
2293
+ ];
2294
+ /**
2295
+ * A lead's campaigns behind each of its `visibleTo` tokens
2296
+ * (`host:abc~{campaignId}`), for the Leads list's Campaign filter under a
2297
+ * site: there the scope clause is the query's one array clause, and a
2298
+ * campaign asked of these keys answers both at once — the way the search
2299
+ * folds into the scope through `scopedSearchTokens`.
2300
+ */ export const CRM_LEAD_SCOPED_CAMPAIGNS_FIELD = 'scopedCampaignIds';
2301
+ function leadCampaignIds(lead) {
2302
+ const raw = lead['campaignIds'];
2303
+ return Array.isArray(raw) ? raw.filter((entry)=>typeof entry === 'string' && entry.length > 0) : [];
2304
+ }
2305
+ /**
2306
+ * Every field the Leads list queries by, from the lead as stored (or as it
2307
+ * will be once written): the search tokens, its campaigns behind its scope,
2308
+ * the status — `new` for a lead nobody has touched, written so `status in
2309
+ * [new, working]` finds it — the lead source key and the address verdict.
2310
+ */ export function crmLeadListFields(record) {
2311
+ const lead = record;
2312
+ return _extends({}, crmSearchFields(lead['visibleTo'], CRM_LEAD_SEARCH_SOURCES.map((field)=>lead[field])), {
2313
+ scopedCampaignIds: scopedSearchTokens(lead['visibleTo'], leadCampaignIds(lead)),
2314
+ status: crmLeadStatus(lead),
2315
+ leadSourceKey: crmLeadSourceKey(lead['leadSource']),
2316
+ emailStatus: crmEmailStatusKey(lead)
2317
+ });
2318
+ }
2319
+ /** What a company's search box reads: its name and its domain. */ export const CRM_COMPANY_SEARCH_SOURCES = [
2320
+ 'name',
2321
+ 'domain'
2322
+ ];
2323
+ export function crmCompanyListFields(record) {
2324
+ const company = record;
2325
+ return crmSearchFields(company['visibleTo'], CRM_COMPANY_SEARCH_SOURCES.map((field)=>company[field]));
2326
+ }
2327
+ /**
2328
+ * What a deal's search box reads: its title (AGL-3315) — as word prefixes,
2329
+ * and as `titleLower`, the whole title's key, which a reader whose access
2330
+ * is some sites searches by its start.
2331
+ */ export function crmDealListFields(record) {
2332
+ const deal = record;
2333
+ return _extends({}, crmSearchFields(deal['visibleTo'], [
2334
+ deal['title']
2335
+ ]), {
2336
+ titleLower: SEARCH_KEY(typeof deal['title'] === 'string' ? deal['title'] : '')
2337
+ });
2338
+ }
2339
+ /** What a task's search box reads: its title. */ export function crmTaskListFields(record) {
2340
+ const task = record;
2341
+ return crmSearchFields(task['visibleTo'], [
2342
+ task['title']
2343
+ ]);
2344
+ }
2345
+ /*------------------------------------------
2346
+ * A CONTACT'S FACET KEYS.
2347
+ *
2348
+ * An owner, a stage, a source, a company, a tag and a custom value live on
2349
+ * a holder's facet — `facets.{groupId}` — so a `where` on one would be a path
2350
+ * per holder and an index per holder. `facetKeys` flattens them into one
2351
+ * array a query can ask `array-contains` of:
2352
+ *
2353
+ * `{groupId}:{field}={value}` this holder's value
2354
+ * `{groupId}:{field}` this holder has one (for "is set")
2355
+ * `*:{field}={value}`, `*:{field}` any holder's, for the organization level
2356
+ *
2357
+ * `field` is `owner`, `stage`, `source`, `company`, `tag` or `custom.{key}`,
2358
+ * plus two presence-only fields: `orders` for a holder the person has bought
2359
+ * from, and `ltv` for one they are worth something to — the per-holder
2360
+ * figures a range cannot reach, asked only whether there are any.
2361
+ * A text value is keyed lower-cased and single-spaced; a number or a flag
2362
+ * as its string. Under a site the viewing group's keys are asked; at the
2363
+ * organization level, where no group is viewing, the `*` keys are.
2364
+ *-----------------------------------------*/ export const CRM_CONTACT_FACET_KEYS_FIELD = 'facetKeys';
2365
+ /** The group a key names at the organization level: any holder. */ export const CRM_FACET_KEY_ANY_GROUP = '*';
2366
+ /** The most facet keys one contact keeps. */ export const CRM_FACET_KEYS_MAX = 600;
2367
+ /** A facet value as its key spells it, or `null` for a value a key cannot hold. */ export function crmFacetKeyValue(value) {
2368
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
2369
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
2370
+ if (typeof value !== 'string') return null;
2371
+ const key = SEARCH_KEY(value).slice(0, 120);
2372
+ return key || null;
2373
+ }
2374
+ /** One facet key: `{group}:{field}` for presence, `{group}:{field}={value}` for a value. */ export function crmFacetKey(group, field, value) {
2375
+ return value === undefined ? `${group}:${field}` : `${group}:${field}=${value}`;
2376
+ }
2377
+ /** The keys one holder's facet contributes, under `group`. */ function facetKeysOf(group, facet, into) {
2378
+ const add = (field, raw)=>{
2379
+ const value = crmFacetKeyValue(raw);
2380
+ if (value === null) return;
2381
+ into.add(crmFacetKey(group, field));
2382
+ into.add(crmFacetKey(group, field, value));
2383
+ };
2384
+ add('owner', facet['ownerUid']);
2385
+ if (isContactLifecycleStage(facet['lifecycleStage'])) add('stage', facet['lifecycleStage']);
2386
+ add('company', facet['companyId']);
2387
+ const sources = facet['sources'];
2388
+ if (sources && typeof sources === 'object' && !Array.isArray(sources)) {
2389
+ for (const [source, on] of Object.entries(sources)){
2390
+ if (on) add('source', source);
2391
+ }
2392
+ }
2393
+ if (Array.isArray(facet['tags'])) {
2394
+ for (const tag of facet['tags'])add('tag', tag);
2395
+ }
2396
+ // Presence only: a holder's commercial figures are asked "any?", never ranged.
2397
+ if (typeof facet['ordersCount'] === 'number' && facet['ordersCount'] > 0) {
2398
+ into.add(crmFacetKey(group, 'orders'));
2399
+ }
2400
+ if (typeof facet['ltvCents'] === 'number' && facet['ltvCents'] > 0) {
2401
+ into.add(crmFacetKey(group, 'ltv'));
2402
+ }
2403
+ const custom = facet['custom'];
2404
+ if (custom && typeof custom === 'object' && !Array.isArray(custom)) {
2405
+ for (const [key, value] of Object.entries(custom)){
2406
+ if (/^[A-Za-z0-9_-]{1,64}$/.test(key)) add(`custom.${key}`, value);
2407
+ }
2408
+ }
2409
+ }
2410
+ /** Every facet key a contact carries — see the block above. */ export function crmContactFacetKeys(record) {
2411
+ const facets = record[CONTACT_FACETS_FIELD];
2412
+ if (!facets || typeof facets !== 'object' || Array.isArray(facets)) return [];
2413
+ const groups = new Set();
2414
+ const any = new Set();
2415
+ for (const [groupId, facet] of Object.entries(facets)){
2416
+ if (!groupId || !facet || typeof facet !== 'object' || Array.isArray(facet)) continue;
2417
+ facetKeysOf(groupId, facet, groups);
2418
+ facetKeysOf(CRM_FACET_KEY_ANY_GROUP, facet, any);
2419
+ }
2420
+ return [
2421
+ ...any,
2422
+ ...groups
2423
+ ].slice(0, CRM_FACET_KEYS_MAX);
2424
+ }
2425
+ /**
2426
+ * What a contact's search box reads: the shared identity — the canonical
2427
+ * name and every address — and the two profile values a person is looked
2428
+ * up by, through their top-level search echoes (`HostContact.phone`,
2429
+ * `HostContact.companyName`): the company's words, and the phone's digits.
2430
+ */ export const CRM_CONTACT_SEARCH_SOURCES = [
2431
+ 'name',
2432
+ 'email',
2433
+ CONTACT_ALTERNATE_EMAILS_FIELD,
2434
+ 'companyName'
2435
+ ];
2436
+ /**
2437
+ * A phone number as the words a search box finds it by: its digits whole,
2438
+ * without a leading country code of one to three digits, and its last seven
2439
+ * and last four — so `5551234`, `555123` and `4567` all find +1 555 123 4567.
2440
+ * Typed as digits: a search reads one word, and `(555)` is not one of these.
2441
+ */ export function crmPhoneSearchWords(phone) {
2442
+ const digits = typeof phone === 'string' ? phone.replace(/\D/g, '') : '';
2443
+ if (digits.length < 4) return [];
2444
+ const words = new Set([
2445
+ digits
2446
+ ]);
2447
+ for(let strip = 1; strip <= 3; strip += 1){
2448
+ if (digits.length - strip >= 7) words.add(digits.slice(strip));
2449
+ }
2450
+ if (digits.length > 7) words.add(digits.slice(-7));
2451
+ words.add(digits.slice(-4));
2452
+ return [
2453
+ ...words
2454
+ ];
2455
+ }
2456
+ export function crmContactListFields(record) {
2457
+ const contact = record;
2458
+ return _extends({}, crmSearchFields(contact['visibleTo'], [
2459
+ ...CRM_CONTACT_SEARCH_SOURCES.map((field)=>contact[field]),
2460
+ crmPhoneSearchWords(contact['phone'])
2461
+ ]), {
2462
+ facetKeys: crmContactFacetKeys(contact),
2463
+ emailStatus: crmEmailStatusKey(contact)
2464
+ });
2465
+ }
2466
+ /**
2467
+ * The stored fields each collection's list fields are computed from. A
2468
+ * write that sets one of them must restamp the record — or spread
2469
+ * {@link crmListFields} over the document it wrote.
2470
+ */ export const CRM_LIST_FIELD_INPUTS = {
2471
+ leads: [
2472
+ 'visibleTo',
2473
+ ...CRM_LEAD_SEARCH_SOURCES,
2474
+ 'status',
2475
+ 'leadSource',
2476
+ 'emailState',
2477
+ 'campaignIds'
2478
+ ],
2479
+ contacts: [
2480
+ 'visibleTo',
2481
+ ...CRM_CONTACT_SEARCH_SOURCES,
2482
+ 'phone',
2483
+ CONTACT_FACETS_FIELD,
2484
+ 'emailState'
2485
+ ],
2486
+ companies: [
2487
+ 'visibleTo',
2488
+ ...CRM_COMPANY_SEARCH_SOURCES
2489
+ ],
2490
+ deals: [
2491
+ 'visibleTo',
2492
+ 'title'
2493
+ ],
2494
+ crmTasks: [
2495
+ 'visibleTo',
2496
+ 'title'
2497
+ ]
2498
+ };
2499
+ /** Every list field of one record, from the record as stored. */ export function crmListFields(collection, record) {
2500
+ switch(collection){
2501
+ case 'leads':
2502
+ return crmLeadListFields(record);
2503
+ case 'contacts':
2504
+ return crmContactListFields(record);
2505
+ case 'companies':
2506
+ return crmCompanyListFields(record);
2507
+ case 'deals':
2508
+ return crmDealListFields(record);
2509
+ case 'crmTasks':
2510
+ return crmTaskListFields(record);
2511
+ }
2512
+ }
2513
+ const sameValue = (a, b)=>{
2514
+ if (Array.isArray(a) && Array.isArray(b)) {
2515
+ return a.length === b.length && a.every((entry, at)=>entry === b[at]);
2516
+ }
2517
+ return a === b;
2518
+ };
2519
+ /**
2520
+ * The list fields a stored record carries WRONGLY — the patch that brings
2521
+ * it level, `{}` when it already is. What a restamp and a backfill write.
2522
+ */ export function crmListFieldsPatch(collection, record) {
2523
+ const fields = crmListFields(collection, record);
2524
+ const stored = record;
2525
+ const patch = {};
2526
+ for (const [field, value] of Object.entries(fields)){
2527
+ if (!sameValue(stored[field], value)) patch[field] = value;
2528
+ }
2529
+ return patch;
2530
+ }
2531
+ /**
2532
+ * The field each collection's list ORDERS or asks `null` of, which a record
2533
+ * must therefore carry even when it has no value: a contact, a company and a
2534
+ * deal carry `nextTaskAtMs` ("No next activity" asks `== null`), and a task
2535
+ * carries `dueAtMs` (every task view orders by it, and Firestore leaves a
2536
+ * document without the ordered field out of the answer). An absent field is
2537
+ * neither `null` nor anything else a query can ask for.
2538
+ */ export const CRM_LIST_NULLABLE_FIELD = {
2539
+ leads: null,
2540
+ contacts: 'nextTaskAtMs',
2541
+ companies: 'nextTaskAtMs',
2542
+ deals: 'nextTaskAtMs',
2543
+ crmTasks: 'dueAtMs'
2544
+ };
2545
+ /**
2546
+ * The list fields a NEW record is written with: {@link crmListFields}, and
2547
+ * the collection's {@link CRM_LIST_NULLABLE_FIELD} as `null` when the record
2548
+ * does not set it — nothing is scheduled against a record that did not
2549
+ * exist, and a task created undated is still one its views must reach.
2550
+ */ export function crmNewRecordListFields(collection, record) {
2551
+ const fields = crmListFields(collection, record);
2552
+ const nullable = CRM_LIST_NULLABLE_FIELD[collection];
2553
+ const stored = record[nullable != null ? nullable : ''];
2554
+ if (!nullable || nullable in record && stored !== undefined) return fields;
2555
+ return _extends({}, fields, {
2556
+ [nullable]: null
2557
+ });
2558
+ }
2559
+ /** Whether a patch touches a field the collection's list fields are computed from. */ export function crmListFieldsTouched(collection, patch) {
2560
+ const inputs = CRM_LIST_FIELD_INPUTS[collection];
2561
+ return Object.keys(patch).some((key)=>inputs.some((input)=>key === input || key.startsWith(`${input}.`)));
2562
+ }
2176
2563
 
2177
2564
  //# sourceMappingURL=crm.js.map