diff --git a/package.json b/package.json index d46cef02..75d7d3de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index c89bd768..3987bbe8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index d5ebe7fc..cf8a1b55 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index 64b951cf..b72368ee 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index ee134446..6aaa1e94 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index e7686f38..3f950df1 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 924924fa..9673e489 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index acb53278..7a7a3a8c 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 44f22fcc..634044c6 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.0.9", + "version": "3.0.11", "displayName": "ZenStack V3 Language Tools", "description": "VSCode extension for ZenStack (v3) ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index b20e9815..e36f2df0 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index f1b46e84..7df81364 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -126,11 +126,11 @@ function dbgenerated(expr: String?): Any { function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean { } @@@expressionContext([AccessPolicy, ValidationRule]) -/** - * If the field value matches the search condition with [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). Need to enable "fullTextSearch" preview feature to use. - */ -function search(field: String, search: String): Boolean { -} @@@expressionContext([AccessPolicy]) +// /** +// * If the field value matches the search condition with [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). Need to enable "fullTextSearch" preview feature to use. +// */ +// function search(field: String, search: String): Boolean { +// } @@@expressionContext([AccessPolicy]) /** * Checks the field value starts with the search string. By default, the search is case-sensitive, and @@ -151,25 +151,25 @@ function endsWith(field: String, search: String, caseInSensitive: Boolean?): Boo } @@@expressionContext([AccessPolicy, ValidationRule]) /** - * If the field value (a list) has the given search value + * Checks if the list field has the given search value */ function has(field: Any[], search: Any): Boolean { } @@@expressionContext([AccessPolicy, ValidationRule]) /** - * If the field value (a list) has every element of the search list + * Checks if the list field has at least one element of the search list */ -function hasEvery(field: Any[], search: Any[]): Boolean { +function hasSome(field: Any[], search: Any[]): Boolean { } @@@expressionContext([AccessPolicy, ValidationRule]) /** - * If the field value (a list) has at least one element of the search list + * Checks if the list field has every element of the search list */ -function hasSome(field: Any[], search: Any[]): Boolean { +function hasEvery(field: Any[], search: Any[]): Boolean { } @@@expressionContext([AccessPolicy, ValidationRule]) /** - * If the field value (a list) is empty + * Checks if the list field is empty */ function isEmpty(field: Any[]): Boolean { } @@@expressionContext([AccessPolicy, ValidationRule]) @@ -551,9 +551,9 @@ function length(field: Any): Int { /** - * Validates a string field value matches a regex. + * Validates a string field value matches a regex pattern. */ -function regex(field: String, regex: String): Boolean { +function regex(field: String, pattern: String): Boolean { } @@@expressionContext([ValidationRule]) /** diff --git a/packages/language/src/module.ts b/packages/language/src/module.ts index cff4ac0e..df83d4e0 100644 --- a/packages/language/src/module.ts +++ b/packages/language/src/module.ts @@ -94,6 +94,8 @@ export function createZModelLanguageServices( // when documents reach Parsed state, inspect plugin declarations and load corresponding // plugin zmodel docs + // Note we must use `onBuildPhase` instead of `onDocumentPhase` here because the latter is + // not called when not running inside a language server. shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Parsed, async (documents) => { for (const doc of documents) { if (doc.parseResult.lexerErrors.length > 0 || doc.parseResult.parserErrors.length > 0) { @@ -101,6 +103,10 @@ export function createZModelLanguageServices( continue; } + if (doc.uri.scheme !== 'file') { + continue; + } + const schemaPath = fileURLToPath(doc.uri.toString()); const pluginSchemas = getPluginDocuments(doc.parseResult.value as Model, schemaPath); for (const plugin of pluginSchemas) { diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index c361feee..894c6fc7 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -443,9 +443,9 @@ export function getAllDeclarationsIncludingImports(documents: LangiumDocuments, } export function getAuthDecl(decls: (DataModel | TypeDef)[]) { - let authModel = decls.find((m) => hasAttribute(m, '@@auth')); + let authModel = decls.find((d) => hasAttribute(d, '@@auth')); if (!authModel) { - authModel = decls.find((m) => m.name === 'User'); + authModel = decls.find((d) => d.name === 'User'); } return authModel; } diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index 981eb814..c7563bab 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -290,11 +290,13 @@ export default class AttributeApplicationValidator implements AstValidator { @@ -321,7 +323,7 @@ export default class AttributeApplicationValidator implements AstValidator { return (procOptions[name] as Function).apply(this, [this, ...args]); } + async $connect() { + await this.kysely.connection().execute(async (conn) => { + await conn.executeQuery(sql`select 1`.compile(this.kysely)); + }); + } + async $disconnect() { await this.kysely.destroy(); } diff --git a/packages/runtime/src/client/constants.ts b/packages/runtime/src/client/constants.ts index 4d457e9c..129cf349 100644 --- a/packages/runtime/src/client/constants.ts +++ b/packages/runtime/src/client/constants.ts @@ -11,7 +11,7 @@ export const NUMERIC_FIELD_TYPES = ['Int', 'Float', 'BigInt', 'Decimal']; /** * Client API methods that are not supported in transactions. */ -export const TRANSACTION_UNSUPPORTED_METHODS = ['$transaction', '$disconnect', '$use'] as const; +export const TRANSACTION_UNSUPPORTED_METHODS = ['$transaction', '$connect', '$disconnect', '$use'] as const; /** * Prefix for JSON field used to store joined delegate rows. diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index d2c19cb1..0d90bc88 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -151,7 +151,12 @@ export type ClientContract = { $unuseAll(): ClientContract; /** - * Disconnects the underlying Kysely instance from the database. + * Eagerly connects to the database. + */ + $connect(): Promise; + + /** + * Explicitly disconnects from the database. */ $disconnect(): Promise; diff --git a/packages/runtime/src/client/crud/dialects/base-dialect.ts b/packages/runtime/src/client/crud/dialects/base-dialect.ts index 1b8b1e1c..258cf9fb 100644 --- a/packages/runtime/src/client/crud/dialects/base-dialect.ts +++ b/packages/runtime/src/client/crud/dialects/base-dialect.ts @@ -19,7 +19,6 @@ import { InternalError, QueryError } from '../../errors'; import type { ClientOptions } from '../../options'; import { aggregate, - buildFieldRef, buildJoinPairs, ensureArray, flattenCompoundUniqueFilters, @@ -89,14 +88,7 @@ export abstract class BaseCrudDialect { result = this.buildSkipTake(result, skip, take); // orderBy - result = this.buildOrderBy( - result, - model, - modelAlias, - args.orderBy, - skip !== undefined || take !== undefined, - negateOrderBy, - ); + result = this.buildOrderBy(result, model, modelAlias, args.orderBy, negateOrderBy); // distinct if ('distinct' in args && (args as any).distinct) { @@ -748,15 +740,10 @@ export abstract class BaseCrudDialect { model: string, modelAlias: string, orderBy: OrArray, boolean, boolean>> | undefined, - useDefaultIfEmpty: boolean, negated: boolean, ) { if (!orderBy) { - if (useDefaultIfEmpty) { - orderBy = makeDefaultOrderBy(this.schema, model); - } else { - return query; - } + return query; } let result = query; @@ -862,7 +849,7 @@ export abstract class BaseCrudDialect { ), ); }); - result = this.buildOrderBy(result, fieldDef.type, relationModel, value, false, negated); + result = this.buildOrderBy(result, fieldDef.type, relationModel, value, negated); } } } @@ -943,15 +930,13 @@ export abstract class BaseCrudDialect { field: string, ): SelectQueryBuilder { const fieldDef = requireField(this.schema, model, field); - if (fieldDef.computed) { - // TODO: computed field from delegate base? - return query.select(() => this.fieldRef(model, field, modelAlias).as(field)); - } else if (!fieldDef.originModel) { - // regular field - return query.select(this.eb.ref(`${modelAlias}.${field}`).as(field)); - } else { - return this.buildSelectField(query, fieldDef.originModel, fieldDef.originModel, field); - } + + // if field is defined on a delegate base, the base model is joined with its + // model name from outer query, so we should use it directly as the alias + const fieldModel = fieldDef.originModel ?? model; + const alias = fieldDef.originModel ?? modelAlias; + + return query.select(() => this.fieldRef(fieldModel, field, alias).as(field)); } buildDelegateJoin( @@ -1083,7 +1068,26 @@ export abstract class BaseCrudDialect { } fieldRef(model: string, field: string, modelAlias?: string, inlineComputedField = true) { - return buildFieldRef(this.schema, model, field, this.options, this.eb, modelAlias, inlineComputedField); + const fieldDef = requireField(this.schema, model, field); + + if (!fieldDef.computed) { + // regular field + return this.eb.ref(modelAlias ? `${modelAlias}.${field}` : field); + } else { + // computed field + if (!inlineComputedField) { + return this.eb.ref(modelAlias ? `${modelAlias}.${field}` : field); + } + let computer: Function | undefined; + if ('computedFields' in this.options) { + const computedFields = this.options.computedFields as Record; + computer = computedFields?.[fieldDef.originModel ?? model]?.[field]; + } + if (!computer) { + throw new QueryError(`Computed field "${field}" implementation not provided for model "${model}"`); + } + return computer(this.eb, { modelAlias }); + } } protected canJoinWithoutNestedSelect( diff --git a/packages/runtime/src/client/crud/operations/aggregate.ts b/packages/runtime/src/client/crud/operations/aggregate.ts index 6362fbe6..f92a8518 100644 --- a/packages/runtime/src/client/crud/operations/aggregate.ts +++ b/packages/runtime/src/client/crud/operations/aggregate.ts @@ -52,14 +52,7 @@ export class AggregateOperationHandler extends BaseOpe subQuery = this.dialect.buildSkipTake(subQuery, skip, take); // orderBy - subQuery = this.dialect.buildOrderBy( - subQuery, - this.model, - this.model, - parsedArgs.orderBy, - skip !== undefined || take !== undefined, - negateOrderBy, - ); + subQuery = this.dialect.buildOrderBy(subQuery, this.model, this.model, parsedArgs.orderBy, negateOrderBy); return subQuery.as('$sub'); }); diff --git a/packages/runtime/src/client/crud/operations/group-by.ts b/packages/runtime/src/client/crud/operations/group-by.ts index 4f4a083f..44829ee2 100644 --- a/packages/runtime/src/client/crud/operations/group-by.ts +++ b/packages/runtime/src/client/crud/operations/group-by.ts @@ -11,51 +11,32 @@ export class GroupByOperationHandler extends BaseOpera // parse args const parsedArgs = this.inputValidator.validateGroupByArgs(this.model, normalizedArgs); - let query = this.kysely.selectFrom((eb) => { - // nested query for filtering and pagination - - // where - let subQuery = eb - .selectFrom(this.model) - .selectAll() - .where(() => this.dialect.buildFilter(this.model, this.model, parsedArgs?.where)); - - // skip & take - const skip = parsedArgs?.skip; - let take = parsedArgs?.take; - let negateOrderBy = false; - if (take !== undefined && take < 0) { - negateOrderBy = true; - take = -take; - } - subQuery = this.dialect.buildSkipTake(subQuery, skip, take); - - // default orderBy - subQuery = this.dialect.buildOrderBy( - subQuery, - this.model, - this.model, - undefined, - skip !== undefined || take !== undefined, - negateOrderBy, - ); - - return subQuery.as('$sub'); - }); + let query = this.kysely + .selectFrom(this.model) + .where(() => this.dialect.buildFilter(this.model, this.model, parsedArgs?.where)); - const fieldRef = (field: string) => this.dialect.fieldRef(this.model, field, '$sub'); + const fieldRef = (field: string) => this.dialect.fieldRef(this.model, field); // groupBy const bys = typeof parsedArgs.by === 'string' ? [parsedArgs.by] : (parsedArgs.by as string[]); query = query.groupBy(bys.map((by) => fieldRef(by))); - // orderBy - if (parsedArgs.orderBy) { - query = this.dialect.buildOrderBy(query, this.model, '$sub', parsedArgs.orderBy, false, false); + // skip & take + const skip = parsedArgs?.skip; + let take = parsedArgs?.take; + let negateOrderBy = false; + if (take !== undefined && take < 0) { + negateOrderBy = true; + take = -take; } + query = this.dialect.buildSkipTake(query, skip, take); + + // orderBy + query = this.dialect.buildOrderBy(query, this.model, this.model, parsedArgs.orderBy, negateOrderBy); + // having if (parsedArgs.having) { - query = query.having(() => this.dialect.buildFilter(this.model, '$sub', parsedArgs.having)); + query = query.having(() => this.dialect.buildFilter(this.model, this.model, parsedArgs.having)); } // select all by fields diff --git a/packages/runtime/src/client/crud/validator/index.ts b/packages/runtime/src/client/crud/validator/index.ts index 09bfb8e5..1e32a865 100644 --- a/packages/runtime/src/client/crud/validator/index.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -50,11 +50,11 @@ import { addStringValidation, } from './utils'; +const schemaCache = new WeakMap>(); + type GetSchemaFunc = (model: GetModels, options: Options) => ZodType; export class InputValidator { - private schemaCache = new Map(); - constructor(private readonly client: ClientContract) {} private get schema() { @@ -192,6 +192,24 @@ export class InputValidator { ); } + private getSchemaCache(cacheKey: string) { + let thisCache = schemaCache.get(this.schema); + if (!thisCache) { + thisCache = new Map(); + schemaCache.set(this.schema, thisCache); + } + return thisCache.get(cacheKey); + } + + private setSchemaCache(cacheKey: string, schema: ZodType) { + let thisCache = schemaCache.get(this.schema); + if (!thisCache) { + thisCache = new Map(); + schemaCache.set(this.schema, thisCache); + } + return thisCache.set(cacheKey, schema); + } + private validate( model: GetModels, operation: string, @@ -200,14 +218,16 @@ export class InputValidator { args: unknown, ) { const cacheKey = stableStringify({ + type: 'model', model, operation, options, + extraValidationsEnabled: this.extraValidationsEnabled, }); - let schema = this.schemaCache.get(cacheKey!); + let schema = this.getSchemaCache(cacheKey!); if (!schema) { schema = getSchema(model, options); - this.schemaCache.set(cacheKey!, schema); + this.setSchemaCache(cacheKey!, schema); } const { error, data } = schema.safeParse(args); if (error) { @@ -293,8 +313,12 @@ export class InputValidator { } private makeTypeDefSchema(type: string): z.ZodType { - const key = `$typedef-${type}`; - let schema = this.schemaCache.get(key); + const key = stableStringify({ + type: 'typedef', + name: type, + extraValidationsEnabled: this.extraValidationsEnabled, + }); + let schema = this.getSchemaCache(key!); if (schema) { return schema; } @@ -316,7 +340,7 @@ export class InputValidator { ), ) .passthrough(); - this.schemaCache.set(key, schema); + this.setSchemaCache(key!, schema); return schema; } diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index f3e855fa..e1fb6298 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -22,7 +22,7 @@ import { type RootOperationNode, } from 'kysely'; import { match } from 'ts-pattern'; -import type { GetModels, SchemaDef } from '../../schema'; +import type { GetModels, ModelDef, SchemaDef, TypeDefDef } from '../../schema'; import { type ClientImpl } from '../client-impl'; import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { InternalError, QueryError, ZenStackError } from '../errors'; @@ -42,7 +42,7 @@ type MutationInfo = { }; export class ZenStackQueryExecutor extends DefaultQueryExecutor { - private readonly nameMapper: QueryNameMapper; + private readonly nameMapper: QueryNameMapper | undefined; constructor( private client: ClientImpl, @@ -54,7 +54,21 @@ export class ZenStackQueryExecutor extends DefaultQuer private suppressMutationHooks: boolean = false, ) { super(compiler, adapter, connectionProvider, plugins); - this.nameMapper = new QueryNameMapper(client.$schema); + + if (this.schemaHasMappedNames(client.$schema)) { + this.nameMapper = new QueryNameMapper(client.$schema); + } + } + + private schemaHasMappedNames(schema: Schema) { + const hasMapAttr = (decl: ModelDef | TypeDefDef) => { + if (decl.attributes?.some((attr) => attr.name === '@@map')) { + return true; + } + return Object.values(decl.fields).some((field) => field.attributes?.some((attr) => attr.name === '@map')); + }; + + return Object.values(schema.models).some(hasMapAttr) || Object.values(schema.typeDefs ?? []).some(hasMapAttr); } private get kysely() { @@ -170,7 +184,7 @@ export class ZenStackQueryExecutor extends DefaultQuer if (this.suppressMutationHooks || !this.isMutationNode(query) || !this.hasEntityMutationPlugins) { // no need to handle mutation hooks, just proceed - const finalQuery = this.nameMapper.transformNode(query); + const finalQuery = this.processNameMapping(query); compiled = this.compileQuery(finalQuery); if (parameters) { compiled = { ...compiled, parameters }; @@ -189,7 +203,7 @@ export class ZenStackQueryExecutor extends DefaultQuer returning: ReturningNode.create([SelectionNode.createSelectAll()]), }; } - const finalQuery = this.nameMapper.transformNode(query); + const finalQuery = this.processNameMapping(query); compiled = this.compileQuery(finalQuery); if (parameters) { compiled = { ...compiled, parameters }; @@ -239,6 +253,10 @@ export class ZenStackQueryExecutor extends DefaultQuer return result; } + private processNameMapping(query: Node): Node { + return this.nameMapper?.transformNode(query) ?? query; + } + private createClientForConnection(connection: DatabaseConnection, inTx: boolean) { const innerExecutor = this.withConnectionProvider(new SingleConnectionProvider(connection)); innerExecutor.suppressMutationHooks = true; diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index b5107cdf..9f97305b 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -6,7 +6,6 @@ import { TableNode, type Expression, type ExpressionBuilder, - type ExpressionWrapper, type OperationNode, } from 'kysely'; import { match } from 'ts-pattern'; @@ -15,7 +14,6 @@ import { extractFields } from '../utils/object-utils'; import type { AGGREGATE_OPERATORS } from './constants'; import type { OrderBy } from './crud-types'; import { InternalError, QueryError } from './errors'; -import type { ClientOptions } from './options'; export function hasModel(schema: SchemaDef, model: string) { return Object.keys(schema.models) @@ -180,34 +178,6 @@ export function getIdValues(schema: SchemaDef, model: string, data: any): Record return idFields.reduce((acc, field) => ({ ...acc, [field]: data[field] }), {}); } -export function buildFieldRef( - schema: Schema, - model: string, - field: string, - options: ClientOptions, - eb: ExpressionBuilder, - modelAlias?: string, - inlineComputedField = true, -): ExpressionWrapper { - const fieldDef = requireField(schema, model, field); - if (!fieldDef.computed) { - return eb.ref(modelAlias ? `${modelAlias}.${field}` : field); - } else { - if (!inlineComputedField) { - return eb.ref(modelAlias ? `${modelAlias}.${field}` : field); - } - let computer: Function | undefined; - if ('computedFields' in options) { - const computedFields = options.computedFields as Record; - computer = computedFields?.[model]?.[field]; - } - if (!computer) { - throw new QueryError(`Computed field "${field}" implementation not provided for model "${model}"`); - } - return computer(eb, { modelAlias }); - } -} - export function fieldHasDefaultValue(fieldDef: FieldDef) { return fieldDef.default !== undefined || fieldDef.updatedAt; } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index daf9e27e..5cb03d87 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 92cb74de..0ed46656 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 4c0f2ba3..48c935a2 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 0ee27d46..33125139 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "", "type": "module", "main": "index.js", diff --git a/samples/blog/package.json b/samples/blog/package.json index b68731ef..00ec0b00 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "description": "", "main": "index.js", "scripts": { diff --git a/samples/blog/zenstack/schema.zmodel b/samples/blog/zenstack/schema.zmodel index 6cf112e2..3669d799 100644 --- a/samples/blog/zenstack/schema.zmodel +++ b/samples/blog/zenstack/schema.zmodel @@ -10,7 +10,8 @@ enum Role { } plugin policy { - // due to pnpm layout we can't directly use package name here + // due to pnpm layout we can't directly use package name here, + // don't do this in your code and use "@zenstackhq/plugin-policy" instead provider = '../node_modules/@zenstackhq/plugin-policy/dist/index.js' } diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 84d006b8..3363414b 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1,15 +1,9 @@ import { createTestClient } from '@zenstackhq/testtools'; -import { afterEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; describe('Computed fields tests', () => { - let db: any; - - afterEach(async () => { - await db?.$disconnect(); - }); - it('works with non-optional fields', async () => { - db = await createTestClient( + const db = await createTestClient( ` model User { id Int @id @default(autoincrement()) @@ -97,7 +91,7 @@ model User { }); it('is typed correctly for non-optional fields', async () => { - db = await createTestClient( + await createTestClient( ` model User { id Int @id @default(autoincrement()) @@ -137,7 +131,7 @@ main(); }); it('works with optional fields', async () => { - db = await createTestClient( + const db = await createTestClient( ` model User { id Int @id @default(autoincrement()) @@ -164,7 +158,7 @@ model User { }); it('is typed correctly for optional fields', async () => { - db = await createTestClient( + await createTestClient( ` model User { id Int @id @default(autoincrement()) @@ -203,7 +197,7 @@ main(); }); it('works with read from a relation', async () => { - db = await createTestClient( + const db = await createTestClient( ` model User { id Int @id @default(autoincrement()) @@ -240,4 +234,42 @@ model Post { author: expect.objectContaining({ postCount: 1 }), }); }); + + it('allows sub models to use computed fields from delegate base', async () => { + const db = await createTestClient( + ` +model Content { + id Int @id @default(autoincrement()) + title String + isNews Boolean @computed + contentType String + @@delegate(contentType) +} + +model Post extends Content { + body String +} +`, + { + computedFields: { + Content: { + isNews: (eb: any) => eb('title', 'like', '%news%'), + }, + }, + } as any, + ); + + const posts = await db.post.createManyAndReturn({ + data: [ + { id: 1, title: 'latest news', body: 'some news content' }, + { id: 2, title: 'random post', body: 'some other content' }, + ], + }); + expect(posts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 1, isNews: true }), + expect.objectContaining({ id: 2, isNews: false }), + ]), + ); + }); }); diff --git a/tests/e2e/orm/client-api/connect-disconnect.test.ts b/tests/e2e/orm/client-api/connect-disconnect.test.ts new file mode 100644 index 00000000..5154ead9 --- /dev/null +++ b/tests/e2e/orm/client-api/connect-disconnect.test.ts @@ -0,0 +1,29 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Client $connect and $disconnect tests', () => { + it('works with connect and disconnect', async () => { + const db = await createTestClient( + ` + model User { + id String @id @default(cuid()) + email String @unique + } + `, + ); + + // connect to the database + await db.$connect(); + + // perform a simple operation + await db.user.create({ + data: { + email: 'u1@test.com', + }, + }); + + await db.$disconnect(); + + await expect(db.user.findFirst()).rejects.toThrow(); + }); +}); diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 6d188843..cdf67584 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -187,6 +187,7 @@ describe('Client find tests ', () => { await expect( client.user.findMany({ cursor: { id: user2.id }, + orderBy: { id: 'asc' }, }), ).resolves.toEqual([user2, user3]); @@ -195,6 +196,7 @@ describe('Client find tests ', () => { client.user.findMany({ skip: 1, cursor: { id: user1.id }, + orderBy: { id: 'asc' }, }), ).resolves.toEqual([user2, user3]); @@ -221,6 +223,7 @@ describe('Client find tests ', () => { client.user.findMany({ skip: 1, cursor: { id: user1.id, role: 'ADMIN' }, + orderBy: { id: 'asc' }, }), ).resolves.toEqual([user2, user3]); @@ -238,6 +241,7 @@ describe('Client find tests ', () => { skip: 1, take: -2, cursor: { id: user3.id }, + orderBy: { id: 'asc' }, }), ).resolves.toEqual([user1, user2]); }); @@ -343,6 +347,7 @@ describe('Client find tests ', () => { posts: { skip: 1, take: -2, + orderBy: { id: 'asc' }, }, }, }), diff --git a/tests/e2e/orm/client-api/group-by.test.ts b/tests/e2e/orm/client-api/group-by.test.ts index 08da59fe..d4eb2beb 100644 --- a/tests/e2e/orm/client-api/group-by.test.ts +++ b/tests/e2e/orm/client-api/group-by.test.ts @@ -1,7 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ClientContract } from '@zenstackhq/runtime'; -import { schema } from '../schemas/basic'; import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { schema } from '../schemas/basic'; import { createPosts, createUser } from './utils'; describe('Client groupBy tests', () => { @@ -57,7 +57,7 @@ describe('Client groupBy tests', () => { take: -1, orderBy: { email: 'desc' }, }), - ).resolves.toEqual([{ email: 'u1@test.com' }]); + ).resolves.toEqual([{ email: 'u3@test.com' }]); await expect( client.user.groupBy({ @@ -66,7 +66,7 @@ describe('Client groupBy tests', () => { take: -2, orderBy: { email: 'desc' }, }), - ).resolves.toEqual(expect.arrayContaining([{ email: 'u2@test.com' }, { email: 'u1@test.com' }])); + ).resolves.toEqual(expect.arrayContaining([{ email: 'u2@test.com' }, { email: 'u3@test.com' }])); await expect( client.user.groupBy({ @@ -88,10 +88,12 @@ describe('Client groupBy tests', () => { }, _count: true, }), - ).resolves.toEqual([ - { name: 'User', role: 'USER', _count: 2 }, - { name: 'Admin', role: 'ADMIN', _count: 1 }, - ]); + ).resolves.toEqual( + expect.arrayContaining([ + { name: 'User', role: 'USER', _count: 2 }, + { name: 'Admin', role: 'ADMIN', _count: 1 }, + ]), + ); await expect( client.post.groupBy({ diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 101221c2..e737cf2f 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index 773a689d..e2dd39d8 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.0.0-beta.10", + "version": "3.0.0-beta.11", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/test/issue-283.test.ts b/tests/regression/test/issue-283.test.ts new file mode 100644 index 00000000..a36d04f6 --- /dev/null +++ b/tests/regression/test/issue-283.test.ts @@ -0,0 +1,23 @@ +import { loadSchemaWithError } from '@zenstackhq/testtools'; +import { describe, it } from 'vitest'; + +describe('Regression for issue #283', () => { + it('verifies issue 283', async () => { + await loadSchemaWithError( + ` +model Base { + id Int @id @default(autoincrement()) + x Int + type String + @@delegate(type) +} + +model Sub extends Base { + y Int + @@index([x, y]) +} +`, + 'Cannot use fields inherited from a polymorphic base model', + ); + }); +});