fix(api): reconcile plugin tables on upgrade and skip a missing one on uninstall - #3
Merged
zscontributor merged 19 commits intoJul 31, 2026
Merged
Conversation
…private products from lockfile
…arity on API routing
…; clarify job count, add changelog details, and enhance localization explanations
- Implemented `sortHref` function to manage sorting behavior in list URLs. - Created `ResourceForm` component for creating and editing plugin resources. - Added tests for detail fields, including sorting and filtering functionality. - Developed comprehensive tests for `PluginAdminController` to ensure correct behavior for detail and list operations, including permission checks and input validation.
The admin answers at /admin on every tenant hostname, so the login page
already knows which site it belongs to — it just had no session to ask
with, and greeted everyone with the same Z-CMS mark on every domain.
- cms-api: GET /public/sites/branding?hostname= returns { siteId, name,
brand, host }. Public and rate limited (120/min/IP): the caller must
know a hostname to ask, and every field is already on that hostname's
public home page. Unlike RenderService.resolveHost it answers for a
site in ANY status — a draft site's owner still has to sign in.
Cross-tenant lookup via the system client, www/apex folded, cached ten
minutes under cms:brand:<host>.
- CacheService.forgetHosts purges the new key alongside cms:host:<host>,
so a renamed site or a new logo shows on the next reload rather than
whenever the TTL lapses.
- admin-web: the login page resolves x-forwarded-host through that
endpoint and leads with the site's logo and name, plus a link out to
the site's front page. Logo URLs are vetted (http/https or a path on
this origin) because they are values out of a JSON column. Anything
that does not resolve — the dedicated admin hostname, an unregistered
domain, an API outage — renders exactly the page it always was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Added optional `group` field to `FormFieldSchema` for organizing fields. - Introduced `FormGroupSchema` to define groups of fields with labels and actions. - Updated `FormDefinitionSchema` to include optional groups. - Enhanced validation logic to check for duplicate group IDs and ensure fields reference valid groups. - Implemented `checkFormField` and `checkFormValues` functions for improved client-side validation. - Added tests for new validation logic and form group functionality. feat(reference-field): implement reference field component for better user experience - Created `ReferenceField` component to allow users to search and select references. - Integrated debounced fetching of options from the server based on user input. - Improved accessibility and user interaction with clear labels and loading states. feat(return-to): implement return-to functionality for record navigation - Added `withReturnTo` and `returnHref` functions to manage navigation back to lists with preserved query parameters. - Implemented `searchOf` function to normalize search parameters for safe URL handling. test: add comprehensive tests for form validation and return-to functionality - Developed tests for `checkFormField` and `checkFormValues` to ensure accurate error reporting. - Created tests for `return-to` utility functions to validate query parameter handling. - Added tests for `FormIsland` component to verify inline validation and group behavior.
… input processing
…nhance archive handling
- Added "fieldOutOfRange" error message in English, Japanese, and Vietnamese locales. - Introduced "newTitle" and "referenceForbidden" messages in English, Japanese, and Vietnamese plugin locales. feat(plugin-sdk): enhance plugin resource column functionality - Added `editable` property to `PluginResourceColumn` interface to allow inline editing of list columns. - Introduced `columnBounds` property to define numeric column limits (min/max) for better data validation. - Implemented `pluginColumnBounds` function to retrieve declared bounds for numeric columns. - Updated `coercePluginRow` function to validate numeric inputs against declared bounds, adding range error handling. feat(admin-web): create new record creation panel and page - Implemented `CreatePanel` component for handling new record creation with proper routing and state management. - Developed `PluginRecordCreatePage` to manage the create screen for plugin resources, including form handling and navigation. feat(site-runtime): add form pick functionality for plugin forms - Created `form-pick` module to manage item selection and quantity for forms, enhancing user interaction with plugin-driven forms. - Implemented tests for form pick functionality to ensure correct behavior and integration with local storage. test: add comprehensive tests for new features - Added tests for `form-pick` functionality to validate item selection and quantity management. - Created tests for `plugin-query` to ensure correct attribute handling and data binding in dynamic rows.
…ndpoint handling and add tests
… and quantities in the form
…on fixes - Added detailed comments to clarify the hydration process and its implications on React rendering. - Implemented a drawer mechanism for the form pick feature, allowing users to view selected items without navigating away from the current page. - Updated the `writePicks` function to repaint the UI correctly based on the current state of the picks. - Created comprehensive tests for the drawer functionality, ensuring it opens correctly, displays the right items, and handles user interactions (incrementing, decrementing, and removing items). - Introduced hydration tests to prevent React hydration errors by using `suppressHydrationWarning` on elements modified by the runtime. - Enhanced the plugin query system to support scoped queries, allowing for dynamic updates based on user selections.
…andle missing tables
The generated `src/locales.ts` carries each locale's coverage, and adding the stale-deploy error strings moved Japanese from 98% to 99%. `pnpm --filter @zcmsorg/i18n test` refuses to pass while the checked-in catalogue disagrees with the locale files, which is what caught it.
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.
Three faults found while shipping a plugin version that introduces a new table. Each one made the previous one harder to see.
1. An upgrade of a RUNNING plugin never ran its schema
ensurePluginTables+setup()run on activation, and nobody activates a plugin that is already active — the admin only activatesif (!isActive). Installing a new version over a running one swappedversionIdand nothing else, so a table the new version declares was never created.What the operator saw: the new admin screen answered 404, because the generic plugin resource page turns any 403/404/500 from the endpoint into
notFound()— a missing table is indistinguishable from a missing screen.installnow runs the same two steps activation runs when the row was ACTIVE and the version changed. Both are idempotent (CREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS, asetup()that seeds only what is missing) — this isdbDeltaon upgrade. If the new version cannot start, the install stands, the plugin isFAILED, and its own message comes back inerrorrather than a clean "ok" the operator discovers is a lie on the live site.2. Uninstall — the way OUT of a broken install — needed the tables to exist
purgePluginSiteDatarunsDELETEfor every table the manifest declares. Against a table that was never created that is42P01, surfaced as "Internal server error" with the plugin still installed. It now probes for the table and skips a missing one: nothing to delete is not a failure, it is the answer.3. …and the first version of that probe was a 500 of its own
SELECT to_regclass($1)returns typeregclass, which Prisma's pg adapter cannot deserialize (UnsupportedNativeDataType/ P2010). The NULL test now happens in Postgres and only a boolean crosses the wire. Mocked unit tests cannot catch a driver-level type fault, so the test asserts the SQL shape instead, and the query was run against a real database.Also here
admin.error.*— a Server Action id is baked into the page at build time, so a tab left open across a deploy posts an id the new build has never heard of. Next's message reads like a fault in the software; it is neither, and Retry is the most dangerous button on that screen — it re-sends a dead id from stale JavaScript for an action that has usually already run (the uninstall above completed; only the response path failed). The boundary now recognises it and offers a full reload.Tests
cms-api 745, admin-web 323, i18n 118 — green; every package typechecks.
plugins.controller.test.ts— upgrade-while-active reconciles; an inactive install is left alone; same version re-consented does nothing; a failing setup marks FAILED and reports.plugins.service.test.ts— purge deletes only this site's rows; skips a table that was never created; asks Postgres for a boolean, never for an oid.error.test.tsx— stale action offers reload and never callsreset(); an ordinary failure still retries and still shows its message.