Skip to content

feat: Support renames in reference expressions#860

Merged
kingston merged 11 commits intomainfrom
kingston/eng-1070-support-renames-in-reference-expressions
Mar 22, 2026
Merged

feat: Support renames in reference expressions#860
kingston merged 11 commits intomainfrom
kingston/eng-1070-support-renames-in-reference-expressions

Conversation

@kingston
Copy link
Collaborator

@kingston kingston commented Mar 21, 2026

Summary by CodeRabbit

  • New Features
    • Added support for automatic updates to reference expressions when fields, relations, or roles are renamed in your project definition.

@changeset-bot
Copy link

changeset-bot bot commented Mar 21, 2026

🦋 Changeset detected

Latest commit: fd0e7a3

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

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

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

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

@cloudflare-workers-and-pages
Copy link

Deploying baseplate-storybook with  Cloudflare Pages  Cloudflare Pages

Latest commit: d37f5ca
Status: ✅  Deploy successful!
Preview URL: https://ef898967.baseplate-storybook.pages.dev
Branch Preview URL: https://kingston-eng-1070-support-re.baseplate-storybook.pages.dev

View logs

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request adds support for automatically updating authorizer expressions when fields, relations, or roles are renamed. It introduces expression parser infrastructure (specs, refs, validation), implements rename orchestration logic, and integrates these into the definition save/validation pipeline across server and web packages.

Changes

Cohort / File(s) Summary
Metadata & Configuration
.changeset/support-expression-renames.md, .claude/settings.json
Added changelog entry for patch releases documenting expression rename support; extended Bash command allowlist to permit git mv.
Authorizer Expression Parser Implementation
packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-parser.ts, packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-rename.unit.test.ts, packages/project-builder-lib/src/expression-parsers/authorizer/index.ts
New AuthorizerExpressionParser class implementing RefExpressionParser with getReferencedEntities() to extract rename dependencies (field/relation/role references with positions) from parsed authorizer expressions. Comprehensive test coverage for dependency resolution and rename application scenarios.
Expression Parser Abstraction & Registration
packages/project-builder-lib/src/references/expression-parser-ref.ts, packages/project-builder-lib/src/references/expression-parser-spec.ts, packages/project-builder-lib/src/expression-parsers/register-core-module.ts
Introduced ExpressionParserRef interface for deferred parser resolution via name lookup, expressionParserSpec for parser registry management, and expressionParserCoreModule for registering the authorizer parser into the plugin system.
Reference Type & Interface Updates
packages/project-builder-lib/src/references/expression-types.ts, packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts, packages/project-builder-lib/src/references/parse-schema-with-references.ts
Tightened ProjectDefinition typing; replaced getDependencies/updateForRename methods with getReferencedEntities(); added support for ExpressionParserRef resolution in withExpression; exported ParseSchemaWithTransformedReferencesOptions type alias.
Expression Rename Orchestration
packages/project-builder-lib/src/references/fix-expression-renames.ts, packages/project-builder-lib/src/references/fix-expression-renames.unit.test.ts, packages/project-builder-lib/src/references/fix-definition-refs.ts
Implemented applyExpressionRenames to parse old expressions, detect renames via entity ID comparison, and apply position-based substring replacements; fixDefinitionRefs orchestrates both expression renames and reference deletion fixes in a single pass. Comprehensive unit tests validate single/multiple renames and edge cases.
Schema & Parser Context Integration
packages/project-builder-lib/src/schema/creator/schema-creator.ts, packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts, packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-ref.ts, packages/project-builder-lib/src/schema/models/authorizer/authorizer.ts, packages/project-builder-lib/src/schema/models/authorizer/index.ts
Removed old in-schema AuthorizerExpressionParser implementation; created authorizerExpressionRef using new ref/spec infrastructure; wired parser context to use plugin-injected parsers; updated barrel exports to import from centralized expression-parsers module.
Definition Issue Collection
packages/project-builder-lib/src/parser/collect-expression-issues.ts, packages/project-builder-lib/src/parser/collect-expression-issues.unit.test.ts, packages/project-builder-lib/src/parser/collect-definition-issues.ts, packages/project-builder-lib/src/parser/collect-definition-issues.unit.test.ts
Refactored collectExpressionIssues to accept structured CollectExpressionIssuesInput with pre-resolved expressions; added error wrapping for parser failures; updated all callers and tests to use new parameter signature and extraction flow.
Test Utilities & Helpers
packages/project-builder-lib/src/testing/expression-stub-parser.test-helper.ts, packages/project-builder-lib/src/testing/expression-warning-parser.test-helper.ts, packages/project-builder-lib/src/testing/parser-context.test-helper.ts, packages/project-builder-lib/src/testing/project-definition-container.test-helper.ts
Updated stub/warning parsers to implement getReferencedEntities() instead of deprecated getDependencies/updateForRename; registered expressionParserCoreModule in test helper plugin stores.
References Barrel Exports
packages/project-builder-lib/src/references/index.ts, packages/project-builder-lib/src/index.ts, packages/project-builder-lib/src/references/extract-definition-refs.ts
Added re-exports for expression parser ref/spec and ref-fixing utilities; exported expressionParserCoreModule from main package; minor comment clarification in extract-definition-refs.
Server-Side Draft & Validation
packages/project-builder-server/src/actions/definition/draft-session.ts, packages/project-builder-server/src/actions/definition/validate-draft.ts, packages/project-builder-server/src/actions/definition/stage-update-entity.action.ts, packages/project-builder-server/src/actions/definition/definition-test-fixtures.test-helper.ts, packages/project-builder-server/src/actions/definition/draft-lifecycle.int.test.ts
Added oldRefPayload field to DraftSessionContext and threaded it through validation pipeline; updated fixAndValidateDraftDefinition and validateAndSaveDraft to call fixDefinitionRefs with old payload for expression rename handling.
Server & Web Core Module Registration
packages/project-builder-server/src/core-modules/index.ts, packages/project-builder-web/src/core-modules/index.ts, packages/project-builder-web/src/app/project-definition-provider/project-definition-provider.tsx
Registered expressionParserCoreModule in both server and web core module arrays; updated web provider to pass oldRefPayload to fixDefinitionRefs during save operations.

Sequence Diagram

sequenceDiagram
    actor User
    participant Web as Web App
    participant Server as Server<br/>(Draft Handler)
    participant Parser as Expression<br/>Parser
    participant Refs as Ref<br/>Orchestrator
    participant Schema as Schema<br/>Validator

    User->>Web: Update field/role name
    Web->>Server: Stage update with old definition
    Server->>Server: Load draft with oldRefPayload
    Note over Server: oldRefPayload = previous<br/>expression resolution state
    Server->>Refs: fixDefinitionRefs(schema, newDef,<br/>{ oldRefPayload })
    Refs->>Parser: Parse old expressions
    Note over Parser: Extract referenced entities<br/>via getReferencedEntities()
    Parser-->>Refs: Dependencies with positions
    Refs->>Refs: Compare old vs new<br/>entity IDs (detect renames)
    Note over Refs: Old field "x" → New "y"<br/>Maps position ranges to<br/>new names
    Refs->>Refs: Apply substring replacements<br/>to expressions in descending<br/>position order
    Refs->>Schema: fixRefDeletions(schema,<br/>renamed definition)
    Schema-->>Refs: Clean definition
    Refs-->>Server: Updated definition<br/>+ modified flag
    Server->>Server: Save to project
    Server-->>Web: Success
    Web-->>User: Field renamed, expressions updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Support renames in reference expressions' directly and concisely describes the main feature being added—enabling automatic updates of authorizer expressions when referenced fields, relations, or roles are renamed.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kingston/eng-1070-support-renames-in-reference-expressions

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.

Tip

CodeRabbit can use oxc to improve the quality of JavaScript and TypeScript code reviews.

Add a configuration file to your project to customize how CodeRabbit runs oxc.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

54-57: Clarify that dependency ranges are [start, end).

The new authorizer parser is emitting exclusive end offsets, but this interface only says “end position”. Making the convention explicit here will help future parsers avoid off-by-one replacements.

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

In `@packages/project-builder-lib/src/references/expression-types.ts` around lines
54 - 57, The comment for the dependency range fields is ambiguous about
inclusive/exclusive end offsets; update the JSDoc for the start and end
properties (the start and end fields in the same interface) to explicitly state
the range is half-open [start, end) so callers know end is exclusive; change the
existing comment lines to mention "end is exclusive" (or "range is [start,
end)"), and ensure any surrounding description refers to that convention so
parsers emitting exclusive end offsets are correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/settings.json:
- Line 61: The permission pattern "Bash(git mv:*)" is malformed and should use
space-delimited wildcard syntax; update the entry that contains "Bash(git mv:*)"
to use a space between the command and the wildcard (e.g., "Bash(git mv *)") so
it correctly matches git mv invocations, and ensure the updated pattern follows
the same format as the other Bash(...) entries in the settings file.

In `@packages/project-builder-lib/src/parser/collect-expression-issues.ts`:
- Around line 41-47: Wrap each call to expression.parser.validate inside a
try/catch so a thrown exception from plugin code does not abort the entire loop;
in the catch create a warning object describing the exception and push it into
the same per-expression warnings collection (the one currently assigned to the
variable warnings or whatever is stored on the expression), ensuring you still
continue to the next expression. Specifically modify the loop that iterates over
expressions and calls expression.parser.validate(...) so any error is caught and
converted into a warning attached to that expression (include error.message and
identifying context like expression.value/parser name) instead of rethrowing.

In `@packages/project-builder-lib/src/references/expression-parser-ref.ts`:
- Around line 21-25: The JSDoc example for createExpressionParserRef incorrectly
passes a Zod schema instance to createExpressionParserRef; update the example so
that createExpressionParserRef is called with a schema factory function (e.g.,
pass a function that returns z.string().min(1, 'Expression is required')) when
creating authorizerExpressionRef to match the API signature of
createExpressionParserRef.

In `@packages/project-builder-lib/src/references/fix-expression-renames.ts`:
- Around line 62-117: The current rewrite unconditionally applies replacements
built from oldDefinition which can overwrite edits or resurrect deleted nodes in
newDefinition; change the logic after building updated to first read the current
value at expression.path from newDefinition (or use get(newDefinition,
expression.path)) and only push an update if that current value strictly equals
the original old expression.value (expression.value) — also skip pushing if the
path does not exist in newDefinition to avoid resurrecting deleted nodes;
finally, when applying updates keep the existing set(result, path, value) but
only for updates that passed those checks so you never clobber newer edits or
recreate removed nodes.

---

Nitpick comments:
In `@packages/project-builder-lib/src/references/expression-types.ts`:
- Around line 54-57: The comment for the dependency range fields is ambiguous
about inclusive/exclusive end offsets; update the JSDoc for the start and end
properties (the start and end fields in the same interface) to explicitly state
the range is half-open [start, end) so callers know end is exclusive; change the
existing comment lines to mention "end is exclusive" (or "range is [start,
end)"), and ensure any surrounding description refers to that convention so
parsers emitting exclusive end offsets are correct.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 42f9653e-90de-4f28-b051-ccc4e176d1db

📥 Commits

Reviewing files that changed from the base of the PR and between fc8f158 and d37f5ca.

📒 Files selected for processing (45)
  • .changeset/support-expression-renames.md
  • .claude/settings.json
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-acorn-parser.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-acorn-parser.unit.test.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-ast.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-parser.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-rename.unit.test.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-validator.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-validator.unit.test.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/authorizer-expression-visitor.ts
  • packages/project-builder-lib/src/expression-parsers/authorizer/index.ts
  • packages/project-builder-lib/src/expression-parsers/register-core-module.ts
  • packages/project-builder-lib/src/index.ts
  • packages/project-builder-lib/src/parser/collect-definition-issues.ts
  • packages/project-builder-lib/src/parser/collect-definition-issues.unit.test.ts
  • packages/project-builder-lib/src/parser/collect-expression-issues.ts
  • packages/project-builder-lib/src/parser/collect-expression-issues.unit.test.ts
  • packages/project-builder-lib/src/references/expression-parser-ref.ts
  • packages/project-builder-lib/src/references/expression-parser-spec.ts
  • packages/project-builder-lib/src/references/expression-types.ts
  • packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts
  • packages/project-builder-lib/src/references/extract-definition-refs.ts
  • packages/project-builder-lib/src/references/fix-definition-refs.ts
  • packages/project-builder-lib/src/references/fix-expression-renames.ts
  • packages/project-builder-lib/src/references/fix-expression-renames.unit.test.ts
  • packages/project-builder-lib/src/references/index.ts
  • packages/project-builder-lib/src/references/parse-schema-with-references.ts
  • packages/project-builder-lib/src/schema/creator/schema-creator.ts
  • packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts
  • packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-ref.ts
  • packages/project-builder-lib/src/schema/models/authorizer/authorizer.ts
  • packages/project-builder-lib/src/schema/models/authorizer/index.ts
  • packages/project-builder-lib/src/testing/expression-stub-parser.test-helper.ts
  • packages/project-builder-lib/src/testing/expression-warning-parser.test-helper.ts
  • packages/project-builder-lib/src/testing/parser-context.test-helper.ts
  • packages/project-builder-lib/src/testing/project-definition-container.test-helper.ts
  • packages/project-builder-server/src/actions/definition/definition-test-fixtures.test-helper.ts
  • packages/project-builder-server/src/actions/definition/draft-lifecycle.int.test.ts
  • packages/project-builder-server/src/actions/definition/draft-session.ts
  • packages/project-builder-server/src/actions/definition/stage-update-entity.action.ts
  • packages/project-builder-server/src/actions/definition/validate-draft.ts
  • packages/project-builder-server/src/core-modules/index.ts
  • packages/project-builder-web/src/app/project-definition-provider/project-definition-provider.tsx
  • packages/project-builder-web/src/core-modules/index.ts
  • plugins/plugin-payments/src/stripe/core/schema/schema-issue-checker.ts
💤 Files with no reviewable changes (1)
  • packages/project-builder-lib/src/schema/models/authorizer/authorizer-expression-parser.ts

Comment on lines +62 to +117
for (const expression of oldExpressions) {
// Parse and resolve entities against the OLD definition where old names still exist
const parseResult = expression.parser.parse(
expression.value,
oldDefinition,
);
if (!parseResult.success) {
// Don't touch broken expressions
continue;
}

const refs = expression.parser.getReferencedEntities(
expression.value,
parseResult,
oldDefinition,
expression.resolvedSlots,
);

// Build replacements for renamed entities
const replacements: { start: number; end: number; newValue: string }[] = [];
for (const ref of refs) {
const newName = renames.get(ref.entityId);
if (newName !== undefined) {
replacements.push({
start: ref.start,
end: ref.end,
newValue: newName,
});
}
}

if (replacements.length === 0) {
continue;
}

// Sort by position descending so earlier replacements don't shift later positions
replacements.sort((a, b) => b.start - a.start);

let updated = expression.value as string;
for (const { start, end, newValue } of replacements) {
updated = updated.slice(0, start) + newValue + updated.slice(end);
}

modified = true;
// Update at the same path in the NEW definition
updates.push({ path: expression.path, value: updated });
}

if (!modified) {
return { value: newDefinition, modified: false };
}

const result = structuredClone(newDefinition) as object;
for (const { path, value } of updates) {
set(result, path, value);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Prevent stale expression rewrites and path resurrection during rename application.

Current logic rewrites from oldRefPayload text and unconditionally sets by old path. If the expression was edited or removed in the new definition, this can clobber newer content or recreate deleted nodes.

🔧 Proposed fix
-import { set } from 'es-toolkit/compat';
+import { get, set } from 'es-toolkit/compat';
@@
   for (const expression of oldExpressions) {
+    const currentValue = get(newDefinition as object, expression.path);
+    if (typeof currentValue !== 'string') {
+      // Expression was removed or shape changed in new definition.
+      continue;
+    }
+
     // Parse and resolve entities against the OLD definition where old names still exist
     const parseResult = expression.parser.parse(
-      expression.value,
+      currentValue,
       oldDefinition,
     );
@@
     const refs = expression.parser.getReferencedEntities(
-      expression.value,
+      currentValue,
       parseResult,
       oldDefinition,
       expression.resolvedSlots,
     );
@@
-    let updated = expression.value as string;
+    let updated = currentValue;
     for (const { start, end, newValue } of replacements) {
       updated = updated.slice(0, start) + newValue + updated.slice(end);
     }
 
-    modified = true;
-    // Update at the same path in the NEW definition
-    updates.push({ path: expression.path, value: updated });
+    if (updated !== currentValue) {
+      modified = true;
+      // Update at the same path in the NEW definition
+      updates.push({ path: expression.path, value: updated });
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-lib/src/references/fix-expression-renames.ts` around
lines 62 - 117, The current rewrite unconditionally applies replacements built
from oldDefinition which can overwrite edits or resurrect deleted nodes in
newDefinition; change the logic after building updated to first read the current
value at expression.path from newDefinition (or use get(newDefinition,
expression.path)) and only push an update if that current value strictly equals
the original old expression.value (expression.value) — also skip pushing if the
path does not exist in newDefinition to avoid resurrecting deleted nodes;
finally, when applying updates keep the existing set(result, path, value) but
only for updates that passed those checks so you never clobber newer edits or
recreate removed nodes.

@kingston kingston merged commit 2a514a6 into main Mar 22, 2026
9 of 10 checks passed
@kingston kingston deleted the kingston/eng-1070-support-renames-in-reference-expressions branch March 22, 2026 07:50
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