@aglyn/aglyn 1.0.0-beta.231 → 1.0.0-beta.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/account-acquisition.d.ts +15 -2
- package/src/lib/app-utils/account-acquisition.js +20 -1
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/artifact-list-queries.d.ts +35 -0
- package/src/lib/app-utils/artifact-list-queries.js +203 -0
- package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
- package/src/lib/app-utils/business-profile.d.ts +171 -0
- package/src/lib/app-utils/business-profile.js +277 -0
- package/src/lib/app-utils/business-profile.js.map +1 -0
- package/src/lib/app-utils/child-contract.d.ts +4 -1
- package/src/lib/app-utils/child-contract.js +5 -2
- package/src/lib/app-utils/child-contract.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +5 -0
- package/src/lib/app-utils/console-routes.js +1 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/content-schema-type.d.ts +9 -0
- package/src/lib/app-utils/content-schema-type.js +4 -0
- package/src/lib/app-utils/content-schema-type.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +41 -5
- package/src/lib/app-utils/docs-help.generated.js +83 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +181 -13
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
- package/src/lib/app-utils/entry-list-declaration.js +172 -0
- package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
- package/src/lib/app-utils/first-version-seed.d.ts +19 -0
- package/src/lib/app-utils/first-version-seed.js +53 -0
- package/src/lib/app-utils/first-version-seed.js.map +1 -0
- package/src/lib/app-utils/local-business.d.ts +8 -0
- package/src/lib/app-utils/local-business.js +4 -0
- package/src/lib/app-utils/local-business.js.map +1 -1
- package/src/lib/app-utils/page-markdown.js +3 -0
- package/src/lib/app-utils/page-markdown.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +19 -0
- package/src/lib/plugin-manager/feature-plugins.js +8 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +105 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
- package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
- package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
- package/src/lib/plugin-manager/plugin-product-catalog.d.ts +5 -0
- package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
- package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
- package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
- package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
- package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
- package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
- package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.232",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
46
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
47
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
48
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
49
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
40
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.232",
|
|
41
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.232",
|
|
42
|
+
"@aglyn/shared-data-types": "1.0.0-beta.232",
|
|
43
|
+
"@aglyn/shared-util-email": "1.0.0-beta.232",
|
|
44
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.232",
|
|
45
|
+
"@aglyn/shared-util-http": "1.0.0-beta.232",
|
|
46
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.232",
|
|
47
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.232",
|
|
48
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.232",
|
|
49
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.232",
|
|
50
50
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
51
51
|
"@msgpack/msgpack": "^3.1.3",
|
|
52
52
|
"@types/unist": "^3.0.3",
|
|
@@ -47,10 +47,23 @@ import { type AcquisitionChannel, type FirstTouchClickId, type FirstTouchUtm } f
|
|
|
47
47
|
export declare const ACCOUNT_ACQUISITION_FIELD = "acquisition";
|
|
48
48
|
/**
|
|
49
49
|
* How an account was created: the signup form with a password, the signup
|
|
50
|
-
* form through Google
|
|
50
|
+
* form through Google — in a popup, or by the full-page redirect phones use —
|
|
51
|
+
* accepting an invitation, or single sign-on.
|
|
51
52
|
*/
|
|
52
|
-
export declare const ACQUISITION_DOORS: readonly ["signup-password", "signup-google", "invite", "sso"];
|
|
53
|
+
export declare const ACQUISITION_DOORS: readonly ["signup-password", "signup-google", "signup-google-redirect", "invite", "sso"];
|
|
53
54
|
export type AcquisitionDoor = (typeof ACQUISITION_DOORS)[number] | 'unknown';
|
|
55
|
+
/** The sign-up form's doors: the ones a person reaches by signing up themselves. */
|
|
56
|
+
export type SignUpAcquisitionDoor = Extract<AcquisitionDoor, 'signup-password' | 'signup-google' | 'signup-google-redirect'>;
|
|
57
|
+
/**
|
|
58
|
+
* The sign-up door an account took, from the provider its VERIFIED token
|
|
59
|
+
* names, or null when that provider is not one the sign-up form offers.
|
|
60
|
+
*
|
|
61
|
+
* The provider decides; the page's `hint` only says which of a provider's
|
|
62
|
+
* doors it was, so a Google account can say "redirect" and nothing else — it
|
|
63
|
+
* can never claim the password door, and a password account can never claim
|
|
64
|
+
* a Google one.
|
|
65
|
+
*/
|
|
66
|
+
export declare function signUpDoorFor(provider: string | null | undefined, hint?: unknown): SignUpAcquisitionDoor | null;
|
|
54
67
|
/** What wrote a record: a door at creation, an organization copying its creator's, or the backfill. */
|
|
55
68
|
export type AcquisitionRecordedBy = 'signup' | 'sso' | 'org-creation' | 'backfill';
|
|
56
69
|
/** Where the account-creating request came from, as the edge reported it. */
|
|
@@ -45,13 +45,30 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
45
45
|
*/ /** The field on `users/{uid}` and `orgs/{orgId}`. */ export const ACCOUNT_ACQUISITION_FIELD = 'acquisition';
|
|
46
46
|
/**
|
|
47
47
|
* How an account was created: the signup form with a password, the signup
|
|
48
|
-
* form through Google
|
|
48
|
+
* form through Google — in a popup, or by the full-page redirect phones use —
|
|
49
|
+
* accepting an invitation, or single sign-on.
|
|
49
50
|
*/ export const ACQUISITION_DOORS = [
|
|
50
51
|
'signup-password',
|
|
51
52
|
'signup-google',
|
|
53
|
+
'signup-google-redirect',
|
|
52
54
|
'invite',
|
|
53
55
|
'sso'
|
|
54
56
|
];
|
|
57
|
+
/**
|
|
58
|
+
* The sign-up door an account took, from the provider its VERIFIED token
|
|
59
|
+
* names, or null when that provider is not one the sign-up form offers.
|
|
60
|
+
*
|
|
61
|
+
* The provider decides; the page's `hint` only says which of a provider's
|
|
62
|
+
* doors it was, so a Google account can say "redirect" and nothing else — it
|
|
63
|
+
* can never claim the password door, and a password account can never claim
|
|
64
|
+
* a Google one.
|
|
65
|
+
*/ export function signUpDoorFor(provider, hint) {
|
|
66
|
+
if (provider === 'password') return 'signup-password';
|
|
67
|
+
if (provider === 'google.com') {
|
|
68
|
+
return hint === 'signup-google-redirect' ? 'signup-google-redirect' : 'signup-google';
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
55
72
|
const MAX_LABEL = 100;
|
|
56
73
|
const PROVIDER_SHAPE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
57
74
|
const RECORDED_BY = [
|
|
@@ -278,6 +295,8 @@ function doorPhrase(record) {
|
|
|
278
295
|
return 'signed up with password';
|
|
279
296
|
case 'signup-google':
|
|
280
297
|
return 'signed up with Google';
|
|
298
|
+
case 'signup-google-redirect':
|
|
299
|
+
return 'signed up with Google, by redirect';
|
|
281
300
|
case 'invite':
|
|
282
301
|
return provider ? `signed up from an invitation, with ${provider}` : 'signed up from an invitation';
|
|
283
302
|
case 'sso':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/account-acquisition.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 {\n classifyChannel,\n displayHost,\n sanitizeFirstTouch,\n sourceAndMedium,\n type AcquisitionChannel,\n type FirstTouchClickId,\n type FirstTouchUtm,\n} from '@aglyn/shared-util-first-touch'\n\n/**\n * Where an account came from, written ONCE on the account when it is created\n * (AGL-3289) — `users/{uid}.acquisition`, and on the organization created in\n * the same flow as `orgs/{orgId}.acquisition`.\n *\n * ## Why this exists\n *\n * Answering \"where did this person come from?\" for one new workspace took a\n * sales session twenty-five minutes across five tools: the staff user page\n * had no source, Firestore held nothing beyond names, and the answer — a\n * review site's referral — was finally found in an analytics property and in\n * the marketing host's daily referrer counts. The platform never stored the\n * fact on the account it was about. This record is that fact, read from one\n * card on the staff user and organization pages.\n *\n * ## What writes it, and what never may\n *\n * The capture library keeps the first touch on the visitor's device; the\n * platform turns it into this record at account creation, server-side, and\n * the rules refuse the field to every client — owner and staff alike. A\n * record the account holder could restate would be a record nobody could\n * trust. It is never overwritten: a later touch, if one is ever kept, goes\n * BESIDE it as its own field.\n *\n * Every field here is built from the untrusted touch by re-scrubbing it, the\n * same way the capture scrubbed it on the device, and nothing the record says\n * grants anything: it is a label for a staff reader.\n */\n\n/** The field on `users/{uid}` and `orgs/{orgId}`. */\nexport const ACCOUNT_ACQUISITION_FIELD = 'acquisition'\n\n/**\n * How an account was created: the signup form with a password, the signup\n * form through Google, accepting an invitation, or single sign-on.\n */\nexport const ACQUISITION_DOORS = [\n 'signup-password',\n 'signup-google',\n 'invite',\n 'sso',\n] as const\nexport type AcquisitionDoor = (typeof ACQUISITION_DOORS)[number] | 'unknown'\n\n/** What wrote a record: a door at creation, an organization copying its creator's, or the backfill. */\nexport type AcquisitionRecordedBy = 'signup' | 'sso' | 'org-creation' | 'backfill'\n\n/** Where the account-creating request came from, as the edge reported it. */\nexport interface AcquisitionGeo {\n country: string | null\n region: string | null\n city: string | null\n}\n\n/** The stored record. */\nexport interface AccountAcquisition {\n v: 1\n /**\n * Where the visit came from, spelled the way reports spell it — a\n * `utm_source`, a referring host, `(direct)` — or `unknown` when nothing was\n * captured.\n */\n source: string\n /** `utm_medium`, or what the channel implies; null when unknown. */\n medium: string | null\n /** `utm_campaign`, when the landing URL named one. */\n campaign: string | null\n /** The channel, from the platform's own rule table. */\n channel: AcquisitionChannel\n /** When the first visit landed; null when it was never captured. */\n capturedAt: number | null\n /** The first page the visitor landed on. */\n landing: { host: string; path: string } | null\n /** The external host that sent them. */\n referrerHost: string | null\n /** One of the platform's own hosts they came from before anything captured them. */\n viaHost: string | null\n /** Every `utm_*` the landing URL carried. */\n utm: FirstTouchUtm | null\n /** Which ad click ids the landing URL carried — presence only. */\n clickIds: FirstTouchClickId[]\n /**\n * The Google Ads campaign the click came from (`gad_campaignid`), which\n * auto-tagging appends whether or not the ad carries a `utm_campaign`.\n */\n adCampaignId: string | null\n /** How the account was created. */\n door: AcquisitionDoor\n /** The sign-in provider it was created with: `password`, `google.com`, `saml.…`. */\n provider: string | null\n /** The organization whose invitation the person signed up to accept. */\n invitedToOrgId: string | null\n /** Where the account-creating request came from. */\n geo: AcquisitionGeo | null\n /** When the auth record says the account was created. */\n accountCreatedAt: number | null\n /** When this record was written. */\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n /** On an organization's copy: the account it was copied from. */\n copiedFromUid?: string\n}\n\nconst MAX_LABEL = 100\nconst PROVIDER_SHAPE = /^[a-z0-9][a-z0-9._-]{0,63}$/i\nconst RECORDED_BY: readonly AcquisitionRecordedBy[] = [\n 'signup',\n 'sso',\n 'org-creation',\n 'backfill',\n]\n\nfunction label(value: unknown, max = MAX_LABEL): string | null {\n if (typeof value !== 'string') return null\n const clean = value.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim() // eslint-disable-line no-control-regex\n return clean ? clean.slice(0, max) : null\n}\n\nfunction doorOf(value: unknown): AcquisitionDoor {\n return (ACQUISITION_DOORS as readonly string[]).includes(value as string)\n ? (value as AcquisitionDoor)\n : 'unknown'\n}\n\nfunction providerOf(value: unknown): string | null {\n return typeof value === 'string' && PROVIDER_SHAPE.test(value.trim())\n ? value.trim().toLowerCase()\n : null\n}\n\nfunction geoOf(value: unknown): AcquisitionGeo | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const geo: AcquisitionGeo = {\n country: label(raw['country'], 8)?.toUpperCase() ?? null,\n region: label(raw['region'], 64),\n city: label(raw['city'], 64),\n }\n return geo.country || geo.region || geo.city ? geo : null\n}\n\nfunction millisOf(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\nfunction uidOf(value: unknown): string | null {\n return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) ? value : null\n}\n\n/** What a door knows when it records an account. */\nexport interface AccountAcquisitionInput {\n /** The first touch the capture kept — untrusted, from the device. */\n touch: unknown\n door: unknown\n provider?: unknown\n geo?: unknown\n invitedToOrgId?: unknown\n /** When the auth record says the account was created. */\n accountCreatedAtMs?: number | null\n recordedBy: AcquisitionRecordedBy\n nowMs: number\n}\n\n/**\n * The record for an account being created now.\n *\n * A missing or unusable touch still produces a record — `source: 'unknown'`\n * with whatever the door knows — because \"we asked and nothing came back\" is\n * a fact about the account too, and a card that renders blank would read as\n * \"never asked\".\n */\nexport function buildAccountAcquisition(input: AccountAcquisitionInput): AccountAcquisition {\n const touch = sanitizeFirstTouch(input.touch, input.nowMs)\n const base = {\n door: doorOf(input.door),\n provider: providerOf(input.provider),\n invitedToOrgId: uidOf(input.invitedToOrgId),\n geo: geoOf(input.geo),\n accountCreatedAt: millisOf(input.accountCreatedAtMs),\n recordedAt: input.nowMs,\n recordedBy: input.recordedBy,\n }\n if (!touch) return unknownAccountAcquisition(base)\n const { source, medium } = sourceAndMedium(touch)\n return {\n v: 1,\n source,\n medium,\n campaign: touch.utm?.campaign ?? null,\n channel: classifyChannel(touch),\n capturedAt: touch.at,\n landing: { host: touch.host, path: touch.path },\n referrerHost: touch.ref,\n viaHost: touch.via ?? null,\n utm: touch.utm ?? null,\n clickIds: touch.click ?? [],\n adCampaignId: touch.adCampaign ?? null,\n ...base,\n }\n}\n\n/** A record for an account nothing was captured for. */\nexport function unknownAccountAcquisition(input: {\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n door?: AcquisitionDoor\n provider?: string | null\n invitedToOrgId?: string | null\n geo?: AcquisitionGeo | null\n accountCreatedAt?: number | null\n}): AccountAcquisition {\n return {\n v: 1,\n source: 'unknown',\n medium: null,\n campaign: null,\n channel: 'unknown',\n capturedAt: null,\n landing: null,\n referrerHost: null,\n viaHost: null,\n utm: null,\n clickIds: [],\n adCampaignId: null,\n door: input.door ?? 'unknown',\n provider: input.provider ?? null,\n invitedToOrgId: input.invitedToOrgId ?? null,\n geo: input.geo ?? null,\n accountCreatedAt: input.accountCreatedAt ?? null,\n recordedAt: input.recordedAt,\n recordedBy: input.recordedBy,\n }\n}\n\n/**\n * The organization's copy of its creator's record, or an `unknown` one naming\n * the creator when the account has none — so an organization's card is never\n * blank either, and always says whose record it is.\n */\nexport function organizationAcquisition(\n creatorRecord: unknown,\n creatorUid: string,\n nowMs: number,\n): AccountAcquisition {\n const record = readAccountAcquisition(creatorRecord)\n const copied = record\n ? { ...record }\n : unknownAccountAcquisition({ recordedAt: nowMs, recordedBy: 'org-creation' })\n delete copied.copiedFromUid\n return { ...copied, copiedFromUid: creatorUid }\n}\n\n/**\n * A stored record as a reader should trust it: re-validated field by field,\n * so a document written by an older version, a backfill or a hand edit still\n * renders, and renders nothing it should not. Null when the value is not a\n * record at all.\n */\nexport function readAccountAcquisition(value: unknown): AccountAcquisition | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const source = label(raw['source'])\n if (!source) return null\n const channel = raw['channel']\n const landing = raw['landing'] as Record<string, unknown> | null\n const utmRaw = raw['utm'] as Record<string, unknown> | null\n const utm: FirstTouchUtm = {}\n for (const key of ['source', 'medium', 'campaign', 'content', 'term'] as const) {\n const cleaned = label(utmRaw?.[key])\n if (cleaned) utm[key] = cleaned\n }\n const clickIds = Array.isArray(raw['clickIds'])\n ? (['gclid', 'fbclid', 'msclkid'] as const).filter((id) =>\n (raw['clickIds'] as unknown[]).includes(id),\n )\n : []\n const recordedBy = RECORDED_BY.includes(raw['recordedBy'] as AcquisitionRecordedBy)\n ? (raw['recordedBy'] as AcquisitionRecordedBy)\n : 'backfill'\n const record: AccountAcquisition = {\n v: 1,\n source,\n medium: label(raw['medium']),\n campaign: label(raw['campaign']),\n channel:\n typeof channel === 'string' &&\n [\n 'organic-search',\n 'paid-search',\n 'social',\n 'referral',\n 'email',\n 'direct',\n 'unknown',\n ].includes(channel)\n ? (channel as AcquisitionChannel)\n : 'unknown',\n capturedAt:\n typeof raw['capturedAt'] === 'number' && raw['capturedAt'] > 0\n ? (raw['capturedAt'] as number)\n : null,\n landing:\n landing && label(landing['host'], 253)\n ? { host: label(landing['host'], 253) as string, path: label(landing['path'], 200) ?? '/' }\n : null,\n referrerHost: label(raw['referrerHost'], 253),\n viaHost: label(raw['viaHost'], 253),\n utm: Object.keys(utm).length ? utm : null,\n clickIds,\n adCampaignId:\n typeof raw['adCampaignId'] === 'string' && /^\\d{1,20}$/.test(raw['adCampaignId'])\n ? raw['adCampaignId']\n : null,\n door: doorOf(raw['door']),\n provider: providerOf(raw['provider']),\n invitedToOrgId: uidOf(raw['invitedToOrgId']),\n geo: geoOf(raw['geo']),\n accountCreatedAt: millisOf(raw['accountCreatedAt']),\n recordedAt:\n typeof raw['recordedAt'] === 'number' ? (raw['recordedAt'] as number) : 0,\n recordedBy,\n }\n const copiedFrom = uidOf(raw['copiedFromUid'])\n if (copiedFrom) record.copiedFromUid = copiedFrom\n return record\n}\n\n/** The Google Ads page for a campaign id, opened in whichever account is signed in. */\nexport function googleAdsCampaignUrl(campaignId: string): string {\n return `https://ads.google.com/aw/campaigns?campaignId=${encodeURIComponent(campaignId)}`\n}\n\n/** A sign-in provider id as a person reads it. */\nexport function providerLabel(provider: string | null | undefined): string | null {\n if (!provider) return null\n if (provider === 'password') return 'password'\n if (provider === 'google.com') return 'Google'\n if (provider === 'apple.com') return 'Apple'\n if (provider === 'microsoft.com') return 'Microsoft'\n if (provider === 'github.com') return 'GitHub'\n if (provider.startsWith('saml.') || provider.startsWith('oidc.')) return 'single sign-on'\n return provider\n}\n\nfunction channelPhrase(record: AccountAcquisition): string {\n const from = displayHost(record.referrerHost) || record.source\n const named = record.campaign ?? (record.adCampaignId ? `campaign ${record.adCampaignId}` : null)\n const campaign = named ? `, ${named}` : ''\n switch (record.channel) {\n case 'referral':\n return `Referral from ${from}${campaign}`\n case 'organic-search':\n return `Organic search from ${from}`\n case 'paid-search':\n return `Paid search (${record.source}${campaign})`\n case 'social':\n return `Social from ${from}${campaign}`\n case 'email':\n return `Email (${record.source}${campaign})`\n case 'direct':\n return record.viaHost\n ? `Direct, first seen arriving from ${record.viaHost}`\n : 'Direct'\n default:\n return record.recordedBy === 'backfill'\n ? 'Source unknown — the account predates capture'\n : 'Source unknown — nothing was captured'\n }\n}\n\nfunction doorPhrase(record: AccountAcquisition): string {\n const provider = providerLabel(record.provider)\n switch (record.door) {\n case 'signup-password':\n return 'signed up with password'\n case 'signup-google':\n return 'signed up with Google'\n case 'invite':\n return provider\n ? `signed up from an invitation, with ${provider}`\n : 'signed up from an invitation'\n case 'sso':\n return 'signed in with single sign-on'\n default:\n return provider ? `created with ${provider}` : 'created'\n }\n}\n\n/**\n * The record as one sentence a staff member reads at a glance:\n * \"Referral from g2.com → /pricing → signed up with password\".\n *\n * The landing is a bare path when it was on `primaryHost` — the platform's own\n * home — and host-and-path anywhere else, so a docs landing says so.\n */\nexport function describeAccountAcquisition(\n record: AccountAcquisition | null | undefined,\n options: { primaryHost?: string | null } = {},\n): string {\n if (!record) return 'No acquisition record'\n const parts = [channelPhrase(record)]\n if (record.landing) {\n const onPrimary =\n options.primaryHost &&\n displayHost(record.landing.host) === displayHost(options.primaryHost)\n parts.push(\n onPrimary ? record.landing.path : `${displayHost(record.landing.host)}${record.landing.path}`,\n )\n }\n parts.push(doorPhrase(record))\n return parts.join(' → ')\n}\n"],"names":["classifyChannel","displayHost","sanitizeFirstTouch","sourceAndMedium","ACCOUNT_ACQUISITION_FIELD","ACQUISITION_DOORS","MAX_LABEL","PROVIDER_SHAPE","RECORDED_BY","label","value","max","clean","replace","trim","slice","doorOf","includes","providerOf","test","toLowerCase","geoOf","raw","geo","country","toUpperCase","region","city","millisOf","Number","isFinite","uidOf","buildAccountAcquisition","input","touch","nowMs","base","door","provider","invitedToOrgId","accountCreatedAt","accountCreatedAtMs","recordedAt","recordedBy","unknownAccountAcquisition","source","medium","v","campaign","utm","channel","capturedAt","at","landing","host","path","referrerHost","ref","viaHost","via","clickIds","click","adCampaignId","adCampaign","organizationAcquisition","creatorRecord","creatorUid","record","readAccountAcquisition","copied","copiedFromUid","utmRaw","key","cleaned","Array","isArray","filter","id","Object","keys","length","copiedFrom","googleAdsCampaignUrl","campaignId","encodeURIComponent","providerLabel","startsWith","channelPhrase","from","named","doorPhrase","describeAccountAcquisition","options","parts","onPrimary","primaryHost","push","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/account-acquisition.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 {\n classifyChannel,\n displayHost,\n sanitizeFirstTouch,\n sourceAndMedium,\n type AcquisitionChannel,\n type FirstTouchClickId,\n type FirstTouchUtm,\n} from '@aglyn/shared-util-first-touch'\n\n/**\n * Where an account came from, written ONCE on the account when it is created\n * (AGL-3289) — `users/{uid}.acquisition`, and on the organization created in\n * the same flow as `orgs/{orgId}.acquisition`.\n *\n * ## Why this exists\n *\n * Answering \"where did this person come from?\" for one new workspace took a\n * sales session twenty-five minutes across five tools: the staff user page\n * had no source, Firestore held nothing beyond names, and the answer — a\n * review site's referral — was finally found in an analytics property and in\n * the marketing host's daily referrer counts. The platform never stored the\n * fact on the account it was about. This record is that fact, read from one\n * card on the staff user and organization pages.\n *\n * ## What writes it, and what never may\n *\n * The capture library keeps the first touch on the visitor's device; the\n * platform turns it into this record at account creation, server-side, and\n * the rules refuse the field to every client — owner and staff alike. A\n * record the account holder could restate would be a record nobody could\n * trust. It is never overwritten: a later touch, if one is ever kept, goes\n * BESIDE it as its own field.\n *\n * Every field here is built from the untrusted touch by re-scrubbing it, the\n * same way the capture scrubbed it on the device, and nothing the record says\n * grants anything: it is a label for a staff reader.\n */\n\n/** The field on `users/{uid}` and `orgs/{orgId}`. */\nexport const ACCOUNT_ACQUISITION_FIELD = 'acquisition'\n\n/**\n * How an account was created: the signup form with a password, the signup\n * form through Google — in a popup, or by the full-page redirect phones use —\n * accepting an invitation, or single sign-on.\n */\nexport const ACQUISITION_DOORS = [\n 'signup-password',\n 'signup-google',\n 'signup-google-redirect',\n 'invite',\n 'sso',\n] as const\nexport type AcquisitionDoor = (typeof ACQUISITION_DOORS)[number] | 'unknown'\n\n/** The sign-up form's doors: the ones a person reaches by signing up themselves. */\nexport type SignUpAcquisitionDoor = Extract<\n AcquisitionDoor,\n 'signup-password' | 'signup-google' | 'signup-google-redirect'\n>\n\n/**\n * The sign-up door an account took, from the provider its VERIFIED token\n * names, or null when that provider is not one the sign-up form offers.\n *\n * The provider decides; the page's `hint` only says which of a provider's\n * doors it was, so a Google account can say \"redirect\" and nothing else — it\n * can never claim the password door, and a password account can never claim\n * a Google one.\n */\nexport function signUpDoorFor(\n provider: string | null | undefined,\n hint?: unknown,\n): SignUpAcquisitionDoor | null {\n if (provider === 'password') return 'signup-password'\n if (provider === 'google.com') {\n return hint === 'signup-google-redirect' ? 'signup-google-redirect' : 'signup-google'\n }\n return null\n}\n\n/** What wrote a record: a door at creation, an organization copying its creator's, or the backfill. */\nexport type AcquisitionRecordedBy = 'signup' | 'sso' | 'org-creation' | 'backfill'\n\n/** Where the account-creating request came from, as the edge reported it. */\nexport interface AcquisitionGeo {\n country: string | null\n region: string | null\n city: string | null\n}\n\n/** The stored record. */\nexport interface AccountAcquisition {\n v: 1\n /**\n * Where the visit came from, spelled the way reports spell it — a\n * `utm_source`, a referring host, `(direct)` — or `unknown` when nothing was\n * captured.\n */\n source: string\n /** `utm_medium`, or what the channel implies; null when unknown. */\n medium: string | null\n /** `utm_campaign`, when the landing URL named one. */\n campaign: string | null\n /** The channel, from the platform's own rule table. */\n channel: AcquisitionChannel\n /** When the first visit landed; null when it was never captured. */\n capturedAt: number | null\n /** The first page the visitor landed on. */\n landing: { host: string; path: string } | null\n /** The external host that sent them. */\n referrerHost: string | null\n /** One of the platform's own hosts they came from before anything captured them. */\n viaHost: string | null\n /** Every `utm_*` the landing URL carried. */\n utm: FirstTouchUtm | null\n /** Which ad click ids the landing URL carried — presence only. */\n clickIds: FirstTouchClickId[]\n /**\n * The Google Ads campaign the click came from (`gad_campaignid`), which\n * auto-tagging appends whether or not the ad carries a `utm_campaign`.\n */\n adCampaignId: string | null\n /** How the account was created. */\n door: AcquisitionDoor\n /** The sign-in provider it was created with: `password`, `google.com`, `saml.…`. */\n provider: string | null\n /** The organization whose invitation the person signed up to accept. */\n invitedToOrgId: string | null\n /** Where the account-creating request came from. */\n geo: AcquisitionGeo | null\n /** When the auth record says the account was created. */\n accountCreatedAt: number | null\n /** When this record was written. */\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n /** On an organization's copy: the account it was copied from. */\n copiedFromUid?: string\n}\n\nconst MAX_LABEL = 100\nconst PROVIDER_SHAPE = /^[a-z0-9][a-z0-9._-]{0,63}$/i\nconst RECORDED_BY: readonly AcquisitionRecordedBy[] = [\n 'signup',\n 'sso',\n 'org-creation',\n 'backfill',\n]\n\nfunction label(value: unknown, max = MAX_LABEL): string | null {\n if (typeof value !== 'string') return null\n const clean = value.replace(/[\\u0000-\\u001f\\u007f]/g, '').trim() // eslint-disable-line no-control-regex\n return clean ? clean.slice(0, max) : null\n}\n\nfunction doorOf(value: unknown): AcquisitionDoor {\n return (ACQUISITION_DOORS as readonly string[]).includes(value as string)\n ? (value as AcquisitionDoor)\n : 'unknown'\n}\n\nfunction providerOf(value: unknown): string | null {\n return typeof value === 'string' && PROVIDER_SHAPE.test(value.trim())\n ? value.trim().toLowerCase()\n : null\n}\n\nfunction geoOf(value: unknown): AcquisitionGeo | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const geo: AcquisitionGeo = {\n country: label(raw['country'], 8)?.toUpperCase() ?? null,\n region: label(raw['region'], 64),\n city: label(raw['city'], 64),\n }\n return geo.country || geo.region || geo.city ? geo : null\n}\n\nfunction millisOf(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\nfunction uidOf(value: unknown): string | null {\n return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) ? value : null\n}\n\n/** What a door knows when it records an account. */\nexport interface AccountAcquisitionInput {\n /** The first touch the capture kept — untrusted, from the device. */\n touch: unknown\n door: unknown\n provider?: unknown\n geo?: unknown\n invitedToOrgId?: unknown\n /** When the auth record says the account was created. */\n accountCreatedAtMs?: number | null\n recordedBy: AcquisitionRecordedBy\n nowMs: number\n}\n\n/**\n * The record for an account being created now.\n *\n * A missing or unusable touch still produces a record — `source: 'unknown'`\n * with whatever the door knows — because \"we asked and nothing came back\" is\n * a fact about the account too, and a card that renders blank would read as\n * \"never asked\".\n */\nexport function buildAccountAcquisition(input: AccountAcquisitionInput): AccountAcquisition {\n const touch = sanitizeFirstTouch(input.touch, input.nowMs)\n const base = {\n door: doorOf(input.door),\n provider: providerOf(input.provider),\n invitedToOrgId: uidOf(input.invitedToOrgId),\n geo: geoOf(input.geo),\n accountCreatedAt: millisOf(input.accountCreatedAtMs),\n recordedAt: input.nowMs,\n recordedBy: input.recordedBy,\n }\n if (!touch) return unknownAccountAcquisition(base)\n const { source, medium } = sourceAndMedium(touch)\n return {\n v: 1,\n source,\n medium,\n campaign: touch.utm?.campaign ?? null,\n channel: classifyChannel(touch),\n capturedAt: touch.at,\n landing: { host: touch.host, path: touch.path },\n referrerHost: touch.ref,\n viaHost: touch.via ?? null,\n utm: touch.utm ?? null,\n clickIds: touch.click ?? [],\n adCampaignId: touch.adCampaign ?? null,\n ...base,\n }\n}\n\n/** A record for an account nothing was captured for. */\nexport function unknownAccountAcquisition(input: {\n recordedAt: number\n recordedBy: AcquisitionRecordedBy\n door?: AcquisitionDoor\n provider?: string | null\n invitedToOrgId?: string | null\n geo?: AcquisitionGeo | null\n accountCreatedAt?: number | null\n}): AccountAcquisition {\n return {\n v: 1,\n source: 'unknown',\n medium: null,\n campaign: null,\n channel: 'unknown',\n capturedAt: null,\n landing: null,\n referrerHost: null,\n viaHost: null,\n utm: null,\n clickIds: [],\n adCampaignId: null,\n door: input.door ?? 'unknown',\n provider: input.provider ?? null,\n invitedToOrgId: input.invitedToOrgId ?? null,\n geo: input.geo ?? null,\n accountCreatedAt: input.accountCreatedAt ?? null,\n recordedAt: input.recordedAt,\n recordedBy: input.recordedBy,\n }\n}\n\n/**\n * The organization's copy of its creator's record, or an `unknown` one naming\n * the creator when the account has none — so an organization's card is never\n * blank either, and always says whose record it is.\n */\nexport function organizationAcquisition(\n creatorRecord: unknown,\n creatorUid: string,\n nowMs: number,\n): AccountAcquisition {\n const record = readAccountAcquisition(creatorRecord)\n const copied = record\n ? { ...record }\n : unknownAccountAcquisition({ recordedAt: nowMs, recordedBy: 'org-creation' })\n delete copied.copiedFromUid\n return { ...copied, copiedFromUid: creatorUid }\n}\n\n/**\n * A stored record as a reader should trust it: re-validated field by field,\n * so a document written by an older version, a backfill or a hand edit still\n * renders, and renders nothing it should not. Null when the value is not a\n * record at all.\n */\nexport function readAccountAcquisition(value: unknown): AccountAcquisition | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const source = label(raw['source'])\n if (!source) return null\n const channel = raw['channel']\n const landing = raw['landing'] as Record<string, unknown> | null\n const utmRaw = raw['utm'] as Record<string, unknown> | null\n const utm: FirstTouchUtm = {}\n for (const key of ['source', 'medium', 'campaign', 'content', 'term'] as const) {\n const cleaned = label(utmRaw?.[key])\n if (cleaned) utm[key] = cleaned\n }\n const clickIds = Array.isArray(raw['clickIds'])\n ? (['gclid', 'fbclid', 'msclkid'] as const).filter((id) =>\n (raw['clickIds'] as unknown[]).includes(id),\n )\n : []\n const recordedBy = RECORDED_BY.includes(raw['recordedBy'] as AcquisitionRecordedBy)\n ? (raw['recordedBy'] as AcquisitionRecordedBy)\n : 'backfill'\n const record: AccountAcquisition = {\n v: 1,\n source,\n medium: label(raw['medium']),\n campaign: label(raw['campaign']),\n channel:\n typeof channel === 'string' &&\n [\n 'organic-search',\n 'paid-search',\n 'social',\n 'referral',\n 'email',\n 'direct',\n 'unknown',\n ].includes(channel)\n ? (channel as AcquisitionChannel)\n : 'unknown',\n capturedAt:\n typeof raw['capturedAt'] === 'number' && raw['capturedAt'] > 0\n ? (raw['capturedAt'] as number)\n : null,\n landing:\n landing && label(landing['host'], 253)\n ? { host: label(landing['host'], 253) as string, path: label(landing['path'], 200) ?? '/' }\n : null,\n referrerHost: label(raw['referrerHost'], 253),\n viaHost: label(raw['viaHost'], 253),\n utm: Object.keys(utm).length ? utm : null,\n clickIds,\n adCampaignId:\n typeof raw['adCampaignId'] === 'string' && /^\\d{1,20}$/.test(raw['adCampaignId'])\n ? raw['adCampaignId']\n : null,\n door: doorOf(raw['door']),\n provider: providerOf(raw['provider']),\n invitedToOrgId: uidOf(raw['invitedToOrgId']),\n geo: geoOf(raw['geo']),\n accountCreatedAt: millisOf(raw['accountCreatedAt']),\n recordedAt:\n typeof raw['recordedAt'] === 'number' ? (raw['recordedAt'] as number) : 0,\n recordedBy,\n }\n const copiedFrom = uidOf(raw['copiedFromUid'])\n if (copiedFrom) record.copiedFromUid = copiedFrom\n return record\n}\n\n/** The Google Ads page for a campaign id, opened in whichever account is signed in. */\nexport function googleAdsCampaignUrl(campaignId: string): string {\n return `https://ads.google.com/aw/campaigns?campaignId=${encodeURIComponent(campaignId)}`\n}\n\n/** A sign-in provider id as a person reads it. */\nexport function providerLabel(provider: string | null | undefined): string | null {\n if (!provider) return null\n if (provider === 'password') return 'password'\n if (provider === 'google.com') return 'Google'\n if (provider === 'apple.com') return 'Apple'\n if (provider === 'microsoft.com') return 'Microsoft'\n if (provider === 'github.com') return 'GitHub'\n if (provider.startsWith('saml.') || provider.startsWith('oidc.')) return 'single sign-on'\n return provider\n}\n\nfunction channelPhrase(record: AccountAcquisition): string {\n const from = displayHost(record.referrerHost) || record.source\n const named = record.campaign ?? (record.adCampaignId ? `campaign ${record.adCampaignId}` : null)\n const campaign = named ? `, ${named}` : ''\n switch (record.channel) {\n case 'referral':\n return `Referral from ${from}${campaign}`\n case 'organic-search':\n return `Organic search from ${from}`\n case 'paid-search':\n return `Paid search (${record.source}${campaign})`\n case 'social':\n return `Social from ${from}${campaign}`\n case 'email':\n return `Email (${record.source}${campaign})`\n case 'direct':\n return record.viaHost\n ? `Direct, first seen arriving from ${record.viaHost}`\n : 'Direct'\n default:\n return record.recordedBy === 'backfill'\n ? 'Source unknown — the account predates capture'\n : 'Source unknown — nothing was captured'\n }\n}\n\nfunction doorPhrase(record: AccountAcquisition): string {\n const provider = providerLabel(record.provider)\n switch (record.door) {\n case 'signup-password':\n return 'signed up with password'\n case 'signup-google':\n return 'signed up with Google'\n case 'signup-google-redirect':\n return 'signed up with Google, by redirect'\n case 'invite':\n return provider\n ? `signed up from an invitation, with ${provider}`\n : 'signed up from an invitation'\n case 'sso':\n return 'signed in with single sign-on'\n default:\n return provider ? `created with ${provider}` : 'created'\n }\n}\n\n/**\n * The record as one sentence a staff member reads at a glance:\n * \"Referral from g2.com → /pricing → signed up with password\".\n *\n * The landing is a bare path when it was on `primaryHost` — the platform's own\n * home — and host-and-path anywhere else, so a docs landing says so.\n */\nexport function describeAccountAcquisition(\n record: AccountAcquisition | null | undefined,\n options: { primaryHost?: string | null } = {},\n): string {\n if (!record) return 'No acquisition record'\n const parts = [channelPhrase(record)]\n if (record.landing) {\n const onPrimary =\n options.primaryHost &&\n displayHost(record.landing.host) === displayHost(options.primaryHost)\n parts.push(\n onPrimary ? record.landing.path : `${displayHost(record.landing.host)}${record.landing.path}`,\n )\n }\n parts.push(doorPhrase(record))\n return parts.join(' → ')\n}\n"],"names":["classifyChannel","displayHost","sanitizeFirstTouch","sourceAndMedium","ACCOUNT_ACQUISITION_FIELD","ACQUISITION_DOORS","signUpDoorFor","provider","hint","MAX_LABEL","PROVIDER_SHAPE","RECORDED_BY","label","value","max","clean","replace","trim","slice","doorOf","includes","providerOf","test","toLowerCase","geoOf","raw","geo","country","toUpperCase","region","city","millisOf","Number","isFinite","uidOf","buildAccountAcquisition","input","touch","nowMs","base","door","invitedToOrgId","accountCreatedAt","accountCreatedAtMs","recordedAt","recordedBy","unknownAccountAcquisition","source","medium","v","campaign","utm","channel","capturedAt","at","landing","host","path","referrerHost","ref","viaHost","via","clickIds","click","adCampaignId","adCampaign","organizationAcquisition","creatorRecord","creatorUid","record","readAccountAcquisition","copied","copiedFromUid","utmRaw","key","cleaned","Array","isArray","filter","id","Object","keys","length","copiedFrom","googleAdsCampaignUrl","campaignId","encodeURIComponent","providerLabel","startsWith","channelPhrase","from","named","doorPhrase","describeAccountAcquisition","options","parts","onPrimary","primaryHost","push","join"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
/** `hosts/{hostId}/layouts`. */
|
|
26
|
+
export declare const LAYOUT_LIST_QUERY: ListQueryDeclaration;
|
|
27
|
+
export declare const LAYOUT_LIST_HEADERS: Readonly<Record<string, string>>;
|
|
28
|
+
/** `hosts/{hostId}/components`. */
|
|
29
|
+
export declare const COMPONENT_LIST_QUERY: ListQueryDeclaration;
|
|
30
|
+
export declare const COMPONENT_LIST_HEADERS: Readonly<Record<string, string>>;
|
|
31
|
+
export declare const TEMPLATE_LIST_BASE: readonly ListQueryFilter[];
|
|
32
|
+
export declare const TEMPLATE_LIST_QUERY: ListQueryDeclaration;
|
|
33
|
+
export declare const TEMPLATE_LIST_HEADERS: Readonly<Record<string, string>>;
|
|
34
|
+
/** The template kinds, by the stored `kind`. */
|
|
35
|
+
export declare const TEMPLATE_KIND_OPTIONS: readonly ListFilterOption[];
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
* - Column sorting: the only declared order is the walk.
|
|
62
|
+
*/ /** The walk every artifact list keeps: the document name, ascending. */ export const ARTIFACT_LIST_ORDER = {
|
|
63
|
+
path: LIST_QUERY_ID_PATH,
|
|
64
|
+
direction: 'asc'
|
|
65
|
+
};
|
|
66
|
+
/** The name, by word prefix (`contains`) or whole (`equals`). */ const NAME_FIELD = {
|
|
67
|
+
column: 'displayName',
|
|
68
|
+
kind: 'text',
|
|
69
|
+
path: 'displayName',
|
|
70
|
+
lowerPath: 'nameLower',
|
|
71
|
+
tokensPath: 'nameTokens',
|
|
72
|
+
operators: [
|
|
73
|
+
'contains',
|
|
74
|
+
'equals'
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
/** The document id, which the walk already orders by. */ const ID_FIELD = {
|
|
78
|
+
column: '$id',
|
|
79
|
+
kind: 'id',
|
|
80
|
+
path: LIST_QUERY_ID_PATH
|
|
81
|
+
};
|
|
82
|
+
/** The one range: `updatedAt`, which every writer stamps. */ const UPDATED_FIELD = {
|
|
83
|
+
column: 'updatedAt',
|
|
84
|
+
kind: 'date',
|
|
85
|
+
path: 'updatedAt',
|
|
86
|
+
operators: [
|
|
87
|
+
'is',
|
|
88
|
+
'after',
|
|
89
|
+
'onOrAfter',
|
|
90
|
+
'before',
|
|
91
|
+
'onOrBefore'
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
/** The quick search every artifact list and gallery shelf offers: the name's word prefixes. */ export const ARTIFACT_NAME_SEARCH = {
|
|
95
|
+
tokensPath: 'nameTokens'
|
|
96
|
+
};
|
|
97
|
+
/** `hosts/{hostId}/layouts`. */ export const LAYOUT_LIST_QUERY = {
|
|
98
|
+
fields: [
|
|
99
|
+
NAME_FIELD,
|
|
100
|
+
ID_FIELD,
|
|
101
|
+
UPDATED_FIELD
|
|
102
|
+
],
|
|
103
|
+
sorts: [
|
|
104
|
+
ARTIFACT_LIST_ORDER
|
|
105
|
+
],
|
|
106
|
+
search: ARTIFACT_NAME_SEARCH
|
|
107
|
+
};
|
|
108
|
+
export const LAYOUT_LIST_HEADERS = {
|
|
109
|
+
displayName: 'Display name',
|
|
110
|
+
$id: 'ID',
|
|
111
|
+
updatedAt: 'Updated'
|
|
112
|
+
};
|
|
113
|
+
/** `hosts/{hostId}/components`. */ export const COMPONENT_LIST_QUERY = {
|
|
114
|
+
fields: [
|
|
115
|
+
NAME_FIELD,
|
|
116
|
+
{
|
|
117
|
+
column: 'kind',
|
|
118
|
+
kind: 'exact',
|
|
119
|
+
path: 'kind',
|
|
120
|
+
operators: [
|
|
121
|
+
'equals',
|
|
122
|
+
'isAnyOf'
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
ID_FIELD,
|
|
126
|
+
UPDATED_FIELD
|
|
127
|
+
],
|
|
128
|
+
sorts: [
|
|
129
|
+
ARTIFACT_LIST_ORDER
|
|
130
|
+
],
|
|
131
|
+
search: ARTIFACT_NAME_SEARCH
|
|
132
|
+
};
|
|
133
|
+
export const COMPONENT_LIST_HEADERS = {
|
|
134
|
+
displayName: 'Display name',
|
|
135
|
+
kind: 'Used in',
|
|
136
|
+
$id: 'ID',
|
|
137
|
+
updatedAt: 'Updated'
|
|
138
|
+
};
|
|
139
|
+
/*
|
|
140
|
+
* `hosts/{hostId}/templates`, as the library lists it: one row per template,
|
|
141
|
+
* except that a multi-page STARTER is one row (AGL-696) — its first live page
|
|
142
|
+
* carries `libraryRow: true` and the rest `false`, and a deleted template
|
|
143
|
+
* `false`, so the base clause below is both "one row per starter" and "no
|
|
144
|
+
* tombstones" without a single row being dropped after the read. Every page
|
|
145
|
+
* of a starter carries the starter's name keys, so a search for it finds the
|
|
146
|
+
* row whichever page leads it.
|
|
147
|
+
*/ export const TEMPLATE_LIST_BASE = [
|
|
148
|
+
{
|
|
149
|
+
path: 'libraryRow',
|
|
150
|
+
op: '==',
|
|
151
|
+
value: true
|
|
152
|
+
}
|
|
153
|
+
];
|
|
154
|
+
export const TEMPLATE_LIST_QUERY = {
|
|
155
|
+
fields: [
|
|
156
|
+
NAME_FIELD,
|
|
157
|
+
{
|
|
158
|
+
column: 'kind',
|
|
159
|
+
kind: 'exact',
|
|
160
|
+
path: 'kind',
|
|
161
|
+
operators: [
|
|
162
|
+
'equals',
|
|
163
|
+
'isAnyOf'
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
column: 'source',
|
|
168
|
+
kind: 'exact',
|
|
169
|
+
path: 'source.type',
|
|
170
|
+
operators: [
|
|
171
|
+
'equals',
|
|
172
|
+
'isAnyOf'
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
UPDATED_FIELD
|
|
176
|
+
],
|
|
177
|
+
sorts: [
|
|
178
|
+
ARTIFACT_LIST_ORDER
|
|
179
|
+
],
|
|
180
|
+
search: ARTIFACT_NAME_SEARCH
|
|
181
|
+
};
|
|
182
|
+
export const TEMPLATE_LIST_HEADERS = {
|
|
183
|
+
displayName: 'Display name',
|
|
184
|
+
kind: 'Kind',
|
|
185
|
+
source: 'Source',
|
|
186
|
+
updatedAt: 'Updated'
|
|
187
|
+
};
|
|
188
|
+
/** The template kinds, by the stored `kind`. */ export const TEMPLATE_KIND_OPTIONS = [
|
|
189
|
+
{
|
|
190
|
+
value: 'page',
|
|
191
|
+
label: 'Page'
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
value: 'component',
|
|
195
|
+
label: 'Component'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
value: 'layout',
|
|
199
|
+
label: 'Layout'
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
//# 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 * - Column sorting: the only declared order is the walk.\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/** `hosts/{hostId}/layouts`. */\nexport const LAYOUT_LIST_QUERY: ListQueryDeclaration = {\n fields: [NAME_FIELD, ID_FIELD, UPDATED_FIELD],\n sorts: [ARTIFACT_LIST_ORDER],\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/** `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],\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\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],\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","LAYOUT_LIST_QUERY","fields","sorts","search","LAYOUT_LIST_HEADERS","displayName","$id","updatedAt","COMPONENT_LIST_QUERY","COMPONENT_LIST_HEADERS","TEMPLATE_LIST_BASE","op","value","TEMPLATE_LIST_QUERY","TEMPLATE_LIST_HEADERS","source","TEMPLATE_KIND_OPTIONS","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,kBAAkB,QAIb,sDAAqD;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CC,GAED,sEAAsE,GACtE,OAAO,MAAMC,sBAAqC;IAChDC,MAAMF;IACNG,WAAW;AACb,EAAC;AAED,+DAA+D,GAC/D,MAAMC,aAA8B;IAClCC,QAAQ;IACRC,MAAM;IACNJ,MAAM;IACNK,WAAW;IACXC,YAAY;IACZC,WAAW;QAAC;QAAY;KAAS;AACnC;AAEA,uDAAuD,GACvD,MAAMC,WAA4B;IAChCL,QAAQ;IACRC,MAAM;IACNJ,MAAMF;AACR;AAEA,2DAA2D,GAC3D,MAAMW,gBAAiC;IACrCN,QAAQ;IACRC,MAAM;IACNJ,MAAM;IACNO,WAAW;QAAC;QAAM;QAAS;QAAa;QAAU;KAAa;AACjE;AAEA,6FAA6F,GAC7F,OAAO,MAAMG,uBAAuB;IAAEJ,YAAY;AAAa,EAAC;AAEhE,8BAA8B,GAC9B,OAAO,MAAMK,oBAA0C;IACrDC,QAAQ;QAACV;QAAYM;QAAUC;KAAc;IAC7CI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMK,sBAAwD;IACnEC,aAAa;IACbC,KAAK;IACLC,WAAW;AACb,EAAC;AAED,iCAAiC,GACjC,OAAO,MAAMC,uBAA6C;IACxDP,QAAQ;QACNV;QACA;YAAEC,QAAQ;YAAQC,MAAM;YAASJ,MAAM;YAAQO,WAAW;gBAAC;gBAAU;aAAU;QAAC;QAChFC;QACAC;KACD;IACDI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMU,yBAA2D;IACtEJ,aAAa;IACbZ,MAAM;IACNa,KAAK;IACLC,WAAW;AACb,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMG,qBAAiD;IAC5D;QAAErB,MAAM;QAAcsB,IAAI;QAAMC,OAAO;IAAK;CAC7C,CAAA;AAED,OAAO,MAAMC,sBAA4C;IACvDZ,QAAQ;QACNV;QACA;YAAEC,QAAQ;YAAQC,MAAM;YAASJ,MAAM;YAAQO,WAAW;gBAAC;gBAAU;aAAU;QAAC;QAChF;YACEJ,QAAQ;YACRC,MAAM;YACNJ,MAAM;YACNO,WAAW;gBAAC;gBAAU;aAAU;QAClC;QACAE;KACD;IACDI,OAAO;QAACd;KAAoB;IAC5Be,QAAQJ;AACV,EAAC;AAED,OAAO,MAAMe,wBAA0D;IACrET,aAAa;IACbZ,MAAM;IACNsB,QAAQ;IACRR,WAAW;AACb,EAAC;AAED,8CAA8C,GAC9C,OAAO,MAAMS,wBAAqD;IAChE;QAAEJ,OAAO;QAAQK,OAAO;IAAO;IAC/B;QAAEL,OAAO;QAAaK,OAAO;IAAY;IACzC;QAAEL,OAAO;QAAUK,OAAO;IAAS;CACpC,CAAA"}
|