Skip to content

A few small fixes#2487

Open
elliots wants to merge 5 commits intozenstackhq:devfrom
elliots:multiple-files-fixes
Open

A few small fixes#2487
elliots wants to merge 5 commits intozenstackhq:devfrom
elliots:multiple-files-fixes

Conversation

@elliots
Copy link

@elliots elliots commented Mar 15, 2026

I'm importing schemas across packages, came across a few issues as I went. Happy to split it out if you prefer.

  • fix: load plugins with jiti: I wasnt able to load a plugin from a workspace package
  • createOpposite option on @ relation: im importing schema files across packages, I'd like a one way import.
  • fix: resolve plugin paths against the schema file where the plugin is: previously it was loading relative to the main schema, not the one where the plugin is actually defined.
  • fix: add migrate diff command: was missing, was it meant to be?

Summary by CodeRabbit

  • New Features

    • createOpposite option to auto-generate missing opposite relation fields
    • New migrate "diff" subcommand with related options
    • SDK helper to find models by name
  • Bug Fixes

    • Suppress missing-opposite relation errors when createOpposite is used
  • Improvements

    • Improved plugin resolution for bare package specifiers and schema-relative paths
  • Tests

    • Added tests for plugin resolution, migrate diff, and relation-generation scenarios

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2259b72-33ae-4392-9699-1c3abd8739b3

📥 Commits

Reviewing files that changed from the base of the PR and between 7c5318b and 4fe4eaa.

📒 Files selected for processing (2)
  • packages/cli/src/actions/generate.ts
  • packages/cli/test/plugins/prisma-plugin.test.ts

📝 Walkthrough

Walkthrough

Adds jiti fallback for resolving plugin modules from bare package specifiers, changes plugin module resolution to derive the load directory from the schema file that declares the plugin, introduces a new migrate diff subcommand with options, and implements createOpposite with automatic opposite-relation generation in Prisma schema output.

Changes

Cohort / File(s) Summary
Plugin loading & resolution
packages/cli/src/actions/action-utils.ts, packages/cli/src/actions/check.ts, packages/cli/src/actions/generate.ts
Added a jiti-based fallback import attempt for bare package specifiers when loading plugin modules. Non-core plugin resolution now derives the pluginSourcePath from plugin.$cstNode?.parent?.element.$document?.uri?.fsPath (falling back to schemaFile) and loads modules relative to that directory. Error propagation behavior preserved.
Migrate diff command
packages/cli/src/actions/migrate.ts, packages/cli/src/index.ts
Introduced DiffOptions and runDiff to compose and execute prisma migrate diff with flags (from-empty, to-empty, from/to schema datamodel, from/to migrations directory, from/to URL, shadow-database-url, --script, --exit-code, plus extra args). Wired new migrate diff CLI subcommand.
Language: relation attribute
packages/language/res/stdlib.zmodel, packages/language/src/validators/datamodel-validator.ts
Extended @relation attribute signature to include optional createOpposite: Boolean?. Validator updated to skip MissingOppositeRelation errors when createOpposite is true (imports/uses isBooleanLiteral).
SDK: Prisma schema generation
packages/sdk/src/prisma/prisma-builder.ts, packages/sdk/src/prisma/prisma-schema-generator.ts
Added PrismaModel.findModel(name). Implemented automatic generation of missing opposite relation fields for relations annotated with createOpposite: true, including handling array vs scalar sides and composite keys; filters out ZenStack-only relation args (e.g., createOpposite) from generated Prisma attributes.
Tests
packages/cli/test/generate.test.ts, packages/cli/test/migrate.test.ts, packages/cli/test/plugins/prisma-plugin.test.ts
Added tests for jiti-based plugin loading from bare package specifiers, plugin resolution relative to the declaring schema file, migrate diff invocation with flags, and extensive Prisma plugin behavior around createOpposite. Also added formatDocument import and getDefaultPrelude usage.
Manifest
package.json
Small manifest change (lines changed: +8/-0).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I sniffed the module burrows, followed each trail,
Found bare-package doors where imports might fail.
I planted opposites where relations were slim,
And told migrate diff how to trace each limb.
Hooray — carrots, tests, and no more derail!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'A few small fixes' is vague and generic, using non-descriptive language that does not convey meaningful information about the substantive changes in the pull request. Consider a more specific title that reflects the main changes, such as: 'Add createOpposite relation option, plugin loading improvements, and migrate diff command' or focus on the primary change if one stands out.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review 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.

❤️ Share

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

Copy link
Contributor

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/actions/migrate.ts (1)

50-58: ⚠️ Potential issue | 🟠 Major

Move schema generation into runDiff or make it conditional for non-schema-based diff modes.

The run() function unconditionally calls requireDataSourceUrl() and generateTempPrismaSchema() at lines 52-55 before the switch statement. This will fail for valid diff modes like URL-to-URL or migrations-to-migrations that don't require the current project's schema. The temp schema should only be generated when fromSchemaDatamodel or toSchemaDatamodel is set to a schema-based mode, not for datasource-to-datasource comparisons.

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

In `@packages/cli/src/actions/migrate.ts` around lines 50 - 58, The current run()
unconditionally calls requireDataSourceUrl(schemaFile) and
generateTempPrismaSchema(schemaFile, prismaSchemaDir), which breaks URL-to-URL
and migrations-only diff modes; move schema validation/generation into runDiff
or guard it with checks on the diff modes so a temp schema is only created when
a schema-based side is required (e.g., when fromSchemaDatamodel or
toSchemaDatamodel equals a schema mode). Concretely, remove or defer the
requireDataSourceUrl/schema generation from run(), and in runDiff (or the call
site that inspects diff mode) call requireDataSourceUrl and
generateTempPrismaSchema only when fromSchemaDatamodel === 'schema' or
toSchemaDatamodel === 'schema' (or whichever enum/value you use for schema-based
modes), preserving the existing prismaSchemaDir logic when constructing the temp
schema.
🧹 Nitpick comments (2)
packages/cli/test/migrate.test.ts (1)

73-76: Strengthen this test beyond “no throw”.

This currently behaves as a smoke test only. If runCli exposes stdout/stderr, assert the --script output contains expected SQL diff content to catch functional regressions in migrate diff.

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

In `@packages/cli/test/migrate.test.ts` around lines 73 - 76, The test only checks
that migrate diff doesn't throw; enhance it by capturing runCli's stdout/stderr
and asserting the --script output contains expected SQL diff text (e.g., "CREATE
TABLE", table/column names from the provided model, or specific ALTER/CREATE
statements) to validate the generated SQL; update the test case in
migrate.test.ts (the "supports migrate diff with --from-empty and
--to-schema-datamodel" it block) to call runCli in a way that returns
stdout/stderr and add assertions against that output rather than relying solely
on no exception.
packages/cli/src/actions/generate.ts (1)

189-193: Consider extracting shared plugin source-path resolution logic.

This logic now exists in both generate.ts and check.ts. Moving it to action-utils.ts would reduce drift risk and keep plugin resolution behavior centralized.

Refactor sketch
+// action-utils.ts
+export function getPluginSourcePath(plugin: { $cstNode?: any }, schemaFile: string) {
+    return plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
+}
-const pluginSourcePath =
-    plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
+const pluginSourcePath = getPluginSourcePath(plugin, schemaFile);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/actions/generate.ts` around lines 189 - 193, Extract the
repeated plugin source-path resolution into a shared helper in action-utils.ts
(e.g., resolvePluginSourceDir or getPluginSourceDir) that accepts a plugin node
and a fallback schemaFile and returns the directory path (using
plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile then
path.dirname(...)); replace the inlined logic in generate.ts (where cliPlugin is
loaded with loadPluginModule(provider, path.dirname(pluginSourcePath))) and the
similar code in check.ts to call the new helper and pass its result into
loadPluginModule so both places share the same resolution behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/actions/migrate.ts`:
- Around line 162-205: runDiff constructs a shell command by joining parts and
calls execPrisma with a single string, which allows unescaped values
(options.fromUrl, options.toUrl, options.shadowDatabaseUrl, options.extraArgs)
to be interpreted by the shell; change execPrisma usage so arguments are passed
safely (either by making execPrisma accept an argv array and call
child_process.spawn/execFile without a shell, or by properly shell-escaping each
part before joining) and update runDiff to pass the array of parts (or escaped
parts) instead of parts.join(' '); reference runDiff and execPrisma (and the
exec-utils helper that currently uses execSync) when making the change.

In `@packages/sdk/src/prisma/prisma-schema-generator.ts`:
- Around line 556-590: The FK generation uses ID fields from oppositeModel and
only the first ID, causing wrong references and dropping composite keys; update
the logic in prisma-schema-generator to call getIdFields(decl) (not
oppositeModel), iterate all returned id field names to (a) create corresponding
ref FK fields on prismaOppositeModel (using ModelFieldType and allFields to
mirror types) and (b) build the `@relation` 'fields' and 'references' arrays by
mapping each idFieldName to a PrismaAttributeArgValue('FieldReference', new
PrismaFieldReference(...)) — use fieldName+CapitalizedId for the FK names and
idFieldName for references so addField(fieldName, new
ModelFieldType(decl.name,...), [new PrismaFieldAttribute('@relation', [...])])
contains arrays for all id fields rather than a single idFields[0].
- Around line 542-555: In generateMissingOppositeRelations(), the opposite-field
lookup currently matches only by target model name (using
getAllFields(...).filter(f => f.type.reference?.ref?.name === decl.name));
change this to extract the `@relation` name from the field (e.g., read relation
name from the field's relation metadata—refer to symbols fieldRel / thisRelation
used by the validator) and match opposite fields by relation identity (relation
name) in addition to the target model so multiple relations to the same model
are distinguished; update the filter to compare the extracted relation name for
both the current field and each candidate opposite field. Also replace the
silent continue when a generated fieldName collides with an existing
prismaOppositeModel field (the prismaOppositeModel.fields.some check) with
explicit error handling (throw or processLogger.error + throw) so name
collisions are surfaced instead of being skipped.

---

Outside diff comments:
In `@packages/cli/src/actions/migrate.ts`:
- Around line 50-58: The current run() unconditionally calls
requireDataSourceUrl(schemaFile) and generateTempPrismaSchema(schemaFile,
prismaSchemaDir), which breaks URL-to-URL and migrations-only diff modes; move
schema validation/generation into runDiff or guard it with checks on the diff
modes so a temp schema is only created when a schema-based side is required
(e.g., when fromSchemaDatamodel or toSchemaDatamodel equals a schema mode).
Concretely, remove or defer the requireDataSourceUrl/schema generation from
run(), and in runDiff (or the call site that inspects diff mode) call
requireDataSourceUrl and generateTempPrismaSchema only when fromSchemaDatamodel
=== 'schema' or toSchemaDatamodel === 'schema' (or whichever enum/value you use
for schema-based modes), preserving the existing prismaSchemaDir logic when
constructing the temp schema.

---

Nitpick comments:
In `@packages/cli/src/actions/generate.ts`:
- Around line 189-193: Extract the repeated plugin source-path resolution into a
shared helper in action-utils.ts (e.g., resolvePluginSourceDir or
getPluginSourceDir) that accepts a plugin node and a fallback schemaFile and
returns the directory path (using
plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile then
path.dirname(...)); replace the inlined logic in generate.ts (where cliPlugin is
loaded with loadPluginModule(provider, path.dirname(pluginSourcePath))) and the
similar code in check.ts to call the new helper and pass its result into
loadPluginModule so both places share the same resolution behavior.

In `@packages/cli/test/migrate.test.ts`:
- Around line 73-76: The test only checks that migrate diff doesn't throw;
enhance it by capturing runCli's stdout/stderr and asserting the --script output
contains expected SQL diff text (e.g., "CREATE TABLE", table/column names from
the provided model, or specific ALTER/CREATE statements) to validate the
generated SQL; update the test case in migrate.test.ts (the "supports migrate
diff with --from-empty and --to-schema-datamodel" it block) to call runCli in a
way that returns stdout/stderr and add assertions against that output rather
than relying solely on no exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 218dfde8-e071-42b3-b015-88e6eca927af

📥 Commits

Reviewing files that changed from the base of the PR and between 0778e49 and 83a882d.

📒 Files selected for processing (12)
  • packages/cli/src/actions/action-utils.ts
  • packages/cli/src/actions/check.ts
  • packages/cli/src/actions/generate.ts
  • packages/cli/src/actions/migrate.ts
  • packages/cli/src/index.ts
  • packages/cli/test/generate.test.ts
  • packages/cli/test/migrate.test.ts
  • packages/cli/test/plugins/prisma-plugin.test.ts
  • packages/language/res/stdlib.zmodel
  • packages/language/src/validators/datamodel-validator.ts
  • packages/sdk/src/prisma/prisma-builder.ts
  • packages/sdk/src/prisma/prisma-schema-generator.ts

Comment on lines +162 to +205
function runDiff(prismaSchemaFile: string, options: DiffOptions) {
try {
const parts = ['migrate diff'];

if (options.fromEmpty) {
parts.push('--from-empty');
}
if (options.toEmpty) {
parts.push('--to-empty');
}
if (options.fromSchemaDatamodel) {
parts.push(`--from-schema-datamodel "${prismaSchemaFile}"`);
}
if (options.toSchemaDatamodel) {
parts.push(`--to-schema-datamodel "${prismaSchemaFile}"`);
}
if (options.fromMigrationsDirectory) {
parts.push(`--from-migrations-directory "${options.fromMigrationsDirectory}"`);
}
if (options.toMigrationsDirectory) {
parts.push(`--to-migrations-directory "${options.toMigrationsDirectory}"`);
}
if (options.fromUrl) {
parts.push(`--from-url "${options.fromUrl}"`);
}
if (options.toUrl) {
parts.push(`--to-url "${options.toUrl}"`);
}
if (options.shadowDatabaseUrl) {
parts.push(`--shadow-database-url "${options.shadowDatabaseUrl}"`);
}
if (options.script) {
parts.push('--script');
}
if (options.exitCode) {
parts.push('--exit-code');
}

// pass through any extra args
if (options.extraArgs?.length) {
parts.push(...options.extraArgs);
}

execPrisma(parts.join(' '));
Copy link
Contributor

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:

find . -type f -name "migrate.ts" | grep -E "(packages/cli|src)" | head -5

Repository: zenstackhq/zenstack

Length of output: 100


🏁 Script executed:

find . -type f -name "exec-utils.ts" | head -5

Repository: zenstackhq/zenstack

Length of output: 101


🏁 Script executed:

# Read the migrate.ts file to see the actual code
git ls-files "packages/cli/src/actions/migrate.ts" && \
cat -n "packages/cli/src/actions/migrate.ts" | sed -n '150,220p'

Repository: zenstackhq/zenstack

Length of output: 2688


🏁 Script executed:

# Read the exec-utils.ts file to understand how execPrisma works
git ls-files "packages/cli/src/utils/exec-utils.ts" && \
wc -l "packages/cli/src/utils/exec-utils.ts"

Repository: zenstackhq/zenstack

Length of output: 139


🏁 Script executed:

cat -n "packages/cli/src/utils/exec-utils.ts"

Repository: zenstackhq/zenstack

Length of output: 2292


Escape all arguments before shell execution.

Line 205 passes parts.join(' ') directly to execPrisma, which executes it as a shell command via execSync (exec-utils.ts:60). Values like options.fromUrl, options.toUrl, options.shadowDatabaseUrl, and options.extraArgs are not escaped, so any containing spaces, $, backticks, or quotes will be interpreted by the shell before Prisma sees them. Use proper shell escaping or switch to argv-based execution.

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

In `@packages/cli/src/actions/migrate.ts` around lines 162 - 205, runDiff
constructs a shell command by joining parts and calls execPrisma with a single
string, which allows unescaped values (options.fromUrl, options.toUrl,
options.shadowDatabaseUrl, options.extraArgs) to be interpreted by the shell;
change execPrisma usage so arguments are passed safely (either by making
execPrisma accept an argv array and call child_process.spawn/execFile without a
shell, or by properly shell-escaping each part before joining) and update
runDiff to pass the array of parts (or escaped parts) instead of parts.join('
'); reference runDiff and execPrisma (and the exec-utils helper that currently
uses execSync) when making the change.

@elliots elliots force-pushed the multiple-files-fixes branch from 83a882d to 7c5318b Compare March 15, 2026 09:24
Copy link
Contributor

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

🧹 Nitpick comments (1)
packages/cli/test/generate.test.ts (1)

287-305: Optional: extract plugin fixture writer to reduce duplication.

Both new tests hand-roll similar plugin module scaffolding; a small helper in packages/cli/test/utils.ts would keep future plugin-resolution tests shorter and easier to maintain.

Also applies to: 337-346

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

In `@packages/cli/test/generate.test.ts` around lines 287 - 305, Extract the
repeated scaffolding that creates a fake node_modules plugin into a reusable
helper (e.g., createPluginFixture or writePluginModule) placed in
packages/cli/test/utils.ts; the helper should accept parameters for workDir,
packageName, main entry (index.ts), and file contents, and it should encapsulate
the mkdirSync, package.json write, and index.ts write logic currently duplicated
in generate.test.ts (the block creating node_modules/my-test-plugin and its
package.json/index.ts) and the similar block at 337-346; then replace the
duplicated code in generate.test.ts with calls to the new helper (referencing
the helper by name) to keep tests DRY and easier to maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/test/plugins/prisma-plugin.test.ts`:
- Around line 131-151: The test reveals that generateMissingOppositeRelations()
errors are being swallowed by the try-catch in generate (the block around the
generate.ts try { ... } catch { ... }), causing silent success; update the catch
in the generate() flow so it does not silently swallow exceptions from
generateMissingOppositeRelations(): either rethrow the caught error (or call
process.exit(1) after logging) so the process returns non-zero, or explicitly
detect and propagate validation/generation errors to the caller; ensure the
change references generateMissingOppositeRelations() and the try-catch in
generate so the field name collision causes a visible failure instead of a
silent missing schema file.

---

Nitpick comments:
In `@packages/cli/test/generate.test.ts`:
- Around line 287-305: Extract the repeated scaffolding that creates a fake
node_modules plugin into a reusable helper (e.g., createPluginFixture or
writePluginModule) placed in packages/cli/test/utils.ts; the helper should
accept parameters for workDir, packageName, main entry (index.ts), and file
contents, and it should encapsulate the mkdirSync, package.json write, and
index.ts write logic currently duplicated in generate.test.ts (the block
creating node_modules/my-test-plugin and its package.json/index.ts) and the
similar block at 337-346; then replace the duplicated code in generate.test.ts
with calls to the new helper (referencing the helper by name) to keep tests DRY
and easier to maintain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 730ab75b-9dae-4cec-93bc-06d31faf0494

📥 Commits

Reviewing files that changed from the base of the PR and between 83a882d and 7c5318b.

📒 Files selected for processing (11)
  • packages/cli/src/actions/check.ts
  • packages/cli/src/actions/generate.ts
  • packages/cli/src/actions/migrate.ts
  • packages/cli/src/index.ts
  • packages/cli/test/generate.test.ts
  • packages/cli/test/migrate.test.ts
  • packages/cli/test/plugins/prisma-plugin.test.ts
  • packages/language/res/stdlib.zmodel
  • packages/language/src/validators/datamodel-validator.ts
  • packages/sdk/src/prisma/prisma-builder.ts
  • packages/sdk/src/prisma/prisma-schema-generator.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/cli/src/index.ts
  • packages/language/res/stdlib.zmodel
  • packages/cli/src/actions/generate.ts
  • packages/cli/src/actions/check.ts
  • packages/cli/src/actions/migrate.ts
  • packages/sdk/src/prisma/prisma-builder.ts
  • packages/cli/test/migrate.test.ts

@ymc9
Copy link
Member

ymc9 commented Mar 17, 2026

Hi @elliots , thanks for making this PR!

I have a few general thoughts:

  1. About migrate diff

    Do you use it frequently? I can't remember exactly, but maybe I intentionally left it out when working on v3. This command feels too much coupled with Prisma Migrate - which may be replaced by a zenstack-home-made migration engine in the future. Also, to wrap around prisma migrate diff, when specifying a ZModel as output, I believe we also need to handle the prisma-generated schema and merge it into ZModel. Can be a bit tricky and fragile.

  2. About one-direction relation

    This has been brought up quite a few times back in v2, and I think it's a very useful feature to have. Besides automatically filling the opposite side of relation on prisma schema level, I'm not sure if anything needs to be done on zenstack tool chain yet - the ORM API, zod schemas, tanstack hooks, etc.

    Shall we spin it out into a separate PR? It's more like a new feature.

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.

2 participants