Skip to content

Conversation

@sanny-io
Copy link
Contributor

@sanny-io sanny-io commented Feb 12, 2026

Allows developers to specify a custom implementation of an ID generation function.

Sample Usage

model User {
    id String @id @default(customId()) // length chosen by implementation
}

model Post {
    id String @id @default(customId(8)) // length chosen by schema
}
export const db = new ZenStackClient(schema, {
    dialect: new SqliteDialect({
        database: new SQLite(':memory:'),
    }),

    customId: ({ model, length }) => {
        return ...
    },
});

Closes #2348
Prisma issues: prisma/prisma#6719

Summary by CodeRabbit

  • New Features

    • Configurable custom ID generation via a client-supplied function, with optional length.
    • Default ID generation now receives per-entity and per-field context so IDs can vary by entity/field.
    • Runtime checks ensure a provided length is a positive number and generated IDs are non-empty strings.
  • Tests

    • Added unit tests for length validation.
    • Added end-to-end tests covering custom ID outputs, nested creations, and missing/invalid implementation cases.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new default-value generator customId(length: Int?): language declaration and validation, ORM client option and types for a user-provided generator, wiring of model+field context into default-value evaluation, and end-to-end tests covering success and failure scenarios.

Changes

Cohort / File(s) Summary
Language Layer
packages/language/res/stdlib.zmodel, packages/language/src/validators/function-invocation-validator.ts, packages/language/test/function-invocation.test.ts
Declared public customId(length: Int?) with @@@expressionContext([DefaultValue]). Added validator for customId to ensure optional length is a positive number. Added tests asserting non‑positive lengths are rejected.
ORM Client — Types & Options
packages/orm/src/client/options.ts
Exported CustomIdFunctionContext<Schema> and CustomIdFunction<Schema> types. Extended ClientOptions<Schema> with optional customId?: CustomIdFunction<Schema> and inline docs describing usage with @default(customId()).
ORM Client — Default Generation Logic
packages/orm/src/client/crud/operations/base.ts
Updated BaseOperationHandler.evalGenerator signature to accept (defaultValue, model, field). Implemented customId generator handling: requires client.options.customId, calls it with { client, model, field, length? }, and enforces a non-empty string return. Updated call sites to pass model and field.
End-to-end Tests
tests/e2e/orm/client-api/custom-id.test.ts
Added E2E tests covering default IDs, custom length, nested/deep create behavior, and failure cases when customId is missing or returns invalid values (undefined, empty, non-string).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble code and weave a name,
For models and fields I stake my claim.
Give length or none, I hop and make,
A tiny string for records' sake.
Sniff, stamp, bounce — your IDs awake!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR addresses issue #2348 by enabling custom ID generation, but implements a generic customId function rather than the requested nanoid custom alphabet feature. Clarify whether the customId function fully satisfies issue #2348's requirements or if nanoid custom alphabet support should be added as a follow-up.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: customId function' is clear and directly related to the main change: adding a new customId function for custom ID generation.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the customId function: stdlib declaration, validation, tests, type definitions, and runtime implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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

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

🤖 Fix all issues with AI agents
In `@tests/e2e/orm/client-api/custom-id.test.ts`:
- Around line 140-160: Remove the unnecessary `@ts-expect-error` comments that
precede the assertion lines invoking client.user.create in the test (these are
the three occurrences after each createTestClient call where you do await
expect(client.user.create({ data: {} })).rejects.toThrowError(...)); leave the
valid `@ts-expect-error` comments that suppress the type errors on the customId
function arguments intact, and run typecheck to confirm no other TS errors are
introduced.
🧹 Nitpick comments (3)
packages/language/src/validators/function-invocation-validator.ts (1)

240-252: Duplicated logic with _checkNanoid.

_checkCustomId is identical to _checkNanoid (lines 226–238). Consider extracting a shared helper like _checkPositiveFirstArg to avoid the duplication.

♻️ Proposed refactor
+    private _checkPositiveFirstArg(expr: InvocationExpr, accept: ValidationAcceptor) {
+        const lengthArg = expr.args[0]?.value;
+        if (lengthArg) {
+            const length = getLiteral<number>(lengthArg);
+            if (length !== undefined && length <= 0) {
+                accept('error', 'first argument must be a positive number', {
+                    node: expr.args[0]!,
+                });
+            }
+        }
+    }
+
     `@func`('nanoid')
     private _checkNanoid(expr: InvocationExpr, accept: ValidationAcceptor) {
-        // first argument must be positive if provided
-        const lengthArg = expr.args[0]?.value;
-        if (lengthArg) {
-            const length = getLiteral<number>(lengthArg);
-            if (length !== undefined && length <= 0) {
-                accept('error', 'first argument must be a positive number', {
-                    node: expr.args[0]!,
-                });
-            }
-        }
+        this._checkPositiveFirstArg(expr, accept);
     }
 
     `@func`('customId')
     private _checkCustomId(expr: InvocationExpr, accept: ValidationAcceptor) {
-        // first argument must be positive if provided
-        const lengthArg = expr.args[0]?.value;
-        if (lengthArg) {
-            const length = getLiteral<number>(lengthArg);
-            if (length !== undefined && length <= 0) {
-                accept('error', 'first argument must be a positive number', {
-                    node: expr.args[0]!,
-                });
-            }
-        }
+        this._checkPositiveFirstArg(expr, accept);
     }
packages/language/test/function-invocation.test.ts (1)

418-434: Test coverage is incomplete compared to the nanoid() suite.

The nanoid() length validation tests (lines 362–416) cover both positive acceptance and rejection of 0 and -1. This suite only checks -1. Consider adding a positive-length acceptance test and a customId(0) rejection test for parity.

tests/e2e/orm/client-api/custom-id.test.ts (1)

24-24: Unnecessary async on describe callback.

The describe block doesn't use top-level await, so the async modifier is unnecessary and could be removed.

@sanny-io
Copy link
Contributor Author

@ymc9 only non-empty strings are allowed at the moment. Should the restrictions be relaxed, or maybe tightened?

Not sure if, for example, a string full of only whitespaces could cause issues.

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

🤖 Fix all issues with AI agents
In `@packages/orm/src/client/crud/operations/base.ts`:
- Around line 1098-1109: The current use of invariant in the customId generation
path (the with('customId', ...) block that calls this.client.$options.customId)
loses error messages in production; replace those invariant calls with a
preserved-message error constructor (e.g., call createInvalidInputError or
createInternalError) so users see meaningful messages if customId is missing or
returns a non-string. Specifically, where the code currently does
invariant(this.client.$options.customId, ...) and invariant(generated && typeof
generated === 'string', ...), throw a new preserved-message error (using
createInvalidInputError/createInternalError) with the same descriptive text and
include context (model, field, length) as appropriate to aid debugging.
- Line 1107: The validation for generated custom IDs in base.ts currently only
checks `generated && typeof generated === 'string'`, which permits
whitespace-only strings; update the check used around the invariant (the
`generated` validation for "customId") to ensure the string is not all
whitespace by using a trimmed-length test (e.g., verify `typeof generated ===
'string' && generated.trim().length > 0`) before calling invariant, and if
whitespace-only IDs are intentionally allowed, add a clear comment documenting
that exception near the `customId` handling.
🧹 Nitpick comments (2)
packages/orm/src/client/options.ts (2)

43-65: model could use GetModels<Schema> for stronger typing.

ZModelFunctionContext on line 24 types model as GetModels<Schema>, but CustomIdFunctionContext uses a plain string. Using GetModels<Schema> would give consumers autocomplete and type checking on the model name.

Also, minor grammar nit: lines 50 and 55 say "for which the ID should be generated for" — the trailing "for" is redundant.

Proposed fix
 export type CustomIdFunctionContext<Schema extends SchemaDef> = {
     /**
      * ZenStack client instance.
      */
     client: ClientContract<Schema>;
 
     /**
-     * The model for which the ID should be generated for.
+     * The model for which the ID should be generated.
      */
-    model: string;
+    model: GetModels<Schema>;
 
     /**
-     * The field for which the ID should be generated for.
+     * The field for which the ID should be generated.
      */
     field: string;
 
     /**
      * The length of the ID as requested by the schema.
      */
     length?: number;
 };

65-65: CustomIdFunction is synchronous — async ID generation is unsupported.

Returning string (not string | Promise<string>) prevents users from generating IDs via async sources (e.g., a remote ID service, DB sequence lookup). Since fillGeneratedAndDefaultValues and evalGenerator in base.ts are both synchronous, this would need broader changes to support, but it's worth flagging as a potential limitation consumers may hit.

If you don't plan to support async generation now, consider documenting this limitation in the JSDoc. If you do want to support it, evalGenerator and fillGeneratedAndDefaultValues would need to become async.

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.

nanoid support custom alphabet

1 participant