v0.0.2-alpha.48
Pre-releaseReleased 17 packages at 0.0.2-alpha.48 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
-
#417
1f81cf3Thanks @mobeenabdullah! - Read every related row through one code path, so a capability added to relationship population applies everywhere a relationship is populated instead of at whichever call sites were remembered. -
#423
2f05141Thanks @mobeenabdullah! - Fixes PostgreSQL index introspection reading indexes from the wrong table. Table names are unique per schema rather than per database, so a table with the same name in another schema had its indexes merged into the one being inspected. That could hide an index that needed creating, or report one that was never there.Refuses to run a schema sync while a field group storage migration is in flight. Mid-run some tables carry their old names and some their new ones, and the registry rows pointing at them move one step at a time, so a sync during that window could delete storage it could not account for.
Also further groundwork for that migration: it can now execute its rename steps and check its own work. A table, its localization companion and the registry row pointing at them move as one step, and on PostgreSQL and SQLite they commit together. MySQL applies a schema change as soon as it is issued, so there the halves land in sequence and a resume completes whatever did not; a reader in that window sees a table as missing rather than reading anything wrong. Every step verifies against the database rather than trusting that it ran, and index survival is checked by name, so an index dropped and replaced by another is caught rather than passing on an unchanged count. Nothing calls the migration itself yet.
-
#424
0538f4fThanks @mobeenabdullah! - Let a form be updated without resending its fields. Changing a form's name or settings failed with "Form must have at least one field" because an absentfieldsin the patch was treated as an empty one. -
#422
a4dad07Thanks @mobeenabdullah! - Emit aform.submission.createdwebhook when a form submission is created. The event type was already subscribable in the admin UI but had no producer, so an operator could subscribe to an event that never fired.Form submissions carry visitor-entered answers plus
ipAddress/userAgent, so the submissions collection suppresses the PII-bearingentry.*events. It now instead emits a curated, metadata-onlyform.submission.createdcarrying only which form, when, and the status — never the answers, IP, or user agent. The event is recorded in the same transaction as the submission, so it commits atomically and is never delivered for a rolled-back write.This is driven by a new declarative
webhooks.emitcollection option ({ event, fields }): any PII-bearing collection can replace its defaultentry.*events with a safe curated one that ships only an allowlisted set of fields (default-deny). The resource kind is derived from the event name. -
#421
83ed5c9Thanks @mobeenabdullah! - A string stored in a JSON field no longer fails the write on PostgreSQL and MySQL.A field backed by a JSON column accepts any JSON document, a plain string included. A string that is not itself encoded JSON was passed through to the driver as bare text, which PostgreSQL and MySQL reject as invalid JSON, so storing
"hello"in ajsonfield failed the write outright. On SQLite, where the column is plain text, it was stored in a form no read could recover as what was written. Such a value is now encoded, so it round-trips as the string it was.A string that already parses as JSON is still passed through untouched, so content a previous write encoded is not wrapped a second time.
-
#415
0e18a97Thanks @mobeenabdullah! - Apply a target collection's read rule when it filters on one of that collection's localized fields, so populating a relationship returns the rows the rule permits instead of withholding every one of them. -
#405
e1467e8Thanks @mobeenabdullah! - Plugin-contributed field types are now first-class in generated output, in the manifest, and in the validation a plugin can reuse.nextly buildemitted nothing at all for a custom field type: the generators test membership of the built-in list, so the field was skipped while its value was still stored, leaving apps with no generated type and no schema entry for it. A type now states its own rendering throughPluginFieldType.codegen, receiving the field as declared so a type whose options narrow what it stores can narrow what it generates.A type's options can now be held in a
pluginOptionscontainer core never reads, so an option may use a name the field schema already declares —options,fields,admin,label— which was previously judged against the core meaning and refused. Options written directly on a field are still read, and a type is handed one flat view of both, so where an option was stored is not something a plugin author tracks.A user field whose type a plugin contributed can now be declared from code with
pluginUserField(), which was previously impossible without a cast:UserFieldConfigadmits only the built-in shapes, and widening it to accept an unknown type token would have made a malformed built-in declaration pass too.validateFieldValuesis now available from the plugin SDK, marked experimental until a first-party plugin depends on it, so a plugin storing structured content of its own applies the same rules a write does rather than reimplementingrequired, the per-type checks, and every plugin field type'svalidate.Several correctness fixes ride along, most of them about a value reaching a column its type cannot hold.
A JSON column stores a JSON document, and
trueor42is a document as much as{}is; only objects were encoded, so a scalar reached the driver as its own type and could not round-trip through a SQLite text column. The four write paths that each carried their own copy of that encoding now share one.A value written to a custom user field was never checked against the column its type stores in, and a failed
user_extwrite is read as the extension table being absent — so the user was created without the value, with extensions disabled for the rest of the process, rather than the write being refused. Such a value is now refused with the field named. A required single field backed by a plugin type was seeded with the wrong kind of value for the same reason, which could stop the single being created at all.nextly buildnow generates types for a project made only of singles, field groups or user fields, where it previously wrote nothing or left a stale file, and narrowsPermissionSlugandEventNameasgenerate:typesdoes — a deployment build no longer widens types a development run had narrowed.db:sync --watchnow keeps watching such a project too, instead of exiting its watch loop and never re-syncing.A key named
__proto__was silently dropped when rebuilding an object from data nobody validates, which lost it from a delivered webhook envelope, from a stored version diff, and from the declaration a plugin validator judges. Anddb:sync --watchcould classify one config's columns with another config's field types, because a reload replaces the process-wide registry while the previous sync is still running; work now resolves against the config it started from, and a reload whose watcher was replaced mid-flight no longer applies its result or leaves its registrations behind. -
#418
21bb5b3Thanks @mobeenabdullah! - Stop serving unpublished rows through relationships. A related row is now filtered by Draft/Published exactly as a direct read of it is, so a published document linking to a draft one no longer discloses that draft's contents. -
#414
45faba9Thanks @mobeenabdullah! - Emituser.createdanduser.deletedwebhook events. Both were already advertised as subscribable in the admin UI but had no emit sites, so an operator could subscribe to events that never fired. They now record into the transactional outbox atomically with the account change, through a new Drizzle-transaction recorder (recordEventInTx) that lets services running onBaseService.withTransaction— like the auth service — participate in the outbox without the adapter's positional transaction context. Each event is attributed to the authenticated caller and, like the content write paths, offers the fast-path drain and a bounded retention prune after commit — including on self-registration — so delivery and outbox pruning do not wait for the scheduled drain. The delete event reads the removed account's identity inside the delete transaction, so a concurrent update cannot make it report a stale address. The payload is PII-safe: identity only (id, email, name), never the password hash, a token, or role assignments.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly