Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "ZenStack",
"packageManager": "[email protected]",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.20",
"version": "3.0.0-beta.21",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/tanstack-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/tanstack-query",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "TanStack Query Client for consuming ZenStack v3's CRUD service",
"main": "index.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/config/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"private": true,
"license": "MIT"
}
2 changes: 1 addition & 1 deletion packages/config/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
12 changes: 6 additions & 6 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -454,12 +454,12 @@ attribute @db.JsonB() @@@targetField([JsonField]) @@@prisma

attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma

// /**
// * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema.
// *
// * @param: The name of the database schema.
// */
// attribute @@schema(_ name: String) @@@prisma
/**
* Specifies the schema to use in a multi-schema PostgreSQL database.
*
* @param name: The name of the database schema.
*/
attribute @@schema(_ name: String) @@@prisma

//////////////////////////////////////////////
// Begin validation attributes and functions
Expand Down
12 changes: 10 additions & 2 deletions packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME, type ExpressionContext } from './constants';
import {
InternalAttribute,
isArrayExpr,
isBinaryExpr,
isConfigArrayExpr,
Expand Down Expand Up @@ -173,7 +174,7 @@ export function getRecursiveBases(
bases.forEach((base) => {
// avoid using .ref since this function can be called before linking
const baseDecl = decl.$container.declarations.find(
(d): d is TypeDef | DataModel => isTypeDef(d) || (isDataModel(d) && d.name === base.$refText),
(d): d is TypeDef | DataModel => (isTypeDef(d) || isDataModel(d)) && d.name === base.$refText,
);
if (baseDecl) {
if (!includeDelegate && isDelegateModel(baseDecl)) {
Expand Down Expand Up @@ -321,8 +322,15 @@ function getArray(expr: Expression | ConfigExpr | undefined) {
return isArrayExpr(expr) || isConfigArrayExpr(expr) ? expr.items : undefined;
}

export function getAttributeArg(
attr: DataModelAttribute | DataFieldAttribute | InternalAttribute,
name: string,
): Expression | undefined {
return attr.args.find((arg) => arg.$resolvedParam?.name === name)?.value;
}

export function getAttributeArgLiteral<T extends string | number | boolean>(
attr: DataModelAttribute | DataFieldAttribute,
attr: DataModelAttribute | DataFieldAttribute | InternalAttribute,
name: string,
): T | undefined {
for (const arg of attr.args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { invariant } from '@zenstackhq/common-helpers';
import { AstUtils, type ValidationAcceptor } from 'langium';
import pluralize from 'pluralize';
import type { BinaryExpr, DataModel, Expression } from '../ast';
Expand All @@ -13,14 +14,19 @@ import {
ReferenceExpr,
isArrayExpr,
isAttribute,
isConfigArrayExpr,
isDataField,
isDataModel,
isDataSource,
isEnum,
isLiteralExpr,
isModel,
isReferenceExpr,
isTypeDef,
} from '../generated/ast';
import {
getAllAttributes,
getAttributeArg,
getStringLiteral,
hasAttribute,
isAuthOrAuthMemberAccess,
Expand Down Expand Up @@ -291,7 +297,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
@check('@@index')
@check('@@unique')
private _checkConstraint(attr: AttributeApplication, accept: ValidationAcceptor) {
const fields = attr.args[0]?.value;
const fields = getAttributeArg(attr, 'fields');
const attrName = attr.decl.ref?.name;
if (!fields) {
accept('error', `expects an array of field references`, {
Expand Down Expand Up @@ -331,6 +337,28 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
}
}

@check('@@schema')
private _checkSchema(attr: AttributeApplication, accept: ValidationAcceptor) {
const schemaName = getStringLiteral(attr.args[0]?.value);
invariant(schemaName, `@@schema expects a string literal`);

// verify the schema name is defined in the datasource
const zmodel = AstUtils.getContainerOfType(attr, isModel)!;
const datasource = zmodel.declarations.find(isDataSource);
if (datasource) {
let found = false;
const schemas = datasource.fields.find((f) => f.name === 'schemas');
if (schemas && isConfigArrayExpr(schemas.value)) {
found = schemas.value.items.some((item) => isLiteralExpr(item) && item.value === schemaName);
}
if (!found) {
accept('error', `Schema "${schemaName}" is not defined in the datasource`, {
node: attr,
});
}
}
}

private validatePolicyKinds(
kind: string,
candidates: string[],
Expand Down
68 changes: 50 additions & 18 deletions packages/language/src/validators/datasource-validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ValidationAcceptor } from 'langium';
import { SUPPORTED_PROVIDERS } from '../constants';
import { DataSource, isInvocationExpr } from '../generated/ast';
import { DataSource, isConfigArrayExpr, isInvocationExpr, isLiteralExpr } from '../generated/ast';
import { getStringLiteral } from '../utils';
import { validateDuplicatedDeclarations, type AstValidator } from './common';

Expand All @@ -12,7 +12,6 @@ export default class DataSourceValidator implements AstValidator<DataSource> {
validateDuplicatedDeclarations(ds, ds.fields, accept);
this.validateProvider(ds, accept);
this.validateUrl(ds, accept);
this.validateRelationMode(ds, accept);
}

private validateProvider(ds: DataSource, accept: ValidationAcceptor) {
Expand All @@ -24,20 +23,63 @@ export default class DataSourceValidator implements AstValidator<DataSource> {
return;
}

const value = getStringLiteral(provider.value);
if (!value) {
const providerValue = getStringLiteral(provider.value);
if (!providerValue) {
accept('error', '"provider" must be set to a string literal', {
node: provider.value,
});
} else if (!SUPPORTED_PROVIDERS.includes(value)) {
} else if (!SUPPORTED_PROVIDERS.includes(providerValue)) {
accept(
'error',
`Provider "${value}" is not supported. Choose from ${SUPPORTED_PROVIDERS.map((p) => '"' + p + '"').join(
' | ',
)}.`,
`Provider "${providerValue}" is not supported. Choose from ${SUPPORTED_PROVIDERS.map(
(p) => '"' + p + '"',
).join(' | ')}.`,
{ node: provider.value },
);
}

const defaultSchemaField = ds.fields.find((f) => f.name === 'defaultSchema');
let defaultSchemaValue: string | undefined;
if (defaultSchemaField) {
if (providerValue !== 'postgresql') {
accept('error', '"defaultSchema" is only supported for "postgresql" provider', {
node: defaultSchemaField,
});
}

defaultSchemaValue = getStringLiteral(defaultSchemaField.value);
if (!defaultSchemaValue) {
accept('error', '"defaultSchema" must be a string literal', {
node: defaultSchemaField.value,
});
}
}

const schemasField = ds.fields.find((f) => f.name === 'schemas');
if (schemasField) {
if (providerValue !== 'postgresql') {
accept('error', '"schemas" is only supported for "postgresql" provider', {
node: schemasField,
});
}
const schemasValue = schemasField.value;
if (
!isConfigArrayExpr(schemasValue) ||
!schemasValue.items.every((e) => isLiteralExpr(e) && typeof getStringLiteral(e) === 'string')
) {
accept('error', '"schemas" must be an array of string literals', {
node: schemasField,
});
} else if (
// validate `defaultSchema` is included in `schemas`
defaultSchemaValue &&
!schemasValue.items.some((e) => getStringLiteral(e) === defaultSchemaValue)
) {
accept('error', `"${defaultSchemaValue}" must be included in the "schemas" array`, {
node: schemasField,
});
}
}
}

private validateUrl(ds: DataSource, accept: ValidationAcceptor) {
Expand All @@ -53,14 +95,4 @@ export default class DataSourceValidator implements AstValidator<DataSource> {
});
}
}

private validateRelationMode(ds: DataSource, accept: ValidationAcceptor) {
const field = ds.fields.find((f) => f.name === 'relationMode');
if (field) {
const val = getStringLiteral(field.value);
if (!val || !['foreignKeys', 'prisma'].includes(val)) {
accept('error', '"relationMode" must be set to "foreignKeys" or "prisma"', { node: field.value });
}
}
}
}
4 changes: 2 additions & 2 deletions packages/orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/orm",
"version": "3.0.0-beta.20",
"version": "3.0.0-beta.21",
"description": "ZenStack ORM",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -67,6 +67,7 @@
"@zenstackhq/common-helpers": "workspace:*",
"decimal.js": "catalog:",
"json-stable-stringify": "^1.3.0",
"kysely": "catalog:",
"nanoid": "^5.0.9",
"toposort": "^2.0.2",
"ts-pattern": "catalog:",
Expand All @@ -76,7 +77,6 @@
},
"peerDependencies": {
"better-sqlite3": "catalog:",
"kysely": "catalog:",
"pg": "catalog:",
"zod": "catalog:"
},
Expand Down
29 changes: 25 additions & 4 deletions packages/orm/src/client/executor/name-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,9 @@ export class QueryNameMapper extends OperationNodeTransformer {
mappedTableName = this.mapTableName(scope.model);
}
}

return ReferenceNode.create(
ColumnNode.create(mappedFieldName),
mappedTableName ? TableNode.create(mappedTableName) : undefined,
mappedTableName ? this.createTableNode(mappedTableName, undefined) : undefined,
);
} else {
// no name mapping needed
Expand Down Expand Up @@ -316,7 +315,9 @@ export class QueryNameMapper extends OperationNodeTransformer {
if (!TableNode.is(node)) {
return super.transformNode(node);
}
return TableNode.create(this.mapTableName(node.table.identifier.name));
const mappedName = this.mapTableName(node.table.identifier.name);
const tableSchema = this.getTableSchema(node.table.identifier.name);
return this.createTableNode(mappedName, tableSchema);
}

private getMappedName(def: ModelDef | FieldDef) {
Expand Down Expand Up @@ -362,8 +363,9 @@ export class QueryNameMapper extends OperationNodeTransformer {
const modelName = innerNode.table.identifier.name;
const mappedName = this.mapTableName(modelName);
const finalAlias = alias ?? (mappedName !== modelName ? IdentifierNode.create(modelName) : undefined);
const tableSchema = this.getTableSchema(modelName);
return {
node: this.wrapAlias(TableNode.create(mappedName), finalAlias),
node: this.wrapAlias(this.createTableNode(mappedName, tableSchema), finalAlias),
scope: {
alias: alias ?? IdentifierNode.create(modelName),
model: modelName,
Expand All @@ -384,6 +386,21 @@ export class QueryNameMapper extends OperationNodeTransformer {
}
}

private getTableSchema(model: string) {
if (this.schema.provider.type !== 'postgresql') {
return undefined;
}
let schema = this.schema.provider.defaultSchema ?? 'public';
const schemaAttr = this.schema.models[model]?.attributes?.find((attr) => attr.name === '@@schema');
if (schemaAttr) {
const nameArg = schemaAttr.args?.find((arg) => arg.name === 'name');
if (nameArg && nameArg.value.kind === 'literal') {
schema = nameArg.value.value as string;
}
}
return schema;
}

private createSelectAllFields(model: string, alias: OperationNode | undefined) {
const modelDef = requireModel(this.schema, model);
return this.getModelFields(modelDef).map((fieldDef) => {
Expand Down Expand Up @@ -454,5 +471,9 @@ export class QueryNameMapper extends OperationNodeTransformer {
});
}

private createTableNode(tableName: string, schemaName: string | undefined) {
return schemaName ? TableNode.createWithSchema(schemaName, tableName) : TableNode.create(tableName);
}

// #endregion
}
Loading