Skip to content

Commit fd5db63

Browse files
authored
fix(zmodel): unique attribute validation issues (#383)
* fix(zmodel): unique attribute validation issues * update
1 parent e37a6cf commit fd5db63

File tree

7 files changed

+760
-30
lines changed

7 files changed

+760
-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: 2 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,7 @@ 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');
295296
const attrName = attr.decl.ref?.name;
296297
if (!fields) {
297298
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: 38 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,7 +58,6 @@ 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

6363
const dbUrl =
@@ -108,35 +108,48 @@ export async function createTestClient<Schema extends SchemaDef>(
108108
console.log(`Work directory: ${workDir}`);
109109
}
110110

111+
// copy db file to workDir if specified
112+
if (options?.dbFile) {
113+
if (provider !== 'sqlite') {
114+
throw new Error('dbFile option is only supported for sqlite provider');
115+
}
116+
fs.copyFileSync(options.dbFile, path.join(workDir, dbName));
117+
}
118+
111119
const { plugins, ...rest } = options ?? {};
112120
const _options: ClientOptions<Schema> = {
113121
...rest,
114122
} as ClientOptions<Schema>;
115123

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

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

156169
let client = new ZenStackClient(_schema, _options);
157170

158-
if (!options?.usePrismaPush) {
171+
if (!options?.usePrismaPush && !options?.dbFile) {
159172
await client.$pushSchema();
160173
}
161174

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!*.db
204 KB
Binary file not shown.

0 commit comments

Comments
 (0)