Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
}

if (['uuid', 'ulid', 'cuid', 'nanoid'].includes(funcDecl.name)) {
const formatParamIdx = funcDecl.params.findIndex(param => param.name === 'format');
const formatParamIdx = funcDecl.params.findIndex((param) => param.name === 'format');
const formatArg = getLiteral<string>(expr.args[formatParamIdx]?.value);
if (
formatArg !== undefined &&
Expand Down Expand Up @@ -192,6 +192,48 @@ export default class FunctionInvocationValidator implements AstValidator<Express
return true;
}

@func('uuid')
private _checkUuid(expr: InvocationExpr, accept: ValidationAcceptor) {
// first argument must be 4 or 7 if provided
const versionArg = expr.args[0]?.value;
if (versionArg) {
const version = getLiteral<number>(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<number>(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<number>(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) {
Expand Down
157 changes: 152 additions & 5 deletions packages/language/test/function-invocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -185,7 +186,9 @@ describe('Function Invocation Tests', () => {
model User {
id String @id @default(cuid(2, ''))
}
`, 'argument must include');
`,
'argument must include',
);

await loadSchemaWithError(
`
Expand All @@ -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(
`
Expand All @@ -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(
`
Expand Down Expand Up @@ -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',
);
});
});
});
11 changes: 6 additions & 5 deletions packages/orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand All @@ -98,8 +99,8 @@
"peerDependencies": {
"better-sqlite3": "catalog:",
"pg": "catalog:",
"zod": "catalog:",
"sql.js": "catalog:"
"sql.js": "catalog:",
"zod": "catalog:"
},
"peerDependenciesMeta": {
"better-sqlite3": {
Expand All @@ -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"
}
}
33 changes: 15 additions & 18 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -859,28 +860,24 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {

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]))
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading