Commit Graph
1798 Commits
Author SHA1 Message Date
zhangwenjian c0ca07c95b fix🐛: refuse generator names that cannot be path components
The package, table and business names a table's configuration carries
become directories and file names, and nothing on the server checked
them; the config page's patterns were the only check.

Each is now checked against what it becomes: a package name is
lowercase letters and digits, a table name letters, digits and
underscores, a business name an identifier starting with a lowercase
letter. None allows a dot or a separator, and none is stricter than
the config page. The check runs on import, where a list holding one
bad name is refused whole, on save, and again before generating, so a
configuration saved before this check existed is refused rather than
trusted.
2026-09-27 19:43:04 +08:00
zhangwenjian afa8513d4b fix🐛: keep the code generator's writes inside their roots
The generator joins a table's package, table and business names into
the paths it writes, and wrote them with os.MkdirAll and os.WriteFile.
A name carrying "..", or a symlink under the target directory pointing
elsewhere, took the write wherever it led.

Every file is now written through os.Root: backend files under the
working directory, frontend files under gen.frontpath. A path that
resolves outside its root is refused, symlinks included, and the
request reports the write failure without writing anything outside.
2026-09-27 19:42:35 +08:00
wenjianzhang c1d92ad30f Merge pull request #954 from go-admin-team/fix/gen-audit-columns-and-empty-form
fix🐛: generator config for audit timestamps, and no empty form
v2.7.0
2026-09-27 16:16:18 +08:00
zhangwenjian 2642d0959d fix🐛: leave the form out of a generated page with nothing to enter
A table whose columns are all its key and ones the framework fills in
got an add button and an edit link opening an empty form. The page now
leaves out both, the dialog, useForm and the API calls only the form
used. A page with anything to enter is generated byte for byte as
before.
2026-09-27 16:07:10 +08:00
zhangwenjian 8cbbd6eddd feat✨: show created_at and updated_at on the generator's config page
The config page hid both columns along with the ones the framework
fills in, so a generated page could not list or filter by when a row
was created or last changed without editing sys_columns by hand. They
are now shown; id, create_by, update_by and deleted_at stay hidden.
The templates already keep both out of the create/edit form.

The test imports a table, ticks list and query on created_at through
the config page's own endpoints, generates it, and filters the
generated list with the value the generated date picker sends.
2026-09-27 16:07:10 +08:00
zhangwenjian de33e527fc fix🐛: treat a column saved with isQuery "0" as not queryable
The config page saves an unticked query box as "0", and the DTO and API
templates tested IsQuery with a bare if, which is true for any
non-empty string. A column ticked and then unticked became a filter of
the generated list request and a documented query parameter anyway.
They now require "1", as the TypeScript and Vue templates already do.
2026-09-27 16:07:09 +08:00
zhangwenjian d30b9c103f fix🐛: import "time" in a generated model only for fields it declares
The model template imported "time" for any queryable time.Time column,
though whether a column is queryable only shapes the DTO. A queryable
created_at, which the model takes from models.ModelTime rather than
declaring, left the import unused and the generated package failed to
compile.
2026-09-27 16:07:09 +08:00
zhangwenjian 77fddfa356 test✅: hard-delete the generator fixtures, as the delete endpoint does
dropFixture soft-deleted the sys_tables rows it cleaned up. The config
page's business-name check counts soft-deleted rows, so a second test
importing the same table could not save its configuration. The delete
endpoint removes these rows with Unscoped; the fixture now does too.
2026-09-27 16:07:07 +08:00
wenjianzhang 79cc8eba34 Merge pull request #953 from go-admin-team/fix/check-file-create-errors
fix🐛: report failures from app and migrate -g file generation
2026-09-27 14:07:56 +08:00
zhangwenjian 6d06241e0d fix🐛: report failures from app and migrate -g file generation
Both commands discarded genFile's error, and genFile discarded the
errors from rendering its templates and from pkg.FileCreate. A file
that could not be written ended the process from inside FileCreate;
with core v2.11.0 it would have been reported as success instead.

genFile now returns every error, and both commands print it and exit
non-zero. In `app`, a missing router template no longer panics on a
nil template, and neither file is written unless both render.
2026-09-27 14:01:48 +08:00
zhangwenjian d89e0cd8eb chore⬆️: upgrade go-admin-core to v2.11.0
v2.11.0 makes pkg.FileCreate return its error instead of ending the
process when the file cannot be created. Two comments in the code
generator described the old behaviour in the present tense.
2026-09-27 14:01:48 +08:00
wenjianzhang c055dc68c5 Merge pull request #945 from amiaogo/fix/pg-config-sequence
fix🐛: correct sequence name for sys_config in pg.sql
2026-09-26 14:40:27 +08:00
wenjianzhang a60a6438fe Merge pull request #944 from amiaogo/fix/opera-log-utf8-truncate
fix🐛: truncate opera log jsonResult by rune to keep valid UTF-8
2026-09-26 14:32:22 +08:00
zhangwenjian b6595fb076 fix🐛: create the job sequence under the name the calibration uses
The setval below now targets sys_job_job_id_seq, the sequence job_id
actually uses, but the create above it still named sys_job_id_seq, so a
fresh PostgreSQL install was left with an unused sequence of that name.
2026-09-26 14:31:36 +08:00
wenjianzhang f8fa20f4e7 Merge pull request #952 from go-admin-team/fix/gen-swallowed-errors
Code generator: stop swallowing errors when saving and generating
2026-09-26 14:31:28 +08:00
zhangwenjian d84e30afb8 fix🐛: report a table the generator cannot read
Preview, GenCode, GenApiToFile and GenMenuAndApi read a table's
configuration with tab, _ := table.Get(...), so an id that matched nothing,
or a failed query, carried on with an empty table. GenCode and Preview then
refused it as having no primary key, and GenMenuAndApi went on to seed menus
and APIs for it.

Each now answers with the lookup's error.
2026-09-26 14:17:22 +08:00
zhangwenjian 83064f0b37 fix🐛: render every generated file before writing any, and check both
Preview, NOActionsGen and genApiToFile assigned each template's Execute
error to err and never read it - 19 times - so a template failing part-way
left a truncated file behind it and the request reported success. The
files were written through pkg.FileCreate, which returns no error at all:
when it cannot create a file it closes a nil *os.File and calls
log.Fatalln, ending the server process. Four of the directories it writes
into were created with _ = pkg.PathCreate(...).

Every template now renders into memory first; if any fails, nothing is
written and the response names the template. Files are then written
through writeGenerated, which creates the directory and returns any error
instead of exiting. NOActionsGen and genApiToFile report whether they
succeeded, so GenCode and GenApiToFile no longer follow an error response
with "Code generated successfully".
2026-09-26 14:17:22 +08:00
zhangwenjian 88b14a5e0f fix🐛: write a table's configuration and its columns in one transaction
SysTables.Create and Update wrote each column with _, _ = …, so a column
the database rejected was dropped while the request reported success. And
because neither ran in a transaction, returning that error alone would not
have been enough: the table row and every column before the failing one
were already committed, leaving a table the config page lists with columns
missing and code generated from fewer columns than the table has.

Both now run the table and its columns in one transaction and return the
first column's error, named. Create rolls back to nothing; Update leaves the
table and every column as they were. Both callers already answer an error
with a 500.
2026-09-26 14:17:19 +08:00
wenjianzhang 827ad1146e Merge pull request #951 from go-admin-team/feat/gen-string-key-form
Code generator: a string primary key can be created from the generated page
2026-09-25 21:20:28 +08:00
zhangwenjian 6e8b1891bb feat✨: let a generated page create a record keyed on a string
The generated form never showed the key column, which is right for a key
the database assigns and left a string key with nowhere to be typed: a
table keyed on one could be created through the API but not from its page.

For a string key the form now carries an input for it, required, and
disabled once the record exists - bound to form.isEdit, the same way the
hand-written dictionary and config pages lock their natural keys. A page for
a table keyed on an int renders exactly as before.

This needs go-admin-ui's useForm to record whether a form was opened to
create or to edit (go-admin-ui#310). The useForm before it inferred the mode
from the key's presence, so typing the key into a create form turned the
submit into an update.
2026-09-25 20:21:40 +08:00
wenjianzhang 719d54cf0e Merge pull request #950 from go-admin-team/fix/gen-non-id-primary-key
Code generator: support any single-column primary key
2026-09-25 20:10:48 +08:00
zhangwenjian 0ff5f1a027 test✅: import and generate every primary-key shape, then drive the result
The generator had no test that compiled what it writes; test/gen_test.go is
commented out. This one goes through the real path end to end: fixture
tables in MySQL, the import handler, GenCode writing its files, the files
compiled into this module's own packages through a -overlay, and a CRUD
test run over HTTP against the generated handlers.

The shapes are an int key named id, an int key named otherwise, a string
key, no key and a composite key. The last two must be refused with nothing
written. For the rest: read and update through the path parameter, a
missing key neither found nor created by an update, a key of "1=1" read as
a value, and delete by key.

The importer only reads MySQL, so the test needs GO_ADMIN_TEST_MYSQL_DSN;
without it the test skips locally and fails under CI. It also asserts each
generated CRUD test actually ran, since a -run pattern that matches nothing
exits 0.
2026-09-25 20:00:40 +08:00
zhangwenjian 99d22249ef fix🐛: refuse to generate a table whose key is not one column
The templates address a row by a single key: one field on the model, one
path parameter, one value per row in a delete. A table with no primary key
rendered a model whose GetId returned an empty field name, and a composite
key was silently narrowed to whichever of its columns the importer read
last. Both wrote files that break the build of the whole module.

Preview and NOActionsGen now check first and return an error naming the
table and, for a composite key, its columns. Nothing is written.
2026-09-25 20:00:30 +08:00
zhangwenjian 3112896a83 fix🐛: stop a generated Update when its row is not found
Update looked its row up and ignored the result. When nothing matched -
because the key does not exist, or because the caller's data scope filters
the row out - the model stayed at its zero value, Generate copied the
request onto it, and Save, finding a zero key, inserted a new row. The
update of a row the caller may not see became a row they now own, and the
RowsAffected check meant to refuse it never fired, because the insert did
affect one.

A missing row now ends the update with the same "无权更新该数据" the
RowsAffected check returns, and any other lookup error is returned as is.
This applies to every generated table, an int id included.
2026-09-25 20:00:30 +08:00
zhangwenjian bf8a1b3374 fix🐛: bind the generated service's key as a query value
Get and Update found their row with First(&model, id). GORM reads a string
passed that way as a SQL condition rather than a key, so with a string
primary key the path parameter went into the WHERE clause as written: "c-1"
failed as the expression c - 1, and GET /…/1=1 returned a row.

Both now filter on clause.PrimaryColumn with the id as a bound value, which
GORM resolves to the model's own quoted key column whatever it is called.
Delete keeps Delete(&model, ids): a slice is always taken as key values.
2026-09-25 20:00:21 +08:00
zhangwenjian d53c8abc6c fix🐛: generate a model and DTOs for any single-column primary key
The model and DTO templates assumed every table's key is an int column named
id. For any other key the generated backend did not compile - the model
embedded models.Model, which supplies Id, skipped the real key column, and
GetId then returned a field that did not exist ("e.Code undefined"). Past the
compile error there was more of the same assumption:

  - GetReq and UpdateReq bound the key with uri:"<json field>", while every
    generated router registers it as :id, so any other name bound nothing;
  - InsertReq tagged the key json:"-", so a key the database does not assign,
    such as a string, could never be supplied;
  - DeleteReq took Ids []int, so a string key could not be deleted.

models.Model now stands in for the key only when the key is exactly an int
named Id; anything else is declared as its own primaryKey field. The key is
bound from :id, accepted from the request body when it is not an int, and
deleted by a list of its own type. A table keyed on an int id renders
byte-for-byte as before.
2026-09-25 19:59:59 +08:00
wenjianzhang c463f3696d Merge pull request #949 from go-admin-team/chore/core-v2.10.0
Take go-admin-core v2.10.0, and fix the log call it exposes
2026-09-24 20:48:20 +08:00
zhangwenjian 60db31e0e8 chore⬆️: go-admin-core v2.8.0 -> v2.10.0
Picks up two releases at once:

  - v2.9.0: app.Register normalizes Manifest.Requires the way it already
    normalized Code, so the dependency list stored in sys_app.requires is
    spelled the same way as the app codes beside it; and a version
    component beginning with '+' is reported as a signed component rather
    than as a build-metadata suffix.
  - v2.10.0: builds with Go 1.27, which this repository already declares,
    and fixes a %S verb in the listener's shutdown log.

All three modules move together: example/app-order requires core directly
and test/e2e-apporder does indirectly. No other requirement changes.
2026-09-24 18:14:51 +08:00
zhangwenjian e3e5d3e550 fix🐛: format the dept-list error instead of printing its verb
log.Error is the unformatted entry point, so "find dept list error, %s"
went out with the %s intact and the error text glued to it:

    find dept list error, %sconnection refused

Errorf is what the line meant. go vet reports this once go-admin-core's
logger is recognised as a print-style wrapper, which it is from v2.10.0.
2026-09-24 18:14:38 +08:00
wenjianzhang f427a42b4f Merge pull request #948 from go-admin-team/chore/go-1.27
Build with Go 1.27
2026-09-23 20:26:02 +08:00
zhangwenjian 1169cb3e87 chore⬆️: build with Go 1.27
govulncheck reports seven standard-library vulnerabilities against go1.26.5
that this code actually reaches, and none against go1.27.1.

It also clears the way for the next go-admin-core release, which declares
go 1.27.1: a module cannot require a dependency whose language version is
newer than its own. Doing it in its own commit keeps that upgrade to a
one-line dependency bump.

All three modules in the tree move together. test/e2e-apporder requires the
main module, so leaving it behind fails the moment the main module declares a
newer version - "updates to go.mod needed", before a test runs.
example/app-order only requires go-admin-core and would still have resolved,
but it was declaring 1.25.13, two releases back and out of support.

The workflows pin the Go version explicitly instead of reading go.mod, and
the four READMEs state it under environmental requirements, so those move
with it too.

No dependency changes - go mod tidy leaves every go.sum untouched.
2026-09-23 17:25:51 +08:00
amiaogo 2bf6ff2df4 fix🐛: correct sequence name for sys_job in pg.sql 2026-09-23 09:46:23 +08:00
wenjianzhang ec10917272 Merge pull request #947 from go-admin-team/fix/915-scheduler-lease
fix🐛: make the job scheduler single-writer with a database lease
2026-09-22 22:57:21 +08:00
wenjianzhang b8af16baf0 Merge pull request #946 from go-admin-team/test/job-remove-goroutine-leak
test✅: fail when Remove parks a goroutine on an abandoned channel
2026-09-22 22:54:55 +08:00
wenjianzhang 92987cdee3 Merge pull request #943 from go-admin-team/feat/010-generator-vue3
feat✨: generate Vue 3 pages from the code generator (PRD 010)
2026-09-22 21:42:43 +08:00
amiaogo 7a0a58a28c fix🐛: correct sequence name for sys_config in pg.sql 2026-09-22 15:57:09 +08:00
amiaogo 29d9e50f98 fix🐛: truncate opera log jsonResult by rune to keep valid UTF-8 2026-09-21 22:44:31 +08:00
zhangwenjian df98ffb5c5 docs📝: say what a second replica now does to the scheduler
The note told the reader the scheduler stood in the way of raising the
replica count. It no longer does: one enabled job fires once however many
pods there are, a pod that loses the lease stops scheduling, and one that
exits hands the lease back rather than making its successor wait it out.

The shared log volume still does stand in the way, and that is now the only
thing the note asks for before the number goes up.
2026-09-20 18:31:44 +08:00
zhangwenjian acc9378283 fix🐛: schedule jobs only while this instance holds the lease
A second instance pointed at the same database did not divide the work, it
overwrote it. Every instance registered the whole enabled list into its own
cron.Cron, and startup ran `UPDATE sys_job SET entry_id = 0 WHERE entry_id
> 0` across the entire table, so the newest process erased the ids the
previous one wrote and put its own over the top. Neither symptom logged
anything: an enabled job fired once per instance, and stopping one from the
UI removed an entry from the wrong process and answered 200 either way.

A supervisor per tenant now keeps the scheduler in step with the lease.
Holding it is not decided once at startup, because both of the other
answers are wrong for longer than a moment:

  - an instance that never got the lease keeps asking, so the death of the
    holder does not stop the jobs until somebody restarts a process by hand;
  - an instance that holds it stops scheduling as soon as its lease has
    lapsed, because a holder still scheduling after the lease has gone
    elsewhere is the two-schedulers defect reached from the other side.

A failed renewal is not a lost lease. The scheduler keeps running until the
lease could actually have expired: a database that is briefly unreachable
must not stop the jobs, and cannot have handed them to anyone else, because
nobody else can reach it to take the lease either.

Stopping is no longer arranged by startCrontab. A scheduler now stops for
two different reasons and only the supervisor knows which - and since a
start happens every time the lease is taken, registering a shutdown
callback there would add one per leadership change for the life of the
process, because SetShutdown appends.

sys_job.entry_id keeps its meaning. This does not distribute the jobs and is
not meant to: the HTTP side scales, the scheduler stays single-writer.

Fixes #915.
2026-09-20 18:31:44 +08:00
zhangwenjian b4b5bc5b3a feat✨: a database lease the schedulers compete for
One row per database, taken and renewed by single UPDATE statements whose
RowsAffected the database decides. Nothing uses it yet; the scheduler is
wired to it next.

The two timestamps are epoch milliseconds in a BIGINT rather than timestamp
columns, which is the one decision here worth explaining. A timestamp does
not survive the trip through a driver unchanged: read over go-admin's own
`parseTime=True&loc=Local` DSN, MySQL's UTC_TIMESTAMP arrives relabelled as
local time, and on a UTC+8 host every lease is eight hours out. It is
invisible to any test that compares the lease against itself, because each
instance's own arithmetic stays self-consistent - only the comparison
between two instances is wrong, which is the only comparison that matters.
An integer has no timezone for a driver to apply.

The migration seeds the row free. There is no insert path at runtime, so two
instances starting together cannot race to create the row they are both
trying to claim, and neither has to tell a duplicate-key error apart from a
real one in whichever driver it is running against. A missing row is
therefore reported rather than recovered from: silently never scheduling
anywhere is the worse failure.

The current-time expression differs per dialect and all four are covered:
MySQL, PostgreSQL and SQL Server against real servers, SQLite by default.
2026-09-20 18:31:44 +08:00
zhangwenjian 27ad988fd7 ci👷: run the tests against MySQL too
MySQL is the dialect most installations run and the only registered driver
with no service here. The scheduler lease that follows reads the database's
clock, and the first implementation read it as a timestamp: over go-admin's
own `parseTime=True&loc=Local` DSN, MySQL's UTC_TIMESTAMP comes back
relabelled as local time, so on any host that is not UTC every lease was one
zone offset out. Every assertion that compared the lease only against itself
still passed, and the three dialects already here could not see it.

The DSN keeps loc=Local on purpose: it is what config/settings.yml ships. A
DSN here that quietly differed would test a configuration nobody runs.
2026-09-20 18:31:44 +08:00
zhangwenjian effc3a3e69 Merge branches 'feat/010-gen-plumbing' and 'feat/010-vue3-template' into integ/010-all 2026-09-19 21:21:25 +08:00
zhangwenjian 08f789737f fix🐛: escape the literal T in the datetime value-format
value-format="YYYY-MM-DDTHH:mm:ssZ" works today only because dayjs does
not currently give T a format-token meaning, so it passes through as a
literal character -- an accident of the current token table, not
something this string declares. Escaped it to YYYY-MM-DD[T]HH:mm:ssZ,
dayjs's own syntax for "this character, verbatim, not a token": produces
byte-for-byte the same output today (confirmed below) and stops
depending on T staying meaningless in a future dayjs version.

Re-verified both directions against the escaped string, and did so
against the real el-date-picker component this time rather than dayjs
alone: mounted element-plus's actual ElDatePicker with
value-format="YYYY-MM-DD[T]HH:mm:ssZ" (copied from a real rendering of
this fixed template, not retyped) and confirmed it renders a real,
non-blank date -- not "Invalid Date" -- when its modelValue is set to
either shape Go's encoding/json actually sends (a numeric offset or a
literal Z for UTC), and that both render identically since they are the
same instant. Submission was re-checked through the same dayjs call the
component itself makes to format a picked value. A fourth check formats
the same instant with both the old and the escaped string and asserts
they are equal, so this suite would have caught the difference if the
escape had changed anything instead of just hardening it.
2026-09-19 21:21:01 +08:00
zhangwenjian f57bf5d61d fix🐛: query-less table's Query type broke FK dropdown fetches (P0)
3625ce8 fixed the no-empty-object-type lint error by switching a
zero-IsQuery table's {ClassName}Query to `Record<string, never>`, the
same default useTable.ts's own `TQuery extends object = Record<string,
never>` uses. That default is safe there only because useTable.ts's one
internal `TQuery & PageQuery` goes through an `as` cast rather than a
structural check. Code that builds the object literal directly does not
get that protection - and vue.go.template's foreign-key dropdown fetch
does exactly that: `list{FkClass}({ pageIndex: 1, pageSize: 100 })`.

Record<string, never> is a mapped type over every string key, each
mapped to never, so `Record<string, never> & PageQuery` does not leave
PageQuery's own properties alone: pageIndex becomes `never & number`,
i.e. never, and no value can be passed for it - a straight type error,
not a lint warning, so the previous fix's pnpm lint pass did not catch
it. Trips on any query-less table referenced by a foreign key - a plain
lookup/dict table used as a dropdown source, an ordinary shape, not a
rare one.

Switched to Record<never, never>: a mapped type over the empty key
set, which behaves as the empty object type `{}` under intersection
(PageQuery's properties come through unchanged) while still satisfying
`TQuery extends object` and not tripping no-empty-object-type (it is a
generic instantiation, not a literal `{}` type annotation) - confirmed
all three separately before touching the template.

Verified with node 24.11.0: generated a real zero-IsQuery table, copied
its .ts into go-admin-ui alongside a throwaway file reproducing
vue.go.template's exact FK call site
(`await list{Class}({ pageIndex: 1, pageSize: 100 })`), and ran both
eslint and vue-tsc --noEmit - the type-check step lint alone cannot
cover, which is what let this through the first time. Confirmed red
first (swapped the generated file's Record<never, never> back to
Record<string, never>): vue-tsc reported the exact "Property 'pageIndex'
is incompatible with index signature" error. Restored the fix - both
clean.
2026-09-19 21:20:41 +08:00
zhangwenjian 143dbf19a2 Merge branches 'feat/010-gen-plumbing' and 'feat/010-vue3-template' into integ/010-all 2026-09-19 20:51:39 +08:00
zhangwenjian f6bd306d6d fix🐛: send datetime fields as RFC3339, not space-separated local time
Both date-pickers -- the search filter and the insert/edit form -- used
value-format="YYYY-MM-DD HH:mm:ss", which formats a picked instant as
e.g. "2026-09-19 12:30:00": no T separator, no offset. dto.go.template
declares every datetime column's InsertReq/UpdateReq field as
time.Time with a plain `json:"..."` tag (R6 leaves that file alone, so
there is no time_format tag to reach for instead), and encoding/json's
default (Un)MarshalJSON for time.Time only accepts RFC3339. The
generated form would submit new and edited datetime values in a shape
Go's JSON decoder cannot parse -- a runtime failure on every create and
update, with nothing in `pnpm type-check` or `pnpm lint` positioned to
see it: both check the request is well-typed TypeScript, not that the
string it produces is a string Go can read.

Changed both to value-format="YYYY-MM-DDTHH:mm:ssZ" -- dayjs's Z token
renders the picker's own local offset, which is what a zero-nanosecond
time.Time (anything without a database column storing sub-second
precision) round-trips to on either side of the wire; confirmed
separately against Go's actual json.Marshal/Unmarshal, not assumed from
the RFC.

The search filter needed the same fix, not just the form: GetPageReq
binds a `time.Time` query field via `form:"..."` (dto.go.template),
and gin's own default for an untagged time.Time binding is also
RFC3339 -- the same failure mode on the query side, one call the
report didn't name but the same root cause reaches.

This is a runtime behaviour change no compiler catches, so it was
verified as one: a Go program exercising encoding/json directly (not
assumed from reading the RFC) confirmed a zero-nanosecond time.Time
marshals to plain RFC3339 with no fractional seconds, and unmarshals
correctly from both a numeric offset and a literal Z. Separately, a
Node script loaded go-admin-ui's own installed dayjs 1.11.21 with the
customParseFormat plugin -- the same plugin element-plus's date-picker
extends dayjs with -- and called the same parseDate path date-picker
panel.mjs uses (time-picker/src/utils.ts, no strict flag passed, so
lenient parsing): formatting with this value-format produced a valid
submission string, and parsing either an offset or a literal-Z string
back with the same format produced a valid, correctly-valued date --
covering create, edit prefill, and update in the two directions that
matter (browser to Go, Go to browser) without needing a live backend.
2026-09-19 20:50:13 +08:00
zhangwenjian 3beb00143a fix🐛: give FK and dict options separate variable names
A column configured with both FkTableName and DictType -- and reaching
a branch of each, e.g. required + IsQuery=1 + IsInsert=1 with
HtmlType=radio -- had both blocks declare `const {JsonField}Options`:
the dict branch from useDict, the FK branch from ref(). TS2451,
Cannot redeclare block-scoped variable, and the page does not compile.

This is reachable precisely because FkTableName and DictType do not
exclude each other consistently: search, list and the form's select
branch check FK first and fall back to dict, but the form's radio
branch never looks at FK at all -- it was already established (the
$dictUsed/$fkUsed audit two commits back) that a radio column's dict
options are used regardless of whatever FkTableName says. A column
that is both radio and query-or-insert-select can legitimately need
both sources at once, under one shared name.

Renamed to {JsonField}DictOptions and {JsonField}FkOptions and updated
every consuming branch to the name that matches what it was already
branching on: FK branches (search select, form select, the list
column's Label function) read FkOptions; dict branches (search select,
form select, form radio, the list column's dictLabel call) read
DictOptions. Mechanical rename, no new conditions -- each site already
knew which source it wanted from its own if/else-if.

Verified with team-lead's exact repro (FkTableName + DictType +
IsQuery=1 + IsInsert=1 + HtmlType=radio) rendered through the real
template.Execute and checked against a throwaway go-admin-ui worktree
(deleted afterwards): TS2451 fired twice before this change, zero
after -- and the rendered file confirms the search select actually
reads kindFkOptions (FK wins search's priority) while the insert radio
reads kindDictOptions (radio never checks FK), so both sources are
live, not just declared. Re-ran every fixture from every previous
round alongside it; all stayed green. pnpm type-check and pnpm lint
both zero error, on Node 24.11.0.
2026-09-19 20:49:41 +08:00
zhangwenjian 7a52a50964 fix🐛: dedupe FK imports by target table and filter by actual use
Two columns pointing at the same foreign table -- owner and approver
both selecting from the same users table, say -- each triggered their
own `import { listX } from ...` / `import type { X } from ...` line,
which is a duplicate ES module import once both fire: TS2300. Nothing
here ever asked whether a target table had already been imported by an
earlier column, because nothing tracked target tables at all -- only
source columns, and "one FK-configured column" was never the same
thing as "one distinct target table".

The same import was also gated on the column having FkTableName set,
not on $fkUsed -- the condition the previous fix already applies to the
const declarations that read the import. A column carrying FK metadata
but reaching no query, list or insert-select branch imported a module
nothing in the file references.

text/template has no set to check membership in, so the dedup is a
nested range: a column only imports its target if no earlier,
equally-used column already claimed the same FkTableNameClass. $fkUsed
is recomputed for both the outer and the inner column rather than
factored out, since text/template has no way to carry a per-column
value computed in one range into a second, later range over the same
data.

Verified with a fixture carrying three columns pointing at the same
target table -- one read only from search, one only from the list, one
from neither -- rendered through the real template.Execute and checked
against a throwaway go-admin-ui worktree (deleted afterwards): before
this change, TS2300 fired six times (the function and the type, three
times over); after, exactly one import of each, and the unused third
column contributes neither. Re-ran the previous rounds' fixtures
alongside it; all stayed green. pnpm type-check and pnpm lint both zero
error, on Node 24.11.0.
2026-09-19 20:48:58 +08:00
zhangwenjian 630e13686c fix🐛: gate optional imports on where they're actually used, not on raw metadata
Every $hasX flag controlling an optional import matched "this column
carries the metadata" rather than "some rendered branch actually reads
it" -- necessary but not sufficient, since FkTableName/DictType lose to
each other by priority (FK wins search, list and the form's select
branch; the form's radio branch never checks FK at all) and a column
can carry either while being neither queryable, listed, nor an
insertable select/radio.

$hasDatetime was the reachable case integration testing found:
sys_tables.go assigns HtmlType "datetime" to any timestamp/datetime
column on import regardless of IsList, because GetList's audit-column
exclusion is a separate, later step editTable.vue never surfaces
created_at/updated_at through anyway. Nearly every real table has both,
so nearly every table imported DateCell without using it.

$hasFk and $hasDict had the identical shape one level down: the
per-column ref/onMounted/useDict declarations were gated on "this
column has FkTableName/DictType", not on whether the column reaches a
branch that reads the resulting Options ref -- an FK column used only
via search (no IsList, no insert-select) still declared a Label
function nothing calls, and a dict column used only in an insert radio
(no IsQuery, no IsList) still would have, had the two flags controlling
its import stayed as wide as the per-column check they were meant to
gate.

Rewrote both to a shared $dictUsed/$fkUsed condition, matching each
consuming branch's own guard term for term, and split the FK block's
Label function under its own IsList check -- Options can be needed for
search or the form's select without List ever being true. $hasDictList
already had this shape from the previous fix and needed no change.

Verified with two new fixtures, rendered through the real
template.Execute and checked against a throwaway go-admin-ui worktree
(deleted afterwards) with hand-written API-module stubs: a bare table
carrying only the standard created_at/updated_at pair -- confirmed red
on DateCell before this change, green after -- and a table exercising
every optional import through a path distinct from the ones the two
earlier verification rounds covered (a dict column read only from an
insert radio, an FK column read only from search, and a business
datetime column that IS listed, so DateCell still has to import when
the real thing needs it). Re-ran the three fixtures from the previous
two rounds alongside these two; all five stayed green. pnpm type-check
and pnpm lint both zero error, on Node 24.11.0.
2026-09-19 20:40:29 +08:00
zhangwenjian 05661e2f3e fix🐛: jsonField format check relaxed to any legal identifier
jsonFieldPattern copied businessName's rule (^[a-z][A-Za-z]+$: at least
two letters, no digits) on the theory that jsonField should tighten to
the same identifier shape. That does not hold: businessName is typed
by a person on genInfoForm.vue, so a strict pattern is a reasonable
guardrail on human input. jsonField is computed by the importer from
the column name (sys_tables.go's namelist/JsonField loop) - nobody
types it, so the same pattern only rejected names the importer
legitimately produces: a single-letter column ("x") or one whose last
segment ends in a digit ("address2", "a1") both collapse to a single
camelCase word with nothing left to re-capitalize, and both failed the
old check.

The blast radius is wider than "this one column can't be edited":
validateAndSanitizeColumns runs over every column on every Update, so
a table that merely contains one such column could not save any
config change at all, including edits with nothing to do with that
column.

Relaxed to ^[a-z][A-Za-z0-9]*$ - any legal JS/TS identifier starting
with a lowercase letter. Still rejects what has to be rejected: empty,
whitespace/punctuation, and leading-digit names, since those cannot be
unquoted object keys in the generated interface/lang file at all.
Uniqueness and the expression-content check on defaultValue are
unchanged - defaultValue is genuinely user-typed (F6's config page),
so tightening it was the right call to begin with; this was the only
place a human-input rule had been copied onto machine-generated data.

Verified against the real import path, not hand-typed jsonField values:
built a table with columns id/x/address2/a1 in a fake information_schema,
ran it through the real SysTable.Insert, confirmed the importer computes
exactly jsonField x/address2/a1, then submitted an update through the
real SysTable.Update changing only tableComment (nothing about those
columns). Confirmed red first - 500, "jsonField 格式不合法:\"x\"" - a
change unrelated to any of the three columns was rejected solely because
they existed on the table. Restored the fix - 200, "修改成功".
2026-09-19 20:37:01 +08:00