Skip to content

Update bunny deploy script for build:edge - #2

Merged
stefan-burke merged 3 commits into
mainfrom
claude/update-bunny-deploy-script-ymkaB
Jan 21, 2026
Merged

Update bunny deploy script for build:edge#2
stefan-burke merged 3 commits into
mainfrom
claude/update-bunny-deploy-script-ymkaB

Conversation

@stefan-burke

Copy link
Copy Markdown
Member

No description provided.

The build step referenced a non-existent script. Bunny Edge
Scripting can run TypeScript directly without a build step.
Basic edge script with health check endpoint.
- Add build:edge script to bundle edge entry point
- Create src/edge/bunny-script.ts entry point that wraps handleRequest
- Add @bunny.net/edgescript-sdk dependency
- Restore build:edge step in deploy workflow
- Build script rewrites imports to esm.sh URLs for edge runtime
@stefan-burke
stefan-burke merged commit 5efa2e9 into main Jan 21, 2026
1 check passed
@stefan-burke
stefan-burke deleted the claude/update-bunny-deploy-script-ymkaB branch January 31, 2026 03:26
stefan-burke pushed a commit that referenced this pull request Jul 2, 2026
…cation

Codex #2: a valid export with >25 parents tripped the request N+1 guard
(one getParentIds query per parent) and the transaction round-trip cap (one
INSERT per parent). Batch the nested-parent check via getChildListingIds and
make addParentEdgesTx a single multi-row INSERT.

Codex #3: the group-duplicate flow cloned listing names verbatim via raw
insertStatement, bypassing the new uniqueness validator and leaving duplicate
names that make later name-based imports ambiguous. Validate the new group
name and every clone name (against the catalog and within the batch) before
writing, rejecting with an operator-facing message. Existing duplicate tests
updated to supply a find/replace that keeps clone names unique.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX
BlueHairMinerBoy pushed a commit to BlueHairMinerBoy/tickets that referenced this pull request Jul 4, 2026
…ueness (chobbledotcom#1511)

* Enforce cross-entity name uniqueness for listings and groups

Add a name-registry that treats listing and group display names as one
namespace: a listing may not share a name with another listing or with a
group, and vice versa. Names are the stable key the coming catalog
import/export will reference, so uniqueness is what keeps that resolution
unambiguous.

- New src/shared/db/name-registry.ts: cached, decrypt-in-memory name
  index (no schema column needed) providing isNameTakenAnywhere plus
  name→id resolution (matchName) with missing/ambiguous outcomes.
- Wire the check into validateListingInput and validateGroupWithPackage
  (covering HTML forms and the admin JSON API for both entities), and add
  the missing validate to the group create resource so creation is
  guarded too.
- New error.name_in_use message.
- Tests for the registry and both validators; update the old
  "duplicate slug auto-uniquifies" listing test to assert the new
  name-uniqueness rejection.
- Drive-by: fix a pre-existing noUselessFragments lint error in
  entity-pages.tsx that blocks lint:ci.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Add catalog transfer engine: export/import a listing or group as JSON

Introduce src/features/admin/catalog-transfer with a versioned, id-free
JSON format that captures a single listing or group and all its facets —
prices, group memberships (with package price/quantity/day overrides),
and parent references — cross-referenced by name so a blob is portable
across installs.

- schema.ts: valibot discriminated union (listing/group) as the single
  source of truth; validates an incoming blob at the boundary and types
  the exporter's output. formatTransferIssues renders per-field messages.
- export.ts: build the blob from the decrypted stored row and its facets,
  resolving every reference to a name. Images/attachments, ledger, and
  attendees are deliberately excluded.
- import.ts: parse → check name uniqueness → resolve parents/groups/
  members by name (intelligible missing/ambiguous errors) → reuse the
  shared listing/group validators (type compatibility, package rules,
  parent-edge compatibility) → write in one transaction. Never throws for
  bad input.

Supporting db helpers: getListingGroupMemberships + addGroupMembershipTx
(groups), listingGroupDayInsertStatements (listing-prices, targeted so
importing into a populated package can't disturb other members),
addParentEdgesTx (listing-parents), and an exported allPackageableMembers.

Round-trip and validation-failure tests cover both entity kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Wire catalog import/export into the admin UI and routes

Add content-gated admin routes and UI for the transfer engine:

- GET /admin/listing/:id/export.json and /admin/groups/:id/export.json
  download the entity as a JSON attachment (named from the entity).
- GET/POST /admin/catalog/import: an upload form that parses the JSON,
  runs importCatalog, and redirects with a success flash to the created
  entity's list — or an intelligible error flash on any failure (invalid
  JSON, missing file, or a validation/resolution error).
- Export links on the listing and group detail pages; an "Import from
  file" button on the listings and groups list pages.
- New catalog-transfer i18n namespace.

Route- and template-level tests cover download, the round-trip upload,
and every error path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Mutation-harden the catalog-transfer modules

Strengthen tests so the transfer schema/export/import/routes reach a 100%
mutation kill rate, and record the genuinely-equivalent survivors:

- Cover a package member priced explicitly free (0 vs null), closesAt
  preservation and the post-import price re-sync, an incompatible
  parent edge, a package-member-with-parent conflict, root-type and
  nested-field parse messages, and activity logging on import.
- Simplify formatTransferIssues to root+nested only (the object-variant
  schemas never produce a pathless "other" issue), removing dead
  branches.
- Record array/object `?? → ||` equivalents in equivalent-mutants.txt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Cover the new export/import UI links and the entity-pages refactor

Add rendering assertions for the listing/group export links and the
import-from-file buttons, and test the entity-pages activity "view all"
link — bringing every template/module the feature touched under a changed
test so the mutation gate has its killers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Move import-only membership writers out of shared db files

Extract the catalog-import group-membership read/write helpers into
src/features/admin/catalog-transfer/membership.ts, reverting the
additions to src/shared/db/groups.ts and src/shared/db/listing-prices.ts
so those heavily-shared modules leave the change set entirely
(addGroupMembershipTx now reuses the existing groupDayPriceStatements,
dropping its leading full-group DELETE).

Bring the covering tests for the remaining touched modules into the
change set with real assertions: addParentEdgesTx, the group add-listings
activity log, an API-level group name-uniqueness rejection, and the
sold-hidden-package delete guard. Refresh the shifted listing-parents
equivalent-mutant line numbers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Lock editor access to catalog import/export

Add editor-role tests: an editor can open the import page and the
listing/group export routes (all content-gated), and can import a
listing from an uploaded JSON file. The export links themselves live on
the staff-only detail pages, but the export routes are content-gated so
editors reach them directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Close pre-existing mutation gaps exposed by the name-uniqueness change

Touching validateListingInput / validateGroupWithPackage pulls their whole
files into the mutation gate, surfacing latent coverage gaps in code the
feature does not itself change. Fill them with direct tests and record the
provably-equivalent survivors:

- listings-actions: unit tests for toggleListingActive (no-op, deactivate,
  reactivate + activity log), performListingDelete (row + storage-file
  cleanup + activity log), package-membership edge rules (child-of-another,
  hidden vs visible package gating children), renewal config (mixed
  purchase-only/hidden), maxPrice arithmetic, and the create-ignores-slug
  path. Record the optional-field `?? →||` defaults as equivalents.
- groups: a regular group with a sold-out but visible member stays
  shareable (kills the share-computation `!` mutant); record the object/
  Map/0-fallback `?? →||` equivalents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Address Codex review and close catalog-transfer coverage gaps

Apply the review fixes (bounded batched membership writes, day-count key
validation, builder-disabled and editor policy handling, duplicate-reference
rejection, nested-parent guard) and bring the new modules to 100% line and
branch coverage: drop the unreachable name-lookup guards in the exporter, pass
resolved group ids into the parent-edge check to remove a dead nullish branch,
and cover the empty-name, punctuation-only-slug, and package-member-as-child
edges. Dedupe the two builder-site import tests behind a shared helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Fix CI: alias-rule violation and duplicate-name test fixtures

- schema.ts: define PriceSchema via intAtLeast(0) instead of aliasing
  NonNegativeIntSchema, which the code-quality no-aliasing rule forbids.
- admin-api-security: give the two content-type-case POSTs distinct listing
  names so the second is not rejected by the new name-uniqueness rule (the
  test is about content-type handling, not uniqueness).
- server-agent-deliveries: the run-sheet tie-break fixture created two
  listings with the same name; names are now unique, so the second uses a
  distinct name and the tie-break resolves deterministically by name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Harden catalog import/export per second Codex review

- Hide webhook_url from listing exports for editors (P1): the edit form
  already hides this PII-sink URL from editors, so an editor's export must
  not reveal it. exportListing takes the admin level and excludes the column.
- Clear uses_logistics on import when logistics is disabled, mirroring the
  form which forces it off (assign_built_site already did this).
- Reject non-date closesAt/date blobs with a field error instead of letting
  the datetime normaliser silently store an empty value.
- Constrain bookableDays to real weekday names so a typo is a field error
  rather than a daily listing whose dates never match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Batch parent-edge import; enforce name uniqueness in bulk group duplication

Codex chobbledotcom#2: a valid export with >25 parents tripped the request N+1 guard
(one getParentIds query per parent) and the transaction round-trip cap (one
INSERT per parent). Batch the nested-parent check via getChildListingIds and
make addParentEdgesTx a single multi-row INSERT.

Codex chobbledotcom#3: the group-duplicate flow cloned listing names verbatim via raw
insertStatement, bypassing the new uniqueness validator and leaving duplicate
names that make later name-based imports ambiguous. Validate the new group
name and every clone name (against the catalog and within the batch) before
writing, rejecting with an operator-facing message. Existing duplicate tests
updated to supply a find/replace that keeps clone names unique.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Unify listing field-value validation across form, API, and import

Codex kept finding fields where a raw import/API build accepts a value the
listing form would reject (over-cap duration silently clamped, invalid contact
fields dropped, bad weekday names, impossible datetimes rolled over). Root
cause: the form's per-field validators live only in the form's field defs, so
the JSON API and the catalog import bypass them.

- Extract those validators (contact fields, weekday names, duration cap, plus
  a combined validateListingFieldValues) into #shared/listing-field-validators;
  the form re-exports them so its behaviour is unchanged, and the JSON API now
  runs them too (validateListingApiInput).
- The catalog transfer schema validates the same field values at its wire
  boundary, including a self-contained strict datetime check that rejects
  impossible calendar dates (e.g. 2026-02-30) rather than letting the storage
  layer roll them over. Kept self-contained (no Temporal/timezone import) so
  this early-loaded schema module stays out of the settings-loading graph.
- Field-value import tests live in their own file to keep the main
  catalog-transfer suite's per-request read count under the N+1 guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Scope import field validation to the schema; fix datetime regex anchor

The previous commit ran the form's field validators through the JSON API too,
but the API deliberately CLAMPS out-of-range values (no form layer — see the
duration-days e2e), so rejecting there broke intended behaviour and a
code-quality gate. Revert the API and the fields.ts extraction; keep the actual
fix — the catalog transfer schema validates datetime, duration cap, contact
fields, and weekday names at its wire boundary, which is where the import
findings apply.

Also anchor the datetime regex so a valid prefix with trailing junk
("2030-01-01T00:00not-a-zone") is a field error rather than being silently
emptied by the storage normaliser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Import: clear non-package overrides, reject hidden-package parents, disable in demo

Three more Codex import-boundary fixes:
- Clear package price/quantity/day-price overrides for any membership whose
  group isn't a package (listing import, per group) or whose imported group
  blob isn't a package — matching the normal group save, so a blob can't plant
  a hidden free price that activates if the group is later converted.
- Reject a parent that is a hidden-package member (batched via
  getGroupIdsByListingIds + the cached group set, no N+1), mirroring the edge
  editor's packageChildEdgeConflict rule.
- Disable catalog import in demo mode, since a raw blob bypasses the form's
  demo-field scrubbing and webhook clearing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Batch group-compat validation so many-group imports stay under the N+1 guard

Importing a listing that belongs to many groups routed through
validateListingInput, whose group check ran one sibling SELECT per group
via validateGroupListingType. Past ~25 groups the per-request N+1 read
guard tripped before any write, so a valid export of a group-heavy
listing could not be imported.

validateListingGroup now batches those reads: one cached getAllGroups()
plus one getListingsByGroupIds() for every referenced group, with the
homogeneity check run in memory per group via a new groupListingTypeError
helper (the pure core validateGroupListingType now delegates to).
getListingsByGroupIds is the batched, inactive-inclusive form of
getListingsByGroupId; getActiveListingsByGroupIds becomes a thin wrapper
over it. Reads are now constant regardless of group count, and the change
is behaviour-preserving for the form and API create/edit paths.

Covered by an "imports a listing that belongs to many groups" test (30
groups).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Fix many-group import test; reject bad datetime offsets; batch package-group check

Three follow-ups from review on the prior commit:

- The "imports a listing that belongs to many groups" test created its 30
  groups via createTestGroup, whose trailing getAllGroups() re-reads the
  cache (invalidated by each create) 30 times in one test context and trips
  the N+1 read guard during setup. Build the groups with a direct
  groupsTable.insert instead, mirroring the many-parents test.

- isStorableDatetime accepted out-of-range timezone offsets like "+99:99"
  because the offset digits were never range-checked; the storage
  normaliser then treats the whole value as invalid and silently empties
  the column. Capture the offset hours/minutes and reject > 23 / > 59, and
  cover it with an "out-of-range offset" import test.

- firstPackageGroup still did one groupsTable.findById per group, so a
  child listing that joins many groups could trip the N+1 guard before the
  batched parent validation. Resolve against the request-cached group set
  via a new shared getGroupsById() helper (also used by
  validateListingGroup, replacing its inline map). Covered by an "imports a
  child listing that also belongs to a regular group" test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Reject fractional datetimes without a seconds component on import

The datetime regex placed the optional fractional-seconds group outside
the optional seconds group, so "2030-01-01T00:00.123Z" (a fraction with no
seconds) passed validation; the storage normaliser then treats the whole
value as invalid and silently empties the date/closesAt column. Tie the
fractional part to the seconds group so a fraction is only accepted after
seconds. Covered by "rejects fractional seconds without a seconds
component" and a positive "accepts fractional seconds after a seconds
component" test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Reject unsafe-integer prices in catalog imports

intAtLeast only checked integer-ness and the lower bound, so a price like
1e100 (an "integer" to Number.isInteger but outside the safe range) passed
and would be rounded or throw a raw error at the storage layer. Require
v.safeInteger() in the shared intAtLeast helper (and DurationDaysSchema),
which covers prices, quantities, counts, and durations at once, matching
the form's money parser. Covered by a "rejects a price above the
safe-integer range" test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Reject package day-price overrides for spans a member doesn't offer on import

The package editor only renders day-price override inputs for a member's
available day counts (a customisable listing's priced spans within its
duration). A raw import bypassed that, persisting a group_day override for
any day count in the global 1..90 range — a hidden row that could activate
after a later duration/day-price edit.

Add memberDayOverrideError, reused on both membership sides: a group import
validates each existing member's overrides, and a listing import validates
the new listing's own group overrides, against availableDayCounts. Out-of-
range overrides are now a field-level error. The tests live in a new
catalog-transfer-packages.test.ts (creating customisable members is
read-heavy; folding them into catalog-transfer.test.ts tips that file past
the per-request N+1 read guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Format catalog-transfer-packages test (biome)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Run child add-on reachability check on imported parent edges

An imported listing created as a child that also joins a group could
inherit a group-scoped opt-in add-on. If that add-on's would-be scope
reached only the (suppressed) child and not the parent's booking page, the
add-on became unbookable — the dead-end the interactive child-edge editor
already rejects, but which the import skipped (it stopped at field
compatibility).

validateParentEdges now builds the would-be listing set (the new child
appended at placeholder id 0 with its would-be group memberships) and,
per named parent, runs the same reachability core the edge editor uses.
To keep it under the request N+1 guard for a many-parent import, add
childOnlyAddOnCheckerForListings, which resolves every add-on's scope once
and returns a reusable per-parent checker. Covered by rejects/accepts
tests in a new catalog-transfer-reachability.test.ts (its modifier setup
is read-heavy, so it lives outside catalog-transfer.test.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Make export tolerant of unexportable rows; filter over-duration day prices

Two linked findings on the export/import schema:

- Export no longer 500s on a stored row the transfer schema can't
  represent. A listing created through the JSON API can hold values the
  API accepts but the transfer format rejects (e.g. bookable_days
  ["Funday"], an invalid fields string), and export ran them through a
  strict v.parse that threw. exportListing/exportGroup now return a
  CatalogExportError (via a safeParse helper) which the download route
  surfaces as an operator-facing 422, never a raw 500.

- A listing's own dayPrices beyond its durationDays are now filtered on
  import, matching the form which only reads day_price_1..durationDays. A
  "5"-day price on a 2-day listing was persisted and would activate if the
  duration were later raised; it's dropped instead. (This is the listing's
  own field; package-member overrides for unoffered spans are still
  rejected, mirroring the package editor.)

Covered by "returns 422 …" listing and group export tests and a "filters a
listing's own day prices beyond its duration" import test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Cover the default-duration branch of the day-price filter

The dayPrices filter's `durationDays ?? 1` right operand was never
exercised (every test set durationDays), so deno flagged schema.ts:209.
Add an import test with dayPrices and no durationDays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Dedupe the day-price import tests via a shared helper

The new default-duration test duplicated the import+unwrap+getListing
block, tripping the test cpd gate. Extract importStoredListing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Carry bookable_alone through catalog transfers

The transfer schema predated the bookable_alone column, so rowToInput
supplied bookableAlone but ListingFieldsSchema never declared it and
parseExport dropped it. A re-imported child then defaulted to false,
losing its standalone /ticket page, catalog/API eligibility, and any
add-on reachability that depended on the child's own page. Declare the
field so it round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Drop the now-dead group-siblings fallback in listing validation

The merge unified getListingsByGroupIds to seed an entry for every id it
is asked about, so siblingsByGroup.get(groupId) always resolves for the
ids validateListingGroup iterates. The '?? []' fallback was therefore
unreachable — a branch the 100% coverage gate could never cover. Assert
the invariant instead so the branch is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

* Exempt bookable-alone children from the import add-on reachability check

Now that imports carry bookableAlone, a child imported with
bookable_alone=true kept its own /ticket page but was still run through
the child-only add-on reachability guard, so a valid exported child in an
add-on-scoped group could be rejected when its parent wasn't in scope.
The edge editor exempts a bookable_alone child (its own page still offers
the add-on); mirror that by skipping the guard here too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zHSr7TjvPW4WVciygozHX

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants