Skip to content

feat: support natural keys in the API #2188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

lsmith77
Copy link
Contributor

@lsmith77 lsmith77 commented Jul 17, 2025

this makes it possible to use a natural key even while also using surrogate primary keys

https://www.prisma.io/dataguide/datamodeling/correctness-constraints#the-primary-key

Using the surrogate key might be sensible since it is more compact for foreign keys. Also the tag name might be editable. But inside the API it makes no sense to use the surrogate key.

ie. /tags/1 makes no sense. /tags/blue is the much better developer experience.

In order to make use of this a unique key needs to be defined and referenced in the RESTApiHandler Options:

RESTApiHandler({
      ...
      externalIdMapping: {Post: source__post_id},
    });
  }

ie. in the model

model Post {
    id Int @id @default(autoincrement()) @deny('all', true)
    post_id Int
    source String
    title String?
    @@unique([source, post_id], name: "source__post_id")
}

This will result in a compound id in the API based on the fields in the unique index similar to when using a compound @id.

Note as shown in the above example, if the surrogate key should be hidden in the API response, it is possible to do this via @deny('all', true)

Copy link
Contributor

coderabbitai bot commented Jul 17, 2025

📝 Walkthrough
## Walkthrough

This change introduces optional support for external ID mapping in the REST API request handler. It adds an optional `externalIdMapping` option, a new private method for ID field resolution that prioritizes external mappings, and updates all internal usages to leverage the new logic, affecting ID extraction and serialization processes.

## Changes

| Cohort / File(s)                                                                                     | Change Summary                                                                                          |
|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| **REST API Handler: External ID Mapping** <br> `packages/server/src/api/rest/index.ts`               | Adds optional `externalIdMapping` option, introduces private `getIdFields` method with external mapping support, replaces direct global calls with method calls, and normalizes mapping keys. |
| **REST API Handler Tests: External ID Mapping** <br> `packages/server/tests/api/rest.test.ts`        | Adds new test suite verifying REST API behavior with external ID mapping, including compound unique index usage and related entity resolution. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant RequestHandler
    participant ModelMeta

    Client->>RequestHandler: API request with model
    RequestHandler->>RequestHandler: getIdFields(modelMeta, model)
    alt External ID mapping exists
        RequestHandler->>ModelMeta: Lookup unique constraints for external key
        ModelMeta-->>RequestHandler: Return ID fields
    else No external ID mapping
        RequestHandler->>Global: getIdFields(modelMeta, model)
        Global-->>RequestHandler: Return ID fields
    end
    RequestHandler-->>Client: Processed response using resolved ID fields

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Possibly related PRs

Suggested reviewers

  • ymc9

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details open="true">
> <summary>⚡️ Unit Test Generation is now available in beta!</summary>
> 
> Learn more [here](https://docs.coderabbit.ai/finishing-touches/unit-test-generation), or try it out under "Finishing Touches" below.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9f1d3a4a775e189fb17f0e5373cd6774bb28baf4 and bb2012c243200a29471c345279b3cba2efd84b8d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/server/tests/api/rest.test.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/server/tests/api/rest.test.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: build-test (20.x)
* GitHub Check: build-test (20.x)
* GitHub Check: dependency-review
* GitHub Check: build-test (20.x)
* GitHub Check: OSSAR-Scan

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings
<details>
<summary>🧪 Generate unit tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post copyable unit tests in a comment

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=zenstackhq/zenstack&utm_content=2188):

> ‼️ **IMPORTANT**
> Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@lsmith77 lsmith77 changed the title @@externalid support natural keys in the API Jul 17, 2025
@lsmith77 lsmith77 changed the title support natural keys in the API feat: support natural keys in the API Jul 17, 2025
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 (2)
packages/schema/src/res/stdlib.zmodel (1)

741-744: Enhance documentation with usage examples and validation guidance.

The new @@externalId attribute definition is syntactically correct, but the documentation could be more comprehensive to help developers understand its intended usage.

Consider expanding the documentation to include:

  • Usage examples showing how it relates to unique constraints
  • Clarification that the name parameter should match an existing unique constraint name
  • Explanation of how this integrates with the runtime behavior
 /**
- * Specifies an external identifier for the model other than a primary key
+ * Specifies an external identifier for the model other than a primary key.
+ * The name parameter should match an existing unique constraint name that will be used
+ * for external API interactions instead of the primary key.
+ * 
+ * @param name: The name of the unique constraint to use as external identifier
+ * 
+ * @example
+ * model Tag {
+ *   id   Int    @id @default(autoincrement())
+ *   name String @unique
+ *   
+ *   @@externalId("name")
+ * }
  */
 attribute @@externalId(_ name: String)
packages/runtime/src/cross/utils.ts (1)

59-61: Consider adding type safety for attribute argument access.

The current implementation assumes a specific structure for the attribute arguments without type checking.

Consider adding type validation:

-        if (externalId && externalId.args[0] !== undefined) {
-            entries = entries.filter((entry) => entry.name === externalId.args[0].value);
+        if (externalId && externalId.args?.[0]?.value && typeof externalId.args[0].value === 'string') {
+            const externalIdName = externalId.args[0].value as string;
+            entries = entries.filter((entry) => entry.name === externalIdName);

This ensures the external ID name is actually a string before using it for filtering.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 037fc2c and 9c860e8.

📒 Files selected for processing (2)
  • packages/runtime/src/cross/utils.ts (1 hunks)
  • packages/schema/src/res/stdlib.zmodel (1 hunks)

Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

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

Hi @lsmith77 ,

I have two main concerns with introducing the @externalId attribute:

  1. A toplevel attribute ideally should have a meaning from data modeling point of view. From Prisma ORM API point view, one can already address an entity either with an id or a unique field, so such a configuration feels redundant.
  2. The getIdFields helper is called in many places in the code base, and there might be places where it's assumed it returns ID fields ... I'm not sure though, just feeling changing its semantic can be quite dangerous.

Do you feel again this can be a configuration just at the rest api handler (and open api) level? Since its effect is just there.

@lsmith77
Copy link
Contributor Author

I can certainly refactor it to be a Handler option like the model name mapping. That being said, the argument you bring forth to some degree also applies to the @@allow/@@deny annotations, that do not really apply to data modelling but rather to the API only.

As for changing the semantics of getIdFields(). That is very hard for me to ascertain the risks involved given that I still have a very limited overview of the codebase.

Your proposal would rather be we do not call this method but rather receive a list of fields from the Handler config, which is then only checked at runtime against the defined fields (in order to map the model attributes to RDBMS field names to handle if the model used @map)?

I guess this would end up duplicating a fair bit of code which comes with its own set of risks. The advantage is of course it should prevent unwanted side-effects and whatever bugs it introduces would be isolated to this feature. If you feel more comfortable with this approach I am happy to implement it.

@lsmith77
Copy link
Contributor Author

FTR I just tried to quickly implement this but it appears that it is not enough to override all calls to getIdFields() in the Rest Handler. In fact just using GET calls, the above changes seem to have no impact at all:

diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts
index 75196b1a..8c5b4461 100644
--- a/packages/server/src/api/rest/index.ts
+++ b/packages/server/src/api/rest/index.ts
@@ -8,6 +8,7 @@ import {
     clone,
     enumerate,
     getIdFields,
+    requireField,
     isPrismaClientKnownRequestError,
 } from '@zenstackhq/runtime';
 import { lowerCaseFirst, upperCaseFirst, paramCase } from '@zenstackhq/runtime/local-helpers';
@@ -52,6 +53,8 @@ export type Options = {
     urlSegmentCharset?: string;
 
     modelNameMapping?: Record<string, string>;
+
+    externalIdMapping: Record<string, string>;
 };
 
 type RelationshipInfo = {
@@ -238,6 +241,7 @@ class RequestHandler extends APIHandlerBase {
     private urlPatternMap: Record<UrlPatterns, UrlPattern>;
     private modelNameMapping: Record<string, string>;
     private reverseModelNameMapping: Record<string, string>;
+    private externalIdMapping: Record<string, string>;
 
     constructor(private readonly options: Options) {
         super();
@@ -251,6 +255,7 @@ class RequestHandler extends APIHandlerBase {
         this.reverseModelNameMapping = Object.fromEntries(
             Object.entries(this.modelNameMapping).map(([k, v]) => [v, k])
         );
+        this.externalIdMapping = options.externalIdMapping ?? {};
         this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
     }
 
@@ -1166,11 +1171,27 @@ class RequestHandler extends APIHandlerBase {
     }
 
     //#region utilities
+    private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
+        if (!(model in this.externalIdMapping)) {
+            return getIdFields(modelMeta, model);
+        }
+
+        const metaData = modelMeta.models[lowerCaseFirst(model)] ?? {};
+        const uniqueConstraints = metaData.uniqueConstraints ?? {};
+        const externalIdName = this.externalIdMapping[model];
+        for (const [name, constraint] of Object.entries(uniqueConstraints)) {
+            if (name === externalIdName) {
+                return constraint.fields.map((f) => requireField(modelMeta, model, f));
+            }
+        }
+
+        return [];
+    }
 
     private buildTypeMap(logger: LoggerConfig | undefined, modelMeta: ModelMeta): void {
         this.typeMap = {};
         for (const [model, { fields }] of Object.entries(modelMeta.models)) {
-            const idFields = getIdFields(modelMeta, model);
+            const idFields = this.getIdFields(modelMeta, model);
             if (idFields.length === 0) {
                 logWarning(logger, `Not including model ${model} in the API because it has no ID field`);
                 continue;
@@ -1186,7 +1207,7 @@ class RequestHandler extends APIHandlerBase {
                 if (!fieldInfo.isDataModel) {
                     continue;
                 }
-                const fieldTypeIdFields = getIdFields(modelMeta, fieldInfo.type);
+                const fieldTypeIdFields = this.getIdFields(modelMeta, fieldInfo.type);
                 if (fieldTypeIdFields.length === 0) {
                     logWarning(
                         logger,
@@ -1214,7 +1235,7 @@ class RequestHandler extends APIHandlerBase {
         const linkers: Record<string, Linker<any>> = {};
 
         for (const model of Object.keys(modelMeta.models)) {
-            const ids = getIdFields(modelMeta, model);
+            const ids = this.getIdFields(modelMeta, model);
             const mappedModel = this.mapModelName(model);
 
             if (ids.length < 1) {
@@ -1266,7 +1287,7 @@ class RequestHandler extends APIHandlerBase {
                 if (!fieldSerializer) {
                     continue;
                 }
-                const fieldIds = getIdFields(modelMeta, fieldMeta.type);
+                const fieldIds = this.getIdFields(modelMeta, fieldMeta.type);
                 if (fieldIds.length > 0) {
                     const mappedModel = this.mapModelName(model);
 
@@ -1306,7 +1327,7 @@ class RequestHandler extends APIHandlerBase {
         if (!data) {
             return undefined;
         }
-        const ids = getIdFields(modelMeta, model);
+        const ids = this.getIdFields(modelMeta, model);
         if (ids.length === 0) {
             return undefined;
         } else {

So it seems like further investigation is needed where all getIdFields() needs to be overridden to get the desired change in behavior. Which however then might lead to additional complexity introduced in even more places :-/

@ymc9
Copy link
Member

ymc9 commented Jul 20, 2025

Author

Hi @lsmith77 , sorry I didn't put it clearly in my previous comments. What I meant was @@externalId's effect is only at the HTTP API layer (specifically about the REST flavor API). On the contrary, @@allow and @@deny are built-in features at the data access layer. The enhanced PrismaClient recognizes these rules and uses them to enforce access control, regardless the access originates from hand-coded ORM calls or requests to the automatic HTTP CRUD endpoints.

The rules are effective in manually written backend code:

const db = enhance(prisma, { user: getAuthUser() });
// only posts allowed to be read are returned
const posts = await db.post.findMany();

, and when requested from automatic http api:

// requesting "/api/model/post" will only return readable posts
app.use(
    '/api/model',
    ZenStackMiddleware({
        handler: RestApiHandler(...),
        getPrisma: (request) => enhance(prisma, { user: getAuthUser(request) }),
    })
);

@lsmith77
Copy link
Contributor Author

OK thx for this additional explanation.
I am off this week but will look into this further next week but as my above explanation shows, it will require changes beyond the Rest Handler to get this to work. In ligt of this, are you still open to such a change?

@ymc9
Copy link
Member

ymc9 commented Jul 27, 2025

OK thx for this additional explanation. I am off this week but will look into this further next week but as my above explanation shows, it will require changes beyond the Rest Handler to get this to work. In ligt of this, are you still open to such a change?

Yes, an option in the rest handler sounds good to me.

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/server/src/api/rest/index.ts (1)

1179-1195: Good proof of concept implementation with room for future enhancements.

This implementation successfully demonstrates the external ID concept by leveraging existing unique constraints. As mentioned in the PR objectives, future enhancements could include:

  1. Validation at schema compilation time to ensure @@externalId references an existing unique constraint
  2. Storing external ID metadata in the model to avoid runtime lookups
  3. Adding support for composite external IDs if needed

The current approach is clean and appropriate for validating the concept.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35d4d70 and ec07a4a.

📒 Files selected for processing (2)
  • packages/runtime/src/cross/utils.ts (1 hunks)
  • packages/server/src/api/rest/index.ts (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime/src/cross/utils.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: ymc9
PR: zenstackhq/zenstack#2187
File: packages/server/src/api/rest/index.ts:287-289
Timestamp: 2025-07-16T03:09:04.699Z
Learning: In REST API URL pattern matching with model name mapping, only the `type` parameter (representing model names) should be mapped using `modelNameMapping`. The `relationship` parameter represents field names on models and should not be mapped, as field names are distinct from model names in the API structure.
packages/server/src/api/rest/index.ts (1)

Learnt from: ymc9
PR: #2187
File: packages/server/src/api/rest/index.ts:287-289
Timestamp: 2025-07-16T03:09:04.699Z
Learning: In REST API URL pattern matching with model name mapping, only the type parameter (representing model names) should be mapped using modelNameMapping. The relationship parameter represents field names on models and should not be mapped, as field names are distinct from model names in the API structure.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: build-test (20.x)
  • GitHub Check: build-test (20.x)
  • GitHub Check: dependency-review
  • GitHub Check: OSSAR-Scan
  • GitHub Check: build-test (20.x)
🔇 Additional comments (3)
packages/server/src/api/rest/index.ts (3)

57-57: Good architectural choice for the external ID mapping option.

Making externalIdMapping a required option ensures explicit configuration and follows the same pattern as other mapping options in the handler.


259-262: Consistent normalization of external ID mapping.

The normalization follows the same pattern as modelNameMapping, ensuring consistent handling of model names throughout the handler.


1200-1200: All ID field retrievals correctly updated to use the new method.

The consistent replacement of the global getIdFields function with the instance method ensures that external ID mappings are applied uniformly across all operations including type mapping, serialization, and relationship handling.

Also applies to: 1216-1216, 1244-1244, 1296-1296, 1336-1336

@lsmith77 lsmith77 force-pushed the externalid branch 2 times, most recently from 3d5022e to 20410b9 Compare July 30, 2025 11:20
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: 2

♻️ Duplicate comments (1)
packages/server/src/api/rest/index.ts (1)

1179-1195: Improve error handling and fix potential runtime errors

The implementation has several issues that need to be addressed:

  1. Potential undefined access: The code doesn't handle the case where modelMeta.models[modelLower] is undefined before accessing uniqueConstraints.

  2. Inconsistent model name usage: Using model in error messages and requireField while using modelLower for metadata lookup could cause confusion.

Apply this diff to fix these issues:

 private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
     const modelLower = lowerCaseFirst(model);
     if (!(modelLower in this.externalIdMapping)) {
         return getIdFields(modelMeta, model);
     }
 
-    const metaData = modelMeta.models[modelLower] ?? {};
+    const metaData = modelMeta.models[modelLower];
+    if (!metaData) {
+        throw new Error(`Model ${modelLower} not found in metadata`);
+    }
     const externalIdName = this.externalIdMapping[modelLower];
     const uniqueConstraints = metaData.uniqueConstraints ?? {};
+    const availableKeys = Object.keys(uniqueConstraints);
     for (const [name, constraint] of Object.entries(uniqueConstraints)) {
         if (name === externalIdName) {
-            return constraint.fields.map((f) => requireField(modelMeta, model, f));
+            return constraint.fields.map((f) => requireField(modelMeta, modelLower, f));
         }
     }
 
-    throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
+    throw new Error(`Model ${modelLower} does not have unique key "${externalIdName}". Available unique keys: ${availableKeys.join(', ') || 'none'}`);
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec07a4a and 20410b9.

📒 Files selected for processing (1)
  • packages/server/src/api/rest/index.ts (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
packages/server/src/api/rest/index.ts (1)

Learnt from: ymc9
PR: #2187
File: packages/server/src/api/rest/index.ts:287-289
Timestamp: 2025-07-16T03:09:04.699Z
Learning: In REST API URL pattern matching with model name mapping, only the type parameter (representing model names) should be mapped using modelNameMapping. The relationship parameter represents field names on models and should not be mapped, as field names are distinct from model names in the API structure.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build-test (20.x)
  • GitHub Check: build-test (20.x)
  • GitHub Check: build-test (20.x)
  • GitHub Check: dependency-review
🔇 Additional comments (2)
packages/server/src/api/rest/index.ts (2)

244-244: LGTM!

The initialization correctly handles the optional mapping and normalizes keys consistently with the model name mapping pattern.

Also applies to: 259-263


1200-1200: Consistent application of external ID mapping

All calls to retrieve ID fields have been correctly updated to use the new instance method, ensuring external ID mapping is applied uniformly throughout the handler.

Also applies to: 1216-1216, 1244-1244, 1296-1296, 1336-1336

Comment on lines 1179 to 1200
private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
const modelLower = lowerCaseFirst(model);
if (!(modelLower in this.externalIdMapping)) {
return getIdFields(modelMeta, model);
}

const metaData = modelMeta.models[modelLower] ?? {};
const externalIdName = this.externalIdMapping[modelLower];
const uniqueConstraints = metaData.uniqueConstraints ?? {};
for (const [name, constraint] of Object.entries(uniqueConstraints)) {
if (name === externalIdName) {
return constraint.fields.map((f) => requireField(modelMeta, model, f));
}
}

throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Document the implications of external ID mapping

This feature creates a separation between API-level IDs (natural keys) and database-level IDs (surrogate keys). This could lead to confusion when debugging or writing custom backend code.

Consider:

  1. Adding comprehensive documentation about this behavior
  2. Providing utility functions to convert between external and internal IDs
  3. Ensuring error messages clearly indicate which type of ID is expected

To verify potential issues with ID mismatches, let's check if there are any direct ID references in error messages or logging:


🏁 Script executed:

#!/bin/bash
# Search for places where IDs are used in error messages or logging
rg -A 3 -B 3 "resourceId|item\.id|\.id\b" packages/server/src/api/rest/index.ts | grep -E "(error|Error|log|Log|throw|makeError)"

# Check if there are any other files that might be affected by this change
fd -e ts -e js . packages/server/src | xargs rg "getIdFields.*modelMeta"

Length of output: 1198


Document and Enhance External ID Mapping Support

The REST handler now accepts natural keys at the API boundary while Prisma continues to use surrogate IDs, which can be confusing when diving into database operations or writing custom backend logic. Let’s make this explicit and provide tooling around it.

• Add documentation (e.g. in docs/api.md or inline JSDoc) noting that
– API endpoints use “external”/natural IDs
– all Prisma calls still use the internal surrogate key
• Create utility functions (e.g. toExternalId(id: number): string, toInternalId(natural: string): number) and export them from packages/server/src/api/rest/utils.ts
• In packages/server/src/api/rest/index.ts (around getIdFields):
– Update thrown errors to specify “external ID” vs. “internal ID”
– Add a brief comment above getIdFields summarizing its role in mapping between ID types

This will ensure maintainers and integrators understand the dual-ID model, avoid debugging confusion, and have ready-to-use conversion helpers.

🤖 Prompt for AI Agents
In packages/server/src/api/rest/index.ts around lines 1179 to 1195, add a brief
JSDoc comment above the getIdFields method explaining that it maps between
external (natural) IDs used at the API boundary and internal surrogate IDs used
by Prisma. Update the error message thrown to clearly distinguish that the
missing key is an external ID, not an internal one. Additionally, create and
export utility functions named toExternalId and toInternalId in
packages/server/src/api/rest/utils.ts to convert between internal numeric IDs
and external string IDs. Finally, add documentation in docs/api.md or inline
noting that API endpoints use external IDs while Prisma uses internal surrogate
keys to clarify this dual-ID model for maintainers and integrators.

…T API

this makes it possible to use a natural key even while also using surrogate primary keys
@lsmith77 lsmith77 force-pushed the externalid branch 2 times, most recently from a682d40 to 3b4a931 Compare July 30, 2025 11:50
@lsmith77
Copy link
Contributor Author

@ymc9 ok got it to work. the issue came from model sometimes not having lowerCaseFirst applied already.

the second commit is not necessary but felt like a sensible optimization but I am happy to revert if you prefer.

@lsmith77
Copy link
Contributor Author

lsmith77 commented Jul 30, 2025

I will now look into updating the openapi plugin so that filter[id] sets the appropriate string type where necessary. Or should I rather just hide this filter?

Actually upon further investigation it appears that right now you just list all the compound id fields as separate filters, so I guess there isn't really anything to do here.

We will likely just add our own logic to remove filter[id] entirely since we do not want people to be able to interact with the surrogate key in our API. But I guess this will remain custom code.

@lsmith77
Copy link
Contributor Author

Hmm we would however like to not include the id in the output. But I cannot use @ignore on the field unless we change all our foreign keys to use the natural keys as well, which we obviously do not want to do.

What would you advise here? How could be prevent exposing id while still being able to use it as a surrogate key in the model?

model Project {
    id                       Int                   @id @default(autoincrement())
    ..
image

@lsmith77
Copy link
Contributor Author

ok found a solution using @deny:

id                       Int                   @id @default(autoincrement()) @deny('all', true)

@lsmith77 lsmith77 requested a review from ymc9 July 31, 2025 10:03
@lsmith77
Copy link
Contributor Author

lsmith77 commented Aug 4, 2025

@ymc9 could you have another look if this is now the right direction and what is left to do to get this merged?

private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
const modelLower = lowerCaseFirst(model);

if (this.typeMap && modelLower in this.typeMap) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a performance optimization but not sure if its really needed

Copy link
Member

Choose a reason for hiding this comment

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

I believe after typeMap is built, every model with proper id fields will have an entry in it. Will this code block then completely mute the "externalIdMapping" check below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that was the idea as my assumption was that there is no way things could change at run time.

private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
const modelLower = lowerCaseFirst(model);

if (this.typeMap && modelLower in this.typeMap) {
Copy link
Member

Choose a reason for hiding this comment

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

I believe after typeMap is built, every model with proper id fields will have an entry in it. Will this code block then completely mute the "externalIdMapping" check below?

const metaData = modelMeta.models[modelLower] ?? {};
const externalIdName = this.externalIdMapping[modelLower];
const uniqueConstraints = metaData.uniqueConstraints ?? {};
for (const [name, constraint] of Object.entries(uniqueConstraints)) {
Copy link
Member

Choose a reason for hiding this comment

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

There's some complexity here due to how Prisma handles multi-field id fields or unique fields.

If a model has id like:

model Model {
  ...
  @@id([x, y])
}

The model meta's uniqueConstraint for this model will have an entry like:

{
  ...
  x_y: { name: 'x_y', fields: ['x', 'y']
}

, and when querying, the synthetic compound field "x_y" must be used as a unique filter.

db.model.findUnique({ where: { x_y: { x: 1, y: 2 } });

For this new feature, do you plan to support mapping multi-field id/unique fields? That'll result in some more complexity. However, if not, we can have a check in the loop and only match unique constraint that has a single field.

I don't personally see too much value to support multi field.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For @id fields, this feature is not needed, since the current code already handles this. so I am only supporting model (not field level) @unique. I have added an example at the very top of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, there is still an issue here.

If I map the unique index name to something arbitrary, it doesn't work as the current code calling findUnqiue() apparently always then uses the fieldA_fieldB in the where rather than the unique index name, causing an error.

But digging into the source code I found that this is a known limitation:
https://github.com/zenstackhq/zenstack/blob/main/packages/server/src/api/rest/index.ts#L1415

@ymc9
Copy link
Member

ymc9 commented Aug 6, 2025

Hi @lsmith77 , I've left a few review comments. Please take a look when you have time. Thanks!

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: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b4a931 and 5a3a17c.

📒 Files selected for processing (2)
  • packages/server/src/api/rest/index.ts (9 hunks)
  • packages/server/tests/api/rest.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/api/rest/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: build-test (20.x)
  • GitHub Check: OSSAR-Scan
  • GitHub Check: build-test (20.x)
  • GitHub Check: build-test (20.x)
  • GitHub Check: dependency-review

Comment on lines 3149 to 3195
it('works with id mapping', async () => {
await prisma.user.create({
data: { id: 1, name: 'User1', email: 'user1@abc.com' },
});

// user is no longer exposed using the `id` field
let r = await handler({
method: 'get',
path: '/user/1',
query: {},
prisma,
});

expect(r.status).toBe(404);

// user is exposed using the fields from the `name__email` multi-column unique index
r = await handler({
method: 'get',
path: '/user/User1_user1@abc.com',
query: {},
prisma,
});

expect(r.status).toBe(200);
expect(r.body.data.attributes.email).toBe('user1@abc.com');
expect(r.body.data.attributes.name).toBe('User1');

await prisma.post.create({
data: { id: 1, title: 'Title1', authorId: 1 },
});

// post is exposed using the `id` field
r = await handler({
method: 'get',
path: '/post/1',
query: {},
prisma,
});

expect(r.status).toBe(200);
expect(r.body.data.attributes.title).toBe('Title1');
expect(r.body.data.attributes.author.name).toBe('User1');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for external ID mapping feature.

The current test only covers GET operations. To ensure comprehensive testing of the external ID mapping feature, consider adding tests for:

  1. POST operations with relationships using external IDs
  2. PUT operations on resources with external IDs
  3. DELETE operations on resources with external IDs
  4. Relationship operations (POST, PATCH, DELETE on relationships)
  5. Error scenarios (malformed external IDs, non-existent resources)

Example additional test cases:

// Test POST with external ID relationship
it('creates post with external ID relationship', async () => {
    await prisma.user.create({
        data: { id: '1', name: 'User1', email: 'user1@abc.com' },
    });

    const r = await handler({
        method: 'post',
        path: '/post',
        requestBody: {
            data: {
                type: 'post',
                attributes: { title: 'New Post' },
                relationships: {
                    author: { data: { type: 'user', id: 'User1_user1@abc.com' } }
                }
            }
        },
        prisma,
    });

    expect(r.status).toBe(201);
});

// Test PUT with external ID
it('updates user using external ID', async () => {
    await prisma.user.create({
        data: { id: '1', name: 'User1', email: 'user1@abc.com' },
    });

    const r = await handler({
        method: 'put',
        path: '/user/User1_user1@abc.com',
        requestBody: {
            data: {
                type: 'user',
                attributes: { name: 'Updated User' }
            }
        },
        prisma,
    });

    expect(r.status).toBe(200);
    expect(r.body.data.attributes.name).toBe('Updated User');
});
🤖 Prompt for AI Agents
In packages/server/tests/api/rest.test.ts around lines 3149 to 3191, the
existing test only covers GET requests for external ID mapping. To improve
coverage, add tests for POST, PUT, DELETE methods using external IDs, including
creating resources with relationships via external IDs, updating resources by
external ID, deleting resources by external ID, relationship operations (POST,
PATCH, DELETE), and error cases like malformed or non-existent external IDs.
Implement these tests by simulating requests with appropriate method, path, and
requestBody fields referencing external IDs, and assert expected status codes
and response data.

@lsmith77 lsmith77 force-pushed the externalid branch 2 times, most recently from 7a409b2 to 9f1d3a4 Compare August 6, 2025 11:34
@lsmith77 lsmith77 requested a review from ymc9 August 6, 2025 11:46
prisma,
});

expect(r.status).toBe(400);
Copy link
Contributor Author

@lsmith77 lsmith77 Aug 6, 2025

Choose a reason for hiding this comment

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

this here imho should be a 404 but it causes a 400 due to "detail": "\nInvalid prisma.post.findUnique() invocation as it expects a _ separated string and therefore lacks the number of parameters for this method call.

that being said, the same happens when using the existing compound id feature. so this issue be addressed in another PR, though I am not entirely sure where. if you give me a hint I can look into this in another PR.

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