From 99ee73a5bc4d515cf3bf254f01f2faf52043bcd8 Mon Sep 17 00:00:00 2001 From: FTB_lag Date: Sun, 28 Dec 2025 19:35:25 +0300 Subject: [PATCH 1/5] feat(orm): support where enum --- .../orm/src/client/executor/name-mapper.ts | 38 ++++++++++- tests/e2e/orm/client-api/name-mapping.test.ts | 65 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 68a10490..9c1c8e01 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -1,6 +1,7 @@ import { invariant } from '@zenstackhq/common-helpers'; import { AliasNode, + BinaryOperationNode, CaseWhenBuilder, ColumnNode, ColumnUpdateNode, @@ -10,6 +11,7 @@ import { FromNode, IdentifierNode, InsertQueryNode, + type OperationNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, @@ -17,13 +19,13 @@ import { SelectAllNode, SelectionNode, SelectQueryNode, + type SimpleReferenceExpressionNode, TableNode, UpdateQueryNode, ValueListNode, ValueNode, ValuesNode, - type OperationNode, - type SimpleReferenceExpressionNode, + WhereNode, } from 'kysely'; import type { EnumDef, EnumField, FieldDef, ModelDef, SchemaDef } from '../../schema'; import { @@ -183,6 +185,38 @@ export class QueryNameMapper extends OperationNodeTransformer { return ColumnNode.create(mappedName); } + protected override transformWhere(node: WhereNode): WhereNode { + if ( + BinaryOperationNode.is(node.where) && + ReferenceNode.is(node.where.leftOperand) && + ColumnNode.is(node.where.leftOperand.column) && + node.where.leftOperand.table && + TableNode.is(node.where.leftOperand.table) && + // where: { enumColumn: Enum.VALUE } + (ValueNode.is(node.where.rightOperand) || + // where: { enumColumn: { in: [Enum.VALUE] } } + PrimitiveValueListNode.is(node.where.rightOperand)) + ) { + const tableName = node.where.leftOperand.table.table.identifier.name; + const columnNode = node.where.leftOperand.column; + const valueNode = node.where.rightOperand; + + let resultValue: OperationNode = valueNode; + + if (ValueNode.is(valueNode)) { + resultValue = this.processEnumMappingForValue(tableName, columnNode, valueNode) as OperationNode; + } else if (PrimitiveValueListNode.is(valueNode)) { + resultValue = PrimitiveValueListNode.create( + this.processEnumMappingForValues(tableName, [columnNode], valueNode.values), + ); + } + + return WhereNode.create(BinaryOperationNode.create(node.where.leftOperand, node.where.operator, resultValue)); + } + + return super.transformWhere(node); + } + protected override transformUpdateQuery(node: UpdateQueryNode) { if (!node.table) { return super.transformUpdateQuery(node); diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index 5fd699a9..5e340ade 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -46,6 +46,16 @@ describe('Name mapping tests', () => { user_role: 'role_user', }); + rawRead = await db.$qbRaw + .selectFrom('users') + .where('user_role', '=', 'role_user') + .selectAll() + .executeTakeFirst(); + await expect(rawRead).toMatchObject({ + user_email: 'u1@test.com', + user_role: 'role_user', + }); + await expect( db.user.create({ data: { @@ -66,6 +76,15 @@ describe('Name mapping tests', () => { user_role: 'MODERATOR', }); + rawRead = await db.$qbRaw + .selectFrom('users') + .where('user_role', '=', 'MODERATOR') + .selectAll() + .executeTakeFirst(); + await expect(rawRead).toMatchObject({ + user_role: 'MODERATOR', + }); + await expect( db.$qb .insertInto('User') @@ -146,6 +165,52 @@ describe('Name mapping tests', () => { posts: [{ title: 'Post1' }], }); + await expect( + db.user.findFirst({ + where: { role: 'USER' }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + await expect( + db.user.findMany({ + where: { role: 'USER' }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + + await expect( + db.user.findFirst({ + where: { role: { in: ['USER'] } }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + await expect( + db.user.findMany({ + where: { role: { in: ['USER'] } }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + // select all await expect( db.user.findFirst({ From c8bf5f56cf8e2cf8dccab88f007afaf86bb3ce7f Mon Sep 17 00:00:00 2001 From: FTB_lag Date: Sun, 28 Dec 2025 19:59:38 +0300 Subject: [PATCH 2/5] fix(orm): super transform for where --- packages/orm/src/client/executor/name-mapper.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 9c1c8e01..831aca89 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -207,11 +207,15 @@ export class QueryNameMapper extends OperationNodeTransformer { resultValue = this.processEnumMappingForValue(tableName, columnNode, valueNode) as OperationNode; } else if (PrimitiveValueListNode.is(valueNode)) { resultValue = PrimitiveValueListNode.create( - this.processEnumMappingForValues(tableName, [columnNode], valueNode.values), + this.processEnumMappingForValues(tableName, valueNode.values.map(() => columnNode), valueNode.values), ); } - return WhereNode.create(BinaryOperationNode.create(node.where.leftOperand, node.where.operator, resultValue)); + return super.transformWhere( + WhereNode.create( + BinaryOperationNode.create(node.where.leftOperand, node.where.operator, resultValue) + ) + ); } return super.transformWhere(node); From ce999fd0b2cc924bb2b18ce85ed97351efd4193f Mon Sep 17 00:00:00 2001 From: FTB_lag Date: Sun, 28 Dec 2025 23:36:22 +0300 Subject: [PATCH 3/5] feat(orm): support nested enum in where and/or --- .../orm/src/client/executor/name-mapper.ts | 43 ++++++++++--------- tests/e2e/orm/client-api/name-mapping.test.ts | 16 +++++++ 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 831aca89..339bedde 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -25,7 +25,6 @@ import { ValueListNode, ValueNode, ValuesNode, - WhereNode, } from 'kysely'; import type { EnumDef, EnumField, FieldDef, ModelDef, SchemaDef } from '../../schema'; import { @@ -185,21 +184,20 @@ export class QueryNameMapper extends OperationNodeTransformer { return ColumnNode.create(mappedName); } - protected override transformWhere(node: WhereNode): WhereNode { + protected override transformBinaryOperation(node: BinaryOperationNode): BinaryOperationNode { if ( - BinaryOperationNode.is(node.where) && - ReferenceNode.is(node.where.leftOperand) && - ColumnNode.is(node.where.leftOperand.column) && - node.where.leftOperand.table && - TableNode.is(node.where.leftOperand.table) && - // where: { enumColumn: Enum.VALUE } - (ValueNode.is(node.where.rightOperand) || - // where: { enumColumn: { in: [Enum.VALUE] } } - PrimitiveValueListNode.is(node.where.rightOperand)) + ReferenceNode.is(node.leftOperand) && + ColumnNode.is(node.leftOperand.column) && + node.leftOperand.table && + TableNode.is(node.leftOperand.table) && + // { enumColumn: Enum.VALUE } + (ValueNode.is(node.rightOperand) || + // { enumColumn: { in: [Enum.VALUE] } } + PrimitiveValueListNode.is(node.rightOperand)) ) { - const tableName = node.where.leftOperand.table.table.identifier.name; - const columnNode = node.where.leftOperand.column; - const valueNode = node.where.rightOperand; + const tableName = node.leftOperand.table.table.identifier.name; + const columnNode = node.leftOperand.column; + const valueNode = node.rightOperand; let resultValue: OperationNode = valueNode; @@ -207,18 +205,21 @@ export class QueryNameMapper extends OperationNodeTransformer { resultValue = this.processEnumMappingForValue(tableName, columnNode, valueNode) as OperationNode; } else if (PrimitiveValueListNode.is(valueNode)) { resultValue = PrimitiveValueListNode.create( - this.processEnumMappingForValues(tableName, valueNode.values.map(() => columnNode), valueNode.values), + this.processEnumMappingForValues( + tableName, + valueNode.values.map(() => columnNode), + valueNode.values, + ), ); } - return super.transformWhere( - WhereNode.create( - BinaryOperationNode.create(node.where.leftOperand, node.where.operator, resultValue) - ) - ); + return super.transformBinaryOperation({ + ...node, + rightOperand: resultValue, + }); } - return super.transformWhere(node); + return super.transformBinaryOperation(node); } protected override transformUpdateQuery(node: UpdateQueryNode) { diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index 5e340ade..f7f8ec94 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -211,6 +211,22 @@ describe('Name mapping tests', () => { }), ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + await expect( + db.user.findMany({ + where: { + AND: [ + { role: { in: ['USER'] } }, + { role: { in: ['USER'] } }, + { OR: [{ role: { in: ['USER'] } }] }, + ], + }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + // select all await expect( db.user.findFirst({ From 7a40cba34396a73822d9dbc4eaabd29cbb14392f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:58:14 +0800 Subject: [PATCH 4/5] fix: resolve field's model from scope instead of relying on table qualification --- .../orm/src/client/executor/name-mapper.ts | 66 +++++++++++-------- tests/e2e/orm/client-api/name-mapping.test.ts | 42 ++++++++++-- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 339bedde..a3e5938a 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -184,39 +184,51 @@ export class QueryNameMapper extends OperationNodeTransformer { return ColumnNode.create(mappedName); } - protected override transformBinaryOperation(node: BinaryOperationNode): BinaryOperationNode { + protected override transformBinaryOperation(node: BinaryOperationNode) { + // transform enum name mapping for enum values used inside binary operations + // 1. simple value: column = EnumValue + // 2. list value: column IN [EnumValue, EnumValue2] + + // note: Kysely only allows column ref on the left side of a binary operation + if ( ReferenceNode.is(node.leftOperand) && ColumnNode.is(node.leftOperand.column) && - node.leftOperand.table && - TableNode.is(node.leftOperand.table) && - // { enumColumn: Enum.VALUE } - (ValueNode.is(node.rightOperand) || - // { enumColumn: { in: [Enum.VALUE] } } - PrimitiveValueListNode.is(node.rightOperand)) + (ValueNode.is(node.rightOperand) || PrimitiveValueListNode.is(node.rightOperand)) ) { - const tableName = node.leftOperand.table.table.identifier.name; const columnNode = node.leftOperand.column; - const valueNode = node.rightOperand; - - let resultValue: OperationNode = valueNode; - - if (ValueNode.is(valueNode)) { - resultValue = this.processEnumMappingForValue(tableName, columnNode, valueNode) as OperationNode; - } else if (PrimitiveValueListNode.is(valueNode)) { - resultValue = PrimitiveValueListNode.create( - this.processEnumMappingForValues( - tableName, - valueNode.values.map(() => columnNode), - valueNode.values, - ), - ); - } - return super.transformBinaryOperation({ - ...node, - rightOperand: resultValue, - }); + // resolve field from scope in case it's not directly qualified with a table name + const resolvedScope = this.resolveFieldFromScopes( + columnNode.column.name, + node.leftOperand.table?.table.identifier.name, + ); + + if (resolvedScope?.model) { + const valueNode = node.rightOperand; + let resultValue: OperationNode = valueNode; + + if (ValueNode.is(valueNode)) { + resultValue = this.processEnumMappingForValue( + resolvedScope.model, + columnNode, + valueNode, + ) as OperationNode; + } else if (PrimitiveValueListNode.is(valueNode)) { + resultValue = PrimitiveValueListNode.create( + this.processEnumMappingForValues( + resolvedScope.model, + valueNode.values.map(() => columnNode), + valueNode.values, + ), + ); + } + + return super.transformBinaryOperation({ + ...node, + rightOperand: resultValue, + }); + } } return super.transformBinaryOperation(node); diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index f7f8ec94..2b70cd46 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -8,10 +8,10 @@ describe('Name mapping tests', () => { let db: ClientContract; beforeEach(async () => { - db = (await createTestClient(schema, { + db = await createTestClient(schema, { usePrismaPush: true, schemaFile: path.join(__dirname, '../schemas/name-mapping/schema.zmodel'), - })) as any; + }); }); afterEach(async () => { @@ -214,11 +214,7 @@ describe('Name mapping tests', () => { await expect( db.user.findMany({ where: { - AND: [ - { role: { in: ['USER'] } }, - { role: { in: ['USER'] } }, - { OR: [{ role: { in: ['USER'] } }] }, - ], + AND: [{ role: { in: ['USER'] } }, { role: { in: ['USER'] } }, { OR: [{ role: { in: ['USER'] } }] }], }, select: { email: true, @@ -266,6 +262,38 @@ describe('Name mapping tests', () => { role: 'USER', }); + // name mapping for enum value in where clause, with unqualified column name + await expect( + db.$qb.selectFrom('User').select(['User.email', 'User.role']).where('role', '=', 'USER').executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + // name mapping for enum value in simple where clause, with qualified column name + await expect( + db.$qb + .selectFrom('User as u') + .select(['u.email', 'u.role']) + .where('u.role', '=', 'USER') + .executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + // enum value in list + await expect( + db.$qb + .selectFrom('User') + .select(['User.email', 'User.role']) + .where('role', 'in', ['USER', 'ADMIN']) + .executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + await expect( db.$qb .selectFrom('User') From 652a1b048bfb4304f97ac1ab31271d2bf77bf8f0 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:06:12 +0800 Subject: [PATCH 5/5] fix(orm): cuid() generator should respect version argument --- .../function-invocation-validator.ts | 44 ++++- .../language/test/function-invocation.test.ts | 157 +++++++++++++++++- packages/orm/package.json | 11 +- .../orm/src/client/crud/operations/base.ts | 33 ++-- pnpm-lock.yaml | 12 ++ .../e2e/orm/client-api/default-values.test.ts | 108 +++--------- tests/e2e/package.json | 5 +- 7 files changed, 258 insertions(+), 112 deletions(-) diff --git a/packages/language/src/validators/function-invocation-validator.ts b/packages/language/src/validators/function-invocation-validator.ts index 321f95dc..d3bb9e7c 100644 --- a/packages/language/src/validators/function-invocation-validator.ts +++ b/packages/language/src/validators/function-invocation-validator.ts @@ -88,7 +88,7 @@ export default class FunctionInvocationValidator implements AstValidator param.name === 'format'); + const formatParamIdx = funcDecl.params.findIndex((param) => param.name === 'format'); const formatArg = getLiteral(expr.args[formatParamIdx]?.value); if ( formatArg !== undefined && @@ -192,6 +192,48 @@ export default class FunctionInvocationValidator implements AstValidator(versionArg); + if (version !== 4 && version !== 7) { + accept('error', 'first argument must be 4 or 7', { + node: expr.args[0]!, + }); + } + } + } + + @func('cuid') + private _checkCuid(expr: InvocationExpr, accept: ValidationAcceptor) { + // first argument must be 1 or 2 if provided + const versionArg = expr.args[0]?.value; + if (versionArg) { + const version = getLiteral(versionArg); + if (version !== 1 && version !== 2) { + accept('error', 'first argument must be 1 or 2', { + 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(lengthArg); + if (length !== undefined && length <= 0) { + accept('error', 'first argument must be a positive number', { + node: expr.args[0]!, + }); + } + } + } + @func('auth') private _checkAuth(expr: InvocationExpr, accept: ValidationAcceptor) { if (!expr.$resolvedType) { diff --git a/packages/language/test/function-invocation.test.ts b/packages/language/test/function-invocation.test.ts index a4e1da0f..ff6bb45e 100644 --- a/packages/language/test/function-invocation.test.ts +++ b/packages/language/test/function-invocation.test.ts @@ -176,7 +176,8 @@ describe('Function Invocation Tests', () => { }); it('id functions should reject invalid format strings', async () => { - await loadSchemaWithError(` + await loadSchemaWithError( + ` datasource db { provider = 'sqlite' url = 'file:./dev.db' @@ -185,7 +186,9 @@ describe('Function Invocation Tests', () => { model User { id String @id @default(cuid(2, '')) } - `, 'argument must include'); + `, + 'argument must include', + ); await loadSchemaWithError( ` @@ -197,7 +200,9 @@ describe('Function Invocation Tests', () => { model User { id String @id @default(uuid(4, '\\\\%s')) } - `, 'argument must include'); + `, + 'argument must include', + ); await loadSchemaWithError( ` @@ -209,7 +214,9 @@ describe('Function Invocation Tests', () => { model User { id String @id @default(uuid(4, '\\\\%s\\\\%s')) } - `, 'argument must include'); + `, + 'argument must include', + ); await loadSchemaWithError( ` @@ -262,9 +269,149 @@ describe('Function Invocation Tests', () => { model User { id String @id @default(cuid(2, 'user_%')) - } + } `, 'argument must include', ); }); + + describe('uuid() version validation', () => { + it('should accept valid uuid versions', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(4)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7)) + } + `); + }); + + it('should reject invalid uuid versions', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(1)) + } + `, + 'first argument must be 4 or 7', + ); + }); + }); + + describe('cuid() version validation', () => { + it('should accept valid cuid versions', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(1)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2)) + } + `); + }); + + it('should reject invalid cuid versions', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(0)) + } + `, + 'first argument must be 1 or 2', + ); + }); + }); + + describe('nanoid() length validation', () => { + it('should accept positive nanoid lengths', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(1)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(21)) + } + `); + }); + + it('should reject non-positive nanoid lengths', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(0)) + } + `, + 'first argument must be a positive number', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(-1)) + } + `, + 'first argument must be a positive number', + ); + }); + }); }); diff --git a/packages/orm/package.json b/packages/orm/package.json index 32b4743b..d7c16a10 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -83,8 +83,9 @@ }, "dependencies": { "@paralleldrive/cuid2": "^2.2.2", - "@zenstackhq/schema": "workspace:*", "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/schema": "workspace:*", + "cuid": "^3.0.0", "decimal.js": "catalog:", "json-stable-stringify": "^1.3.0", "kysely": "catalog:", @@ -98,8 +99,8 @@ "peerDependencies": { "better-sqlite3": "catalog:", "pg": "catalog:", - "zod": "catalog:", - "sql.js": "catalog:" + "sql.js": "catalog:", + "zod": "catalog:" }, "peerDependenciesMeta": { "better-sqlite3": { @@ -115,12 +116,12 @@ "devDependencies": { "@types/better-sqlite3": "catalog:", "@types/pg": "^8.0.0", + "@types/sql.js": "^1.4.9", "@types/toposort": "^2.0.7", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "tsx": "^4.19.2", - "zod": "^4.1.0", - "@types/sql.js": "^1.4.9" + "zod": "^4.1.0" } } diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 8fb69846..54168951 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1,5 +1,6 @@ -import { createId } from '@paralleldrive/cuid2'; +import { createId as cuid2 } from '@paralleldrive/cuid2'; import { clone, enumerate, invariant, isPlainObject } from '@zenstackhq/common-helpers'; +import { default as cuid1 } from 'cuid'; import { createQueryId, DeleteResult, @@ -859,28 +860,24 @@ export abstract class BaseOperationHandler { private evalGenerator(defaultValue: Expression) { if (ExpressionUtils.isCall(defaultValue)) { + const firstArgVal = + defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) + ? defaultValue.args[0].value + : undefined; return match(defaultValue.function) - .with('cuid', () => this.formatGeneratedValue(createId(), defaultValue.args?.[1])) + .with('cuid', () => { + const version = firstArgVal; + const generated = version === 2 ? cuid2() : cuid1(); + return this.formatGeneratedValue(generated, defaultValue.args?.[1]); + }) .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(); - + const version = firstArgVal; + const generated = version === 7 ? uuid.v7() : uuid.v4(); return this.formatGeneratedValue(generated, defaultValue.args?.[1]); }) .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 length = firstArgVal; + const generated = typeof length === 'number' ? nanoid(length) : nanoid(); return this.formatGeneratedValue(generated, defaultValue.args?.[1]); }) .with('ulid', () => this.formatGeneratedValue(ulid(), defaultValue.args?.[0])) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20aba240..ba5f04d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -477,6 +477,9 @@ importers: better-sqlite3: specifier: 'catalog:' version: 12.5.0 + cuid: + specifier: ^3.0.0 + version: 3.0.0 decimal.js: specifier: 'catalog:' version: 10.6.0 @@ -1013,6 +1016,9 @@ importers: better-sqlite3: specifier: 'catalog:' version: 12.5.0 + cuid: + specifier: ^3.0.0 + version: 3.0.0 decimal.js: specifier: 'catalog:' version: 10.6.0 @@ -4580,6 +4586,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cuid@3.0.0: + resolution: {integrity: sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==} + deprecated: Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead. + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -12009,6 +12019,8 @@ snapshots: csstype@3.2.3: {} + cuid@3.0.0: {} + damerau-levenshtein@1.0.8: {} data-urls@6.0.0: diff --git a/tests/e2e/orm/client-api/default-values.test.ts b/tests/e2e/orm/client-api/default-values.test.ts index dea5a3bc..c9d104e8 100644 --- a/tests/e2e/orm/client-api/default-values.test.ts +++ b/tests/e2e/orm/client-api/default-values.test.ts @@ -1,92 +1,38 @@ -import { isCuid } from '@paralleldrive/cuid2'; -import { ZenStackClient } from '@zenstackhq/orm'; -import { ExpressionUtils, type SchemaDef } from '@zenstackhq/orm/schema'; -import SQLite from 'better-sqlite3'; -import { SqliteDialect } from 'kysely'; +import { isCuid as isCuidV2 } from '@paralleldrive/cuid2'; +import { isCuid as isCuidV1 } from 'cuid'; +import { createTestClient } from '@zenstackhq/testtools'; import { isValid as isValidUlid } from 'ulid'; -import { validate as isValidUuid } from 'uuid'; +import { validate as isValidUuid, version as getUuidVersion } from 'uuid'; import { describe, expect, it } from 'vitest'; -const schema = { - provider: { - type: 'sqlite', - }, - models: { - Model: { - name: 'Model', - fields: { - id: { - name: 'id', - type: 'Int', - id: true, - }, - uuid: { - name: 'uuid', - type: 'String', - default: ExpressionUtils.call('uuid'), - }, - uuid7: { - name: 'uuid7', - type: 'String', - default: ExpressionUtils.call('uuid', [ExpressionUtils.literal(7)]), - }, - cuid: { - name: 'cuid', - type: 'String', - default: ExpressionUtils.call('cuid'), - }, - cuid2: { - name: 'cuid2', - type: 'String', - default: ExpressionUtils.call('cuid', [ExpressionUtils.literal(2)]), - }, - nanoid: { - name: 'nanoid', - type: 'String', - default: ExpressionUtils.call('nanoid'), - }, - nanoid8: { - name: 'nanoid8', - type: 'String', - default: ExpressionUtils.call('nanoid', [ExpressionUtils.literal(8)]), - }, - ulid: { - name: 'ulid', - type: 'String', - default: ExpressionUtils.call('ulid'), - }, - dt: { - name: 'dt', - type: 'DateTime', - default: ExpressionUtils.call('now'), - }, - bool: { - name: 'bool', - type: 'Boolean', - default: false, - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'Int' }, - }, - }, - }, - plugins: {}, -} as const satisfies SchemaDef; +const schema = ` +model Model { + id Int @id + uuid String @default(uuid()) + uuid4 String @default(uuid(4)) + uuid7 String @default(uuid(7)) + cuid String @default(cuid()) + cuid1 String @default(cuid(1)) + cuid2 String @default(cuid(2)) + nanoid String @default(nanoid()) + nanoid8 String @default(nanoid(8)) + ulid String @default(ulid()) + dt DateTime @default(now()) + bool Boolean @default(false) +} +`; describe('default values tests', () => { it('supports defaults', async () => { - const client = new ZenStackClient(schema, { - dialect: new SqliteDialect({ database: new SQLite(':memory:') }), - }); - await client.$pushSchema(); + const client = await createTestClient(schema); const entity = await client.model.create({ data: { id: 1 } }); - expect(entity.uuid).toSatisfy(isValidUuid); - expect(entity.uuid7).toSatisfy(isValidUuid); - expect(entity.cuid).toSatisfy(isCuid); - expect(entity.cuid2).toSatisfy(isCuid); + expect(entity.uuid).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 4); + expect(entity.uuid4).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 4); + expect(entity.uuid7).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 7); + expect(entity.cuid).toSatisfy(isCuidV1); + expect(entity.cuid1).toSatisfy(isCuidV1); + expect(entity.cuid2).toSatisfy(isCuidV2); expect(entity.nanoid).toSatisfy((id) => id.length >= 21); expect(entity.nanoid8).toSatisfy((id) => id.length === 8); expect(entity.ulid).toSatisfy(isValidUlid); diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 973612fc..d1127361 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -15,16 +15,17 @@ "@paralleldrive/cuid2": "^2.2.2", "@zenstackhq/cli": "workspace:*", "@zenstackhq/language": "workspace:*", - "@zenstackhq/schema": "workspace:*", "@zenstackhq/orm": "workspace:*", "@zenstackhq/plugin-policy": "workspace:*", + "@zenstackhq/schema": "workspace:*", "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", "better-sqlite3": "catalog:", "decimal.js": "catalog:", "kysely": "catalog:", "ulid": "^3.0.0", - "uuid": "^11.0.5" + "uuid": "^11.0.5", + "cuid": "^3.0.0" }, "devDependencies": { "@zenstackhq/cli": "workspace:*",