Skip to content

merge dev to main #494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 15, 2023
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
56 changes: 43 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@

## What it is

ZenStack is a toolkit that simplifies the development of a web app's backend. It supercharges [Prisma ORM](https://prisma.io) with a powerful access control layer and unleashes its full potential for web development.
ZenStack is a Node.js/TypeScript toolkit that simplifies the development of a web app's backend. It supercharges [Prisma ORM](https://prisma.io) with a powerful access control layer and unleashes its full potential for full-stack development.

Our goal is to let you save time writing boilerplate code and focus on building real features!

## How it works

ZenStack extended Prisma schema language for supporting custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma.
ZenStack incrementally extends Prisma's power with the following four layers:

### 1. ZModel - an extended Prisma schema language

ZenStack introduces a data modeling language called "ZModel" - a superset of Prisma schema language. It extended Prisma schema with custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma.

```prisma
// schema.zmodel
Expand All @@ -47,34 +51,58 @@ model Post {
}
```

At runtime, transparent proxies are created around Prisma clients for intercepting queries and mutations to enforce access policies. Moreover, framework integration packages help you wrap an access-control-enabled Prisma client into backend APIs that can be safely called from the frontend.
The `zenstack` CLI transpiles the ZModel into a standard Prisma schema, which you can use with the regular Prisma workflows.

### 2. Runtime enhancements to Prisma client

At runtime, transparent proxies are created around Prisma clients for intercepting queries and mutations to enforce access policies.

```ts
// Next.js example: pages/api/model/[...path].ts
import { withPolicy } from '@zenstackhq/runtime';

// a regular Prisma client
const prisma = new PrismaClient();

async function getPosts(userId: string) {
// create an enhanced Prisma client that has access control enabled
const enhanced = withPolicy(prisma, { user: userId });

// only posts that're visible to the user will be returned
return enhanced.post.findMany();
}
```

### 3. Automatic RESTful APIs through server adapters

Server adapter packages help you wrap an access-control-enabled Prisma client into backend CRUD APIs that can be safely called from the frontend. Here's an example for Next.js:

```ts
// pages/api/model/[...path].ts

import { requestHandler } from '@zenstackhq/next';
import { withPolicy } from '@zenstackhq/runtime';
import { getSessionUser } from '@lib/auth';
import { prisma } from '@lib/db';

// Mount Prisma-style APIs: "/api/model/post/findMany", "/api/model/post/create", etc.
// Can be configured to provide standard RESTful APIs (using JSON:API) instead.
export default requestHandler({
getPrisma: (req, res) => withPolicy(prisma, { user: getSessionUser(req, res) }),
});
```

Plugins can generate strong-typed client libraries that talk to the APIs:
### 4. Generated client libraries (hooks) for data access

Plugins can generate strong-typed client libraries that talk to the aforementioned APIs. Here's an example for React:

```tsx
// React example: components/MyPosts.tsx
// components/MyPosts.tsx

import { usePost } from '@lib/hooks';
import { useFindManyPost } from '@lib/hooks';

const MyPosts = () => {
// Post CRUD hooks
const { findMany } = usePost();

// list all posts that're visible to the current user, together with their authors
const { data: posts } = findMany({
const { data: posts } = useFindManyPost({
include: { author: true },
orderBy: { createdAt: 'desc' },
});
Expand All @@ -91,7 +119,9 @@ const MyPosts = () => {
};
```

The following diagram gives a high-level overview of how it works.
## Architecture

The following diagram gives a high-level architecture overview of ZenStack.

![Architecture](https://zenstack.dev/img/architecture-light.png)

Expand Down Expand Up @@ -123,7 +153,7 @@ The following diagram gives a high-level overview of how it works.

### Framework adapters

- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next)
- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next) (including support for the new "app directory" in Next.js 13)
- [SvelteKit](https://zenstack.dev/docs/reference/server-adapters/sveltekit)
- [Fastify](https://zenstack.dev/docs/reference/server-adapters/fastify)
- [ExpressJS](https://zenstack.dev/docs/reference/server-adapters/express)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "pnpm -r run test --silent",
"lint": "pnpm -r lint",
"publish-all": "pnpm --filter \"./packages/**\" -r publish --access public",
"publish-preview": "pnpm --filter \"./packages/**\" -r publish --registry https://packagecloud.io/ymc9/zenstack-preview/npm"
"publish-preview": "pnpm --filter \"./packages/**\" -r publish --registry http://localhost:4873"
},
"keywords": [],
"author": "",
Expand Down
22 changes: 11 additions & 11 deletions packages/runtime/src/enhancements/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,11 @@ export class PolicyUtil {
const ids = this.getEntityIds(fieldInfo.type, fieldData);

if (Object.keys(ids).length !== 0) {
if (this.logger.enabled('info')) {
this.logger.info(
`Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`
);
}
// if (this.logger.enabled('info')) {
// this.logger.info(
// `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`
// );
// }
await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
}
}
Expand Down Expand Up @@ -749,9 +749,9 @@ export class PolicyUtil {
return;
}

if (this.logger.enabled('info')) {
this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`);
}
// if (this.logger.enabled('info')) {
// this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`);
// }

const queryFilter = deepcopy(filter);

Expand Down Expand Up @@ -835,9 +835,9 @@ export class PolicyUtil {
db: Record<string, DbOperations>,
preValue: any
) {
if (this.logger.enabled('info')) {
this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`);
}
// if (this.logger.enabled('info')) {
// this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`);
// }

const guard = await this.getAuthGuard(model, 'postUpdate', preValue);

Expand Down