Skip to content

feat: Generate model authorizers from UI#753

Merged
kingston merged 19 commits into
mainfrom
kingston/eng-956-add-authorizer-configuration-to-project-builder-and-wire
Feb 10, 2026
Merged

feat: Generate model authorizers from UI#753
kingston merged 19 commits into
mainfrom
kingston/eng-956-add-authorizer-configuration-to-project-builder-and-wire

Conversation

@kingston

@kingston kingston commented Feb 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Model-level authorization with role-based, expression-driven rules.
    • Automatic generation of per-model authorizers from configured roles.
    • Authorization tab in the model editor with role list, add/edit role dialog, and delete confirmation.
    • Rich expression editor: autocomplete, syntax validation/linting, and real-time diagnostics for role expressions.

@vercel

vercel Bot commented Feb 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
baseplate-project-builder-web Ready Ready Preview, Comment Feb 9, 2026 6:29pm

Request Review

@changeset-bot

changeset-bot Bot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 253b0cb

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

This PR includes changesets to release 20 packages
Name Type
@baseplate-dev/fastify-generators Patch
@baseplate-dev/project-builder-server 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/create-project Patch
@baseplate-dev/project-builder-cli Patch
@baseplate-dev/project-builder-common Patch
@baseplate-dev/project-builder-test Patch
@baseplate-dev/project-builder-web Patch
@baseplate-dev/code-morph Patch
@baseplate-dev/core-generators Patch
@baseplate-dev/project-builder-lib 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 Feb 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds model-level authorizers: expression DSL parsing/AST, validation and Zod schema integration, propagation of expression markers through reference collection, Prisma generator to emit per-model authorizer files, backend compiler wiring to generate generator descriptors, and web UI (editor, autocomplete, linter, dialogs) plus CodeMirror-based code editor component.

Changes

Cohort / File(s) Summary
Reference & Expression Core
packages/project-builder-lib/src/references/...
expression-types.ts, markers.ts, collect-refs.ts, extract-definition-refs.ts, extend-parser-context-with-refs.ts, strip-ref-markers.ts, index.ts, expression-stub-parser.test-helper.ts
Introduce DefinitionExpression marker/annotation and RefExpressionParser API; add withExpression wrapper; propagate expressions through collect/extract/strip pipelines; add test helpers and export expression types.
Authorizer AST & Parser
packages/project-builder-lib/src/schema/models/authorizer/...
authorizer-expression-ast.ts, authorizer-expression-acorn-parser.ts, authorizer-expression-acorn-parser.unit.test.ts, authorizer-expression-parser.ts, index.ts, types.ts
Add authorizer expression AST, Acorn-based parser producing AST + dependency info and parse errors, parser wrapper and comprehensive unit tests.
Authorizer Validation & Schema
packages/project-builder-lib/src/schema/models/authorizer/...
authorizer-expression-validator.ts, authorizer-expression-validator.unit.test.ts, authorizer.ts, types.ts
Add AST validation against model context, role extraction helpers, Zod schemas for authorizer roles and model authorizer config, and model-authorizer-role entity type.
Model Schema Rework
packages/project-builder-lib/src/schema/models/models.ts, packages/project-builder-lib/src/schema/models/index.ts
Reintroduce model schema builders in new models.ts, integrate authorizer sub-schema, and change index to re-export consolidated model-related modules.
Expression Integration in Parser Context
packages/project-builder-lib/src/references/types.ts, resolve-zod-ref-payload-names.ts, types.ts, schema/creator/types.ts
Extend collected/resolved payload and schema parser context to include expressions and expose withExpression in parser context types.
Prisma Model Authorizer Generator
packages/fastify-generators/src/generators/prisma/prisma-model-authorizer/...
prisma-model-authorizer.generator.ts, index.ts, prisma/index.ts
Add prisma/prisma-model-authorizer generator and provider interface that validates descriptors and emits per-model {module}/authorizers/{model}.authorizer.ts; re-export from Prisma barrel.
Backend Compiler Integration
packages/project-builder-server/src/compiler/backend/...
authorizers.ts, authorizers.unit.test.ts, feature.ts
Add codegen to convert authorizer AST to runtime TS code, validate model PK presence, and wire per-feature prisma model authorizer generator descriptors into build output.
Web UI: Model Authorization Editor
packages/project-builder-web/src/routes/data/models/edit.$key/...
authorization.tsx, route.tsx, route-tree.gen.ts, -components/authorizer/*
Add Authorization route, tab and page; provide CodeMirror-based expression editor with autocomplete and linter, role form/dialog, and roles-management UI components.
Code Editor Component & UI
packages/ui-components/src/components/ui/code-editor-field/*, packages/ui-components/src/base-styles.css, packages/ui-components/src/utilities.css, packages/ui-components/package.json, packages/ui-components/src/components/ui/index.ts
Introduce CodeEditorField (CodeMirror) with RHF integration, stories, CSS z-index/ligature utilities, and package dependency updates.
Examples & File Maps
examples/*
blog-with-auth, todo-with-auth0 (project-definition.json, file-id-map.json, generated user.authorizer.ts, settings.local.json)`
Add authorizer configuration to example project definitions, add generated user.authorizer.ts files and file-id-map entries, and remove a local permissions block.
Tooling & Manifests
packages/project-builder-lib/package.json, packages/project-builder-web/package.json, root package.json, .changeset/..., AGENTS.md
Add acorn and CodeMirror deps, update scripts (remove precheck, add check), update AGENTS.md guidance and add changeset for the model-authorizer generator.

Sequence Diagram(s)

sequenceDiagram
  participant UI as "Web UI (editor/form)"
  participant Builder as "Project Builder Lib"
  participant Compiler as "Server Compiler"
  participant Generator as "Prisma Generator"
  participant Output as "Emitted authorizer file"

  UI->>Builder: User edits model.authorizer.roles (withExpression marker)
  Builder->>Builder: collect/extract-definition-refs → include expressions + resolved slots
  Compiler->>Compiler: buildAuthorizersForFeature(parse expressions, validate PK)
  Compiler->>Generator: instantiate prismaModelAuthorizerGenerator per model with roles
  Generator->>Generator: parse AST → generate TS role functions
  Generator->>Output: emit src/.../authorizers/{model}.authorizer.ts
  Output-->>UI: file-id-map updated / artifact available
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Generate model authorizers from UI' accurately describes the main objective of the PR, which is adding functionality to generate model authorizers from the Project Builder UI.
Linked Issues check ✅ Passed The PR implements all major coding requirements from ENG-956: authorizer configuration schemas, expression parsing/validation, model authorizer generation, UI components for role management, and wiring into the compilation pipeline.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing model authorizer functionality. Minor updates to documentation (AGENTS.md) and generated files align with the feature's scope.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kingston/eng-956-add-authorizer-configuration-to-project-builder-and-wire

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
packages/fastify-generators/src/generators/prisma/prisma-model-authorizer/prisma-model-authorizer.generator.ts (1)

144-165: Consider caching validRoles outside the getRoleFragment closure.

validRoles is recomputed from roles.map(...) on every call to getRoleFragment. Since roles is immutable (from the descriptor), this set could be computed once.

♻️ Suggested refactor
+            const validRoleNames = new Set(roles.map((r) => r.name));
+
             return {
               prismaModelAuthorizer: {
                 getAuthorizerName() {
                   return authorizerName;
                 },
                 getAuthorizerFragment() {
                   return TsCodeUtils.importFragment(
                     authorizerName,
                     authorizerPath,
                   );
                 },
                 getRoleFragment(roleName: string) {
-                  const validRoles = roles.map((r) => r.name);
-                  if (!validRoles.includes(roleName)) {
+                  if (!validRoleNames.has(roleName)) {
                     throw new Error(
-                      `Role '${roleName}' not found on ${modelName} authorizer. Available: ${validRoles.join(', ')}`,
+                      `Role '${roleName}' not found on ${modelName} authorizer. Available: ${[...validRoleNames].join(', ')}`,
                     );
                   }
                   return tsTemplate`${TsCodeUtils.importFragment(authorizerName, authorizerPath)}.roles.${roleName}`;
                 },
               },
             };

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.

@socket-security

socket-security Bot commented Feb 8, 2026

Copy link
Copy Markdown

@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: 12

🤖 Fix all issues with AI agents
In
`@packages/fastify-generators/src/generators/prisma/prisma-model-authorizer/prisma-model-authorizer.generator.ts`:
- Around line 125-132: The getModelById currently hardcodes the where clause key
as id in the generated fragment; update the generation for the getModelById
callback in the tsTemplate (the block creating export const ${authorizerName}
via prismaAuthorizerUtilsImports.createModelAuthorizer.fragment()) so it uses
the descriptor's idFieldName dynamically for the findUnique where clause (i.e.
construct the where object with the actual primary key name from idFieldName
instead of the literal id), referencing prismaModelFragment and idFieldName to
build the correct lookup.

In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-acorn-parser.ts`:
- Around line 308-380: The example in the authorizer docs conflicts with the
parser behavior in convertFieldRef: the parser only accepts bare userId for
implicit auth context (and rejects auth.userId/member access), so update the
`@example` in authorizer.ts to use userId (e.g., change "model.id === auth.userId"
to "model.id === userId") so docs match the convertFieldRef behavior and avoid
suggesting unsupported syntax.

In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts`:
- Around line 90-101: When parseResult is falsy and
parseAuthorizerExpression(value) throws an AuthorizerExpressionParseError, the
generated warning currently only sets message and discards positional info;
update the catch block that checks error instanceof
AuthorizerExpressionParseError to push a warning object that includes
error.startPosition and error.endPosition (e.g. warnings.push({ message:
error.message, startPosition: error.startPosition, endPosition:
error.endPosition })), ensuring the warning shape matches consumers and retains
inline highlighting info for the UI.

In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-validator.ts`:
- Around line 48-55: Update the JSDoc for the authorizer expression validator to
accurately state that no errors are thrown and all issues are returned as
warnings: edit the comment above the function (referencing the parameters ast,
modelContext, container and the validator function in
authorizer-expression-validator.ts) to change the `@returns` description to
something like "Array of warnings (validation issues are returned as warnings;
this function does not throw)". Ensure the summary and `@returns` lines
consistently reflect that validation problems are returned rather than thrown.

In `@packages/project-builder-lib/src/schema/models/authorizer/authorizer.ts`:
- Around line 32-44: The JSDoc examples for the authorizer expression are
invalid because the parser expects auth context as bare identifiers and only
allows model.* member access; update the examples around the expression:
ctx.withExpression(authorizerExpressionParser, { model: modelSlot }) to use the
supported forms — e.g. replace `model.id === auth.userId` with `model.id ===
userId`, replace `auth.hasRole("admin")` with `hasRole('admin')`, and
remove/avoid examples that reference `auth.*` beyond the implicit userId (e.g.
do not use `model.organizationId === auth.organizationId`).

In `@packages/project-builder-lib/src/schema/models/models.ts`:
- Around line 93-104: The transform that clears options.enumRef when value.type
!== 'enum' should also remove options.defaultEnumValueRef to avoid leaving an
orphaned default; update the anonymous transform in models.ts to, when
value.type !== 'enum' and value.options.enumRef is present, return a copy of
value with options where both enumRef and defaultEnumValueRef are set to
undefined (or deleted) so defaultEnumValueRef cannot remain when enumRef is
cleared.
- Around line 78-92: The superRefine block validating default values currently
only checks booleans; extend it to validate numeric defaults for fields where
arg.type is 'int', 'float', or 'decimal' by reading arg.options.default and
rejecting non-numeric strings: for 'int' require an integer format (e.g.,
optional leading sign and digits), for 'float' allow a valid floating-point
format (digits with optional decimal point and exponent), and for 'decimal'
enforce a decimal/hex format or at least a valid numeric string per your decimal
handling; when invalid call ctx.addIssue with path ['options','default'], code
'custom', and an appropriate message. Locate this logic in the existing
superRefine that references arg, ctx, arg.options.default, and ctx.addIssue and
add the conditional branches for arg.type 'int'|'float'|'decimal' performing
regex/parse checks and adding issues on failure.

In `@packages/project-builder-server/src/compiler/backend/authorizers.ts`:
- Around line 34-39: The generated TypeScript injects raw role strings into
single-quoted literals causing syntax errors for roles with quotes; update the
cases for hasRole and hasSomeRole so that node.role and each element of
node.roles are safely serialized (e.g., use JSON.stringify(role) or an escape
function) before interpolation into the returned expressions
ctx.auth.hasRole(...) and ctx.auth.hasSomeRole(...), ensuring both the
single-value and array branches produce valid quoted string literals even when
role names contain quotes or special characters.

In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/authorizer/model-authorizer-role-form.tsx:
- Around line 54-79: The superRefine block on roleSchema currently adds two
issues for parse errors because the generic ctx.addIssue is called
unconditionally; update the catch in roleSchema.superRefine so that when error
is an instance of AuthorizerExpressionParseError you only call ctx.addIssue once
with the error.message (and path ['expression']), and for any other error you
add a single generic ctx.addIssue with message 'Invalid expression syntax';
locate this logic around roleSchema.superRefine / parseAuthorizerExpression and
remove the unconditional second ctx.addIssue so duplicate issues are not
emitted.

In
`@packages/ui-components/src/components/ui/code-editor-field/code-editor-field.tsx`:
- Around line 56-61: The language prop accepts 'json' but languageExtension (the
useMemo in code-editor-field.tsx) doesn't handle it; import the JSON extension
from '@codemirror/lang-json' and add a branch in the useMemo to return json()
when language === 'json' (keeping the same return shape as for
javascript/typescript); update imports to include json and ensure Storybook
argTypes remain valid or remove 'json' from allowed options if you prefer not to
add the dependency.
- Around line 88-93: The inline style for the border is using hsl(var(--border))
which is invalid because --border contains an oklch() color; change the style in
CodeEditorField (code-editor-field.tsx) to use border: '1px solid var(--border)'
(or equivalent) so the raw CSS variable is used; additionally, ensure the editor
loads a JSON language extension when the language prop supports 'json' (the
current language handling in the block around the language check/extension
loading that covers javascript/typescript must be extended to import/activate
the JSON extension) and export the CodeEditorFieldControllerProps interface from
the module so it is available to consumers.

In `@packages/ui-components/src/utilities.css`:
- Around line 100-104: The custom `@utility` font-mono currently overrides the
built-in font-mono but only sets font-variant-ligatures, which drops the
monospace font-family; update the custom utility to also include the font-family
(e.g., font-family: var(--font-mono)) so it fully preserves the original
font-mono behavior, or instead rename the utility (e.g., font-mono-no-ligatures)
and keep your ligature rule there to avoid shadowing theme.css's
var(--font-mono).
🧹 Nitpick comments (20)
packages/ui-components/package.json (1)

68-69: Version pinning is inconsistent with the rest of the file.

All other non-catalog dependencies in this file use exact versions (e.g., "5.2.5", "0.7.1"), but the new CodeMirror packages use caret ranges (^6.2.4, ^6.39.0, ^4.25.4). Consider pinning these to exact versions for consistency and reproducible builds.

Proposed fix
-    "@codemirror/lang-javascript": "^6.2.4",
-    "@codemirror/view": "^6.39.0",
+    "@codemirror/lang-javascript": "6.2.4",
+    "@codemirror/view": "6.39.0",
...
-    "@uiw/react-codemirror": "^4.25.4",
+    "@uiw/react-codemirror": "4.25.4",

Also applies to: 75-75

examples/blog-with-auth/apps/backend/src/modules/accounts/authorizers/user.authorizer.ts (1)

4-9: Missing JSDoc on exported userAuthorizer constant.

The coding guidelines for examples/blog-with-auth/**/*.{ts,tsx} require JSDocs on all exported functions, interfaces, and classes. Since this is generated code, the fix should be in the generator template (prisma-model-authorizer.generator.ts) rather than here.

📝 Example JSDoc
+/** Model authorizer for the user model with owner-based access control. */
 export const userAuthorizer = createModelAuthorizer({

As per coding guidelines, examples/blog-with-auth/**/*.{ts,tsx}: "Add JSDocs to all exported functions, interfaces, and classes with documentation of parameters, return values, and all fields."

examples/todo-with-auth0/apps/backend/src/modules/accounts/users/authorizers/user.authorizer.ts (1)

4-9: Missing JSDoc on exported userAuthorizer constant.

Same as the blog-with-auth counterpart — the generator template should emit a JSDoc comment for the exported constant. As per coding guidelines, examples/todo-with-auth0/**/*.{ts,tsx}: "Add JSDocs to all exported functions, interfaces, and classes with documentation of the function, its parameters, return value, and all fields."

packages/ui-components/src/components/ui/code-editor-field/code-editor-field.tsx (2)

103-110: CodeEditorFieldControllerProps interface should be exported.

Per coding guidelines, all interfaces and types should be exported. This enables consumers to type-annotate variables referencing this controller's props.

♻️ Proposed fix
-interface CodeEditorFieldControllerProps<
+export interface CodeEditorFieldControllerProps<

As per coding guidelines: "If a particular interface or type is not exported, change the file so it is exported."


46-48: Default extensions = [] creates a new array reference each render, defeating useMemo.

Every render without an explicit extensions prop allocates a fresh [], changing the reference and causing allExtensions to recompute. A stable empty-array constant avoids this.

♻️ Proposed fix
+const EMPTY_EXTENSIONS: Extension[] = [];
+
 function CodeEditorField({
   ...
-  extensions = [],
+  extensions = EMPTY_EXTENSIONS,
   ...

Also applies to: 65-72

packages/project-builder-web/src/routes/data/models/edit.$key/-components/authorizer/authorizer-expression-autocomplete.ts (2)

199-203: Replace magic number 6 with "model.".length for clarity.

The hardcoded offset 6 on Line 201 corresponds to the length of "model.". Using the expression directly makes the intent self-documenting and reduces breakage risk if the prefix changes.

♻️ Suggested change
-      return {
-        from: word.from + 6, // Length of "model."
-        options: modelFieldCompletions,
-      };
+      return {
+        from: word.from + 'model.'.length,
+        options: modelFieldCompletions,
+      };

176-192: Redundant fallback block duplicates the !pathResult branch.

Lines 178–192 duplicate the logic from lines 160–174. The comment acknowledges this "should not reach here." Consider removing this dead-code path or consolidating both into a shared helper to reduce duplication.

♻️ Suggested simplification
     const { path } = pathResult;
 
-    if (path.length === 0) {
-      // Should not reach here due to check above, but keep for safety
-      const word = context.matchBefore(/\w+/);
-      if (!word) return null;
-
-      // Don't show top-level completions inside string literals
-      if (isInsideStringLiteral(context)) {
-        return null;
-      }
-
-      return {
-        from: word.from,
-        options: topLevelCompletions,
-      };
-    }
+    if (path.length === 0) {
+      // completionPath returned an empty path — treat as top-level
+      return null;
+    }
packages/project-builder-lib/src/references/resolve-zod-ref-payload-names.unit.test.ts (1)

14-14: Consider adding a test verifying non-empty expressions are preserved in output.

All existing tests use expressions: []. A dedicated test asserting that populated expressions are passed through to the resolved output would strengthen coverage of this new field.

packages/project-builder-web/src/routes/data/models/edit.$key/-components/authorizer/model-authorizer-role-dialog.tsx (1)

16-23: Export the ModelAuthorizerRoleDialogProps interface.

Per coding guidelines, all interfaces and types should be exported.

Proposed fix
-interface ModelAuthorizerRoleDialogProps {
+export interface ModelAuthorizerRoleDialogProps {
packages/project-builder-web/src/routes/data/models/edit.$key/-components/authorizer/model-authorizer-roles-section.tsx (1)

26-29: Export the Props interface.

Per coding guidelines, all interfaces and types should be exported.

Proposed fix
-interface Props {
+export interface Props {
   className?: string;
   formProps: UseFormReturn<ModelConfigInput>;
 }

As per coding guidelines, "If a particular interface or type is not exported, change the file so it is exported".

packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts (2)

132-153: Stub methods with TODOs — consider tracking.

getDependencies and updateForRename are no-ops. This means field renames won't update authorizer expressions, which could silently break them.

Would you like me to open an issue to track implementing rename support for authorizer expressions?


158-198: getModelContext relies on fragile path navigation and unsafe casting.

The method navigates the raw definition object using the resolved slot path with as Record<string | number, unknown> casts and then casts the result to a specific shape. This is brittle — if the definition structure changes, it will silently return undefined instead of surfacing an error.

That said, the defensive checks are in place and this mirrors how the broader system works, so it's acceptable for now.

packages/project-builder-web/src/routes/data/models/edit.$key/-components/authorizer/model-authorizer-role-form.tsx (2)

38-43: ModelAuthorizerRoleFormProps should be exported.

As per coding guidelines, interfaces and types should be exported.

♻️ Proposed fix
-interface ModelAuthorizerRoleFormProps {
+export interface ModelAuthorizerRoleFormProps {

105-110: Prefer ?? over || for nullish coalescing.

Line 108 uses || which would treat an empty string "" as falsy. If the intent is to only replace undefined/null, use ??. If the || is intentional (to also handle empty string), consider adding a comment to explain the intent, as the ESLint rule enforces ??. As per coding guidelines, "Prefer using nullish coalescing operator (??) instead of logical or (||), enforced via ESLint rule".

packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-validator.ts (2)

73-116: Consider adding exhaustive switch handling for future-proofing.

The walk function handles all current AuthorizerExpressionNode types but has no default case. If a new node type is added to the union, this will silently skip validation for it. A default: never assertion pattern would catch this at compile time.

♻️ Suggested addition
       case 'binaryLogical': {
         walk(node.left);
         walk(node.right);
         break;
       }
+
+      default: {
+        const _exhaustive: never = node;
+        throw new Error(`Unhandled node type: ${(_exhaustive as AuthorizerExpressionNode).type}`);
+      }
     }

30-33: VALID_AUTH_FIELDS is not exported — consider exporting for consumers and tests.

Tests and autocomplete/linting consumers may need to reference the valid auth field names. Exporting this constant would improve reusability. As per coding guidelines, interfaces and types should be exported; the same principle benefits this closely related constant.

packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-ast.ts (1)

83-91: Parallel arrays for role positions — consider a structured alternative.

roles, rolesStart, and rolesEnd as parallel arrays work but require callers to keep indices in sync. An array-of-structs approach (e.g., roles: Array<{ name: string; start: number; end: number }>) would be more self-documenting and harder to misuse. That said, the current approach is fine for this use case.

packages/project-builder-lib/src/references/expression-types.ts (1)

40-54: Nit: Record<string, never> default produces a loose mapped type for "no slots" case.

When T defaults to Record<string, never>, keyof T is string, so ExpressionSlotMap / ResolvedExpressionSlots become Record<string, RefContextSlot<never>> / Record<string, ReferencePath> rather than empty objects. Using Record<never, DefinitionEntityType> as the default would produce an empty mapped type ({}), which is more precise for the "no required slots" case:

 export type ExpressionSlotMap<
-  T extends Record<string, DefinitionEntityType> = Record<string, never>,
+  T extends Record<string, DefinitionEntityType> = Record<never, DefinitionEntityType>,
 > = {

This is cosmetic — the current code works correctly at runtime.

packages/fastify-generators/src/generators/prisma/prisma-model-authorizer/prisma-model-authorizer.generator.ts (2)

24-35: Consider exporting inferred types from the schemas.

Per coding guidelines, types should be exported. Exporting the inferred descriptor type (e.g., export type PrismaModelAuthorizerDescriptor = z.infer<typeof descriptorSchema>) would make it easier for callers to construct descriptors in a type-safe way without coupling to the zod schema directly.


153-159: validRoles is recomputed on every getRoleFragment call.

This is a minor efficiency nit — roles.map(r => r.name) could be computed once outside the closure. Given this is unlikely to be in a hot path, it's negligible, but worth noting.

Comment on lines +308 to +380
function convertFieldRef(node: Expression): FieldRefNode {
// Handle standalone identifier (implicit auth context)
if (node.type === 'Identifier') {
const identifier = node;
const { name } = identifier;

// Only userId is valid as a standalone identifier (implicit auth context)
if (name === 'userId') {
return {
type: 'fieldRef',
source: 'auth',
field: 'userId',
start: node.start,
end: node.end,
};
}

throw new AuthorizerExpressionParseError(
`Unknown identifier '${name}'. Did you mean 'model.${name}' or use 'userId' for auth context?`,
node,
);
}

if (node.type !== 'MemberExpression') {
throw new AuthorizerExpressionParseError(
'Expected field reference (model.field or userId)',
node,
);
}

const memberExpr = node;

// Object must be an identifier
if (memberExpr.object.type !== 'Identifier') {
throw new AuthorizerExpressionParseError(
'Field reference must start with model',
memberExpr.object,
);
}

const source = memberExpr.object.name;
if (source !== 'model') {
throw new AuthorizerExpressionParseError(
`Field reference must start with 'model', not '${source}'`,
memberExpr.object,
);
}

// Property must be a simple identifier (not computed)
if (memberExpr.computed) {
throw new AuthorizerExpressionParseError(
'Computed property access (e.g., model["field"]) is not supported',
memberExpr.property,
);
}

if (memberExpr.property.type !== 'Identifier') {
throw new AuthorizerExpressionParseError(
'Expected field name identifier',
memberExpr.property,
);
}

const field = memberExpr.property.name;

return {
type: 'fieldRef',
source: 'model',
field,
start: node.start,
end: node.end,
};
}

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

convertFieldRef correctly restricts auth references to implicit userId syntax only.

The function properly handles bare userId as implicit auth context (line 315) and restricts MemberExpression to model.* only (line 349). This means users write userId instead of auth.userId.

Note: the @example in authorizer.ts (line 38) shows model.id === auth.userId which this parser would reject. That documentation should be updated to model.id === userId to match the actual parser behavior.

🤖 Prompt for AI Agents
In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-acorn-parser.ts`
around lines 308 - 380, The example in the authorizer docs conflicts with the
parser behavior in convertFieldRef: the parser only accepts bare userId for
implicit auth context (and rejects auth.userId/member access), so update the
`@example` in authorizer.ts to use userId (e.g., change "model.id === auth.userId"
to "model.id === userId") so docs match the convertFieldRef behavior and avoid
suggesting unsupported syntax.

Comment on lines +90 to +101
if (!parseResult) {
try {
parseAuthorizerExpression(value);
} catch (error) {
if (error instanceof AuthorizerExpressionParseError) {
warnings.push({
message: error.message,
});
}
}
return warnings;
}

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

Parse error warnings lack position information for inline highlighting.

When converting AuthorizerExpressionParseError to a warning, the startPosition and endPosition from the error are discarded. This means the UI won't be able to highlight the error location inline.

Proposed fix
       if (error instanceof AuthorizerExpressionParseError) {
          warnings.push({
            message: error.message,
+           start: error.startPosition,
+           end: error.endPosition,
          });
        }
🤖 Prompt for AI Agents
In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts`
around lines 90 - 101, When parseResult is falsy and
parseAuthorizerExpression(value) throws an AuthorizerExpressionParseError, the
generated warning currently only sets message and discards positional info;
update the catch block that checks error instanceof
AuthorizerExpressionParseError to push a warning object that includes
error.startPosition and error.endPosition (e.g. warnings.push({ message:
error.message, startPosition: error.startPosition, endPosition:
error.endPosition })), ensuring the warning shape matches consumers and retains
inline highlighting info for the UI.

Comment on lines +48 to +55
/**
* Validate an authorizer expression AST against model and project context.
*
* @param ast - The parsed expression AST
* @param modelContext - Information about the parent model
* @param container - The project definition container for accessing roles
* @returns Array of warnings (errors are thrown, warnings are returned)
*

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

Misleading doc comment: no errors are thrown in this function.

Line 54 says @returns Array of warnings (errors are thrown, warnings are returned) but the function body never throws. All validation issues are returned as warnings. This could confuse consumers.

📝 Proposed fix
- * `@returns` Array of warnings (errors are thrown, warnings are returned)
+ * `@returns` Array of warnings for non-blocking validation issues
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Validate an authorizer expression AST against model and project context.
*
* @param ast - The parsed expression AST
* @param modelContext - Information about the parent model
* @param container - The project definition container for accessing roles
* @returns Array of warnings (errors are thrown, warnings are returned)
*
/**
* Validate an authorizer expression AST against model and project context.
*
* `@param` ast - The parsed expression AST
* `@param` modelContext - Information about the parent model
* `@param` container - The project definition container for accessing roles
* `@returns` Array of warnings for non-blocking validation issues
*
🤖 Prompt for AI Agents
In
`@packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-validator.ts`
around lines 48 - 55, Update the JSDoc for the authorizer expression validator
to accurately state that no errors are thrown and all issues are returned as
warnings: edit the comment above the function (referencing the parameters ast,
modelContext, container and the validator function in
authorizer-expression-validator.ts) to change the `@returns` description to
something like "Array of warnings (validation issues are returned as warnings;
this function does not throw)". Ensure the summary and `@returns` lines
consistently reflect that validation problems are returned rather than thrown.

Comment on lines +34 to +39
case 'hasRole': {
return `ctx.auth.hasRole('${node.role}')`;
}
case 'hasSomeRole': {
const roles = node.roles.map((r: string) => `'${r}'`).join(', ');
return `ctx.auth.hasSomeRole([${roles}])`;

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

Role names with special characters (e.g., quotes) would produce invalid TypeScript.

node.role and node.roles are interpolated directly into single-quoted strings without escaping. If a role name ever contains a single quote (e.g., from a parsed string literal like hasRole('it\'s')), the generated code would be syntactically broken.

Consider escaping or using JSON.stringify for safety:

🛡️ Proposed fix
     case 'hasRole': {
-      return `ctx.auth.hasRole('${node.role}')`;
+      return `ctx.auth.hasRole(${JSON.stringify(node.role)})`;
     }
     case 'hasSomeRole': {
-      const roles = node.roles.map((r: string) => `'${r}'`).join(', ');
+      const roles = node.roles.map((r: string) => JSON.stringify(r)).join(', ');
       return `ctx.auth.hasSomeRole([${roles}])`;
     }
🤖 Prompt for AI Agents
In `@packages/project-builder-server/src/compiler/backend/authorizers.ts` around
lines 34 - 39, The generated TypeScript injects raw role strings into
single-quoted literals causing syntax errors for roles with quotes; update the
cases for hasRole and hasSomeRole so that node.role and each element of
node.roles are safely serialized (e.g., use JSON.stringify(role) or an escape
function) before interpolation into the returned expressions
ctx.auth.hasRole(...) and ctx.auth.hasSomeRole(...), ensuring both the
single-value and array branches produce valid quoted string literals even when
role names contain quotes or special characters.

Comment on lines +56 to +61
const languageExtension = useMemo(() => {
if (language === 'javascript' || language === 'typescript') {
return javascript({ typescript: language === 'typescript' });
}
return [];
}, [language]);

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

json language option is accepted but not handled — no JSON extension is loaded.

The language prop type allows 'json', and the Storybook argTypes list it as an option, but languageExtension returns an empty array for it. Either add @codemirror/lang-json support or remove 'json' from the type until it's implemented.

🤖 Prompt for AI Agents
In
`@packages/ui-components/src/components/ui/code-editor-field/code-editor-field.tsx`
around lines 56 - 61, The language prop accepts 'json' but languageExtension
(the useMemo in code-editor-field.tsx) doesn't handle it; import the JSON
extension from '@codemirror/lang-json' and add a branch in the useMemo to return
json() when language === 'json' (keeping the same return shape as for
javascript/typescript); update imports to include json and ensure Storybook
argTypes remain valid or remove 'json' from allowed options if you prefer not to
add the dependency.

Comment on lines +100 to +104

/* Disable ligatures in monospace fonts (Geist Mono merges === into a single ligature) */
@utility font-mono {
font-variant-ligatures: no-common-ligatures;
}

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:

#!/bin/bash
# First, find font-mono usage in the codebase
echo "=== Searching for font-mono usage ==="
rg -n 'font-mono' --type=css --type=html -g '!node_modules' -i
echo ""
echo "=== Searching for font-mono in TypeScript/JSX ==="
rg -n 'font-mono' --type=ts --type=tsx -g '!node_modules' -i

Repository: halfdomelabs/baseplate

Length of output: 1418


🏁 Script executed:

#!/bin/bash
# Check for Tailwind config files and v4 syntax
echo "=== Looking for Tailwind config ==="
find . -maxdepth 3 -type f \( -name "tailwind.config.*" -o -name "tailwind.config.ts" -o -name "tailwind.config.js" \) 2>/dev/null
echo ""
echo "=== Checking the full utilities.css file to understand context ==="
wc -l packages/ui-components/src/utilities.css 2>/dev/null && echo "File exists"

Repository: halfdomelabs/baseplate

Length of output: 226


🏁 Script executed:

#!/bin/bash
# Check if there are other `@utility` overrides in the file
echo "=== Checking for other `@utility` directives in utilities.css ==="
rg -n "@utility" packages/ui-components/src/utilities.css

Repository: halfdomelabs/baseplate

Length of output: 425


🏁 Script executed:

#!/bin/bash
# Check the theme.css file to understand --font-mono usage
echo "=== Checking theme.css around the font-mono variable ==="
sed -n '70,85p' packages/ui-components/src/theme.css
echo ""
echo "=== Full context of theme.css ==="
wc -l packages/ui-components/src/theme.css && echo "---" && head -20 packages/ui-components/src/theme.css

Repository: halfdomelabs/baseplate

Length of output: 870


🏁 Script executed:

#!/bin/bash
# Check how the `@utility` utilities are used - look for actual class usage
echo "=== Searching for actual usage of the custom utilities (surface-default, text-style-lead, etc.) ==="
rg -n 'surface-default|text-style-lead|text-style-large|text-style-small|text-style-muted|text-style-prose' --type=css -g '!node_modules'
echo ""
echo "=== Also check in JSX files ==="
fd '\.jsx' -g '!node_modules' -x rg -l 'surface-default|text-style-lead|font-mono' {}

Repository: halfdomelabs/baseplate

Length of output: 6598


🏁 Script executed:

#!/bin/bash
# View the full utilities.css to understand the intent and context
echo "=== Full utilities.css file ==="
cat -n packages/ui-components/src/utilities.css

Repository: halfdomelabs/baseplate

Length of output: 3144


🌐 Web query:

Tailwind CSS v4 @Utility directive behavior and font-mono built-in utility

💡 Result:

Tailwind CSS v4 @utility directive — what it does / why it exists

  • In v4, Tailwind uses native CSS cascade layers and no longer “hijacks” @layer, so classes you write inside @layer utilities / @layer components are not automatically treated as Tailwind “utilities” for things like variant generation and @apply. Tailwind introduced @utility as the replacement API for registering custom utilities. [1]
  • @utility registers a class as a real Tailwind utility, so it:
    • can be used in markup like normal (class="content-auto")
    • works with variants (hover:…, lg:…, etc.)
    • is inserted into the utilities layer alongside built-ins. [2]

Example:

`@utility` content-auto {
  content-visibility: auto;
}

[2]

Using @apply with your own classes in v4

If you want a custom class name to be “known” to Tailwind (so you can reference it with @apply elsewhere), define it with @utility. Also, if you want that rule to live in a different cascade layer, you can put an @layer block inside @utility:

`@utility` b-h1 {
  `@layer` base {
    `@apply` text-3xl font-bold;
  }
}

This pattern is specifically recommended because v4 no longer treats plain @layer ... { .foo { ... } } as something Tailwind should track for @apply. [3]


font-mono in v4 (built-in utility)

Yes — font-mono is a built-in utility. It sets:

  • font-family: var(--font-mono) with a default monospace stack (ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace). [4]

You can customize it by setting the theme variable (for example) --font-mono via @theme. [4]

Sources: Tailwind v4 upgrade guide [1], Tailwind “Adding custom styles” docs (@utility) [2], Tailwind Labs discussion on @apply + @layer changes [3], Tailwind font-family docs [4].


Custom @utility font-mono incompletely shadows the built-in utility.

In Tailwind CSS v4, defining @utility font-mono here registers a custom utility that overrides the built-in font-mono class. Since this definition only includes font-variant-ligatures: no-common-ligatures; and omits the font-family declaration, any use of the font-mono class would lose its monospace font-family (currently defined as var(--font-mono) in theme.css).

While the font-mono class is not currently used in the codebase, this incomplete utility should be fixed before it's adopted. Either:

  1. Include the font-family declaration in the override, or
  2. Use a distinct utility name (e.g., font-mono-no-ligatures) to preserve font-mono's original behavior.
🧰 Tools
🪛 Biome (2.3.13)

[error] 102-104: Tailwind-specific syntax is disabled.

Enable tailwindDirectives in the css parser options, or remove this if you are not using Tailwind CSS.

(parse)

🤖 Prompt for AI Agents
In `@packages/ui-components/src/utilities.css` around lines 100 - 104, The custom
`@utility` font-mono currently overrides the built-in font-mono but only sets
font-variant-ligatures, which drops the monospace font-family; update the custom
utility to also include the font-family (e.g., font-family: var(--font-mono)) so
it fully preserves the original font-mono behavior, or instead rename the
utility (e.g., font-mono-no-ligatures) and keep your ligature rule there to
avoid shadowing theme.css's var(--font-mono).

@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: 1

🤖 Fix all issues with AI agents
In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/authorizer/model-authorizer-role-form.tsx:
- Around line 38-43: Export the ModelAuthorizerRoleFormProps interface so it can
be used by other modules; change the declaration from "interface
ModelAuthorizerRoleFormProps { ... }" to "export interface
ModelAuthorizerRoleFormProps { ... }" and ensure any imports/exports referencing
ModelAuthorizerRoleFormProps elsewhere still work (update named exports if this
file uses an export list).
🧹 Nitpick comments (4)
packages/project-builder-web/src/routes/data/models/edit.$key/-components/authorizer/model-authorizer-role-form.tsx (3)

139-153: modelConfig as ModelConfig type assertion suggests a type mismatch worth investigating.

If useEditedModelConfig returns a different type than ModelConfig, the cast on line 143 may mask type errors. Consider narrowing the return type at the hook call site (line 116) instead of casting downstream.

Also, the intermediate exts variable (lines 140–152) adds no value — you can return the array literal directly.


179-199: Consider extracting the description JSX into a constant or helper.

The inline description for the CodeEditorFieldController (20 lines of JSX) makes the return block harder to scan. Extracting it into a const expressionDescription = (...) above the return would improve readability without changing behavior.


115-127: Use the proper field type instead of inline annotation.

Line 123: The inline type (field: { name: string }) bypasses the actual field type. Replace it with ModelScalarFieldConfigInput from @baseplate-dev/project-builder-lib for stronger type safety:

modelConfig.model.fields.map((field: ModelScalarFieldConfigInput) => field.name)
packages/fastify-generators/src/generators/prisma/prisma-model-authorizer/prisma-model-authorizer.generator.ts (1)

142-163: Minor: validRoles is recomputed on every getRoleFragment call.

roles.map((r) => r.name) is recomputed each invocation. Since roles is captured from the descriptor and never changes, this could be hoisted. Not a real performance concern given expected call frequency, but it would improve clarity.

♻️ Suggested refactor
+            const validRoleNames = roles.map((r) => r.name);
+
             return {
               prismaModelAuthorizer: {
                 getAuthorizerName() {
                   return authorizerName;
                 },
                 getAuthorizerFragment() {
                   return TsCodeUtils.importFragment(
                     authorizerName,
                     authorizerPath,
                   );
                 },
                 getRoleFragment(roleName: string) {
-                  const validRoles = roles.map((r) => r.name);
-                  if (!validRoles.includes(roleName)) {
+                  if (!validRoleNames.includes(roleName)) {
                     throw new Error(
-                      `Role '${roleName}' not found on ${modelName} authorizer. Available: ${validRoles.join(', ')}`,
+                      `Role '${roleName}' not found on ${modelName} authorizer. Available: ${validRoleNames.join(', ')}`,
                     );
                   }

Comment on lines +38 to +43
interface ModelAuthorizerRoleFormProps {
className?: string;
defaultValues?: AuthorizerRoleConfig;
onSubmit: (role: AuthorizerRoleConfig) => void;
onCancel: () => void;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export the ModelAuthorizerRoleFormProps interface.

As per coding guidelines, "If a particular interface or type is not exported, change the file so it is exported."

Proposed fix
-interface ModelAuthorizerRoleFormProps {
+export interface ModelAuthorizerRoleFormProps {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface ModelAuthorizerRoleFormProps {
className?: string;
defaultValues?: AuthorizerRoleConfig;
onSubmit: (role: AuthorizerRoleConfig) => void;
onCancel: () => void;
}
export interface ModelAuthorizerRoleFormProps {
className?: string;
defaultValues?: AuthorizerRoleConfig;
onSubmit: (role: AuthorizerRoleConfig) => void;
onCancel: () => void;
}
🤖 Prompt for AI Agents
In
`@packages/project-builder-web/src/routes/data/models/edit`.$key/-components/authorizer/model-authorizer-role-form.tsx
around lines 38 - 43, Export the ModelAuthorizerRoleFormProps interface so it
can be used by other modules; change the declaration from "interface
ModelAuthorizerRoleFormProps { ... }" to "export interface
ModelAuthorizerRoleFormProps { ... }" and ensure any imports/exports referencing
ModelAuthorizerRoleFormProps elsewhere still work (update named exports if this
file uses an export list).

@kingston kingston merged commit ef1354d into main Feb 10, 2026
16 checks passed
@kingston kingston deleted the kingston/eng-956-add-authorizer-configuration-to-project-builder-and-wire branch February 10, 2026 08:55
@github-actions github-actions Bot mentioned this pull request Feb 10, 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