diff --git a/package.json b/package.json index 2d32c2a8..ae210ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 6f90d159..4ad399a6 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-alpha.19", + "version": "3.0.0-alpha.20", "type": "module", "author": { "name": "ZenStack Team" @@ -50,7 +50,7 @@ "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", - "better-sqlite3": "^11.8.1", + "better-sqlite3": "^12.2.0", "tmp": "catalog:" } } diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index a3dd1a34..104c563f 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-alpha.19", + "version": "3.0.0-alpha.20", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index dcd787d1..b66726b0 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "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 7f637c42..af6aaa63 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-alpha.19", + "version": "3.0.0-alpha.20", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 050cc7de..37a80369 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 17e50f4e..b78337b4 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index e30222a5..99f6eb5c 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-alpha.19", + "version": "3.0.0-alpha.20", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/language/src/validators/datamodel-validator.ts b/packages/language/src/validators/datamodel-validator.ts index 49c8dfc7..cbcbf896 100644 --- a/packages/language/src/validators/datamodel-validator.ts +++ b/packages/language/src/validators/datamodel-validator.ts @@ -107,7 +107,7 @@ export default class DataModelValidator implements AstValidator { if (field.type.array && !isDataModel(field.type.reference?.ref)) { const provider = this.getDataSourceProvider(AstUtils.getContainerOfType(field, isModel)!); if (provider === 'sqlite') { - accept('error', `Array type is not supported for "${provider}" provider.`, { node: field.type }); + accept('error', `List type is not supported for "${provider}" provider.`, { node: field.type }); } } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 3511eca9..cc2882e7 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "ZenStack Runtime", "type": "module", "scripts": { @@ -89,7 +89,7 @@ } }, "devDependencies": { - "@types/better-sqlite3": "^7.0.0", + "@types/better-sqlite3": "^7.6.13", "@types/pg": "^8.0.0", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/language": "workspace:*", diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index f8b1ee53..a75dffc2 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -556,7 +556,12 @@ function createModelCrudHandler { - return createPromise('groupBy', args, new GroupByOperationHandler(client, model, inputValidator)); + return createPromise( + 'groupBy', + args, + new GroupByOperationHandler(client, model, inputValidator), + true, + ); }, } as ModelOperations; } diff --git a/packages/runtime/src/client/constants.ts b/packages/runtime/src/client/constants.ts index 746cb900..4d457e9c 100644 --- a/packages/runtime/src/client/constants.ts +++ b/packages/runtime/src/client/constants.ts @@ -17,3 +17,14 @@ export const TRANSACTION_UNSUPPORTED_METHODS = ['$transaction', '$disconnect', ' * Prefix for JSON field used to store joined delegate rows. */ export const DELEGATE_JOINED_FIELD_PREFIX = '$delegate$'; + +/** + * Logical combinators used in filters. + */ +export const LOGICAL_COMBINATORS = ['AND', 'OR', 'NOT'] as const; + +/** + * Aggregation operators. + */ +export const AGGREGATE_OPERATORS = ['_count', '_sum', '_avg', '_min', '_max'] as const; +export type AGGREGATE_OPERATORS = (typeof AGGREGATE_OPERATORS)[number]; diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 48577a32..06d131ab 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -752,17 +752,18 @@ export type ModelOperations` * - * // group by with sorting, the `orderBy` fields must be in the `by` list + * // group by with sorting, the `orderBy` fields must be either an aggregation + * // or a field used in the `by` list * await db.profile.groupBy({ * by: 'country', * orderBy: { country: 'desc' } * }); * - * // group by with having (post-aggregation filter), the `having` fields must - * // be in the `by` list + * // group by with having (post-aggregation filter), the fields used in `having` must + * // be either an aggregation, or a field used in the `by` list * await db.profile.groupBy({ * by: 'country', - * having: { country: 'US' } + * having: { country: 'US', age: { _avg: { gte: 18 } } } * }); */ groupBy>( diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index 4c4986b7..b41cf728 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -209,6 +209,7 @@ export type WhereInput< Schema extends SchemaDef, Model extends GetModels, ScalarOnly extends boolean = false, + WithAggregations extends boolean = false, > = { [Key in GetModelFields as ScalarOnly extends true ? Key extends RelationFields @@ -223,7 +224,12 @@ export type WhereInput< : FieldIsArray extends true ? ArrayFilter> : // primitive - PrimitiveFilter, ModelFieldIsOptional>; + PrimitiveFilter< + Schema, + GetModelFieldType, + ModelFieldIsOptional, + WithAggregations + >; } & { $expr?: (eb: ExpressionBuilder, Model>) => OperandExpression; } & { @@ -249,21 +255,32 @@ type ArrayFilter = { isEmpty?: boolean; }; -type PrimitiveFilter = T extends 'String' - ? StringFilter +type PrimitiveFilter< + Schema extends SchemaDef, + T extends string, + Nullable extends boolean, + WithAggregations extends boolean, +> = T extends 'String' + ? StringFilter : T extends 'Int' | 'Float' | 'Decimal' | 'BigInt' - ? NumberFilter + ? NumberFilter : T extends 'Boolean' - ? BooleanFilter + ? BooleanFilter : T extends 'DateTime' - ? DateTimeFilter + ? DateTimeFilter : T extends 'Bytes' - ? BytesFilter + ? BytesFilter : T extends 'Json' ? 'Not implemented yet' // TODO: Json filter : never; -type CommonPrimitiveFilter = { +type CommonPrimitiveFilter< + Schema extends SchemaDef, + DataType, + T extends BuiltinType, + Nullable extends boolean, + WithAggregations extends boolean, +> = { equals?: NullableIf; in?: DataType[]; notIn?: DataType[]; @@ -271,16 +288,23 @@ type CommonPrimitiveFilter; + not?: PrimitiveFilter; }; -export type StringFilter = +export type StringFilter = | NullableIf - | (CommonPrimitiveFilter & { + | (CommonPrimitiveFilter & { contains?: string; startsWith?: string; endsWith?: string; - } & (ProviderSupportsCaseSensitivity extends true + } & (WithAggregations extends true + ? { + _count?: NumberFilter; + _min?: StringFilter; + _max?: StringFilter; + } + : {}) & + (ProviderSupportsCaseSensitivity extends true ? { mode?: 'default' | 'insensitive'; } @@ -290,27 +314,58 @@ export type NumberFilter< Schema extends SchemaDef, T extends 'Int' | 'Float' | 'Decimal' | 'BigInt', Nullable extends boolean, -> = NullableIf | CommonPrimitiveFilter; + WithAggregations extends boolean, +> = + | NullableIf + | (CommonPrimitiveFilter & + (WithAggregations extends true + ? { + _count?: NumberFilter; + _avg?: NumberFilter; + _sum?: NumberFilter; + _min?: NumberFilter; + _max?: NumberFilter; + } + : {})); -export type DateTimeFilter = +export type DateTimeFilter = | NullableIf - | CommonPrimitiveFilter; + | (CommonPrimitiveFilter & + (WithAggregations extends true + ? { + _count?: NumberFilter; + _min?: DateTimeFilter; + _max?: DateTimeFilter; + } + : {})); -export type BytesFilter = +export type BytesFilter = | NullableIf - | { + | ({ equals?: NullableIf; in?: Uint8Array[]; notIn?: Uint8Array[]; - not?: BytesFilter; - }; + not?: BytesFilter; + } & (WithAggregations extends true + ? { + _count?: NumberFilter; + _min?: BytesFilter; + _max?: BytesFilter; + } + : {})); -export type BooleanFilter = +export type BooleanFilter = | NullableIf - | { + | ({ equals?: NullableIf; - not?: BooleanFilter; - }; + not?: BooleanFilter; + } & (WithAggregations extends true + ? { + _count?: NumberFilter; + _min?: BooleanFilter; + _max?: BooleanFilter; + } + : {})); export type SortOrder = 'asc' | 'desc'; export type NullsOrder = 'first' | 'last'; @@ -340,14 +395,15 @@ export type OrderBy< : {}) & (WithAggregation extends true ? { - _count?: OrderBy; + _count?: OrderBy; + _min?: MinMaxInput; + _max?: MinMaxInput; } & (NumericFields extends never ? {} : { - _avg?: SumAvgInput; - _sum?: SumAvgInput; - _min?: MinMaxInput; - _max?: MinMaxInput; + // aggregations specific to numeric fields + _avg?: SumAvgInput; + _sum?: SumAvgInput; }) : {}); @@ -931,13 +987,13 @@ export type AggregateArgs>; } & { _count?: true | CountAggregateInput; + _min?: MinMaxInput; + _max?: MinMaxInput; } & (NumericFields extends never ? {} : { - _avg?: SumAvgInput; - _sum?: SumAvgInput; - _min?: MinMaxInput; - _max?: MinMaxInput; + _avg?: SumAvgInput; + _sum?: SumAvgInput; }); type NumericFields> = keyof { @@ -952,16 +1008,16 @@ type NumericFields> = : never]: GetModelField; }; -type SumAvgInput> = { - [Key in NumericFields]?: true; +type SumAvgInput, ValueType> = { + [Key in NumericFields]?: ValueType; }; -type MinMaxInput> = { +type MinMaxInput, ValueType> = { [Key in GetModelFields as FieldIsArray extends true ? never : FieldIsRelation extends true ? never - : Key]?: true; + : Key]?: ValueType; }; export type AggregateResult< @@ -1006,21 +1062,28 @@ type AggCommonOutput = Input extends true // #region GroupBy +type GroupByHaving> = Omit< + WhereInput, + '$expr' +>; + export type GroupByArgs> = { where?: WhereInput; orderBy?: OrArray>; by: NonRelationFields | NonEmptyArray>; - having?: WhereInput; + having?: GroupByHaving; take?: number; skip?: number; + // aggregations _count?: true | CountAggregateInput; + _min?: MinMaxInput; + _max?: MinMaxInput; } & (NumericFields extends never ? {} : { - _avg?: SumAvgInput; - _sum?: SumAvgInput; - _min?: MinMaxInput; - _max?: MinMaxInput; + // aggregations specific to numeric fields + _avg?: SumAvgInput; + _sum?: SumAvgInput; }); export type GroupByResult< diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index d6bb705e..a38cb479 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -5,7 +5,7 @@ import { match, P } from 'ts-pattern'; import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import { enumerate } from '../../../utils/enumerate'; import type { OrArray } from '../../../utils/type-utils'; -import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; +import { AGGREGATE_OPERATORS, DELEGATE_JOINED_FIELD_PREFIX, LOGICAL_COMBINATORS } from '../../constants'; import type { BooleanFilter, BytesFilter, @@ -18,6 +18,7 @@ import type { import { InternalError, QueryError } from '../../errors'; import type { ClientOptions } from '../../options'; import { + aggregate, buildFieldRef, buildJoinPairs, flattenCompoundUniqueFilters, @@ -83,7 +84,7 @@ export abstract class BaseCrudDialect { continue; } - if (key === 'AND' || key === 'OR' || key === 'NOT') { + if (this.isLogicalCombinator(key)) { result = this.and(eb, result, this.buildCompositeFilter(eb, model, modelAlias, key, payload)); continue; } @@ -118,11 +119,15 @@ export abstract class BaseCrudDialect { return result; } + private isLogicalCombinator(key: string): key is (typeof LOGICAL_COMBINATORS)[number] { + return LOGICAL_COMBINATORS.includes(key as any); + } + protected buildCompositeFilter( eb: ExpressionBuilder, model: string, modelAlias: string, - key: 'AND' | 'OR' | 'NOT', + key: (typeof LOGICAL_COMBINATORS)[number], payload: any, ): Expression { return match(key) @@ -500,6 +505,20 @@ export abstract class BaseCrudDialect { .with('gt', () => eb(lhs, '>', rhs)) .with('gte', () => eb(lhs, '>=', rhs)) .with('not', () => eb.not(recurse(value))) + // aggregations + .with(P.union(...AGGREGATE_OPERATORS), (op) => { + const innerResult = this.buildStandardFilter( + eb, + type, + value, + aggregate(eb, lhs, op), + getRhs, + recurse, + throwIfInvalid, + ); + consumedKeys.push(...innerResult.consumedKeys); + return this.and(eb, ...innerResult.conditions); + }) .otherwise(() => { if (throwIfInvalid) { throw new QueryError(`Invalid filter key: ${op}`); @@ -520,7 +539,7 @@ export abstract class BaseCrudDialect { private buildStringFilter( eb: ExpressionBuilder, fieldRef: Expression, - payload: StringFilter, + payload: StringFilter, ) { let mode: 'default' | 'insensitive' | undefined; if (payload && typeof payload === 'object' && 'mode' in payload) { @@ -533,7 +552,7 @@ export abstract class BaseCrudDialect { payload, mode === 'insensitive' ? eb.fn('lower', [fieldRef]) : fieldRef, (value) => this.prepStringCasing(eb, value, mode), - (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), + (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), ); if (payload && typeof payload === 'object') { @@ -610,7 +629,7 @@ export abstract class BaseCrudDialect { private buildBooleanFilter( eb: ExpressionBuilder, fieldRef: Expression, - payload: BooleanFilter, + payload: BooleanFilter, ) { const { conditions } = this.buildStandardFilter( eb, @@ -618,7 +637,7 @@ export abstract class BaseCrudDialect { payload, fieldRef, (value) => this.transformPrimitive(value, 'Boolean', false), - (value) => this.buildBooleanFilter(eb, fieldRef, value as BooleanFilter), + (value) => this.buildBooleanFilter(eb, fieldRef, value as BooleanFilter), true, ['equals', 'not'], ); @@ -628,7 +647,7 @@ export abstract class BaseCrudDialect { private buildDateTimeFilter( eb: ExpressionBuilder, fieldRef: Expression, - payload: DateTimeFilter, + payload: DateTimeFilter, ) { const { conditions } = this.buildStandardFilter( eb, @@ -636,20 +655,24 @@ export abstract class BaseCrudDialect { payload, fieldRef, (value) => this.transformPrimitive(value, 'DateTime', false), - (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), + (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), true, ); return this.and(eb, ...conditions); } - private buildBytesFilter(eb: ExpressionBuilder, fieldRef: Expression, payload: BytesFilter) { + private buildBytesFilter( + eb: ExpressionBuilder, + fieldRef: Expression, + payload: BytesFilter, + ) { const conditions = this.buildStandardFilter( eb, 'Bytes', payload, fieldRef, (value) => this.transformPrimitive(value, 'Bytes', false), - (value) => this.buildBytesFilter(eb, fieldRef, value as BytesFilter), + (value) => this.buildBytesFilter(eb, fieldRef, value as BytesFilter), true, ['equals', 'in', 'notIn', 'not'], ); @@ -704,7 +727,7 @@ export abstract class BaseCrudDialect { for (const [k, v] of Object.entries(value)) { invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( - (eb) => eb.fn(field.slice(1), [sql.ref(k)]), + (eb) => aggregate(eb, sql.ref(`${modelAlias}.${k}`), field as AGGREGATE_OPERATORS), sql.raw(this.negateSort(v, negated)), ); } diff --git a/packages/runtime/src/client/crud/operations/group-by.ts b/packages/runtime/src/client/crud/operations/group-by.ts index 009fa3b5..cdf99b8c 100644 --- a/packages/runtime/src/client/crud/operations/group-by.ts +++ b/packages/runtime/src/client/crud/operations/group-by.ts @@ -44,9 +44,9 @@ export class GroupByOperationHandler extends BaseOpera return subQuery.as('$sub'); }); + // groupBy const bys = typeof parsedArgs.by === 'string' ? [parsedArgs.by] : (parsedArgs.by as string[]); - - query = query.groupBy(bys as any); + query = query.groupBy(bys.map((by) => sql.ref(`$sub.${by}`))); // orderBy if (parsedArgs.orderBy) { @@ -54,7 +54,7 @@ export class GroupByOperationHandler extends BaseOpera } if (parsedArgs.having) { - query = query.having((eb1) => this.dialect.buildFilter(eb1, this.model, '$sub', parsedArgs.having)); + query = query.having((eb) => this.dialect.buildFilter(eb, this.model, '$sub', parsedArgs.having)); } // select all by fields diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index c586abfd..5b660aee 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -4,7 +4,9 @@ import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodType } from 'zod'; import { type BuiltinType, type EnumDef, type FieldDef, type GetModels, type SchemaDef } from '../../schema'; -import { NUMERIC_FIELD_TYPES } from '../constants'; +import { enumerate } from '../../utils/enumerate'; +import { extractFields } from '../../utils/object-utils'; +import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../constants'; import { type AggregateArgs, type CountArgs, @@ -231,10 +233,10 @@ export class InputValidator { } else { return match(type) .with('String', () => z.string()) - .with('Int', () => z.number()) + .with('Int', () => z.int()) .with('Float', () => z.number()) .with('Boolean', () => z.boolean()) - .with('BigInt', () => z.union([z.number(), z.bigint()])) + .with('BigInt', () => z.union([z.int(), z.bigint()])) .with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()])) .with('DateTime', () => z.union([z.date(), z.string().datetime()])) .with('Bytes', () => z.instanceof(Uint8Array)) @@ -268,7 +270,12 @@ export class InputValidator { return schema; } - private makeWhereSchema(model: string, unique: boolean, withoutRelationFields = false): ZodType { + private makeWhereSchema( + model: string, + unique: boolean, + withoutRelationFields = false, + withAggregations = false, + ): ZodType { const modelDef = getModel(this.schema, model); if (!modelDef) { throw new QueryError(`Model "${model}" not found in schema`); @@ -313,14 +320,18 @@ export class InputValidator { if (enumDef) { // enum if (Object.keys(enumDef).length > 0) { - fieldSchema = this.makeEnumFilterSchema(enumDef, !!fieldDef.optional); + fieldSchema = this.makeEnumFilterSchema(enumDef, !!fieldDef.optional, withAggregations); } } else if (fieldDef.array) { // array field fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); } else { // primitive field - fieldSchema = this.makePrimitiveFilterSchema(fieldDef.type as BuiltinType, !!fieldDef.optional); + fieldSchema = this.makePrimitiveFilterSchema( + fieldDef.type as BuiltinType, + !!fieldDef.optional, + withAggregations, + ); } } @@ -344,7 +355,7 @@ export class InputValidator { if (enumDef) { // enum if (Object.keys(enumDef).length > 0) { - fieldSchema = this.makeEnumFilterSchema(enumDef, !!def.optional); + fieldSchema = this.makeEnumFilterSchema(enumDef, !!def.optional, false); } else { fieldSchema = z.never(); } @@ -353,6 +364,7 @@ export class InputValidator { fieldSchema = this.makePrimitiveFilterSchema( def.type as BuiltinType, !!def.optional, + false, ); } return [key, fieldSchema]; @@ -407,20 +419,16 @@ export class InputValidator { return result; } - private makeEnumFilterSchema(enumDef: EnumDef, optional: boolean) { + private makeEnumFilterSchema(enumDef: EnumDef, optional: boolean, withAggregations: boolean) { const baseSchema = z.enum(Object.keys(enumDef) as [string, ...string[]]); - const components = this.makeCommonPrimitiveFilterComponents(baseSchema, optional, () => - z.lazy(() => this.makeEnumFilterSchema(enumDef, optional)), + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.lazy(() => this.makeEnumFilterSchema(enumDef, optional, withAggregations)), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, ); - return z.union([ - this.nullableIf(baseSchema, optional), - z.strictObject({ - equals: components.equals, - in: components.in, - notIn: components.notIn, - not: components.not, - }), - ]); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } private makeArrayFilterSchema(type: BuiltinType) { @@ -433,20 +441,20 @@ export class InputValidator { }); } - private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean) { + private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean, withAggregations: boolean) { if (this.schema.typeDefs && type in this.schema.typeDefs) { // typed JSON field return this.makeTypeDefFilterSchema(type, optional); } return ( match(type) - .with('String', () => this.makeStringFilterSchema(optional)) + .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional), + this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional, withAggregations), ) - .with('Boolean', () => this.makeBooleanFilterSchema(optional)) - .with('DateTime', () => this.makeDateTimeFilterSchema(optional)) - .with('Bytes', () => this.makeBytesFilterSchema(optional)) + .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) + .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) + .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) // TODO: JSON filters .with('Json', () => z.any()) .with('Unsupported', () => z.never()) @@ -459,40 +467,48 @@ export class InputValidator { return z.never(); } - private makeDateTimeFilterSchema(optional: boolean): ZodType { - return this.makeCommonPrimitiveFilterSchema(z.union([z.string().datetime(), z.date()]), optional, () => - z.lazy(() => this.makeDateTimeFilterSchema(optional)), + private makeDateTimeFilterSchema(optional: boolean, withAggregations: boolean): ZodType { + return this.makeCommonPrimitiveFilterSchema( + z.union([z.iso.datetime(), z.date()]), + optional, + () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), + withAggregations ? ['_count', '_min', '_max'] : undefined, ); } - private makeBooleanFilterSchema(optional: boolean): ZodType { - return z.union([ - this.nullableIf(z.boolean(), optional), - z.strictObject({ - equals: this.nullableIf(z.boolean(), optional).optional(), - not: z.lazy(() => this.makeBooleanFilterSchema(optional)).optional(), - }), - ]); + private makeBooleanFilterSchema(optional: boolean, withAggregations: boolean): ZodType { + const components = this.makeCommonPrimitiveFilterComponents( + z.boolean(), + optional, + () => z.lazy(() => this.makeBooleanFilterSchema(optional, withAggregations)), + ['equals', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, + ); + return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); } - private makeBytesFilterSchema(optional: boolean): ZodType { + private makeBytesFilterSchema(optional: boolean, withAggregations: boolean): ZodType { const baseSchema = z.instanceof(Uint8Array); - const components = this.makeCommonPrimitiveFilterComponents(baseSchema, optional, () => - z.instanceof(Uint8Array), + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.instanceof(Uint8Array), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, ); - return z.union([ - this.nullableIf(baseSchema, optional), - z.strictObject({ - equals: components.equals, - in: components.in, - notIn: components.notIn, - not: components.not, - }), - ]); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } - private makeCommonPrimitiveFilterComponents(baseSchema: ZodType, optional: boolean, makeThis: () => ZodType) { - return { + private makeCommonPrimitiveFilterComponents( + baseSchema: ZodType, + optional: boolean, + makeThis: () => ZodType, + supportedOperators: string[] | undefined = undefined, + withAggregations: Array<'_count' | '_avg' | '_sum' | '_min' | '_max'> | undefined = undefined, + ) { + const commonAggSchema = () => + this.makeCommonPrimitiveFilterSchema(baseSchema, false, makeThis, undefined).optional(); + let result = { equals: this.nullableIf(baseSchema.optional(), optional), notEquals: this.nullableIf(baseSchema.optional(), optional), in: baseSchema.array().optional(), @@ -502,28 +518,54 @@ export class InputValidator { gt: baseSchema.optional(), gte: baseSchema.optional(), not: makeThis().optional(), + ...(withAggregations?.includes('_count') + ? { _count: this.makeNumberFilterSchema(z.int(), false, false).optional() } + : {}), + ...(withAggregations?.includes('_avg') ? { _avg: commonAggSchema() } : {}), + ...(withAggregations?.includes('_sum') ? { _sum: commonAggSchema() } : {}), + ...(withAggregations?.includes('_min') ? { _min: commonAggSchema() } : {}), + ...(withAggregations?.includes('_max') ? { _max: commonAggSchema() } : {}), }; + if (supportedOperators) { + const keys = [...supportedOperators, ...(withAggregations ?? [])]; + result = extractFields(result, keys) as typeof result; + } + return result; } - private makeCommonPrimitiveFilterSchema(baseSchema: ZodType, optional: boolean, makeThis: () => ZodType) { + private makeCommonPrimitiveFilterSchema( + baseSchema: ZodType, + optional: boolean, + makeThis: () => ZodType, + withAggregations: Array | undefined = undefined, + ): z.ZodType { return z.union([ this.nullableIf(baseSchema, optional), - z.strictObject(this.makeCommonPrimitiveFilterComponents(baseSchema, optional, makeThis)), + z.strictObject( + this.makeCommonPrimitiveFilterComponents(baseSchema, optional, makeThis, undefined, withAggregations), + ), ]); } - private makeNumberFilterSchema(baseSchema: ZodType, optional: boolean): ZodType { - return this.makeCommonPrimitiveFilterSchema(baseSchema, optional, () => - z.lazy(() => this.makeNumberFilterSchema(baseSchema, optional)), + private makeNumberFilterSchema(baseSchema: ZodType, optional: boolean, withAggregations: boolean): ZodType { + return this.makeCommonPrimitiveFilterSchema( + baseSchema, + optional, + () => z.lazy(() => this.makeNumberFilterSchema(baseSchema, optional, withAggregations)), + withAggregations ? ['_count', '_avg', '_sum', '_min', '_max'] : undefined, ); } - private makeStringFilterSchema(optional: boolean): ZodType { + private makeStringFilterSchema(optional: boolean, withAggregations: boolean): ZodType { return z.union([ this.nullableIf(z.string(), optional), z.strictObject({ - ...this.makeCommonPrimitiveFilterComponents(z.string(), optional, () => - z.lazy(() => this.makeStringFilterSchema(optional)), + ...this.makeCommonPrimitiveFilterComponents( + z.string(), + optional, + () => z.lazy(() => this.makeStringFilterSchema(optional, withAggregations)), + undefined, + withAggregations ? ['_count', '_min', '_max'] : undefined, ), startsWith: z.string().optional(), endsWith: z.string().optional(), @@ -973,7 +1015,7 @@ export class InputValidator { return z.object({ where: this.makeWhereSchema(model, false).optional(), data: this.makeUpdateDataSchema(model, [], true), - limit: z.number().int().nonnegative().optional(), + limit: z.int().nonnegative().optional(), }); } @@ -1113,7 +1155,7 @@ export class InputValidator { return z .object({ where: this.makeWhereSchema(model, false).optional(), - limit: z.number().int().nonnegative().optional(), + limit: z.int().nonnegative().optional(), }) .optional(); @@ -1214,7 +1256,7 @@ export class InputValidator { where: this.makeWhereSchema(model, false).optional(), orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), by: this.orArray(z.enum(nonRelationFields), true), - having: this.makeWhereSchema(model, false, true).optional(), + having: this.makeHavingSchema(model).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), _count: this.makeCountAggregateInputSchema(model).optional(), @@ -1223,26 +1265,41 @@ export class InputValidator { _min: this.makeMinMaxInputSchema(model).optional(), _max: this.makeMinMaxInputSchema(model).optional(), }); + + // fields used in `having` must be either in the `by` list, or aggregations schema = schema.refine((value) => { const bys = typeof value.by === 'string' ? [value.by] : value.by; - if ( - value.having && - Object.keys(value.having) - .filter((f) => !f.startsWith('_')) - .some((key) => !bys.includes(key)) - ) { - return false; - } else { - return true; + if (value.having && typeof value.having === 'object') { + for (const [key, val] of Object.entries(value.having)) { + if (AGGREGATE_OPERATORS.includes(key as any)) { + continue; + } + if (bys.includes(key)) { + continue; + } + // we have a key not mentioned in `by`, in this case it must only use + // aggregations in the condition + + // 1. payload must be an object + if (!val || typeof val !== 'object') { + return false; + } + // 2. payload must only contain aggregations + if (!this.onlyAggregationFields(val)) { + return false; + } + } } + return true; }, 'fields in "having" must be in "by"'); + // fields used in `orderBy` must be either in the `by` list, or aggregations schema = schema.refine((value) => { const bys = typeof value.by === 'string' ? [value.by] : value.by; if ( value.orderBy && Object.keys(value.orderBy) - .filter((f) => !f.startsWith('_')) + .filter((f) => !AGGREGATE_OPERATORS.includes(f as AGGREGATE_OPERATORS)) .some((key) => !bys.includes(key)) ) { return false; @@ -1254,16 +1311,37 @@ export class InputValidator { return schema; } + private onlyAggregationFields(val: object) { + for (const [key, value] of Object.entries(val)) { + if (AGGREGATE_OPERATORS.includes(key as any)) { + // aggregation field + continue; + } + if (LOGICAL_COMBINATORS.includes(key as any)) { + // logical operators + if (enumerate(value).every((v) => this.onlyAggregationFields(v))) { + continue; + } + } + return false; + } + return true; + } + + private makeHavingSchema(model: GetModels) { + return this.makeWhereSchema(model, false, true, true); + } + // #endregion // #region Helpers private makeSkipSchema() { - return z.number().int().nonnegative(); + return z.int().nonnegative(); } private makeTakeSchema() { - return z.number().int(); + return z.int(); } private refineForSelectIncludeMutuallyExclusive(schema: ZodType) { diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index 7302933b..1cad9bb1 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -1,5 +1,8 @@ -import type { ExpressionBuilder, ExpressionWrapper } from 'kysely'; +import type { Expression, ExpressionBuilder, ExpressionWrapper } from 'kysely'; +import { match } from 'ts-pattern'; import { ExpressionUtils, type FieldDef, type GetModels, type ModelDef, type SchemaDef } from '../schema'; +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'; @@ -282,15 +285,6 @@ export function safeJSONStringify(value: unknown) { }); } -export function extractFields(object: any, fields: string[]) { - return fields.reduce((acc: any, field) => { - if (field in object) { - acc[field] = object[field]; - } - return acc; - }, {}); -} - export function extractIdFields(entity: any, schema: SchemaDef, model: string) { const idFields = getIdFields(schema, model); return extractFields(entity, idFields); @@ -323,3 +317,17 @@ export function getDelegateDescendantModels( }); return [...collected]; } + +export function aggregate( + eb: ExpressionBuilder, + expr: Expression, + op: AGGREGATE_OPERATORS, +): Expression { + return match(op) + .with('_count', () => eb.fn.count(expr)) + .with('_sum', () => eb.fn.sum(expr)) + .with('_avg', () => eb.fn.avg(expr)) + .with('_min', () => eb.fn.min(expr)) + .with('_max', () => eb.fn.max(expr)) + .exhaustive(); +} diff --git a/packages/runtime/test/client-api/group-by.test.ts b/packages/runtime/test/client-api/group-by.test.ts index 98c155fc..a9c0d56a 100644 --- a/packages/runtime/test/client-api/group-by.test.ts +++ b/packages/runtime/test/client-api/group-by.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ClientContract } from '../../src/client'; import { schema } from '../schemas/basic'; import { createClientSpecs } from './client-specs'; -import { createUser } from './utils'; +import { createPosts, createUser } from './utils'; const PG_DB_NAME = 'client-api-group-by-tests'; @@ -33,6 +33,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createCl name: 'User', role: 'USER', }); + await createPosts(client, '1'); await expect( client.user.groupBy({ @@ -67,7 +68,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createCl take: -2, orderBy: { email: 'desc' }, }), - ).resolves.toEqual([{ email: 'u2@test.com' }, { email: 'u1@test.com' }]); + ).resolves.toEqual(expect.arrayContaining([{ email: 'u2@test.com' }, { email: 'u1@test.com' }])); await expect( client.user.groupBy({ @@ -93,6 +94,18 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createCl { name: 'User', role: 'USER', _count: 2 }, { name: 'Admin', role: 'ADMIN', _count: 1 }, ]); + + await expect( + client.post.groupBy({ + by: ['published'], + _count: true, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { published: true, _count: 1 }, + { published: false, _count: 1 }, + ]), + ); }); it('works with multiple bys', async () => { @@ -130,12 +143,14 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createCl it('works with different types of aggregation', async () => { await client.profile.create({ data: { + id: '1', age: 10, bio: 'bio', }, }); await client.profile.create({ data: { + id: '2', age: 20, bio: 'bio', }, @@ -144,26 +159,108 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createCl await expect( client.profile.groupBy({ by: ['bio'], - _count: { age: true }, + _count: { age: true, id: true }, _avg: { age: true }, _sum: { age: true }, - _min: { age: true }, - _max: { age: true }, + _min: { age: true, id: true }, + _max: { age: true, id: true }, }), ).resolves.toEqual( expect.arrayContaining([ { bio: 'bio', - _count: { age: 2 }, + _count: { age: 2, id: 2 }, _avg: { age: 15 }, _sum: { age: 30 }, - _min: { age: 10 }, - _max: { age: 20 }, + _min: { age: 10, id: '1' }, + _max: { age: 20, id: '2' }, }, ]), ); }); + it('works with using aggregations in having', async () => { + await client.profile.create({ + data: { + id: '1', + age: 10, + bio: 'bio1', + }, + }); + await client.profile.create({ + data: { + id: '2', + age: 20, + bio: 'bio1', + }, + }); + await client.profile.create({ + data: { + id: '3', + age: 30, + bio: 'bio2', + }, + }); + await client.profile.create({ + data: { + id: '4', + age: 40, + bio: 'bio2', + }, + }); + + await expect( + client.profile.groupBy({ + by: ['bio'], + having: { + age: { _avg: { gt: 15, lt: 50 }, _sum: { equals: 70 } }, + }, + }), + ).resolves.toEqual(expect.arrayContaining([{ bio: 'bio2' }])); + }); + + it('works with using aggregations in orderBy', async () => { + await client.profile.create({ + data: { + id: '1', + age: 10, + bio: 'bio1', + }, + }); + await client.profile.create({ + data: { + id: '2', + age: 20, + bio: 'bio1', + }, + }); + await client.profile.create({ + data: { + id: '3', + age: 30, + bio: 'bio2', + }, + }); + await client.profile.create({ + data: { + id: '4', + age: 40, + bio: 'bio2', + }, + }); + + await expect( + client.profile.groupBy({ + by: ['bio'], + orderBy: { + _avg: { + age: 'desc', + }, + }, + }), + ).resolves.toEqual(expect.arrayContaining([{ bio: 'bio2' }])); + }); + it('complains about fields in having that are not in by', async () => { await expect( client.profile.groupBy({ diff --git a/packages/runtime/test/schemas/basic/schema.ts b/packages/runtime/test/schemas/basic/schema.ts index d594432e..8fd0b900 100644 --- a/packages/runtime/test/schemas/basic/schema.ts +++ b/packages/runtime/test/schemas/basic/schema.ts @@ -61,6 +61,11 @@ export const schema = { type: "Profile", optional: true, relation: { opposite: "user" } + }, + meta: { + name: "meta", + type: "Json", + optional: true } }, attributes: [ diff --git a/packages/runtime/test/schemas/basic/schema.zmodel b/packages/runtime/test/schemas/basic/schema.zmodel index e65f74e6..d3ace0a1 100644 --- a/packages/runtime/test/schemas/basic/schema.zmodel +++ b/packages/runtime/test/schemas/basic/schema.zmodel @@ -24,6 +24,7 @@ model User with CommonFields { role Role @default(USER) posts Post[] profile Profile? + meta Json? // Access policies @@allow('all', auth().id == id) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 62c376ed..31d01fa1 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 93785acb..bd38910f 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-alpha.19", + "version": "3.0.0-alpha.20", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 354b08dc..aeabf58b 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "ZenStack Test Tools", "type": "module", "scripts": { @@ -37,7 +37,7 @@ "typescript": "catalog:" }, "peerDependencies": { - "better-sqlite3": "^11.8.1", + "better-sqlite3": "^12.2.0", "pg": "^8.13.1" }, "devDependencies": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index f0d6d30d..331ad959 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 89977323..fe2127a0 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index cae1d515..895d1885 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "", "type": "module", "main": "index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccb8d834..bf8203d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,8 +130,8 @@ importers: specifier: workspace:* version: link:../vitest-config better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + specifier: ^12.2.0 + version: 12.2.0 tmp: specifier: 'catalog:' version: 0.2.3 @@ -289,8 +289,8 @@ importers: version: 4.0.5 devDependencies: '@types/better-sqlite3': - specifier: ^7.0.0 - version: 7.6.12 + specifier: ^7.6.13 + version: 7.6.13 '@types/pg': specifier: ^8.0.0 version: 8.11.11 @@ -372,8 +372,8 @@ importers: specifier: workspace:* version: link:../sdk better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + specifier: ^12.2.0 + version: 12.2.0 glob: specifier: ^11.0.2 version: 11.0.2 @@ -432,15 +432,15 @@ importers: specifier: workspace:* version: link:../../packages/runtime better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + specifier: ^12.2.0 + version: 12.2.0 kysely: specifier: 'catalog:' version: 0.27.6 devDependencies: '@types/better-sqlite3': - specifier: ^7.6.12 - version: 7.6.12 + specifier: ^7.6.13 + version: 7.6.13 '@zenstackhq/cli': specifier: workspace:* version: link:../../packages/cli @@ -1083,9 +1083,6 @@ packages: peerDependencies: react: ^18 || ^19 - '@types/better-sqlite3@7.6.12': - resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} - '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} @@ -1255,9 +1252,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-sqlite3@11.8.1: - resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} - better-sqlite3@12.2.0: resolution: {integrity: sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==} engines: {node: 20.x || 22.x || 23.x || 24.x} @@ -3003,10 +2997,6 @@ snapshots: '@tanstack/query-core': 5.81.0 react: 19.1.0 - '@types/better-sqlite3@7.6.12': - dependencies: - '@types/node': 20.17.24 - '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 20.17.24 @@ -3211,11 +3201,6 @@ snapshots: base64-js@1.5.1: {} - better-sqlite3@11.8.1: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - better-sqlite3@12.2.0: dependencies: bindings: 1.5.0 diff --git a/samples/blog/package.json b/samples/blog/package.json index 75d07851..b31f346d 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "description": "", "main": "index.js", "scripts": { @@ -14,11 +14,11 @@ "license": "MIT", "dependencies": { "@zenstackhq/runtime": "workspace:*", - "better-sqlite3": "^11.8.1", + "better-sqlite3": "^12.2.0", "kysely": "catalog:" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.12", + "@types/better-sqlite3": "^7.6.13", "prisma": "catalog:", "@zenstackhq/cli": "workspace:*", "@zenstackhq/typescript-config": "workspace:*" diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 76daf5b7..4da5fbb2 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.19", + "version": "3.0.0-alpha.20", "private": true, "type": "module", "scripts": {