Skip to content

feat: Move mutation authorization from GraphQL-level to service-level#793

Merged
kingston merged 3 commits into
mainfrom
kingston/eng-1053-add-model-authorizers-to-service-functions
Mar 5, 2026
Merged

feat: Move mutation authorization from GraphQL-level to service-level#793
kingston merged 3 commits into
mainfrom
kingston/eng-1053-add-model-authorizers-to-service-functions

Conversation

@kingston

@kingston kingston commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Service method authorization with configurable global and instance roles
    • New Service Method Authorization UI (compact grid role picker) and derived-auth badges in the GraphQL mutations UI
    • Added blog update/delete mutations and pagination args for list queries
  • Bug Fixes

    • Disabled service methods are hidden from the GraphQL mutations section
  • Refactor

    • Authorization enforcement moved from GraphQL layer to service methods; system role removed from user-facing role pickers

@changeset-bot

changeset-bot Bot commented Mar 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 715171f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@baseplate-dev/project-builder-lib Patch
@baseplate-dev/project-builder-server Patch
@baseplate-dev/project-builder-web Patch
@baseplate-dev/fastify-generators Patch
@baseplate-dev/create-project Patch
@baseplate-dev/project-builder-cli Patch
@baseplate-dev/project-builder-common Patch
@baseplate-dev/project-builder-dev Patch
@baseplate-dev/plugin-auth Patch
@baseplate-dev/plugin-email Patch
@baseplate-dev/plugin-queue Patch
@baseplate-dev/plugin-rate-limit Patch
@baseplate-dev/plugin-storage Patch
@baseplate-dev/project-builder-test Patch
@baseplate-dev/code-morph Patch
@baseplate-dev/core-generators Patch
@baseplate-dev/react-generators Patch
@baseplate-dev/sync Patch
@baseplate-dev/tools Patch
@baseplate-dev/ui-components Patch
@baseplate-dev/utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Moves GraphQL mutation role declarations into service-method-level authorization (globalRoles, instanceRoles), updates schema, generators, compiler, UI, and example projects to generate and enforce service-level authorization instead of per-mutation GraphQL roles.

Changes

Cohort / File(s) Summary
Changeset & Migration
.changeset/service-method-auth.md, packages/project-builder-lib/src/migrations/migration-025-service-method-auth.*, packages/project-builder-lib/src/migrations/index.ts
Adds migration 025 that migrates graphql.mutations.roles into service.{create,update,delete}.globalRoles/instanceRoles; registers migration and adds unit tests.
Schema Definitions
packages/project-builder-lib/src/schema/models/models.ts, packages/project-builder-lib/src/schema/models/graphql.ts
Adds service-level globalRoles and instanceRoles arrays; removes roles from GraphQL mutation schema and updates normalization/defaulting logic.
Generator Helpers
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.ts, .../index.ts
Introduces generateAuthorizeFragment utility and re-exports it for building authorize fragments combining global/instance roles and validating presence of model authorizers.
Prisma Generators
packages/fastify-generators/.../prisma-data-create.generator.ts, prisma-data-update.generator.ts, prisma-data-delete.generator.ts
Extends descriptors to accept globalRoles/instanceRoles, wires generateAuthorizeFragment into emitted create/update/delete implementations, and conditions loadExisting for instance role handling.
Compiler / Backend
packages/project-builder-server/src/compiler/backend/services.ts, .../graphql.ts
Compiler now maps role IDs to names, injects global/instance roles into generated data service calls, and derives GraphQL mutation authorize directives from service-level roles via deriveMutationAuthorize.
Web UI — Service & GraphQL Sections
packages/project-builder-web/src/routes/data/models/.../service-method-auth-section.tsx, .../graph-ql-mutations-section.tsx, .../graph-ql-object-type-section.tsx, .../graph-ql-queries-section.tsx, .../service.tsx
Adds ServiceMethodAuthSection matrix UI for assigning global/instance roles; replaces inline mutation role pickers with derived auth badges; filters out system role from role pickers; adjusts layout and link behavior.
Admin UI
packages/project-builder-web/src/routes/packages/apps.$key/web/admin.tsx
Filters out system role from admin access role options.
Examples — Blog
examples/blog-with-auth/baseplate/project-definition.json, apps/backend/schema.graphql, .../file-id-map.json, .../blog.*
Updates project schema to v25, adds blog authorizer, adds blog update/delete service methods and mutations, and adds pagination to blogs/users queries.
Examples — Todo
examples/todo-with-better-auth/baseplate/project-definition.json, various apps/backend/src/modules/... and tests
Updates project schema to v25, adds globalRoles usage across service mutations, updates data services to require authorization, and adapts integration tests to use authenticated test contexts.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI (Service Method Auth)
  participant Project as Project Definition
  participant Migrator as Migration (025)
  participant Compiler as Project Builder Compiler
  participant Generator as Fastify Generators
  participant Service as Runtime Service
  participant DB as Database (Prisma)

  UI->>Project: user assigns global/instance roles (save)
  Project->>Migrator: migration025 applied (roles -> service methods)
  Migrator-->>Project: updated project-definition
  Project->>Compiler: compile app config
  Compiler->>Generator: request data-service code with role metadata
  Generator->>Generator: generate authorize fragment (global+instance)
  Generator-->>Compiler: emitted service code with authorize checks
  Compiler-->>Service: deploy/run service code
  Service->>DB: execute authorized operation (authorize enforced)
  DB-->>Service: result
  Service-->>UI: operation response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main architectural change: moving mutation authorization checks from GraphQL-level to service-level across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kingston/eng-1053-add-model-authorizers-to-service-functions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (10)
examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.crud.int.test.ts (1)

9-16: Prefer a least-privilege auth fixture for this test.

Using public + user + admin in one shared context can mask authorization regressions now that checks are in the service layer. Prefer the minimum role needed for this path (and separate fixtures/tests for allow/deny cases).

♻️ Suggested tightening
 const context = createTestServiceContext({
   auth: createAuthContextFromSessionInfo({
     type: 'user',
     id: 'test-session',
     userId: '00000000-0000-0000-0000-000000000001',
-    roles: ['public', 'user', 'admin'],
+    roles: ['admin'],
   }),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.crud.int.test.ts`
around lines 9 - 16, The test uses an overly-privileged auth fixture which can
hide authorization regressions; update the fixture created via
createTestServiceContext and createAuthContextFromSessionInfo to use the
least-privilege roles required by this test (e.g., remove 'admin' and/or 'user'
if only 'public' is needed, or set just 'user' if that's the minimal allowed
role), and add separate tests/fixtures that explicitly cover admin-only and
denied cases so authorization checks in the service layer remain covered.
examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.ts (2)

105-132: Add JSDoc for touched exported service functions.

createUser, updateUser, and deleteUser are exported but undocumented; please add full JSDoc (params + return) per repo standards. As per coding guidelines, "Add JSDoc comments to all exported functions, interfaces, and classes documenting the function/type, parameters, return value, and all fields".

Also applies to: 136-166, 168-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.ts`
around lines 105 - 132, Add JSDoc comments for the exported service functions
createUser, updateUser, and deleteUser (the functions using composeCreate and
commitCreate) describing the function, each parameter (e.g., the destructured
data/input, query, context and generic TQueryArgs), and the Promise return type
(GetPayload<'user', TQueryArgs> or corresponding types) following the repo
standard; place the JSDoc directly above each exported function, include `@param`
tags for data/input, query, context and any generics, and an `@returns` describing
the resolved payload and possible errors.

119-119: Extract repeated authorization literal to a shared constant.

['admin'] is duplicated three times; centralizing it will reduce drift when policy names evolve.

♻️ Proposed refactor
+const USER_ADMIN_AUTHORIZE = ['admin'] as const;
+
 export async function createUser<
   TQueryArgs extends ModelQuery<'user'> = ModelQuery<'user'>,
 >({
@@
-    authorize: ['admin'],
+    authorize: USER_ADMIN_AUTHORIZE,
   });
@@
 export async function updateUser<
   TQueryArgs extends ModelQuery<'user'> = ModelQuery<'user'>,
 >({
@@
-    authorize: ['admin'],
+    authorize: USER_ADMIN_AUTHORIZE,
   });
@@
 export async function deleteUser<
   TQueryArgs extends ModelQuery<'user'> = ModelQuery<'user'>,
 >({
@@
-    authorize: ['admin'],
+    authorize: USER_ADMIN_AUTHORIZE,
   });
 }

Also applies to: 152-152, 188-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.ts`
at line 119, Extract the repeated literal ['admin'] into a single exported
constant (e.g., ADMIN_AUTH or ADMIN_ROLE) and replace each authorize: ['admin']
occurrence in user.data-service.ts (the authorize properties currently present
on the relevant handlers/methods) with a reference to that constant; ensure the
constant is exported from a shared location (or this module if intended local)
so other files can reuse it and update the three occurrences mentioned to use
the new constant.
examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.int.test.ts (1)

36-43: Use least-privilege roles in the default test context.

Line 41 includes admin in the shared context. That can make auth tests pass even if service checks are accidentally tightened to admin-only. Prefer a baseline of ['public', 'user'], and add admin only in tests that explicitly need it.

Suggested change
   auth: createAuthContextFromSessionInfo({
     type: 'user',
     id: 'test-session',
     userId: TEST_USER_ID,
-    roles: ['public', 'user', 'admin'],
+    roles: ['public', 'user'],
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.int.test.ts`
around lines 36 - 43, The default test auth context is too privileged: in the
createTestServiceContext call that uses createAuthContextFromSessionInfo (with
id 'test-session' and userId TEST_USER_ID) remove 'admin' from the roles array
so it is ['public','user'] instead of ['public','user','admin']; keep tests that
require admin rights to explicitly add the 'admin' role in their own context
setup rather than relying on the shared default.
examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item.data-service.ts (1)

65-102: Add JSDoc for touched exported functions.

Please add JSDoc for createTodoItem, updateTodoItem, and deleteTodoItem.

As per coding guidelines "Add JSDoc comments to all exported functions, interfaces, and classes documenting the function/type, parameters, return value, and all fields".

Also applies to: 106-146, 148-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item.data-service.ts`
around lines 65 - 102, Add JSDoc comments for the exported functions
createTodoItem, updateTodoItem, and deleteTodoItem: for each function add a
short description of its purpose, `@param` tags describing each parameter
(data/input, query, context and any specific fields like assigneeId/todoListId),
and an `@returns` tag describing the Promise return type (GetPayload<'todoItem',
TQueryArgs> or appropriate generic). Place the JSDoc immediately above each
exported function (createTodoItem, updateTodoItem, deleteTodoItem) and include
notes about authorization behavior and any side effects (e.g., DB
create/update/delete) and thrown errors if applicable.
examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts (1)

41-74: Add JSDoc to touched exported service functions.

createTodoListShare, updateTodoListShare, and deleteTodoListShare are exported and currently undocumented.

As per coding guidelines "Add JSDoc comments to all exported functions, interfaces, and classes documenting the function/type, parameters, return value, and all fields".

Also applies to: 80-116, 118-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts`
around lines 41 - 74, Add JSDoc comments to the exported service functions
createTodoListShare, updateTodoListShare, and deleteTodoListShare: add a brief
description of what each function does, annotate parameters (e.g., the
DataCreateInput/DataUpdateInput/DataDeleteInput object and its properties like
data/input, query, context), document the returned Promise type
(GetPayload<'todoListShare', TQueryArgs> or appropriate generic), and note any
thrown errors or side-effects (e.g., DB mutations via
commitCreate/commitUpdate/commitDelete). Place the JSDoc immediately above each
exported function declaration and ensure it covers parameter shapes and return
value generics to satisfy the project's documentation guidelines.
examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list.data-service.ts (1)

49-81: Add JSDoc for touched exported service functions.

Please document createTodoList, updateTodoList, and deleteTodoList with JSDoc.

As per coding guidelines "Add JSDoc comments to all exported functions, interfaces, and classes documenting the function/type, parameters, return value, and all fields".

Also applies to: 85-120, 122-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list.data-service.ts`
around lines 49 - 81, Add JSDoc comments to the exported service functions
createTodoList, updateTodoList, and deleteTodoList: for each function provide a
short description, document parameters (e.g., the destructured { data/input,
query, context } or DataCreateInput<'todoList', typeof todoListInputFields,
TQueryArgs>), describe the return type (e.g., Promise<GetPayload<'todoList',
TQueryArgs>> or appropriate payload), and note any errors thrown or
authorization behavior (authorize: ['user']). Ensure parameter names match the
functions' signatures (input/data, query, context, TQueryArgs) and mention
important fields like ownerId when relevant.
examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item-service.int.test.ts (1)

12-31: Add one explicit unauthorized-user integration test in this suite.

Given this PR’s auth migration scope, this file should also assert that a different authenticated user cannot mutate another owner’s todo item/list data.

Also applies to: 210-271

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item-service.int.test.ts`
around lines 12 - 31, The suite needs an explicit integration test that verifies
an authenticated but different user cannot mutate another owner’s todo data: add
a test in this file that, after the existing beforeEach setup (which creates
owner and sets context via createTestServiceContext and
createAuthContextFromSessionInfo and sets ownerId), creates a second user via
prisma.user.create, builds a new context for that user using
createTestServiceContext/createAuthContextFromSessionInfo, then attempts a
mutation on the owner’s todo list/item (use the same service methods exercised
elsewhere in this test file, e.g., TodoItemService or the list mutation
functions) and assert it fails with an authorization error (or returns
unauthorized) rather than succeeding; ensure the test covers both list-level and
item-level mutation paths if those are present in lines ~210-271.
packages/project-builder-lib/src/migrations/migration-025-service-method-auth.unit.test.ts (1)

165-192: Add a regression test for preserving extra mutation entries.

Current tests verify preserving graphql siblings, but not unknown keys inside graphql.mutations itself (for example custom mutation configs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/project-builder-lib/src/migrations/migration-025-service-method-auth.unit.test.ts`
around lines 165 - 192, Add a regression test to
migration-025-service-method-auth.unit.test.ts that ensures
migration025ServiceMethodAuth.migrate preserves unknown keys inside
graphql.mutations (e.g., a custom mutation entry) in addition to existing checks
for siblings; create a model config with graphql.mutations containing both known
keys (create/update/delete) and an extra custom key (like customX: { enabled:
true, roles: [...] }) call migration025ServiceMethodAuth.migrate(config) and
assert that result.models?.[0].graphql?.mutations still contains the custom
mutation entry unchanged while existing mutation entries remain correct.
examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts (1)

7-8: Add explicit return types to inline functions in this TypeScript file.

getModelById and owner currently rely on inferred return types; this file path requires explicit function return types.

As per coding guidelines, "Always specify explicit return types for functions".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts`
around lines 7 - 8, Add explicit return types for the inline functions: annotate
getModelById to return Promise<Blog | null> (or the appropriate Prisma Blog
type) and annotate the owner role function to return boolean; update the
getModelById signature (the getModelById property) and the owner function inside
roles (roles.owner) accordingly and, if necessary, import the Blog type from
`@prisma/client` or your Prisma types so the annotations compile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts`:
- Line 8: The owner role lambda in blog.authorizer.ts (roles: { owner: (ctx,
model) => model.userId === ctx.auth.userId }) can throw when ctx.auth is
missing; update the owner evaluator to first guard that ctx.auth and
ctx.auth.userId exist (e.g., check ctx.auth && ctx.auth.userId or
ctx.auth?.userId) and return false if missing, then compare model.userId to the
userId only when present so authorization safely denies instead of throwing.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/schema/blog.mutations.ts`:
- Line 25: The schema-level authorize array for the updateBlog and deleteBlog
mutations is too permissive (includes 'public'); update the authorize entries in
blog.mutations.ts for the updateBlog and deleteBlog fields to match the
service-level enforcement by replacing ['public','user','system','admin'] with
the restrictive set used by the data service: ['admin',
blogAuthorizer.roles.owner] so schema and the updateBlog/deleteBlog resolve
paths align.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/services/blog.data-service.ts`:
- Around line 31-40: Add JSDoc blocks for the exported service functions by
documenting parameters, return types, and behavior for updateBlog and
deleteBlog; locate the functions updateBlog and deleteBlog in
blog.data-service.ts and add standard JSDoc comments above each exported
function describing the function purpose, each parameter (where, data/input,
query, context) including types, and the Promise/return payload
(GetPayload<'blog', TQueryArgs>), ensuring the docs follow the project guideline
format for exported functions.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts`:
- Line 57: The exported service functions createTodoListShare,
updateTodoListShare, and deleteTodoListShare currently only require
authenticated users (authorize: ['user']) but lack instance-level ownership
checks; add either a model authorizer (e.g., todoListShareAuthorizer similar to
userAuthorizer) or explicit service-level validation that the requesting user
owns the target todo list (verify ownerId on the TodoList before
creating/updating/deleting shares) and throw an authorization error if not
authorized. Also add JSDoc comments above each exported function
(createTodoListShare, updateTodoListShare, deleteTodoListShare) documenting
parameters, return values, and errors thrown. Ensure the authorization check
runs before any DB mutations and reuse existing user identity from the
request/context.

In
`@packages/project-builder-lib/src/migrations/migration-025-service-method-auth.ts`:
- Around line 88-92: The migration currently replaces graphql.mutations with
only create/update/delete and thus discards any other custom mutation entries;
update the migration logic in migration-025-service-method-auth.ts to merge into
the existing graphql.mutations object instead of overwriting it — e.g., read the
existing graphql.mutations (if any), preserve all its keys, and only
set/override the create/update/delete entries using mutations.create?.enabled ??
false etc., so non-CRUD mutation configurations remain intact.

In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/graphql/graph-ql-mutations-section.tsx:
- Around line 50-58: The current rendering branch for hasInstanceRoles in
graph-ql-mutations-section.tsx collapses mixed auth rules into a single
"Authenticated" badge and hides configured globalRoles; update the UI logic in
the component (the block guarded by hasInstanceRoles) to display both the
instance-level indicator and any globalRoles that exist (e.g., show the Badge
for Authenticated and also render the globalRoles list or badges when
globalRoles is present) so the full effective access rule set is visible; locate
and update the JSX around hasInstanceRoles, instanceRoles and globalRoles to
combine both displays instead of replacing globalRoles with a single
Authenticated label.

In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/graphql/graph-ql-queries-section.tsx:
- Around line 111-118: The Count Query SwitchFieldController
("graphql.queries.list.count.enabled") currently only respects isListEnabled and
can remain editable when the parent object type is disabled; update this
SwitchFieldController to use the same disabled condition as the parent query
toggles (i.e., mirror the object-type disabled flag they use) so it is disabled
whenever the object type is disabled — locate the parent query toggle usage to
copy the exact disabled expression and add disabled={...} to the
SwitchFieldController here.

---

Nitpick comments:
In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts`:
- Around line 7-8: Add explicit return types for the inline functions: annotate
getModelById to return Promise<Blog | null> (or the appropriate Prisma Blog
type) and annotate the owner role function to return boolean; update the
getModelById signature (the getModelById property) and the owner function inside
roles (roles.owner) accordingly and, if necessary, import the Blog type from
`@prisma/client` or your Prisma types so the annotations compile.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.crud.int.test.ts`:
- Around line 9-16: The test uses an overly-privileged auth fixture which can
hide authorization regressions; update the fixture created via
createTestServiceContext and createAuthContextFromSessionInfo to use the
least-privilege roles required by this test (e.g., remove 'admin' and/or 'user'
if only 'public' is needed, or set just 'user' if that's the minimal allowed
role), and add separate tests/fixtures that explicitly cover admin-only and
denied cases so authorization checks in the service layer remain covered.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.int.test.ts`:
- Around line 36-43: The default test auth context is too privileged: in the
createTestServiceContext call that uses createAuthContextFromSessionInfo (with
id 'test-session' and userId TEST_USER_ID) remove 'admin' from the roles array
so it is ['public','user'] instead of ['public','user','admin']; keep tests that
require admin rights to explicitly add the 'admin' role in their own context
setup rather than relying on the shared default.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.ts`:
- Around line 105-132: Add JSDoc comments for the exported service functions
createUser, updateUser, and deleteUser (the functions using composeCreate and
commitCreate) describing the function, each parameter (e.g., the destructured
data/input, query, context and generic TQueryArgs), and the Promise return type
(GetPayload<'user', TQueryArgs> or corresponding types) following the repo
standard; place the JSDoc directly above each exported function, include `@param`
tags for data/input, query, context and any generics, and an `@returns` describing
the resolved payload and possible errors.
- Line 119: Extract the repeated literal ['admin'] into a single exported
constant (e.g., ADMIN_AUTH or ADMIN_ROLE) and replace each authorize: ['admin']
occurrence in user.data-service.ts (the authorize properties currently present
on the relevant handlers/methods) with a reference to that constant; ensure the
constant is exported from a shared location (or this module if intended local)
so other files can reuse it and update the three occurrences mentioned to use
the new constant.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item-service.int.test.ts`:
- Around line 12-31: The suite needs an explicit integration test that verifies
an authenticated but different user cannot mutate another owner’s todo data: add
a test in this file that, after the existing beforeEach setup (which creates
owner and sets context via createTestServiceContext and
createAuthContextFromSessionInfo and sets ownerId), creates a second user via
prisma.user.create, builds a new context for that user using
createTestServiceContext/createAuthContextFromSessionInfo, then attempts a
mutation on the owner’s todo list/item (use the same service methods exercised
elsewhere in this test file, e.g., TodoItemService or the list mutation
functions) and assert it fails with an authorization error (or returns
unauthorized) rather than succeeding; ensure the test covers both list-level and
item-level mutation paths if those are present in lines ~210-271.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item.data-service.ts`:
- Around line 65-102: Add JSDoc comments for the exported functions
createTodoItem, updateTodoItem, and deleteTodoItem: for each function add a
short description of its purpose, `@param` tags describing each parameter
(data/input, query, context and any specific fields like assigneeId/todoListId),
and an `@returns` tag describing the Promise return type (GetPayload<'todoItem',
TQueryArgs> or appropriate generic). Place the JSDoc immediately above each
exported function (createTodoItem, updateTodoItem, deleteTodoItem) and include
notes about authorization behavior and any side effects (e.g., DB
create/update/delete) and thrown errors if applicable.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts`:
- Around line 41-74: Add JSDoc comments to the exported service functions
createTodoListShare, updateTodoListShare, and deleteTodoListShare: add a brief
description of what each function does, annotate parameters (e.g., the
DataCreateInput/DataUpdateInput/DataDeleteInput object and its properties like
data/input, query, context), document the returned Promise type
(GetPayload<'todoListShare', TQueryArgs> or appropriate generic), and note any
thrown errors or side-effects (e.g., DB mutations via
commitCreate/commitUpdate/commitDelete). Place the JSDoc immediately above each
exported function declaration and ensure it covers parameter shapes and return
value generics to satisfy the project's documentation guidelines.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list.data-service.ts`:
- Around line 49-81: Add JSDoc comments to the exported service functions
createTodoList, updateTodoList, and deleteTodoList: for each function provide a
short description, document parameters (e.g., the destructured { data/input,
query, context } or DataCreateInput<'todoList', typeof todoListInputFields,
TQueryArgs>), describe the return type (e.g., Promise<GetPayload<'todoList',
TQueryArgs>> or appropriate payload), and note any errors thrown or
authorization behavior (authorize: ['user']). Ensure parameter names match the
functions' signatures (input/data, query, context, TQueryArgs) and mention
important fields like ownerId when relevant.

In
`@packages/project-builder-lib/src/migrations/migration-025-service-method-auth.unit.test.ts`:
- Around line 165-192: Add a regression test to
migration-025-service-method-auth.unit.test.ts that ensures
migration025ServiceMethodAuth.migrate preserves unknown keys inside
graphql.mutations (e.g., a custom mutation entry) in addition to existing checks
for siblings; create a model config with graphql.mutations containing both known
keys (create/update/delete) and an extra custom key (like customX: { enabled:
true, roles: [...] }) call migration025ServiceMethodAuth.migrate(config) and
assert that result.models?.[0].graphql?.mutations still contains the custom
mutation entry unchanged while existing mutation entries remain correct.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 52b327bd-95fa-4da7-9473-e2cc928b4f62

📥 Commits

Reviewing files that changed from the base of the PR and between bb655d6 and d202a2b.

⛔ Files ignored due to path filters (9)
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/accounts/services/user.data-service.ts is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/blogs/authorizers/blog.authorizer.ts is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/blogs/index.ts is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/blogs/schema/blog.mutations.ts is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/blogs/services/blog.data-service.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-better-auth/apps/backend/baseplate/generated/src/modules/accounts/users/services/user.data-service.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-better-auth/apps/backend/baseplate/generated/src/modules/todos/services/todo-item.data-service.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-better-auth/apps/backend/baseplate/generated/src/modules/todos/services/todo-list-share.data-service.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-better-auth/apps/backend/baseplate/generated/src/modules/todos/services/todo-list.data-service.ts is excluded by !**/generated/**, !**/generated/**
📒 Files selected for processing (35)
  • .changeset/service-method-auth.md
  • examples/blog-with-auth/apps/backend/baseplate/file-id-map.json
  • examples/blog-with-auth/apps/backend/schema.graphql
  • examples/blog-with-auth/apps/backend/src/modules/accounts/services/user.data-service.ts
  • examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts
  • examples/blog-with-auth/apps/backend/src/modules/blogs/index.ts
  • examples/blog-with-auth/apps/backend/src/modules/blogs/schema/blog.mutations.ts
  • examples/blog-with-auth/apps/backend/src/modules/blogs/services/blog.data-service.ts
  • examples/blog-with-auth/baseplate/project-definition.json
  • examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.crud.int.test.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.int.test.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/services/user.data-service.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item-service.int.test.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-item.data-service.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts
  • examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list.data-service.ts
  • examples/todo-with-better-auth/baseplate/project-definition.json
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.ts
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/index.ts
  • packages/fastify-generators/src/generators/prisma/prisma-data-create/prisma-data-create.generator.ts
  • packages/fastify-generators/src/generators/prisma/prisma-data-delete/prisma-data-delete.generator.ts
  • packages/fastify-generators/src/generators/prisma/prisma-data-update/prisma-data-update.generator.ts
  • packages/project-builder-lib/src/migrations/index.ts
  • packages/project-builder-lib/src/migrations/migration-025-service-method-auth.ts
  • packages/project-builder-lib/src/migrations/migration-025-service-method-auth.unit.test.ts
  • packages/project-builder-lib/src/schema/models/graphql.ts
  • packages/project-builder-lib/src/schema/models/models.ts
  • packages/project-builder-server/src/compiler/backend/graphql.ts
  • packages/project-builder-server/src/compiler/backend/services.ts
  • packages/project-builder-web/src/routes/data/models/edit.$key/-components/graphql/graph-ql-mutations-section.tsx
  • packages/project-builder-web/src/routes/data/models/edit.$key/-components/graphql/graph-ql-object-type-section.tsx
  • packages/project-builder-web/src/routes/data/models/edit.$key/-components/graphql/graph-ql-queries-section.tsx
  • packages/project-builder-web/src/routes/data/models/edit.$key/-components/service/service-method-auth-section.tsx
  • packages/project-builder-web/src/routes/data/models/edit.$key/service.tsx
  • packages/project-builder-web/src/routes/packages/apps.$key/web/admin.tsx
💤 Files with no reviewable changes (1)
  • packages/project-builder-lib/src/schema/models/graphql.ts

model: 'blog',
idField: 'id',
getModelById: (id) => prisma.blog.findUnique({ where: { id } }),
roles: { owner: (ctx, model) => model.userId === ctx.auth.userId },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard owner-role evaluation against missing auth context.

Line 8 directly reads ctx.auth.userId; if auth context is absent, authorization can throw instead of denying access.

Proposed fix
-  roles: { owner: (ctx, model) => model.userId === ctx.auth.userId },
+  roles: {
+    owner: (ctx, model): boolean =>
+      ctx.auth?.userId != null && model.userId === ctx.auth.userId,
+  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/authorizers/blog.authorizer.ts`
at line 8, The owner role lambda in blog.authorizer.ts (roles: { owner: (ctx,
model) => model.userId === ctx.auth.userId }) can throw when ctx.auth is
missing; update the owner evaluator to first guard that ctx.auth and
ctx.auth.userId exist (e.g., check ctx.auth && ctx.auth.userId or
ctx.auth?.userId) and return false if missing, then compare model.userId to the
userId only when present so authorization safely denies instead of throwing.

data: t.input.field({ required: true, type: updateBlogDataInputType }),
},
payload: { blog: t.payload.field({ type: blogObjectType }) },
authorize: ['public', 'user', 'system', 'admin'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n examples/blog-with-auth/apps/backend/src/modules/blogs/schema/blog.mutations.ts

Repository: halfdomelabs/baseplate

Length of output: 2032


🏁 Script executed:

rg -n "authorize:" --type=ts examples/blog-with-auth/apps/backend -C 2 | head -80

Repository: halfdomelabs/baseplate

Length of output: 7456


🏁 Script executed:

# Check if there are query/subscription files to compare authorization patterns
fd -e ts examples/blog-with-auth/apps/backend/src/modules --name "*.queries.ts" --o --name "*.subscriptions.ts" | xargs -I {} sh -c 'echo "=== {} ===" && rg -n "authorize:" {} -C 1'

Repository: halfdomelabs/baseplate

Length of output: 316


Remove 'public' from the authorize array.

The mutations declare ['public', 'user', 'system', 'admin'], but the underlying service methods (updateBlog and deleteBlog in blog.data-service.ts) enforce ['admin', blogAuthorizer.roles.owner]. This inconsistency means the schema-level authorization is misleading. Change authorize to match the service layer: ['admin', blogAuthorizer.roles.owner] for both updateBlog and deleteBlog mutations (or use the restrictive authorization from the service methods that are actually called in the resolve functions).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/schema/blog.mutations.ts`
at line 25, The schema-level authorize array for the updateBlog and deleteBlog
mutations is too permissive (includes 'public'); update the authorize entries in
blog.mutations.ts for the updateBlog and deleteBlog fields to match the
service-level enforcement by replacing ['public','user','system','admin'] with
the restrictive set used by the data service: ['admin',
blogAuthorizer.roles.owner] so schema and the updateBlog/deleteBlog resolve
paths align.

Comment on lines +31 to +40
export async function updateBlog<
TQueryArgs extends ModelQuery<'blog'> = ModelQuery<'blog'>,
>({
where,
data: input,
query,
context,
}: DataUpdateInput<'blog', typeof blogInputFields, TQueryArgs>): Promise<
GetPayload<'blog', TQueryArgs>
> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add JSDoc blocks for exported service functions.

updateBlog and deleteBlog are exported but currently undocumented.

📝 Suggested patch
+/**
+ * Updates a blog by unique filter and applies service-level authorization.
+ *
+ * `@param` where - Unique blog selector.
+ * `@param` data - Partial update payload validated by service input schema.
+ * `@param` query - Prisma selection/include query args.
+ * `@param` context - Request/service execution context.
+ * `@returns` Updated blog payload matching `TQueryArgs`.
+ */
 export async function updateBlog<
   TQueryArgs extends ModelQuery<'blog'> = ModelQuery<'blog'>,
 >({
@@
+/**
+ * Deletes a blog by unique filter and applies service-level authorization.
+ *
+ * `@param` where - Unique blog selector.
+ * `@param` query - Prisma selection/include query args.
+ * `@param` context - Request/service execution context.
+ * `@returns` Deleted blog payload matching `TQueryArgs`.
+ */
 export async function deleteBlog<
   TQueryArgs extends ModelQuery<'blog'> = ModelQuery<'blog'>,
 >({

As per coding guidelines Add JSDocs to all exported functions, interfaces, and classes with documentation of parameters, return values, and all fields.

Also applies to: 63-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/blog-with-auth/apps/backend/src/modules/blogs/services/blog.data-service.ts`
around lines 31 - 40, Add JSDoc blocks for the exported service functions by
documenting parameters, return types, and behavior for updateBlog and
deleteBlog; locate the functions updateBlog and deleteBlog in
blog.data-service.ts and add standard JSDoc comments above each exported
function describing the function purpose, each parameter (where, data/input,
query, context) including types, and the Promise/return payload
(GetPayload<'blog', TQueryArgs>), ensuring the docs follow the project guideline
format for exported functions.

fields: todoListShareInputFields,
input,
context,
authorize: ['user'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for authorization patterns and tests around todo-list-share operations
fd "todo-list-share" examples/todo-with-better-auth

Repository: halfdomelabs/baseplate

Length of output: 939


🏁 Script executed:

# Check the file structure and exported functions
head -150 examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts | tail -120

Repository: halfdomelabs/baseplate

Length of output: 3195


🏁 Script executed:

# Search for authorizer configurations and instance-level checks
rg -n -C5 "todoListShare|Authorizer|instanceRoles|owner" examples/todo-with-better-auth/apps/backend/src --max-count=30

Repository: halfdomelabs/baseplate

Length of output: 50380


🏁 Script executed:

# Look for tests that verify authorization enforcement
rg -n "todoListShare|forbidden|unauthorized|403" examples/todo-with-better-auth --glob "*test*" -C3

Repository: halfdomelabs/baseplate

Length of output: 48


🏁 Script executed:

# Search for todoListShare authorizer configuration
rg -n "todoListShare.*Authorizer|createModelAuthorizer.*todoListShare" examples/todo-with-better-auth --max-count=20

Repository: halfdomelabs/baseplate

Length of output: 48


🏁 Script executed:

# Check the composeCreate/composeUpdate/composeDelete implementations for authorization enforcement
rg -n "export.*function.*compose(Create|Update|Delete)" examples/todo-with-better-auth/apps/backend/src --max-count=10 -A 5

Repository: halfdomelabs/baseplate

Length of output: 1664


🏁 Script executed:

# Search for JSDoc comments in the data-service file
head -75 examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts

Repository: halfdomelabs/baseplate

Length of output: 2024


🏁 Script executed:

# Check the composeCreate implementation to understand authorization enforcement
sed -n '27,70p' examples/todo-with-better-auth/apps/backend/src/utils/data-operations/compose-operations.ts

Repository: halfdomelabs/baseplate

Length of output: 1416


🏁 Script executed:

# Search for any authorizer definitions in the entire codebase
fd "authorizer" examples/todo-with-better-auth/apps/backend/src --type f

Repository: halfdomelabs/baseplate

Length of output: 236


🏁 Script executed:

# Look for model-specific authorizer setups
rg "createModelAuthorizer|ModelAuthorizer" examples/todo-with-better-auth/apps/backend/src/modules --max-count=20

Repository: halfdomelabs/baseplate

Length of output: 390


🏁 Script executed:

# Check the user.authorizer to understand the pattern
cat examples/todo-with-better-auth/apps/backend/src/modules/accounts/users/authorizers/user.authorizer.ts

Repository: halfdomelabs/baseplate

Length of output: 408


🏁 Script executed:

# Search specifically for todoListShare authorizer or instance checks
rg "todoListShare" examples/todo-with-better-auth/apps/backend/src/modules --max-count=50

Repository: halfdomelabs/baseplate

Length of output: 8029


🏁 Script executed:

# Check if there's a separate authorization layer in GraphQL mutations
rg -B5 -A10 "createTodoListShare|updateTodoListShare|deleteTodoListShare" examples/todo-with-better-auth/apps/backend/src/modules/todos/schema/todo-list-share.mutations.ts | head -100

Repository: halfdomelabs/baseplate

Length of output: 2908


Add row-level authorization checks and JSDoc documentation to exported functions.

The authorize: ['user'] setting enforces authenticated-user access only; it does not verify ownership of the todo list being shared. Without instance-level authorization checks (e.g., a todoListShareAuthorizer pattern similar to userAuthorizer), any authenticated user can mutate shares for lists they don't own. Implement instance-level authorization via model authorizer or service-level ownership validation.

Additionally, per coding guidelines, all three exported functions (createTodoListShare, updateTodoListShare, deleteTodoListShare) require JSDoc comments documenting the function, parameters, and return value.

Also applies to: 98-99, 138-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@examples/todo-with-better-auth/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts`
at line 57, The exported service functions createTodoListShare,
updateTodoListShare, and deleteTodoListShare currently only require
authenticated users (authorize: ['user']) but lack instance-level ownership
checks; add either a model authorizer (e.g., todoListShareAuthorizer similar to
userAuthorizer) or explicit service-level validation that the requesting user
owns the target todo list (verify ownerId on the TodoList before
creating/updating/deleting shares) and throw an authorization error if not
authorized. Also add JSDoc comments above each exported function
(createTodoListShare, updateTodoListShare, deleteTodoListShare) documenting
parameters, return values, and errors thrown. Ensure the authorization check
runs before any DB mutations and reuse existing user identity from the
request/context.

Comment on lines +50 to +58
if (hasInstanceRoles) {
return (
<div className="ml-10 flex items-center gap-2">
<Badge variant="secondary">Authenticated</Badge>
<span className="text-xs text-muted-foreground">
Service-level authorization applies
</span>
</div>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t collapse mixed auth rules into only Authenticated.

When instanceRoles exists, this branch hides any configured globalRoles (e.g. admin), so the UI no longer reflects the full effective access rule set.

💡 Suggested patch
   if (hasInstanceRoles) {
     return (
       <div className="ml-10 flex items-center gap-2">
-        <Badge variant="secondary">Authenticated</Badge>
+        {hasGlobalRoles &&
+          globalRoles.map((roleId) => (
+            <Badge key={roleId} variant="secondary">
+              {roleMap.get(roleId) ?? roleId}
+            </Badge>
+          ))}
+        <Badge variant="secondary">Instance-authorized</Badge>
         <span className="text-xs text-muted-foreground">
           Service-level authorization applies
         </span>
       </div>
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/graphql/graph-ql-mutations-section.tsx
around lines 50 - 58, The current rendering branch for hasInstanceRoles in
graph-ql-mutations-section.tsx collapses mixed auth rules into a single
"Authenticated" badge and hides configured globalRoles; update the UI logic in
the component (the block guarded by hasInstanceRoles) to display both the
instance-level indicator and any globalRoles that exist (e.g., show the Badge
for Authenticated and also render the globalRoles list or badges when
globalRoles is present) so the full effective access rule set is visible; locate
and update the JSX around hasInstanceRoles, instanceRoles and globalRoles to
combine both displays instead of replacing globalRoles with a single
Authenticated label.

@kingston kingston merged commit 6e2675d into main Mar 5, 2026
12 of 13 checks passed
@kingston kingston deleted the kingston/eng-1053-add-model-authorizers-to-service-functions branch March 5, 2026 10:32
@github-actions github-actions Bot mentioned this pull request Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant