@aglyn/plugins-marketplace 1.0.0-beta.199 → 1.0.0-beta.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/marketplace-browse.component.js +104 -169
  3. package/src/lib/components/marketplace-browse.component.js.map +1 -1
  4. package/src/lib/components/marketplace-reports-page.component.js +9 -0
  5. package/src/lib/components/marketplace-reports-page.component.js.map +1 -1
  6. package/src/lib/components/org-licences-panel.component.js +212 -170
  7. package/src/lib/components/org-licences-panel.component.js.map +1 -1
  8. package/src/lib/components/org-seller-panel.component.js +23 -5
  9. package/src/lib/components/org-seller-panel.component.js.map +1 -1
  10. package/src/lib/components/plugin-reviews-queue.component.d.ts +9 -0
  11. package/src/lib/components/plugin-reviews-queue.component.js +190 -114
  12. package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
  13. package/src/lib/hooks/use-listing-name-lookup.d.ts +34 -0
  14. package/src/lib/hooks/use-listing-name-lookup.js +131 -0
  15. package/src/lib/hooks/use-listing-name-lookup.js.map +1 -0
  16. package/src/lib/model/listing-query.d.ts +140 -0
  17. package/src/lib/model/listing-query.js +393 -0
  18. package/src/lib/model/listing-query.js.map +1 -0
  19. package/src/lib/model/marketplace.d.ts +11 -0
  20. package/src/lib/model/marketplace.js.map +1 -1
  21. package/src/lib/server/admin-reports.js +30 -12
  22. package/src/lib/server/admin-reports.js.map +1 -1
  23. package/src/lib/server/admin-reviews.js +180 -128
  24. package/src/lib/server/admin-reviews.js.map +1 -1
  25. package/src/lib/server/admin-trust.js +2 -1
  26. package/src/lib/server/admin-trust.js.map +1 -1
  27. package/src/lib/server/billing-webhook.js +13 -1
  28. package/src/lib/server/billing-webhook.js.map +1 -1
  29. package/src/lib/server/checkout.js +3 -1
  30. package/src/lib/server/checkout.js.map +1 -1
  31. package/src/lib/server/install-email-starter.js +9 -2
  32. package/src/lib/server/install-email-starter.js.map +1 -1
  33. package/src/lib/server/install-layout.js +17 -5
  34. package/src/lib/server/install-layout.js.map +1 -1
  35. package/src/lib/server/install-template.js +15 -5
  36. package/src/lib/server/install-template.js.map +1 -1
  37. package/src/lib/server/install.js +10 -2
  38. package/src/lib/server/install.js.map +1 -1
  39. package/src/lib/server/listing-query-fields.d.ts +38 -0
  40. package/src/lib/server/listing-query-fields.js +50 -0
  41. package/src/lib/server/listing-query-fields.js.map +1 -0
  42. package/src/lib/server/publish-dataset-schema.js +6 -2
  43. package/src/lib/server/publish-dataset-schema.js.map +1 -1
  44. package/src/lib/server/publish-email-starter.js +6 -2
  45. package/src/lib/server/publish-email-starter.js.map +1 -1
  46. package/src/lib/server/publish-email-template.js +6 -2
  47. package/src/lib/server/publish-email-template.js.map +1 -1
  48. package/src/lib/server/publish-layout.js +6 -2
  49. package/src/lib/server/publish-layout.js.map +1 -1
  50. package/src/lib/server/publish-plugin.js +75 -7
  51. package/src/lib/server/publish-plugin.js.map +1 -1
  52. package/src/lib/server/publish-template.js +6 -2
  53. package/src/lib/server/publish-template.js.map +1 -1
  54. package/src/lib/server/publish-theme.js +6 -2
  55. package/src/lib/server/publish-theme.js.map +1 -1
  56. package/src/lib/server/publish.js +6 -2
  57. package/src/lib/server/publish.js.map +1 -1
  58. package/src/lib/server/update-artifact.js +18 -4
  59. package/src/lib/server/update-artifact.js.map +1 -1
@@ -0,0 +1,131 @@
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
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { nameSearchNormalizers } from "@aglyn/aglyn/app-utils/name-search";
19
+ import { planListQuery } from "@aglyn/shared-ui-jsx/const/list-query-plan";
20
+ import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
21
+ import { listQueryConstraints } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
22
+ import { collection, documentId, getDocs, limit, query, where } from "firebase/firestore";
23
+ import { useEffect, useMemo, useState } from "react";
24
+ import { LICENCE_LISTING_MATCH_CAP, LISTING_NAME_LOOKUP } from "../model/listing-query.js";
25
+ /**
26
+ * The listings whose NAME holds a typed word (AGL-3321).
27
+ *
28
+ * A license row names its listing by id, and the name lives on the listing,
29
+ * where the publisher can change it. So a license search asks the listings
30
+ * first — `nameTokens array-contains` the word, the key every listing writer
31
+ * stamps — and the license query then asks for those listing ids. Nothing is
32
+ * matched over rows already loaded.
33
+ *
34
+ * Firestore takes at most thirty values in one `in`, so a word naming more
35
+ * than thirty listings is refused by name rather than answered with thirty of
36
+ * them.
37
+ */ export function useListingNameLookup(words) {
38
+ var _read_data;
39
+ const firestore = useFirestore();
40
+ const plan = useMemo(()=>planListQuery(LISTING_NAME_LOOKUP, {
41
+ clauses: [],
42
+ search: words
43
+ }, nameSearchNormalizers), // The words are data; their text is their identity.
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ [
46
+ words.join(' ')
47
+ ]);
48
+ const read = useFirestoreCollection(()=>plan.searched ? query(collection(firestore, 'marketplaceListings'), ...listQueryConstraints(plan), limit(LICENCE_LISTING_MATCH_CAP + 1)) : null, [
49
+ firestore,
50
+ plan.searched
51
+ ], {
52
+ idField: '$id'
53
+ });
54
+ if (!plan.searched) {
55
+ return {
56
+ ids: null,
57
+ pending: false,
58
+ refused: plan.refused,
59
+ notices: []
60
+ };
61
+ }
62
+ if (read.status !== 'success') {
63
+ return {
64
+ ids: null,
65
+ pending: true,
66
+ refused: [],
67
+ notices: plan.notices
68
+ };
69
+ }
70
+ const ids = ((_read_data = read.data) != null ? _read_data : []).map((row)=>row.$id);
71
+ if (ids.length > LICENCE_LISTING_MATCH_CAP) {
72
+ return {
73
+ ids: null,
74
+ pending: false,
75
+ refused: [
76
+ {
77
+ clause: 'search',
78
+ reason: `more than ${LICENCE_LISTING_MATCH_CAP} listings have a name ` + 'with that word — type more of it'
79
+ }
80
+ ],
81
+ notices: []
82
+ };
83
+ }
84
+ return {
85
+ ids,
86
+ pending: false,
87
+ refused: [],
88
+ notices: plan.notices
89
+ };
90
+ }
91
+ /**
92
+ * Listing id → display name, for the rows on screen: read BY ID, thirty at a
93
+ * time, so a license always names its listing however large the catalog is.
94
+ * A listing that cannot be read keeps its id as its label.
95
+ */ export function useListingNames(ids) {
96
+ const firestore = useFirestore();
97
+ const key = [
98
+ ...new Set(ids.filter(Boolean))
99
+ ].sort().join(',');
100
+ const [names, setNames] = useState({});
101
+ useEffect(()=>{
102
+ const wanted = key ? key.split(',') : [];
103
+ if (!wanted.length) return;
104
+ let active = true;
105
+ const chunks = [];
106
+ for(let at = 0; at < wanted.length; at += LICENCE_LISTING_MATCH_CAP){
107
+ chunks.push(wanted.slice(at, at + LICENCE_LISTING_MATCH_CAP));
108
+ }
109
+ void Promise.all(chunks.map((chunk)=>getDocs(query(collection(firestore, 'marketplaceListings'), where(documentId(), 'in', chunk))).catch(()=>null))).then((snapshots)=>{
110
+ if (!active) return;
111
+ const found = {};
112
+ for (const snapshot of snapshots){
113
+ var _ref;
114
+ for (const listing of (_ref = snapshot == null ? void 0 : snapshot.docs) != null ? _ref : []){
115
+ const name = listing.get('displayName');
116
+ if (typeof name === 'string' && name) found[listing.id] = name;
117
+ }
118
+ }
119
+ setNames((previous)=>Object.entries(found).every(([id, name])=>previous[id] === name) ? previous : _extends({}, previous, found));
120
+ });
121
+ return ()=>{
122
+ active = false;
123
+ };
124
+ }, [
125
+ firestore,
126
+ key
127
+ ]);
128
+ return names;
129
+ }
130
+
131
+ //# sourceMappingURL=use-listing-name-lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-listing-name-lookup.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'use client'\n\nimport { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n type ListQueryRefusal,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useFirestore, useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { listQueryConstraints } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport {\n collection,\n documentId,\n getDocs,\n limit,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n LICENCE_LISTING_MATCH_CAP,\n LISTING_NAME_LOOKUP,\n} from '../model/listing-query'\n\nexport interface ListingNameLookup {\n /**\n * The listings a search names, for a `listingId in [...]` clause; `null`\n * when nothing is searched, so the list is not narrowed at all.\n */\n ids: string[] | null\n /** Still asking which listings the word names. */\n pending: boolean\n /** The search, refused — too many listings share the word. */\n refused: ListQueryRefusal[]\n /** Said about what was searched, e.g. one word of two. */\n notices: string[]\n}\n\n/**\n * The listings whose NAME holds a typed word (AGL-3321).\n *\n * A license row names its listing by id, and the name lives on the listing,\n * where the publisher can change it. So a license search asks the listings\n * first — `nameTokens array-contains` the word, the key every listing writer\n * stamps — and the license query then asks for those listing ids. Nothing is\n * matched over rows already loaded.\n *\n * Firestore takes at most thirty values in one `in`, so a word naming more\n * than thirty listings is refused by name rather than answered with thirty of\n * them.\n */\nexport function useListingNameLookup(words: readonly string[]): ListingNameLookup {\n const firestore = useFirestore()\n const plan = useMemo(\n () =>\n planListQuery(LISTING_NAME_LOOKUP, { clauses: [], search: words }, nameSearchNormalizers),\n // The words are data; their text is their identity.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [words.join(' ')],\n )\n const read = useFirestoreCollection<{ $id: string }>(\n () =>\n plan.searched\n ? query(\n collection(firestore, 'marketplaceListings'),\n ...listQueryConstraints(plan),\n limit(LICENCE_LISTING_MATCH_CAP + 1),\n )\n : null,\n [firestore, plan.searched],\n { idField: '$id' },\n )\n if (!plan.searched) {\n return { ids: null, pending: false, refused: plan.refused, notices: [] }\n }\n if (read.status !== 'success') {\n return { ids: null, pending: true, refused: [], notices: plan.notices }\n }\n const ids = (read.data ?? []).map((row) => row.$id)\n if (ids.length > LICENCE_LISTING_MATCH_CAP) {\n return {\n ids: null,\n pending: false,\n refused: [\n {\n clause: 'search',\n reason:\n `more than ${LICENCE_LISTING_MATCH_CAP} listings have a name ` +\n 'with that word — type more of it',\n },\n ],\n notices: [],\n }\n }\n return { ids, pending: false, refused: [], notices: plan.notices }\n}\n\n/**\n * Listing id → display name, for the rows on screen: read BY ID, thirty at a\n * time, so a license always names its listing however large the catalog is.\n * A listing that cannot be read keeps its id as its label.\n */\nexport function useListingNames(ids: readonly string[]): Record<string, string> {\n const firestore = useFirestore()\n const key = [...new Set(ids.filter(Boolean))].sort().join(',')\n const [names, setNames] = useState<Record<string, string>>({})\n useEffect(() => {\n const wanted = key ? key.split(',') : []\n if (!wanted.length) return\n let active = true\n const chunks: string[][] = []\n for (let at = 0; at < wanted.length; at += LICENCE_LISTING_MATCH_CAP) {\n chunks.push(wanted.slice(at, at + LICENCE_LISTING_MATCH_CAP))\n }\n void Promise.all(\n chunks.map((chunk) =>\n getDocs(\n query(\n collection(firestore, 'marketplaceListings'),\n where(documentId(), 'in', chunk),\n ),\n ).catch(() => null),\n ),\n ).then((snapshots) => {\n if (!active) return\n const found: Record<string, string> = {}\n for (const snapshot of snapshots) {\n for (const listing of snapshot?.docs ?? []) {\n const name = listing.get('displayName')\n if (typeof name === 'string' && name) found[listing.id] = name\n }\n }\n setNames((previous) =>\n Object.entries(found).every(([id, name]) => previous[id] === name)\n ? previous\n : { ...previous, ...found },\n )\n })\n return () => {\n active = false\n }\n }, [firestore, key])\n return names\n}\n"],"names":["nameSearchNormalizers","planListQuery","useFirestore","useFirestoreCollection","listQueryConstraints","collection","documentId","getDocs","limit","query","where","useEffect","useMemo","useState","LICENCE_LISTING_MATCH_CAP","LISTING_NAME_LOOKUP","useListingNameLookup","words","read","firestore","plan","clauses","search","join","searched","idField","ids","pending","refused","notices","status","data","map","row","$id","length","clause","reason","useListingNames","key","Set","filter","Boolean","sort","names","setNames","wanted","split","active","chunks","at","push","slice","Promise","all","chunk","catch","then","snapshots","found","snapshot","listing","docs","name","get","id","previous","Object","entries","every"],"mappings":"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"}
@@ -0,0 +1,140 @@
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 { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
18
+ import { type ListQueryDeclaration, type ListQueryFilter, type ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
19
+ /** Everyone, as a browse audience scope. */
20
+ export declare const BROWSE_EVERYONE = "*";
21
+ /** Joins an audience scope to the publisher it is narrowed to. */
22
+ export declare const BROWSE_PUBLISHER_JOIN = "|";
23
+ export declare const LISTING_BROWSE_AUDIENCE = "browseAudience";
24
+ export declare const LISTING_BROWSE_TOKENS = "browseTokens";
25
+ export declare const LISTING_NAME_TOKENS = "nameTokens";
26
+ export declare const LISTING_TAKEN_DOWN = "takenDown";
27
+ /** The fields a listing's lists query by, all derived. */
28
+ export interface ListingQueryFields {
29
+ nameLower: string;
30
+ nameTokens: string[];
31
+ nameReversed: string;
32
+ browseAudience: string[];
33
+ browseTokens: string[];
34
+ takenDown: boolean;
35
+ }
36
+ /** What the derivation reads off a listing. */
37
+ export interface ListingQuerySource {
38
+ displayName?: unknown;
39
+ profileId?: unknown;
40
+ deletedAt?: unknown;
41
+ hiddenAt?: unknown;
42
+ visibility?: string;
43
+ reviewStatus?: string;
44
+ artifactType?: string;
45
+ type?: string;
46
+ kind?: string;
47
+ }
48
+ /**
49
+ * Who browse shows a listing to — the same rule the grid applied in memory:
50
+ * never a deleted or private listing; a browsable one to everyone; one that
51
+ * is not browsable (in review, rejected, taken down) to its own publisher
52
+ * only, so a publisher can watch a submission move through review.
53
+ */
54
+ export declare function listingBrowseAudience(listing: ListingQuerySource): string[];
55
+ /** Every derived field, from the listing as stored. */
56
+ export declare function listingQueryFields(listing: ListingQuerySource): ListingQueryFields;
57
+ /**
58
+ * What a listing that never had one holds in each field browse ORDERS by. An
59
+ * `orderBy` drops a document missing its field, so a listing nobody has
60
+ * installed or rated would vanish from Most installed and Highest rated
61
+ * rather than sort last. `null` sorts after every number in a descending
62
+ * order, which is where "not yet rated" belongs; the reviews route writes a
63
+ * number over it on the first rating. Only ever stamped where the field is
64
+ * ABSENT — the install and review routes own the values.
65
+ */
66
+ export declare const LISTING_SORT_DEFAULTS: Readonly<Record<string, number | null>>;
67
+ /**
68
+ * The derived fields a stored listing is missing or holds stale, plus any
69
+ * sort field it lacks, or null when it is current — what a writer's refresh
70
+ * and the backfill write.
71
+ */
72
+ export declare function listingQueryFieldsPatch(listing: ListingQuerySource & Record<string, unknown>): Record<string, unknown> | null;
73
+ export type BrowseSort = 'newest' | 'installed' | 'rated';
74
+ export declare const BROWSE_SORTS: Readonly<Record<BrowseSort, ListQuerySort>>;
75
+ /**
76
+ * The browse shelf: a category chip, the search box and three orders, every
77
+ * one on the query. The audience clause is its base (`browseBase`), and the
78
+ * search folds into it.
79
+ */
80
+ export declare const MARKETPLACE_BROWSE_QUERY: ListQueryDeclaration;
81
+ /** The base clause's path, for the index enumeration. */
82
+ export declare const MARKETPLACE_BROWSE_BASE_PATHS: readonly [{
83
+ readonly path: "browseAudience";
84
+ readonly array: true;
85
+ }];
86
+ /**
87
+ * What a reader may see on browse: everything public, plus their own org's
88
+ * listings still in review — narrowed to one publisher on a publisher page.
89
+ */
90
+ export declare function browseBase(viewerOrgId: string | null | undefined, publisherId?: string | null): ListQueryFilter[];
91
+ /** "This workspace": who on the team bought it. */
92
+ export declare const HELD_LICENCE_QUERY: ListQueryDeclaration;
93
+ /**
94
+ * "Bought by you": which workspace a license landed in. A purchase that
95
+ * names none (before AGL-2331) is stored with `buyerOrgId: null` and is
96
+ * asked for as empty.
97
+ */
98
+ export declare const MINE_LICENCE_QUERY: ListQueryDeclaration;
99
+ /** The picked value that means "every workspace" — a purchase naming none. */
100
+ export declare const EVERY_WORKSPACE = "every";
101
+ /**
102
+ * The Licensed-to select's "every workspace" as the query reads it: a
103
+ * purchase whose `buyerOrgId` is null.
104
+ */
105
+ export declare function mineLicenceClauses(clauses: readonly ListFilterClause[]): ListFilterClause[];
106
+ /**
107
+ * At most this many listings are named by one license search: they go on the
108
+ * purchases query as `listingId in [...]`, which Firestore caps at thirty.
109
+ */
110
+ export declare const LICENCE_LISTING_MATCH_CAP = 30;
111
+ /**
112
+ * The listing-name lookup a license search runs first: the listings whose
113
+ * name has the typed word, by the same token array the staff queue searches.
114
+ */
115
+ export declare const LISTING_NAME_LOOKUP: ListQueryDeclaration;
116
+ /**
117
+ * A license table's scope: its owner clause, live purchases only (a refunded
118
+ * purchase is not a license, AGL-1546), and the listings a search named.
119
+ */
120
+ export declare function licenceBase(owner: {
121
+ path: 'buyerOrgId' | 'buyerUid';
122
+ value: string;
123
+ }, listingIds: readonly string[] | null): ListQueryFilter[];
124
+ /** The queue's Status select, as clauses the query serves. */
125
+ export declare const REVIEW_QUEUE_QUERY: ListQueryDeclaration;
126
+ /** The three sections the queue reads, each its own query. */
127
+ export type ReviewQueueSection = 'queue' | 'listed' | 'verification';
128
+ /** A listing's newest bytes have no approval: the `latestVersionReviewState` values that mean it. */
129
+ export declare const AWAITING_REVIEW_STATES: readonly ["pending", "rejected"];
130
+ export declare const LISTED_REVIEW_STATUSES: readonly ["listed", "verified"];
131
+ /**
132
+ * One section's scope: live plugin listings, narrowed to the section. The
133
+ * Listed section's own `reviewStatus in [...]` steps aside for a Status
134
+ * clause that names one of its two values, and a clause naming any other
135
+ * value leaves the section empty by definition — `null`, never queried.
136
+ */
137
+ export declare function reviewQueueBase(section: ReviewQueueSection, clauses: readonly ListFilterClause[]): ListQueryFilter[] | null;
138
+ /** The queue's Status select as the clause it asks for, or none for "All". */
139
+ export declare function reviewQueueStatusClauses(status: string): ListFilterClause[];
140
+ export declare const MARKETPLACE_REPORTS_QUERY: ListQueryDeclaration;
@@ -0,0 +1,393 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { displayNameSearchFields, scopedSearch, scopedSearchTokens } from "@aglyn/aglyn/app-utils/name-search";
18
+ import { isListingBrowsable, isListingDeleted, isPrivateListing } from "@aglyn/aglyn/app-utils/marketplace-listing-visibility";
19
+ import { LIST_QUERY_ID_PATH } from "@aglyn/shared-ui-jsx/const/list-query-plan";
20
+ /*
21
+ * EVERY MARKETPLACE LIST ON ITS QUERY (AGL-3321).
22
+ *
23
+ * Browse, the licenses tab, the staff review queue and the abuse-report queue
24
+ * each matched their filters and search over the rows they had loaded — a
25
+ * window of ninety listings, two hundred reports, a hundred plugins. A match
26
+ * on the next page was answered "no match". Each list now declares what its
27
+ * query can hold, and the fields below are what the listing writers stamp so
28
+ * the query has something to ask.
29
+ *
30
+ * ## What a listing carries for its lists
31
+ *
32
+ * - `nameLower`/`nameTokens`/`nameReversed` — the display name's search
33
+ * keys (`displayNameSearchFields`), for the staff queue's search and for
34
+ * the licenses tab's listing-name lookup.
35
+ * - `browseAudience` — who browse shows the listing to, as scope tokens:
36
+ * `*` for everyone, the publisher's org id for the owner's own view of a
37
+ * listing still in review or taken down, and each again joined to the
38
+ * publisher (`*|<org>`) so a publisher page asks the same question
39
+ * narrowed to one publisher without a second equality. Empty for a
40
+ * deleted or private listing, which browse never shows anyone.
41
+ * - `browseTokens` — the name tokens under each audience scope
42
+ * (`scopedSearchTokens`), so browse's search folds into its audience
43
+ * clause: one `array-contains-any` answers both.
44
+ * - `takenDown` — `hiddenAt` as a boolean, for the staff queue's Taken down
45
+ * status by equality rather than by a range that would lead the order.
46
+ *
47
+ * All five follow from fields other writers own (`displayName`, `deletedAt`,
48
+ * `visibility`, `hiddenAt`, `reviewStatus`, the artifact discriminators,
49
+ * `profileId`), so no writer computes them from its own patch: each calls
50
+ * `refreshListingQueryFields` after it writes, which re-derives them from the
51
+ * document as stored. `tools/scripts/lib/listing-query-fields.mjs`
52
+ * restates the derivation for the seeds and the backfill, held to
53
+ * `listing-query-fields.fixtures.json` beside it by both sides.
54
+ */ /** Everyone, as a browse audience scope. */ export const BROWSE_EVERYONE = '*';
55
+ /** Joins an audience scope to the publisher it is narrowed to. */ export const BROWSE_PUBLISHER_JOIN = '|';
56
+ export const LISTING_BROWSE_AUDIENCE = 'browseAudience';
57
+ export const LISTING_BROWSE_TOKENS = 'browseTokens';
58
+ export const LISTING_NAME_TOKENS = 'nameTokens';
59
+ export const LISTING_TAKEN_DOWN = 'takenDown';
60
+ /**
61
+ * Who browse shows a listing to — the same rule the grid applied in memory:
62
+ * never a deleted or private listing; a browsable one to everyone; one that
63
+ * is not browsable (in review, rejected, taken down) to its own publisher
64
+ * only, so a publisher can watch a submission move through review.
65
+ */ export function listingBrowseAudience(listing) {
66
+ if (isListingDeleted(listing) || isPrivateListing(listing)) return [];
67
+ const publisher = typeof listing.profileId === 'string' ? listing.profileId : '';
68
+ const scope = isListingBrowsable(listing) ? BROWSE_EVERYONE : publisher;
69
+ if (!scope) return [];
70
+ return publisher ? [
71
+ scope,
72
+ `${scope}${BROWSE_PUBLISHER_JOIN}${publisher}`
73
+ ] : [
74
+ scope
75
+ ];
76
+ }
77
+ /** Every derived field, from the listing as stored. */ export function listingQueryFields(listing) {
78
+ const name = displayNameSearchFields(listing.displayName);
79
+ const browseAudience = listingBrowseAudience(listing);
80
+ return _extends({}, name, {
81
+ browseAudience,
82
+ browseTokens: scopedSearchTokens(browseAudience, name.nameTokens),
83
+ takenDown: Boolean(listing.hiddenAt)
84
+ });
85
+ }
86
+ const same = (left, right)=>JSON.stringify(left != null ? left : null) === JSON.stringify(right != null ? right : null);
87
+ /**
88
+ * What a listing that never had one holds in each field browse ORDERS by. An
89
+ * `orderBy` drops a document missing its field, so a listing nobody has
90
+ * installed or rated would vanish from Most installed and Highest rated
91
+ * rather than sort last. `null` sorts after every number in a descending
92
+ * order, which is where "not yet rated" belongs; the reviews route writes a
93
+ * number over it on the first rating. Only ever stamped where the field is
94
+ * ABSENT — the install and review routes own the values.
95
+ */ export const LISTING_SORT_DEFAULTS = {
96
+ installCount: 0,
97
+ ratingAverage: null
98
+ };
99
+ /**
100
+ * The derived fields a stored listing is missing or holds stale, plus any
101
+ * sort field it lacks, or null when it is current — what a writer's refresh
102
+ * and the backfill write.
103
+ */ export function listingQueryFieldsPatch(listing) {
104
+ const computed = listingQueryFields(listing);
105
+ const patch = {};
106
+ for (const [key, value] of Object.entries(computed)){
107
+ if (!same(listing[key], value)) patch[key] = value;
108
+ }
109
+ for (const [key, value] of Object.entries(LISTING_SORT_DEFAULTS)){
110
+ if (listing[key] === undefined) patch[key] = value;
111
+ }
112
+ return Object.keys(patch).length ? patch : null;
113
+ }
114
+ export const BROWSE_SORTS = {
115
+ newest: {
116
+ path: 'createdAt',
117
+ direction: 'desc'
118
+ },
119
+ installed: {
120
+ path: 'installCount',
121
+ direction: 'desc'
122
+ },
123
+ rated: {
124
+ path: 'ratingAverage',
125
+ direction: 'desc'
126
+ }
127
+ };
128
+ /**
129
+ * The browse shelf: a category chip, the search box and three orders, every
130
+ * one on the query. The audience clause is its base (`browseBase`), and the
131
+ * search folds into it.
132
+ */ export const MARKETPLACE_BROWSE_QUERY = {
133
+ fields: [
134
+ {
135
+ column: 'category',
136
+ kind: 'exact',
137
+ path: 'category',
138
+ operators: [
139
+ 'equals'
140
+ ]
141
+ }
142
+ ],
143
+ sorts: [
144
+ BROWSE_SORTS.newest,
145
+ BROWSE_SORTS.installed,
146
+ BROWSE_SORTS.rated
147
+ ],
148
+ search: {
149
+ tokensPath: LISTING_BROWSE_TOKENS,
150
+ scoped: scopedSearch(LISTING_BROWSE_TOKENS)
151
+ }
152
+ };
153
+ /** The base clause's path, for the index enumeration. */ export const MARKETPLACE_BROWSE_BASE_PATHS = [
154
+ {
155
+ path: LISTING_BROWSE_AUDIENCE,
156
+ array: true
157
+ }
158
+ ];
159
+ /**
160
+ * What a reader may see on browse: everything public, plus their own org's
161
+ * listings still in review — narrowed to one publisher on a publisher page.
162
+ */ export function browseBase(viewerOrgId, publisherId) {
163
+ const scopes = [
164
+ BROWSE_EVERYONE,
165
+ ...viewerOrgId ? [
166
+ viewerOrgId
167
+ ] : []
168
+ ];
169
+ return [
170
+ {
171
+ path: LISTING_BROWSE_AUDIENCE,
172
+ op: 'array-contains-any',
173
+ value: publisherId ? scopes.map((scope)=>`${scope}${BROWSE_PUBLISHER_JOIN}${publisherId}`) : scopes
174
+ }
175
+ ];
176
+ }
177
+ /*==========================================
178
+ * Licenses (`marketplacePurchases`, web SDK, buyer/org-gated).
179
+ *=========================================*/ /**
180
+ * Both license tables keep the order they always had — Firestore's, by
181
+ * document id — which index merging serves with no composite at all.
182
+ */ const BY_ID = {
183
+ path: LIST_QUERY_ID_PATH,
184
+ direction: 'asc'
185
+ };
186
+ /** "This workspace": who on the team bought it. */ export const HELD_LICENCE_QUERY = {
187
+ fields: [
188
+ {
189
+ column: 'buyerUid',
190
+ kind: 'exact',
191
+ path: 'buyerUid',
192
+ operators: [
193
+ 'equals'
194
+ ]
195
+ }
196
+ ],
197
+ sorts: [
198
+ BY_ID
199
+ ]
200
+ };
201
+ /**
202
+ * "Bought by you": which workspace a license landed in. A purchase that
203
+ * names none (before AGL-2331) is stored with `buyerOrgId: null` and is
204
+ * asked for as empty.
205
+ */ export const MINE_LICENCE_QUERY = {
206
+ fields: [
207
+ {
208
+ column: 'buyerOrgId',
209
+ kind: 'exact',
210
+ path: 'buyerOrgId',
211
+ presence: 'nullable',
212
+ operators: [
213
+ 'equals',
214
+ 'isEmpty'
215
+ ]
216
+ }
217
+ ],
218
+ sorts: [
219
+ BY_ID
220
+ ]
221
+ };
222
+ /** The picked value that means "every workspace" — a purchase naming none. */ export const EVERY_WORKSPACE = 'every';
223
+ /**
224
+ * The Licensed-to select's "every workspace" as the query reads it: a
225
+ * purchase whose `buyerOrgId` is null.
226
+ */ export function mineLicenceClauses(clauses) {
227
+ return clauses.map((clause)=>clause.field === 'buyerOrgId' && clause.op === 'equals' && clause.value === EVERY_WORKSPACE ? _extends({}, clause, {
228
+ op: 'isEmpty',
229
+ value: ''
230
+ }) : clause);
231
+ }
232
+ /**
233
+ * At most this many listings are named by one license search: they go on the
234
+ * purchases query as `listingId in [...]`, which Firestore caps at thirty.
235
+ */ export const LICENCE_LISTING_MATCH_CAP = 30;
236
+ /**
237
+ * The listing-name lookup a license search runs first: the listings whose
238
+ * name has the typed word, by the same token array the staff queue searches.
239
+ */ export const LISTING_NAME_LOOKUP = {
240
+ fields: [],
241
+ sorts: [
242
+ BY_ID
243
+ ],
244
+ search: {
245
+ tokensPath: LISTING_NAME_TOKENS
246
+ }
247
+ };
248
+ /**
249
+ * A license table's scope: its owner clause, live purchases only (a refunded
250
+ * purchase is not a license, AGL-1546), and the listings a search named.
251
+ */ export function licenceBase(owner, listingIds) {
252
+ return [
253
+ {
254
+ path: owner.path,
255
+ op: '==',
256
+ value: owner.value
257
+ },
258
+ {
259
+ path: 'refundedAt',
260
+ op: '==',
261
+ value: null
262
+ },
263
+ ...listingIds ? [
264
+ {
265
+ path: 'listingId',
266
+ op: 'in',
267
+ value: [
268
+ ...listingIds
269
+ ]
270
+ }
271
+ ] : []
272
+ ];
273
+ }
274
+ /*==========================================
275
+ * The staff review queue (Admin SDK, staff route).
276
+ *=========================================*/ /** The queue's Status select, as clauses the query serves. */ export const REVIEW_QUEUE_QUERY = {
277
+ fields: [
278
+ {
279
+ column: 'reviewStatus',
280
+ kind: 'exact',
281
+ path: 'reviewStatus',
282
+ operators: [
283
+ 'equals'
284
+ ]
285
+ },
286
+ {
287
+ column: LISTING_TAKEN_DOWN,
288
+ kind: 'boolean',
289
+ path: LISTING_TAKEN_DOWN
290
+ }
291
+ ],
292
+ sorts: [
293
+ BY_ID
294
+ ],
295
+ search: {
296
+ tokensPath: LISTING_NAME_TOKENS
297
+ }
298
+ };
299
+ /** A listing's newest bytes have no approval: the `latestVersionReviewState` values that mean it. */ export const AWAITING_REVIEW_STATES = [
300
+ 'pending',
301
+ 'rejected'
302
+ ];
303
+ export const LISTED_REVIEW_STATUSES = [
304
+ 'listed',
305
+ 'verified'
306
+ ];
307
+ /**
308
+ * One section's scope: live plugin listings, narrowed to the section. The
309
+ * Listed section's own `reviewStatus in [...]` steps aside for a Status
310
+ * clause that names one of its two values, and a clause naming any other
311
+ * value leaves the section empty by definition — `null`, never queried.
312
+ */ export function reviewQueueBase(section, clauses) {
313
+ const base = [
314
+ {
315
+ path: 'type',
316
+ op: '==',
317
+ value: 'plugin'
318
+ },
319
+ {
320
+ path: 'deletedAt',
321
+ op: '==',
322
+ value: null
323
+ }
324
+ ];
325
+ if (section === 'queue') {
326
+ base.push({
327
+ path: 'latestVersionReviewState',
328
+ op: 'in',
329
+ value: [
330
+ ...AWAITING_REVIEW_STATES
331
+ ]
332
+ });
333
+ } else if (section === 'verification') {
334
+ base.push({
335
+ path: 'verificationRequest.state',
336
+ op: '==',
337
+ value: 'pending'
338
+ });
339
+ } else {
340
+ const status = clauses.find((clause)=>clause.field === 'reviewStatus' && clause.op === 'equals' && clause.value);
341
+ if (status) {
342
+ if (!LISTED_REVIEW_STATUSES.includes(status.value)) return null;
343
+ } else {
344
+ base.push({
345
+ path: 'reviewStatus',
346
+ op: 'in',
347
+ value: [
348
+ ...LISTED_REVIEW_STATUSES
349
+ ]
350
+ });
351
+ }
352
+ }
353
+ return base;
354
+ }
355
+ /** The queue's Status select as the clause it asks for, or none for "All". */ export function reviewQueueStatusClauses(status) {
356
+ if (!status || status === 'all') return [];
357
+ if (status === 'hidden') return [
358
+ {
359
+ field: LISTING_TAKEN_DOWN,
360
+ op: 'is',
361
+ value: 'true'
362
+ }
363
+ ];
364
+ return [
365
+ {
366
+ field: 'reviewStatus',
367
+ op: 'equals',
368
+ value: status
369
+ }
370
+ ];
371
+ }
372
+ /*==========================================
373
+ * The abuse-report queue (Admin SDK, staff route).
374
+ *=========================================*/ export const MARKETPLACE_REPORTS_QUERY = {
375
+ fields: [
376
+ {
377
+ column: 'status',
378
+ kind: 'exact',
379
+ path: 'status',
380
+ operators: [
381
+ 'equals'
382
+ ]
383
+ }
384
+ ],
385
+ sorts: [
386
+ {
387
+ path: 'updatedAt',
388
+ direction: 'desc'
389
+ }
390
+ ]
391
+ };
392
+
393
+ //# sourceMappingURL=listing-query.js.map