feat: Generate model authorizers from UI#753
Conversation
…n-to-project-builder-and-wire
…n-to-project-builder-and-wire
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 253b0cb The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
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 |
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
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 exporteduserAuthorizerconstant.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 exporteduserAuthorizerconstant.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:CodeEditorFieldControllerPropsinterface 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: Defaultextensions = []creates a new array reference each render, defeatinguseMemo.Every render without an explicit
extensionsprop allocates a fresh[], changing the reference and causingallExtensionsto 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 number6with"model.".lengthfor clarity.The hardcoded offset
6on 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!pathResultbranch.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-emptyexpressionsare 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 theModelAuthorizerRoleDialogPropsinterface.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 thePropsinterface.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.
getDependenciesandupdateForRenameare 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:getModelContextrelies 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 returnundefinedinstead 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:ModelAuthorizerRoleFormPropsshould 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 replaceundefined/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
walkfunction handles all currentAuthorizerExpressionNodetypes but has nodefaultcase. If a new node type is added to the union, this will silently skip validation for it. Adefault: neverassertion 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_FIELDSis 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, androlesEndas 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
Tdefaults toRecord<string, never>,keyof Tisstring, soExpressionSlotMap/ResolvedExpressionSlotsbecomeRecord<string, RefContextSlot<never>>/Record<string, ReferencePath>rather than empty objects. UsingRecord<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:validRolesis recomputed on everygetRoleFragmentcall.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.
| 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| if (!parseResult) { | ||
| try { | ||
| parseAuthorizerExpression(value); | ||
| } catch (error) { | ||
| if (error instanceof AuthorizerExpressionParseError) { | ||
| warnings.push({ | ||
| message: error.message, | ||
| }); | ||
| } | ||
| } | ||
| return warnings; | ||
| } |
There was a problem hiding this comment.
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.
| /** | ||
| * 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) | ||
| * |
There was a problem hiding this comment.
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.
| /** | |
| * 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.
| case 'hasRole': { | ||
| return `ctx.auth.hasRole('${node.role}')`; | ||
| } | ||
| case 'hasSomeRole': { | ||
| const roles = node.roles.map((r: string) => `'${r}'`).join(', '); | ||
| return `ctx.auth.hasSomeRole([${roles}])`; |
There was a problem hiding this comment.
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.
| const languageExtension = useMemo(() => { | ||
| if (language === 'javascript' || language === 'typescript') { | ||
| return javascript({ typescript: language === 'typescript' }); | ||
| } | ||
| return []; | ||
| }, [language]); |
There was a problem hiding this comment.
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.
|
|
||
| /* Disable ligatures in monospace fonts (Geist Mono merges === into a single ligature) */ | ||
| @utility font-mono { | ||
| font-variant-ligatures: no-common-ligatures; | ||
| } |
There was a problem hiding this comment.
🧩 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' -iRepository: 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.cssRepository: 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.cssRepository: 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.cssRepository: 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 componentsare not automatically treated as Tailwind “utilities” for things like variant generation and@apply. Tailwind introduced@utilityas the replacement API for registering custom utilities. [1] @utilityregisters 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
utilitieslayer alongside built-ins. [2]
- can be used in markup like normal (
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:
- Include the font-family declaration in the override, or
- Use a distinct utility name (e.g.,
font-mono-no-ligatures) to preservefont-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).
There was a problem hiding this comment.
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 ModelConfigtype assertion suggests a type mismatch worth investigating.If
useEditedModelConfigreturns a different type thanModelConfig, 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
extsvariable (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 aconst 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 withModelScalarFieldConfigInputfrom@baseplate-dev/project-builder-libfor 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:validRolesis recomputed on everygetRoleFragmentcall.
roles.map((r) => r.name)is recomputed each invocation. Sincerolesis 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(', ')}`, ); }
| interface ModelAuthorizerRoleFormProps { | ||
| className?: string; | ||
| defaultValues?: AuthorizerRoleConfig; | ||
| onSubmit: (role: AuthorizerRoleConfig) => void; | ||
| onCancel: () => void; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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).
Summary by CodeRabbit