From bb86490b6ce5e9180da056b22912b2e65acd1c10 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 04:29:49 +0000 Subject: [PATCH 1/6] Add support for generated identifier format strings (ID prefixing). --- packages/language/res/stdlib.zmodel | 10 +++---- .../orm/src/client/crud/operations/base.ts | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index bbeafb07..e4b93d31 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -83,25 +83,25 @@ function now(): DateTime { /** * Generates a globally unique identifier based on the UUID specs. */ -function uuid(version: Int?): String { +function uuid(version: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates a globally unique identifier based on the CUID spec. */ -function cuid(version: Int?): String { +function cuid(version: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates an identifier based on the nanoid spec. */ -function nanoid(length: Int?): String { +function nanoid(length: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates an identifier based on the ulid spec. */ -function ulid(): String { +function ulid(format: String?): String { } @@@expressionContext([DefaultValue]) /** @@ -356,7 +356,7 @@ enum SortOrder { attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?, type: IndexType?) @@@prisma /** - * Defines meta information about the relation. + * Defines meta information about the F. * * @param name: Sometimes (e.g. to disambiguate a relation) Defines the name of the relationship. In an m-n-relation, it also determines the name of the underlying relation table. * @param fields: A list of fields of the current model diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 0ac2fe8c..db833c04 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -860,22 +860,22 @@ export abstract class BaseOperationHandler { private evalGenerator(defaultValue: Expression) { if (ExpressionUtils.isCall(defaultValue)) { return match(defaultValue.function) - .with('cuid', () => createId()) + .with('cuid', () => this.formatGeneratedValue(createId(), defaultValue.args?.[1])) .with('uuid', () => defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args?.[0]) && - defaultValue.args[0].value === 7 + this.formatGeneratedValue(defaultValue.args[0].value === 7 ? uuid.v7() - : uuid.v4(), + : uuid.v4(), defaultValue.args?.[1]), ) .with('nanoid', () => defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) && - typeof defaultValue.args[0].value === 'number' + this.formatGeneratedValue(typeof defaultValue.args[0].value === 'number' ? nanoid(defaultValue.args[0].value) - : nanoid(), + : nanoid(), defaultValue.args?.[1]), ) - .with('ulid', () => ulid()) + .with('ulid', () => this.formatGeneratedValue(ulid(), defaultValue.args?.[0])) .otherwise(() => undefined); } else if ( ExpressionUtils.isMember(defaultValue) && @@ -893,6 +893,24 @@ export abstract class BaseOperationHandler { } } + private formatGeneratedValue(generated?: string, expr?: Expression) { + if (!generated) { + return undefined; + } + + if (!expr || !ExpressionUtils.isLiteral(expr)) { + return generated; + } + + const format = expr.value; + + invariant(typeof format === 'string', 'generated identifier format value must be a string') + invariant(format.includes('%s'), 'generated identifier format strings must include "%s"'); + + // replaceAll instead? Does anyone even need that...? + return format.replace('%s', generated); + } + protected async update( kysely: AnyKysely, model: string, From f199feead13d21c78a46e7493b0257c84cb9c243 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 04:30:00 +0000 Subject: [PATCH 2/6] Add tests. --- .../generated-id-format-strings.test.ts | 1167 +++++++++++++++++ 1 file changed, 1167 insertions(+) create mode 100644 tests/e2e/orm/client-api/generated-id-format-strings.test.ts diff --git a/tests/e2e/orm/client-api/generated-id-format-strings.test.ts b/tests/e2e/orm/client-api/generated-id-format-strings.test.ts new file mode 100644 index 00000000..6a95005a --- /dev/null +++ b/tests/e2e/orm/client-api/generated-id-format-strings.test.ts @@ -0,0 +1,1167 @@ +import { ZenStackClient } from '@zenstackhq/orm'; +import { type SchemaDef } from '@zenstackhq/orm/schema'; +import SQLite from 'better-sqlite3'; +import { SqliteDialect } from 'kysely'; +import { describe, expect, it } from 'vitest'; + +const schema = { + provider: { + type: 'sqlite' + }, + models: { + User: { + name: 'User', + fields: { + id: { + name: 'id', + type: 'Int', + id: true, + attributes: [ + { + name: '@id' + } + ] + }, + uuid: { + name: 'uuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'user_uuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'user_uuid_%s' + } + ] + } + }, + uuid7: { + name: 'uuid7', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'user_uuid7_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'user_uuid7_%s' + } + ] + } + }, + cuid: { + name: 'cuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'user_cuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'user_cuid_%s' + } + ] + } + }, + cuid2: { + name: 'cuid2', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'user_cuid2_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'user_cuid2_%s' + } + ] + } + }, + nanoid: { + name: 'nanoid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'user_nanoid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'user_nanoid_%s' + } + ] + } + }, + nanoid8: { + name: 'nanoid8', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'user_nanoid8_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'user_nanoid8_%s' + } + ] + } + }, + ulid: { + name: 'ulid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'user_ulid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'user_ulid_%s' + } + ] + } + }, + posts: { + name: 'posts', + type: 'Post', + array: true, + relation: { + opposite: 'user' + } + } + }, + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'Int' + } + } + }, + Post: { + name: 'Post', + fields: { + id: { + name: 'id', + type: 'Int', + id: true, + attributes: [ + { + name: '@id' + } + ] + }, + uuid: { + name: 'uuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'post_uuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'post_uuid_%s' + } + ] + } + }, + uuid7: { + name: 'uuid7', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'post_uuid7_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'post_uuid7_%s' + } + ] + } + }, + cuid: { + name: 'cuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'post_cuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'post_cuid_%s' + } + ] + } + }, + cuid2: { + name: 'cuid2', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'post_cuid2_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'post_cuid2_%s' + } + ] + } + }, + nanoid: { + name: 'nanoid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'post_nanoid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'post_nanoid_%s' + } + ] + } + }, + nanoid8: { + name: 'nanoid8', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'post_nanoid8_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'post_nanoid8_%s' + } + ] + } + }, + ulid: { + name: 'ulid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'post_ulid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'post_ulid_%s' + } + ] + } + }, + userId: { + name: 'userId', + type: 'Int', + foreignKeyFor: [ + 'user' + ] + }, + user: { + name: 'user', + type: 'User', + attributes: [ + { + name: '@relation', + args: [ + { + name: 'fields', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'userId' + } + ] + } + }, + { + name: 'references', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'id' + } + ] + } + } + ] + } + ], + relation: { + opposite: 'posts', + fields: [ + 'userId' + ], + references: [ + 'id' + ] + } + }, + comments: { + name: 'comments', + type: 'Comment', + array: true, + relation: { + opposite: 'post' + } + } + }, + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'Int' + } + } + }, + Comment: { + name: 'Comment', + fields: { + id: { + name: 'id', + type: 'Int', + id: true, + attributes: [ + { + name: '@id' + } + ] + }, + uuid: { + name: 'uuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'comment_uuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 4 + }, + { + kind: 'literal', + value: 'comment_uuid_%s' + } + ] + } + }, + uuid7: { + name: 'uuid7', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'comment_uuid7_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid', + args: [ + { + kind: 'literal', + value: 7 + }, + { + kind: 'literal', + value: 'comment_uuid7_%s' + } + ] + } + }, + cuid: { + name: 'cuid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'comment_cuid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'comment_cuid_%s' + } + ] + } + }, + cuid2: { + name: 'cuid2', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'comment_cuid2_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid', + args: [ + { + kind: 'literal', + value: 2 + }, + { + kind: 'literal', + value: 'comment_cuid2_%s' + } + ] + } + }, + nanoid: { + name: 'nanoid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'comment_nanoid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 21 + }, + { + kind: 'literal', + value: 'comment_nanoid_%s' + } + ] + } + }, + nanoid8: { + name: 'nanoid8', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'comment_nanoid8_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'nanoid', + args: [ + { + kind: 'literal', + value: 8 + }, + { + kind: 'literal', + value: 'comment_nanoid8_%s' + } + ] + } + }, + ulid: { + name: 'ulid', + type: 'String', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'comment_ulid_%s' + } + ] + } + } + ] + } + ], + default: { + kind: 'call', + function: 'ulid', + args: [ + { + kind: 'literal', + value: 'comment_ulid_%s' + } + ] + } + }, + postId: { + name: 'postId', + type: 'Int', + foreignKeyFor: [ + 'post' + ] + }, + post: { + name: 'post', + type: 'Post', + attributes: [ + { + name: '@relation', + args: [ + { + name: 'fields', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'postId' + } + ] + } + }, + { + name: 'references', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'id' + } + ] + } + } + ] + } + ], + relation: { + opposite: 'comments', + fields: [ + 'postId' + ], + references: [ + 'id' + ] + } + } + }, + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'Int' + } + } + } + }, + authType: 'User', + plugins: {} +} as const satisfies SchemaDef; + +describe('generated id format strings', () => { + it('supports top-level generated id format strings', async () => { + const client = new ZenStackClient(schema, { + dialect: new SqliteDialect({ database: new SQLite(':memory:') }), + }); + await client.$pushSchema(); + + const user = await client.user.create({ + data: { + id: 1, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + }); + + it('supports nested generated id format strings', async () => { + const client = new ZenStackClient(schema, { + dialect: new SqliteDialect({ database: new SQLite(':memory:') }), + }); + await client.$pushSchema(); + + const user = await client.user.create({ + data: { + id: 1, + + posts: { + create: { + id: 1, + }, + }, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + + const post = await client.post.findUniqueOrThrow({ where: { id: 1 } }); + expect(post.uuid).toMatch(/^post_uuid_/); + expect(post.uuid7).toMatch(/^post_uuid7_/); + expect(post.cuid).toMatch(/^post_cuid_/); + expect(post.cuid2).toMatch(/^post_cuid2_/); + expect(post.nanoid).toMatch(/^post_nanoid_/); + expect(post.nanoid8).toMatch(/^post_nanoid8_/); + expect(post.ulid).toMatch(/^post_ulid_/); + }); + + it('supports deeply nested generated id format strings', async () => { + const client = new ZenStackClient(schema, { + dialect: new SqliteDialect({ database: new SQLite(':memory:') }), + }); + await client.$pushSchema(); + + const user = await client.user.create({ + data: { + id: 1, + + posts: { + create: { + id: 1, + + comments: { + create: { + id: 1, + }, + }, + }, + }, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + + const post = await client.post.findUniqueOrThrow({ where: { id: 1 } }); + expect(post.uuid).toMatch(/^post_uuid_/); + expect(post.uuid7).toMatch(/^post_uuid7_/); + expect(post.cuid).toMatch(/^post_cuid_/); + expect(post.cuid2).toMatch(/^post_cuid2_/); + expect(post.nanoid).toMatch(/^post_nanoid_/); + expect(post.nanoid8).toMatch(/^post_nanoid8_/); + expect(post.ulid).toMatch(/^post_ulid_/); + + const comment = await client.comment.findUniqueOrThrow({ where: { id: 1 } }); + expect(comment.uuid).toMatch(/^comment_uuid_/); + expect(comment.uuid7).toMatch(/^comment_uuid7_/); + expect(comment.cuid).toMatch(/^comment_cuid_/); + expect(comment.cuid2).toMatch(/^comment_cuid2_/); + expect(comment.nanoid).toMatch(/^comment_nanoid_/); + expect(comment.nanoid8).toMatch(/^comment_nanoid8_/); + expect(comment.ulid).toMatch(/^comment_ulid_/); + }); +}); \ No newline at end of file From afe1e7d3675783c50cc81b5d0fd3c78f2b9c0a6d Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 04:31:57 +0000 Subject: [PATCH 3/6] Add missing semicolon. --- packages/orm/src/client/crud/operations/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index db833c04..487290e3 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -904,7 +904,7 @@ export abstract class BaseOperationHandler { const format = expr.value; - invariant(typeof format === 'string', 'generated identifier format value must be a string') + invariant(typeof format === 'string', 'generated identifier format value must be a string'); invariant(format.includes('%s'), 'generated identifier format strings must include "%s"'); // replaceAll instead? Does anyone even need that...? From 7b3f9f4a8e095d12147faaa9f563daf72a148dce Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 05:54:55 +0000 Subject: [PATCH 4/6] Cleanup logic. --- .../orm/src/client/crud/operations/base.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 487290e3..4a30317c 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -861,20 +861,32 @@ export abstract class BaseOperationHandler { if (ExpressionUtils.isCall(defaultValue)) { return match(defaultValue.function) .with('cuid', () => this.formatGeneratedValue(createId(), defaultValue.args?.[1])) - .with('uuid', () => - defaultValue.args?.[0] && - ExpressionUtils.isLiteral(defaultValue.args?.[0]) && - this.formatGeneratedValue(defaultValue.args[0].value === 7 + .with('uuid', () => { + const version = defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) + ? defaultValue.args[0].value + : undefined; + + const generated = version === 7 ? uuid.v7() - : uuid.v4(), defaultValue.args?.[1]), - ) - .with('nanoid', () => - defaultValue.args?.[0] && - ExpressionUtils.isLiteral(defaultValue.args[0]) && - this.formatGeneratedValue(typeof defaultValue.args[0].value === 'number' - ? nanoid(defaultValue.args[0].value) - : nanoid(), defaultValue.args?.[1]), - ) + : uuid.v4(); + + const format = defaultValue.args?.[1]; + + return this.formatGeneratedValue(generated, format); + }) + .with('nanoid', () => { + const length = defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) + ? defaultValue.args[0].value + : undefined; + + const generated = typeof length === 'number' + ? nanoid(length) + : nanoid(); + + const format = defaultValue.args?.[1]; + + return this.formatGeneratedValue(generated, format); + }) .with('ulid', () => this.formatGeneratedValue(ulid(), defaultValue.args?.[0])) .otherwise(() => undefined); } else if ( @@ -893,11 +905,7 @@ export abstract class BaseOperationHandler { } } - private formatGeneratedValue(generated?: string, expr?: Expression) { - if (!generated) { - return undefined; - } - + private formatGeneratedValue(generated: string, expr?: Expression) { if (!expr || !ExpressionUtils.isLiteral(expr)) { return generated; } From 6ab3c72e61e36943b02b79f013c91f328508390a Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 06:30:32 +0000 Subject: [PATCH 5/6] Fix typo. --- packages/language/res/stdlib.zmodel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index e4b93d31..81d52dc9 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -356,7 +356,7 @@ enum SortOrder { attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?, type: IndexType?) @@@prisma /** - * Defines meta information about the F. + * Defines meta information about the relation. * * @param name: Sometimes (e.g. to disambiguate a relation) Defines the name of the relationship. In an m-n-relation, it also determines the name of the underlying relation table. * @param fields: A list of fields of the current model From 37c3182307e0b9d4cadd0615e770aa637d978173 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Wed, 17 Dec 2025 06:40:03 +0000 Subject: [PATCH 6/6] Use `replaceAll` instead. --- packages/orm/src/client/crud/operations/base.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 4a30317c..708a33f9 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -915,8 +915,7 @@ export abstract class BaseOperationHandler { invariant(typeof format === 'string', 'generated identifier format value must be a string'); invariant(format.includes('%s'), 'generated identifier format strings must include "%s"'); - // replaceAll instead? Does anyone even need that...? - return format.replace('%s', generated); + return format.replaceAll('%s', generated); } protected async update(