e7e04da9 Changed the field type catalog to separate structure from validation
ref https://linear.app/ghost/issue/BER-3794/members-manage-their-own-custom-fields-in-their-account
The catalog answers two questions that are usually asked by different people. What
parts a field type has — an address has six, and the last of them holds a country
code — is what anything drawing a form needs. What counts as a valid value is what
the server needs when someone submits one. Both were declared together, built out
of zod, so asking the first question meant importing the second.
Measured through Portal's own build, with the import actually used: taking the
whole catalog costs seventeen and a half kilobytes gzipped and puts zod in the
bundle. Taking the structure alone costs a hundred and seventy-seven bytes and
puts none there. Ghost's server enforces the rules and can afford them; Portal
only draws inputs, and it loads on every page view of every themed site. Asking it
to carry a validator it will never run, to find out that an address has six parts,
is the wrong trade.
So the structural half now stands alone and imports nothing. A renderer that wants
it gets the types with it, rather than hardcoding part names as strings and hoping
they match. Anything needing both halves still imports one place; the split is
about what a bundle pays for, not about where the catalog lives.
The two cannot drift, because the rules are now built from the structure rather
than declared beside it: an address's parts are stated once, and the schema for
each is looked up from that statement. A part added without a rule, or a rule
without a part, fails to compile.
Splitting them also emptied something. A field type used to carry a map of its
parts pairing each name with both its declared type and its rule, because the two
were needed in different places and there was nowhere else to state the type. The
type is stated in the structure now, and nothing reads that map any more, so it
goes and takes the last of the coupling with it: a field type is a kind and a
rule, and what it is made of is described elsewhere. ae9b4a69 Changed the field type catalog to separate structure from validation
ref https://linear.app/ghost/issue/BER-3794/members-manage-their-own-custom-fields-in-their-account
The catalog answers two questions that are usually asked by different people. What
parts a field type has — an address has six, and the last of them holds a country
code — is what anything drawing a form needs. What counts as a valid value is what
the server needs when someone submits one. Both were declared together, built out
of zod, so asking the first question meant importing the second.
Measured through Portal's own build, with the import actually used: taking the
whole catalog costs seventeen and a half kilobytes gzipped and puts zod in the
bundle. Taking the structure alone costs a hundred and seventy-seven bytes and
puts none there. Ghost's server enforces the rules and can afford them; Portal
only draws inputs, and it loads on every page view of every themed site. Asking it
to carry a validator it will never run, to find out that an address has six parts,
is the wrong trade.
So the structural half now stands alone and imports nothing. A renderer that wants
it gets the types with it, rather than hardcoding part names as strings and hoping
they match. Anything needing both halves still imports one place; the split is
about what a bundle pays for, not about where the catalog lives.
The two cannot drift, because the rules are now built from the structure rather
than declared beside it: an address's parts are stated once, and the schema for
each is looked up from that statement. A part added without a rule, or a rule
without a part, fails to compile.
Splitting them also emptied something. A field type used to carry a map of its
parts pairing each name with both its declared type and its rule, because the two
were needed in different places and there was nowhere else to state the type. The
type is stated in the structure now, and nothing reads that map any more, so it
goes and takes the last of the coupling with it: a field type is a kind and a
rule, and what it is made of is described elsewhere. a4347e66 Added a host limit that can switch custom member fields off per site
Custom member fields need to be sellable as part of a plan, which the labs flag
cannot express: the flag says whether the feature exists in this build, not
whether a given site is entitled to it.
A host can now set limitCustomFields and the site stops being able to define
fields. Nothing sets it anywhere, so no site behaves differently today; choosing
which plan includes custom fields becomes a change to host configuration and
nothing else, which is the point. Because limits now live in this repository,
that took a name and no release.
The refusal covers only defining a field: the routes that create, reorder, edit
and delete one. Reading stays open, so a site that drops onto a plan without the
feature keeps seeing, filtering and exporting what it already collected. That
works because a definition is the only root of the feature, with values and
checkout bindings both keyed off one, so a site that cannot define fields has
nothing downstream to reach.
Those routes are also now a router mounted at the path they share, where each
guard is stated once and order carries the rule: a request walks the stack from
the top, so the reads are answered before the guards are reached and anything
added after them passes through both. One incidental change comes with that: a
method the router has no route for is refused by the guards rather than falling
through unmatched.
Refusing answers 403 rather than the 404 the flag gives, because "your plan does
not include this" is a different thing to tell a caller than "this does not exist
here", and only the first is something a publisher can act on. It carries
whatever wording the host supplies, or the limit service's own, which names the
feature.
On the Admin side one hook folds the flag and the limit together so no screen can
check one and forget the other, and removing the flag at GA touches only that
hook. What a publisher on the wrong plan should be shown instead is a design
question and is not answered here.
ref https://linear.app/ghost/issue/BER-3797 8a29cf40 Added a host limit that can switch custom member fields off per site
Custom member fields need to be sellable as part of a plan, which the labs flag
cannot express: the flag says whether the feature exists in this build, not
whether a given site is entitled to it.
A host can now set limitCustomFields and the site stops being able to define
fields. Nothing sets it anywhere, so no site behaves differently today; choosing
which plan includes custom fields becomes a change to host configuration and
nothing else, which is the point. Because limits now live in this repository,
that took a name and no release.
The refusal covers only defining a field: the routes that create, reorder, edit
and delete one. Reading stays open, so a site that drops onto a plan without the
feature keeps seeing, filtering and exporting what it already collected. That
works because a definition is the only root of the feature, with values and
checkout bindings both keyed off one, so a site that cannot define fields has
nothing downstream to reach.
Those routes are also now a router mounted at the path they share, where each
guard is stated once and order carries the rule: a request walks the stack from
the top, so the reads are answered before the guards are reached and anything
added after them passes through both. One incidental change comes with that: a
method the router has no route for is refused by the guards rather than falling
through unmatched.
Refusing answers 403 rather than the 404 the flag gives, because "your plan does
not include this" is a different thing to tell a caller than "this does not exist
here", and only the first is something a publisher can act on. It carries
whatever wording the host supplies, or the limit service's own, which names the
feature.
On the Admin side one hook folds the flag and the limit together so no screen can
check one and forget the other, and removing the flag at GA touches only that
hook. What a publisher on the wrong plan should be shown instead is a design
question and is not answered here.
ref https://linear.app/ghost/issue/BER-3797