Skip to content

Commit 967d2b2

Browse files
committed
fix(zmodel): unique attribute validation issues
1 parent e37a6cf commit 967d2b2

File tree

5 files changed

+767
-30
lines changed

5 files changed

+767
-30
lines changed

packages/language/src/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from 'node:path';
55
import { fileURLToPath, pathToFileURL } from 'node:url';
66
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME, type ExpressionContext } from './constants';
77
import {
8+
InternalAttribute,
89
isArrayExpr,
910
isBinaryExpr,
1011
isConfigArrayExpr,
@@ -173,7 +174,7 @@ export function getRecursiveBases(
173174
bases.forEach((base) => {
174175
// avoid using .ref since this function can be called before linking
175176
const baseDecl = decl.$container.declarations.find(
176-
(d): d is TypeDef | DataModel => isTypeDef(d) || (isDataModel(d) && d.name === base.$refText),
177+
(d): d is TypeDef | DataModel => (isTypeDef(d) || isDataModel(d)) && d.name === base.$refText,
177178
);
178179
if (baseDecl) {
179180
if (!includeDelegate && isDelegateModel(baseDecl)) {
@@ -321,8 +322,15 @@ function getArray(expr: Expression | ConfigExpr | undefined) {
321322
return isArrayExpr(expr) || isConfigArrayExpr(expr) ? expr.items : undefined;
322323
}
323324

325+
export function getAttributeArg(
326+
attr: DataModelAttribute | DataFieldAttribute | InternalAttribute,
327+
name: string,
328+
): Expression | undefined {
329+
return attr.args.find((arg) => arg.$resolvedParam?.name === name)?.value;
330+
}
331+
324332
export function getAttributeArgLiteral<T extends string | number | boolean>(
325-
attr: DataModelAttribute | DataFieldAttribute,
333+
attr: DataModelAttribute | DataFieldAttribute | InternalAttribute,
326334
name: string,
327335
): T | undefined {
328336
for (const arg of attr.args) {

packages/language/src/validators/attribute-application-validator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '../generated/ast';
2222
import {
2323
getAllAttributes,
24+
getAttributeArg,
2425
getStringLiteral,
2526
hasAttribute,
2627
isAuthOrAuthMemberAccess,
@@ -291,7 +292,9 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
291292
@check('@@index')
292293
@check('@@unique')
293294
private _checkConstraint(attr: AttributeApplication, accept: ValidationAcceptor) {
294-
const fields = attr.args[0]?.value;
295+
const fields = getAttributeArg(attr, 'fields');
296+
297+
// const fields = attr.args[0]?.value;
295298
const attrName = attr.decl.ref?.name;
296299
if (!fields) {
297300
accept('error', `expects an array of field references`, {

packages/sdk/src/ts-schema-generator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
UnaryExpr,
3636
type Model,
3737
} from '@zenstackhq/language/ast';
38-
import { getAllAttributes, getAllFields, isDataFieldReference } from '@zenstackhq/language/utils';
38+
import { getAllAttributes, getAllFields, getAttributeArg, isDataFieldReference } from '@zenstackhq/language/utils';
3939
import fs from 'node:fs';
4040
import path from 'node:path';
4141
import { match } from 'ts-pattern';
@@ -840,7 +840,11 @@ export class TsSchemaGenerator {
840840
const seenKeys = new Set<string>();
841841
for (const attr of allAttributes) {
842842
if (attr.decl.$refText === '@@id' || attr.decl.$refText === '@@unique') {
843-
const fieldNames = this.getReferenceNames(attr.args[0]!.value);
843+
const fieldsArg = getAttributeArg(attr, 'fields');
844+
if (!fieldsArg) {
845+
continue;
846+
}
847+
const fieldNames = this.getReferenceNames(fieldsArg);
844848
if (!fieldNames) {
845849
continue;
846850
}

packages/testtools/src/client.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type CreateTestClientOptions<Schema extends SchemaDef> = Omit<ClientOptio
3838
extraSourceFiles?: Record<string, string>;
3939
workDir?: string;
4040
debug?: boolean;
41+
dbFile?: string;
4142
};
4243

4344
export async function createTestClient<Schema extends SchemaDef>(
@@ -57,9 +58,14 @@ export async function createTestClient<Schema extends SchemaDef>(
5758
let workDir = options?.workDir;
5859
let _schema: Schema;
5960
const provider = options?.provider ?? getTestDbProvider() ?? 'sqlite';
60-
6161
const dbName = options?.dbName ?? getTestDbName(provider);
6262

63+
if (options?.dbFile) {
64+
if (provider !== 'sqlite') {
65+
throw new Error('dbFile option is only supported for sqlite provider');
66+
}
67+
}
68+
6369
const dbUrl =
6470
provider === 'sqlite'
6571
? `file:${dbName}`
@@ -108,35 +114,48 @@ export async function createTestClient<Schema extends SchemaDef>(
108114
console.log(`Work directory: ${workDir}`);
109115
}
110116

117+
// copy db file to workDir if specified
118+
if (options?.dbFile) {
119+
if (provider !== 'sqlite') {
120+
throw new Error('dbFile option is only supported for sqlite provider');
121+
}
122+
fs.copyFileSync(options.dbFile, path.join(workDir, dbName));
123+
}
124+
111125
const { plugins, ...rest } = options ?? {};
112126
const _options: ClientOptions<Schema> = {
113127
...rest,
114128
} as ClientOptions<Schema>;
115129

116-
if (options?.usePrismaPush) {
117-
invariant(typeof schema === 'string' || schemaFile, 'a schema file must be provided when using prisma db push');
118-
if (!model) {
119-
const r = await loadDocumentWithPlugins(path.join(workDir, 'schema.zmodel'));
120-
if (!r.success) {
121-
throw new Error(r.errors.join('\n'));
130+
if (!options?.dbFile) {
131+
if (options?.usePrismaPush) {
132+
invariant(
133+
typeof schema === 'string' || schemaFile,
134+
'a schema file must be provided when using prisma db push',
135+
);
136+
if (!model) {
137+
const r = await loadDocumentWithPlugins(path.join(workDir, 'schema.zmodel'));
138+
if (!r.success) {
139+
throw new Error(r.errors.join('\n'));
140+
}
141+
model = r.model;
142+
}
143+
const prismaSchema = new PrismaSchemaGenerator(model);
144+
const prismaSchemaText = await prismaSchema.generate();
145+
fs.writeFileSync(path.resolve(workDir!, 'schema.prisma'), prismaSchemaText);
146+
execSync('npx prisma db push --schema ./schema.prisma --skip-generate --force-reset', {
147+
cwd: workDir,
148+
stdio: 'ignore',
149+
});
150+
} else {
151+
if (provider === 'postgresql') {
152+
invariant(dbName, 'dbName is required');
153+
const pgClient = new PGClient(TEST_PG_CONFIG);
154+
await pgClient.connect();
155+
await pgClient.query(`DROP DATABASE IF EXISTS "${dbName}"`);
156+
await pgClient.query(`CREATE DATABASE "${dbName}"`);
157+
await pgClient.end();
122158
}
123-
model = r.model;
124-
}
125-
const prismaSchema = new PrismaSchemaGenerator(model);
126-
const prismaSchemaText = await prismaSchema.generate();
127-
fs.writeFileSync(path.resolve(workDir!, 'schema.prisma'), prismaSchemaText);
128-
execSync('npx prisma db push --schema ./schema.prisma --skip-generate --force-reset', {
129-
cwd: workDir,
130-
stdio: 'ignore',
131-
});
132-
} else {
133-
if (provider === 'postgresql') {
134-
invariant(dbName, 'dbName is required');
135-
const pgClient = new PGClient(TEST_PG_CONFIG);
136-
await pgClient.connect();
137-
await pgClient.query(`DROP DATABASE IF EXISTS "${dbName}"`);
138-
await pgClient.query(`CREATE DATABASE "${dbName}"`);
139-
await pgClient.end();
140159
}
141160
}
142161

@@ -155,7 +174,7 @@ export async function createTestClient<Schema extends SchemaDef>(
155174

156175
let client = new ZenStackClient(_schema, _options);
157176

158-
if (!options?.usePrismaPush) {
177+
if (!options?.usePrismaPush && !options?.dbFile) {
159178
await client.$pushSchema();
160179
}
161180

0 commit comments

Comments
 (0)