@aglyn/aglyn 1.0.0-beta.264 → 1.0.0-beta.268
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 +12 -12
- package/src/lib/app-utils/account-acquisition.d.ts +13 -0
- package/src/lib/app-utils/account-acquisition.js +84 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +70 -0
- package/src/lib/app-utils/analytics-events.js +8 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +4 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +8 -1
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +31 -10
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.js +14 -7
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +120 -8
- package/src/lib/app-utils/health-report.js +138 -3
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/live-document-count.client.d.ts +28 -0
- package/src/lib/app-utils/live-document-count.client.js +35 -0
- package/src/lib/app-utils/live-document-count.client.js.map +1 -0
- package/src/lib/app-utils/live-document-count.d.ts +91 -0
- package/src/lib/app-utils/live-document-count.js +81 -0
- package/src/lib/app-utils/live-document-count.js.map +1 -0
- package/src/lib/app-utils/notification-sounds.d.ts +49 -0
- package/src/lib/app-utils/notification-sounds.js +54 -0
- package/src/lib/app-utils/notification-sounds.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +2 -0
- package/src/lib/app-utils/notifications.js +38 -10
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/screen-analytics-aggregate.d.ts +10 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js +11 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
- package/src/lib/app-utils/upload-inspection.d.ts +9 -0
- package/src/lib/app-utils/upload-inspection.js +27 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +6 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +14 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +30 -3
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-erase.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-erase.js +56 -0
- package/src/lib/plugin-manager/plugin-site-erase.js.map +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
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 Query } from 'firebase/firestore';
|
|
18
|
+
/**
|
|
19
|
+
* The web SDK's half of the live-count rule (AGL-3918): how many of
|
|
20
|
+
* [documents] are not deleted, as `/api/hosts/resources` counts them for a
|
|
21
|
+
* plan allowance. Every document less the tombstones (`deletedAt != null`);
|
|
22
|
+
* an archived document is live and counted. See `live-document-count.ts` for
|
|
23
|
+
* why it is two aggregates.
|
|
24
|
+
*
|
|
25
|
+
* Rejects when either aggregate is refused, so a caller keeps whatever
|
|
26
|
+
* fallback it already has rather than reading a confident 0.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getLiveCountFromServer(documents: Query): Promise<number>;
|
|
@@ -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
|
+
*/ import { getCountFromServer, query, where } from "firebase/firestore";
|
|
17
|
+
import { liveDocumentCount, TOMBSTONE_FIELD } from "./live-document-count.js";
|
|
18
|
+
/**
|
|
19
|
+
* The web SDK's half of the live-count rule (AGL-3918): how many of
|
|
20
|
+
* [documents] are not deleted, as `/api/hosts/resources` counts them for a
|
|
21
|
+
* plan allowance. Every document less the tombstones (`deletedAt != null`);
|
|
22
|
+
* an archived document is live and counted. See `live-document-count.ts` for
|
|
23
|
+
* why it is two aggregates.
|
|
24
|
+
*
|
|
25
|
+
* Rejects when either aggregate is refused, so a caller keeps whatever
|
|
26
|
+
* fallback it already has rather than reading a confident 0.
|
|
27
|
+
*/ export async function getLiveCountFromServer(documents) {
|
|
28
|
+
const [total, tombstones] = await Promise.all([
|
|
29
|
+
getCountFromServer(documents),
|
|
30
|
+
getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null)))
|
|
31
|
+
]);
|
|
32
|
+
return liveDocumentCount(total.data().count, tombstones.data().count);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=live-document-count.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.client.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 { getCountFromServer, query, where, type Query } from 'firebase/firestore'\nimport { liveDocumentCount, TOMBSTONE_FIELD } from './live-document-count'\n\n/**\n * The web SDK's half of the live-count rule (AGL-3918): how many of\n * [documents] are not deleted, as `/api/hosts/resources` counts them for a\n * plan allowance. Every document less the tombstones (`deletedAt != null`);\n * an archived document is live and counted. See `live-document-count.ts` for\n * why it is two aggregates.\n *\n * Rejects when either aggregate is refused, so a caller keeps whatever\n * fallback it already has rather than reading a confident 0.\n */\nexport async function getLiveCountFromServer(documents: Query): Promise<number> {\n const [total, tombstones] = await Promise.all([\n getCountFromServer(documents),\n getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null))),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n"],"names":["getCountFromServer","query","where","liveDocumentCount","TOMBSTONE_FIELD","getLiveCountFromServer","documents","total","tombstones","Promise","all","data","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,EAAEC,KAAK,EAAEC,KAAK,QAAoB,qBAAoB;AACjF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,2BAAuB;AAE1E;;;;;;;;;CASC,GACD,OAAO,eAAeC,uBAAuBC,SAAgB;IAC3D,MAAM,CAACC,OAAOC,WAAW,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC5CV,mBAAmBM;QACnBN,mBAAmBC,MAAMK,WAAWJ,MAAME,iBAAiB,MAAM;KAClE;IACD,OAAOD,kBAAkBI,MAAMI,IAAI,GAAGC,KAAK,EAAEJ,WAAWG,IAAI,GAAGC,KAAK;AACtE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* How a plan allowance counts a collection whose delete is a SOFT delete
|
|
19
|
+
* (AGL-3918).
|
|
20
|
+
*
|
|
21
|
+
* The rule: a deleted document frees its slot, an archived one keeps it.
|
|
22
|
+
* Deleting stamps `deletedAt` and leaves the document in place (a tenant page
|
|
23
|
+
* keeps grafting until its next revalidate, a past order still names the
|
|
24
|
+
* product), so a plain `count()` charged a site for everything it had ever
|
|
25
|
+
* deleted: a product deleted on Starter never gave its slot back, and the
|
|
26
|
+
* Automation card read "4/25 workflows" beside a list of three. Archiving is a
|
|
27
|
+
* status the owner can reverse in place, so an archived product or a disabled
|
|
28
|
+
* workflow is still counted.
|
|
29
|
+
*
|
|
30
|
+
* ## Why total minus tombstones
|
|
31
|
+
*
|
|
32
|
+
* A live document has no `deletedAt` at all (created through
|
|
33
|
+
* `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
|
|
34
|
+
* writer, a marketplace install). Firestore cannot match an absent field, so
|
|
35
|
+
* `where('deletedAt', '==', null)` counts the second shape alone. The
|
|
36
|
+
* TOMBSTONES are one shape — a value that is not `null` — and
|
|
37
|
+
* `where('deletedAt', '!=', null)` matches exactly them, so the total less
|
|
38
|
+
* that count is exactly the documents the server calls live. Two aggregates,
|
|
39
|
+
* one billed read each at these sizes, on the automatic single-field index
|
|
40
|
+
* `deletedAt` already has; no composite index.
|
|
41
|
+
*
|
|
42
|
+
* This module is SDK-agnostic: it names the Admin SDK's query shape
|
|
43
|
+
* structurally so a plugin, core and the console reach it without a second
|
|
44
|
+
* copy of the rule. The web SDK's counterpart is
|
|
45
|
+
* `live-document-count.client.ts`.
|
|
46
|
+
*/
|
|
47
|
+
/** The field every soft-deleting collection stamps on delete. */
|
|
48
|
+
export declare const TOMBSTONE_FIELD = "deletedAt";
|
|
49
|
+
/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */
|
|
50
|
+
export declare function liveDocumentCount(total: number, tombstones: number): number;
|
|
51
|
+
/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */
|
|
52
|
+
export declare function isLiveDocument(deletedAt: unknown): boolean;
|
|
53
|
+
/** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */
|
|
54
|
+
export interface LiveCountSnapshot {
|
|
55
|
+
data(): {
|
|
56
|
+
count: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** An aggregate query the Admin SDK can execute directly. */
|
|
60
|
+
export interface LiveCountAggregate {
|
|
61
|
+
get(): Promise<LiveCountSnapshot>;
|
|
62
|
+
}
|
|
63
|
+
/** A collection or query the Admin SDK can count and filter. */
|
|
64
|
+
export interface LiveCountQuery<A extends LiveCountAggregate> {
|
|
65
|
+
count(): A;
|
|
66
|
+
where(field: string, op: '!=', value: null): {
|
|
67
|
+
count(): A;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The live documents [documents] matches: every one less the tombstones.
|
|
72
|
+
*
|
|
73
|
+
* `read` executes each aggregate, and defaults to executing it directly. A
|
|
74
|
+
* create that counts and writes in one transaction passes
|
|
75
|
+
* `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
|
|
76
|
+
* reads and the lock they take serializes a concurrent create (AGL-2231).
|
|
77
|
+
*/
|
|
78
|
+
export declare function countLiveDocuments<A extends LiveCountAggregate>(documents: LiveCountQuery<A>, read?: (aggregate: A) => Promise<LiveCountSnapshot>): Promise<number>;
|
|
79
|
+
/** The `source.type` a platform-seeded starter template carries (AGL-687). */
|
|
80
|
+
export declare const STARTER_TEMPLATE_SOURCE = "starter";
|
|
81
|
+
/**
|
|
82
|
+
* Of a site's deleted templates, how many `templatesPerHost`'s first count
|
|
83
|
+
* included: `where('source.type', '!=', 'starter')` matches a template that
|
|
84
|
+
* carries a `source.type` other than a starter's, and skips one without the
|
|
85
|
+
* field. Subtracting these from that count leaves the live templates the
|
|
86
|
+
* allowance counts, without the composite index a second inequality on
|
|
87
|
+
* `deletedAt` beside it would need.
|
|
88
|
+
*/
|
|
89
|
+
export declare function deletedTemplatesCounted(tombstones: ReadonlyArray<{
|
|
90
|
+
get(field: string): unknown;
|
|
91
|
+
}>): number;
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
* How a plan allowance counts a collection whose delete is a SOFT delete
|
|
18
|
+
* (AGL-3918).
|
|
19
|
+
*
|
|
20
|
+
* The rule: a deleted document frees its slot, an archived one keeps it.
|
|
21
|
+
* Deleting stamps `deletedAt` and leaves the document in place (a tenant page
|
|
22
|
+
* keeps grafting until its next revalidate, a past order still names the
|
|
23
|
+
* product), so a plain `count()` charged a site for everything it had ever
|
|
24
|
+
* deleted: a product deleted on Starter never gave its slot back, and the
|
|
25
|
+
* Automation card read "4/25 workflows" beside a list of three. Archiving is a
|
|
26
|
+
* status the owner can reverse in place, so an archived product or a disabled
|
|
27
|
+
* workflow is still counted.
|
|
28
|
+
*
|
|
29
|
+
* ## Why total minus tombstones
|
|
30
|
+
*
|
|
31
|
+
* A live document has no `deletedAt` at all (created through
|
|
32
|
+
* `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
|
|
33
|
+
* writer, a marketplace install). Firestore cannot match an absent field, so
|
|
34
|
+
* `where('deletedAt', '==', null)` counts the second shape alone. The
|
|
35
|
+
* TOMBSTONES are one shape — a value that is not `null` — and
|
|
36
|
+
* `where('deletedAt', '!=', null)` matches exactly them, so the total less
|
|
37
|
+
* that count is exactly the documents the server calls live. Two aggregates,
|
|
38
|
+
* one billed read each at these sizes, on the automatic single-field index
|
|
39
|
+
* `deletedAt` already has; no composite index.
|
|
40
|
+
*
|
|
41
|
+
* This module is SDK-agnostic: it names the Admin SDK's query shape
|
|
42
|
+
* structurally so a plugin, core and the console reach it without a second
|
|
43
|
+
* copy of the rule. The web SDK's counterpart is
|
|
44
|
+
* `live-document-count.client.ts`.
|
|
45
|
+
*/ /** The field every soft-deleting collection stamps on delete. */ export const TOMBSTONE_FIELD = 'deletedAt';
|
|
46
|
+
/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */ export function liveDocumentCount(total, tombstones) {
|
|
47
|
+
return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0));
|
|
48
|
+
}
|
|
49
|
+
/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */ export function isLiveDocument(deletedAt) {
|
|
50
|
+
return deletedAt == null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The live documents [documents] matches: every one less the tombstones.
|
|
54
|
+
*
|
|
55
|
+
* `read` executes each aggregate, and defaults to executing it directly. A
|
|
56
|
+
* create that counts and writes in one transaction passes
|
|
57
|
+
* `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
|
|
58
|
+
* reads and the lock they take serializes a concurrent create (AGL-2231).
|
|
59
|
+
*/ export async function countLiveDocuments(documents, read = (aggregate)=>aggregate.get()) {
|
|
60
|
+
const [total, tombstones] = await Promise.all([
|
|
61
|
+
read(documents.count()),
|
|
62
|
+
read(documents.where(TOMBSTONE_FIELD, '!=', null).count())
|
|
63
|
+
]);
|
|
64
|
+
return liveDocumentCount(total.data().count, tombstones.data().count);
|
|
65
|
+
}
|
|
66
|
+
/** The `source.type` a platform-seeded starter template carries (AGL-687). */ export const STARTER_TEMPLATE_SOURCE = 'starter';
|
|
67
|
+
/**
|
|
68
|
+
* Of a site's deleted templates, how many `templatesPerHost`'s first count
|
|
69
|
+
* included: `where('source.type', '!=', 'starter')` matches a template that
|
|
70
|
+
* carries a `source.type` other than a starter's, and skips one without the
|
|
71
|
+
* field. Subtracting these from that count leaves the live templates the
|
|
72
|
+
* allowance counts, without the composite index a second inequality on
|
|
73
|
+
* `deletedAt` beside it would need.
|
|
74
|
+
*/ export function deletedTemplatesCounted(tombstones) {
|
|
75
|
+
return tombstones.filter((entry)=>{
|
|
76
|
+
const type = entry.get('source.type');
|
|
77
|
+
return type != null && type !== STARTER_TEMPLATE_SOURCE;
|
|
78
|
+
}).length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=live-document-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.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\n/**\n * How a plan allowance counts a collection whose delete is a SOFT delete\n * (AGL-3918).\n *\n * The rule: a deleted document frees its slot, an archived one keeps it.\n * Deleting stamps `deletedAt` and leaves the document in place (a tenant page\n * keeps grafting until its next revalidate, a past order still names the\n * product), so a plain `count()` charged a site for everything it had ever\n * deleted: a product deleted on Starter never gave its slot back, and the\n * Automation card read \"4/25 workflows\" beside a list of three. Archiving is a\n * status the owner can reverse in place, so an archived product or a disabled\n * workflow is still counted.\n *\n * ## Why total minus tombstones\n *\n * A live document has no `deletedAt` at all (created through\n * `/api/hosts/resources`) or an explicit `deletedAt: null` (every product\n * writer, a marketplace install). Firestore cannot match an absent field, so\n * `where('deletedAt', '==', null)` counts the second shape alone. The\n * TOMBSTONES are one shape — a value that is not `null` — and\n * `where('deletedAt', '!=', null)` matches exactly them, so the total less\n * that count is exactly the documents the server calls live. Two aggregates,\n * one billed read each at these sizes, on the automatic single-field index\n * `deletedAt` already has; no composite index.\n *\n * This module is SDK-agnostic: it names the Admin SDK's query shape\n * structurally so a plugin, core and the console reach it without a second\n * copy of the rule. The web SDK's counterpart is\n * `live-document-count.client.ts`.\n */\n\n/** The field every soft-deleting collection stamps on delete. */\nexport const TOMBSTONE_FIELD = 'deletedAt'\n\n/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */\nexport function liveDocumentCount(total: number, tombstones: number): number {\n return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0))\n}\n\n/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */\nexport function isLiveDocument(deletedAt: unknown): boolean {\n return deletedAt == null\n}\n\n/** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */\nexport interface LiveCountSnapshot {\n data(): { count: number }\n}\n\n/** An aggregate query the Admin SDK can execute directly. */\nexport interface LiveCountAggregate {\n get(): Promise<LiveCountSnapshot>\n}\n\n/** A collection or query the Admin SDK can count and filter. */\nexport interface LiveCountQuery<A extends LiveCountAggregate> {\n count(): A\n where(field: string, op: '!=', value: null): { count(): A }\n}\n\n/**\n * The live documents [documents] matches: every one less the tombstones.\n *\n * `read` executes each aggregate, and defaults to executing it directly. A\n * create that counts and writes in one transaction passes\n * `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's\n * reads and the lock they take serializes a concurrent create (AGL-2231).\n */\nexport async function countLiveDocuments<A extends LiveCountAggregate>(\n documents: LiveCountQuery<A>,\n read: (aggregate: A) => Promise<LiveCountSnapshot> = (aggregate) => aggregate.get(),\n): Promise<number> {\n const [total, tombstones] = await Promise.all([\n read(documents.count()),\n read(documents.where(TOMBSTONE_FIELD, '!=', null).count()),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n\n/** The `source.type` a platform-seeded starter template carries (AGL-687). */\nexport const STARTER_TEMPLATE_SOURCE = 'starter'\n\n/**\n * Of a site's deleted templates, how many `templatesPerHost`'s first count\n * included: `where('source.type', '!=', 'starter')` matches a template that\n * carries a `source.type` other than a starter's, and skips one without the\n * field. Subtracting these from that count leaves the live templates the\n * allowance counts, without the composite index a second inequality on\n * `deletedAt` beside it would need.\n */\nexport function deletedTemplatesCounted(\n tombstones: ReadonlyArray<{ get(field: string): unknown }>,\n): number {\n return tombstones.filter((entry) => {\n const type = entry.get('source.type')\n return type != null && type !== STARTER_TEMPLATE_SOURCE\n }).length\n}\n"],"names":["TOMBSTONE_FIELD","liveDocumentCount","total","tombstones","Math","max","Number","isLiveDocument","deletedAt","countLiveDocuments","documents","read","aggregate","get","Promise","all","count","where","data","STARTER_TEMPLATE_SOURCE","deletedTemplatesCounted","filter","entry","type","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,+DAA+D,GAC/D,OAAO,MAAMA,kBAAkB,YAAW;AAE1C,yJAAyJ,GACzJ,OAAO,SAASC,kBAAkBC,KAAa,EAAEC,UAAkB;IACjE,OAAOC,KAAKC,GAAG,CAAC,GAAG,AAACC,CAAAA,OAAOJ,UAAU,CAAA,IAAMI,CAAAA,OAAOH,eAAe,CAAA;AACnE;AAEA,6EAA6E,GAC7E,OAAO,SAASI,eAAeC,SAAkB;IAC/C,OAAOA,aAAa;AACtB;AAkBA;;;;;;;CAOC,GACD,OAAO,eAAeC,mBACpBC,SAA4B,EAC5BC,OAAqD,CAACC,YAAcA,UAAUC,GAAG,EAAE;IAEnF,MAAM,CAACX,OAAOC,WAAW,GAAG,MAAMW,QAAQC,GAAG,CAAC;QAC5CJ,KAAKD,UAAUM,KAAK;QACpBL,KAAKD,UAAUO,KAAK,CAACjB,iBAAiB,MAAM,MAAMgB,KAAK;KACxD;IACD,OAAOf,kBAAkBC,MAAMgB,IAAI,GAAGF,KAAK,EAAEb,WAAWe,IAAI,GAAGF,KAAK;AACtE;AAEA,4EAA4E,GAC5E,OAAO,MAAMG,0BAA0B,UAAS;AAEhD;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdjB,UAA0D;IAE1D,OAAOA,WAAWkB,MAAM,CAAC,CAACC;QACxB,MAAMC,OAAOD,MAAMT,GAAG,CAAC;QACvB,OAAOU,QAAQ,QAAQA,SAASJ;IAClC,GAAGK,MAAM;AACX"}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The notification sounds a person picks from (Notification settings), the
|
|
19
|
+
* same on every surface: the console's chime, the APNs `sound` the Apple apps
|
|
20
|
+
* play and the Android notification channel.
|
|
21
|
+
*
|
|
22
|
+
* NO IMPORTS, on purpose: the native apps read this list through the
|
|
23
|
+
* generated contracts, so it must stay a plain module.
|
|
24
|
+
*
|
|
25
|
+
* The audio is synthesized by `tools/notification-sounds/generate.py`, which
|
|
26
|
+
* writes each sound to the three paths named here; the two lists name the
|
|
27
|
+
* same ids.
|
|
28
|
+
*/
|
|
29
|
+
/** `users/{uid}.notificationSound`: the id of the sound the person picked. */
|
|
30
|
+
export declare const NOTIFICATION_SOUND_FIELD = "notificationSound";
|
|
31
|
+
export interface NotificationSound {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** The console's file, served from `apps/console/public`. */
|
|
35
|
+
web: string;
|
|
36
|
+
/** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */
|
|
37
|
+
apple: string;
|
|
38
|
+
/**
|
|
39
|
+
* The Android raw resource and the notification channel that plays it. A
|
|
40
|
+
* channel's sound cannot change once created, so each sound has its own.
|
|
41
|
+
*/
|
|
42
|
+
android: string;
|
|
43
|
+
}
|
|
44
|
+
/** In the order the picker lists them; the first is the default. */
|
|
45
|
+
export declare const NOTIFICATION_SOUNDS: readonly NotificationSound[];
|
|
46
|
+
/** What a person hears before they pick one. */
|
|
47
|
+
export declare const DEFAULT_NOTIFICATION_SOUND = "xylophone";
|
|
48
|
+
/** The stored pick as a sound, the default for no pick or an id no longer listed. */
|
|
49
|
+
export declare function notificationSound(value: unknown): NotificationSound;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* The notification sounds a person picks from (Notification settings), the
|
|
18
|
+
* same on every surface: the console's chime, the APNs `sound` the Apple apps
|
|
19
|
+
* play and the Android notification channel.
|
|
20
|
+
*
|
|
21
|
+
* NO IMPORTS, on purpose: the native apps read this list through the
|
|
22
|
+
* generated contracts, so it must stay a plain module.
|
|
23
|
+
*
|
|
24
|
+
* The audio is synthesized by `tools/notification-sounds/generate.py`, which
|
|
25
|
+
* writes each sound to the three paths named here; the two lists name the
|
|
26
|
+
* same ids.
|
|
27
|
+
*/ /** `users/{uid}.notificationSound`: the id of the sound the person picked. */ export const NOTIFICATION_SOUND_FIELD = 'notificationSound';
|
|
28
|
+
function sound(id, label) {
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
label,
|
|
32
|
+
web: `/sounds/notifications/${id}.wav`,
|
|
33
|
+
apple: `aglyn-${id}.caf`,
|
|
34
|
+
android: `aglyn_sound_${id}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** In the order the picker lists them; the first is the default. */ export const NOTIFICATION_SOUNDS = [
|
|
38
|
+
sound('xylophone', 'Xylophone'),
|
|
39
|
+
sound('signature', 'Signature'),
|
|
40
|
+
sound('tada', 'Ta-da'),
|
|
41
|
+
sound('plink', 'Plink'),
|
|
42
|
+
sound('arcade', 'Arcade'),
|
|
43
|
+
sound('cowbell', 'Cowbell'),
|
|
44
|
+
sound('bubbles', 'Bubbles'),
|
|
45
|
+
sound('steel_pan', 'Steel pan'),
|
|
46
|
+
sound('cash_register', 'Cash register')
|
|
47
|
+
];
|
|
48
|
+
/** What a person hears before they pick one. */ export const DEFAULT_NOTIFICATION_SOUND = 'xylophone';
|
|
49
|
+
/** The stored pick as a sound, the default for no pick or an id no longer listed. */ export function notificationSound(value) {
|
|
50
|
+
var _NOTIFICATION_SOUNDS_find;
|
|
51
|
+
return (_NOTIFICATION_SOUNDS_find = NOTIFICATION_SOUNDS.find((entry)=>entry.id === value)) != null ? _NOTIFICATION_SOUNDS_find : NOTIFICATION_SOUNDS.find((entry)=>entry.id === DEFAULT_NOTIFICATION_SOUND);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=notification-sounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/notification-sounds.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\n/**\n * The notification sounds a person picks from (Notification settings), the\n * same on every surface: the console's chime, the APNs `sound` the Apple apps\n * play and the Android notification channel.\n *\n * NO IMPORTS, on purpose: the native apps read this list through the\n * generated contracts, so it must stay a plain module.\n *\n * The audio is synthesized by `tools/notification-sounds/generate.py`, which\n * writes each sound to the three paths named here; the two lists name the\n * same ids.\n */\n\n/** `users/{uid}.notificationSound`: the id of the sound the person picked. */\nexport const NOTIFICATION_SOUND_FIELD = 'notificationSound'\n\nexport interface NotificationSound {\n id: string\n label: string\n /** The console's file, served from `apps/console/public`. */\n web: string\n /** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */\n apple: string\n /**\n * The Android raw resource and the notification channel that plays it. A\n * channel's sound cannot change once created, so each sound has its own.\n */\n android: string\n}\n\nfunction sound(id: string, label: string): NotificationSound {\n return {\n id,\n label,\n web: `/sounds/notifications/${id}.wav`,\n apple: `aglyn-${id}.caf`,\n android: `aglyn_sound_${id}`,\n }\n}\n\n/** In the order the picker lists them; the first is the default. */\nexport const NOTIFICATION_SOUNDS: readonly NotificationSound[] = [\n sound('xylophone', 'Xylophone'),\n sound('signature', 'Signature'),\n sound('tada', 'Ta-da'),\n sound('plink', 'Plink'),\n sound('arcade', 'Arcade'),\n sound('cowbell', 'Cowbell'),\n sound('bubbles', 'Bubbles'),\n sound('steel_pan', 'Steel pan'),\n sound('cash_register', 'Cash register'),\n]\n\n/** What a person hears before they pick one. */\nexport const DEFAULT_NOTIFICATION_SOUND = 'xylophone'\n\n/** The stored pick as a sound, the default for no pick or an id no longer listed. */\nexport function notificationSound(value: unknown): NotificationSound {\n return (\n NOTIFICATION_SOUNDS.find((entry) => entry.id === value) ??\n (NOTIFICATION_SOUNDS.find((entry) => entry.id === DEFAULT_NOTIFICATION_SOUND) as NotificationSound)\n )\n}\n"],"names":["NOTIFICATION_SOUND_FIELD","sound","id","label","web","apple","android","NOTIFICATION_SOUNDS","DEFAULT_NOTIFICATION_SOUND","notificationSound","value","find","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,4EAA4E,GAC5E,OAAO,MAAMA,2BAA2B,oBAAmB;AAgB3D,SAASC,MAAMC,EAAU,EAAEC,KAAa;IACtC,OAAO;QACLD;QACAC;QACAC,KAAK,CAAC,sBAAsB,EAAEF,GAAG,IAAI,CAAC;QACtCG,OAAO,CAAC,MAAM,EAAEH,GAAG,IAAI,CAAC;QACxBI,SAAS,CAAC,YAAY,EAAEJ,IAAI;IAC9B;AACF;AAEA,kEAAkE,GAClE,OAAO,MAAMK,sBAAoD;IAC/DN,MAAM,aAAa;IACnBA,MAAM,aAAa;IACnBA,MAAM,QAAQ;IACdA,MAAM,SAAS;IACfA,MAAM,UAAU;IAChBA,MAAM,WAAW;IACjBA,MAAM,WAAW;IACjBA,MAAM,aAAa;IACnBA,MAAM,iBAAiB;CACxB,CAAA;AAED,8CAA8C,GAC9C,OAAO,MAAMO,6BAA6B,YAAW;AAErD,mFAAmF,GACnF,OAAO,SAASC,kBAAkBC,KAAc;QAE5CH;IADF,QACEA,4BAAAA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKQ,kBAAjDH,4BACCA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKM;AAEtD"}
|
|
@@ -349,6 +349,8 @@ export declare const NOTIFICATION_CHANNEL_DEFAULTS: Readonly<Record<Notification
|
|
|
349
349
|
* see {@link NOTIFICATION_SELF_SENT_EMAIL_TYPES} and the `skipEmail` callers.
|
|
350
350
|
*/
|
|
351
351
|
export declare const NOTIFICATION_TYPE_CHANNEL_DEFAULTS: Partial<Record<AglynNotificationType, Partial<Record<NotificationChannel, boolean>>>>;
|
|
352
|
+
/** The words on a notification email's button: the record it opens, else "Open in {product}". */
|
|
353
|
+
export declare function notificationEmailAction(type: AglynNotificationType | string, productName: string): string;
|
|
352
354
|
/** What a type does on a channel when nobody has answered for it or its category. */
|
|
353
355
|
export declare function notificationTypeChannelDefault(type: AglynNotificationType | string, channel: NotificationChannel): boolean;
|
|
354
356
|
/**
|
|
@@ -300,7 +300,7 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
|
|
|
300
300
|
/**
|
|
301
301
|
* What a category does when nobody has said otherwise.
|
|
302
302
|
*
|
|
303
|
-
* Console on, email OFF, for every category
|
|
303
|
+
* Console on, email OFF, for every category but `staff`. The types a person needs to hear
|
|
304
304
|
* about without opening the console are the exception, by type, in
|
|
305
305
|
* {@link NOTIFICATION_TYPE_CHANNEL_DEFAULTS}. Email defaults off because the inbox is
|
|
306
306
|
* not ours to fill: the product already sends transactional mail nobody opted
|
|
@@ -335,13 +335,13 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
|
|
|
335
335
|
console: true,
|
|
336
336
|
email: false
|
|
337
337
|
},
|
|
338
|
-
// Console ON
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
338
|
+
// Console and email ON: staff are the platform's own people, every one of
|
|
339
|
+
// them is meant to hear about growth and money moving, and an account that
|
|
340
|
+
// never opened its settings heard about almost none of it. The one category
|
|
341
|
+
// whose recipients are all us, so the inbox reasoning above does not apply.
|
|
342
342
|
staff: {
|
|
343
343
|
console: true,
|
|
344
|
-
email:
|
|
344
|
+
email: true
|
|
345
345
|
}
|
|
346
346
|
};
|
|
347
347
|
export const NOTIFICATION_CHANNEL_DEFAULTS = inCategoryOrder(CORE_CHANNEL_DEFAULTS, (declaration)=>_extends({}, declaration.defaults));
|
|
@@ -437,12 +437,40 @@ export const NOTIFICATION_CHANNEL_DEFAULTS = inCategoryOrder(CORE_CHANNEL_DEFAUL
|
|
|
437
437
|
},
|
|
438
438
|
'system.disputeUnattributed': {
|
|
439
439
|
email: true
|
|
440
|
-
},
|
|
441
|
-
// Staff hear when somebody starts building.
|
|
442
|
-
'staff.siteCreated': {
|
|
443
|
-
email: true
|
|
444
440
|
}
|
|
445
441
|
};
|
|
442
|
+
/**
|
|
443
|
+
* What a notification email's button says it opens (AGL-3897), by type: the
|
|
444
|
+
* record the notification links to, named, rather than "Open in Aglyn" on
|
|
445
|
+
* every one. A type with no entry keeps that general label.
|
|
446
|
+
*/ const NOTIFICATION_EMAIL_ACTIONS = {
|
|
447
|
+
'content.formSubmission': 'View submission',
|
|
448
|
+
'content.booking': 'View booking',
|
|
449
|
+
'content.order': 'View order',
|
|
450
|
+
'content.lowStock': 'View product',
|
|
451
|
+
'content.taskAssigned': 'Open task',
|
|
452
|
+
'content.taskReminder': 'Open task',
|
|
453
|
+
'content.contactAssigned': 'Open contact',
|
|
454
|
+
'content.leadAssigned': 'Open lead',
|
|
455
|
+
'billing.invoice': 'Open billing',
|
|
456
|
+
'billing.paymentFailed': 'Update payment method',
|
|
457
|
+
'billing.subscriptionCanceled': 'Open billing',
|
|
458
|
+
'billing.usage': 'Open billing',
|
|
459
|
+
'support.ticketOpened': 'Open ticket',
|
|
460
|
+
'support.ticketReply': 'Open ticket',
|
|
461
|
+
'marketplace.review': 'Open listing',
|
|
462
|
+
'staff.userSignedUp': 'Open account',
|
|
463
|
+
'staff.orgCreated': 'Open workspace',
|
|
464
|
+
'staff.siteCreated': 'Open site',
|
|
465
|
+
'staff.subscriptionStarted': 'Open workspace',
|
|
466
|
+
'staff.subscriptionCanceled': 'Open workspace',
|
|
467
|
+
'staff.planChanged': 'Open workspace',
|
|
468
|
+
'staff.paymentFailed': 'Open workspace'
|
|
469
|
+
};
|
|
470
|
+
/** The words on a notification email's button: the record it opens, else "Open in {product}". */ export function notificationEmailAction(type, productName) {
|
|
471
|
+
var _NOTIFICATION_EMAIL_ACTIONS_type;
|
|
472
|
+
return (_NOTIFICATION_EMAIL_ACTIONS_type = NOTIFICATION_EMAIL_ACTIONS[type]) != null ? _NOTIFICATION_EMAIL_ACTIONS_type : `Open in ${productName}`;
|
|
473
|
+
}
|
|
446
474
|
/** What a type does on a channel when nobody has answered for it or its category. */ export function notificationTypeChannelDefault(type, channel) {
|
|
447
475
|
var _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type;
|
|
448
476
|
const own = (_NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type = NOTIFICATION_TYPE_CHANNEL_DEFAULTS[type]) == null ? void 0 : _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type[channel];
|