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
6 changes: 5 additions & 1 deletion docs/guides/authentication/lucia.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ sidebar_position: 5
sidebar_label: Lucia
---

# Integrating With Lucia
# Integrating With Lucia (Deprecated)

:::info
Lucia's is [not in active development anymore](https://github.com/lucia-auth/lucia/discussions/1714).
:::

[Lucia](https://lucia-auth.com/) is an auth library for your server that abstracts away the complexity of handling sessions. It is a good choice if you need to add custom logic to the auth flow or use email and password authentication.

Expand Down
10 changes: 5 additions & 5 deletions docs/guides/authentication/next-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ sidebar_label: Auth.js (NextAuth)

# Integrating With Auth.js (NextAuth)

[NextAuth](https://authjs.dev/) is a comprehensive framework for implementing authentication in Next.js projects. It offers a pluggable mechanism for configuring how user data is persisted.
[Auth.js](https://authjs.dev/) is a comprehensive framework for implementing authentication in Next.js projects. It offers a pluggable mechanism for configuring how user data is persisted.

To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with NextAuth. You can find a complete example [here](https://github.com/zenstackhq/sample-todo-nextjs ':target=blank').
To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with Auth.js. You can find a complete example [here](https://github.com/zenstackhq/sample-todo-nextjs ':target=blank').

### Data model requirement

NextAuth is agnostic about the underlying database type, but it requires specific table structures, depending on how you configure it. Therefore, your ZModel definitions should reflect these requirements. A sample `User` model is shown here (to be used with `CredentialsProvider`):
Auth.js is agnostic about the underlying database type, but it requires specific table structures, depending on how you configure it. Therefore, your ZModel definitions should reflect these requirements. A sample `User` model is shown here (to be used with `CredentialsProvider`):

```zmodel title='/schema.zmodel'
model User {
Expand All @@ -35,7 +35,7 @@ You can find the detailed database model requirements [here](https://authjs.dev/

### Adapter

Adapter is a NextAuth mechanism for hooking in custom persistence of auth-related entities, like User, Account, etc. Since ZenStack is based on Prisma, you can use PrismaAdapter for the job:
Adapter is a Auth.js mechanism for hooking in custom persistence of auth-related entities, like User, Account, etc. Since ZenStack is based on Prisma, you can use PrismaAdapter for the job:

```ts title='/src/pages/api/auth/[...nextauth].ts'
import { PrismaAdapter } from "@next-auth/prisma-adapter";
Expand Down Expand Up @@ -108,7 +108,7 @@ function authorize(prisma: PrismaClient) {

You can create an enhanced Prisma client which automatically validates access policies, field validation rules etc., during CRUD operations. For more details, please refer to [ZModel Language](../../reference/zmodel-language) reference.

To create such a client with a standard setup, call the `enhance` API with a regular Prisma client and the current user (fetched with NextAuth API). Here's an example:
To create such a client with a standard setup, call the `enhance` API with a regular Prisma client and the current user (fetched with Auth.js API). Here's an example:

```ts
import type { NextApiRequest, NextApiResponse } from 'next';
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/edge.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Guide for deploying ZenStack to edge runtime.
sidebar_position: 12
---

# Deploying to Edge Runtime (Preview)
# Deploying to Edge Runtime

## Introduction

Expand Down
211 changes: 211 additions & 0 deletions docs/guides/typing-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
description: Typing JSON fields
sidebar_position: 15
---

# Typing JSON Fields (Preview)

> The design and implementation are inspired by [prisma-json-types-generator](https://github.com/arthurfiorette/prisma-json-types-generator).

Relational databases provide the benefit of strong schema enforcement, but sometimes, you want to step out of that guardrail and have more flexibility around data modeling. Prisma's JSON type allows you to store arbitrary JSON data in a column. It can be a good fit for use cases like:

- You want to store objects along with the main entity, but it's not worth creating a separate table for them.
- Your objects have many possible fields, but you don't want to create tables with wide columns.
- Your object's structure is not fixed and can change over time in a non-backward-compatible way.

JSON fields provide great flexibility but at the cost of losing strong typing and validation. ZenStack's strongly typed JSON field feature helps you regain the lost benefits.

## Defining types

To type JSON fields, you'll first need to create type declarations in your ZModel schema. For example, you can define a user profile type like:

```zmodel
type Profile {
name String
age Int
}
```

Types can include fields of other types too:

```zmodel
type Address {
state String
city String
zip String
}

type Profile {
name String
age Int
address Address?
}
```

Type declarations are similar to model declarations with the following differences:

- Types are not mapped to database tables.
- Types cannot inherit from other types yet. This will be supported in future releases.
- Types cannot have relations with models (since they aren't tables anyway).

## Defining strongly-typed JSON fields

You can then use these types to define strongly typed JSON fields in data models:

```zmodel
model User {
id String @id @default(cuid())
profile Profile @json

@@allow('all', true)
}
```

:::info

Strongly typed JSON fields must be annotated with the `@json` attribute. The purpose is to make them easily distinguishable from relational fields.

The feature is only supported for PostgreSQL database for now.

:::

## `PrismaClient` typing

ZenStack's enhanced `PrismaClient` provides type-safe mutation input and query results for strongly typed JSON fields.

```ts
import { enhance } from '@zenstackhq/runtime';
import { prisma } from '~/db';

const db = enhance(prisma);

// The following create call results in a type error because of the
// incorrect type of the `age` field
await db.user.create({
data: {
profile: { name: 'Alice', age: '30' /* incorrect type */ }
}
});

// The query result is typed as:
// {
// id: string;
// profile: {
// name: string;
// age: number;
// address: {
// state: string;
// city: string;
// zip: string;
// } | null;
// };
// }
await user = await db.user.findFirstOrThrow();
```

The query also deals with transforming JSON fields into proper JavaScript types. For example, `DateTime` field values will be parsed from strings into `Date` objects.

Please note that ZenStack **DOES NOT** validate if the query result data conforms to the JSON field types. If you have non-conforming existing data, you'll have to do additional checking or transformation, instead of trusting the TypeScript typing. We thought this was a reasonable trade-off to preserve the flexibility of JSON fields.

:::info
Prisma filter clauses are not enhanced, which means when you filter by the JSON fields, you'll still need to use [Prisma's JSON filter syntax](https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/working-with-json-fields#filter-on-a-json-field-simple).

In the future, we may provide a more natural filtering experience, like:

```ts
await db.user.findMany({
where: {
profile: { name: 'Alice' }
}
});
```

However, it can potentially confuse code readers on whether the filter happens on a JSON field or a relational field (which involves joins). [Let us know your thoughts](https://discord.gg/Ykhr738dUe) on this!

:::

## Mutation payload validation

During mutation, the enhanced `PrismaClient` validates if the input data conforms to the JSON field type. Even if you bypass TypeScript's type checking, ZenStack will reject an incorrect payload with a runtime error.

```ts
await db.user.create({
data: {
email: 'abc',
profile: { name: 'Alice', age: '30' /* incorrect type */ },
},
} as any // bypass TypeScript type checking
);
```

```plain
Error calling enhanced Prisma method `user.create`: denied by policy: user
entities failed 'create' check, input failed validation: Validation error:
Expected number, received string at "profile.age"
```

You can also annotate the type declaration with additional [validation attributes](../reference/zmodel-language#data-validation):

```zmodel
type Profile {
name String
age Int @gte(18) @lt(150) // must be between 18 and 150
address Address?
}
```

Such validations will also be enforced during mutation. For example, the following call will be rejected:

```ts
await db.user.create({
data: {
email: 'abc',
profile: { name: 'Alice', age: 16 },
},
});
```

```plain
Error calling enhanced Prisma method `user.create`: denied by policy: user
entities failed 'create' check, input failed validation: Validation error:
Number must be greater than or equal to 18 at "profile.age"
```

## TypeScript types

ZenStack generates TypeScript types for the "type" declarations. You can import them from `@zenstackhq/runtime/models`:

```ts
import type { Profile } from '@zenstackhq/runtime/models';

const profile: Profile = {
name: 'Alice',
age: 30,
address: { state: 'WA', city: 'Seattle', zip: '98019' }
};
```

## Zod schemas

ZenStack also generates Zod schemas for the "type" declarations. You can import them from `@zenstackhq/runtime/zod/models`:

```ts
import { ProfileSchema } from '@zenstackhq/runtime/zod/models';

const profile = ProfileSchema.parse({
name: 'Alice',
age: 30,
address: { state: 'WA', city: 'Seattle', zip: '98019' }
});
```

## Limitations

This feature is in preview and has the following limitations:

1. Types cannot inherit from other types.
1. Models cannot inherit from types.
1. Types and abstract models are conceptually similar and they should probably be consolidated.
1. JSON field members cannot be used in access policies.

We would like to continue gathering feedback and address these limitations in future releases.
3 changes: 2 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ You don't need to `await` the Prisma method call result. The REPL session will a
| ----- | ------------------- | ------- |
| --debug | Enable debug output. Can be toggled on the fly in the repl session with the ".debug" command. | false |
| --table | Enable table format. Can be toggled on the fly in the repl session with the ".table" command. | false |
| --prisma-client | Path to load PrismaClient module. | "./node_modules/.prisma/client" |
| --prisma-client <path> | Path to load PrismaClient module. | "node_modules/.prisma/client" |
| --load-path <path> | Path to load modules generated by ZenStack. | "node_modules/.zenstack" |

#### Repl Commands

Expand Down
Loading