@aglyn/aglyn 1.0.0-beta.231 → 1.0.0-beta.233

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 (88) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +15 -2
  3. package/src/lib/app-utils/account-acquisition.js +20 -1
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/admin-audit-index.d.ts +3 -1
  6. package/src/lib/app-utils/admin-audit-index.js +9 -1
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  8. package/src/lib/app-utils/analytics-summary.d.ts +95 -0
  9. package/src/lib/app-utils/analytics-summary.js +113 -0
  10. package/src/lib/app-utils/analytics-summary.js.map +1 -0
  11. package/src/lib/app-utils/artifact-list-keys.js +12 -0
  12. package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
  13. package/src/lib/app-utils/artifact-list-queries.d.ts +51 -0
  14. package/src/lib/app-utils/artifact-list-queries.js +321 -0
  15. package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
  16. package/src/lib/app-utils/business-profile.d.ts +171 -0
  17. package/src/lib/app-utils/business-profile.js +277 -0
  18. package/src/lib/app-utils/business-profile.js.map +1 -0
  19. package/src/lib/app-utils/child-contract.d.ts +4 -1
  20. package/src/lib/app-utils/child-contract.js +5 -2
  21. package/src/lib/app-utils/child-contract.js.map +1 -1
  22. package/src/lib/app-utils/console-routes.d.ts +5 -0
  23. package/src/lib/app-utils/console-routes.js +1 -0
  24. package/src/lib/app-utils/console-routes.js.map +1 -1
  25. package/src/lib/app-utils/content-schema-type.d.ts +9 -0
  26. package/src/lib/app-utils/content-schema-type.js +4 -0
  27. package/src/lib/app-utils/content-schema-type.js.map +1 -1
  28. package/src/lib/app-utils/crm.d.ts +30 -2
  29. package/src/lib/app-utils/crm.js +77 -15
  30. package/src/lib/app-utils/crm.js.map +1 -1
  31. package/src/lib/app-utils/docs-help.generated.d.ts +58 -10
  32. package/src/lib/app-utils/docs-help.generated.js +118 -4
  33. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  34. package/src/lib/app-utils/docs-index.generated.js +320 -19
  35. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  36. package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
  37. package/src/lib/app-utils/entry-list-declaration.js +172 -0
  38. package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
  39. package/src/lib/app-utils/first-version-seed.d.ts +19 -0
  40. package/src/lib/app-utils/first-version-seed.js +53 -0
  41. package/src/lib/app-utils/first-version-seed.js.map +1 -0
  42. package/src/lib/app-utils/local-business.d.ts +8 -0
  43. package/src/lib/app-utils/local-business.js +4 -0
  44. package/src/lib/app-utils/local-business.js.map +1 -1
  45. package/src/lib/app-utils/lockdown.js +1 -1
  46. package/src/lib/app-utils/lockdown.js.map +1 -1
  47. package/src/lib/app-utils/message-search.js +5 -2
  48. package/src/lib/app-utils/message-search.js.map +1 -1
  49. package/src/lib/app-utils/name-search.js +15 -3
  50. package/src/lib/app-utils/name-search.js.map +1 -1
  51. package/src/lib/app-utils/page-markdown.js +3 -0
  52. package/src/lib/app-utils/page-markdown.js.map +1 -1
  53. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
  54. package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
  55. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
  56. package/src/lib/app-utils/screen-kind.d.ts +25 -0
  57. package/src/lib/app-utils/screen-kind.js +25 -0
  58. package/src/lib/app-utils/screen-kind.js.map +1 -0
  59. package/src/lib/app-utils/screen-route.d.ts +1 -5
  60. package/src/lib/app-utils/screen-route.js +2 -4
  61. package/src/lib/app-utils/screen-route.js.map +1 -1
  62. package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
  63. package/src/lib/app-utils/site-journey-steps.js +54 -0
  64. package/src/lib/app-utils/site-journey-steps.js.map +1 -0
  65. package/src/lib/app-utils/site-journey.d.ts +2 -22
  66. package/src/lib/app-utils/site-journey.js +2 -32
  67. package/src/lib/app-utils/site-journey.js.map +1 -1
  68. package/src/lib/plugin-manager/feature-plugins.d.ts +19 -0
  69. package/src/lib/plugin-manager/feature-plugins.js +8 -0
  70. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  71. package/src/lib/plugin-manager/first-party-plugins.generated.js +124 -2
  72. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
  74. package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
  75. package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
  76. package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
  77. package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
  78. package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
  79. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +5 -0
  80. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
  81. package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
  82. package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
  83. package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
  84. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
  85. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
  86. package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
  87. package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
  88. package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-keys.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { displayNameSearchFields } from './name-search'\n\n/*\n * THE KEYS A SITE ARTIFACT'S LISTS QUERY BY (AGL-3321).\n *\n * The screens, layouts, reusable components and templates of a site are\n * listed, searched and filtered by Firestore queries, so every key a list\n * asks about has to be WRITTEN — a query cannot find a document by a value it\n * derives, and cannot find one that lacks the field at all:\n *\n * nameLower / nameTokens / nameReversed\n * the name keys (`displayNameSearchFields`), on all four;\n * kind on a component, `site` or `email` — a component stored\n * before AGL-3287 carries none and is read as `site`, so the\n * \"Used in: Page\" filter needs the word stored;\n * on a template, `page`, `component` or `layout` — a template\n * stored with none is read as a page template;\n * source.type on a template, its provenance — one stored with no\n * `source`, or with the legacy `workspace`, was saved here,\n * which is `authored`;\n * libraryRow on a template, whether it is its own row of the templates\n * library: a multi-page STARTER is one row (AGL-696), led by\n * its first live page, so the other pages carry `false` — and\n * so does a deleted template (`artifactDeleteListKeys`), which\n * is how the library's query leaves tombstones out;\n *\n * deletedAt on a screen, `null` — the flag a campaign's screens list\n * asks for (`deletedAt == null`) to leave tombstones out. A\n * query cannot ask for a field to be absent, so a live\n * screen must STORE the null; a delete overwrites it with\n * the time (and the security rules let an editor do that\n * only while it is null, so a tombstone stays one);\n *\n * A DELETED screen gets none of them. The email templates list orders by\n * `nameLower`, and an email template's delete clears its name keys so the\n * tombstone leaves that order (a query cannot ask for `deletedAt` to be\n * absent) — so no create, restore or backfill may stamp them back.\n *\n * Every CREATE stamps them (`artifactCreateListKeys`) and every rename\n * restamps the name keys (`artifactRenameListKeys`).\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps the documents\n * written before them; a script cannot import this library, so it reads the\n * documents the way this module does, and both sides answer\n * `tools/scripts/lib/artifact-list-keys.fixtures.json` — this module's spec\n * and the backfill's `--self-test`.\n */\n\n/** The site artifact collections whose lists query these keys. */\nexport const LISTED_ARTIFACT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'templates',\n] as const\n\nexport type ListedArtifactCollection =\n (typeof LISTED_ARTIFACT_COLLECTIONS)[number]\n\n/** Whether a host subcollection is one whose lists query these keys. */\nexport const isListedArtifactCollection = (\n collection: string,\n): collection is ListedArtifactCollection =>\n (LISTED_ARTIFACT_COLLECTIONS as readonly string[]).includes(collection)\n\n/** The kinds a template's `kind` holds; absent reads as `page`. */\nconst TEMPLATE_KINDS = ['page', 'component', 'layout']\n\n/**\n * A provenance word no writer stores any more: a detached marketplace copy\n * was stamped `workspace` before it was stamped `authored` (AGL-3321).\n */\nconst LEGACY_AUTHORED_SOURCE = 'workspace'\n\nconst record = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n\n/**\n * The name a list finds an artifact by.\n *\n * Its `displayName` — except a page of a multi-page STARTER, which the\n * templates library lists as ONE row under the starter's name\n * (`source.starterName`), so every page of it carries that name's keys and a\n * search for the starter finds the row whichever page leads it.\n */\nexport function artifactSearchName(\n collection: string,\n doc: Record<string, unknown>,\n): unknown {\n if (collection === 'templates') {\n const starterName = record(doc['source'])['starterName']\n if (typeof starterName === 'string' && starterName.trim()) return starterName\n }\n return doc['displayName']\n}\n\n/**\n * The list keys a CREATE of `doc` into `collection` stamps: the name keys,\n * and the stored default of a kind a reader otherwise infers. Nothing for a\n * collection no artifact list queries.\n *\n * Spread AFTER the document's own fields: the keys are derived from them.\n */\nexport function artifactCreateListKeys(\n collection: string,\n doc: Record<string, unknown>,\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n if (collection === 'screens' && doc['deletedAt'] != null) return {}\n const keys: Record<string, unknown> = {\n ...displayNameSearchFields(artifactSearchName(collection, doc)),\n }\n // Stored, not omitted: `deletedAt == null` matches only a document that\n // holds the field (AGL-3321, a campaign's screens).\n if (collection === 'screens') keys['deletedAt'] = null\n if (collection === 'components' && doc['kind'] !== 'email') keys['kind'] = 'site'\n if (collection === 'templates') {\n if (!TEMPLATE_KINDS.includes(String(doc['kind']))) keys['kind'] = 'page'\n // Provenance is server-managed (AGL-666), and a writer that states it is\n // never overridden here. None at all, or the legacy word a detached copy\n // carried, was saved here, which is `authored`.\n const sourceType = record(doc['source'])['type']\n if (typeof sourceType !== 'string' || sourceType === LEGACY_AUTHORED_SOURCE) {\n keys['source'] = { ...record(doc['source']), type: 'authored' }\n }\n keys['libraryRow'] = doc['deletedAt'] == null && leadsItsStarter(doc)\n }\n return keys\n}\n\n/**\n * Whether a template leads its starter's library row when the starter is\n * written whole: every template that is not a starter page does, and of a\n * starter's pages the first (`starterOrder` 0). A later delete moves the lead\n * to the next live page — see `artifactDeleteListKeys`.\n */\nfunction leadsItsStarter(doc: Record<string, unknown>): boolean {\n const source = record(doc['source'])\n const starterId = source['starterId']\n if (typeof starterId !== 'string' || !starterId) return true\n return Number(source['starterOrder'] ?? 0) === 0\n}\n\n/**\n * The list keys a SOFT DELETE writes beside `deletedAt`: a deleted template\n * is no longer a library row. A caller deleting the page that LEADS a\n * starter's row gives the lead to the next live page (`libraryRow: true`),\n * or the starter disappears from the library with pages still in it.\n */\nexport function artifactDeleteListKeys(\n collection: string,\n): Record<string, unknown> {\n return collection === 'templates' ? { libraryRow: false } : {}\n}\n\n/**\n * The name keys a RENAME writes beside the new `displayName`. `doc` is the\n * document as read, for the one case whose search name is not its display\n * name (a starter page — see `artifactSearchName`).\n */\nexport function artifactRenameListKeys(\n collection: string,\n displayName: unknown,\n doc: Record<string, unknown> = {},\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n return displayNameSearchFields(\n artifactSearchName(collection, { ...doc, displayName }),\n )\n}\n"],"names":["displayNameSearchFields","LISTED_ARTIFACT_COLLECTIONS","isListedArtifactCollection","collection","includes","TEMPLATE_KINDS","LEGACY_AUTHORED_SOURCE","record","value","artifactSearchName","doc","starterName","trim","artifactCreateListKeys","keys","String","sourceType","type","leadsItsStarter","source","starterId","Number","artifactDeleteListKeys","libraryRow","artifactRenameListKeys","displayName"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,QAAQ,mBAAe;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GAED,gEAAgE,GAChE,OAAO,MAAMC,8BAA8B;IACzC;IACA;IACA;IACA;CACD,CAAS;AAKV,sEAAsE,GACtE,OAAO,MAAMC,6BAA6B,CACxCC,aAEA,AAACF,4BAAkDG,QAAQ,CAACD,YAAW;AAEzE,iEAAiE,GACjE,MAAME,iBAAiB;IAAC;IAAQ;IAAa;CAAS;AAEtD;;;CAGC,GACD,MAAMC,yBAAyB;AAE/B,MAAMC,SAAS,CAACC,QACdA,SAAS,OAAOA,UAAU,WAAYA,QAAoC,CAAC;AAE7E;;;;;;;CAOC,GACD,OAAO,SAASC,mBACdN,UAAkB,EAClBO,GAA4B;IAE5B,IAAIP,eAAe,aAAa;QAC9B,MAAMQ,cAAcJ,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,cAAc;QACxD,IAAI,OAAOC,gBAAgB,YAAYA,YAAYC,IAAI,IAAI,OAAOD;IACpE;IACA,OAAOD,GAAG,CAAC,cAAc;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,uBACdV,UAAkB,EAClBO,GAA4B;IAE5B,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,IAAIA,eAAe,aAAaO,GAAG,CAAC,YAAY,IAAI,MAAM,OAAO,CAAC;IAClE,MAAMI,OAAgC,aACjCd,wBAAwBS,mBAAmBN,YAAYO;IAE5D,wEAAwE;IACxE,oDAAoD;IACpD,IAAIP,eAAe,WAAWW,IAAI,CAAC,YAAY,GAAG;IAClD,IAAIX,eAAe,gBAAgBO,GAAG,CAAC,OAAO,KAAK,SAASI,IAAI,CAAC,OAAO,GAAG;IAC3E,IAAIX,eAAe,aAAa;QAC9B,IAAI,CAACE,eAAeD,QAAQ,CAACW,OAAOL,GAAG,CAAC,OAAO,IAAII,IAAI,CAAC,OAAO,GAAG;QAClE,yEAAyE;QACzE,yEAAyE;QACzE,gDAAgD;QAChD,MAAME,aAAaT,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO;QAChD,IAAI,OAAOM,eAAe,YAAYA,eAAeV,wBAAwB;YAC3EQ,IAAI,CAAC,SAAS,GAAG,aAAKP,OAAOG,GAAG,CAAC,SAAS;gBAAGO,MAAM;;QACrD;QACAH,IAAI,CAAC,aAAa,GAAGJ,GAAG,CAAC,YAAY,IAAI,QAAQQ,gBAAgBR;IACnE;IACA,OAAOI;AACT;AAEA;;;;;CAKC,GACD,SAASI,gBAAgBR,GAA4B;QAIrCS;IAHd,MAAMA,SAASZ,OAAOG,GAAG,CAAC,SAAS;IACnC,MAAMU,YAAYD,MAAM,CAAC,YAAY;IACrC,IAAI,OAAOC,cAAc,YAAY,CAACA,WAAW,OAAO;IACxD,OAAOC,QAAOF,uBAAAA,MAAM,CAAC,eAAe,YAAtBA,uBAA0B,OAAO;AACjD;AAEA;;;;;CAKC,GACD,OAAO,SAASG,uBACdnB,UAAkB;IAElB,OAAOA,eAAe,cAAc;QAAEoB,YAAY;IAAM,IAAI,CAAC;AAC/D;AAEA;;;;CAIC,GACD,OAAO,SAASC,uBACdrB,UAAkB,EAClBsB,WAAoB,EACpBf,MAA+B,CAAC,CAAC;IAEjC,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,OAAOH,wBACLS,mBAAmBN,YAAY,aAAKO;QAAKe;;AAE7C"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-keys.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { displayNameSearchFields } from './name-search'\n\n/*\n * THE KEYS A SITE ARTIFACT'S LISTS QUERY BY (AGL-3321).\n *\n * The screens, layouts, reusable components and templates of a site are\n * listed, searched and filtered by Firestore queries, so every key a list\n * asks about has to be WRITTEN — a query cannot find a document by a value it\n * derives, and cannot find one that lacks the field at all:\n *\n * nameLower / nameTokens / nameReversed\n * the name keys (`displayNameSearchFields`), on all four;\n * kind on a component, `site` or `email` — a component stored\n * before AGL-3287 carries none and is read as `site`, so the\n * \"Used in: Page\" filter needs the word stored;\n * on a template, `page`, `component` or `layout` — a template\n * stored with none is read as a page template;\n * source.type on a template, its provenance — one stored with no\n * `source`, or with the legacy `workspace`, was saved here,\n * which is `authored`;\n * libraryRow on a template, whether it is its own row of the templates\n * library: a multi-page STARTER is one row (AGL-696), led by\n * its first live page, so the other pages carry `false` — and\n * so does a deleted template (`artifactDeleteListKeys`), which\n * is how the library's query leaves tombstones out;\n *\n * description on a layout or component that has none, `null` — the\n * lists sort by it (AGL-3680), and an `orderBy` drops every\n * document that lacks the field. A writer that passes a\n * partial document here must pass its `description` too, or\n * the null would overwrite the one it writes;\n * deletedAt on a screen, `null` — the flag a campaign's screens list\n * asks for (`deletedAt == null`) to leave tombstones out. A\n * query cannot ask for a field to be absent, so a live\n * screen must STORE the null; a delete overwrites it with\n * the time (and the security rules let an editor do that\n * only while it is null, so a tombstone stays one);\n *\n * A DELETED screen gets none of them. The email templates list orders by\n * `nameLower`, and an email template's delete clears its name keys so the\n * tombstone leaves that order (a query cannot ask for `deletedAt` to be\n * absent) — so no create, restore or backfill may stamp them back.\n *\n * Every CREATE stamps them (`artifactCreateListKeys`) and every rename\n * restamps the name keys (`artifactRenameListKeys`).\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps the documents\n * written before them; a script cannot import this library, so it reads the\n * documents the way this module does, and both sides answer\n * `tools/scripts/lib/artifact-list-keys.fixtures.json` — this module's spec\n * and the backfill's `--self-test`.\n */\n\n/** The site artifact collections whose lists query these keys. */\nexport const LISTED_ARTIFACT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'templates',\n] as const\n\nexport type ListedArtifactCollection =\n (typeof LISTED_ARTIFACT_COLLECTIONS)[number]\n\n/** Whether a host subcollection is one whose lists query these keys. */\nexport const isListedArtifactCollection = (\n collection: string,\n): collection is ListedArtifactCollection =>\n (LISTED_ARTIFACT_COLLECTIONS as readonly string[]).includes(collection)\n\n/** The kinds a template's `kind` holds; absent reads as `page`. */\nconst TEMPLATE_KINDS = ['page', 'component', 'layout']\n\n/**\n * A provenance word no writer stores any more: a detached marketplace copy\n * was stamped `workspace` before it was stamped `authored` (AGL-3321).\n */\nconst LEGACY_AUTHORED_SOURCE = 'workspace'\n\nconst record = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n\n/**\n * The name a list finds an artifact by.\n *\n * Its `displayName` — except a page of a multi-page STARTER, which the\n * templates library lists as ONE row under the starter's name\n * (`source.starterName`), so every page of it carries that name's keys and a\n * search for the starter finds the row whichever page leads it.\n */\nexport function artifactSearchName(\n collection: string,\n doc: Record<string, unknown>,\n): unknown {\n if (collection === 'templates') {\n const starterName = record(doc['source'])['starterName']\n if (typeof starterName === 'string' && starterName.trim()) return starterName\n }\n return doc['displayName']\n}\n\n/**\n * The list keys a CREATE of `doc` into `collection` stamps: the name keys,\n * and the stored default of a kind a reader otherwise infers. Nothing for a\n * collection no artifact list queries.\n *\n * Spread AFTER the document's own fields: the keys are derived from them.\n */\nexport function artifactCreateListKeys(\n collection: string,\n doc: Record<string, unknown>,\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n if (collection === 'screens' && doc['deletedAt'] != null) return {}\n const keys: Record<string, unknown> = {\n ...displayNameSearchFields(artifactSearchName(collection, doc)),\n }\n // Stored, not omitted: `deletedAt == null` matches only a document that\n // holds the field (AGL-3321, a campaign's screens).\n if (collection === 'screens') keys['deletedAt'] = null\n // A live layout too: its list asks for `deletedAt == null` rather than\n // dropping tombstones from the page they fall in (AGL-3680).\n if (collection === 'layouts' && doc['deletedAt'] == null) keys['deletedAt'] = null\n if (collection === 'components' && doc['kind'] !== 'email') keys['kind'] = 'site'\n // Stored, not omitted: the Description header orders by it (AGL-3680).\n if ((collection === 'layouts' || collection === 'components') && doc['description'] == null) {\n keys['description'] = null\n }\n if (collection === 'templates') {\n if (!TEMPLATE_KINDS.includes(String(doc['kind']))) keys['kind'] = 'page'\n // Provenance is server-managed (AGL-666), and a writer that states it is\n // never overridden here. None at all, or the legacy word a detached copy\n // carried, was saved here, which is `authored`.\n const sourceType = record(doc['source'])['type']\n if (typeof sourceType !== 'string' || sourceType === LEGACY_AUTHORED_SOURCE) {\n keys['source'] = { ...record(doc['source']), type: 'authored' }\n }\n keys['libraryRow'] = doc['deletedAt'] == null && leadsItsStarter(doc)\n }\n return keys\n}\n\n/**\n * Whether a template leads its starter's library row when the starter is\n * written whole: every template that is not a starter page does, and of a\n * starter's pages the first (`starterOrder` 0). A later delete moves the lead\n * to the next live page — see `artifactDeleteListKeys`.\n */\nfunction leadsItsStarter(doc: Record<string, unknown>): boolean {\n const source = record(doc['source'])\n const starterId = source['starterId']\n if (typeof starterId !== 'string' || !starterId) return true\n return Number(source['starterOrder'] ?? 0) === 0\n}\n\n/**\n * The list keys a SOFT DELETE writes beside `deletedAt`: a deleted template\n * is no longer a library row. A caller deleting the page that LEADS a\n * starter's row gives the lead to the next live page (`libraryRow: true`),\n * or the starter disappears from the library with pages still in it.\n */\nexport function artifactDeleteListKeys(\n collection: string,\n): Record<string, unknown> {\n return collection === 'templates' ? { libraryRow: false } : {}\n}\n\n/**\n * The name keys a RENAME writes beside the new `displayName`. `doc` is the\n * document as read, for the one case whose search name is not its display\n * name (a starter page — see `artifactSearchName`).\n */\nexport function artifactRenameListKeys(\n collection: string,\n displayName: unknown,\n doc: Record<string, unknown> = {},\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n return displayNameSearchFields(\n artifactSearchName(collection, { ...doc, displayName }),\n )\n}\n"],"names":["displayNameSearchFields","LISTED_ARTIFACT_COLLECTIONS","isListedArtifactCollection","collection","includes","TEMPLATE_KINDS","LEGACY_AUTHORED_SOURCE","record","value","artifactSearchName","doc","starterName","trim","artifactCreateListKeys","keys","String","sourceType","type","leadsItsStarter","source","starterId","Number","artifactDeleteListKeys","libraryRow","artifactRenameListKeys","displayName"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
18
+ import { type ListQueryDeclaration, type ListQueryFilter, type ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
19
+ /** The walk every artifact list keeps: the document name, ascending. */
20
+ export declare const ARTIFACT_LIST_ORDER: ListQuerySort;
21
+ /** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */
22
+ export declare const ARTIFACT_NAME_SEARCH: {
23
+ tokensPath: string;
24
+ };
25
+ /** The layouts page's header sorts; the ID ascending one is the default. */
26
+ export declare const LAYOUT_LIST_SORTS: readonly ListQuerySort[];
27
+ /**
28
+ * The layouts list's scope: live layouts. Every create stores `deletedAt: null`
29
+ * (`artifactCreateListKeys`) and a delete stamps the time, so the query leaves
30
+ * tombstones out instead of the page dropping them after the read (AGL-3680).
31
+ */
32
+ export declare const LAYOUT_LIST_BASE: readonly ListQueryFilter[];
33
+ /** `hosts/{hostId}/layouts`. */
34
+ export declare const LAYOUT_LIST_QUERY: ListQueryDeclaration;
35
+ export declare const LAYOUT_LIST_HEADERS: Readonly<Record<string, string>>;
36
+ /** The components card's header sorts; the ID ascending one is the default. */
37
+ export declare const COMPONENT_LIST_SORTS: readonly ListQuerySort[];
38
+ /** `hosts/{hostId}/components`. */
39
+ export declare const COMPONENT_LIST_QUERY: ListQueryDeclaration;
40
+ export declare const COMPONENT_LIST_HEADERS: Readonly<Record<string, string>>;
41
+ export declare const TEMPLATE_LIST_BASE: readonly ListQueryFilter[];
42
+ /**
43
+ * The templates library's header sorts. It shows no ID column, so the walk
44
+ * stays the unlabelled default; Description sorts the page (see "The header
45
+ * sorts").
46
+ */
47
+ export declare const TEMPLATE_LIST_SORTS: readonly ListQuerySort[];
48
+ export declare const TEMPLATE_LIST_QUERY: ListQueryDeclaration;
49
+ export declare const TEMPLATE_LIST_HEADERS: Readonly<Record<string, string>>;
50
+ /** The template kinds, by the stored `kind`. */
51
+ export declare const TEMPLATE_KIND_OPTIONS: readonly ListFilterOption[];
@@ -0,0 +1,321 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { LIST_QUERY_ID_PATH } from "@aglyn/shared-util-tools/list-query/list-query-plan";
17
+ /*
18
+ * WHAT THE SITE ARTIFACT LISTS ASK FIRESTORE (AGL-3321).
19
+ *
20
+ * The layouts page, the components card and the templates library put every
21
+ * clause their Filters panel accepts, and the quick-search word, on the query
22
+ * — composed by `planListQuery` — and page what it answers. Nothing is matched
23
+ * over the rows a page happened to read. The template gallery's shelves ask
24
+ * the same keys (`template-gallery-dialog.component.tsx`), and so does the
25
+ * email plugin's templates list, from its own declaration.
26
+ *
27
+ * ## The keys are WRITTEN
28
+ *
29
+ * A query finds a document by a stored value, so each list reads keys every
30
+ * writer stamps (`artifactCreateListKeys`/`artifactRenameListKeys` in
31
+ * `@aglyn/aglyn/app-utils/artifact-list-keys`) and
32
+ * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps on the documents
33
+ * written before them:
34
+ *
35
+ * nameTokens the name's word prefixes — the search box and `contains`;
36
+ * nameLower the normalized name — `equals`;
37
+ * kind a component's `site`/`email`, a template's page/component/
38
+ * layout, stored even where a reader used to infer it;
39
+ * source.type a template's provenance, `authored` where there was none;
40
+ * libraryRow a template that is its own library row (below).
41
+ *
42
+ * ## The order is the document NAME, and that is what keeps it cheap
43
+ *
44
+ * `hostArtifactQuery` explains why these lists walk the document id: it is
45
+ * the one order no writer can omit, so the walk is total. It is also the
46
+ * order index merging serves with Firestore's automatic single-field
47
+ * indexes: any mix of the equalities and the one array clause above, ordered
48
+ * by `__name__`, needs no composite at all. The composites below exist only
49
+ * for the one range each list offers — `Updated` — which must lead the order
50
+ * it ranges over, so it pairs with each equality once.
51
+ *
52
+ * ## What is not offered, and why
53
+ *
54
+ * - `startsWith`/`endsWith` on the name: a prefix range must lead the order,
55
+ * and these lists are not sorted by name.
56
+ * - `description` (a filter and a search target before): no token field,
57
+ * and a second token array could not share the query's one array clause
58
+ * with the search anyway.
59
+ * - `Created` ranges: a second range order would double the composites for
60
+ * a question `Updated` answers nearly as well; one range per list.
61
+ *
62
+ * ## The header sorts (AGL-3680)
63
+ *
64
+ * Every column sorts on the QUERY, by a stored value every writer stamps:
65
+ *
66
+ * Display name `nameLower` (above; a starter page's is its starter's
67
+ * name, which is the name its library row shows);
68
+ * ID the walk itself, either way;
69
+ * Updated, `updatedAt` / `createdAt` — stamped by every create, and by
70
+ * Created `backfill-artifacts-list-keys.mjs` on a document that
71
+ * predates either;
72
+ * Used in, Kind the stored `kind`; Source the stored `source.type`;
73
+ * Description a layout's or component's `description`, stored as `null`
74
+ * when it has none (`artifactCreateListKeys` + the backfill),
75
+ * because an `orderBy` DROPS a document that lacks the field.
76
+ * A template row's description is its starter's when it
77
+ * leads one, which is no stored field: it sorts the page.
78
+ *
79
+ * Each is `alone` — served only while no filter or search is on, falling
80
+ * back to the list's default header order with a notice — so on the layouts
81
+ * and components subcollections it costs no composite. Updated newest-first
82
+ * is not `alone`: it is the order
83
+ * the Updated range already imposes, so it pairs with every equality and
84
+ * its composites are the ones the range needed anyway. The templates
85
+ * library's `libraryRow` scope is an equality every order pairs with, so
86
+ * each of its orders has one `(libraryRow, order)` composite.
87
+ */ /** The walk every artifact list keeps: the document name, ascending. */ export const ARTIFACT_LIST_ORDER = {
88
+ path: LIST_QUERY_ID_PATH,
89
+ direction: 'asc'
90
+ };
91
+ /** The name, by word prefix (`contains`) or whole (`equals`). */ const NAME_FIELD = {
92
+ column: 'displayName',
93
+ kind: 'text',
94
+ path: 'displayName',
95
+ lowerPath: 'nameLower',
96
+ tokensPath: 'nameTokens',
97
+ operators: [
98
+ 'contains',
99
+ 'equals'
100
+ ]
101
+ };
102
+ /** The document id, which the walk already orders by. */ const ID_FIELD = {
103
+ column: '$id',
104
+ kind: 'id',
105
+ path: LIST_QUERY_ID_PATH
106
+ };
107
+ /** The one range: `updatedAt`, which every writer stamps. */ const UPDATED_FIELD = {
108
+ column: 'updatedAt',
109
+ kind: 'date',
110
+ path: 'updatedAt',
111
+ operators: [
112
+ 'is',
113
+ 'after',
114
+ 'onOrAfter',
115
+ 'before',
116
+ 'onOrBefore'
117
+ ]
118
+ };
119
+ /** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */ export const ARTIFACT_NAME_SEARCH = {
120
+ tokensPath: 'nameTokens'
121
+ };
122
+ /** One header's two orders, `alone` (see "The header sorts"). */ const headerSorts = (column, path, label)=>[
123
+ {
124
+ path,
125
+ direction: 'asc',
126
+ column,
127
+ label,
128
+ alone: true
129
+ },
130
+ {
131
+ path,
132
+ direction: 'desc',
133
+ column,
134
+ label,
135
+ alone: true
136
+ }
137
+ ];
138
+ /**
139
+ * The ID header: the walk ascending — the list's default, served under every
140
+ * filter — and descending, `alone`.
141
+ */ const ID_SORTS = [
142
+ {
143
+ path: LIST_QUERY_ID_PATH,
144
+ direction: 'asc',
145
+ column: '$id',
146
+ label: 'ID'
147
+ },
148
+ {
149
+ path: LIST_QUERY_ID_PATH,
150
+ direction: 'desc',
151
+ column: '$id',
152
+ label: 'ID',
153
+ alone: true
154
+ }
155
+ ];
156
+ /**
157
+ * Updated newest first FIRST: it is the order the Updated range imposes
158
+ * (`planListQuery` takes the first declared order on the range's field), so
159
+ * it is not `alone` — the range's composites already serve it.
160
+ */ const TIME_SORTS = [
161
+ {
162
+ path: 'updatedAt',
163
+ direction: 'desc',
164
+ column: 'updatedAt',
165
+ label: 'Updated'
166
+ },
167
+ {
168
+ path: 'updatedAt',
169
+ direction: 'asc',
170
+ column: 'updatedAt',
171
+ label: 'Updated',
172
+ alone: true
173
+ },
174
+ ...headerSorts('createdAt', 'createdAt', 'Created')
175
+ ];
176
+ const NAME_SORTS = headerSorts('displayName', 'nameLower', 'Display name');
177
+ const DESCRIPTION_SORTS = headerSorts('description', 'description', 'Description');
178
+ /** The layouts page's header sorts; the ID ascending one is the default. */ export const LAYOUT_LIST_SORTS = [
179
+ ...ID_SORTS,
180
+ ...NAME_SORTS,
181
+ ...DESCRIPTION_SORTS,
182
+ ...TIME_SORTS
183
+ ];
184
+ /**
185
+ * The layouts list's scope: live layouts. Every create stores `deletedAt: null`
186
+ * (`artifactCreateListKeys`) and a delete stamps the time, so the query leaves
187
+ * tombstones out instead of the page dropping them after the read (AGL-3680).
188
+ */ export const LAYOUT_LIST_BASE = [
189
+ {
190
+ path: 'deletedAt',
191
+ op: '==',
192
+ value: null
193
+ }
194
+ ];
195
+ /** `hosts/{hostId}/layouts`. */ export const LAYOUT_LIST_QUERY = {
196
+ fields: [
197
+ NAME_FIELD,
198
+ ID_FIELD,
199
+ UPDATED_FIELD
200
+ ],
201
+ sorts: [
202
+ ARTIFACT_LIST_ORDER,
203
+ ...LAYOUT_LIST_SORTS
204
+ ],
205
+ search: ARTIFACT_NAME_SEARCH
206
+ };
207
+ export const LAYOUT_LIST_HEADERS = {
208
+ displayName: 'Display name',
209
+ $id: 'ID',
210
+ updatedAt: 'Updated'
211
+ };
212
+ /** The components card's header sorts; the ID ascending one is the default. */ export const COMPONENT_LIST_SORTS = [
213
+ ...ID_SORTS,
214
+ ...NAME_SORTS,
215
+ ...headerSorts('kind', 'kind', 'Used in'),
216
+ ...DESCRIPTION_SORTS,
217
+ ...TIME_SORTS
218
+ ];
219
+ /** `hosts/{hostId}/components`. */ export const COMPONENT_LIST_QUERY = {
220
+ fields: [
221
+ NAME_FIELD,
222
+ {
223
+ column: 'kind',
224
+ kind: 'exact',
225
+ path: 'kind',
226
+ operators: [
227
+ 'equals',
228
+ 'isAnyOf'
229
+ ]
230
+ },
231
+ ID_FIELD,
232
+ UPDATED_FIELD
233
+ ],
234
+ sorts: [
235
+ ARTIFACT_LIST_ORDER,
236
+ ...COMPONENT_LIST_SORTS
237
+ ],
238
+ search: ARTIFACT_NAME_SEARCH
239
+ };
240
+ export const COMPONENT_LIST_HEADERS = {
241
+ displayName: 'Display name',
242
+ kind: 'Used in',
243
+ $id: 'ID',
244
+ updatedAt: 'Updated'
245
+ };
246
+ /*
247
+ * `hosts/{hostId}/templates`, as the library lists it: one row per template,
248
+ * except that a multi-page STARTER is one row (AGL-696) — its first live page
249
+ * carries `libraryRow: true` and the rest `false`, and a deleted template
250
+ * `false`, so the base clause below is both "one row per starter" and "no
251
+ * tombstones" without a single row being dropped after the read. Every page
252
+ * of a starter carries the starter's name keys, so a search for it finds the
253
+ * row whichever page leads it.
254
+ */ export const TEMPLATE_LIST_BASE = [
255
+ {
256
+ path: 'libraryRow',
257
+ op: '==',
258
+ value: true
259
+ }
260
+ ];
261
+ /**
262
+ * The templates library's header sorts. It shows no ID column, so the walk
263
+ * stays the unlabelled default; Description sorts the page (see "The header
264
+ * sorts").
265
+ */ export const TEMPLATE_LIST_SORTS = [
266
+ ...NAME_SORTS,
267
+ ...headerSorts('kind', 'kind', 'Kind'),
268
+ ...headerSorts('source', 'source.type', 'Source'),
269
+ ...TIME_SORTS
270
+ ];
271
+ export const TEMPLATE_LIST_QUERY = {
272
+ fields: [
273
+ NAME_FIELD,
274
+ {
275
+ column: 'kind',
276
+ kind: 'exact',
277
+ path: 'kind',
278
+ operators: [
279
+ 'equals',
280
+ 'isAnyOf'
281
+ ]
282
+ },
283
+ {
284
+ column: 'source',
285
+ kind: 'exact',
286
+ path: 'source.type',
287
+ operators: [
288
+ 'equals',
289
+ 'isAnyOf'
290
+ ]
291
+ },
292
+ UPDATED_FIELD
293
+ ],
294
+ sorts: [
295
+ ARTIFACT_LIST_ORDER,
296
+ ...TEMPLATE_LIST_SORTS
297
+ ],
298
+ search: ARTIFACT_NAME_SEARCH
299
+ };
300
+ export const TEMPLATE_LIST_HEADERS = {
301
+ displayName: 'Display name',
302
+ kind: 'Kind',
303
+ source: 'Source',
304
+ updatedAt: 'Updated'
305
+ };
306
+ /** The template kinds, by the stored `kind`. */ export const TEMPLATE_KIND_OPTIONS = [
307
+ {
308
+ value: 'page',
309
+ label: 'Page'
310
+ },
311
+ {
312
+ value: 'component',
313
+ label: 'Component'
314
+ },
315
+ {
316
+ value: 'layout',
317
+ label: 'Layout'
318
+ }
319
+ ];
320
+
321
+ //# sourceMappingURL=artifact-list-queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-queries.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-util-tools/list-query/list-filter-codecs'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n type ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\n\n/*\n * WHAT THE SITE ARTIFACT LISTS ASK FIRESTORE (AGL-3321).\n *\n * The layouts page, the components card and the templates library put every\n * clause their Filters panel accepts, and the quick-search word, on the query\n * — composed by `planListQuery` — and page what it answers. Nothing is matched\n * over the rows a page happened to read. The template gallery's shelves ask\n * the same keys (`template-gallery-dialog.component.tsx`), and so does the\n * email plugin's templates list, from its own declaration.\n *\n * ## The keys are WRITTEN\n *\n * A query finds a document by a stored value, so each list reads keys every\n * writer stamps (`artifactCreateListKeys`/`artifactRenameListKeys` in\n * `@aglyn/aglyn/app-utils/artifact-list-keys`) and\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps on the documents\n * written before them:\n *\n * nameTokens the name's word prefixes — the search box and `contains`;\n * nameLower the normalized name — `equals`;\n * kind a component's `site`/`email`, a template's page/component/\n * layout, stored even where a reader used to infer it;\n * source.type a template's provenance, `authored` where there was none;\n * libraryRow a template that is its own library row (below).\n *\n * ## The order is the document NAME, and that is what keeps it cheap\n *\n * `hostArtifactQuery` explains why these lists walk the document id: it is\n * the one order no writer can omit, so the walk is total. It is also the\n * order index merging serves with Firestore's automatic single-field\n * indexes: any mix of the equalities and the one array clause above, ordered\n * by `__name__`, needs no composite at all. The composites below exist only\n * for the one range each list offers — `Updated` — which must lead the order\n * it ranges over, so it pairs with each equality once.\n *\n * ## What is not offered, and why\n *\n * - `startsWith`/`endsWith` on the name: a prefix range must lead the order,\n * and these lists are not sorted by name.\n * - `description` (a filter and a search target before): no token field,\n * and a second token array could not share the query's one array clause\n * with the search anyway.\n * - `Created` ranges: a second range order would double the composites for\n * a question `Updated` answers nearly as well; one range per list.\n *\n * ## The header sorts (AGL-3680)\n *\n * Every column sorts on the QUERY, by a stored value every writer stamps:\n *\n * Display name `nameLower` (above; a starter page's is its starter's\n * name, which is the name its library row shows);\n * ID the walk itself, either way;\n * Updated, `updatedAt` / `createdAt` — stamped by every create, and by\n * Created `backfill-artifacts-list-keys.mjs` on a document that\n * predates either;\n * Used in, Kind the stored `kind`; Source the stored `source.type`;\n * Description a layout's or component's `description`, stored as `null`\n * when it has none (`artifactCreateListKeys` + the backfill),\n * because an `orderBy` DROPS a document that lacks the field.\n * A template row's description is its starter's when it\n * leads one, which is no stored field: it sorts the page.\n *\n * Each is `alone` — served only while no filter or search is on, falling\n * back to the list's default header order with a notice — so on the layouts\n * and components subcollections it costs no composite. Updated newest-first\n * is not `alone`: it is the order\n * the Updated range already imposes, so it pairs with every equality and\n * its composites are the ones the range needed anyway. The templates\n * library's `libraryRow` scope is an equality every order pairs with, so\n * each of its orders has one `(libraryRow, order)` composite.\n */\n\n/** The walk every artifact list keeps: the document name, ascending. */\nexport const ARTIFACT_LIST_ORDER: ListQuerySort = {\n path: LIST_QUERY_ID_PATH,\n direction: 'asc',\n}\n\n/** The name, by word prefix (`contains`) or whole (`equals`). */\nconst NAME_FIELD: ListFilterField = {\n column: 'displayName',\n kind: 'text',\n path: 'displayName',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals'],\n}\n\n/** The document id, which the walk already orders by. */\nconst ID_FIELD: ListFilterField = {\n column: '$id',\n kind: 'id',\n path: LIST_QUERY_ID_PATH,\n}\n\n/** The one range: `updatedAt`, which every writer stamps. */\nconst UPDATED_FIELD: ListFilterField = {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */\nexport const ARTIFACT_NAME_SEARCH = { tokensPath: 'nameTokens' }\n\n/** One header's two orders, `alone` (see \"The header sorts\"). */\nconst headerSorts = (column: string, path: string, label: string): ListQuerySort[] => [\n { path, direction: 'asc', column, label, alone: true },\n { path, direction: 'desc', column, label, alone: true },\n]\n\n/**\n * The ID header: the walk ascending — the list's default, served under every\n * filter — and descending, `alone`.\n */\nconst ID_SORTS: readonly ListQuerySort[] = [\n { path: LIST_QUERY_ID_PATH, direction: 'asc', column: '$id', label: 'ID' },\n { path: LIST_QUERY_ID_PATH, direction: 'desc', column: '$id', label: 'ID', alone: true },\n]\n\n/**\n * Updated newest first FIRST: it is the order the Updated range imposes\n * (`planListQuery` takes the first declared order on the range's field), so\n * it is not `alone` — the range's composites already serve it.\n */\nconst TIME_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt', label: 'Updated' },\n { path: 'updatedAt', direction: 'asc', column: 'updatedAt', label: 'Updated', alone: true },\n ...headerSorts('createdAt', 'createdAt', 'Created'),\n]\n\nconst NAME_SORTS = headerSorts('displayName', 'nameLower', 'Display name')\nconst DESCRIPTION_SORTS = headerSorts('description', 'description', 'Description')\n\n/** The layouts page's header sorts; the ID ascending one is the default. */\nexport const LAYOUT_LIST_SORTS: readonly ListQuerySort[] = [\n ...ID_SORTS,\n ...NAME_SORTS,\n ...DESCRIPTION_SORTS,\n ...TIME_SORTS,\n]\n\n/**\n * The layouts list's scope: live layouts. Every create stores `deletedAt: null`\n * (`artifactCreateListKeys`) and a delete stamps the time, so the query leaves\n * tombstones out instead of the page dropping them after the read (AGL-3680).\n */\nexport const LAYOUT_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'deletedAt', op: '==', value: null },\n]\n\n/** `hosts/{hostId}/layouts`. */\nexport const LAYOUT_LIST_QUERY: ListQueryDeclaration = {\n fields: [NAME_FIELD, ID_FIELD, UPDATED_FIELD],\n sorts: [ARTIFACT_LIST_ORDER, ...LAYOUT_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const LAYOUT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/** The components card's header sorts; the ID ascending one is the default. */\nexport const COMPONENT_LIST_SORTS: readonly ListQuerySort[] = [\n ...ID_SORTS,\n ...NAME_SORTS,\n ...headerSorts('kind', 'kind', 'Used in'),\n ...DESCRIPTION_SORTS,\n ...TIME_SORTS,\n]\n\n/** `hosts/{hostId}/components`. */\nexport const COMPONENT_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n ID_FIELD,\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER, ...COMPONENT_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const COMPONENT_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Used in',\n $id: 'ID',\n updatedAt: 'Updated',\n}\n\n/*\n * `hosts/{hostId}/templates`, as the library lists it: one row per template,\n * except that a multi-page STARTER is one row (AGL-696) — its first live page\n * carries `libraryRow: true` and the rest `false`, and a deleted template\n * `false`, so the base clause below is both \"one row per starter\" and \"no\n * tombstones\" without a single row being dropped after the read. Every page\n * of a starter carries the starter's name keys, so a search for it finds the\n * row whichever page leads it.\n */\nexport const TEMPLATE_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'libraryRow', op: '==', value: true },\n]\n\n/**\n * The templates library's header sorts. It shows no ID column, so the walk\n * stays the unlabelled default; Description sorts the page (see \"The header\n * sorts\").\n */\nexport const TEMPLATE_LIST_SORTS: readonly ListQuerySort[] = [\n ...NAME_SORTS,\n ...headerSorts('kind', 'kind', 'Kind'),\n ...headerSorts('source', 'source.type', 'Source'),\n ...TIME_SORTS,\n]\n\nexport const TEMPLATE_LIST_QUERY: ListQueryDeclaration = {\n fields: [\n NAME_FIELD,\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n {\n column: 'source',\n kind: 'exact',\n path: 'source.type',\n operators: ['equals', 'isAnyOf'],\n },\n UPDATED_FIELD,\n ],\n sorts: [ARTIFACT_LIST_ORDER, ...TEMPLATE_LIST_SORTS],\n search: ARTIFACT_NAME_SEARCH,\n}\n\nexport const TEMPLATE_LIST_HEADERS: Readonly<Record<string, string>> = {\n displayName: 'Display name',\n kind: 'Kind',\n source: 'Source',\n updatedAt: 'Updated',\n}\n\n/** The template kinds, by the stored `kind`. */\nexport const TEMPLATE_KIND_OPTIONS: readonly ListFilterOption[] = [\n { value: 'page', label: 'Page' },\n { value: 'component', label: 'Component' },\n { value: 'layout', label: 'Layout' },\n]\n"],"names":["LIST_QUERY_ID_PATH","ARTIFACT_LIST_ORDER","path","direction","NAME_FIELD","column","kind","lowerPath","tokensPath","operators","ID_FIELD","UPDATED_FIELD","ARTIFACT_NAME_SEARCH","headerSorts","label","alone","ID_SORTS","TIME_SORTS","NAME_SORTS","DESCRIPTION_SORTS","LAYOUT_LIST_SORTS","LAYOUT_LIST_BASE","op","value","LAYOUT_LIST_QUERY","fields","sorts","search","LAYOUT_LIST_HEADERS","displayName","$id","updatedAt","COMPONENT_LIST_SORTS","COMPONENT_LIST_QUERY","COMPONENT_LIST_HEADERS","TEMPLATE_LIST_BASE","TEMPLATE_LIST_SORTS","TEMPLATE_LIST_QUERY","TEMPLATE_LIST_HEADERS","source","TEMPLATE_KIND_OPTIONS"],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type HostSeoEntity } from './content-authors';
18
+ /**
19
+ * A site's BUSINESS PROFILE (AGL-3661): what the business is, as everything
20
+ * that writes about it needs to know — the site's own pages, the structured
21
+ * data, and any generator a plugin brings.
22
+ *
23
+ * ── Most of it is already stored, so this stores only the rest ─────────────
24
+ *
25
+ * The site's name, its one-line description, its contact details, its hours,
26
+ * the areas it serves and its social profiles all have a home on the host
27
+ * document already: `seo.entity` (the publisher entity the JSON-LD is built
28
+ * from, edited in Setup → SEO) and `business` (the contact card the `host.*`
29
+ * tokens and the footer read, edited in Setup → Basic details). Copying them
30
+ * here would make two places to keep in step, and the copy would drift.
31
+ *
32
+ * So the profile document holds only what has no home yet — the services, the
33
+ * audience, the tone of voice — plus a line about the business and the area it
34
+ * serves for a site whose SEO entity does not say. {@link resolveBusinessProfile}
35
+ * reads the two together into one picture, field by field, and says where
36
+ * each value came from.
37
+ *
38
+ * ── Where it is stored ────────────────────────────────────────────────────
39
+ *
40
+ * `hosts/{hostId}/businessProfile/profile` for a site and
41
+ * `orgs/{orgId}/businessProfile/defaults` for the workspace, whose values a
42
+ * site inherits where it has none of its own. A subcollection document rather
43
+ * than a key on the host: the host document's `business` map is replaced
44
+ * whole by the contact card's save, and a key beside it would be one more
45
+ * thing a partial write could blank.
46
+ *
47
+ * ── Whose words win ───────────────────────────────────────────────────────
48
+ *
49
+ * Every stored value carries its source. What the owner typed is never
50
+ * replaced by anything else; what the guided start was told replaces only
51
+ * what a generator guessed; and a generator's guess fills only what is empty.
52
+ * {@link mergeBusinessProfilePrefill} is the one place that rule is applied.
53
+ */
54
+ /** The collection a profile document lives in, under a host or an org. */
55
+ export declare const BUSINESS_PROFILE_SUBCOLLECTION = "businessProfile";
56
+ /** The site's profile document id. */
57
+ export declare const BUSINESS_PROFILE_SITE_DOC = "profile";
58
+ /** The workspace defaults document id. */
59
+ export declare const BUSINESS_PROFILE_WORKSPACE_DOC = "defaults";
60
+ /** Who wrote a stored value: the owner, the guided start's answers, or a generator. */
61
+ export type BusinessProfileSource = 'owner' | 'start' | 'ai';
62
+ /**
63
+ * Where a RESOLVED value came from: a stored source, the site's own settings
64
+ * (`seo.entity`, `business`, the display name), or the workspace defaults.
65
+ */
66
+ export type BusinessProfileOrigin = BusinessProfileSource | 'site' | 'workspace';
67
+ export declare const BUSINESS_TONES: readonly ["friendly", "professional", "playful", "premium", "plain"];
68
+ export type BusinessTone = (typeof BUSINESS_TONES)[number];
69
+ /** Each tone as the owner picks it and as a writer is told it. */
70
+ export declare const BUSINESS_TONE_LABELS: Readonly<Record<BusinessTone, string>>;
71
+ /** The fields the profile document stores itself. */
72
+ export declare const BUSINESS_PROFILE_FIELDS: readonly ["whatYouDo", "services", "serviceArea", "audience", "tone", "toneNotes"];
73
+ export type BusinessProfileField = (typeof BUSINESS_PROFILE_FIELDS)[number];
74
+ /** Each field's ceiling, in characters; `services` is per item. */
75
+ export declare const BUSINESS_PROFILE_MAX_CHARS: Readonly<Record<Exclude<BusinessProfileField, 'tone'>, number>>;
76
+ /** At most this many services are kept. */
77
+ export declare const BUSINESS_PROFILE_MAX_SERVICES = 12;
78
+ /** The stored document, at either level. */
79
+ export interface BusinessProfileDoc {
80
+ whatYouDo?: string;
81
+ services?: string[];
82
+ serviceArea?: string;
83
+ audience?: string;
84
+ tone?: BusinessTone | null;
85
+ toneNotes?: string;
86
+ /** Who wrote each stored value; a value without one is the owner's. */
87
+ sources?: Partial<Record<BusinessProfileField, BusinessProfileSource>>;
88
+ updatedAt?: unknown;
89
+ updatedBy?: string | null;
90
+ }
91
+ /** The values a write carries, before they are sanitized. */
92
+ export type BusinessProfileValues = Partial<Pick<BusinessProfileDoc, 'whatYouDo' | 'services' | 'serviceArea' | 'audience' | 'tone' | 'toneNotes'>>;
93
+ /** The host fields the profile reads. A closed surface, as the host tokens' is. */
94
+ export interface BusinessProfileHost {
95
+ displayName?: string;
96
+ logoUrl?: string;
97
+ seo?: {
98
+ description?: string;
99
+ entity?: HostSeoEntity | null;
100
+ } | null;
101
+ business?: {
102
+ supportEmail?: string;
103
+ address?: string;
104
+ socialLinks?: Array<{
105
+ label?: string;
106
+ url?: string;
107
+ } | null> | null;
108
+ } | null;
109
+ }
110
+ export interface ResolvedBusinessField<T> {
111
+ value: T;
112
+ origin: BusinessProfileOrigin;
113
+ }
114
+ /** The picture every reader works from: each field's value and where it came from. */
115
+ export interface ResolvedBusinessProfile {
116
+ name: ResolvedBusinessField<string> | null;
117
+ whatYouDo: ResolvedBusinessField<string> | null;
118
+ services: ResolvedBusinessField<string[]> | null;
119
+ serviceArea: ResolvedBusinessField<string> | null;
120
+ audience: ResolvedBusinessField<string> | null;
121
+ tone: ResolvedBusinessField<BusinessTone> | null;
122
+ toneNotes: ResolvedBusinessField<string> | null;
123
+ /** The schema.org business type the SEO entity names, e.g. `Plumber`. */
124
+ businessType: string | null;
125
+ /** Real contact details, each only where the owner entered it in the site's settings. */
126
+ contact: {
127
+ email: string | null;
128
+ phone: string | null;
129
+ address: string | null;
130
+ hours: string | null;
131
+ };
132
+ /** The site's social and other profiles, https only. */
133
+ profiles: string[];
134
+ logo: string | null;
135
+ }
136
+ /** The source a stored, non-empty value carries; the owner's when none is recorded. */
137
+ export declare function businessProfileSourceOf(doc: BusinessProfileDoc | null | undefined, field: BusinessProfileField): BusinessProfileSource | null;
138
+ /** Every value a write may carry, sanitized; an absent field stays absent. */
139
+ export declare function sanitizeBusinessProfileValues(values: BusinessProfileValues): BusinessProfileValues;
140
+ /**
141
+ * The document a generator's or the guided start's values make of the stored
142
+ * one: each value fills a field that is empty or was written by a source that
143
+ * weighs less, and nothing else. `null` when nothing would change, so a caller
144
+ * writes only when there is something to write.
145
+ */
146
+ export declare function mergeBusinessProfilePrefill(current: BusinessProfileDoc | null | undefined, values: BusinessProfileValues, source: Exclude<BusinessProfileSource, 'owner'>): BusinessProfileDoc | null;
147
+ /**
148
+ * The document the owner's save makes: every field they changed becomes
149
+ * theirs, and a field they left as it was keeps the source it had — so saving
150
+ * the form after correcting one line does not claim every suggestion on it.
151
+ */
152
+ export declare function businessProfileOwnerWrite(current: BusinessProfileDoc | null | undefined, edited: BusinessProfileValues): Pick<BusinessProfileDoc, BusinessProfileField | 'sources'>;
153
+ /**
154
+ * The profile as every reader sees it, field by field (AGL-3661):
155
+ *
156
+ * 1. what the OWNER typed into the site's profile;
157
+ * 2. what the site's own settings say (the SEO entity, the contact card, the
158
+ * display name), which the owner typed too, elsewhere;
159
+ * 3. what the guided start or a generator stored on the site's profile;
160
+ * 4. the workspace defaults.
161
+ *
162
+ * Contact details, the logo and the profiles come from the site's settings
163
+ * only: nothing but the owner's own entry may say how to reach the business.
164
+ */
165
+ export declare function resolveBusinessProfile(input: {
166
+ host: BusinessProfileHost | null | undefined;
167
+ site?: BusinessProfileDoc | null;
168
+ workspace?: BusinessProfileDoc | null;
169
+ }): ResolvedBusinessProfile;
170
+ /** Each origin as the profile screen labels it. */
171
+ export declare const BUSINESS_PROFILE_ORIGIN_LABELS: Readonly<Record<BusinessProfileOrigin, string>>;