Skip to content

v2.0.0-next.16

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 09 Jun 22:50
· 1 commit to next since this release
c6ad0dd

@baeta/auth@2.0.0-next.16

Major Changes

  • The auth package was renamed from @baeta/extension-auth to @baeta/auth and rewritten. The whole authorization model changed: by @andreisergiu98 in #290

    • Setup is now an app plugin (createAuth(...) returns authAppPlugin) instead of an extension registered via createExtensions(...).
    • Scopes and grants are generic type parameters on createAuth, not a global declare global { namespace AuthExtension { ... } } augmentation.
    • Rules are built with a typed scope accessor and rule combinators (rule.and / rule.or / rule.chain / rule.race) instead of plain nested objects with $or / $and / $granted keys.
    • Rules are applied by chaining .$use(auth(...)) on the field/type/module builder instead of the side-effecting $auth(...) / $postAuth(...) methods. $postAuth is now authAfter.
    • Grants now attach to the result object's identity (a WeakMap), with support for arrays and custom target functions, instead of being keyed by resolver path.
    • New per-request scope caching with an optional cacheKeyMap for non-serializable scope arguments.

    1. Setup: extension -> app plugin

    Scopes and grants used to be declared by augmenting a global AuthExtension namespace and the extension was created with authExtension(...) then registered through createExtensions(...).

    v1:

    // src/extensions/auth-extension.ts
    import { authExtension } from "@baeta/extension-auth";
    import { UnauthenticatedError } from "@baeta/errors";
    import type { Context } from "../types/context.ts";
    
    declare global {
      export namespace AuthExtension {
        export interface Scopes {
          isPublic: boolean;
          isLoggedIn: boolean;
          hasAccess: "guest" | "user" | "admin";
        }
        export interface GrantsMap {
          readUserPhotos: boolean;
        }
      }
    }
    
    export const authExt = authExtension<Context>(
      async (ctx) => ({
        isPublic: true,
        isLoggedIn: async () => {
          if (ctx.userId == null) throw new UnauthenticatedError();
          return true;
        },
        hasAccess: (access: string) => ["guest", "user"].includes(access),
      }),
      {
        defaultScopes: {
          Query: { isLoggedIn: true },
          Mutation: { isLoggedIn: true },
          Subscription: { subscribe: { isLoggedIn: true } },
        },
      }
    );
    
    // src/extensions/index.ts
    import { createExtensions } from "@baeta/core";
    import { authExt } from "./auth-extension.ts";
    export default createExtensions(authExt);

    v2: scopes/grants are type parameters, and createAuth returns the building blocks directly. defaultScopes is now a callback that hands you the typed scope and rule builders, and the Subscription default is flat (no subscribe wrapper).

    // src/lib/auth.ts
    import { createAuth } from "@baeta/auth";
    import { UnauthenticatedError } from "@baeta/errors";
    import type { Context } from "../types/context.ts";
    
    export type Scopes = {
      isPublic: boolean;
      isLoggedIn: boolean;
      hasAccess: "guest" | "user" | "admin";
    };
    export type Grants = "readUserPhotos";
    
    export const { auth, authAfter, authAppPlugin, rule, scope } = createAuth<
      Context,
      Scopes,
      Grants
    >(
      async (ctx) => ({
        isPublic: true,
        isLoggedIn: async () => {
          if (ctx.userId == null) throw new UnauthenticatedError();
          return true;
        },
        hasAccess: (access) => ["guest", "user"].includes(access),
      }),
      {
        defaultScopes: ({ scope }) => ({
          Query: scope.isLoggedIn,
          Mutation: scope.isLoggedIn,
          Subscription: scope.isLoggedIn,
        }),
      }
    );

    Register the returned authAppPlugin on the application (extensions are gone):

    // v2 — src/app.ts
    import { createApplication } from "@baeta/core";
    import { authAppPlugin } from "./lib/auth.ts";
    import modules from "./modules/index.ts";
    
    const baeta = createApplication({
      modules,
      plugins: [authAppPlugin],
    });

    If a module calls auth(...) / authAfter(...) but authAppPlugin isn't registered, Baeta throws at schema build time.

    2. Building rules: typed scope + rule instead of object literals

    Scope rules were plain objects keyed by scope name, with $and / $or / $chain / $race and $granted as special keys. They're now constructed with the typed scope accessor and rule combinators.

    • scope.isLoggedIn — boolean scope (read as a property)
    • scope.hasAccess('admin') — parameterized scope (called with its typed argument)
    • scope.$granted('readUserPhotos') — granted-permission check
    • rule.and(...) / rule.or(...) / rule.chain(...) / rule.race(...) — combinators (two or more rules each)

    3. Applying a rule to a resolver: $use(auth(...)) instead of $auth(...)

    v1 called the side-effecting $auth (pre-resolution) / $postAuth (post-resolution) methods. v2 chains .$use(auth(...)) / .$use(authAfter(...)) on the builder. auth and authAfter take the same (rules, options?) shape, where options still carries grants and skipDefaults.

    v1:

    import { ForbiddenError } from "@baeta/errors";
    import { db } from "../../lib/db/prisma.ts";
    import { getUserModule } from "./typedef.ts";
    
    const { Query, Mutation } = getUserModule();
    
    Query.user.$auth(
      { $or: { isPublic: true, isLoggedIn: true } },
      { skipDefaults: true, grants: ["readUserPhotos"] }
    );
    
    // Admin-only
    Mutation.createUser.$auth({ hasAccess: "admin" });
    
    // Dynamic: return true / false / a scope object
    Mutation.updateUser.$auth(async (params) => {
      const user = await db.user.findFirst({ where: params.args.where });
      if (user && user.id === params.ctx.userId) return true;
      if (!user) throw new ForbiddenError();
      return { hasAccess: "admin" };
    });

    v2:

    import { auth, rule, scope } from "../../lib/auth.ts";
    import { db } from "../../lib/db/prisma.ts";
    import { UserModule } from "./typedef.ts";
    
    const { Query, Mutation } = UserModule;
    
    const userQuery = Query.user
      .$use(
        auth(rule.or(scope.isPublic, scope.isLoggedIn), {
          skipDefaults: true,
          grants: ["readUserPhotos"],
        })
      )
      .resolve(({ args }) => db.user.findFirst({ where: args.where }));
    
    // Admin-only
    const createUser = Mutation.createUser
      .$use(auth(scope.hasAccess("admin")))
      .resolve(({ args }) => db.user.create({ data: args.data }));
    
    // Dynamic: return true / false / a ScopeRules value
    const updateUser = Mutation.updateUser
      .$use(
        auth(async ({ args, ctx }) => {
          const user = await db.user.findFirst({ where: args.where });
          if (user && user.id === ctx.userId) return true;
          return scope.hasAccess("admin");
        })
      )
      .resolve(/* ... */);

    auth(...) chains at the type level (Type.$use(auth(...)).$fields({...})) and module level (Module.$use(auth(...)).$schema({...})) too. For subscriptions, each .$use(auth(...)) only protects the phase that follows it (subscribe vs resolve).

    Post-resolution checks move from $postAuth to authAfter (it cannot be used on mutations, since it runs after the resolver):

    // v1
    Query.user.$postAuth((params, user) =>
      user.id === params.ctx.userId ? true : { hasAccess: "admin" }
    );
    
    // v2
    const userQuery = Query.user
      .$use(
        authAfter((params, user) =>
          user && user.id === params.ctx.userId ? true : scope.hasAccess("admin")
        )
      )
      .resolve(({ args }) => db.user.findFirst({ where: args.where }));

    4. Grants

    Grants are attached the same way (via the grants option) and consumed via $granted, but the internal model changed: v1 keyed grants by the resolver path; v2 keys them by the result object's identity (a WeakMap). For array results the grant attaches to each element, and you can redirect a grant to a nested value with a GrantConfig { grant, target }.

    v1 (consume):

    User.photos.$auth({ $granted: "readUserPhotos" });

    v2 (consume):

    const userPhotosResolver = User.photos
      .$use(auth(scope.$granted("readUserPhotos")))
      .resolve(({ source }) =>
        db.userPhoto.findMany({ where: { userId: source.id } })
      );

    Because grants now key off the result object, a child resolver only sees the grant when its params.source is the exact object the granting resolver returned. New in v2, you can target a nested object:

    auth(scope.isLoggedIn, {
      grants: { grant: "readUserPhotos", target: (entry) => entry.user },
    });

    5. Scope caching (new)

    Within a single request each scope-with-argument runs at most once; the cache is per-context. Serializable arguments (primitives, plain objects, arrays of those) are auto-keyed; anything else falls back to reference identity. Provide a cacheKeyMap to give non-serializable arguments (class instances, Date, etc.) a stable key:

    createAuth<Context, Scopes, Grants>(loader, {
      cacheKeyMap: {
        canAccessProject: (project) => project.id,
      },
    });

    Error handling is unchanged in shape: createAuth still accepts an errorResolver, and aggregateErrorResolver / ScopeErrorResolver are still exported.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c, 046dc5c]:
    • @baeta/core@2.0.0-next.16
    • @baeta/errors@2.0.0-next.16
    • @baeta/util-log@2.0.0-next.5

@baeta/cache@2.0.0-next.16

Major Changes

  • The cache system has been fully rewritten and split out of the GraphQL framework into a standalone @baeta/cache package. It no longer depends on @baeta/core or graphql and can be used on its own. by @andreisergiu98 in #290

    The biggest conceptual change: the old GraphQL extension API is gone. There is no more cacheExtension(...), $createCache, $useCache, $cacheRef, $cacheRevision, or $cacheClear. You no longer register a cache as a Baeta extension. Instead you build a cache object directly with createCache(client, options), declare cached queries with defineQuery, and wire it into resolvers with the normal resolver builder (.map() / .resolve()) — the cache is just a library you call.

    Building a cache

    In v1 the storage adapter wrapped a client and you created per-type caches off a GraphQL type via the extension:

    // v1
    import { cacheExtension } from "@baeta/extension-cache";
    import { RedisStore } from "@baeta/extension-cache-redis";
    import Redis from "ioredis";
    
    const redis = new Redis("redis://localhost:6379");
    const redisStore = new RedisStore(redis);
    
    export const cacheExt = cacheExtension(redisStore, { ttl: 3600 });
    
    // in a module
    const userCache = User.$createCache({ revision: 2 });

    In v2 you pass a CacheClient (the adapter) directly to createCache, give the cache a name, and provide parse/serialize. parse/serialize are now required, which lets you validate cached values on read (e.g. with Zod). TTL is now in milliseconds (ttlMs) instead of seconds (ttl):

    // v2
    import { createCache, defineQuery } from "@baeta/cache";
    import { RedisCacheClient } from "@baeta/cache-ioredis";
    import Redis from "ioredis";
    import { z } from "zod";
    
    const redis = new Redis("redis://localhost:6379");
    const redisClient = new RedisCacheClient(redis);
    
    const UserSchema = z.object({
      id: z.string(),
      email: z.string(),
      name: z.string(),
    });
    
    export const userCache = createCache(redisClient, {
      name: "UserCache",
      revision: 2,
      ttlMs: 60 * 60 * 1000, // defaults to 1 hour
      parse: (value) => UserSchema.parse(JSON.parse(value)),
      serialize: (value) => JSON.stringify(value),
    })
      .withQueries({
        findUser: defineQuery({
          resolve: async (args: { id: string | null; email: string | null }) => {
            return db.user.findUnique({
              where: { id: args.id ?? undefined, email: args.email ?? undefined },
            });
          },
        }),
        findUsers: defineQuery({
          resolve: async () => db.user.findMany(),
        }),
      })
      .build();

    createCache(...) returns a factory: call .build() for an item-only cache, or .withQueries({...}).build() to attach typed, cached queries. By default getRef extracts the id field; pass getRef for items keyed differently.

    Reading through the cache

    $useCache(...) as a resolver middleware is gone. A cached query is just an async function (userCache.queries.findUser(args)) that you call from a normal resolver. The resolver builder's .map() turns resolver params into query args:

    // v1
    Query.user.$useCache(userCache);
    
    // v2
    export default Query.$fields({
      user: Query.user.map(({ args }) =>
        userCache.queries.findUser({
          id: args.where.id,
          email: args.where.email,
        })
      ),
      users: Query.users
        .map(() => userCache.queries.findUsers({}))
        .map(({ source }) => source ?? []),
    });

    Reconciling on mutations

    The single save() method is replaced by explicit insert() (new items), update() (existing items) and delete(). These give the cache enough information to reconcile every related query automatically, instead of manually clearing query results:

    // v1
    Mutation.updateUser.$use(async (params, next) => {
      const user = await next();
      await userCache.save(user);
      return user;
    });
    
    // v2
    const createUserMutation = Mutation.createUser.resolve(async ({ args }) => {
      const user = await db.user.create({ data: args.data });
      await userCache.insert(user); // new item
      return user;
    });
    
    const updateUserMutation = Mutation.updateUser
      .$use(async (next) => {
        const user = await next();
        if (user) {
          await userCache.update(user); // existing item — updates all queries
        }
        return user;
      })
      .resolve(({ args }) =>
        db.user.update({ where: { id: args.where.id }, data: args.data })
      );

    Targeted invalidation with indexArgsBy

    Relationship queries ("all photos for user X") can declare indexArgsBy so invalidation is surgical. defineQuery also accepts per-query onInsert / onUpdate / onDelete hooks, which receive helpers (invalidateAll, invalidateByArgs) to invalidate only the matching index keys:

    // v2
    export const userPhotoCache = createCache(redisClient, {
      name: "UserPhotoCache",
      parse: (value) => JSON.parse(value),
      serialize: (value) => JSON.stringify(value),
    })
      .withQueries({
        findUserPhotos: defineQuery({
          resolve: async (args: { userId: string }) =>
            db.userPhoto.findMany({ where: { userId: args.userId } }),
          indexArgsBy: { userId: true },
          onInsert(items, helpers) {
            return helpers.invalidateByArgs(
              items.map((item) => ({ userId: item.userId }))
            );
          },
          onDelete(pairs, helpers) {
            return helpers.invalidateByArgs(
              pairs.flatMap((p) =>
                p.previous ? [{ userId: p.previous.userId }] : []
              )
            );
          },
        }),
      })
      .build();

    Other changes

    • TTL is now milliseconds (ttlMs) everywhere, replacing v1's seconds-based ttl.
    • A cache must have a name; revision, ttlMs and namespace can be set on the cache or defaulted from the client.
    • By default, re-resolving a cached query does not overwrite items already in the cache (preventing a stale query result from clobbering a concurrent mutation). Set replaceExistingItems: true on a query to opt back into overwriting.
    • Custom hooks fire asynchronously by default; pass { waitForHooks: true } to insert/update/delete to await them.

    See the caching guide for an end-to-end setup.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/util-encoding@2.0.0-next.5
    • @baeta/util-log@2.0.0-next.5

@baeta/cache-cloudflare@2.0.0-next.16

Major Changes

  • The Cloudflare adapter was renamed from @baeta/extension-cache-cloudflare to @baeta/cache-cloudflare and rebuilt for the decoupled v2 cache API. It exports CloudflareCacheClient (extending the CacheClient base class from @baeta/cache) plus the BaetaCache Durable Object, replacing the v1 CloudflareStoreAdapter integration. by @andreisergiu98 in #290

    // v1
    import { cacheExtension } from "@baeta/extension-cache";
    import { CloudflareCacheClient } from "@baeta/extension-cache-cloudflare";
    
    // v2
    import { createCache } from "@baeta/cache";
    import { CloudflareCacheClient, BaetaCache } from "@baeta/cache-cloudflare";
    
    export const cloudflareClient = new CloudflareCacheClient(env.BAETA_CACHE);
    // pass cloudflareClient to createCache(cloudflareClient, { ... })

    CloudflareCacheClient is backed by the BaetaCache Durable Object and is intended for Cloudflare Workers deployments.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

@baeta/cache-ioredis@2.0.0-next.16

Major Changes

  • The ioredis adapter was renamed from @baeta/extension-cache-redis to @baeta/cache-ioredis and rebuilt for the decoupled v2 cache API. Instead of the old RedisStore (a Baeta Store), it now exports a RedisCacheClient that extends the CacheClient base class from @baeta/cache and is passed directly to createCache. by @andreisergiu98 in #290

    // v1
    import { cacheExtension } from "@baeta/extension-cache";
    import { RedisStore } from "@baeta/extension-cache-redis";
    import Redis from "ioredis";
    
    const redisStore = new RedisStore(new Redis("redis://localhost:6379"));
    export const cacheExt = cacheExtension(redisStore, { ttl: 3600 });
    
    // v2
    import { createCache } from "@baeta/cache";
    import { RedisCacheClient } from "@baeta/cache-ioredis";
    import Redis from "ioredis";
    
    export const redisClient = new RedisCacheClient(
      new Redis("redis://localhost:6379")
    );
    // pass redisClient to createCache(redisClient, { ... })

    RedisCacheClient accepts both a standalone Redis and a Cluster connection, and exposes pipeline-batching tuning options (maxPipelineSizeLimit, maxPipelineCommandLimit, maxCommandKeysLimit) plus the shared CacheClientOptions (ttlMs, revision, namespace).

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/cache-redis-common@2.0.0-next.14
    • @baeta/cache@2.0.0-next.16

@baeta/cache-iovalkey@2.0.0-next.16

Major Changes

  • @baeta/cache-iovalkey is a new cache adapter for the Valkey key-value store (the open-source Redis fork), using the iovalkey client. It extends the CacheClient base class from @baeta/cache and is a drop-in alternative to the ioredis adapter. by @andreisergiu98 in #290

    import { createCache } from "@baeta/cache";
    import { ValkeyCacheClient } from "@baeta/cache-iovalkey";
    import Valkey from "iovalkey";
    
    export const valkeyClient = new ValkeyCacheClient(
      new Valkey("valkey://localhost:6379")
    );
    // pass valkeyClient to createCache(valkeyClient, { ... })

    ValkeyCacheClient accepts both standalone and cluster connections and shares the same pipeline-batching tuning options and CacheClientOptions as the ioredis adapter.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/cache-redis-common@2.0.0-next.14
    • @baeta/cache@2.0.0-next.16

@baeta/cache-redis-common@2.0.0-next.14

Major Changes

  • @baeta/cache-redis-common is a new package holding the Redis/Valkey building blocks shared by @baeta/cache-ioredis and @baeta/cache-iovalkey — pipeline batching helpers (assertNoPipelineErrors, batchPipeline) and the cached Lua script loader (createRedisScripts, RedisScripts). Most applications depend on a concrete adapter rather than on this package directly. by @andreisergiu98 in #290

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

@baeta/cache-upstash@2.0.0-next.16

Major Changes

  • The Upstash adapter was renamed from @baeta/extension-cache-upstash to @baeta/cache-upstash and rebuilt for the decoupled v2 cache API. It now exports an UpstashCacheClient (extending the CacheClient base class from @baeta/cache) that you pass directly to createCache, replacing the v1 UpstashStore/UpstashClient pair. by @andreisergiu98 in #290

    // v1
    import { cacheExtension } from "@baeta/extension-cache";
    import { UpstashClient, UpstashStore } from "@baeta/extension-cache-upstash";
    
    const store = new UpstashStore(new UpstashClient(/* ... */));
    export const cacheExt = cacheExtension(store, { ttl: 3600 });
    
    // v2
    import { createCache } from "@baeta/cache";
    import { UpstashCacheClient } from "@baeta/cache-upstash";
    
    export const upstashClient = new UpstashCacheClient({
      url: "UPSTASH_REDIS_URL",
      token: "UPSTASH_REDIS_TOKEN",
    });
    // pass upstashClient to createCache(upstashClient, { ... })
  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/cache-redis-common@2.0.0-next.14
    • @baeta/cache@2.0.0-next.16

@baeta/cli@2.0.0-next.16

Major Changes

  • The CLI no longer compiles or bundles your app. It now does one thing — code generation — and assumes your runtime executes TypeScript directly (Node --experimental-strip-types/node:type-stripping, Bun, or Deno). by @andreisergiu98 in #290

    baeta build removed

    The entire build command is gone, along with its --generate, --onSuccess, and --onError flags. Bundling lived in @baeta/compiler, which has been removed in v2. Run your app's source directly instead of building to dist/.

    The only command is now generate (alias g), which is also implied as the default workflow. Its flags are --watch/-w and --run/-r. (The v1 generate command's --skipInitial/-s flag was also removed.)

    --run already existed in v1's generate command, but it is now the way to run your app: Baeta launches the --run command once as a long-lived process and only respawns it if it exits — live reload comes from node --watch (or bun --watch / deno --watch) inside the --run command, which restarts when your source and the regenerated typedefs change.

    v1 package.json:

    {
      "scripts": {
        "build": "baeta build --generate",
        "generate": "baeta generate",
        "start": "baeta build --watch --generate --onSuccess='node --enable-source-maps dist/app.js'"
      }
    }

    v2 package.json:

    {
      "scripts": {
        "build": "baeta generate",
        "start": "baeta generate --watch --run='node --watch --inspect src/app.ts'",
        "types": "tsc --noEmit"
      }
    }

    Note the --run target is now src/app.ts (source) rather than dist/app.js (build output).

    Bun and Deno use the same shape, swapping the runtime inside --run:

    {
      "start:bun": "baeta generate --watch --run='bun --watch --inspect src/app.ts'",
      "start:deno": "baeta generate --watch --run='deno --watch --inspect --allow-net --allow-read --allow-env src/app.ts'"
    }

    Config loading is now a native import

    In v1, a baeta.ts config was transpiled on the fly with esbuild via the optional @baeta/compiler peer dependency. In v2 the config file is loaded with a native import(), so it must be valid ESM your runtime can execute directly (.ts, .mts, .js, or .mjs). @baeta/compiler is no longer a peer dependency.

    defineConfig is now required: it stamps a version: 'v2' marker that the CLI validates on load. A bare config object (or a v1-style config) is rejected.

    import { defineConfig } from "@baeta/cli";
    
    export default defineConfig({
      graphql: {
        schemas: ["src/**/*.gql"],
      },
    });

    The compiler field on BaetaOptions has also been removed, since there is no build step to configure.

    ./ink export removed

    The @baeta/cli/ink subpath export was dropped.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

@baeta/complexity@2.0.0-next.16

Major Changes

  • The complexity extension has been renamed to @baeta/complexity and rebuilt around the v2 app-plugin and builder APIs. The complexityExtension() factory and the magic .$complexity() resolver method are gone. You now call createComplexity() to get back two things — a complexity(...) rule helper applied via the generic .$use(...) builder method, and a complexityAppPlugin registered on createApplication. by @andreisergiu98 in #290

    The scoring options (defaultComplexity, defaultListMultiplier, limit, complexityError) and the per-field settings shape ({ complexity, multiplier }, or false to disable) are unchanged.

    Setup

    v1 — an extension factory, registered through the extensions API:

    // src/modules/extensions.ts
    import { complexityExtension } from "@baeta/extension-complexity";
    import type { Context } from "../context.ts";
    
    export const complexity = complexityExtension<Context>({
      defaultComplexity: 1,
      defaultListMultiplier: 10,
      limit: { depth: 10, breadth: 50, complexity: 1000 },
    });
    
    // createExtensions(complexity, ...)

    v2 — createComplexity() returns a rule helper plus an app plugin:

    // src/lib/complexity.ts
    import { createComplexity } from "@baeta/complexity";
    import type { Context } from "../types/context.ts";
    
    export const { complexity, complexityAppPlugin } = createComplexity<Context>({
      defaultComplexity: 1,
      defaultListMultiplier: 10,
      // static limits, or a (ctx) => limits function for dynamic limits
      async limit(ctx) {
        return { depth: 10, breadth: 50, complexity: 1000 };
      },
    });

    Register the plugin on the application:

    // src/app.ts
    import { createApplication } from "@baeta/core";
    import { complexityAppPlugin } from "./lib/complexity.ts";
    import modules from "./modules/index.ts";
    
    const baeta = createApplication({
      modules,
      plugins: [complexityAppPlugin],
    });

    Using complexity(...) in a module without registering complexityAppPlugin is now a build-time error rather than a silent no-op.

    Applying rules to fields

    v1 — a dedicated .$complexity() method on resolvers, types, and subscriptions, called for its side effect:

    // Per-field override
    Query.users.$complexity(({ args }) => ({ complexity: 1, multiplier: 5 }));
    
    // Disable for a field
    Query.simple.$complexity(() => false);
    
    // Whole type
    User.$complexity(() => ({ complexity: 2, multiplier: 5 }));

    v2 — the complexity(...) helper is passed to the generic .$use(...) builder method and chained into .resolve() / .$fields():

    import { complexity } from "../../lib/complexity.ts";
    import { UserModule } from "./typedef.ts";
    
    const { Query, User } = UserModule;
    
    // Per-field override on a list field
    const usersQuery = Query.users
      .$use(complexity(({ args, ctx }) => ({ complexity: 1, multiplier: 5 })))
      .resolve(() => findUsers());
    
    // Disable complexity for a specific field
    const userQuery = Query.user
      .$use(complexity(() => false))
      .resolve(({ args }) => findUser(args.where));
    
    // Whole type — chain into $fields
    const userResolver = User.$use(complexity(() => ({ complexity: 2 }))).$fields(
      {
        id: User.id.key("id"),
        email: User.email.key("email"),
      }
    );

    The settings function still receives { args, ctx } and returns { complexity?, multiplier? } or false.

    Package rename

    v1 v2
    @baeta/extension-complexity @baeta/complexity

    The error exports (ComplexityError, ComplexityErrorCode, ComplexityErrorKind, GetComplexityError) and the option/type exports (ComplexityExtensionOptions, ComplexityLimit, GetComplexityLimit, FieldSettings, GetFieldSettings, GetFieldSettingsArgs) are unchanged.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

@baeta/core@2.0.0-next.16

Major Changes

  • @baeta/core was rewritten around a composable, side-effect-free builder API. The old functional approach — where calling Query.user(fn) registered a resolver as a side effect and you imported resolver files for their side effects — is gone. You now build each field with chainable helpers, collect them with $fields, and export a module schema with $schema. Type generation no longer mutates anything at import time, so a missing resolver is a compile-time error instead of a runtime surprise. by @andreisergiu98 in #290

    Resolver API: side-effect functions -> composable builders

    Every field is now declared with chainable helpers off the generated module and assembled with $fields/$schema. You must explicitly define a resolver for every field; an unresolved field fails type-checking when you pass the field map into $fields({…}).

    v1:

    // src/modules/user/resolvers.ts
    import { getUserModule } from "./typedef";
    
    const { Query, User } = getUserModule();
    
    // Calling the field registers the resolver as a side effect
    User.name((params) => `${params.root.firstName} ${params.root.lastName}`);
    
    Query.user(async (params) => {
      return db.user.findUnique({ where: params.args.where });
    });
    
    // src/modules/user/index.ts
    import "./resolvers"; // imported for its side effects
    import { getUserModule } from "./typedef";
    
    export const userModule = getUserModule();

    v2:

    // src/modules/user/user.type.ts
    import { UserModule } from "./typedef.ts";
    
    const { User } = UserModule;
    
    export default User.$fields({
      id: User.id.key("id"),
      email: User.email.key("email"),
      name: User.name.resolve(
        ({ source }) => `${source.firstName} ${source.lastName}`
      ),
    });
    
    // src/modules/user/user.queries.ts
    import { UserModule } from "./typedef.ts";
    
    const { Query } = UserModule;
    
    export default Query.$fields({
      user: Query.user.resolve(({ args }) =>
        db.user.findFirst({ where: args.where })
      ),
    });
    
    // src/modules/user/index.ts
    import { UserModule } from "./typedef.ts";
    import queryResolver from "./user.queries.ts";
    import userResolver from "./user.type.ts";
    
    export default UserModule.$schema({
      User: userResolver,
      Query: queryResolver,
    });

    Field helpers (pick the strictest one that fits):

    • .key("field") — read the value straight off the source object
    • .resolve(fn) — custom resolver; return type must match the schema field
    • .map(fn) — same shape as .resolve but accepts any return type (for chained transforms)
    • .to(fn) — transform whatever the previous step produced
    • .withDefault(value) — substitute value when the result is null/undefined
    • .undefinedAsNull() — map undefined to null for nullable fields

    Helpers compose, e.g. User.birthDate.key('birthDate').to(toDate) or User.friends.map(loadFriends).withDefault([]).

    The resolver/middleware params object also renamed root to source ({ source, args, ctx, info }).

    Middleware API: parameter order flipped, chained before .resolve()

    Middlewares are attached by chaining .$use(...) on a field and finishing with .resolve() (or .map()), then passing the result into $fields. The parameter order changed from (params, next) to (next, params).

    v1:

    Mutation.updateUser.$use(async ({ args, ctx }, next) => {
      const user = await next();
      ctx.pubsub.publish("user-updated", user);
      return user;
    });
    
    Mutation.updateUser((params) => db.user.update(/* … */));

    v2:

    const updateUser = Mutation.updateUser
      .$use(async (next, { ctx }) => {
        const user = await next();
        if (user) ctx.pubsub.publish("user-updated", user);
        return user;
      })
      .resolve(({ args }) => db.user.update(/* … */));
    
    export default Mutation.$fields({ updateUser });

    Middlewares can also be applied at the type level (Type.$use(...).$fields(...)) and at the module level (Module.$use(...).$schema(...)). Execution order is module -> type -> field, outside-in.

    Module registration and auto-import are now built-in

    Baeta auto-generates src/modules/index.ts, which collects every module (each module default-exports its schema via $schema()). You import that aggregate and hand it to createApplication. There is no more manual array of modules and no @baeta/plugin-autoload.

    v1:

    import { createApplication } from "@baeta/core";
    import { userModule } from "./modules/user";
    import { postModule } from "./modules/post";
    
    const baeta = createApplication({
      modules: [userModule, postModule],
    });

    v2:

    import { createApplication } from "@baeta/core";
    import modules from "./modules/index.ts"; // auto-generated
    
    const baeta = createApplication({ modules });

    createApplication options also changed: the pruneSchema option was removed (the schema is now built from explicit per-field resolvers), and two new options were added — plugins (app plugins, see below) and buildSchema (override how the executable schema is built).

    Context and custom types moved to a generated src/modules/types.ts

    The graphql.contextType setting in baeta.ts is gone. Source types, scalar mappings, the context type, and the info type are now configured in one generated file, src/modules/types.ts, via the ObjectTypes, Scalars, Ctx, and Info exports.

    v1 (baeta.ts):

    export default defineConfig({
      graphql: {
        schemas: ["src/**/*.gql"],
        contextType: "src/context#Context",
      },
    });

    v2 (src/modules/types.ts):

    import type { GraphQLResolveInfo } from "graphql";
    import type {
      BaseObjectTypes,
      BaseScalars,
    } from "../__generated__/utility.ts";
    import type { User } from "../lib/db/prisma.ts";
    import type { Context } from "../types/context.ts";
    
    export interface Scalars extends BaseScalars {
      DateTime: Date;
    }
    
    export interface ObjectTypes extends BaseObjectTypes {
      User: User; // override the source type for the GraphQL `User` type
    }
    
    export type Ctx = Context;
    export type Info = GraphQLResolveInfo;

    App plugins replace extensions

    The extensions concept and createExtensions({...}) were removed entirely. Cross-cutting features (auth, cache, etc.) now ship as app plugins that you pass to createApplication through the new plugins option. A plugin is created by its feature package (for example createAuth(...) returns an authAppPlugin) and registered once on the application — it mutates the relevant modules at build time, and the app throws if a module relies on a plugin that was not registered.

    v1:

    // src/modules/extensions.ts — discovered via baeta.ts
    import { createExtensions } from "@baeta/core";
    import { authExt } from "./auth-extension.ts";
    import { cacheExt } from "./cache-extension.ts";
    
    export default createExtensions(authExt, cacheExt);

    v2:

    // src/lib/auth.ts
    import { createAuth } from "@baeta/auth";
    
    export const { auth, authAppPlugin, rule, scope } = createAuth<
      Context,
      Scopes,
      Grants
    >(/* … */);
    
    // src/app.ts
    import { createApplication } from "@baeta/core";
    import { authAppPlugin } from "./lib/auth.ts";
    import modules from "./modules/index.ts";
    
    const baeta = createApplication({
      modules,
      plugins: [authAppPlugin],
    });

    Removed packages relevant to core users

    • @baeta/compiler — modern runtimes (Node, Bun, Deno) execute TypeScript natively, so there is no separate compile/build step. baeta build is gone; run your TS entry point directly or use your own bundler.
    • @baeta/plugin-autoload — module auto-importing is now built in via the generated src/modules/index.ts.

    Other notes

    • createContextStoreWithLoader was added alongside createContextStore for context stores that resolve a value lazily.
  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/errors@2.0.0-next.16

@baeta/directives@2.0.0-next.16

Major Changes

  • @baeta/directives still ships the same catalog of built-in input/object directives (@validString, @validInt, @validFloat, @constraints, @trim, @lower, @upper), but the way directives are applied to a module changed along with the new side-effect-free builder API. by @andreisergiu98 in #290

    How directives are registered

    In v1 a module came from a getXModule() factory and $directive was a void side-effect method that pushed a schema transformer onto the module. In v2 the module is a direct export and $directive() is an immutable method you chain before $schema()Module.$directive(transformer).$schema({ ... }) — accepting a single transformer or an array.

    @validString format argument removed

    The format: EMAIL | UUID | URL argument (and the generated StringFormat enum) was dropped from @validString, along with its email-validator and is-url dependencies.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/core@2.0.0-next.16
    • @baeta/errors@2.0.0-next.16

@baeta/env@2.0.0-next.16

Major Changes

  • The public API is unchanged — createEnvParser, the parse function it returns, and the EnvOptions / EnvTypes / EnvInferType types all keep the same shape. What changed is how raw values are coerced and validated, which can surface as new errors (or different results) for inputs that v1 accepted silently. by @andreisergiu98 in #290

    • Numbers are validated. v1 ran every value through Number(value), so a non-numeric env var produced NaN without complaint. v2 requires the value to match a decimal-number pattern and to be finite, throwing a descriptive error otherwise.
    • Booleans accept more spellings. v1 treated only the literal string 'true' as true and everything else (including '1', 'yes', typos, etc.) as false. v2 accepts true/1/yes/on as true and false/0/no/off as false (case-insensitive, trimmed), and throws on any unrecognised value instead of silently returning false.
    • Empty strings fall back to the default. For number and boolean params, an empty-string value ('') is now treated as "not provided" and resolves to options.default instead of being coerced.

    v1:

    const parse = createEnvParser((key) => process.env[key]);
    
    // PORT="abc"
    parse("PORT", { type: "number" }); // => NaN, no error
    
    // FLAG="1"
    parse("FLAG", { type: "boolean" }); // => false

    v2:

    const parse = createEnvParser((key) => process.env[key]);
    
    // PORT="abc"
    parse("PORT", { type: "number" }); // throws: not a valid decimal number
    
    // FLAG="1"
    parse("FLAG", { type: "boolean" }); // => true
  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

@baeta/errors@2.0.0-next.16

Major Changes

  • BaetaErrorCode is no longer a TypeScript enum. It is now a plain const object plus a derived union type that share the same name: by @andreisergiu98 in #290

    v1:

    export enum BaetaErrorCode {
      Unauthenticated = "UNAUTHENTICATED",
      Forbidden = "FORBIDDEN",
      BadUserInput = "BAD_USER_INPUT",
      InternalServerError = "INTERNAL_SERVER_ERROR",
      AggregateError = "AGGREGATE_ERROR",
    }

    v2:

    export const BaetaErrorCode = {
      Unauthenticated: "UNAUTHENTICATED",
      Forbidden: "FORBIDDEN",
      BadUserInput: "BAD_USER_INPUT",
      InternalServerError: "INTERNAL_SERVER_ERROR",
      AggregateError: "AGGREGATE_ERROR",
    } as const;
    
    export type BaetaErrorCode =
      (typeof BaetaErrorCode)[keyof typeof BaetaErrorCode];

    Value access is unchanged (BaetaErrorCode.Forbidden === 'FORBIDDEN'), and BaetaErrorCode still works as a type (it now resolves to the union of code strings). Code that relied on enum-only behavior must be updated: using a single member as a type (BaetaErrorCode.Forbidden in type position) and enum reverse-mapping (BaetaErrorCode['FORBIDDEN']) are no longer available. The error classes (UnauthenticatedError, ForbiddenError, BadUserInput, InternalServerError, AggregateGraphQLError) and their runtime behavior are otherwise unchanged.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/util-env@2.0.0-next.5

@baeta/federation@2.0.0-next.16

Major Changes

  • @baeta/federation is a new package providing the runtime helpers Baeta needs to act as an Apollo Federation subgraph. It is consumed by the code that @baeta/plugin-federation generates, but the helpers are public and can be used directly. by @andreisergiu98 in #290

    It exports two functions:

    • resolveEntities(representations, handlerMap, ctx, info) — implements the _entities resolver. It dispatches each { __typename, ...key } representation to the matching entity handler and resolves them in parallel, throwing on an unknown or missing __typename.
    • createFederationScalar(type, name, description?) — builds the scalars the federation spec requires. type is 'string' (for FieldSet) or 'json' (for _Any).

    The package is side-effect free and only declares a graphql peer dependency, so it has no impact on a non-federated app.

    import { createFederationScalar, resolveEntities } from "@baeta/federation";
    
    // Resolve federation entity references by __typename
    const entities = await resolveEntities(
      representations,
      handlersMap,
      ctx,
      info
    );
    
    // Federation spec scalars
    const anyScalar = createFederationScalar("json", "_Any");
    const fieldSetScalar = createFederationScalar("string", "FieldSet");

    In practice you don't call these by hand — @baeta/plugin-federation generates a baeta-federation module that wires them into the _entities/_service resolvers for you. The only code you write is the entity handlers themselves.

    Install it as a runtime dependency in any subgraph:

    yarn add @baeta/federation
  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

@baeta/generator@2.0.0-next.7

Major Changes

  • @baeta/generator drives the generate/watch pipeline that runs Baeta's codegen plugins. It was reworked for the v2 side-effect-free generation flow. by @andreisergiu98 in #290

    • GeneratorOptions no longer carries the GraphQL-specific knobs that the old graphql-codegen pipeline needed: baseTypesPath, contextType, extensions, and scalars are gone. Type/context/scalar configuration now lives in the generated src/modules/types.ts. A new typesDir option controls where shared __generated__ types are emitted (default ${modulesDir}/../__generated__/).
    • Plugins can now mark individual generated files as non-overwriting. The runner respects disableOverwrite so generator-authored starter files (e.g. a module's index.ts or types.ts) are written once and never clobbered on regeneration.

    Generation remains plugin-driven via generate(options, plugins, hooks) / generateAndWatch(...), with the GraphQL codegen supplied by @baeta/plugin-graphql.

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7

@baeta/generator-sdk@2.0.0-next.7

Major Changes

  • @baeta/generator-sdk provides the building blocks for authoring Baeta generator plugins (File, FileManager, Watcher, createPluginV1, options). It was reworked for v2. by @andreisergiu98 in #290

    • GeneratorOptions dropped the graphql-codegen-era fields (baseTypesPath, contextType, extensions, scalars) and added typesDir. Context/scalar/type configuration now lives in the generated src/modules/types.ts instead of generator options.
    • FileOptions header config changed from individual opt-out flags (disableEslintHeader, disableBiomeHeader) to a single opt-in enableLintHeaders, which accepts true or an object selecting eslint / oxlint / biomeV1 / biomeV2 headers.
    • New FileOptions.disableOverwrite: files written with it use an exclusive-create open and are left untouched if they already exist on disk, so generated starter files are never overwritten.
    • FileManager.createAndAdd(filename, content, tag, options?) now accepts per-file FileOptions as a fourth argument, merged over the manager's defaults.
    • GraphQL schema utilities (schema loading, AST registry walking) are no longer part of this SDK — they were extracted into the new @baeta/util-graphql package, so the SDK no longer pulls in graphql/graphql-tools for plugin authors who don't need them.
  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/plugin@2.0.0-next.5
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin@2.0.0-next.5

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

@baeta/plugin-cloudflare@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c, 046dc5c]:
    • @baeta/cache-cloudflare@2.0.0-next.16
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/subscriptions-cloudflare@0.2.0-next.4

@baeta/plugin-directives@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • @baeta/plugin-directives was updated for the v2 generator API and the new side-effect-free module model. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c, 046dc5c]:
    • @baeta/directives@2.0.0-next.16
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin-exec@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7

@baeta/plugin-federation@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • @baeta/plugin-federation is a new build-time plugin that turns a Baeta service into a valid Apollo Federation subgraph. Add federationPlugin() to your baeta.ts and the generator handles the rest of the federation boilerplate. by @andreisergiu98 in #290

    Setup

    Install the plugin as a dev dependency and the @baeta/federation runtime helpers as a regular dependency:

    yarn add -D @baeta/plugin-federation
    yarn add @baeta/federation

    Enable it in baeta.ts:

    import { defineConfig } from "@baeta/cli";
    import { federationPlugin } from "@baeta/plugin-federation";
    
    export default defineConfig({
      graphql: {
        schemas: ["src/**/*.gql"],
      },
      plugins: [
        federationPlugin({
          // Federation version to target (default: '2.9', supports 2.0–2.9)
          version: "2.9",
          // Extra directives to expose, or 'all'.
          // Default: ['@key', '@external', '@requires', '@provides', '@extends']
          include: "all",
          // Name of the generated module (default: 'baeta-federation')
          moduleName: "baeta-federation",
        }),
      ],
    });

    What it generates

    From your existing .gql schema, the plugin:

    • Makes the federation spec directives (@key, @external, @requires, @provides, @extends, …) available in your schema files, scoped to the configured version and include.
    • Generates a baeta-federation module containing the _entities / _service query resolvers, the _Service type, the _Entity union (built from every type carrying a resolvable @key), and the _Any / FieldSet scalars — using the helpers from @baeta/federation.
    • Emits the printed subgraph SDL exposed via _service { sdl }.
    • Writes typed entity-handler signatures and representation types to __generated__/federation.ts, so each @key is reflected as a strongly typed handler.

    Types referenced only as cross-subgraph stubs — declared with @key(fields: "id", resolvable: false) — are intentionally excluded from the _Entity union and from handler generation, since they are resolved by the subgraph that owns them.

    The one manual step: entity handlers

    Given a keyed entity:

    type Product @key(fields: "id") {
      id: ID!
      name: String!
      price: Float!
    }

    the generator creates a src/modules/<moduleName>/entity-handlers.ts once (and never overwrites it) where you register a handler per entity:

    import type { EntityHandlerMap } from "../../__generated__/federation.ts";
    import { handleProductEntity } from "../product/product.entity.ts";
    
    const entityHandlersMap: EntityHandlerMap = {
      Product: handleProductEntity,
    };
    
    export default entityHandlersMap;

    Each handler receives the typed federation representation (the @key fields plus __typename) and returns the resolved entity:

    import type { ProductEntityHandler } from "../../__generated__/federation.ts";
    
    export const handleProductEntity: ProductEntityHandler = async (
      representation
    ) => {
      return {
        __typename: "Product",
        id: representation.id,
        name: `Product ${representation.id}`,
        price: 9.99,
      };
    };

    The generated module schema is merged into your application like any other Baeta module, so the resulting baeta.schema is a fully federation-compatible subgraph you can serve from Apollo Server, Yoga, or any GraphQL server. See the federation-subgraph-products and federation-subgraph-users examples for complete, runnable subgraphs.

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/util-graphql@2.0.0-next.4
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin-gitignore@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • The gitignore plugin still auto-populates .gitignore with Baeta's generated files, but its options changed. ignoreTags was renamed to skipTags, and a new skipFilesGlobs option lets you exclude specific generated files by glob. Files marked disableOverwrite are now also excluded from .gitignore. by @andreisergiu98 in #290

    v1:

    gitignorePlugin({
      ignoreTags: ["cloudflare"],
    });

    v2:

    gitignorePlugin({
      skipTags: ["cloudflare"],
      skipFilesGlobs: ["src/modules/**/schema.ts"],
    });

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin-graphql@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • The whole generator was rewritten to emit the new side-effect-free builder API (the resolver API itself is documented under @baeta/core). by @andreisergiu98 in #290

    v1 (generated accessor, side-effect registration):

    import { getUserModule } from "./typedef";
    
    const { Query } = getUserModule();
    
    Query.user(async (params) => {
      return { id: params.args.id ?? "id", name: "John Doe" };
    });

    v2 (generated module builder + starter index.ts):

    import { UserModule } from "./typedef.ts";
    
    const { Query, User } = UserModule;
    
    export default UserModule.$schema({
      User: User.$fields({
        id: User.id.key("id"),
        name: User.name.resolve((params) => {
          // Implement resolver logic here
        }),
      }),
      Query: Query.$fields({
        user: Query.user.resolve((params) => {
          // Implement resolver logic here
        }),
      }),
    });

    The full set of field/middleware/builder helpers is documented in the @baeta/core changelog entry.

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/util-graphql@2.0.0-next.4
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin-pagination@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • The pagination plugin still emits the same Relay-style SDL (Connection, Edge, PageInfo), but its generated index.ts now wires up the new v2 builder-pattern resolvers instead of v1's side-effect module getter. by @andreisergiu98 in #290

    The createExport option was removed (the resolver file is always generated), and the types map is now type-checked against your schema via paginationPlugin<T>({ types }).

    Options change:

    // v1
    paginationPlugin({
      types: { User: true },
      createExport: false, // could opt out of the generated export file
    });
    
    // v2
    paginationPlugin<Types>({
      types: { User: true }, // keys are now type-checked; createExport removed
    });

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/util-path@2.0.0-next.6

@baeta/plugin-prisma@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c, 046dc5c]:
    • @baeta/generator-sdk@2.0.0-next.7
    • @baeta/plugin-exec@2.0.0-next.16
    • @baeta/util-path@2.0.0-next.6

@baeta/subscriptions-pubsub@2.0.0-next.16

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • TypedPubSub is now a concrete class you instantiate with new, the createTypedPubSub() factory has been removed, and the package targets graphql-subscriptions v3 only. Support for graphql-subscriptions v2 (and its runtime engine detection) is gone. by @andreisergiu98 in #290

    Creating the PubSub

    In v1, TypedPubSub was a type and you built the instance with the createTypedPubSub() factory, which detected the engine version (asyncIterator for v2, asyncIterableIterator for v3) at runtime. In v2, TypedPubSub is the class itself.

    v1:

    import { createTypedPubSub } from "@baeta/subscriptions-pubsub";
    import { PubSub } from "graphql-subscriptions";
    
    export const pubsub = createTypedPubSub<PubSub, PubSubMap>(new PubSub());

    v2:

    import { TypedPubSub } from "@baeta/subscriptions-pubsub";
    import { PubSub } from "graphql-subscriptions";
    
    export const pubsub = new TypedPubSub<PubSub, PubSubMap>(new PubSub());

    The optional { prefix } options object is unchanged and still passed as the second argument.

    Removed and changed exports

    • createTypedPubSub() — removed; use new TypedPubSub(...).
    • PubSubEngineV2 / PubSubEngineV3 — removed. The package now targets a single graphql-subscriptions v3 engine.
    • TypedPubSub — was a type alias in v1; is now the exported class.
    • TypedPubSubOptions — unchanged.

    Because v2 support is dropped, the asyncIterator method (the v2-only iterator) is no longer available — use asyncIterableIterator(channel) for the async iterable. publish, subscribe, and unsubscribe are unchanged.

@baeta/util-encoding@2.0.0-next.5

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

@baeta/util-env@2.0.0-next.5

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

@baeta/util-graphql@2.0.0-next.4

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • @baeta/util-graphql is a new package. It collects the GraphQL schema utilities that the v1 codegen kept buried inside @baeta/plugin-graphql into a standalone, reusable module shared by the generator and federation packages. by @andreisergiu98 in #290

Patch Changes

  • Updated dependencies [046dc5c, 046dc5c]:
    • @baeta/util-path@2.0.0-next.6

@baeta/util-log@2.0.0-next.5

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • createLogger is now a public export and accepts a log level argument ('debug' | 'info' | 'warn' | 'error', default 'info') so messages below the configured level are dropped. The exported types changed accordingly: the ConsoleLogger and ConsolePayload type exports were removed and replaced by a single Logger type. The pre-built log export is unchanged. by @andreisergiu98 in #290

    v1:

    import { log, type ConsoleLogger } from "@baeta/util-log";
    // createLogger was internal; no level filtering

    v2:

    import { log, createLogger, type Logger } from "@baeta/util-log";
    
    const logger = createLogger("warn"); // debug/info calls become no-ops

@baeta/util-path@2.0.0-next.6

Major Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. by @andreisergiu98 in #290

create-baeta@2.0.0-next.16

Major Changes

  • create baeta (still offering the yoga and apollo templates with Node/Bun/Deno runtime selection) now scaffolds a v2 project end to end. by @andreisergiu98 in #290

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

@baeta/subscriptions-cloudflare@0.2.0-next.4

Minor Changes

  • Drop support for Node.js v23 and v25. Baeta now targets the active LTS releases — Node.js ^22.20.0, ^24.0.0, or >=26.0.0. by @andreisergiu98 in #290

  • Updated for Baeta v2. The public API is unchanged, except that the Subscribe<Map> by @andreisergiu98 in #290
    helper now returns an AsyncIterable instead of an AsyncIterator, matching the v2
    subscription builder's .subscribe().