@crvouga/mockingbird-service-google-maps 0.1.0

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 ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @crvouga/mockingbird-service-google-maps
2
+
3
+ ## 0.1.0 (2026-09-22)
4
+
5
+ Initial release.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @crvouga/mockingbird-service-google-maps
2
+
3
+ Mock of the **Google Maps Platform** surface our member app uses for addresses: Places
4
+ Autocomplete, Place Details and Find Place From Text (the JSON web services), the Geocoding API,
5
+ and a **Maps JavaScript API shim** (`/maps/api/js?libraries=places`) exposing
6
+ `google.maps.places.*` and `google.maps.Geocoder` over the same data. Answers come from a corpus
7
+ matching our QA fixtures, so the address step that waits 5 s for Google predictions (and then
8
+ falls back to manual entry) resolves instantly and deterministically.
9
+
10
+ - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/google-maps/SUPPORT.md)
11
+ - Google publishes no OpenAPI document for these endpoints: `openapi.yaml` is hand-authored
12
+ from Google's documented shapes and the fields our consumer reads.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -D @crvouga/mockingbird-service-google-maps
18
+ ```
19
+
20
+ ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
21
+ `npx mockingbird-google-maps serve`, `createServer` from `./server` (Node), or `createRuntime`
22
+ with any Fetch server.
23
+
24
+ ## Usage
25
+
26
+ The app hardcodes `https://maps.googleapis.com` (seam **G-Y1**: a base-URL env for
27
+ `M/lib/ui/address-autocomplete/address-autocomplete-native-rest.tsx`,
28
+ `M/features/bloodwork/shared/lab-finder/use-geocoded-address.ts` and
29
+ `M/lib/ui/google-maps/load-google-maps-script.ts`). Until it lands, rewrite that host to the mock
30
+ in the stack's web dist (`stack-web-dist.ts`), as for PostHog. `PLACES_KEY` can be any non-empty
31
+ string unless you restrict keys.
32
+
33
+ ```bash
34
+ npx mockingbird-google-maps serve --port 8814 --api-key "$PLACES_KEY"
35
+ ```
36
+
37
+ ```ts
38
+ import { createRuntime } from "@crvouga/mockingbird-service-google-maps"
39
+
40
+ const maps = createRuntime()
41
+ const get = async (path: string) =>
42
+ (await maps.fetch(new Request(`http://maps.test${path}`))).json()
43
+
44
+ const { predictions } = await get(
45
+ "/maps/api/place/autocomplete/json?input=1625%20N%20Central&types=address&components=country:us&key=k",
46
+ )
47
+ const details = await get(
48
+ `/maps/api/place/details/json?place_id=${predictions[0].place_id}&fields=address_component&key=k`,
49
+ )
50
+ // details.result.address_components → 1625 / N Central Ave / Phoenix / AZ / 85004
51
+
52
+ // Make the next two autocomplete calls fail, as QA's manual-entry fallback expects.
53
+ maps.applyPreset("autocomplete_over_query_limit", "default", { count: 2 })
54
+ ```
55
+
56
+ On web, load `<mock>/maps/api/js?key=…&libraries=places` exactly as the app loads Google's
57
+ script; the shim calls back into the mock's REST endpoints on the same origin and namespace.
58
+
59
+ ### Routes
60
+
61
+ Every web-service answer is **HTTP 200** with Google's `status` (`OK`, `ZERO_RESULTS`,
62
+ `INVALID_REQUEST`, `NOT_FOUND`, `REQUEST_DENIED`, `OVER_QUERY_LIMIT`, `UNKNOWN_ERROR`) and, for
63
+ errors, Google's `error_message`. Responses carry `access-control-allow-origin: *` so the shim
64
+ works from a browser.
65
+
66
+ | Route | Behaviour |
67
+ | --- | --- |
68
+ | `GET /maps/api/place/autocomplete/json` | `input` (required, else `INVALID_REQUEST`), `types`, `components=country:us`, `sessiontoken`. `predictions[]` with `description`, `place_id`, `reference`, `structured_formatting{main_text, main_text_matched_substrings, secondary_text}`, `terms`, `types`, `matched_substrings`; at most 5. Matches corpus rows by token prefix (street, city, state, ZIP); `"<number> <street> <corpus city>[ <ST>][ <ZIP>]"` is **synthesized** in that row's city/state/ZIP (QA's fuzzed search). Nothing → `ZERO_RESULTS`; a non-US `components` → `ZERO_RESULTS`. |
69
+ | `GET /maps/api/place/details/json` | `place_id` (required), `fields` (honoured: `address_component` returns only `address_components`; unknown field → `INVALID_REQUEST`), `sessiontoken`. `result` with `address_components` (`street_number`, `route`, `locality`, `administrative_area_level_2`, `administrative_area_level_1` long/short, `country`, `postal_code`), `formatted_address`, `geometry{location, viewport}`, `place_id`, `types`, `name`, `url`, `vicinity`. Unknown id → `NOT_FOUND`. |
70
+ | `GET /maps/api/geocode/json` | `address`, `place_id` or `components=postal_code:…` (none → `INVALID_REQUEST`). `results[0]` with `address_components`, `formatted_address`, `geometry{location, location_type, viewport}`, `place_id`, `types`. A full address (`"line1, city, ST zip"`) resolves to its row or a synthesized address; a bare ZIP or `"City, ST"` to the ZIP centroid; a street without a locatable city is `ZERO_RESULTS`. |
71
+ | `GET /maps/api/place/findplacefromtext/json` | `input` + `inputtype=textquery` (else `INVALID_REQUEST`), `fields` (default: `place_id` only, as Google). Geocoding first, then the looser autocomplete match, so it finds what our geocode fallback needs. |
72
+ | `GET /maps/api/js` | The shim (`text/javascript`): `google.maps.places.AutocompleteService#getPlacePredictions`, `PlacesService#getDetails` / `#findPlaceFromQuery`, `AutocompleteSessionToken`, `PlacesServiceStatus`, `google.maps.Geocoder#geocode`, `GeocoderStatus`, `LatLng` (`lat()`/`lng()`), `LatLngBounds`, `importLibrary`. Callbacks and promises (rejecting with `MapsRequestError` on errors when no callback is given). Calls `window[callback]` for `&callback=`, and `window.gm_authFailure()` when the key is refused. |
73
+
74
+ ### Corpus
75
+
76
+ 59 addresses: every row of QA's `ROUTING_ZIP_CORPUS` (`packages/qa/src/world/gen/addresses.ts`,
77
+ ≥1 real ZIP per state + DC, ids kept), which includes the **Phoenix AZ demo member**
78
+ (`ADDRESS_AT_HOME_PHLEBOTOMY`: 1625 N Central Ave, Phoenix, AZ 85004) and the other
79
+ `packages/app/src/test-addresses` members, plus `ADDRESS_AT_HOME_PHLEBOTOMY_2` (501 N 5th St).
80
+ Each has its real county and the city's coordinates. Place ids are stable (`ChIJ…` for rows;
81
+ synthesized addresses get an `Ei…` id carrying the address, as Google's own address ids do, so
82
+ Details needs no stored state). `GET /health` reports the corpus.
83
+
84
+ ### Admin (beyond the standard contract)
85
+
86
+ | Route | Effect |
87
+ | --- | --- |
88
+ | `GET /__admin/corpus` | The namespace's addresses (custom first) and the custom count. |
89
+ | `PUT /__admin/corpus` | `{addresses: [{line1, city, state, zip, id?, county?, lat?, lng?}]}` replaces the namespace's custom addresses (on top of the built-in corpus). |
90
+ | `DELETE /__admin/corpus` | Drop the custom addresses. |
91
+ | `GET/PUT /__admin/settings` | `{keys?: string[], publicUrl?: string \| null}`. `keys` restricts accepted API keys (others get `REQUEST_DENIED` "The provided API key is invalid."; a missing key always does). `publicUrl` is the origin the JS shim calls back to when it differs from the request's (a rewriting proxy). |
92
+
93
+ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /__admin/faults/presets`):
94
+ `over_query_limit`, `request_denied`, `unknown_error`, `zero_results` (every web service, HTTP 200
95
+ with that status), `geocode_zero_results` (exercises our Find Place fallback),
96
+ `autocomplete_over_query_limit` (two of these flip our sheet to manual entry), `server_error`
97
+ (HTTP 500), `slow` (6 s, past QA's 5 s wait), `script_unavailable` (the JS loader answers 503, so
98
+ the script's `onerror` fires).
99
+
100
+ ### Namespaces
101
+
102
+ `x-mockingbird-namespace`, a `/ns/<name>` prefix on the base URL (the JS shim served under a
103
+ prefix calls back through it), or by API key:
104
+ `PUT /__admin/credentials {"credentials": {"<PLACES_KEY>": "<namespace>"}}` (the `key` query
105
+ parameter is the credential). The request journal records operation, status, the resolved
106
+ `placeId` and the `sessionToken`; never the typed address.
107
+
108
+ ### Tests
109
+
110
+ - `google-maps.property.test.ts`: self-parity over random walks of all four web services (each
111
+ exercised, every response validated against the spec) and a deliberately divergent instance
112
+ caught.
113
+ - `google-maps.acceptance.test.ts`: drives `test/consumer.ts`, a port of our
114
+ `address-autocomplete-native-rest.tsx` (URL builders, parsers, the two-consecutive-failures
115
+ manual-entry switch), `parse-place-details.ts`, `address-autocomplete-web.tsx` and
116
+ `use-geocoded-address.ts` (native REST and web through the shim evaluated in a fake window).
117
+ Also served over HTTP.
118
+ - There is no SDK drop-in test: native uses plain `fetch`, and the web SDK is Google's hosted
119
+ script, which the shim replaces.
120
+ - `bun scripts/parity.ts`: live parity against `maps.googleapis.com` with
121
+ `MOCKINGBIRD_GOOGLE_MAPS_API_KEY` (env or Vault `secret/personal/prd`); exits 2 without it.
122
+
123
+ ### Deliberately not modelled
124
+
125
+ - Addresses outside the corpus: only corpus rows and streets synthesized in corpus cities
126
+ resolve. Add more with `PUT /__admin/corpus`.
127
+ - Google's route expansion (`N Central Ave` → long_name `North Central Avenue`): `long_name`
128
+ echoes the corpus spelling so QA's line1 round-trips. Live parity will show this difference.
129
+ - Maps rendering (`google.maps.Map`, markers, tiles, Static Maps), the new Places API
130
+ (`places.googleapis.com`, `Place` class, `AutocompleteSuggestion`), reverse geocoding
131
+ (`latlng=`), `locationbias`/`radius`, and billing/quotas beyond the presets.
132
+ - Session-token billing semantics: tokens are accepted and journaled, nothing more.
133
+
134
+ ## API
135
+
136
+ | Export | Kind | Description |
137
+ | --- | --- | --- |
138
+ | `GoogleMapsAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `corpus()`, `state`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `corpus`, `settings`. |
139
+ | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, journal). Options: `corpus`, `settings`, `clock`, `seed`, `adminKey`, `onLog`. |
140
+ | `GOOGLE_MAPS_PRESETS` | object | Every named fault preset. |
141
+ | `GOOGLE_MAPS_NAMESPACE` | string | The service name, `"google-maps"`. |
142
+ | `keyCredential` | function | The `key` query parameter of a request (how API keys map to namespaces). |
143
+ | `DEFAULT_CORPUS`, `PHOENIX_DEMO_ADDRESS`, `STATE_NAMES` | values | The address corpus, the Phoenix demo member row, and state names for `administrative_area_level_1`. |
144
+ | `corpusPlaceId` | function | The stable place id of a corpus row. |
145
+ | `mapsJavaScript` | function | Render the Maps JavaScript shim for `{base, key, authFailed, callback}`. |
146
+ | `normalize` | function | The address normalization used for matching. |
147
+ | `MISSING_KEY_MESSAGE`, `INVALID_KEY_MESSAGE` | strings | Google's `error_message` for a missing / refused key. |
148
+ | `document`, `operationIds`, `supportedOperationIds` | values | The OpenAPI contract and its operation ids. |
149
+ | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--api-key`, `--public-url`); port 8814. |
150
+
151
+ Part of [mockingbird](https://github.com/crvouga/mockingbird).