@getvitops/utils 7.0.0 → 8.0.1
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/CHANGELOG.md +133 -13
- package/dist/adc-BFrzpMHq.d.mts +25 -0
- package/dist/adc-BFrzpMHq.d.mts.map +1 -0
- package/dist/adc-D6aw2qgq.mjs +45 -0
- package/dist/adc-D6aw2qgq.mjs.map +1 -0
- package/dist/ads/index.d.mts +0 -1
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +4 -13
- package/dist/ads/index.mjs.map +1 -1
- package/dist/{cloudflare-CD8vwrgj.mjs → cloudflare-CgAsXiqe.mjs} +24 -2
- package/dist/cloudflare-CgAsXiqe.mjs.map +1 -0
- package/dist/color/index.d.mts +0 -3
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +0 -3
- package/dist/color/index.mjs.map +1 -1
- package/dist/index.d.mts +5 -190
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -240
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +2 -24
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/indexing/index.mjs +7 -69
- package/dist/indexing/index.mjs.map +1 -1
- package/dist/json-store-30CFCbZZ.mjs +38 -0
- package/dist/json-store-30CFCbZZ.mjs.map +1 -0
- package/dist/media/index.d.mts +0 -2
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +4 -20
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +1 -2
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/onboarding/index.d.mts +1 -1
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +7 -17
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +154 -0
- package/dist/reviews/index.d.mts.map +1 -0
- package/dist/reviews/index.mjs +218 -0
- package/dist/reviews/index.mjs.map +1 -0
- package/dist/schema/index.d.mts +191 -0
- package/dist/schema/index.d.mts.map +1 -0
- package/dist/schema/index.mjs +241 -0
- package/dist/schema/index.mjs.map +1 -0
- package/dist/security/index.d.mts +48 -0
- package/dist/security/index.d.mts.map +1 -0
- package/dist/security/index.mjs +80 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/{token-BlShEM6J.d.mts → token-Bz_-igw-.d.mts} +3 -2
- package/dist/{token-BlShEM6J.d.mts.map → token-Bz_-igw-.d.mts.map} +1 -1
- package/dist/{token-CuyjDo6I.mjs → token-DkKC5m23.mjs} +16 -3
- package/dist/{token-CuyjDo6I.mjs.map → token-DkKC5m23.mjs.map} +1 -1
- package/dist/tracking/index.d.mts +1 -1
- package/dist/{types-C3lHaSrU.d.mts → types-CHHjzRrv.d.mts} +1 -2
- package/dist/types-CHHjzRrv.d.mts.map +1 -0
- package/package.json +25 -2
- package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
- package/dist/types-C3lHaSrU.d.mts.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { n as writeVersioned, t as readVersioned } from "../json-store-30CFCbZZ.mjs";
|
|
2
|
+
import { a as googleHeaders, i as googleAccessToken, t as SCOPES } from "../token-DkKC5m23.mjs";
|
|
3
|
+
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-D6aw2qgq.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/reviews/plan.ts
|
|
6
|
+
/** Decide which locations to fetch, and why any are skipped. */
|
|
7
|
+
function plan(input) {
|
|
8
|
+
const { config, knownLocations, onlyLocation, hasCredential } = input;
|
|
9
|
+
const notes = [];
|
|
10
|
+
if (!config || Object.keys(config.locations).length === 0) return {
|
|
11
|
+
blocked: "no site.reviews.locations configured — nothing to fetch",
|
|
12
|
+
locations: [],
|
|
13
|
+
notes
|
|
14
|
+
};
|
|
15
|
+
if (!hasCredential) return {
|
|
16
|
+
blocked: "no Google credential — set VITOPS_GOOGLE_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN, or run `gcloud auth application-default login`",
|
|
17
|
+
locations: [],
|
|
18
|
+
notes
|
|
19
|
+
};
|
|
20
|
+
const entries = onlyLocation ? Object.entries(config.locations).filter(([key]) => key === onlyLocation) : Object.entries(config.locations);
|
|
21
|
+
if (onlyLocation && entries.length === 0) return {
|
|
22
|
+
blocked: `--location "${onlyLocation}" is not in site.reviews.locations`,
|
|
23
|
+
locations: [],
|
|
24
|
+
notes
|
|
25
|
+
};
|
|
26
|
+
const locations = entries.map(([key, loc]) => {
|
|
27
|
+
if (!knownLocations.has(key)) return {
|
|
28
|
+
key,
|
|
29
|
+
skip: `organization.locations has no entry keyed "${key}" with a listings.google id`
|
|
30
|
+
};
|
|
31
|
+
const accountId = loc.accountId ?? config.accountId;
|
|
32
|
+
if (!accountId) return {
|
|
33
|
+
key,
|
|
34
|
+
skip: `no accountId — set site.reviews.accountId or locations.${key}.accountId`
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
key,
|
|
38
|
+
accountId,
|
|
39
|
+
locationId: loc.locationId
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
const blocked = locations.filter((l) => l.skip);
|
|
43
|
+
if (blocked.length) for (const l of blocked) notes.push(`${l.key}: skipped — ${l.skip}`);
|
|
44
|
+
return {
|
|
45
|
+
locations,
|
|
46
|
+
notes
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Render a plan as the human-readable block `--dry` prints. */
|
|
50
|
+
function formatPlan(p) {
|
|
51
|
+
if (p.blocked) return `✗ blocked — ${p.blocked}`;
|
|
52
|
+
const lines = [];
|
|
53
|
+
const active = p.locations.filter((l) => !l.skip);
|
|
54
|
+
lines.push(`locations ${active.length} of ${p.locations.length} will be fetched`);
|
|
55
|
+
for (const l of active) lines.push(` ${l.key} ${l.accountId}/${l.locationId}`);
|
|
56
|
+
for (const n of p.notes) lines.push(` ! ${n}`);
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Filter and order one location's reviews for display.
|
|
61
|
+
*
|
|
62
|
+
* Featured ids always win and keep the author's order — an explicit choice
|
|
63
|
+
* overrides any automatic ranking. The remainder is filtered by `minRating`
|
|
64
|
+
* (default 4 — a star-rating widget showing a 1-star review it did not have to
|
|
65
|
+
* is a design choice a config field should not have to spell out every time),
|
|
66
|
+
* sorted, then capped by `limit`.
|
|
67
|
+
*/
|
|
68
|
+
function selectReviews(reviews, config) {
|
|
69
|
+
const featuredIds = config.featured ?? [];
|
|
70
|
+
const byId = new Map(reviews.map((r) => [r.id, r]));
|
|
71
|
+
const featured = featuredIds.map((id) => byId.get(id)).filter((r) => !!r);
|
|
72
|
+
const featuredSet = new Set(featured.map((r) => r.id));
|
|
73
|
+
const minRating = config.minRating ?? 4;
|
|
74
|
+
const rest = reviews.filter((r) => !featuredSet.has(r.id) && r.rating >= minRating).sort((a, b) => config.sort === "rating" ? b.rating - a.rating : new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
|
|
75
|
+
const combined = [...featured, ...rest];
|
|
76
|
+
return config.limit !== void 0 ? combined.slice(0, config.limit) : combined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Merge a fresh fetch into the existing cache.
|
|
80
|
+
*
|
|
81
|
+
* Only the locations actually fetched this run are replaced; every other
|
|
82
|
+
* location's prior reviews pass through untouched. This is what makes a partial
|
|
83
|
+
* failure (one location's fetch errors) non-destructive — `cmdReviews` merges
|
|
84
|
+
* only the locations that succeeded and leaves the rest of the cache alone.
|
|
85
|
+
*/
|
|
86
|
+
function mergeCache(existing, fetched) {
|
|
87
|
+
return {
|
|
88
|
+
version: 1,
|
|
89
|
+
locations: {
|
|
90
|
+
...existing?.locations,
|
|
91
|
+
...fetched
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/reviews/cache.ts
|
|
98
|
+
/** Default location, relative to the consumer's project root. */
|
|
99
|
+
const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
|
|
100
|
+
/**
|
|
101
|
+
* Read the cache, or `undefined` when there isn't a usable one.
|
|
102
|
+
*
|
|
103
|
+
* A corrupt or wrong-version file reads as absent, exactly `readManifest`'s rule:
|
|
104
|
+
* the alternative — reading it as an empty cache — would be indistinguishable
|
|
105
|
+
* from "this business genuinely has no reviews" and silently ship an empty
|
|
106
|
+
* carousel.
|
|
107
|
+
*/
|
|
108
|
+
function readCache(path) {
|
|
109
|
+
return readVersioned(path, "locations");
|
|
110
|
+
}
|
|
111
|
+
/** Build a cache with its keys and each location's reviews in a stable order. */
|
|
112
|
+
function toCache(locations) {
|
|
113
|
+
const sorted = {};
|
|
114
|
+
for (const key of Object.keys(locations).sort()) {
|
|
115
|
+
const entry = locations[key];
|
|
116
|
+
sorted[key] = {
|
|
117
|
+
...entry,
|
|
118
|
+
reviews: [...entry.reviews].sort((a, b) => a.id.localeCompare(b.id))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
version: 1,
|
|
123
|
+
locations: sorted
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Write the cache, creating its directory. */
|
|
127
|
+
function writeCache(path, cache) {
|
|
128
|
+
writeVersioned(path, cache);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/reviews/gbp.ts
|
|
133
|
+
/**
|
|
134
|
+
* Google Business Profile — fetching one location's reviews.
|
|
135
|
+
*
|
|
136
|
+
* Reviews live only on the legacy v4 Business Information API
|
|
137
|
+
* (`mybusiness.googleapis.com/v4`); the newer split APIs (Account Management,
|
|
138
|
+
* Business Information v1) never grew a reviews resource. **The calling GCP
|
|
139
|
+
* project must be allowlisted by Google before any call here succeeds** — a
|
|
140
|
+
* one-time access-request form, turnaround measured in days to weeks — so a 403
|
|
141
|
+
* is treated as that known state rather than surfaced as a raw failure. Scope:
|
|
142
|
+
* `SCOPES.businessProfile` (`../google/token.ts`).
|
|
143
|
+
*
|
|
144
|
+
* TODO — re-check when Google migrates the Reviews endpoint off v4 onto a
|
|
145
|
+
* successor API. Endpoint, auth, pagination and the review object's fields may
|
|
146
|
+
* all change; this module is the one place that assumption lives.
|
|
147
|
+
*
|
|
148
|
+
* Auth is the shared exchange in `../google/token.ts`, same as `../indexing/gsc.ts`
|
|
149
|
+
* — no `googleapis` dependency, one REST endpoint.
|
|
150
|
+
*/
|
|
151
|
+
const STAR_RATING = {
|
|
152
|
+
ONE: 1,
|
|
153
|
+
TWO: 2,
|
|
154
|
+
THREE: 3,
|
|
155
|
+
FOUR: 4,
|
|
156
|
+
FIVE: 5
|
|
157
|
+
};
|
|
158
|
+
function toReview(raw, location) {
|
|
159
|
+
return {
|
|
160
|
+
id: raw.reviewId,
|
|
161
|
+
location,
|
|
162
|
+
rating: STAR_RATING[raw.starRating ?? ""] ?? 0,
|
|
163
|
+
author: raw.reviewer?.isAnonymous ? "A Google user" : raw.reviewer?.displayName ?? "A Google user",
|
|
164
|
+
...raw.reviewer?.profilePhotoUrl ? { authorPhoto: raw.reviewer.profilePhotoUrl } : {},
|
|
165
|
+
...raw.comment ? { text: raw.comment } : {},
|
|
166
|
+
publishedAt: raw.createTime,
|
|
167
|
+
updatedAt: raw.updateTime,
|
|
168
|
+
...raw.reviewReply ? { reply: {
|
|
169
|
+
text: raw.reviewReply.comment,
|
|
170
|
+
at: raw.reviewReply.updateTime
|
|
171
|
+
} } : {}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Fetch every review for one location, paginating to completion.
|
|
176
|
+
*
|
|
177
|
+
* Never throws — a per-location failure is a structured result so a run across
|
|
178
|
+
* several locations can fetch what it can and report the rest, the same contract
|
|
179
|
+
* `../indexing/gsc.ts`'s `inspectUrl` keeps.
|
|
180
|
+
*/
|
|
181
|
+
async function fetchReviews(auth, accountId, locationId, fetchImpl = fetch) {
|
|
182
|
+
const location = locationId.startsWith("locations/") ? locationId : `locations/${locationId}`;
|
|
183
|
+
const account = accountId.startsWith("accounts/") ? accountId : `accounts/${accountId}`;
|
|
184
|
+
const reviews = [];
|
|
185
|
+
let averageRating;
|
|
186
|
+
let totalReviewCount;
|
|
187
|
+
let pageToken;
|
|
188
|
+
do {
|
|
189
|
+
const url = new URL(`https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`);
|
|
190
|
+
url.searchParams.set("pageSize", "50");
|
|
191
|
+
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
192
|
+
const res = await fetchImpl(url, { headers: googleHeaders(auth) });
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const message = res.status === 403 ? `403 — this GCP project is most likely not allowlisted for the Business Profile API yet (a one-time Google approval, requested separately from every other API here), or the credential is not an owner/manager of ${account}/${location}` : `${res.status}: ${await res.text()}`;
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reviews,
|
|
198
|
+
status: res.status,
|
|
199
|
+
message
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const body = await res.json();
|
|
203
|
+
for (const r of body.reviews ?? []) reviews.push(toReview(r, locationId));
|
|
204
|
+
averageRating = body.averageRating ?? averageRating;
|
|
205
|
+
totalReviewCount = body.totalReviewCount ?? totalReviewCount;
|
|
206
|
+
pageToken = body.nextPageToken;
|
|
207
|
+
} while (pageToken);
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
reviews,
|
|
211
|
+
...averageRating !== void 0 ? { averageRating } : {},
|
|
212
|
+
...totalReviewCount !== void 0 ? { totalReviewCount } : {}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
export { REVIEWS_CACHE_PATH, SCOPES, adcCredentialsPath, adcQuotaProject, fetchReviews, formatPlan, googleAccessToken, googleHeaders, mergeCache, parseAdcUser, plan, readCache, selectReviews, toCache, writeCache };
|
|
218
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/reviews/plan.ts","../../src/reviews/cache.ts","../../src/reviews/gbp.ts"],"sourcesContent":["/**\n * Every decision `vitops reviews` makes, as pure functions.\n *\n * Same split as `../indexing/plan.ts`: no `fetch`, no `fs`, no clock. Which\n * locations to fetch (and why one is blocked), and how a location's reviews are\n * filtered down to what a site actually displays, are decided here — `gbp.ts`\n * and `cache.ts` do what they're told and nothing else. That is what lets Astro's\n * `<ReviewCard>` and a future Bricks renderer both call `selectReviews` instead\n * of each re-implementing \"top 5 by rating, minus the ones already featured\".\n */\nimport type {\n LocationReviews,\n Review,\n ReviewCache,\n ReviewLocationConfig,\n ReviewsConfig,\n} from './types.ts';\n\n/** One location's fetch plan, or the reason it was skipped. */\nexport interface LocationPlan {\n key: string;\n accountId?: string;\n locationId?: string;\n /** Present iff this location will not be fetched. */\n skip?: string;\n}\n\nexport interface ReviewsPlan {\n /** Set when the whole run is refused — nothing is fetched. */\n blocked?: string;\n locations: LocationPlan[];\n notes: string[];\n}\n\nexport interface PlanInput {\n config: ReviewsConfig | undefined;\n /** Location keys with a Google listing configured (`organization.locations[].listings.google`). */\n knownLocations: Set<string>;\n /** `--location`: fetch exactly this one, skipping the rest. */\n onlyLocation?: string | undefined;\n /** Whether a Google credential was resolved. */\n hasCredential: boolean;\n}\n\n/** Decide which locations to fetch, and why any are skipped. */\nexport function plan(input: PlanInput): ReviewsPlan {\n const { config, knownLocations, onlyLocation, hasCredential } = input;\n const notes: string[] = [];\n\n if (!config || Object.keys(config.locations).length === 0) {\n return {\n blocked: 'no site.reviews.locations configured — nothing to fetch',\n locations: [],\n notes,\n };\n }\n\n if (!hasCredential) {\n return {\n blocked:\n 'no Google credential — set VITOPS_GOOGLE_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN, or run `gcloud auth application-default login`',\n locations: [],\n notes,\n };\n }\n\n const entries = onlyLocation\n ? Object.entries(config.locations).filter(([key]) => key === onlyLocation)\n : Object.entries(config.locations);\n\n if (onlyLocation && entries.length === 0) {\n return {\n blocked: `--location \"${onlyLocation}\" is not in site.reviews.locations`,\n locations: [],\n notes,\n };\n }\n\n const locations: LocationPlan[] = entries.map(([key, loc]) => {\n if (!knownLocations.has(key))\n return {\n key,\n skip: `organization.locations has no entry keyed \"${key}\" with a listings.google id`,\n };\n const accountId = loc.accountId ?? config.accountId;\n if (!accountId)\n return {\n key,\n skip: `no accountId — set site.reviews.accountId or locations.${key}.accountId`,\n };\n return { key, accountId, locationId: loc.locationId };\n });\n\n const blocked = locations.filter((l) => l.skip);\n if (blocked.length) for (const l of blocked) notes.push(`${l.key}: skipped — ${l.skip}`);\n\n return { locations, notes };\n}\n\n/** Render a plan as the human-readable block `--dry` prints. */\nexport function formatPlan(p: ReviewsPlan): string {\n if (p.blocked) return `✗ blocked — ${p.blocked}`;\n const lines: string[] = [];\n const active = p.locations.filter((l) => !l.skip);\n lines.push(`locations ${active.length} of ${p.locations.length} will be fetched`);\n for (const l of active) lines.push(` ${l.key} ${l.accountId}/${l.locationId}`);\n for (const n of p.notes) lines.push(` ! ${n}`);\n return lines.join('\\n');\n}\n\n/**\n * Filter and order one location's reviews for display.\n *\n * Featured ids always win and keep the author's order — an explicit choice\n * overrides any automatic ranking. The remainder is filtered by `minRating`\n * (default 4 — a star-rating widget showing a 1-star review it did not have to\n * is a design choice a config field should not have to spell out every time),\n * sorted, then capped by `limit`.\n */\nexport function selectReviews(reviews: Review[], config: ReviewLocationConfig): Review[] {\n const featuredIds = config.featured ?? [];\n const byId = new Map(reviews.map((r) => [r.id, r]));\n const featured = featuredIds.map((id) => byId.get(id)).filter((r): r is Review => !!r);\n const featuredSet = new Set(featured.map((r) => r.id));\n\n const minRating = config.minRating ?? 4;\n const rest = reviews\n .filter((r) => !featuredSet.has(r.id) && r.rating >= minRating)\n .sort((a, b) =>\n config.sort === 'rating'\n ? b.rating - a.rating\n : new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),\n );\n\n const combined = [...featured, ...rest];\n return config.limit !== undefined ? combined.slice(0, config.limit) : combined;\n}\n\n/**\n * Merge a fresh fetch into the existing cache.\n *\n * Only the locations actually fetched this run are replaced; every other\n * location's prior reviews pass through untouched. This is what makes a partial\n * failure (one location's fetch errors) non-destructive — `cmdReviews` merges\n * only the locations that succeeded and leaves the rest of the cache alone.\n */\nexport function mergeCache(\n existing: ReviewCache | undefined,\n fetched: Record<string, LocationReviews>,\n): ReviewCache {\n return {\n version: 1,\n locations: { ...existing?.locations, ...fetched },\n };\n}\n","/**\n * The cache — what makes a re-render free and offline.\n *\n * Committed, like `../media/manifest.ts` and for the same reason: reviews change\n * slowly, and a build should not need a live Google credential to render them. The\n * one field that keeps it from being byte-identical across runs is `fetchedAt` per\n * location — acceptable because `vitops reviews` is invoked deliberately\n * (scheduled or manual), not rewritten on every build the way `media-manifest.json`\n * is.\n */\nimport type { LocationReviews, ReviewCache } from './types.ts';\nimport { readVersioned, writeVersioned } from '../json-store.ts';\n\n/** Default location, relative to the consumer's project root. */\nexport const REVIEWS_CACHE_PATH = '.vitops/reviews.json';\n\n/**\n * Read the cache, or `undefined` when there isn't a usable one.\n *\n * A corrupt or wrong-version file reads as absent, exactly `readManifest`'s rule:\n * the alternative — reading it as an empty cache — would be indistinguishable\n * from \"this business genuinely has no reviews\" and silently ship an empty\n * carousel.\n */\nexport function readCache(path: string): ReviewCache | undefined {\n return readVersioned<ReviewCache, 'locations'>(path, 'locations');\n}\n\n/** Build a cache with its keys and each location's reviews in a stable order. */\nexport function toCache(locations: Record<string, LocationReviews>): ReviewCache {\n const sorted: Record<string, LocationReviews> = {};\n for (const key of Object.keys(locations).sort()) {\n const entry = locations[key] as LocationReviews;\n sorted[key] = {\n ...entry,\n reviews: [...entry.reviews].sort((a, b) => a.id.localeCompare(b.id)),\n };\n }\n return { version: 1, locations: sorted };\n}\n\n/** Write the cache, creating its directory. */\nexport function writeCache(path: string, cache: ReviewCache): void {\n writeVersioned(path, cache);\n}\n","/**\n * Google Business Profile — fetching one location's reviews.\n *\n * Reviews live only on the legacy v4 Business Information API\n * (`mybusiness.googleapis.com/v4`); the newer split APIs (Account Management,\n * Business Information v1) never grew a reviews resource. **The calling GCP\n * project must be allowlisted by Google before any call here succeeds** — a\n * one-time access-request form, turnaround measured in days to weeks — so a 403\n * is treated as that known state rather than surfaced as a raw failure. Scope:\n * `SCOPES.businessProfile` (`../google/token.ts`).\n *\n * TODO — re-check when Google migrates the Reviews endpoint off v4 onto a\n * successor API. Endpoint, auth, pagination and the review object's fields may\n * all change; this module is the one place that assumption lives.\n *\n * Auth is the shared exchange in `../google/token.ts`, same as `../indexing/gsc.ts`\n * — no `googleapis` dependency, one REST endpoint.\n */\nimport { googleHeaders, type GoogleAuthLike } from '../google/token.ts';\nimport type { Review } from './types.ts';\n\nconst STAR_RATING: Record<string, number> = {\n ONE: 1,\n TWO: 2,\n THREE: 3,\n FOUR: 4,\n FIVE: 5,\n};\n\ninterface RawReview {\n reviewId: string;\n reviewer?: { displayName?: string; profilePhotoUrl?: string; isAnonymous?: boolean };\n starRating?: string;\n comment?: string;\n createTime: string;\n updateTime: string;\n reviewReply?: { comment: string; updateTime: string };\n}\n\nfunction toReview(raw: RawReview, location: string): Review {\n return {\n id: raw.reviewId,\n location,\n rating: STAR_RATING[raw.starRating ?? ''] ?? 0,\n author: raw.reviewer?.isAnonymous\n ? 'A Google user'\n : (raw.reviewer?.displayName ?? 'A Google user'),\n ...(raw.reviewer?.profilePhotoUrl ? { authorPhoto: raw.reviewer.profilePhotoUrl } : {}),\n ...(raw.comment ? { text: raw.comment } : {}),\n publishedAt: raw.createTime,\n updatedAt: raw.updateTime,\n ...(raw.reviewReply\n ? { reply: { text: raw.reviewReply.comment, at: raw.reviewReply.updateTime } }\n : {}),\n // v4 carries no public review URL on the resource itself. The stable link is\n // `organization.locations[].listings.google` / `sameAs` — the whole profile,\n // not the single review — which is what a \"read more on Google\" affordance\n // should point at instead.\n };\n}\n\n/** One page (or, having paginated, the whole set) of a location's reviews. */\nexport interface FetchReviewsResult {\n ok: boolean;\n reviews: Review[];\n averageRating?: number;\n totalReviewCount?: number;\n status?: number;\n message?: string;\n}\n\n/**\n * Fetch every review for one location, paginating to completion.\n *\n * Never throws — a per-location failure is a structured result so a run across\n * several locations can fetch what it can and report the rest, the same contract\n * `../indexing/gsc.ts`'s `inspectUrl` keeps.\n */\nexport async function fetchReviews(\n auth: GoogleAuthLike,\n accountId: string,\n locationId: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<FetchReviewsResult> {\n const location = locationId.startsWith('locations/') ? locationId : `locations/${locationId}`;\n const account = accountId.startsWith('accounts/') ? accountId : `accounts/${accountId}`;\n const reviews: Review[] = [];\n let averageRating: number | undefined;\n let totalReviewCount: number | undefined;\n let pageToken: string | undefined;\n\n do {\n const url = new URL(`https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`);\n url.searchParams.set('pageSize', '50');\n if (pageToken) url.searchParams.set('pageToken', pageToken);\n\n const res = await fetchImpl(url, { headers: googleHeaders(auth) });\n if (!res.ok) {\n const message =\n res.status === 403\n ? `403 — this GCP project is most likely not allowlisted for the Business Profile API yet ` +\n `(a one-time Google approval, requested separately from every other API here), or the ` +\n `credential is not an owner/manager of ${account}/${location}`\n : `${res.status}: ${await res.text()}`;\n return { ok: false, reviews, status: res.status, message };\n }\n\n const body = (await res.json()) as {\n reviews?: RawReview[];\n averageRating?: number;\n totalReviewCount?: number;\n nextPageToken?: string;\n };\n for (const r of body.reviews ?? []) reviews.push(toReview(r, locationId));\n averageRating = body.averageRating ?? averageRating;\n totalReviewCount = body.totalReviewCount ?? totalReviewCount;\n pageToken = body.nextPageToken;\n } while (pageToken);\n\n return {\n ok: true,\n reviews,\n ...(averageRating !== undefined ? { averageRating } : {}),\n ...(totalReviewCount !== undefined ? { totalReviewCount } : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//#region src/schema/article.d.ts
|
|
2
|
+
interface ArticleAuthor {
|
|
3
|
+
name: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
}
|
|
6
|
+
interface ArticleGraphOptions {
|
|
7
|
+
headline: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
image?: string | string[];
|
|
10
|
+
datePublished: string;
|
|
11
|
+
dateModified?: string;
|
|
12
|
+
author: ArticleAuthor | ArticleAuthor[];
|
|
13
|
+
publisher?: {
|
|
14
|
+
name: string;
|
|
15
|
+
logo?: string;
|
|
16
|
+
};
|
|
17
|
+
mainEntityOfPage?: string;
|
|
18
|
+
articleSection?: string;
|
|
19
|
+
wordCount?: number;
|
|
20
|
+
keywords?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Build a schema.org Article JSON-LD graph. */
|
|
23
|
+
declare function articleGraph(options: ArticleGraphOptions): Record<string, unknown>;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/schema/breadcrumb.d.ts
|
|
26
|
+
interface BreadcrumbItem {
|
|
27
|
+
name: string;
|
|
28
|
+
url: string;
|
|
29
|
+
}
|
|
30
|
+
interface BreadcrumbGraphOptions {
|
|
31
|
+
items: BreadcrumbItem[];
|
|
32
|
+
}
|
|
33
|
+
/** Build a schema.org BreadcrumbList JSON-LD graph. */
|
|
34
|
+
declare function breadcrumbGraph(options: BreadcrumbGraphOptions): Record<string, unknown>;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/schema/faq.d.ts
|
|
37
|
+
interface FAQItem {
|
|
38
|
+
question: string;
|
|
39
|
+
answer: string;
|
|
40
|
+
}
|
|
41
|
+
interface FAQGraphOptions {
|
|
42
|
+
items: FAQItem[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a schema.org FAQPage JSON-LD graph.
|
|
46
|
+
*
|
|
47
|
+
* NOTE: Google recommends FAQ markup only for authoritative government
|
|
48
|
+
* and health sites. For other sites, consider using QAPage schema instead.
|
|
49
|
+
*/
|
|
50
|
+
declare function faqGraph(options: FAQGraphOptions): Record<string, unknown>;
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/schema/itemList.d.ts
|
|
53
|
+
type ItemListEntryType = 'Article' | 'Recipe' | 'Movie' | 'Course' | 'Restaurant' | 'Product' | (string & {});
|
|
54
|
+
interface ItemListEntry {
|
|
55
|
+
type: ItemListEntryType;
|
|
56
|
+
name: string;
|
|
57
|
+
/**
|
|
58
|
+
* Summary-page format: the item's own detail page (unique, same domain).
|
|
59
|
+
* All-in-one-page format: this page's URL plus an HTML anchor pointing at
|
|
60
|
+
* the item on it.
|
|
61
|
+
*/
|
|
62
|
+
url: string;
|
|
63
|
+
image?: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
}
|
|
66
|
+
interface ItemListGraphOptions {
|
|
67
|
+
items: ItemListEntry[];
|
|
68
|
+
/** Recommended, not required by the spec — names the list itself. */
|
|
69
|
+
name?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */
|
|
72
|
+
declare function itemListGraph(options: ItemListGraphOptions): Record<string, unknown>;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/schema/localBusiness.d.ts
|
|
75
|
+
interface OpeningHoursSpecification {
|
|
76
|
+
dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];
|
|
77
|
+
opens: string;
|
|
78
|
+
closes: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A dated deviation from the recurring `openingHoursSpecification` — holiday
|
|
82
|
+
* hours, a one-off closure, a temporary schedule. Same schema.org shape
|
|
83
|
+
* (`OpeningHoursSpecification`), distinguished by carrying `validFrom`. A full
|
|
84
|
+
* closure omits `opens`/`closes` rather than encoding it as an open interval.
|
|
85
|
+
*/
|
|
86
|
+
interface SpecialHoursSpecification {
|
|
87
|
+
validFrom: string;
|
|
88
|
+
validThrough?: string;
|
|
89
|
+
opens?: string;
|
|
90
|
+
closes?: string;
|
|
91
|
+
}
|
|
92
|
+
interface LocalBusinessGraphOptions {
|
|
93
|
+
/** schema.org `LocalBusiness` subtype, e.g. `'Restaurant'`, `'ProfessionalService'`. Defaults to `'LocalBusiness'`. */
|
|
94
|
+
type?: string;
|
|
95
|
+
name: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
url?: string;
|
|
98
|
+
telephone?: string;
|
|
99
|
+
email?: string;
|
|
100
|
+
image?: string | string[];
|
|
101
|
+
priceRange?: string;
|
|
102
|
+
address?: {
|
|
103
|
+
streetAddress?: string;
|
|
104
|
+
addressLocality?: string;
|
|
105
|
+
addressRegion?: string;
|
|
106
|
+
postalCode?: string;
|
|
107
|
+
addressCountry?: string;
|
|
108
|
+
};
|
|
109
|
+
geo?: {
|
|
110
|
+
latitude: number;
|
|
111
|
+
longitude: number;
|
|
112
|
+
};
|
|
113
|
+
openingHoursSpecification?: OpeningHoursSpecification[];
|
|
114
|
+
specialOpeningHoursSpecification?: SpecialHoursSpecification[];
|
|
115
|
+
aggregateRating?: {
|
|
116
|
+
ratingValue: number;
|
|
117
|
+
reviewCount?: number;
|
|
118
|
+
ratingCount?: number;
|
|
119
|
+
bestRating?: number;
|
|
120
|
+
worstRating?: number;
|
|
121
|
+
};
|
|
122
|
+
review?: {
|
|
123
|
+
author: string;
|
|
124
|
+
datePublished: string;
|
|
125
|
+
reviewBody: string;
|
|
126
|
+
reviewRating?: {
|
|
127
|
+
ratingValue: number;
|
|
128
|
+
bestRating?: number;
|
|
129
|
+
worstRating?: number;
|
|
130
|
+
};
|
|
131
|
+
}[];
|
|
132
|
+
paymentAccepted?: string[];
|
|
133
|
+
currenciesAccepted?: string;
|
|
134
|
+
areaServed?: string[];
|
|
135
|
+
sameAs?: string[];
|
|
136
|
+
knowsLanguage?: string[];
|
|
137
|
+
}
|
|
138
|
+
/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */
|
|
139
|
+
declare function localBusinessGraph(options: LocalBusinessGraphOptions): Record<string, unknown>;
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/schema/organization.d.ts
|
|
142
|
+
interface OrganizationGraphOptions {
|
|
143
|
+
name: string;
|
|
144
|
+
url?: string;
|
|
145
|
+
logo?: string;
|
|
146
|
+
description?: string;
|
|
147
|
+
email?: string;
|
|
148
|
+
telephone?: string;
|
|
149
|
+
address?: {
|
|
150
|
+
streetAddress?: string;
|
|
151
|
+
addressLocality?: string;
|
|
152
|
+
addressRegion?: string;
|
|
153
|
+
postalCode?: string;
|
|
154
|
+
addressCountry?: string;
|
|
155
|
+
};
|
|
156
|
+
sameAs?: string[];
|
|
157
|
+
foundingDate?: string;
|
|
158
|
+
founders?: string[];
|
|
159
|
+
numberOfEmployees?: number | {
|
|
160
|
+
minValue: number;
|
|
161
|
+
maxValue: number;
|
|
162
|
+
};
|
|
163
|
+
parentOrganization?: {
|
|
164
|
+
name: string;
|
|
165
|
+
url?: string;
|
|
166
|
+
};
|
|
167
|
+
subOrganization?: {
|
|
168
|
+
name: string;
|
|
169
|
+
url?: string;
|
|
170
|
+
}[];
|
|
171
|
+
contactPoint?: {
|
|
172
|
+
type?: 'customer service' | 'technical support' | 'sales' | 'billing support' | 'reservations';
|
|
173
|
+
telephone: string;
|
|
174
|
+
email?: string;
|
|
175
|
+
contactType: string;
|
|
176
|
+
availableLanguage?: string[];
|
|
177
|
+
areaServed?: string[];
|
|
178
|
+
}[];
|
|
179
|
+
areaServed?: string[];
|
|
180
|
+
brand?: string;
|
|
181
|
+
award?: string[];
|
|
182
|
+
memberOf?: {
|
|
183
|
+
name: string;
|
|
184
|
+
url?: string;
|
|
185
|
+
}[];
|
|
186
|
+
}
|
|
187
|
+
/** Build a schema.org Organization JSON-LD graph. */
|
|
188
|
+
declare function organizationGraph(options: OrganizationGraphOptions): Record<string, unknown>;
|
|
189
|
+
//#endregion
|
|
190
|
+
export { type ArticleAuthor, type ArticleGraphOptions, type BreadcrumbGraphOptions, type BreadcrumbItem, type FAQGraphOptions, type FAQItem, type ItemListEntry, type ItemListEntryType, type ItemListGraphOptions, type LocalBusinessGraphOptions, type OpeningHoursSpecification, type OrganizationGraphOptions, type SpecialHoursSpecification, articleGraph, breadcrumbGraph, faqGraph, itemListGraph, localBusinessGraph, organizationGraph };
|
|
191
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"mappings":";UAAiB,aAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA,UAGY,mBAAA;EACf,QAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA;EACA,YAAA;EACA,MAAA,EAAQ,aAAA,GAAgB,aAAa;EACrC,SAAA;IACE,IAAA;IACA,IAAA;EAAA;EAEF,gBAAA;EACA,cAAA;EACA,SAAA;EACA,QAAA;AAAA;;iBAIc,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,MAAM;;;UCvBjD,cAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA,UAGY,sBAAA;EACf,KAAA,EAAO,cAAc;AAAA;;iBAIP,eAAA,CAAgB,OAAA,EAAS,sBAAA,GAAyB,MAAM;;;UCVvD,OAAA;EACf,QAAA;EACA,MAAM;AAAA;AAAA,UAGS,eAAA;EACf,KAAA,EAAO,OAAO;AAAA;;;;;;;iBASA,QAAA,CAAS,OAAA,EAAS,eAAA,GAAkB,MAAM;;;KCf9C,iBAAA;AAAA,UAWK,aAAA;EACf,IAAA,EAAM,iBAAiB;EACvB,IAAA;EHZA;AACG;AAGL;;;EGcE,GAAA;EACA,KAAA;EACA,WAAA;AAAA;AAAA,UAGe,oBAAA;EACf,KAAA,EAAO,aAAa;EHdpB;EGgBA,IAAA;AAAA;;iBAIc,aAAA,CAAc,OAAA,EAAS,oBAAA,GAAuB,MAAM;;;UC/BnD,yBAAA;EACf,SAAA;EACA,KAAA;EACA,MAAA;AAAA;AJDG;AAGL;;;;;AAHK,UIUY,yBAAA;EACf,SAAA;EACA,YAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,UAGe,yBAAA;EJPf;EISA,IAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;EACA,SAAA;EACA,KAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;IACE,aAAA;IACA,eAAA;IACA,aAAA;IACA,UAAA;IACA,cAAA;EAAA;EAEF,GAAA;IACE,QAAA;IACA,SAAA;EAAA;EAEF,yBAAA,GAA4B,yBAAA;EAC5B,gCAAA,GAAmC,yBAAyB;EAC5D,eAAA;IACE,WAAA;IACA,WAAA;IACA,WAAA;IACA,UAAA;IACA,WAAA;EAAA;EAEF,MAAA;IACE,MAAA;IACA,aAAA;IACA,UAAA;IACA,YAAA;MACE,WAAA;MACA,UAAA;MACA,WAAA;IAAA;EAAA;EAGJ,eAAA;EACA,kBAAA;EACA,UAAA;EACA,MAAA;EACA,aAAA;AAAA;;iBAIc,kBAAA,CAAmB,OAAA,EAAS,yBAAA,GAA4B,MAAM;;;UCnE7D,wBAAA;EACf,IAAA;EACA,GAAA;EACA,IAAA;EACA,WAAA;EACA,KAAA;EACA,SAAA;EACA,OAAA;IACE,aAAA;IACA,eAAA;IACA,aAAA;IACA,UAAA;IACA,cAAA;EAAA;EAEF,MAAA;EACA,YAAA;EACA,QAAA;EACA,iBAAA;IAA+B,QAAA;IAAkB,QAAA;EAAA;EACjD,kBAAA;IACE,IAAA;IACA,GAAA;EAAA;EAEF,eAAA;IACE,IAAA;IACA,GAAA;EAAA;EAEF,YAAA;IACE,IAAA;IACA,SAAA;IACA,KAAA;IACA,WAAA;IACA,iBAAA;IACA,UAAA;EAAA;EAEF,UAAA;EACA,KAAA;EACA,KAAA;EACA,QAAA;IACE,IAAA;IACA,GAAA;EAAA;AAAA;;iBAKY,iBAAA,CAAkB,OAAA,EAAS,wBAAA,GAA2B,MAAM"}
|