Skip to content
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

chore: misc fixes around Redwood #917

Merged
merged 4 commits into from
Jan 1, 2024
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
21 changes: 0 additions & 21 deletions packages/misc/LICENSE

This file was deleted.

62 changes: 40 additions & 22 deletions packages/misc/redwood/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,50 @@ ZenStack is a full-stack toolkit built above Prisma ORM. It extends Prisma at th
- Multi-file schemas
- Custom attributes and functions in schemas

Visit [homepage](https://zenstack.dev) for more details.
You can find a more detailed integration guide [here](https://zenstack.dev/docs/guides/redwood).

## Setting up
### Setting up

This package leverages RedwoodJS's experimental feature to register a custom CLI command to `yarn redwood`. To enable it, add the following to `redwood.toml`:
Run the following package setup command:

```toml
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@zenstackhq/redwood"
```bash
yarn rw setup package @zenstackhq/redwood
```

Then you can run `yarn rw @zenstackhq setup` to install ZenStack into your Redwood project. The setup command will:
The setup command will:

1. Update "redwood.toml" to allow ZenStack CLI plugin.
1. Install ZenStack dependencies.
2. Copy your Prisma schema file "api/db/schema.prisma" to "api/db/schema.zmodel".
3. Add a "zenstack" section into "api/package.json" to specify the location of both the "schema.prisma" and "schema.zmodel" files.
4. Install a GraphQLYoga plugin in "api/src/functions/graphql.[ts|js]".
5. Eject service templates and modify the templates to use `context.db` (ZenStack-enhanced `PrismaClient`) instead of `db` for data access.
1. Copy your Prisma schema file "api/db/schema.prisma" to "api/db/schema.zmodel".
1. Add a "zenstack" section into "api/package.json" to specify the location 1f both the "schema.prisma" and "schema.zmodel" files.
1. Install a GraphQLYoga plugin in "api/src/functions/graphql.[ts|js]".
1. Eject service templates and modify the templates to use `context.db` (ZenStack-enhanced `PrismaClient`) instead of `db` for data access.

### Modeling data and access policies

## Modeling data and access policies
ZenStack's ZModel language is a superset of Prisma schema language. You should use it to define both the data schema and access policies. [The Complete Guide](https://zenstack.dev/docs/the-complete-guide/part1/) of ZenStack is the best way to learn how to author ZModel schemas.

ZenStack's ZModel language is a superset of Prisma schema language. You should use it to define both the data schema and access policies. The regular Prisma schema file will be regenerated from the ZModel file when you run
You should run the following command after updating "schema.zmodel":

```bash
yarn rw @zenstackhq generate
```

[The Complete Guide](https://zenstack.dev/docs/the-complete-guide/part1/) of ZenStack is the best way to learn how to author ZModel schemas. You can also use the
The command does the following things:

1. Regenerate "schema.prisma"
2. Run `prisma generate` to regenerate PrismaClient
3. Generates supporting JS modules for enforcing access policies at runtime

<!-- You can also use the

```bash
yarn rw @zenstackhq sample
```

command to browse a list of sample schemas and create from them.
command to browse a list of sample schemas and create from them. -->

## Development workflow
### Development workflow

The workflow of using ZenStack is very similar to using Prisma in RedwoodJS projects. The two main differences are:

Expand All @@ -62,18 +68,30 @@ The workflow of using ZenStack is very similar to using Prisma in RedwoodJS proj

Other Prisma-related workflows like generation migration or pushing schema to the database stay unchanged.

## Deployment
### Deployment

You should run the "generate" command in your deployment script before `yarn rw deploy`. For example, to deploy to Vercel, the command can be:

```bash
yarn rw @zenstackhq generate && yarn rw deploy vercel
```

## Sample application
### Using the `@zenstackhq` CLI plugin

The `@zenstackhq/redwood` package registers a set of custom commands to the RedwoodJS CLI under the `@zenstackhq` namespace. You can run it with:

```bash
yarn rw @zenstackhq <cmd> [options]
```

The plugin is a simple wrapper of the standard `zenstack` CLI, similar to how RedwoodJS wraps the standard `prisma` CLI. It's equivalent to running `npx zenstack ...` inside the "api" directory.

See the [CLI references](https://zenstack.dev/docs/reference/cli) for the full list of commands.

### Sample application

You can find a complete multi-tenant Todo application built with RedwoodJS and ZenStack at: [https://github.com/zenstackhq/sample-todo-redwood](https://github.com/zenstackhq/sample-todo-redwood).

## Getting help
### Getting help

The best way to getting help and updates about ZenStack is by joining our [Discord server](https://discord.gg/Ykhr738dUe).
The best way to get help and updates about ZenStack is by joining our [Discord server](https://discord.gg/Ykhr738dUe).
3 changes: 3 additions & 0 deletions packages/misc/redwood/bin/cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

require('../dist/setup-package').default();
10 changes: 6 additions & 4 deletions packages/misc/redwood/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"default": "./package.json"
}
},
"bin": "bin/cli",
"engines": {
"redwoodjs": ">=6.0.0"
},
"author": {
"name": "ZenStack Team"
},
Expand All @@ -37,17 +41,15 @@
"dependencies": {
"@zenstackhq/runtime": "workspace:*",
"colors": "1.4.0",
"ts-morph": "^16.0.0"
},
"peerDependencies": {
"ts-morph": "^16.0.0",
"@redwoodjs/cli-helpers": "^6.6.0",
"execa": "^5.0.0",
"listr2": "^6.0.0",
"terminal-link": "^2.0.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@redwoodjs/graphql-server": "^6.6.0",
"@redwoodjs/cli-helpers": "^6.6.0",
"@types/yargs": "^17.0.32",
"graphql-yoga": "^5.0.2"
}
Expand Down
4 changes: 1 addition & 3 deletions packages/misc/redwood/src/cli-passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export function makePassthroughCommand(command: string): CommandModule<unknown>
.strictOptions(false)
.strictCommands(false)
.strict(false)
.parserConfiguration({ 'camel-case-expansion': false, 'boolean-negation': false })
.help(false)
.version(false);
.parserConfiguration({ 'camel-case-expansion': false, 'boolean-negation': false });
},
handler: async ({ _, $0: _$0, ...options }) => {
await runCommand(command, options);
Expand Down
69 changes: 52 additions & 17 deletions packages/misc/redwood/src/commands/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getPaths } from '@redwoodjs/cli-helpers';
import { getPaths, updateTomlConfig } from '@redwoodjs/cli-helpers';
import colors from 'colors';
import execa from 'execa';
import fs from 'fs';
Expand All @@ -9,6 +9,15 @@ import { Project, SyntaxKind, type PropertyAssignment } from 'ts-morph';
import type { CommandModule } from 'yargs';
import { addApiPackages } from '../utils';

function updateToml() {
return {
title: 'Updating redwood.toml...',
task: () => {
updateTomlConfig('@zenstackhq/redwood');
},
};
}

function installDependencies() {
return addApiPackages([
{ pkg: 'zenstack', dev: true },
Expand Down Expand Up @@ -54,15 +63,15 @@ function installGraphQLPlugin() {
title: 'Installing GraphQL plugin...',
task: async () => {
// locate "api/functions/graphql.[js|ts]"
let sourcePath: string | undefined;
let graphQlSourcePath: string | undefined;
const functionsDir = getPaths().api.functions;
if (fs.existsSync(path.join(functionsDir, 'graphql.ts'))) {
sourcePath = path.join(functionsDir, 'graphql.ts');
graphQlSourcePath = path.join(functionsDir, 'graphql.ts');
} else if (fs.existsSync(path.join(functionsDir, 'graphql.js'))) {
sourcePath = path.join(functionsDir, 'graphql.js');
graphQlSourcePath = path.join(functionsDir, 'graphql.js');
}

if (!sourcePath) {
if (!graphQlSourcePath) {
console.warn(
colors.yellow(`Unable to find handler source file: ${path.join(functionsDir, 'graphql.(js|ts)')}`)
);
Expand All @@ -71,21 +80,21 @@ function installGraphQLPlugin() {

// add import
const project = new Project();
const sf = project.addSourceFileAtPathIfExists(sourcePath)!;
let changed = false;
const graphQlSourceFile = project.addSourceFileAtPathIfExists(graphQlSourcePath)!;
let graphQlSourceFileChanged = false;
let identified = false;

const imports = sf.getImportDeclarations();
const imports = graphQlSourceFile.getImportDeclarations();
if (!imports.some((i) => i.getModuleSpecifierValue() === '@zenstackhq/redwood')) {
sf.addImportDeclaration({
graphQlSourceFile.addImportDeclaration({
moduleSpecifier: '@zenstackhq/redwood',
namedImports: ['useZenStack'],
});
changed = true;
graphQlSourceFileChanged = true;
}

// add "extraPlugins" option to `createGraphQLHandler` call
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((expr) => {
graphQlSourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((expr) => {
if (identified) {
return;
}
Expand All @@ -104,15 +113,15 @@ function installGraphQLPlugin() {
if (pluginArr) {
if (!pluginArr.getElements().some((e) => e.getText().includes('useZenStack'))) {
pluginArr.addElement('useZenStack(db)');
changed = true;
graphQlSourceFileChanged = true;
}
}
} else {
arg.addPropertyAssignment({
name: 'extraPlugins',
initializer: '[useZenStack(db)]',
});
changed = true;
graphQlSourceFileChanged = true;
}
}
}
Expand All @@ -126,8 +135,32 @@ function installGraphQLPlugin() {
);
}

if (changed) {
sf.formatText();
if (graphQlSourceFileChanged) {
graphQlSourceFile.formatText();
}

// create type-def file to add `db` into global context
let contextTypeDefCreated = false;
if (graphQlSourcePath.endsWith('.ts')) {
const typeDefPath = path.join(getPaths().api.src, 'zenstack.d.ts');
if (!fs.existsSync(typeDefPath)) {
const typeDefSourceFile = project.createSourceFile(
typeDefPath,
`import type { PrismaClient } from '@prisma/client'

declare module '@redwoodjs/graphql-server' {
interface GlobalContext {
db: PrismaClient
}
}
`
);
typeDefSourceFile.formatText();
contextTypeDefCreated = true;
}
}

if (graphQlSourceFileChanged || contextTypeDefCreated) {
await project.save();
}
},
Expand Down Expand Up @@ -185,8 +218,8 @@ function whatsNext() {
task: (_ctx, task) => {
task.title =
`What's next...\n\n` +
` - Install ${terminalLink('ZenStack IDE extensions', 'https://zenstack.dev/docs/guides/ide')}.\n` +
` - Use "${zmodel}" to model your database schema and access control.\n` +
` - Install ${terminalLink('IDE extensions', 'https://zenstack.dev/docs/guides/ide')}.\n` +
` - Use "${zmodel}" to model database schema and access control.\n` +
` - Run \`yarn rw @zenstackhq generate\` to regenerate Prisma schema and client.\n` +
` - Learn ${terminalLink(
"how ZenStack extends Prisma's power",
Expand All @@ -205,8 +238,10 @@ function whatsNext() {
const setupCommand: CommandModule<unknown> = {
command: 'setup',
describe: 'Set up ZenStack environment',
builder: (yargs) => yargs,
handler: async () => {
const tasks = new Listr([
updateToml(),
installDependencies(),
bootstrapSchema(),
installGraphQLPlugin(),
Expand Down
11 changes: 11 additions & 0 deletions packages/misc/redwood/src/setup-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import setupCommand from './commands/setup';

export default async function setupPackage() {
await yargs(hideBin(process.argv))
.scriptName('zenstack-setup')
// @ts-expect-error yargs types are wrong
.command('$0', 'set up ZenStack', setupCommand.builder, setupCommand.handler)
.parseAsync();
}
Loading
Loading