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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
- [ ] Array update
- [x] Upsert
- [x] Delete
- [ ] Aggregation
- [x] Aggregation
- [x] Count
- [x] Aggregate
- [x] Group by
Expand All @@ -62,6 +62,8 @@
- [ ] Error system
- [x] Custom table name
- [x] Custom field name
- [ ] Empty AND/OR/NOT behavior
- [ ] Strict undefined check
- [ ] Access Policy
- [ ] Short-circuit pre-create check for scalar-field only policies
- [ ] Polymorphism
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ export async function run(options: Options) {
console.log(`You can now create a ZenStack client with it.

\`\`\`
import { createClient } from '@zenstackhq/runtime';
import { ZenStackClient } from '@zenstackhq/runtime';
import { schema } from '${outputPath}/schema';

const db = createClient(schema);
const db = new ZenStackClient(schema);
\`\`\`
`);
}
Expand Down
31 changes: 30 additions & 1 deletion packages/language/src/validators/datamodel-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import {
type DiagnosticInfo,
type ValidationAcceptor,
} from 'langium';
import { IssueCodes, SCALAR_TYPES } from '../constants';
import {
ArrayExpr,
DataModel,
DataModelField,
Model,
ReferenceExpr,
isDataModel,
isDataSource,
isEnum,
isModel,
isStringLiteral,
isTypeDef,
} from '../generated/ast';
import {
findUpInheritance,
getLiteral,
getModelFieldsWithBases,
getModelIdFields,
getModelUniqueFields,
Expand All @@ -25,7 +30,6 @@ import {
} from '../utils';
import { validateAttributeApplication } from './attribute-application-validator';
import { validateDuplicatedDeclarations, type AstValidator } from './common';
import { IssueCodes, SCALAR_TYPES } from '../constants';

/**
* Validates data model declarations.
Expand Down Expand Up @@ -147,6 +151,19 @@ export default class DataModelValidator implements AstValidator<DataModel> {
);
}

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 }
);
}
}

field.attributes.forEach((attr) =>
validateAttributeApplication(attr, accept)
);
Expand All @@ -162,6 +179,18 @@ export default class DataModelValidator implements AstValidator<DataModel> {
}
}

private getDataSourceProvider(model: Model) {
const dataSource = model.declarations.find(isDataSource);
if (!dataSource) {
return undefined;
}
const provider = dataSource?.fields.find((f) => f.name === 'provider');
if (!provider) {
return undefined;
}
return getLiteral<string>(provider.value);
}

private validateAttributes(dm: DataModel, accept: ValidationAcceptor) {
dm.attributes.forEach((attr) =>
validateAttributeApplication(attr, accept)
Expand Down
21 changes: 21 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@
"default": "./dist/plugins/policy.cjs"
}
},
"./utils/pg-utils": {
"import": {
"types": "./dist/utils/pg-utils.d.ts",
"default": "./dist/utils/pg-utils.js"
},
"require": {
"types": "./dist/utils/pg-utils.d.cts",
"default": "./dist/utils/pg-utils.cjs"
}
},
"./utils/sqlite-utils": {
"import": {
"types": "./dist/utils/sqlite-utils.d.ts",
"default": "./dist/utils/sqlite-utils.js"
},
"require": {
"types": "./dist/utils/sqlite-utils.d.cts",
"default": "./dist/utils/sqlite-utils.cjs"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
Expand All @@ -67,6 +87,7 @@
"decimal.js": "^10.4.3",
"kysely": "^0.27.5",
"nanoid": "^5.0.9",
"pg-connection-string": "^2.9.0",
"tiny-invariant": "^1.3.3",
"ts-pattern": "^5.6.0",
"ulid": "^3.0.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime/src/client/crud/dialects/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,8 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
array: Expression<unknown>
): ExpressionWrapper<any, any, number>;

abstract buildArrayLiteralSQL(values: unknown[]): string;

get supportsUpdateWithLimit() {
return true;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/runtime/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,10 @@ export class PostgresCrudDialect<
): ExpressionWrapper<any, any, number> {
return eb.fn('array_length', [array]);
}

override buildArrayLiteralSQL(values: unknown[]): string {
return `ARRAY[${values.map((v) =>
typeof v === 'string' ? `'${v}'` : v
)}]`;
}
}
4 changes: 4 additions & 0 deletions packages/runtime/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,8 @@ export class SqliteCrudDialect<
): ExpressionWrapper<any, any, number> {
return eb.fn('json_array_length', [array]);
}

override buildArrayLiteralSQL(_values: unknown[]): string {
throw new Error('SQLite does not support array literals');
}
}
12 changes: 10 additions & 2 deletions packages/runtime/src/client/crud/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,10 @@ export class InputValidator<Schema extends SchemaDef> {
fieldDef.type
);

if (fieldDef.array) {
fieldSchema = z.array(fieldSchema).optional();
}

if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) {
fieldSchema = fieldSchema.optional();
}
Expand Down Expand Up @@ -1200,7 +1204,9 @@ export class InputValidator<Schema extends SchemaDef> {
const bys = typeof value.by === 'string' ? [value.by] : value.by;
if (
value.having &&
Object.keys(value.having).some((key) => !bys.includes(key))
Object.keys(value.having)
.filter((f) => !f.startsWith('_'))
.some((key) => !bys.includes(key))
) {
return false;
} else {
Expand All @@ -1212,7 +1218,9 @@ export class InputValidator<Schema extends SchemaDef> {
const bys = typeof value.by === 'string' ? [value.by] : value.by;
if (
value.orderBy &&
Object.keys(value.orderBy).some((key) => !bys.includes(key))
Object.keys(value.orderBy)
.filter((f) => !f.startsWith('_'))
.some((key) => !bys.includes(key))
) {
return false;
} else {
Expand Down
10 changes: 7 additions & 3 deletions packages/runtime/src/client/executor/name-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ export class QueryNameMapper extends OperationNodeTransformer {
for (const selection of selections) {
let selectAllFromModel: string | undefined = undefined;
let isSelectAll = false;
let selectAllWithAlias = false;

if (SelectAllNode.is(selection.selection)) {
selectAllFromModel = this.currentModel;
Expand All @@ -179,7 +178,6 @@ export class QueryNameMapper extends OperationNodeTransformer {
selection.selection.table?.table.identifier.name ??
this.currentModel;
isSelectAll = true;
selectAllWithAlias = true;
}

if (isSelectAll) {
Expand All @@ -190,11 +188,17 @@ export class QueryNameMapper extends OperationNodeTransformer {
contextNode,
selectAllFromModel
);
const fromModelDef = requireModel(
this.schema,
selectAllFromModel
);
const mappedTableName =
this.getMappedName(fromModelDef) ?? selectAllFromModel;
result.push(
...scalarFields.map((fieldName) => {
const fieldRef = ReferenceNode.create(
ColumnNode.create(this.mapFieldName(fieldName)),
TableNode.create(selectAllFromModel)
TableNode.create(mappedTableName)
);
return SelectionNode.create(
this.fieldHasMappedName(fieldName)
Expand Down
9 changes: 6 additions & 3 deletions packages/runtime/src/client/helpers/schema-db-pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
}

const type = fieldDef.type as BuiltinType;
let result = match(type)
let result = match<BuiltinType, ColumnDataType>(type)
.with('String', () => 'text')
.with('Boolean', () => 'boolean')
.with('Int', () => 'integer')
Expand All @@ -192,10 +192,13 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
.otherwise(() => {
throw new Error(`Unsupported field type: ${type}`);
});

if (fieldDef.array) {
result = `${result}[]`;
// Kysely doesn't support array type natively
return sql.raw(`${result}[]`);
} else {
return result as ColumnDataType;
}
return result as ColumnDataType;
}

private addForeignKeyConstraint(
Expand Down
30 changes: 18 additions & 12 deletions packages/runtime/src/plugins/policy/expression-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export type ExpressionTransformerContext<Schema extends SchemaDef> = {
model: GetModels<Schema>;
alias?: string;
operation: CRUD;
thisEntity?: Record<string, ValueNode>;
thisEntity?: Record<string, OperationNode>;
thisEntityRaw?: Record<string, unknown>;
auth?: any;
memberFilter?: OperationNode;
memberSelect?: SelectionNode;
Expand Down Expand Up @@ -210,18 +211,23 @@ export class ExpressionTransformer<Schema extends SchemaDef> {
const right = this.transform(expr.right, context);

if (op === 'in') {
invariant(
ValueListNode.is(right),
'"in" operation requires right operand to be a value list'
);
if (this.isNullNode(left)) {
return this.transformValue(false, 'Boolean');
} else {
return BinaryOperationNode.create(
left,
OperatorNode.create('in'),
right
);
if (ValueListNode.is(right)) {
return BinaryOperationNode.create(
left,
OperatorNode.create('in'),
right
);
} else {
// array contains
return BinaryOperationNode.create(
left,
OperatorNode.create('='),
FunctionNode.create('any', [right])
);
}
}
}

Expand Down Expand Up @@ -444,8 +450,8 @@ export class ExpressionTransformer<Schema extends SchemaDef> {
}

if (Expression.isField(arg)) {
return context.thisEntity
? eb.val(context.thisEntity[arg.field]?.value)
return context.thisEntityRaw
? eb.val(context.thisEntityRaw[arg.field])
: eb.ref(arg.field);
}

Expand Down
Loading