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.
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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, "修改成功".
A plain display table with no search form (zero columns marked
IsQuery=1) is a normal shape, not an edge case - but ts.go.template's
Query interface only had a body when the range over .Columns found a
match, so it rendered `export interface {ClassName}Query {}`, which
@typescript-eslint/no-empty-object-type flags and pnpm lint fails on.
Falls back to `export type {ClassName}Query = Record<string, never>`
when no column qualifies - the same answer go-admin-ui's own
useTable.ts already gives this shape (`TQuery extends object =
Record<string, never>`), so it intersects with PageQuery the same way
an empty interface would have and callers do not need to special-case
a query-less table.
Audited the rest of this template for the same "zero of some optional
feature" shape while in here, since this is the third time a query-less/
select-less/whatever-less table has slipped through (dictLabel, DateCell,
this one):
- a zero-column table: cannot occur - a table without at least a
primary key column cannot exist to be imported from information_schema
in the first place, so .Columns is never empty here.
- a table with only its primary key column: the row interface still
has one property (the pk); not empty, no lint issue.
- a table where every column has IsInsert=0: does not affect this
template - add{Class}/update{Class} both take the full row interface
unconditionally (every column, not just IsInsert ones), so there is
no column-count-dependent shape to go empty here.
Verified with node 24.11.0 (not the machine default): generated a
zero-IsQuery table and a with-IsQuery table, copied both into
go-admin-ui and ran eslint + vue-tsc --noEmit. Confirmed red first -
`export interface VerifyNoQueryQuery {}` failed eslint with exactly the
no-empty-object-type error. Restored the fix - clean on both, plus a
throwaway call site instantiating useTable<VerifyNoQuery,
VerifyNoQueryQuery> to prove the type satisfies useTable's `TQuery
extends object` constraint, not just that it parses in isolation.
The rules block joined entries with a $first-flag comma the same way
defaultQuery and defaultModel do, but on its own line rather than all
on one -- so the comma for every entry but the first sat at the start
of its line instead of the end of the one before it. @stylistic/comma-style
requires the opposite, and pnpm lint fails on any table with two or
more required insert fields (one comma is enough to trip it; a table
with 0-1 required fields never renders a second entry to get it wrong).
Reproduced first: rendering a four-required-field fixture reported
three comma-style errors, matching what integration testing found on
qa010_widget and qa010_natural.
Fixed by moving the separator so it trails the previous entry instead
of leading the next one: the else branch now emits ",\n " -- comma,
then the newline and indent -- rather than "\n , ", so the comma lands
on the line that already has content instead of opening a fresh one.
Also fixed while reproducing, found by the same fixture: dictLabel was
imported whenever any column had a DictType, but it is only called from
the list column's dictionary branch. A table using a dictionary solely
in its insert form (no such column in the list) imported dictLabel and
never called it, tripping no-unused-vars. $hasDict now only gates
useDict; a new $hasDictList gates dictLabel specifically.
Verified against three fixtures via a throwaway go-admin-ui worktree
(deleted afterwards) with hand-written API-module stubs standing in for
F5: the regression fixture (four required fields, confirmed red before
the fix, green after), and the two fixtures from the original F4
verification round (full branch coverage, and the all-flags-off edge
case), all of which stayed green. pnpm type-check and pnpm lint both
ran clean with zero errors, on Node 24.11.0 (this machine's default
node is 20.19.0; the project's engines field wants >=22).
MLTBName (table_name with underscores turned to dashes, e.g. "user_profile"
-> "user-profile") is a gorm:"-" field - table.Get never fills it in, the
caller has to. NOActionsGen has done so since it existed; Preview never
did, so every template's import path that reads it
(from '@/api/{PackageName}/{MLTBName}' in vue.go.template, present in
both the pre-Vue3 template and F4's rewrite) rendered with the module
segment missing - "from '@/api/admin/'" - in the preview dialog only.
The real generated file was always correct.
Pre-existing, not introduced by this PRD, but worth fixing now: F3/F9
added two more Preview panes (the language packs) on top of an assumption
- that Preview's output stands in for what NOActionsGen actually writes -
that was never true for this field. Checked the rest of gen.go for the
same "only set on the write path" shape; MLTBName is the only one -
every other field Preview's templates read comes straight off the
sys_tables/sys_columns rows table.Get already loads.
Verified with a throwaway harness driving Gen.Preview through a real gin
Context and sqlite-backed db, parsing the JSON response and reading back
template/vue.go.template's import line. Confirmed red first (temporarily
removed the added line): "from '@/api/verify010/'". Restored it: "from
'@/api/verify010/verify-widget'".