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
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
- [x] Inject "on conflict do update"
- [x] `check` function
- [ ] Custom functions
- [ ] Accessing tables not in the schema
- [x] Accessing tables not in the schema
- [x] Migration
- [ ] Databases
- [x] SQLite
Expand Down
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.30",
"version": "3.0.0-beta.31",
"description": "ZenStack",
"packageManager": "[email protected]",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-adapters/better-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/better-auth",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"type": "module",
"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.30",
"version": "3.0.0-beta.31",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/actions/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ model Post {
`;

export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/orm';
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
import SQLite from 'better-sqlite3';
import { SqliteDialect } from 'kysely';
import { schema } from './zenstack/schema';

async function main() {
const client = new ZenStackClient(schema, {
const db = new ZenStackClient(schema, {
dialect: new SqliteDialect({
database: new SQLite('./zenstack/dev.db'),
}),
});
const user = await client.user.create({
const user = await db.user.create({
data: {
email: '[email protected]',
posts: {
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.30",
"version": "3.0.0-beta.31",
"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.30",
"version": "3.0.0-beta.31",
"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.30",
"version": "3.0.0-beta.31",
"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.30",
"version": "3.0.0-beta.31",
"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.30",
"version": "3.0.0-beta.31",
"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.30",
"version": "3.0.0-beta.31",
"description": "Create a new ZenStack project",
"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.30",
"version": "3.0.0-beta.31",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
32 changes: 23 additions & 9 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, isConfigArrayExpr, isInvocationExpr, isLiteralExpr } from '../generated/ast';
import { DataSource, isConfigArrayExpr, isDataModel, isEnum, isInvocationExpr, isLiteralExpr } from '../generated/ast';
import { getStringLiteral } from '../utils';
import { validateDuplicatedDeclarations, type AstValidator } from './common';

Expand Down Expand Up @@ -70,14 +70,28 @@ export default class DataSourceValidator implements AstValidator<DataSource> {
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,
});
} else {
const schemasArray = schemasValue.items.map((e) => getStringLiteral(e)!);

if (defaultSchemaValue) {
// validate `defaultSchema` is included in `schemas`
if (!schemasArray.includes(defaultSchemaValue)) {
accept('error', `"${defaultSchemaValue}" must be included in the "schemas" array`, {
node: schemasField,
});
}
} else {
// if no explicit default schema is specified, and there are models or enums without '@@schema',
// "public" is implicitly used, so it must be included in the "schemas" array
const hasImplicitPublicSchema = ds.$container.declarations.some(
(d) => (isDataModel(d) || isEnum(d)) && !d.attributes.some((a) => a.decl.$refText === '@@schema'),
);
if (hasImplicitPublicSchema && !schemasArray.includes('public')) {
accept('error', `"public" must be included in the "schemas" array`, {
node: schemasField,
});
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion 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.30",
"version": "3.0.0-beta.31",
"description": "ZenStack ORM",
"type": "module",
"scripts": {
Expand Down
21 changes: 17 additions & 4 deletions packages/orm/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
getDelegateDescendantModels,
getManyToManyRelation,
isRelationField,
isTypeDef,
requireField,
requireIdFields,
requireModel,
Expand Down Expand Up @@ -52,13 +53,25 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
invariant(false, 'should not reach here: AnyNull is not a valid input value');
}

if (Array.isArray(value)) {
// node-pg incorrectly handles array values passed to non-array JSON fields,
// the workaround is to JSON stringify the value
// https://github.com/brianc/node-postgres/issues/374

if (isTypeDef(this.schema, type)) {
// type-def fields (regardless array or scalar) are stored as scalar `Json` and
// their input values need to be stringified if not already (i.e., provided in
// default values)
if (typeof value !== 'string') {
return JSON.stringify(value);
} else {
return value;
}
} else if (Array.isArray(value)) {
if (type === 'Json' && !forArrayField) {
// node-pg incorrectly handles array values passed to non-array JSON fields,
// the workaround is to JSON stringify the value
// https://github.com/brianc/node-postgres/issues/374
// scalar `Json` fields need their input stringified
return JSON.stringify(value);
} else {
// `Json[]` fields need their input as array (not stringified)
return value.map((v) => this.transformPrimitive(v, type, false));
}
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/policy/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/plugin-policy",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack Policy Plugin",
"type": "module",
"scripts": {
Expand Down
11 changes: 11 additions & 0 deletions packages/plugins/policy/src/policy-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf

const { mutationModel } = this.getMutationModel(node);

this.tryRejectNonexistentModel(mutationModel);

// --- Pre mutation work ---

if (InsertQueryNode.is(node)) {
Expand Down Expand Up @@ -331,6 +333,8 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
return super.transformJoin(node);
}

this.tryRejectNonexistentModel(table.model);

// build a nested query with policy filter applied
const filter = this.buildPolicyFilter(table.model, table.alias, 'read');

Expand Down Expand Up @@ -872,6 +876,7 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
const extractResult = this.extractTableName(table);
if (extractResult) {
const { model, alias } = extractResult;
this.tryRejectNonexistentModel(model);
const filter = this.buildPolicyFilter(model, alias, 'read');
return acc ? conjunction(this.dialect, [acc, filter]) : filter;
}
Expand Down Expand Up @@ -1011,5 +1016,11 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
return eb.and([aQuery, bQuery]).toOperationNode();
}

private tryRejectNonexistentModel(model: string) {
if (!QueryUtils.hasModel(this.client.$schema, model) && !this.isManyToManyJoinTable(model)) {
throw createRejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS);
}
}

// #endregion
}
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/schema",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack Runtime Schema",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack SDK",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/server",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack automatic CRUD API handlers and server adapters",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/testtools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/testtools",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "ZenStack Test Tools",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/zod/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/zod",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "",
"type": "module",
"main": "index.js",
Expand Down
2 changes: 1 addition & 1 deletion samples/next.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next.js",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"private": true,
"scripts": {
"generate": "zen generate --lite",
Expand Down
2 changes: 1 addition & 1 deletion samples/orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sample-blog",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"description": "",
"main": "index.js",
"private": true,
Expand Down
69 changes: 69 additions & 0 deletions tests/e2e/orm/client-api/pg-custom-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,75 @@ model Foo {
).rejects.toThrow('"mySchema" must be included in the "schemas" array');
});

it('requires implicit public schema to be included in schemas', async () => {
await expect(
createTestClient(
`
datasource db {
provider = 'postgresql'
schemas = ['mySchema']
url = '$DB_URL'
}

enum Role {
ADMIN
USER
}

model Foo {
id Int @id
name String
role Role
@@schema('mySchema')
}

model Bar {
id Int @id
name String
}
`,
),
).rejects.toThrow('"public" must be included in the "schemas" array');
});

it('does not require public schema when all models and enums have explicit schema', async () => {
const db = await createTestClient(
`
datasource db {
provider = 'postgresql'
schemas = ['mySchema']
url = '$DB_URL'
}

enum Role {
ADMIN
USER
@@schema('mySchema')
}

model Foo {
id Int @id
name String
role Role
@@schema('mySchema')
}

model Bar {
id Int @id
name String
@@schema('mySchema')
}
`,
{
provider: 'postgresql',
usePrismaPush: true,
},
);

await expect(db.foo.create({ data: { id: 1, name: 'test', role: 'ADMIN' } })).toResolveTruthy();
await expect(db.bar.create({ data: { id: 1, name: 'test' } })).toResolveTruthy();
});

it('allows specifying schema only on a few models', async () => {
let fooQueriesVerified = false;
let barQueriesVerified = false;
Expand Down
Loading
Loading