Update bunny deploy script for build:edge - #2
Merged
Conversation
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
4 tasks
This was referenced Jun 19, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.