diff --git a/.changeset/calm-otters-glide.md b/.changeset/calm-otters-glide.md deleted file mode 100644 index 482d90ac..00000000 --- a/.changeset/calm-otters-glide.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@opensaas/stack-core': minor ---- - -Make `calendarDay` a `YYYY-MM-DD` string end-to-end (Keystone's CalendarDay scalar) - -`calendarDay` is now a `YYYY-MM-DD` **string** at the `context.db` boundary in -both directions, so its type, validation, and runtime value finally agree. -Previously the field validated a `YYYY-MM-DD` string but its TypeScript type was -`Date`, so a typed caller passing `new Date(...)` hit a runtime `ValidationError`. - -- The field/read type and the generated `CreateInput`/`UpdateInput` input types - are now `string`. -- Writes accept only a `YYYY-MM-DD` string; a malformed string or a `Date` is - rejected at runtime by validation (a `ValidationError`). -- Storage is unchanged: `DateTime @db.Date` on Postgres/MySQL, the SQLite TEXT - fallback as before. - -**Behavioral change (reads):** reading a `calendarDay` now returns a -`YYYY-MM-DD` string instead of a `Date`. A field `resolveOutput` transform -normalises the value Prisma returns from the `@db.Date` column, using UTC -components to avoid timezone off-by-one. Consumers that previously relied on a -`Date` on read should update to the string form: - -```typescript -const event = await context.db.event.findUnique({ where: { id } }) -event?.startDate // => '2025-01-15' (string, not Date) - -// Writes: pass YYYY-MM-DD strings, not Date objects -await context.db.event.create({ data: { startDate: '2025-01-15' } }) -``` diff --git a/.changeset/calm-otters-narrow.md b/.changeset/calm-otters-narrow.md deleted file mode 100644 index edf53046..00000000 --- a/.changeset/calm-otters-narrow.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@opensaas/stack-cli': minor ---- - -Enforce field-level scalar narrowing at the write call site, and fix `checkbox({ defaultValue: false })` optionality - -The generated `context.db..create()/update()/createMany()/updateMany()` `data` -type now narrows scalar fields to their OpenSaaS `getTypeScriptType()` types instead of -inheriting Prisma's wider input types. Field-level narrowing (e.g. `calendarDay` → `string`) -is now a genuine compile-time error to violate, not just a runtime validation failure. - -```ts -// calendarDay is a `string` end-to-end: -await context.db.event.create({ data: { startDate: new Date() } }) -// ^^^^^^^^^^ Type 'Date' is not assignable to type 'string'. -await context.db.event.create({ data: { startDate: '2026-01-01' } }) // ✅ compiles -``` - -Relationship nested writes (`connect`/`create`/`connectOrCreate`), unchecked foreign keys -(e.g. `authorId`), and `decimal`/`json` writes are unaffected: `decimal` still accepts -`Decimal | number | string` and `json` still accepts Prisma's `JsonNull`/`DbNull` sentinels. - -Also fixes a latent bug where `checkbox({ defaultValue: false })` (and any field with a -falsy-but-present default) was generated as a required field on create — it is now correctly -optional. - -Note: this may surface pre-existing type errors in consumer code that passed a `Date` to a -`calendarDay` field. Such code already failed at runtime; it now fails at compile time. Pass a -`YYYY-MM-DD` string instead. diff --git a/.changeset/clever-foxes-consolidate.md b/.changeset/clever-foxes-consolidate.md deleted file mode 100644 index 7e46d948..00000000 --- a/.changeset/clever-foxes-consolidate.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@opensaas/stack-cli': minor ---- - -Consolidate nullability between the standalone `{List}CreateInput`/`{List}UpdateInput` exports and the call-site write-`data` override into a single source of truth (#608). - -The generated types previously described a list's create/update input shape in two places that disagreed on how a nullable scalar was represented: the write-`data` override emitted `name?: string | null` (matching Prisma's nullable-column input) while the standalone `{List}CreateInput`/`{List}UpdateInput` emitted `name?: string`. Both paths now render each scalar member through one shared helper, so a nullable scalar is consistently `name?: T | null` in every input representation. Required scalars stay required, and `decimal`/`json`/relationship/multi-column handling is unchanged. - -This is a non-breaking type refinement, but if you assigned the standalone `{List}CreateInput`/`{List}UpdateInput` types into a stricter local type, a nullable scalar may now be inferred as `T | null`: - -```typescript -// A nullable text() field on Post now generates: -export type PostCreateInput = { - title: string // required scalar — unchanged - content?: string | null // nullable scalar — now includes `| null` -} -``` diff --git a/.changeset/clever-jars-shine.md b/.changeset/clever-jars-shine.md deleted file mode 100644 index 56f47a5b..00000000 --- a/.changeset/clever-jars-shine.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Enforce required json fields on create: an omitted key is now rejected while any -present value (object, array, primitive, or null) is still accepted. diff --git a/.changeset/loud-foxes-write.md b/.changeset/loud-foxes-write.md deleted file mode 100644 index e5c5363f..00000000 --- a/.changeset/loud-foxes-write.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Make non-sudo writes fail loud in `filterWritableFields` (Keystone parity). - -Undeclared `data` keys on create/update now throw instead of passing through unchecked (#564), and fields denied by field-level access now throw instead of being silently stripped (#568). `sudo` remains the single trusted bypass; system fields and relationship foreign keys still pass through. Raw multi-column split columns (e.g. `media_url`/`media_size` from an `image()`/`file()` field) are now gated by their owning field's write access — supplying them directly under non-sudo when that field denies the write throws, instead of bypassing the field's `access.create`/`access.update`. - -Behavioural narrowing: a list-level `resolveInput` hook that adds keys to `resolvedData` which are not declared fields will now be rejected by the undeclared-key throw. No production hook does this today. diff --git a/.changeset/nested-writes-full-hooks.md b/.changeset/nested-writes-full-hooks.md deleted file mode 100644 index 59bce95d..00000000 --- a/.changeset/nested-writes-full-hooks.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -'@opensaas/stack-core': minor ---- - -Nested relation writes now run the full hook pipeline inside one transaction (#569) - -A record written via a nested `create`, `update`, or `delete` now fires the SAME -list- and field-level `beforeOperation`/`afterOperation` hooks as the equivalent -top-level write — so side effects (workflows, notifications, billing) are -identical whether a record is written nested or top-level. Previously nested -writes ran only `resolveInput`/`validate`/field-rules and silently skipped the -before/after side-effect hooks. - -- Nested **create** runs `beforeOperation` (create) → persist → `afterOperation` - receiving the created `item`. -- Nested **update** runs `afterOperation` receiving both `originalItem` (the row - before) and the updated `item`. -- Nested **delete** runs `beforeOperation`/`afterOperation` receiving the - `originalItem`. - -Existing access control, validation, silent-failure, sudo-bypass, and the #578 -nested-`connect`/`connectOrCreate` read-access + DB-reachability behavior are -unchanged. Pass-through nested kinds (`disconnect`/`set`/`updateMany`/ -`deleteMany`) are out of scope and behave as before. See ADR-0010. - -For to-many nested creates (`create: [{A},{B}]`), each created record's -`afterOperation` now fires exactly once against its OWN distinct row, recovered -by id-diff against the rows that existed before the write — so a pre-existing -sibling is never passed as the "created" item, and multiple creates no longer -collapse to a single row. - -BEHAVIOR CHANGE — every write is now transactional, and a throwing -`beforeOperation`/`afterOperation` (or validation) rolls the whole write back. -The entire operation (parent + all nested writes) now runs inside one -`prisma.$transaction`, so it is atomic. Previously an `afterOperation` that threw -left the row committed; now it rolls back with the transaction (more -Keystone-correct). If you relied on a thrown `afterOperation` leaving the row -persisted, move that work to run after the write returns. - -Inside a `beforeOperation`/`afterOperation` hook, `context.db` (and -`context.prisma`) are now bound to the write's transaction, so any `context.db` -write a hook performs participates in — and rolls back with — the same -transaction. Externally-visible side effects that must survive a rollback should -not use `context.db` from within these hooks (transaction-boundary hooks for -that are deferred — see #590). - -```ts -// Nested create now fires the related list's beforeOperation/afterOperation, -// atomically with the parent — a throw anywhere rolls the whole write back. -await context.db.post.update({ - where: { id }, - data: { - title: 'Updated', - author: { create: { name: 'New Author' } }, // User hooks fire; atomic - }, -}) -``` diff --git a/.changeset/node-build-target.md b/.changeset/node-build-target.md deleted file mode 100644 index d5c62a74..00000000 --- a/.changeset/node-build-target.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@opensaas/stack-cli': minor -'@opensaas/stack-core': minor ---- - -Add an opt-in **Node build** of the generated `.opensaas/` bundle (ADR-0011, #579). - -Setting `output: { buildTarget: 'node' }` in `opensaas.config.ts` makes `opensaas generate` additionally compile the bundle to a plain-Node-loadable ESM form under `.opensaas/dist/` — `.js` + `.d.ts` with a `{"type":"module"}` marker — alongside the default `.ts` bundler form. The compiled entry is `.opensaas/dist/context.js`, with the Prisma client subtree at `.opensaas/dist/prisma-client/**` and the project config compiled in as a sibling, so a live module (e.g. better-auth's Prisma adapter) can be imported in a bundler-less runtime — plain Node, a Playwright e2e helper, or a build-time script — that the default `.ts` form cannot execute. - -The Node build is purely additive: with `output.buildTarget` absent (the default), generation behaves exactly as before and no `.opensaas/dist/` is emitted. - -```typescript -// opensaas.config.ts -export default config({ - output: { buildTarget: 'node' }, - // ... -}) - -// then, from a plain-Node consumer (no bundler, no tsx): -import { createAuth } from '@opensaas/stack-auth/server' -import { config, rawOpensaasContext } from './.opensaas/dist/context.js' - -const auth = createAuth(config, rawOpensaasContext) -await auth.api.signUpEmail({ body: { email, password, name } }) -``` - -The compile runs via the TypeScript compiler API with `rewriteRelativeImportExtensions` (turning the bundle's `.ts`-extension imports into runnable `.js` specifiers), `declaration`, `skipLibCheck`, and `noEmitOnError: false`, so it reuses the bundle's type-clean guarantee without adding a build dependency. `'node'` is the only `buildTarget` today; the field is a string-literal union so future compiled targets can be added without a breaking change. diff --git a/.changeset/prisma-generator-options.md b/.changeset/prisma-generator-options.md deleted file mode 100644 index 85cd6752..00000000 --- a/.changeset/prisma-generator-options.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@opensaas/stack-cli': minor -'@opensaas/stack-core': minor ---- - -Make the generated `.opensaas/prisma-client` subtree statically resolvable by default and add a `db.prismaGeneratorOptions` passthrough. - -The generated `generator client { ... }` block now emits `importFileExtension = "ts"` and `moduleFormat = "esm"` by default, so the prisma-client subtree uses explicit `.ts` import extensions and matches the extension style the rest of the `.opensaas` bundle already uses — the whole import graph is statically resolvable by a bundler out of the box, no post-generation surgery required. - -A new optional `db.prismaGeneratorOptions` lets you override these values when you need a different module/extension story (e.g. emitting `.js` extensions for a plain-Node consumer). Any value you supply wins; omitted keys fall back to the `ts`/`esm` defaults. The existing `previewFeatures = ["multiSchema"]` emission (when `db.schemas` is set) is preserved and coexists with the new options. - -```typescript -export default config({ - db: { - provider: 'postgresql', - prismaGeneratorOptions: { - importFileExtension: 'js', - moduleFormat: 'commonjs', - }, - // ... rest of config - }, - // ... -}) -``` diff --git a/.changeset/quiet-foxes-jump.md b/.changeset/quiet-foxes-jump.md deleted file mode 100644 index 98b33c97..00000000 --- a/.changeset/quiet-foxes-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Required json fields now reject a present `null` during validation rather than failing later as a DB NOT NULL violation. Omitted keys on update are still allowed; the Prisma column nullability is unchanged. diff --git a/.changeset/quiet-otters-fix.md b/.changeset/quiet-otters-fix.md deleted file mode 100644 index 389733fb..00000000 --- a/.changeset/quiet-otters-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Fix update validation rejecting omitted required fields under zod 4.4 by using key-optionality (`.optional()`) instead of `z.union([schema, z.undefined()])`. Partial updates that omit a required-on-create field now validate; present values still enforce their rules. diff --git a/.changeset/silent-otters-connect.md b/.changeset/silent-otters-connect.md deleted file mode 100644 index eeb4f527..00000000 --- a/.changeset/silent-otters-connect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Fix false denial of nested `connect` (and `connectOrCreate`'s connect branch): connect now requires read/query access on the target and evaluates filter results via DB reachability (`findFirst({ where: { AND: [connection, accessFilter] } })`), so nested-relation and `AND`/`OR`/`some`/`none`/`not` filters no longer always fail. diff --git a/.changeset/silent-owls-fix.md b/.changeset/silent-owls-fix.md deleted file mode 100644 index e9f419d3..00000000 --- a/.changeset/silent-owls-fix.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@opensaas/stack-ui': patch ---- - -Fix `ui.listView.initialSort` applying sort client-side instead of as a DB-level `orderBy` - -Previously, `initialSort` was applied to the already-fetched page in memory, meaning a 500-row list with `initialSort: { field: 'sentAt', direction: 'desc' }` would only show the 50 most recent rows of the _current page_ rather than the 50 most recent rows overall. The sort is now passed as `orderBy` to `findMany` so pagination and sorting compose correctly. - -Column-header clicks also now navigate with a `?sort=field:direction` URL param (instead of mutating local state), so subsequent sorts are also DB-level and work correctly across pages. diff --git a/.changeset/silver-foxes-gather.md b/.changeset/silver-foxes-gather.md deleted file mode 100644 index 980de35c..00000000 --- a/.changeset/silver-foxes-gather.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@opensaas/stack-core': minor ---- - -Gate nested `connect` by the owning relationship field's field-level access - -Nested `connect` (and the connect branch of `connectOrCreate`) is now gated by -the owning relationship field's create/update field-level access, in addition to -the target list's read/query access and DB-reachability check. This completes -the Keystone-parity rule that a connect requires both read access on the target -AND write access on the owning relationship field. `sudo` bypasses the check. - -```typescript -Post: list({ - fields: { - // A non-sudo caller can only connect an author when this field's - // update access permits it (and the target User is readable/reachable). - author: relationship({ - ref: 'User.posts', - access: { update: ({ session }) => session?.role === 'editor' }, - }), - }, -}) -``` diff --git a/.changeset/spotted-falcons-merge.md b/.changeset/spotted-falcons-merge.md deleted file mode 100644 index cfa8d0e3..00000000 --- a/.changeset/spotted-falcons-merge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Fix row-level access bypass when an explicit `include` is passed to non-sudo `findUnique`/`findMany`. The caller's `include` is now merged with (not replaced by) the access-controlled include: denied relations are dropped, each relation's access `where` is AND-combined with any caller nested `where`, and nested includes are filtered at every level. Sudo and query-fragment paths are unchanged. When no access-controlled include is computed (inside a `resolveOutput`/virtual-field context, at max include depth, or for a list with no relationships), the caller's `include` is passed through unchanged rather than dropped — avoiding fail-closed data loss. diff --git a/.changeset/swift-foxes-cheer.md b/.changeset/swift-foxes-cheer.md deleted file mode 100644 index d6a9c1ee..00000000 --- a/.changeset/swift-foxes-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-core': patch ---- - -Enforce unique-`where` for `context.db..findUnique` — a non-unique `where` now throws a clear error instead of silently returning a nondeterministic row. Use `findFirst` for non-unique single-row lookups. diff --git a/.changeset/swift-foxes-clean.md b/.changeset/swift-foxes-clean.md deleted file mode 100644 index a756101f..00000000 --- a/.changeset/swift-foxes-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-cli': patch ---- - -Remove unused getRelatedListName helper from the types generator (dead code, no behavior change) diff --git a/.changeset/swift-otters-findfirst.md b/.changeset/swift-otters-findfirst.md deleted file mode 100644 index 5ad47fcf..00000000 --- a/.changeset/swift-otters-findfirst.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@opensaas/stack-core': minor -'@opensaas/stack-cli': minor ---- - -Add `findFirst` to access-controlled `context.db.` delegates - -`findFirst` is sugar over the existing access-filtered `findMany` (`take: 1`), so -it introduces no new access surface: it applies the exact same query-access checks -and access-controlled include building as `findMany`, then returns the first -matching row or `null`. It honours the read-side silent-failure contract — an -access-denied query yields `null` rather than throwing. - -```ts -// Non-unique single-row lookup -const account = await context.db.account.findFirst({ - where: { userId: '123' }, - orderBy: { createdAt: 'desc' }, -}) - -// Narrow the single result with a query fragment -const post = await context.db.post.findFirst({ - where: { published: true }, - query: postFragment, -}) -// post: ResultOf | null -``` - -The CLI type generator now emits a `findFirst` method (and `FindFirstArgs` -type) for each list in the generated `.opensaas/types.ts`, so migrated apps that -reach for the familiar Prisma `findFirst` pattern get full type support. diff --git a/.changeset/transaction-boundary-hooks.md b/.changeset/transaction-boundary-hooks.md deleted file mode 100644 index 98c509bc..00000000 --- a/.changeset/transaction-boundary-hooks.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@opensaas/stack-core': minor ---- - -Add `beforeTransaction` / `afterTransaction` transaction-boundary hooks (list- and field-level) - -These run OUTSIDE the write's database transaction (in addition to the in-transaction `beforeOperation`/`afterOperation`), for non-transactional side effects like external API calls that must not hold a transaction open and cannot be rolled back. They fire per `(list, operation)` involved in the write (the top-level list plus each nested create/update/delete list) and form a symmetric compensation bracket: `afterTransaction` always runs when its paired `beforeTransaction` ran, receiving the outcome (`status: 'committed' | 'rolled-back'` plus `error` on rollback). On commit it gets the persisted `item` (and `originalItem` for update/delete) **only for the top-level record** — for nested lists these are `undefined`, since the per-record persisted row is not recoverable outside the transaction; use the in-transaction `afterOperation` for per-record nested compensation. On rollback it gets no `item` so it can undo what `beforeTransaction` did. `connectOrCreate` is enumerated as a best-effort create involvement (a resolve-to-connect still fires the bracket with no write), so compensators should be idempotent. - -```typescript -list({ - fields: { name: text() }, - hooks: { - // Runs before the transaction opens. - beforeTransaction: async ({ operation, inputData }) => { - await billing.reserveSeat(inputData.seatId) - }, - // Always runs after the transaction settles. - afterTransaction: async (args) => { - if (args.status === 'rolled-back') { - // The write did not persist (args.error explains why) — compensate. - await billing.releaseSeat(args.inputData.seatId) - } else { - await billing.confirmSeat(args.item.seatId) - } - }, - }, -}) -``` - -A throwing `beforeTransaction` aborts the write (the transaction never opens) and fires `afterTransaction` (`rolled-back`) only for lists whose `beforeTransaction` already ran. A throwing `afterTransaction` does not stop the other compensators; errors are surfaced afterward. Sudo does not affect these hooks. This is an additive, non-Keystone extension and does not change the existing `beforeOperation`/`afterOperation` semantics. diff --git a/.changeset/wise-points-cheer.md b/.changeset/wise-points-cheer.md deleted file mode 100644 index dc5ec83c..00000000 --- a/.changeset/wise-points-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@opensaas/stack-cli': patch ---- - -Fix `tsc` failure in generated `prisma-extensions.ts` for multi-column storage fields in `db: { columns: 'keystone' }` mode. The result extension's `needs` now references the physical part columns (e.g. `image_url`, `image_pathname`, …) derived from the field's `getColumnNames`, instead of the logical field name which has no scalar on the model (previously typed `true` against `never`). This removes the last error forcing `@ts-nocheck` on the generated bundle (#559). diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index b5db527b..d2886c0c 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-auth +## 0.25.0 + ## 0.24.0 ## 0.23.0 diff --git a/packages/auth/package.json b/packages/auth/package.json index b6bf740e..09763d9b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-auth", - "version": "0.24.0", + "version": "0.25.0", "description": "Better-auth integration for OpenSaas Stack", "type": "module", "main": "./dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 94cd3185..8f98efea 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,128 @@ # @opensaas/stack-cli +## 0.25.0 + +### Minor Changes + +- [#606](https://github.com/OpenSaasAU/stack/pull/606) [`801230e`](https://github.com/OpenSaasAU/stack/commit/801230e1a95efc17c8bec46c7094f0b72956f54b) Thanks [@borisno2](https://github.com/borisno2)! - Enforce field-level scalar narrowing at the write call site, and fix `checkbox({ defaultValue: false })` optionality + + The generated `context.db..create()/update()/createMany()/updateMany()` `data` + type now narrows scalar fields to their OpenSaaS `getTypeScriptType()` types instead of + inheriting Prisma's wider input types. Field-level narrowing (e.g. `calendarDay` → `string`) + is now a genuine compile-time error to violate, not just a runtime validation failure. + + ```ts + // calendarDay is a `string` end-to-end: + await context.db.event.create({ data: { startDate: new Date() } }) + // ^^^^^^^^^^ Type 'Date' is not assignable to type 'string'. + await context.db.event.create({ data: { startDate: '2026-01-01' } }) // ✅ compiles + ``` + + Relationship nested writes (`connect`/`create`/`connectOrCreate`), unchecked foreign keys + (e.g. `authorId`), and `decimal`/`json` writes are unaffected: `decimal` still accepts + `Decimal | number | string` and `json` still accepts Prisma's `JsonNull`/`DbNull` sentinels. + + Also fixes a latent bug where `checkbox({ defaultValue: false })` (and any field with a + falsy-but-present default) was generated as a required field on create — it is now correctly + optional. + + Note: this may surface pre-existing type errors in consumer code that passed a `Date` to a + `calendarDay` field. Such code already failed at runtime; it now fails at compile time. Pass a + `YYYY-MM-DD` string instead. + +- [#609](https://github.com/OpenSaasAU/stack/pull/609) [`1d79fe6`](https://github.com/OpenSaasAU/stack/commit/1d79fe6aad79a3598ebb2ca973d9936757b25c1f) Thanks [@borisno2](https://github.com/borisno2)! - Consolidate nullability between the standalone `{List}CreateInput`/`{List}UpdateInput` exports and the call-site write-`data` override into a single source of truth ([#608](https://github.com/OpenSaasAU/stack/issues/608)). + + The generated types previously described a list's create/update input shape in two places that disagreed on how a nullable scalar was represented: the write-`data` override emitted `name?: string | null` (matching Prisma's nullable-column input) while the standalone `{List}CreateInput`/`{List}UpdateInput` emitted `name?: string`. Both paths now render each scalar member through one shared helper, so a nullable scalar is consistently `name?: T | null` in every input representation. Required scalars stay required, and `decimal`/`json`/relationship/multi-column handling is unchanged. + + This is a non-breaking type refinement, but if you assigned the standalone `{List}CreateInput`/`{List}UpdateInput` types into a stricter local type, a nullable scalar may now be inferred as `T | null`: + + ```typescript + // A nullable text() field on Post now generates: + export type PostCreateInput = { + title: string // required scalar — unchanged + content?: string | null // nullable scalar — now includes `| null` + } + ``` + +- [#594](https://github.com/OpenSaasAU/stack/pull/594) [`4f0d407`](https://github.com/OpenSaasAU/stack/commit/4f0d40721feff1a3109647a81fcbe47db5970026) Thanks [@borisno2](https://github.com/borisno2)! - Add an opt-in **Node build** of the generated `.opensaas/` bundle (ADR-0011, [#579](https://github.com/OpenSaasAU/stack/issues/579)). + + Setting `output: { buildTarget: 'node' }` in `opensaas.config.ts` makes `opensaas generate` additionally compile the bundle to a plain-Node-loadable ESM form under `.opensaas/dist/` — `.js` + `.d.ts` with a `{"type":"module"}` marker — alongside the default `.ts` bundler form. The compiled entry is `.opensaas/dist/context.js`, with the Prisma client subtree at `.opensaas/dist/prisma-client/**` and the project config compiled in as a sibling, so a live module (e.g. better-auth's Prisma adapter) can be imported in a bundler-less runtime — plain Node, a Playwright e2e helper, or a build-time script — that the default `.ts` form cannot execute. + + The Node build is purely additive: with `output.buildTarget` absent (the default), generation behaves exactly as before and no `.opensaas/dist/` is emitted. + + ```typescript + // opensaas.config.ts + export default config({ + output: { buildTarget: 'node' }, + // ... + }) + + // then, from a plain-Node consumer (no bundler, no tsx): + import { createAuth } from '@opensaas/stack-auth/server' + import { config, rawOpensaasContext } from './.opensaas/dist/context.js' + + const auth = createAuth(config, rawOpensaasContext) + await auth.api.signUpEmail({ body: { email, password, name } }) + ``` + + The compile runs via the TypeScript compiler API with `rewriteRelativeImportExtensions` (turning the bundle's `.ts`-extension imports into runnable `.js` specifiers), `declaration`, `skipLibCheck`, and `noEmitOnError: false`, so it reuses the bundle's type-clean guarantee without adding a build dependency. `'node'` is the only `buildTarget` today; the field is a string-literal union so future compiled targets can be added without a breaking change. + +- [#592](https://github.com/OpenSaasAU/stack/pull/592) [`e355c05`](https://github.com/OpenSaasAU/stack/commit/e355c05a0787980b997609c4571271ab5c250f36) Thanks [@borisno2](https://github.com/borisno2)! - Make the generated `.opensaas/prisma-client` subtree statically resolvable by default and add a `db.prismaGeneratorOptions` passthrough. + + The generated `generator client { ... }` block now emits `importFileExtension = "ts"` and `moduleFormat = "esm"` by default, so the prisma-client subtree uses explicit `.ts` import extensions and matches the extension style the rest of the `.opensaas` bundle already uses — the whole import graph is statically resolvable by a bundler out of the box, no post-generation surgery required. + + A new optional `db.prismaGeneratorOptions` lets you override these values when you need a different module/extension story (e.g. emitting `.js` extensions for a plain-Node consumer). Any value you supply wins; omitted keys fall back to the `ts`/`esm` defaults. The existing `previewFeatures = ["multiSchema"]` emission (when `db.schemas` is set) is preserved and coexists with the new options. + + ```typescript + export default config({ + db: { + provider: 'postgresql', + prismaGeneratorOptions: { + importFileExtension: 'js', + moduleFormat: 'commonjs', + }, + // ... rest of config + }, + // ... + }) + ``` + +- [#584](https://github.com/OpenSaasAU/stack/pull/584) [`b17ec45`](https://github.com/OpenSaasAU/stack/commit/b17ec45127fe55f02437892e9fd389c67373635a) Thanks [@borisno2](https://github.com/borisno2)! - Add `findFirst` to access-controlled `context.db.` delegates + + `findFirst` is sugar over the existing access-filtered `findMany` (`take: 1`), so + it introduces no new access surface: it applies the exact same query-access checks + and access-controlled include building as `findMany`, then returns the first + matching row or `null`. It honours the read-side silent-failure contract — an + access-denied query yields `null` rather than throwing. + + ```ts + // Non-unique single-row lookup + const account = await context.db.account.findFirst({ + where: { userId: '123' }, + orderBy: { createdAt: 'desc' }, + }) + + // Narrow the single result with a query fragment + const post = await context.db.post.findFirst({ + where: { published: true }, + query: postFragment, + }) + // post: ResultOf | null + ``` + + The CLI type generator now emits a `findFirst` method (and `FindFirstArgs` + type) for each list in the generated `.opensaas/types.ts`, so migrated apps that + reach for the familiar Prisma `findFirst` pattern get full type support. + +### Patch Changes + +- [#606](https://github.com/OpenSaasAU/stack/pull/606) [`801230e`](https://github.com/OpenSaasAU/stack/commit/801230e1a95efc17c8bec46c7094f0b72956f54b) Thanks [@borisno2](https://github.com/borisno2)! - Remove unused getRelatedListName helper from the types generator (dead code, no behavior change) + +- [#591](https://github.com/OpenSaasAU/stack/pull/591) [`c741055`](https://github.com/OpenSaasAU/stack/commit/c74105548aadb9991a4cded3b12d9c1a5b0dcd0c) Thanks [@borisno2](https://github.com/borisno2)! - Fix `tsc` failure in generated `prisma-extensions.ts` for multi-column storage fields in `db: { columns: 'keystone' }` mode. The result extension's `needs` now references the physical part columns (e.g. `image_url`, `image_pathname`, …) derived from the field's `getColumnNames`, instead of the logical field name which has no scalar on the model (previously typed `true` against `never`). This removes the last error forcing `@ts-nocheck` on the generated bundle ([#559](https://github.com/OpenSaasAU/stack/issues/559)). + +- Updated dependencies [[`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9), [`be9a896`](https://github.com/OpenSaasAU/stack/commit/be9a8965ad6338c279e99cfe3bf24162e63ffb92), [`e39d6e9`](https://github.com/OpenSaasAU/stack/commit/e39d6e9e37be2337c8cf1979053e76877f14296c), [`fadd9db`](https://github.com/OpenSaasAU/stack/commit/fadd9dbd17085f4dd15899371a054ec46f943ce4), [`4f0d407`](https://github.com/OpenSaasAU/stack/commit/4f0d40721feff1a3109647a81fcbe47db5970026), [`e355c05`](https://github.com/OpenSaasAU/stack/commit/e355c05a0787980b997609c4571271ab5c250f36), [`ca4973b`](https://github.com/OpenSaasAU/stack/commit/ca4973b504eadb123d179e8f4d16d6ec8c9f8fc1), [`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9), [`ecbf834`](https://github.com/OpenSaasAU/stack/commit/ecbf834059a072c428b0739d6ebcf4c74be8c893), [`a93cebb`](https://github.com/OpenSaasAU/stack/commit/a93cebb5a6ba6550d8cdbb94f010c902ad7e29f1), [`481d6e0`](https://github.com/OpenSaasAU/stack/commit/481d6e00be90b1159b0b30eff015e5079c840158), [`4622b5f`](https://github.com/OpenSaasAU/stack/commit/4622b5fa8fc731e2c8995011f1be0cfe341578da), [`b17ec45`](https://github.com/OpenSaasAU/stack/commit/b17ec45127fe55f02437892e9fd389c67373635a), [`8f98e25`](https://github.com/OpenSaasAU/stack/commit/8f98e25fbef4ec0fc3ff0cba456ff7f2f7ba2ea8)]: + - @opensaas/stack-core@0.25.0 + ## 0.24.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index f7c042bc..1c6422b7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-cli", - "version": "0.24.0", + "version": "0.25.0", "description": "CLI tools for OpenSaas Stack", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a17aab6c..2b7dbc29 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,228 @@ # @opensaas/stack-core +## 0.25.0 + +### Minor Changes + +- [#602](https://github.com/OpenSaasAU/stack/pull/602) [`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9) Thanks [@borisno2](https://github.com/borisno2)! - Make `calendarDay` a `YYYY-MM-DD` string end-to-end (Keystone's CalendarDay scalar) + + `calendarDay` is now a `YYYY-MM-DD` **string** at the `context.db` boundary in + both directions, so its type, validation, and runtime value finally agree. + Previously the field validated a `YYYY-MM-DD` string but its TypeScript type was + `Date`, so a typed caller passing `new Date(...)` hit a runtime `ValidationError`. + - The field/read type and the generated `CreateInput`/`UpdateInput` input types + are now `string`. + - Writes accept only a `YYYY-MM-DD` string; a malformed string or a `Date` is + rejected at runtime by validation (a `ValidationError`). + - Storage is unchanged: `DateTime @db.Date` on Postgres/MySQL, the SQLite TEXT + fallback as before. + + **Behavioral change (reads):** reading a `calendarDay` now returns a + `YYYY-MM-DD` string instead of a `Date`. A field `resolveOutput` transform + normalises the value Prisma returns from the `@db.Date` column, using UTC + components to avoid timezone off-by-one. Consumers that previously relied on a + `Date` on read should update to the string form: + + ```typescript + const event = await context.db.event.findUnique({ where: { id } }) + event?.startDate // => '2025-01-15' (string, not Date) + + // Writes: pass YYYY-MM-DD strings, not Date objects + await context.db.event.create({ data: { startDate: '2025-01-15' } }) + ``` + +- [#593](https://github.com/OpenSaasAU/stack/pull/593) [`fadd9db`](https://github.com/OpenSaasAU/stack/commit/fadd9dbd17085f4dd15899371a054ec46f943ce4) Thanks [@{](https://github.com/{)! - Nested relation writes now run the full hook pipeline inside one transaction ([#569](https://github.com/OpenSaasAU/stack/issues/569)) + + A record written via a nested `create`, `update`, or `delete` now fires the SAME + list- and field-level `beforeOperation`/`afterOperation` hooks as the equivalent + top-level write — so side effects (workflows, notifications, billing) are + identical whether a record is written nested or top-level. Previously nested + writes ran only `resolveInput`/`validate`/field-rules and silently skipped the + before/after side-effect hooks. + - Nested **create** runs `beforeOperation` (create) → persist → `afterOperation` + receiving the created `item`. + - Nested **update** runs `afterOperation` receiving both `originalItem` (the row + before) and the updated `item`. + - Nested **delete** runs `beforeOperation`/`afterOperation` receiving the + `originalItem`. + + Existing access control, validation, silent-failure, sudo-bypass, and the [#578](https://github.com/OpenSaasAU/stack/issues/578) + nested-`connect`/`connectOrCreate` read-access + DB-reachability behavior are + unchanged. Pass-through nested kinds (`disconnect`/`set`/`updateMany`/ + `deleteMany`) are out of scope and behave as before. See ADR-0010. + + For to-many nested creates (`create: [{A},{B}]`), each created record's + `afterOperation` now fires exactly once against its OWN distinct row, recovered + by id-diff against the rows that existed before the write — so a pre-existing + sibling is never passed as the "created" item, and multiple creates no longer + collapse to a single row. + + BEHAVIOR CHANGE — every write is now transactional, and a throwing + `beforeOperation`/`afterOperation` (or validation) rolls the whole write back. + The entire operation (parent + all nested writes) now runs inside one + `prisma.$transaction`, so it is atomic. Previously an `afterOperation` that threw + left the row committed; now it rolls back with the transaction (more + Keystone-correct). If you relied on a thrown `afterOperation` leaving the row + persisted, move that work to run after the write returns. + + Inside a `beforeOperation`/`afterOperation` hook, `context.db` (and + `context.prisma`) are now bound to the write's transaction, so any `context.db` + write a hook performs participates in — and rolls back with — the same + transaction. Externally-visible side effects that must survive a rollback should + not use `context.db` from within these hooks (transaction-boundary hooks for + that are deferred — see [#590](https://github.com/OpenSaasAU/stack/issues/590)). + + ```ts + // Nested create now fires the related list's beforeOperation/afterOperation, + // atomically with the parent — a throw anywhere rolls the whole write back. + await context.db.post.update({ + where: { id }, + data: { + title: 'Updated', + create: { name: 'New Author' } }, // User hooks fire; atomic + }, + }) + ``` + +- [#594](https://github.com/OpenSaasAU/stack/pull/594) [`4f0d407`](https://github.com/OpenSaasAU/stack/commit/4f0d40721feff1a3109647a81fcbe47db5970026) Thanks [@borisno2](https://github.com/borisno2)! - Add an opt-in **Node build** of the generated `.opensaas/` bundle (ADR-0011, [#579](https://github.com/OpenSaasAU/stack/issues/579)). + + Setting `output: { buildTarget: 'node' }` in `opensaas.config.ts` makes `opensaas generate` additionally compile the bundle to a plain-Node-loadable ESM form under `.opensaas/dist/` — `.js` + `.d.ts` with a `{"type":"module"}` marker — alongside the default `.ts` bundler form. The compiled entry is `.opensaas/dist/context.js`, with the Prisma client subtree at `.opensaas/dist/prisma-client/**` and the project config compiled in as a sibling, so a live module (e.g. better-auth's Prisma adapter) can be imported in a bundler-less runtime — plain Node, a Playwright e2e helper, or a build-time script — that the default `.ts` form cannot execute. + + The Node build is purely additive: with `output.buildTarget` absent (the default), generation behaves exactly as before and no `.opensaas/dist/` is emitted. + + ```typescript + // opensaas.config.ts + export default config({ + output: { buildTarget: 'node' }, + // ... + }) + + // then, from a plain-Node consumer (no bundler, no tsx): + import { createAuth } from '@opensaas/stack-auth/server' + import { config, rawOpensaasContext } from './.opensaas/dist/context.js' + + const auth = createAuth(config, rawOpensaasContext) + await auth.api.signUpEmail({ body: { email, password, name } }) + ``` + + The compile runs via the TypeScript compiler API with `rewriteRelativeImportExtensions` (turning the bundle's `.ts`-extension imports into runnable `.js` specifiers), `declaration`, `skipLibCheck`, and `noEmitOnError: false`, so it reuses the bundle's type-clean guarantee without adding a build dependency. `'node'` is the only `buildTarget` today; the field is a string-literal union so future compiled targets can be added without a breaking change. + +- [#592](https://github.com/OpenSaasAU/stack/pull/592) [`e355c05`](https://github.com/OpenSaasAU/stack/commit/e355c05a0787980b997609c4571271ab5c250f36) Thanks [@borisno2](https://github.com/borisno2)! - Make the generated `.opensaas/prisma-client` subtree statically resolvable by default and add a `db.prismaGeneratorOptions` passthrough. + + The generated `generator client { ... }` block now emits `importFileExtension = "ts"` and `moduleFormat = "esm"` by default, so the prisma-client subtree uses explicit `.ts` import extensions and matches the extension style the rest of the `.opensaas` bundle already uses — the whole import graph is statically resolvable by a bundler out of the box, no post-generation surgery required. + + A new optional `db.prismaGeneratorOptions` lets you override these values when you need a different module/extension story (e.g. emitting `.js` extensions for a plain-Node consumer). Any value you supply wins; omitted keys fall back to the `ts`/`esm` defaults. The existing `previewFeatures = ["multiSchema"]` emission (when `db.schemas` is set) is preserved and coexists with the new options. + + ```typescript + export default config({ + db: { + provider: 'postgresql', + prismaGeneratorOptions: { + importFileExtension: 'js', + moduleFormat: 'commonjs', + }, + // ... rest of config + }, + // ... + }) + ``` + +- [#600](https://github.com/OpenSaasAU/stack/pull/600) [`a93cebb`](https://github.com/OpenSaasAU/stack/commit/a93cebb5a6ba6550d8cdbb94f010c902ad7e29f1) Thanks [@relationship({](https://github.com/relationship({)! - Gate nested `connect` by the owning relationship field's field-level access + + Nested `connect` (and the connect branch of `connectOrCreate`) is now gated by + the owning relationship field's create/update field-level access, in addition to + the target list's read/query access and DB-reachability check. This completes + the Keystone-parity rule that a connect requires both read access on the target + AND write access on the owning relationship field. `sudo` bypasses the check. + + ```typescript + Post: list({ + fields: { + // A non-sudo caller can only connect an author when this field's + // update access permits it (and the target User is readable/reachable). + + ref: 'User.posts', + access: { update: ({ session }) => session?.role === 'editor' }, + }), + }, + }) + ``` + +- [#584](https://github.com/OpenSaasAU/stack/pull/584) [`b17ec45`](https://github.com/OpenSaasAU/stack/commit/b17ec45127fe55f02437892e9fd389c67373635a) Thanks [@borisno2](https://github.com/borisno2)! - Add `findFirst` to access-controlled `context.db.` delegates + + `findFirst` is sugar over the existing access-filtered `findMany` (`take: 1`), so + it introduces no new access surface: it applies the exact same query-access checks + and access-controlled include building as `findMany`, then returns the first + matching row or `null`. It honours the read-side silent-failure contract — an + access-denied query yields `null` rather than throwing. + + ```ts + // Non-unique single-row lookup + const account = await context.db.account.findFirst({ + where: { userId: '123' }, + orderBy: { createdAt: 'desc' }, + }) + + // Narrow the single result with a query fragment + const post = await context.db.post.findFirst({ + where: { published: true }, + query: postFragment, + }) + // post: ResultOf | null + ``` + + The CLI type generator now emits a `findFirst` method (and `FindFirstArgs` + type) for each list in the generated `.opensaas/types.ts`, so migrated apps that + reach for the familiar Prisma `findFirst` pattern get full type support. + +- [#601](https://github.com/OpenSaasAU/stack/pull/601) [`8f98e25`](https://github.com/OpenSaasAU/stack/commit/8f98e25fbef4ec0fc3ff0cba456ff7f2f7ba2ea8) Thanks [@borisno2](https://github.com/borisno2)! - Add `beforeTransaction` / `afterTransaction` transaction-boundary hooks (list- and field-level) + + These run OUTSIDE the write's database transaction (in addition to the in-transaction `beforeOperation`/`afterOperation`), for non-transactional side effects like external API calls that must not hold a transaction open and cannot be rolled back. They fire per `(list, operation)` involved in the write (the top-level list plus each nested create/update/delete list) and form a symmetric compensation bracket: `afterTransaction` always runs when its paired `beforeTransaction` ran, receiving the outcome (`status: 'committed' | 'rolled-back'` plus `error` on rollback). On commit it gets the persisted `item` (and `originalItem` for update/delete) **only for the top-level record** — for nested lists these are `undefined`, since the per-record persisted row is not recoverable outside the transaction; use the in-transaction `afterOperation` for per-record nested compensation. On rollback it gets no `item` so it can undo what `beforeTransaction` did. `connectOrCreate` is enumerated as a best-effort create involvement (a resolve-to-connect still fires the bracket with no write), so compensators should be idempotent. + + ```typescript + list({ + fields: { name: text() }, + hooks: { + // Runs before the transaction opens. + beforeTransaction: async ({ operation, inputData }) => { + await billing.reserveSeat(inputData.seatId) + }, + // Always runs after the transaction settles. + afterTransaction: async (args) => { + if (args.status === 'rolled-back') { + // The write did not persist (args.error explains why) — compensate. + await billing.releaseSeat(args.inputData.seatId) + } else { + await billing.confirmSeat(args.item.seatId) + } + }, + }, + }) + ``` + + A throwing `beforeTransaction` aborts the write (the transaction never opens) and fires `afterTransaction` (`rolled-back`) only for lists whose `beforeTransaction` already ran. A throwing `afterTransaction` does not stop the other compensators; errors are surfaced afterward. Sudo does not affect these hooks. This is an additive, non-Keystone extension and does not change the existing `beforeOperation`/`afterOperation` semantics. + +### Patch Changes + +- [#603](https://github.com/OpenSaasAU/stack/pull/603) [`be9a896`](https://github.com/OpenSaasAU/stack/commit/be9a8965ad6338c279e99cfe3bf24162e63ffb92) Thanks [@borisno2](https://github.com/borisno2)! - Enforce required json fields on create: an omitted key is now rejected while any + present value (object, array, primitive, or null) is still accepted. + +- [#583](https://github.com/OpenSaasAU/stack/pull/583) [`e39d6e9`](https://github.com/OpenSaasAU/stack/commit/e39d6e9e37be2337c8cf1979053e76877f14296c) Thanks [@borisno2](https://github.com/borisno2)! - Make non-sudo writes fail loud in `filterWritableFields` (Keystone parity). + + Undeclared `data` keys on create/update now throw instead of passing through unchecked ([#564](https://github.com/OpenSaasAU/stack/issues/564)), and fields denied by field-level access now throw instead of being silently stripped ([#568](https://github.com/OpenSaasAU/stack/issues/568)). `sudo` remains the single trusted bypass; system fields and relationship foreign keys still pass through. Raw multi-column split columns (e.g. `media_url`/`media_size` from an `image()`/`file()` field) are now gated by their owning field's write access — supplying them directly under non-sudo when that field denies the write throws, instead of bypassing the field's `access.create`/`access.update`. + + Behavioural narrowing: a list-level `resolveInput` hook that adds keys to `resolvedData` which are not declared fields will now be rejected by the undeclared-key throw. No production hook does this today. + +- [#605](https://github.com/OpenSaasAU/stack/pull/605) [`ca4973b`](https://github.com/OpenSaasAU/stack/commit/ca4973b504eadb123d179e8f4d16d6ec8c9f8fc1) Thanks [@borisno2](https://github.com/borisno2)! - Required json fields now reject a present `null` during validation rather than failing later as a DB NOT NULL violation. Omitted keys on update are still allowed; the Prisma column nullability is unchanged. + +- [#602](https://github.com/OpenSaasAU/stack/pull/602) [`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9) Thanks [@borisno2](https://github.com/borisno2)! - Fix update validation rejecting omitted required fields under zod 4.4 by using key-optionality (`.optional()`) instead of `z.union([schema, z.undefined()])`. Partial updates that omit a required-on-create field now validate; present values still enforce their rules. + +- [#587](https://github.com/OpenSaasAU/stack/pull/587) [`ecbf834`](https://github.com/OpenSaasAU/stack/commit/ecbf834059a072c428b0739d6ebcf4c74be8c893) Thanks [@borisno2](https://github.com/borisno2)! - Fix false denial of nested `connect` (and `connectOrCreate`'s connect branch): connect now requires read/query access on the target and evaluates filter results via DB reachability (`findFirst({ where: { AND: [connection, accessFilter] } })`), so nested-relation and `AND`/`OR`/`some`/`none`/`not` filters no longer always fail. + +- [#589](https://github.com/OpenSaasAU/stack/pull/589) [`481d6e0`](https://github.com/OpenSaasAU/stack/commit/481d6e00be90b1159b0b30eff015e5079c840158) Thanks [@borisno2](https://github.com/borisno2)! - Fix row-level access bypass when an explicit `include` is passed to non-sudo `findUnique`/`findMany`. The caller's `include` is now merged with (not replaced by) the access-controlled include: denied relations are dropped, each relation's access `where` is AND-combined with any caller nested `where`, and nested includes are filtered at every level. Sudo and query-fragment paths are unchanged. When no access-controlled include is computed (inside a `resolveOutput`/virtual-field context, at max include depth, or for a list with no relationships), the caller's `include` is passed through unchanged rather than dropped — avoiding fail-closed data loss. + +- [#586](https://github.com/OpenSaasAU/stack/pull/586) [`4622b5f`](https://github.com/OpenSaasAU/stack/commit/4622b5fa8fc731e2c8995011f1be0cfe341578da) Thanks [@borisno2](https://github.com/borisno2)! - Enforce unique-`where` for `context.db..findUnique` — a non-unique `where` now throws a clear error instead of silently returning a nondeterministic row. Use `findFirst` for non-unique single-row lookups. + ## 0.24.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index e8c7148f..0f86f084 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-core", - "version": "0.24.0", + "version": "0.25.0", "description": "Core stack for OpenSaas - schema definition, access control, and runtime utilities", "type": "module", "main": "./dist/index.js", diff --git a/packages/rag/CHANGELOG.md b/packages/rag/CHANGELOG.md index d7d4e334..dee1e27d 100644 --- a/packages/rag/CHANGELOG.md +++ b/packages/rag/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-rag +## 0.25.0 + ## 0.24.0 ## 0.23.0 diff --git a/packages/rag/package.json b/packages/rag/package.json index 3d6437cd..763b4a07 100644 --- a/packages/rag/package.json +++ b/packages/rag/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-rag", - "version": "0.24.0", + "version": "0.25.0", "description": "RAG and AI embeddings integration for OpenSaas Stack", "type": "module", "main": "./dist/index.js", diff --git a/packages/storage-s3/CHANGELOG.md b/packages/storage-s3/CHANGELOG.md index 674b7d02..aef4090f 100644 --- a/packages/storage-s3/CHANGELOG.md +++ b/packages/storage-s3/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-storage-s3 +## 0.25.0 + ## 0.24.0 ## 0.23.0 diff --git a/packages/storage-s3/package.json b/packages/storage-s3/package.json index a6ba52ca..01ccdbb8 100644 --- a/packages/storage-s3/package.json +++ b/packages/storage-s3/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-storage-s3", - "version": "0.24.0", + "version": "0.25.0", "description": "AWS S3 storage provider for OpenSaas Stack file uploads", "type": "module", "exports": { diff --git a/packages/storage-vercel/CHANGELOG.md b/packages/storage-vercel/CHANGELOG.md index c06062d8..42020535 100644 --- a/packages/storage-vercel/CHANGELOG.md +++ b/packages/storage-vercel/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-storage-vercel +## 0.25.0 + ## 0.24.0 ## 0.23.0 diff --git a/packages/storage-vercel/package.json b/packages/storage-vercel/package.json index 823ede1b..1ad35d68 100644 --- a/packages/storage-vercel/package.json +++ b/packages/storage-vercel/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-storage-vercel", - "version": "0.24.0", + "version": "0.25.0", "description": "Vercel Blob storage provider for OpenSaas Stack file uploads", "type": "module", "exports": { diff --git a/packages/storage/CHANGELOG.md b/packages/storage/CHANGELOG.md index af12726f..5c694446 100644 --- a/packages/storage/CHANGELOG.md +++ b/packages/storage/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-storage +## 0.25.0 + ## 0.24.0 ### Minor Changes diff --git a/packages/storage/package.json b/packages/storage/package.json index 688edc30..aaf31f38 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-storage", - "version": "0.24.0", + "version": "0.25.0", "description": "File and image upload field types with pluggable storage providers for OpenSaas Stack", "type": "module", "exports": { diff --git a/packages/tiptap/CHANGELOG.md b/packages/tiptap/CHANGELOG.md index d8b8a735..b9aabcc9 100644 --- a/packages/tiptap/CHANGELOG.md +++ b/packages/tiptap/CHANGELOG.md @@ -1,5 +1,7 @@ # @opensaas/stack-tiptap +## 0.25.0 + ## 0.24.0 ## 0.23.0 diff --git a/packages/tiptap/package.json b/packages/tiptap/package.json index 786edfcd..0f5b017b 100644 --- a/packages/tiptap/package.json +++ b/packages/tiptap/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-tiptap", - "version": "0.24.0", + "version": "0.25.0", "description": "Tiptap rich text editor integration for OpenSaas Stack", "type": "module", "main": "./dist/index.js", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 8c8373ce..7a648843 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,15 @@ # @opensaas/stack-ui +## 0.25.0 + +### Patch Changes + +- [#607](https://github.com/OpenSaasAU/stack/pull/607) [`61547be`](https://github.com/OpenSaasAU/stack/commit/61547beb5ec7d4aff30753849e36a738c49c91e4) Thanks [@borisno2](https://github.com/borisno2)! - Fix `ui.listView.initialSort` applying sort client-side instead of as a DB-level `orderBy` + + Previously, `initialSort` was applied to the already-fetched page in memory, meaning a 500-row list with `initialSort: { field: 'sentAt', direction: 'desc' }` would only show the 50 most recent rows of the _current page_ rather than the 50 most recent rows overall. The sort is now passed as `orderBy` to `findMany` so pagination and sorting compose correctly. + + Column-header clicks also now navigate with a `?sort=field:direction` URL param (instead of mutating local state), so subsequent sorts are also DB-level and work correctly across pages. + ## 0.24.0 ### Minor Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index ddc05bc7..eef3e8d1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opensaas/stack-ui", - "version": "0.24.0", + "version": "0.25.0", "description": "Composable React UI components for OpenSaas Stack", "type": "module", "main": "./dist/index.js",