├── .github ├── FUNDING.yml └── workflows │ └── unit-tests.yml ├── bin ├── env.mjs ├── postinstall.mjs ├── test.mjs ├── publish │ ├── _pkg.installer.cleanse.js │ ├── _pkg.core.restore.js │ └── _pkg.core.cleanse.js ├── dev.mjs ├── cleans.mjs ├── build.mjs └── publish.mjs ├── packages ├── generator │ ├── src │ │ ├── index.ts │ │ ├── handler.ts │ │ └── generator.ts │ ├── tsconfig.json │ └── package.json ├── server │ ├── src │ │ ├── index.d.ts │ │ ├── lambdaRequest.vtl │ │ ├── lambdaResponse.vtl │ │ ├── index.ts │ │ └── appsync-simulator.ts │ ├── tsconfig.json │ └── package.json ├── boilerplate │ ├── cdk.json │ ├── handler.ts │ ├── cdk │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src │ │ │ └── index.ts │ ├── server │ │ └── server.ts │ ├── tsconfig.json │ └── prisma │ │ └── sqlite.prisma ├── client │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── inspector.ts │ │ └── consts.ts └── installer │ ├── tsconfig.json │ ├── src │ └── index.ts │ └── package.json ├── docs ├── public │ ├── logo.png │ ├── sponsors │ │ ├── kuizto-banner.png │ │ ├── kuizto-logo.jpg │ │ ├── kuizto-square.png │ │ └── travistravis-logo.jpg │ ├── guides │ │ └── hooks-autocompletion.png │ └── logo.svg ├── prisma-appsync-diagram.png ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ ├── Layout.vue │ │ └── styles │ │ │ └── vars.css │ └── config.ts ├── changelog │ ├── index.md │ ├── 1.0.0-rc.3.md │ ├── 1.0.0-rc.4.md │ ├── 1.0.0-rc.2.md │ ├── 1.0.0-rc.5.md │ └── 1.0.0-rc.7.md ├── support.md ├── security │ ├── query-depth.md │ ├── rate-limiter.md │ ├── xss-sanitizer.md │ ├── shield-acl.md │ └── appsync-authz.md ├── index.md ├── quick-start │ ├── usage.md │ ├── deploy.md │ ├── installation.md │ └── getting-started.md ├── features │ ├── gql-schema.md │ ├── resolvers.md │ └── hooks.md ├── tools │ └── appsync-gql-schema-diff.md └── contributing.md ├── prisma-appsync-logo.png ├── CHANGELOG.md ├── CONTRIBUTING.md ├── .vscode ├── extensions.json └── settings.json ├── pnpm-workspace.yaml ├── .markdownlint.json ├── vite.config.ts ├── tests ├── generator │ ├── mock │ │ ├── appsync-scalars.gql │ │ └── appsync-directives.gql │ ├── schemas │ │ ├── crud.gql │ │ ├── generated │ │ │ ├── @gql │ │ │ │ ├── client │ │ │ │ │ ├── inspector.d.ts │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── guard.d.ts │ │ │ │ │ ├── resolver.d.ts │ │ │ │ │ ├── consts.d.ts │ │ │ │ │ ├── core.d.ts │ │ │ │ │ ├── adapter.d.ts │ │ │ │ │ └── utils.d.ts │ │ │ │ └── resolvers.yaml │ │ │ └── crud │ │ │ │ └── client │ │ │ │ ├── inspector.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── guard.d.ts │ │ │ │ ├── resolver.d.ts │ │ │ │ ├── consts.d.ts │ │ │ │ ├── adapter.d.ts │ │ │ │ ├── core.d.ts │ │ │ │ └── utils.d.ts │ │ ├── @gql.prisma │ │ └── crud.prisma │ └── @gql.test.ts └── client │ ├── utils │ └── index.ts │ ├── mocks │ ├── lambda-event.ts │ └── lambda-identity.ts │ └── resolver.test.ts ├── .gitignore ├── .npmignore ├── .eslintrc ├── tsconfig.json ├── LICENSE.txt ├── package.json └── .all-contributorsrc /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [maoosi] 2 | -------------------------------------------------------------------------------- /bin/env.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | process.env.FORCE_COLOR = 3 4 | -------------------------------------------------------------------------------- /packages/generator/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import './handler' 3 | -------------------------------------------------------------------------------- /packages/server/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { createServer, argv } from './index' 2 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /packages/boilerplate/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts cdk/src/index.ts" 3 | } -------------------------------------------------------------------------------- /prisma-appsync-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/prisma-appsync-logo.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [prisma-appsync.vercel.app/changelog/](https://prisma-appsync.vercel.app/changelog/) -------------------------------------------------------------------------------- /docs/prisma-appsync-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/prisma-appsync-diagram.png -------------------------------------------------------------------------------- /docs/public/sponsors/kuizto-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/sponsors/kuizto-banner.png -------------------------------------------------------------------------------- /docs/public/sponsors/kuizto-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/sponsors/kuizto-logo.jpg -------------------------------------------------------------------------------- /docs/public/sponsors/kuizto-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/sponsors/kuizto-square.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [prisma-appsync.vercel.app/contributing.html](https://prisma-appsync.vercel.app/contributing.html) -------------------------------------------------------------------------------- /docs/public/guides/hooks-autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/guides/hooks-autocompletion.png -------------------------------------------------------------------------------- /docs/public/sponsors/travistravis-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoosi/prisma-appsync/HEAD/docs/public/sponsors/travistravis-logo.jpg -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "johnpapa.vscode-peacock" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/src/lambdaRequest.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "operation": "Invoke", 4 | "payload": $util.toJson($context) 5 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/server" 3 | - "packages/client" 4 | - "packages/installer" 5 | - "packages/generator" 6 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-inline-html": false, 4 | "line-length": false, 5 | "no-trailing-punctuation": false 6 | } 7 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/installer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/src/lambdaResponse.vtl: -------------------------------------------------------------------------------- 1 | #if($ctx.error) 2 | $util.error($ctx.error.message, $ctx.error.type, $ctx.result) 3 | #end 4 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme' 2 | import Layout from './Layout.vue' 3 | import './styles/vars.css' 4 | 5 | export default { 6 | extends: Theme, 7 | Layout: Layout 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from 'vitest/config' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths()], 7 | }) 8 | -------------------------------------------------------------------------------- /tests/generator/mock/appsync-scalars.gql: -------------------------------------------------------------------------------- 1 | scalar AWSDate 2 | scalar AWSTime 3 | scalar AWSDateTime 4 | scalar AWSTimestamp 5 | scalar AWSEmail 6 | scalar AWSJSON 7 | scalar AWSURL 8 | scalar AWSPhone 9 | scalar AWSIPAddress 10 | scalar BigInt 11 | scalar Double -------------------------------------------------------------------------------- /packages/installer/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable n/prefer-global/process */ 3 | import { Installer } from './installer' 4 | 5 | async function main(): Promise { 6 | const installer = new Installer() 7 | return await installer.start() 8 | } 9 | 10 | main().catch((e) => { 11 | console.error(e) 12 | process.exit() 13 | }) 14 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-appsync-server", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "maoosi ", 6 | "license": "BSD-2-Clause", 7 | "devDependencies": { 8 | "amplify-appsync-simulator": "^2.4.1", 9 | "chokidar": "^3.5.3", 10 | "cleye": "^1.3.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/postinstall.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import './env.mjs' 3 | 4 | // set DATABASE_URL env variable to docker instance 5 | process.env.DATABASE_URL = 'postgresql://prisma:prisma@localhost:5433/tests' 6 | 7 | // install boilerplate dependencies using Yarn 8 | console.log(chalk.blue('\n📦 [post-install] install cdk boilerplate dependencies\n')) 9 | await $`cd packages/boilerplate/cdk && yarn install` 10 | -------------------------------------------------------------------------------- /docs/changelog/index.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [(latest) v1.0.0](/changelog/1.0.0.html) 4 | - [1.0.0-rc.7](/changelog/1.0.0-rc.7.html) 5 | - [1.0.0-rc.6](/changelog/1.0.0-rc.6.html) 6 | - [1.0.0-rc.5](/changelog/1.0.0-rc.5.html) 7 | - [1.0.0-rc.4](/changelog/1.0.0-rc.4.html) 8 | - [1.0.0-rc.3](/changelog/1.0.0-rc.3.html) 9 | - [1.0.0-rc.2](/changelog/1.0.0-rc.2.html) 10 | - [1.0.0-rc.1](/changelog/1.0.0-rc.1.html) -------------------------------------------------------------------------------- /tests/generator/schemas/crud.gql: -------------------------------------------------------------------------------- 1 | type PublishNotification { 2 | message: String! 3 | } 4 | 5 | extend type Mutation { 6 | """ 7 | Send a notification. 8 | """ 9 | notify(message: String!): PublishNotification 10 | } 11 | 12 | extend type Subscription { 13 | """ 14 | Triggered from `notify` mutation. 15 | """ 16 | onNotification: PublishNotification @aws_subscribe(mutations: ["notify"]) 17 | } 18 | -------------------------------------------------------------------------------- /packages/boilerplate/handler.ts: -------------------------------------------------------------------------------- 1 | import type { AppSyncResolverEvent } from './prisma/generated/prisma-appsync/client' 2 | import { PrismaAppSync } from './prisma/generated/prisma-appsync/client' 3 | 4 | // Instantiate Prisma-AppSync Client 5 | const prismaAppSync = new PrismaAppSync() 6 | 7 | // Lambda handler (AppSync Direct Lambda Resolver) 8 | export const main = async (event: AppSyncResolverEvent) => { 9 | return await prismaAppSync.resolve({ event }) 10 | } 11 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## Sylvain 4 | 5 | **👋 Hi, I’m Sylvain! [On-demand CTO](https://sylvainsimao.com/freelance/) and creator of ◭ Prisma-AppSync.** 6 | 7 | Using Prisma-AppSync in production and looking for hourly paid support? You can contact me on [Twitter](https://twitter.com/Sylvain_Simao), message me on [LinkedIn](https://www.linkedin.com/in/sylvainsimao/), or [send me an email](https://sylvainsimao.com/contact). 8 | -------------------------------------------------------------------------------- /bin/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | /* eslint-disable no-console */ 3 | import './env.mjs' 4 | 5 | // build 6 | await $`zx bin/build.mjs` 7 | 8 | // prisma client for tests 9 | console.log(chalk.blue('\n🧪 [test] run prisma generate')) 10 | await $`npx prisma generate --schema tests/generator/schemas/crud.prisma` 11 | await $`npx prisma generate --schema tests/generator/schemas/@gql.prisma` 12 | 13 | // unit tests 14 | console.log(chalk.blue('🧪 [test] run unit tests\n')) 15 | await $`VITE_CJS_IGNORE_WARNING=true vitest run tests` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | */**/node_modules/ 4 | 5 | # Testing 6 | tests/prisma/generated/ 7 | playground/ 8 | debug/ 9 | 10 | # Dist folder 11 | dist/ 12 | 13 | # Docs 14 | docs/.vitepress/dist/ 15 | docs/.vitepress/cache/ 16 | tmp.md 17 | 18 | # Boilerplate files 19 | packages/boilerplate/cdk/*.lock 20 | packages/boilerplate/cdk/cdk.out 21 | 22 | # Log files 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Editor directories and files 28 | .idea 29 | *.suo 30 | *.ntvs* 31 | *.njsproj 32 | *.sln 33 | *.sw* 34 | -------------------------------------------------------------------------------- /tests/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest' 2 | 3 | function format(str, ...args) { 4 | return str.replace(/{(\d+)}/g, (match, number) => { 5 | return typeof args[number] != 'undefined' ? args[number] : match 6 | }) 7 | } 8 | 9 | // eslint-disable-next-line @typescript-eslint/ban-types 10 | export function testEach(cases: any[][]): (name: string, fn: Function) => void { 11 | return (name, fn) => { 12 | cases.forEach((items) => { 13 | test(format(name, ...items), () => fn(...items)) 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/installer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-prisma-appsync-app", 3 | "private": true, 4 | "version": "1.0.2", 5 | "author": "maoosi ", 6 | "license": "BSD-2-Clause", 7 | "bin": "./bin/index.js", 8 | "devDependencies": { 9 | "@types/degit": "^2.8.6", 10 | "@types/prompts": "^2.4.9", 11 | "degit": "^2.8.4", 12 | "detect-package-manager": "^3.0.1", 13 | "execa": "8.0.1", 14 | "fs-jetpack": "^5.1.0", 15 | "kolorist": "^1.8.0", 16 | "prompts": "^2.4.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/generator/mock/appsync-directives.gql: -------------------------------------------------------------------------------- 1 | directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION 2 | directive @deprecated(reason: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM | ENUM_VALUE 3 | directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION 4 | directive @aws_api_key on FIELD_DEFINITION | OBJECT 5 | directive @aws_iam on FIELD_DEFINITION | OBJECT 6 | directive @aws_oidc on FIELD_DEFINITION | OBJECT 7 | directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION | OBJECT 8 | directive @aws_lambda on FIELD_DEFINITION | OBJECT 9 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-appsync-client", 3 | "private": true, 4 | "version": "1.0.0", 5 | "author": "maoosi ", 6 | "license": "BSD-2-Clause", 7 | "devDependencies": { 8 | "@types/aws-lambda": "^8.10.126", 9 | "@types/micromatch": "^4.0.5", 10 | "deepmerge": "^4.3.1", 11 | "html-entities": "^2.4.0", 12 | "lambda-rate-limiter": "^4.0.0", 13 | "micromatch": "^4.0.5", 14 | "wild-wild-path": "^4.0.0", 15 | "wild-wild-utils": "^5.0.0", 16 | "xss": "^1.0.14" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/boilerplate/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-appsync-cdk", 3 | "version": "1.0.0", 4 | "description": "Sample AWS CDK template for Prisma-AppSync", 5 | "author": "maoosi ", 6 | "private": true, 7 | "license": "BSD-2-Clause", 8 | "devDependencies": { 9 | "@types/js-yaml": "^4.0.5", 10 | "@types/node": "^20.4.8", 11 | "aws-cdk-lib": "^2.90.0", 12 | "constructs": "^10.2.69", 13 | "js-yaml": "^4.1.0", 14 | "scule": "^1.0.0", 15 | "ts-node": "^10.9.1", 16 | "typescript": "^5.1.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-appsync-generator", 3 | "private": true, 4 | "version": "1.0.0", 5 | "author": "maoosi ", 6 | "license": "BSD-2-Clause", 7 | "devDependencies": { 8 | "@prisma/generator-helper": "^5.6.0", 9 | "@types/fs-extra": "^11.0.4", 10 | "@types/pluralize": "^0.0.33", 11 | "@types/prettier": "^3.0.0", 12 | "appsync-schema-converter": "^2.1.4", 13 | "fs-extra": "^11.1.1", 14 | "graphql": "^16.8.1", 15 | "lodash": "^4.17.21", 16 | "pluralize": "^8.0.0", 17 | "prettier": "^3.1.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | */**/node_modules/ 4 | 5 | # Source files 6 | packages/ 7 | bin/ 8 | tests/ 9 | playground/ 10 | docs/ 11 | .editorconfig 12 | .eslintrc 13 | .markdownlint.json 14 | .prettierignore 15 | .prettierrc.cjs 16 | tsconfig.json 17 | 18 | # Package cache files 19 | package-*.json 20 | pnpm-*.yaml 21 | 22 | # Log files 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # Workspace 29 | pnpm-workspace.yaml 30 | vite.config.ts 31 | 32 | # Installer 33 | dist/installer 34 | 35 | # Editor directories and files 36 | .idea 37 | .vscode 38 | .github 39 | *.suo 40 | *.ntvs* 41 | *.njsproj 42 | *.sln 43 | *.sw* 44 | -------------------------------------------------------------------------------- /packages/boilerplate/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": false, 15 | "inlineSourceMap": true, 16 | "inlineSources": true, 17 | "experimentalDecorators": true, 18 | "strictPropertyInitialization": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "@antfu", 4 | "rules": { 5 | "jsonc/indent": ["error", 4, {}], 6 | "@typescript-eslint/indent": [ 7 | "error", 8 | 4, 9 | { 10 | "offsetTernaryExpressions": true, 11 | "ignoredNodes": ["TemplateLiteral *", "TSTypeParameterInstantiation"], 12 | "SwitchCase": 1 13 | } 14 | ], 15 | "@typescript-eslint/consistent-type-definitions": ["error", "type"] 16 | }, 17 | "globals": { 18 | "$": true, 19 | "chalk": true, 20 | "cd": true, 21 | "argv": true, 22 | "fs": true, 23 | "nothrow": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v3 14 | 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v2.2.4 17 | with: 18 | version: 7 19 | 20 | - name: Use Node.js 16 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | cache: "pnpm" 25 | 26 | - name: Install global dependencies 27 | run: "pnpm add -g zx" 28 | 29 | - name: Install project dependencies 30 | run: "pnpm install" 31 | 32 | - name: Run tests 33 | run: "pnpm run test" 34 | -------------------------------------------------------------------------------- /docs/security/query-depth.md: -------------------------------------------------------------------------------- 1 | # Query depth 2 | 3 | ## 👉 Usage 4 | 5 | Prisma-AppSync automatically prevents from abusing query depth, by limiting query complexity. 6 | 7 | **For example, it will prevent from doing this:** 8 | 9 | ```graphql 10 | query IAmEvil { 11 | author(id: "abc") { 12 | posts { 13 | author { 14 | posts { 15 | author { 16 | posts { 17 | author { 18 | # that could go on as deep as the client wants! 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | Default value for the maximum query depth is set to `4`. It is possible to change the default max depth value via the `maxDepth` option: 30 | 31 | ```ts 32 | const prismaAppSync = new PrismaAppSync({ maxDepth: 3 }) 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/boilerplate/server/server.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { readFileSync } from 'fs' 3 | import { load } from 'js-yaml' 4 | import { argv, createServer } from 'prisma-appsync/dist/server' 5 | 6 | (async () => { 7 | const schema = readFileSync(join(process.cwd(), argv.flags.schema), { encoding: 'utf-8' }) 8 | const lambdaHandler = await import(join(process.cwd(), argv.flags.handler)) 9 | const resolvers = load(readFileSync(join(process.cwd(), argv.flags.resolvers), { encoding: 'utf-8' })) 10 | const port = argv.flags.port 11 | const wsPort = argv.flags.wsPort 12 | const watchers = argv.flags.watchers ? JSON.parse(argv.flags.watchers) : [] 13 | 14 | createServer({ 15 | schema, 16 | lambdaHandler, 17 | resolvers, 18 | port, 19 | wsPort, 20 | watchers, 21 | }) 22 | })() 23 | -------------------------------------------------------------------------------- /docs/security/rate-limiter.md: -------------------------------------------------------------------------------- 1 | # Rate limiter (DOS) 2 | 3 | ::: warning WARNING NOTICE 4 | Limits are kept in memory and are not shared between function instantiations. This means limits can reset arbitrarily when new instances get spawned or different instances are used to serve requests. 5 | ::: 6 | 7 | ## 👉 Usage 8 | 9 | Prisma-AppSync uses in-memory rate-limiting to try protect your Database from most common DOS attacks. 10 | 11 | To change the default value (default to 200 requests per user, per minute), you can adjust the `maxReqPerUserMinute` option when instantiating the Client: 12 | 13 | ```ts 14 | const prismaAppSync = new PrismaAppSync({ maxReqPerUserMinute: 500 }) 15 | ``` 16 | 17 | ## 👉 Disable rate limiter 18 | 19 | If you prefer to disable the in-memory rate limiter, set the option to false: 20 | 21 | ```ts 22 | const prismaAppSync = new PrismaAppSync({ maxReqPerUserMinute: false }) 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/boilerplate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": ["es2018"], 8 | "declaration": false, 9 | "strict": true, 10 | "noImplicitAny": false, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "esModuleInterop": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "baseUrl": "." 23 | }, 24 | "include": ["**/*.ts", "**/*.tsx"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /bin/publish/_pkg.installer.cleanse.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Define absolute paths for original pkg and temporary pkg. 5 | const SRC_PKG_PATH = path.resolve(__dirname, '../../packages/installer/package.json') 6 | const DEST_PKG_PATH = path.resolve(__dirname, '../../dist/installer/package.json') 7 | 8 | // Obtain original `package.json` contents. 9 | const pkgData = require(SRC_PKG_PATH) 10 | 11 | // Remove all scripts from the scripts section. 12 | delete pkgData.scripts 13 | 14 | // Remove all pkgs from the dependencies section. 15 | delete pkgData.dependencies 16 | 17 | // Remove all pkgs from the devDependencies section. 18 | delete pkgData.devDependencies 19 | 20 | // Remove private tag 21 | delete pkgData.private 22 | 23 | // Create new `package.json` with new data (i.e. minus the specific data). 24 | fs.writeFile(DEST_PKG_PATH, JSON.stringify(pkgData, null, 4), (err) => { 25 | if (err) 26 | throw err 27 | }) 28 | -------------------------------------------------------------------------------- /packages/boilerplate/prisma/sqlite.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 9 | } 10 | 11 | generator appsync { 12 | provider = "prisma-appsync" 13 | } 14 | 15 | /// @gql(fields: { passwordHash: null }) 16 | model User { 17 | id Int @id @default(autoincrement()) 18 | email String @unique 19 | passwordHash String 20 | posts Post[] 21 | createdAt DateTime @default(now()) 22 | } 23 | 24 | /// @gql(scalars: { source: "AWSURL" }) 25 | model Post { 26 | id Int @id @default(autoincrement()) 27 | title String 28 | source String? 29 | author User? @relation(fields: [authorId], references: [id]) 30 | authorId Int? 31 | updatedAt DateTime @updatedAt 32 | createdAt DateTime @default(now()) 33 | } 34 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: Prisma-AppSync 6 | text: GQL API Generator for Prisma ORM 7 | tagline: Turns your Prisma Schema into a fully-featured GraphQL API, tailored for AWS AppSync. 8 | image: 9 | src: /logo.svg 10 | alt: Prisma-AppSync 11 | actions: 12 | - theme: brand 13 | text: Try Prisma-AppSync 14 | link: /quick-start/getting-started 15 | - theme: alt 16 | text: View on GitHub ↗ 17 | link: https://github.com/maoosi/prisma-appsync 18 | 19 | features: 20 | - icon: ◭ 21 | title: Prisma Schema to CRUD API 22 | details: Deploy a GraphQL API from your Prisma Schema with auto-generated CRUD. 23 | - icon: ⚡️ 24 | title: GraphQL on AWS AppSync 25 | details: Serverless GraphQL with real-time updates and built-in security on AppSync. 26 | - icon: 🧑‍💻 27 | title: Fast and Flexible DX 28 | details: Build and deploy a working API in minutes, easily customise to your needs. 29 | --- -------------------------------------------------------------------------------- /docs/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /bin/dev.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import './env.mjs' 3 | 4 | // path 5 | const playgroundPath = 'playground' 6 | 7 | // reset 8 | if (argv?.reset) { 9 | console.log(chalk.blue('\n💻 [dev] reset `playground` dir')) 10 | await fs.remove(playgroundPath) 11 | } 12 | 13 | // build project 14 | await $`zx bin/build.mjs` 15 | 16 | // install 17 | const playgroundExists = await fs.pathExists(playgroundPath) 18 | console.log('') 19 | 20 | if (!playgroundExists) { 21 | console.log(chalk.blue('💻 [dev] create `playground` dir')) 22 | await fs.ensureDir(playgroundPath) 23 | 24 | console.log(chalk.blue('💻 [dev] run installer')) 25 | cd(playgroundPath) 26 | process.env.INSTALL_MODE = 'contributor' 27 | await $`node ../dist/installer/bin/index.js` 28 | } 29 | else { 30 | console.log(chalk.blue('💻 [dev] run prisma generate\n')) 31 | cd(playgroundPath) 32 | await $`npx prisma generate` 33 | } 34 | 35 | // start dev server 36 | console.log(chalk.blue('💻 [dev] start dev server\n')) 37 | await $`yarn dev` 38 | -------------------------------------------------------------------------------- /docs/quick-start/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## 👉 Folder structure 4 | 5 | Using the CLI Installer (recommended): 6 | 7 | ```bash 8 | project/ 9 | |__ handler.ts # lambda function handler (API resolver) 10 | |__ server.ts # local server (for dev) 11 | |__ cdk/ # AWS CDK deploy boilerplate 12 | |__ prisma/ 13 | |__ schema.prisma # prisma schema (data source) 14 | |__ generated/ # auto-generated after each `npx prisma generate` 15 | ``` 16 | 17 | ## 👉 Generating the API 18 | 19 | Run the below command from the project root directory: 20 | 21 | ```bash 22 | npx prisma generate 23 | ``` 24 | 25 | After each `prisma generate`, files inside `prisma/generated` will be auto-generated. 26 | 27 | ## 👉 Local dev server 28 | 29 | Run the local server and try Prisma-AppSync locally (only if using the CLI Installer): 30 | 31 | ```bash 32 | yarn run dev 33 | ``` 34 | 35 | This will automatically push your Prisma Schema changes to a SQLite database, as well as launch a local GraphQL IDE server (with auto-reload and TS support). 36 | -------------------------------------------------------------------------------- /bin/publish/_pkg.core.restore.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Define absolute paths for original pkg and temporary pkg. 5 | const ORIG_PKG_PATH = path.resolve(__dirname, '../../package.json') 6 | const BACKUP_PKG_PATH = path.resolve(__dirname, '../../package-beforePublish.json') 7 | const RESTORE_PKG_PATH = path.resolve(__dirname, '../../package-afterPublish.json') 8 | 9 | // Obtain original/cached contents (with new version) from `package-afterPublish`. 10 | const pkgData = `${JSON.stringify(require(RESTORE_PKG_PATH), null, 4)}\n` 11 | 12 | // Write data from `package-afterPublish` back to original `package.json`. 13 | fs.writeFile(ORIG_PKG_PATH, pkgData, (err) => { 14 | if (err) 15 | throw err 16 | }) 17 | 18 | // Delete the temporary `package-beforePublish` file. 19 | fs.unlink(BACKUP_PKG_PATH, (err) => { 20 | if (err) 21 | throw err 22 | }) 23 | 24 | // Delete the temporary `package-afterPublish` file. 25 | fs.unlink(RESTORE_PKG_PATH, (err) => { 26 | if (err) 27 | throw err 28 | }) 29 | -------------------------------------------------------------------------------- /bin/cleans.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import './env.mjs' 3 | 4 | console.log(chalk.blue('\n🧹 [chore] deleting all `node_modules` folders\n')) 5 | await $`find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +` 6 | 7 | console.log(chalk.blue('🧹 [chore] deleting all `dist` folders\n')) 8 | await $`find . -name 'dist' -type d -prune -exec rm -rf '{}' +` 9 | 10 | console.log(chalk.blue('🧹 [chore] deleting all `cdk.out` folders\n')) 11 | await $`find . -name 'cdk.out' -type d -prune -exec rm -rf '{}' +` 12 | 13 | console.log(chalk.blue('🧹 [chore] deleting all `generated` folders\n')) 14 | await $`find . -name 'generated' -type d -prune -exec rm -rf '{}' +` 15 | 16 | console.log(chalk.blue('🧹 [chore] deleting all `yarn.lock` files\n')) 17 | await $`find . -name 'yarn.lock' -type f -prune -exec rm -rf '{}' +` 18 | 19 | console.log(chalk.blue('🧹 [chore] deleting all `pnpm-lock.yaml` files\n')) 20 | await $`find . -name 'pnpm-lock.yaml' -type f -prune -exec rm -rf '{}' +` 21 | 22 | console.log(chalk.blue('\n📦 [install] re-installing all dependencies\n')) 23 | await $`pnpm install` 24 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/inspector.d.ts: -------------------------------------------------------------------------------- 1 | import type { logLevel } from './types'; 2 | declare const errorCodes: { 3 | FORBIDDEN: number; 4 | BAD_USER_INPUT: number; 5 | INTERNAL_SERVER_ERROR: number; 6 | TOO_MANY_REQUESTS: number; 7 | }; 8 | export type ErrorExtensions = { 9 | type: keyof typeof errorCodes; 10 | cause?: any; 11 | }; 12 | export type ErrorDetails = { 13 | error: string; 14 | type: ErrorExtensions['type']; 15 | code: number; 16 | cause?: ErrorExtensions['cause']; 17 | }; 18 | export declare class CustomError extends Error { 19 | error: ErrorDetails['error']; 20 | type: ErrorDetails['type']; 21 | code: ErrorDetails['code']; 22 | cause: ErrorDetails['cause']; 23 | details: ErrorDetails; 24 | constructor(message: string, extensions: ErrorExtensions); 25 | } 26 | export declare function parseError(error: Error): CustomError; 27 | export declare function log(message: string, obj?: any, level?: logLevel): void; 28 | export declare function printLog(message: any, level: logLevel): void; 29 | export {}; 30 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/inspector.d.ts: -------------------------------------------------------------------------------- 1 | import type { logLevel } from './types'; 2 | declare const errorCodes: { 3 | FORBIDDEN: number; 4 | BAD_USER_INPUT: number; 5 | INTERNAL_SERVER_ERROR: number; 6 | TOO_MANY_REQUESTS: number; 7 | }; 8 | export type ErrorExtensions = { 9 | type: keyof typeof errorCodes; 10 | cause?: any; 11 | }; 12 | export type ErrorDetails = { 13 | error: string; 14 | type: ErrorExtensions['type']; 15 | code: number; 16 | cause?: ErrorExtensions['cause']; 17 | }; 18 | export declare class CustomError extends Error { 19 | error: ErrorDetails['error']; 20 | type: ErrorDetails['type']; 21 | code: ErrorDetails['code']; 22 | cause: ErrorDetails['cause']; 23 | details: ErrorDetails; 24 | constructor(message: string, extensions: ErrorExtensions); 25 | } 26 | export declare function parseError(error: Error): CustomError; 27 | export declare function log(message: string, obj?: any, level?: logLevel): void; 28 | export declare function printLog(message: any, level: logLevel): void; 29 | export {}; 30 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/index.d.ts: -------------------------------------------------------------------------------- 1 | import { clone, decode, dotate, encode, filterXSS, isEmpty, isMatchingGlob, isUndefined, lowerFirst, merge, replaceAll, walk } from './utils'; 2 | export { PrismaAppSync } from './core'; 3 | export { CustomError, log } from './inspector'; 4 | export { queryBuilder } from './resolver'; 5 | export { QueryParams, QueryParamsCustom, BeforeHookParams, AfterHookParams, Authorization, AppSyncEvent, Identity, API_KEY, AWS_IAM, AMAZON_COGNITO_USER_POOLS, AWS_LAMBDA, OPENID_CONNECT, AppSyncResolverHandler, AppSyncResolverEvent, AppSyncIdentity, } from './types'; 6 | export { Authorizations } from './consts'; 7 | declare const _: { 8 | merge: typeof merge; 9 | clone: typeof clone; 10 | decode: typeof decode; 11 | encode: typeof encode; 12 | dotate: typeof dotate; 13 | isMatchingGlob: typeof isMatchingGlob; 14 | filterXSS: typeof filterXSS; 15 | isEmpty: typeof isEmpty; 16 | isUndefined: typeof isUndefined; 17 | lowerFirst: typeof lowerFirst; 18 | isObject: (val: any) => val is object; 19 | walk: typeof walk; 20 | replaceAll: typeof replaceAll; 21 | }; 22 | export { _ }; 23 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/index.d.ts: -------------------------------------------------------------------------------- 1 | import { clone, decode, dotate, encode, filterXSS, isEmpty, isMatchingGlob, isUndefined, lowerFirst, merge, replaceAll, walk } from './utils'; 2 | export { PrismaAppSync } from './core'; 3 | export { CustomError, log } from './inspector'; 4 | export { queryBuilder } from './resolver'; 5 | export { QueryParams, QueryParamsCustom, BeforeHookParams, AfterHookParams, Authorization, AppSyncEvent, Identity, API_KEY, AWS_IAM, AMAZON_COGNITO_USER_POOLS, AWS_LAMBDA, OPENID_CONNECT, AppSyncResolverHandler, AppSyncResolverEvent, AppSyncIdentity, } from './types'; 6 | export { Authorizations } from './consts'; 7 | declare const _: { 8 | merge: typeof merge; 9 | clone: typeof clone; 10 | decode: typeof decode; 11 | encode: typeof encode; 12 | dotate: typeof dotate; 13 | isMatchingGlob: typeof isMatchingGlob; 14 | filterXSS: typeof filterXSS; 15 | isEmpty: typeof isEmpty; 16 | isUndefined: typeof isUndefined; 17 | lowerFirst: typeof lowerFirst; 18 | isObject: (val: any) => val is object; 19 | walk: typeof walk; 20 | replaceAll: typeof replaceAll; 21 | }; 22 | export { _ }; 23 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clone, 3 | decode, 4 | dotate, 5 | encode, 6 | filterXSS, 7 | isEmpty, 8 | isMatchingGlob, 9 | isObject, 10 | isUndefined, 11 | lowerFirst, 12 | merge, 13 | replaceAll, 14 | walk, 15 | } from './utils' 16 | 17 | export { PrismaAppSync } from './core' 18 | export { CustomError, log } from './inspector' 19 | export { queryBuilder } from './resolver' 20 | export { 21 | QueryParams, 22 | QueryParamsCustom, 23 | BeforeHookParams, 24 | AfterHookParams, 25 | Authorization, 26 | AppSyncEvent, 27 | Identity, 28 | API_KEY, 29 | AWS_IAM, 30 | AMAZON_COGNITO_USER_POOLS, 31 | AWS_LAMBDA, 32 | OPENID_CONNECT, 33 | AppSyncResolverHandler, 34 | AppSyncResolverEvent, 35 | AppSyncIdentity, 36 | } from './types' 37 | export { Authorizations } from './consts' 38 | 39 | const _ = { 40 | merge, 41 | clone, 42 | decode, 43 | encode, 44 | dotate, 45 | isMatchingGlob, 46 | filterXSS, 47 | isEmpty, 48 | isUndefined, 49 | lowerFirst, 50 | isObject, 51 | walk, 52 | replaceAll, 53 | } 54 | 55 | export { _ } 56 | -------------------------------------------------------------------------------- /docs/changelog/1.0.0-rc.3.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | # 1.0.0-rc.3 6 | 7 | **🌟 Help us spread the word about Prisma-AppSync by starring the repo.** 8 | 9 | ## Fixes 10 | 11 | - [Issue with Queries returning a single parameter (such as `count` queries)](https://github.com/maoosi/prisma-appsync/issues/61) 12 | - [Issue with generated GraphQL input `CreateInput` when using `@default(uuid())` inside Prisma Schema](https://github.com/maoosi/prisma-appsync/issues/62) 13 | 14 | ## Credits 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
Sylvain
Sylvain

🧙‍♂️ 💻 🤔 📖
23 | 24 | ## Github sponsors 25 | 26 | Enjoy using Prisma-AppSync? Please consider [💛 Github sponsors](https://github.com/sponsors/maoosi). 27 | -------------------------------------------------------------------------------- /docs/quick-start/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy 2 | 3 | ## 👉 1. Prepare your local machine 4 | 5 | Make sure to install the below on your local machine: 6 | 7 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 8 | - [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install) 9 | - [Docker](https://docs.docker.com/get-docker/) 10 | 11 | Then [configure your local environment](https://docs.aws.amazon.com/cdk/v2/guide/cli.html#cli-environment) with the AWS Account of your choice. 12 | 13 | ## 👉 2. Setup a Database 14 | 15 | Setup the database of your choice. It doesn't have to be hosted on Amazon AWS, you can use any database supported by Prisma. If you are not sure what to use, we recommend using [PlanetScale](https://planetscale.com) and read the following [integration guide](https://planetscale.com/docs/tutorials/prisma-quickstart). 16 | 17 | ## 👉 3. Deploy on AWS 18 | 19 | Run the below CDK CLI command: 20 | 21 | > Where `DATABASE_URL` is your own [database connection url](https://www.prisma.io/docs/reference/database-reference/connection-urls). 22 | 23 | ```bash 24 | DATABASE_URL=mysql://xxx yarn deploy 25 | ``` 26 | 27 | **🚀 Done! Your GraphQL API is now ready to use.** 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": ["es2018"], 8 | "declaration": false, 9 | "strict": true, 10 | "noImplicitAny": false, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "esModuleInterop": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "baseUrl": ".", 24 | "paths": { 25 | "@client": ["packages/client/src/index"], 26 | "@client/*": ["packages/client/src/*"], 27 | "prisma-appsync/dist/server": ["dist/server"] 28 | }, 29 | "types": ["node"], 30 | "plugins": [ 31 | { 32 | "transform": "@zerollup/ts-transform-paths" 33 | } 34 | ] 35 | }, 36 | "include": ["**/*.ts", "**/*.tsx"], 37 | "exclude": ["node_modules"] 38 | } 39 | -------------------------------------------------------------------------------- /docs/security/xss-sanitizer.md: -------------------------------------------------------------------------------- 1 | # XSS sanitizer 2 | 3 | ## 👉 Usage 4 | 5 | Prisma-AppSync automatically perform XSS sanitization and encode all data coming through the GraphQL API. 6 | 7 | **Take a look at this example:** 8 | 9 |
10 | 11 | 1/ Assuming the following GraphQL Input: 12 | 13 | ```graphql 14 | mutation maliciousPost($title: String!) { 15 | createPost(data: { title: $title }) { 16 | title 17 | } 18 | } 19 | ``` 20 | 21 | ```json 22 | { 23 | "title": "" 24 | } 25 | ``` 26 | 27 |
28 | 29 | 2/ Prisma-AppSync will automatically remove the malicious code and encode Html, before storing anything in the database: 30 | 31 | | Column name | Value | 32 | | ------------- |:-------------| 33 | | title | `<img src>` | 34 | 35 |
36 | 37 | 3/ Finally, the GraphQL API will also automatically clarify (decode) all data before sending the response: 38 | 39 | ```ts 40 | console.log(post.title) // output: "" 41 | ``` 42 | 43 |
44 | 45 | ## 👉 Disable xss sanitization 46 | 47 | If you prefer to disable data sanitization, set the `sanitize` option to false when instantiating the Client: 48 | 49 | ```ts 50 | const prismaAppSync = new PrismaAppSync({ sanitize: false }) 51 | ``` 52 | -------------------------------------------------------------------------------- /tests/generator/schemas/@gql.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgres" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 9 | } 10 | 11 | generator appsync { 12 | provider = "./dist/generator.js" 13 | output = "./generated/@gql" 14 | } 15 | 16 | /// @gql(fields: { password: null }, subscriptions: null) 17 | model User { 18 | id Int @id @default(autoincrement()) 19 | email String @unique 20 | password String 21 | posts Post[] 22 | createdAt DateTime? @default(now()) 23 | } 24 | 25 | /// @gql(subscriptions: null, mutations: { delete: null, deleteMany: null }) 26 | /// @gql(scalars: { source: "AWSURL" }) 27 | model Post { 28 | id Int @id @default(autoincrement()) 29 | title String 30 | source String? 31 | author User? @relation(fields: [authorId], references: [id]) 32 | authorId Int? 33 | views Int? @default(1) 34 | status Status? @default(DRAFT) 35 | createdAt DateTime? @default(now()) 36 | } 37 | 38 | enum Status { 39 | DRAFT 40 | PUBLISHED 41 | DELETED 42 | } 43 | 44 | /// @gql(model: null) 45 | model Badge { 46 | level Int 47 | rank Int 48 | 49 | @@id([level, rank]) 50 | @@index([level, rank]) 51 | } 52 | -------------------------------------------------------------------------------- /bin/publish/_pkg.core.cleanse.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Define absolute paths for original pkg and temporary pkg. 5 | const ORIG_PKG_PATH = path.resolve(__dirname, '../../package.json') 6 | const BACKUP_PKG_PATH = path.resolve(__dirname, '../../package-beforePublish.json') 7 | const RESTORE_PKG_PATH = path.resolve(__dirname, '../../package-afterPublish.json') 8 | 9 | // Obtain original `package.json` contents. 10 | const pkgData = require(ORIG_PKG_PATH) 11 | 12 | // Write/cache the original `package.json` data to `package-beforePublish.json` file. 13 | fs.writeFile(BACKUP_PKG_PATH, JSON.stringify(pkgData, null, 4), (err) => { 14 | if (err) 15 | throw err 16 | }) 17 | 18 | // Write/cache the original `package.json` data to `package-afterPublish.json` file. 19 | fs.writeFile(RESTORE_PKG_PATH, JSON.stringify(pkgData, null, 4), (err) => { 20 | if (err) 21 | throw err 22 | }) 23 | 24 | // Remove all scripts from the scripts section. 25 | delete pkgData.scripts 26 | 27 | // Remove all pkgs from the devDependencies section. 28 | delete pkgData.devDependencies 29 | 30 | // Remove pnpm engine 31 | delete pkgData.engines.pnpm 32 | 33 | // Overwrite original `package.json` with new data (i.e. minus the specific data). 34 | fs.writeFile(ORIG_PKG_PATH, JSON.stringify(pkgData, null, 4), (err) => { 35 | if (err) 36 | throw err 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Sylvain Simao 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-brand: #5379f4; 7 | --vp-c-brand-light: #747bff; 8 | --vp-c-brand-lighter: #9499ff; 9 | --vp-c-brand-dark: #535bf2; 10 | --vp-c-brand-darker: #454ce1; 11 | } 12 | 13 | /** 14 | * Component: Button 15 | * -------------------------------------------------------------------------- */ 16 | 17 | :root { 18 | --vp-button-brand-border: var(--vp-c-brand-light); 19 | --vp-button-brand-text: var(--vp-c-text-dark-1); 20 | --vp-button-brand-bg: var(--vp-c-brand); 21 | --vp-button-brand-hover-border: var(--vp-c-brand-light); 22 | --vp-button-brand-hover-text: var(--vp-c-text-dark-1); 23 | --vp-button-brand-hover-bg: var(--vp-c-brand-light); 24 | --vp-button-brand-active-border: var(--vp-c-brand-light); 25 | --vp-button-brand-active-text: var(--vp-c-text-dark-1); 26 | --vp-button-brand-active-bg: var(--vp-button-brand-bg); 27 | } 28 | 29 | /** 30 | * Component: Home 31 | * -------------------------------------------------------------------------- */ 32 | 33 | :root { 34 | --vp-home-hero-name-color: transparent; 35 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #5379f4 30%, #f59533); 36 | } 37 | 38 | /** 39 | * Component: Algolia 40 | * -------------------------------------------------------------------------- */ 41 | 42 | .DocSearch { 43 | --docsearch-primary-color: var(--vp-c-brand) !important; 44 | } 45 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/resolvers.yaml: -------------------------------------------------------------------------------- 1 | - typeName: Query 2 | fieldName: getUser 3 | dataSource: prisma-appsync 4 | - typeName: Query 5 | fieldName: listUsers 6 | dataSource: prisma-appsync 7 | - typeName: Query 8 | fieldName: countUsers 9 | dataSource: prisma-appsync 10 | - typeName: Mutation 11 | fieldName: createUser 12 | dataSource: prisma-appsync 13 | - typeName: Mutation 14 | fieldName: createManyUsers 15 | dataSource: prisma-appsync 16 | - typeName: Mutation 17 | fieldName: updateUser 18 | dataSource: prisma-appsync 19 | - typeName: Mutation 20 | fieldName: updateManyUsers 21 | dataSource: prisma-appsync 22 | - typeName: Mutation 23 | fieldName: upsertUser 24 | dataSource: prisma-appsync 25 | - typeName: Mutation 26 | fieldName: deleteUser 27 | dataSource: prisma-appsync 28 | - typeName: Mutation 29 | fieldName: deleteManyUsers 30 | dataSource: prisma-appsync 31 | - typeName: Query 32 | fieldName: getPost 33 | dataSource: prisma-appsync 34 | - typeName: Query 35 | fieldName: listPosts 36 | dataSource: prisma-appsync 37 | - typeName: Query 38 | fieldName: countPosts 39 | dataSource: prisma-appsync 40 | - typeName: Mutation 41 | fieldName: createPost 42 | dataSource: prisma-appsync 43 | - typeName: Mutation 44 | fieldName: createManyPosts 45 | dataSource: prisma-appsync 46 | - typeName: Mutation 47 | fieldName: updatePost 48 | dataSource: prisma-appsync 49 | - typeName: Mutation 50 | fieldName: updateManyPosts 51 | dataSource: prisma-appsync 52 | - typeName: Mutation 53 | fieldName: upsertPost 54 | dataSource: prisma-appsync 55 | -------------------------------------------------------------------------------- /docs/quick-start/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## 👉 Option 1: Using the CLI Installer (recommended) 4 | 5 | Run the following command and follow the prompts 🙂 6 | 7 | ```bash 8 | npx create-prisma-appsync-app@latest 9 | ``` 10 | 11 | 🚀 Done! 12 | 13 | ## 👉 Option 2: Manual Install 14 | 15 | Add `prisma-appsync` to your project dependencies. 16 | 17 | ```bash 18 | # using yarn 19 | yarn add prisma-appsync 20 | 21 | # using npm 22 | npm i prisma-appsync 23 | ``` 24 | 25 | Edit your `schema.prisma` file and add: 26 | 27 | ```json 28 | generator appsync { 29 | provider = "prisma-appsync" 30 | } 31 | ``` 32 | 33 | Also make sure to use the right binary targets: 34 | 35 | ```json{3} 36 | generator client { 37 | provider = "prisma-client-js" 38 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 39 | } 40 | ``` 41 | 42 | Generate your Prisma Client (this will also generate your Prisma-AppSync client): 43 | 44 | ```bash 45 | npx prisma generate 46 | ``` 47 | 48 | Create your `handler.ts` Lambda handler (AppSync Direct Lambda Resolver): 49 | 50 | ```ts 51 | // Import generated Prisma-AppSync client (adjust path as necessary) 52 | import { PrismaAppSync } from './prisma/generated/prisma-appsync/client' 53 | 54 | // Instantiate Prisma-AppSync Client 55 | const prismaAppSync = new PrismaAppSync() 56 | 57 | // Lambda handler (AppSync Direct Lambda Resolver) 58 | export const main = async (event: any) => { 59 | return await prismaAppSync.resolve({ event }) 60 | } 61 | ``` 62 | 63 | Either copy the AWS CDK boilerplate provided with Prisma-AppSync into your project, OR just use it as a reference for your own CDK config: 64 | 65 | ```bash 66 | # path to cdk boilerplate 67 | ./node_modules/prisma-appsync/dist/boilerplate/cdk/ 68 | ``` 69 | 70 | Refer to [AWS CDK Toolkit docs ↗](https://docs.aws.amazon.com/cdk/v2/guide/cli.html) for more info. 71 | 72 | -------------------------------------------------------------------------------- /tests/generator/schemas/crud.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgres" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 9 | } 10 | 11 | generator appsync { 12 | provider = "./dist/generator.js" 13 | output = "./generated/crud" 14 | extendSchema = "./crud.gql" 15 | } 16 | 17 | model User { 18 | uuid String @id @default(uuid()) @db.VarChar(200) 19 | username String @unique 20 | email String @unique 21 | website String? 22 | hiddenField String? 23 | role Role? @default(USER) 24 | posts Post[] 25 | profile Profile? 26 | comments Comment[] 27 | } 28 | 29 | model Profile { 30 | uuid String @id @default(uuid()) @db.VarChar(200) 31 | owner User? @relation(fields: [ownerUuid], references: [uuid]) 32 | ownerUuid String? @unique @db.VarChar(200) 33 | bio String? 34 | } 35 | 36 | model Post { 37 | id Int @id @default(autoincrement()) 38 | title String 39 | author User? @relation(fields: [authorUuid], references: [uuid]) 40 | authorUuid String? @db.VarChar(200) 41 | published Boolean? @default(false) 42 | comments Comment[] 43 | views Int? @default(0) 44 | lastSavedAt DateTime? @default(now()) 45 | } 46 | 47 | model Comment { 48 | id Int @id @default(autoincrement()) 49 | author User? @relation(fields: [authorUuid], references: [uuid]) 50 | authorUuid String? @db.VarChar(200) 51 | post Post @relation(fields: [postId], references: [id]) 52 | postId Int 53 | message String 54 | lastSavedAt DateTime? @default(now()) 55 | } 56 | 57 | model Like { 58 | id Int @id @default(autoincrement()) 59 | authorUuid String @db.VarChar(200) 60 | postId Int 61 | 62 | @@unique([authorUuid, postId]) 63 | } 64 | 65 | enum Role { 66 | USER 67 | ADMIN 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-appsync", 3 | "version": "1.0.2", 4 | "description": "⚡ AppSync GraphQL API Generator for ◭ Prisma ORM.", 5 | "author": "maoosi ", 6 | "license": "BSD-2-Clause", 7 | "repository": "git@github.com:maoosi/prisma-appsync.git", 8 | "keywords": [ 9 | "api", 10 | "appsync", 11 | "aws", 12 | "crud", 13 | "generator", 14 | "graphql", 15 | "prisma", 16 | "prisma-appsync", 17 | "appsync-crud-api" 18 | ], 19 | "bin": "./dist/generator.js", 20 | "engines": { 21 | "node": ">=14", 22 | "pnpm": ">=6" 23 | }, 24 | "scripts": { 25 | "preinstall": "npx only-allow pnpm", 26 | "postinstall": "zx bin/postinstall.mjs", 27 | "build": "zx bin/build.mjs", 28 | "test": "zx bin/test.mjs", 29 | "dev": "zx bin/dev.mjs", 30 | "cleans": "zx bin/cleans.mjs", 31 | "publish": "zx bin/publish.mjs", 32 | "docs:dev": "vitepress dev docs", 33 | "docs:build": "vitepress build docs", 34 | "docs:serve": "vitepress serve docs" 35 | }, 36 | "devDependencies": { 37 | "@antfu/eslint-config": "^2.4.6", 38 | "@graphql-inspector/core": "^5.0.2", 39 | "@graphql-tools/schema": "^10.0.2", 40 | "@prisma/client": "^5.7.1", 41 | "@types/lodash": "^4.14.202", 42 | "@types/node": "^20.10.5", 43 | "@zerollup/ts-transform-paths": "^1.7.18", 44 | "all-contributors-cli": "^6.26.1", 45 | "easygraphql-tester": "^6.0.1", 46 | "esbuild": "^0.19.10", 47 | "eslint": "^8.56.0", 48 | "graphql": "16.8.1", 49 | "listr": "^0.14.3", 50 | "lodash": "^4.17.21", 51 | "pluralize": "^8.0.0", 52 | "prisma": "^5.7.1", 53 | "prompts": "^2.4.2", 54 | "sass": "^1.69.5", 55 | "ts-node": "^10.9.2", 56 | "tsconfig-paths": "^4.2.0", 57 | "typescript": "^5.3.3", 58 | "vite": "^5.0.10", 59 | "vite-tsconfig-paths": "^4.2.2", 60 | "vitepress": "1.0.0-rc.32", 61 | "vitest": "^1.1.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/client/mocks/lambda-event.ts: -------------------------------------------------------------------------------- 1 | import type { AppSyncEvent, AppSyncIdentity, Identity } from '../../../packages/client/src' 2 | import { _ } from '../../../packages/client/src' 3 | import { graphQlQueryToJson } from './graphql-json' 4 | 5 | export default function mockLambdaEvent({ 6 | request, 7 | graphQLParams, 8 | identity, 9 | }: { 10 | request: any 11 | graphQLParams: { query: string; variables?: any; operationName: string; raw?: any } 12 | identity: Identity 13 | }): AppSyncEvent[] { 14 | const events: AppSyncEvent[] = [] 15 | const selectionSetGraphQL = graphQLParams.query 16 | const variables = graphQLParams.variables || {} 17 | const operationName = graphQLParams.operationName 18 | const queries = graphQlQueryToJson(selectionSetGraphQL, { variables, operationName }) 19 | const parentType: string = Object.keys(queries)[0] 20 | 21 | for (let queryIndex = 0; queryIndex < Object.keys(queries[parentType]).length; queryIndex++) { 22 | const fieldName: string = Object.keys(queries[parentType])[queryIndex] 23 | const parentTypeName = parentType.charAt(0).toUpperCase() + parentType.slice(1) 24 | const selectionSet = queries[parentType][fieldName] 25 | const args = typeof selectionSet.__args !== 'undefined' ? selectionSet.__args : {} 26 | 27 | if (Object.keys(args).length > 0) 28 | delete selectionSet.__args 29 | 30 | const selectionSetList = Object.keys(_.dotate(selectionSet)) 31 | .filter(selection => selection !== '.') 32 | .map(selection => selection.replace(/\./g, '/')) 33 | 34 | selectionSetList.unshift('__typename') 35 | 36 | const event: AppSyncEvent = { 37 | arguments: args, 38 | source: null, 39 | identity: identity as AppSyncIdentity, 40 | info: { 41 | parentTypeName, 42 | fieldName, 43 | variables, 44 | selectionSetList, 45 | selectionSetGraphQL, 46 | }, 47 | request, 48 | prev: { result: {} }, 49 | stash: {}, 50 | } 51 | 52 | events.push(event) 53 | } 54 | 55 | return events 56 | } 57 | -------------------------------------------------------------------------------- /docs/features/gql-schema.md: -------------------------------------------------------------------------------- 1 | # Tweaking GraphQL Schema 2 | 3 | Prisma-AppSync provides ways to tweak and customise the GraphQL Schema output. 4 | 5 | ## 👉 Models directives 6 | 7 | Tweaking the GraphQL schema for a given model require to write directives via AST comments (triple-slash `///`). 8 | 9 | ```prisma 10 | /// @gql(mutations: null, subscriptions: null) 11 | /// @gql(fields: { password: null }) 12 | /// @gql(scalars: { email: "AWSEmail" }) 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | email String 16 | password String 17 | } 18 | ``` 19 | 20 | ## 👉 Usage with @gql syntax 21 | 22 | ### Disabling an entire model 23 | 24 | ```prisma 25 | // Disable all queries, mutations and subscriptions 26 | @gql(model: null) 27 | ``` 28 | 29 | ### Disabling queries 30 | 31 | ```prisma 32 | // Disable all queries (get, list, count, ...) 33 | @gql(queries: null) 34 | 35 | // Disable granular queries 36 | @gql(queries: { list: null, count: null }) 37 | ``` 38 | 39 | ### Disabling mutations 40 | 41 | ```prisma 42 | // Disable all mutations (create, update, upsert, delete, ...) 43 | @gql(mutations: null) 44 | 45 | // Disable granular mutations 46 | @gql(mutations: { update: null, delete: null }) 47 | ``` 48 | 49 | > **Cascading Rules:** 50 | > 51 | > - Disabling `update` **will also disable** `upsert` 52 | > - Disabling `create` **will also disable** `upsert` 53 | > - Disabling `mutations` **will also disable** `subscriptions` 54 | 55 | ### Disabling subscriptions 56 | 57 | ```prisma 58 | // Disable all subscriptions (onCreated, onUpdated, ...) 59 | @gql(subscriptions: null) 60 | 61 | // Disable granular subscriptions 62 | @gql(mutations: { onCreated: null, onUpdated: null }) 63 | ``` 64 | 65 | ### Hiding fields 66 | 67 | ```prisma 68 | // If applied to a model with a `password` field: 69 | // hide `password` field from the generated Type 70 | @gql(fields: { password: null }) 71 | ``` 72 | 73 | > **Note:** To maintain Prisma Client integrity, hidden fields remain writable in mutation operations. 74 | 75 | ### Custom scalars on fields 76 | 77 | ```prisma 78 | // If applied to a model with a `website` (string) field: 79 | // use scalar `AWSURL` instead of default `String` 80 | @gql(scalars: { website: "AWSURL" }) 81 | ``` 82 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/guard.d.ts: -------------------------------------------------------------------------------- 1 | import type { Context, PrismaClient, QueryParams, Shield, ShieldAuthorization } from './types'; 2 | /** 3 | * #### Sanitize data (parse xss + encode html). 4 | * 5 | * @param {any} data 6 | * @returns any 7 | */ 8 | export declare function sanitize(data: any): Promise; 9 | /** 10 | * #### Clarify data (decode html). 11 | * 12 | * @param {any} data 13 | * @returns any 14 | */ 15 | export declare function clarify(data: any): Promise; 16 | /** 17 | * #### Returns an authorization object from a Shield configuration passed as input. 18 | * 19 | * @param {Shield} options.shield 20 | * @param {string[]} options.paths 21 | * @param {Context} options.context 22 | * @returns ShieldAuthorization 23 | */ 24 | export declare function getShieldAuthorization({ shield, paths, context, }: { 25 | shield: Shield; 26 | paths: string[]; 27 | context: Context; 28 | }): Promise; 29 | /** 30 | * #### Returns GraphQL query depth for any given Query. 31 | * 32 | * @param {any} options 33 | * @param {string[]} options.paths 34 | * @param {Context} options.context 35 | * @param {any} options.fieldsMapping 36 | * @returns number 37 | */ 38 | export declare function getDepth({ paths, context, fieldsMapping }: { 39 | paths: string[]; 40 | context: Context; 41 | fieldsMapping: any; 42 | }): number; 43 | /** 44 | * #### Execute hooks that apply to a given Query. 45 | * 46 | * @param {any} options 47 | * @param {'before' | 'after'} options.when 48 | * @param {any} options.hooks 49 | * @param {PrismaClient} options.prismaClient 50 | * @param {QueryParams} options.QueryParams 51 | * @param {any | any[]} options.result 52 | * @returns Promise 53 | */ 54 | export declare function runHooks({ when, hooks, prismaClient, QueryParams, result, }: { 55 | when: 'before' | 'after'; 56 | hooks: any; 57 | prismaClient: PrismaClient; 58 | QueryParams: QueryParams; 59 | result?: any | any[]; 60 | }): Promise; 61 | export declare function preventDOS({ callerUuid, maxReqPerMinute, }: { 62 | callerUuid: string; 63 | maxReqPerMinute: number; 64 | }): Promise<{ 65 | limitExceeded: boolean; 66 | count: number; 67 | }>; 68 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/guard.d.ts: -------------------------------------------------------------------------------- 1 | import type { Context, PrismaClient, QueryParams, Shield, ShieldAuthorization } from './types'; 2 | /** 3 | * #### Sanitize data (parse xss + encode html). 4 | * 5 | * @param {any} data 6 | * @returns any 7 | */ 8 | export declare function sanitize(data: any): Promise; 9 | /** 10 | * #### Clarify data (decode html). 11 | * 12 | * @param {any} data 13 | * @returns any 14 | */ 15 | export declare function clarify(data: any): Promise; 16 | /** 17 | * #### Returns an authorization object from a Shield configuration passed as input. 18 | * 19 | * @param {Shield} options.shield 20 | * @param {string[]} options.paths 21 | * @param {Context} options.context 22 | * @returns ShieldAuthorization 23 | */ 24 | export declare function getShieldAuthorization({ shield, paths, context, }: { 25 | shield: Shield; 26 | paths: string[]; 27 | context: Context; 28 | }): Promise; 29 | /** 30 | * #### Returns GraphQL query depth for any given Query. 31 | * 32 | * @param {any} options 33 | * @param {string[]} options.paths 34 | * @param {Context} options.context 35 | * @param {any} options.fieldsMapping 36 | * @returns number 37 | */ 38 | export declare function getDepth({ paths, context, fieldsMapping }: { 39 | paths: string[]; 40 | context: Context; 41 | fieldsMapping: any; 42 | }): number; 43 | /** 44 | * #### Execute hooks that apply to a given Query. 45 | * 46 | * @param {any} options 47 | * @param {'before' | 'after'} options.when 48 | * @param {any} options.hooks 49 | * @param {PrismaClient} options.prismaClient 50 | * @param {QueryParams} options.QueryParams 51 | * @param {any | any[]} options.result 52 | * @returns Promise 53 | */ 54 | export declare function runHooks({ when, hooks, prismaClient, QueryParams, result, }: { 55 | when: 'before' | 'after'; 56 | hooks: any; 57 | prismaClient: PrismaClient; 58 | QueryParams: QueryParams; 59 | result?: any | any[]; 60 | }): Promise; 61 | export declare function preventDOS({ callerUuid, maxReqPerMinute, }: { 62 | callerUuid: string; 63 | maxReqPerMinute: number; 64 | }): Promise<{ 65 | limitExceeded: boolean; 66 | count: number; 67 | }>; 68 | -------------------------------------------------------------------------------- /packages/boilerplate/cdk/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new */ 2 | import { join } from 'path' 3 | import { App } from 'aws-cdk-lib' 4 | import { AuthorizationType } from 'aws-cdk-lib/aws-appsync' 5 | import { kebabCase } from 'scule' 6 | import { AppSyncStack } from './appsync' 7 | 8 | const app = new App() 9 | 10 | new AppSyncStack(app, kebabCase('{{ projectName }}'), { 11 | resourcesPrefix: '{{ projectName }}', 12 | schema: join(process.cwd(), '{{ relativeGqlSchemaPath }}'), 13 | resolvers: join(process.cwd(), '{{ relativeYmlResolversPath }}'), 14 | function: { 15 | code: join(process.cwd(), '{{ relativeHandlerPath }}'), 16 | memorySize: 1536, 17 | useWarmUp: 0, // useWarmUp > 0 will incur extra costs 18 | environment: { 19 | NODE_ENV: 'production', 20 | DATABASE_URL: process.env.DATABASE_URL, 21 | }, 22 | bundling: { 23 | minify: true, 24 | sourceMap: true, 25 | forceDockerBundling: true, 26 | commandHooks: { 27 | beforeBundling(inputDir: string, outputDir: string): string[] { 28 | return [`cp ${inputDir}/{{ relativePrismaSchemaPath }} ${outputDir}`] 29 | }, 30 | beforeInstall() { 31 | return [] 32 | }, 33 | afterBundling() { 34 | return [ 35 | 'npx prisma generate', 36 | 'rm -rf generated', 37 | 38 | // npm + yarn 1.x 39 | 'rm -rf node_modules/@prisma/engines', 40 | 'rm -rf node_modules/@prisma/client/node_modules', 41 | 'rm -rf node_modules/.bin', 42 | 'rm -rf node_modules/prisma', 43 | 'rm -rf node_modules/prisma-appsync', 44 | ] 45 | }, 46 | }, 47 | nodeModules: ['prisma', '@prisma/client'], 48 | environment: { 49 | NODE_ENV: 'production', 50 | }, 51 | }, 52 | }, 53 | authorizationConfig: { 54 | defaultAuthorization: { 55 | authorizationType: AuthorizationType.API_KEY, 56 | }, 57 | }, 58 | }) 59 | 60 | app.synth() 61 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/prefer-global/process */ 2 | /* eslint-disable no-console */ 3 | import { cli as cleye } from 'cleye' 4 | import { type ServerOptions, useAppSyncSimulator } from './appsync-simulator' 5 | 6 | // npx vite-node ./server.ts --watch -- 7 | // --handler handler.ts 8 | // --schema prisma/generated/prisma-appsync/schema.gql 9 | // --resolvers prisma/generated/prisma-appsync/resolvers.yaml 10 | // --port 4000 11 | // --wsPort 4001 12 | // --watchers '[{"watch":["**/*.prisma","*.prisma"],"exec":"npx prisma generate && touch ./server.ts"}]' 13 | export const argv = cleye({ 14 | name: 'prisma-appsync-server', 15 | flags: { 16 | handler: { 17 | type: String, 18 | description: 'Lambda handler (.ts file)', 19 | default: 'handler.ts', 20 | }, 21 | schema: { 22 | type: String, 23 | description: 'GraphQL schema (.gql file)', 24 | default: 'generated/prisma-appsync/schema.gql', 25 | }, 26 | resolvers: { 27 | type: String, 28 | description: 'Resolvers (.yaml file)', 29 | default: 'generated/prisma-appsync/resolvers.yaml', 30 | }, 31 | port: { 32 | type: Number, 33 | description: 'HTTP server port', 34 | default: 4000, 35 | }, 36 | wsPort: { 37 | type: Number, 38 | description: 'WS server port', 39 | default: 4001, 40 | }, 41 | watchers: { 42 | type: String, 43 | description: 'Watchers config', 44 | default: '', 45 | }, 46 | }, 47 | }) 48 | 49 | export async function createServer(serverOptions: ServerOptions): Promise { 50 | if (!process?.env?.DATABASE_URL) 51 | throw new Error('Missing "DATABASE_URL" env var.') 52 | 53 | if (!serverOptions?.lambdaHandler?.main) 54 | throw new Error('Handler has no exported function "main".') 55 | 56 | const simulator = useAppSyncSimulator(serverOptions) 57 | 58 | await simulator.start() 59 | 60 | console.log(`\n🧩 GraphQL IDE: http://localhost:${serverOptions.port}`) 61 | console.log(`🔌 API endpoint: http://localhost:${serverOptions.port}/graphql`) 62 | } 63 | 64 | export * from './appsync-simulator' 65 | -------------------------------------------------------------------------------- /docs/quick-start/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | **Prisma-AppSync** seamlessly transforms your [Prisma Schema](https://www.prisma.io) into a comprehensive GraphQL API, tailored for [AWS AppSync](https://aws.amazon.com/appsync/). 4 | 5 |
6 | 7 | **From `schema.prisma`:** 8 | 9 | ```prisma 10 | model Post { 11 | id Int 12 | title String 13 | } 14 | ``` 15 | 16 | 17 | 18 | **To full-blown GraphQL API:** 19 | 20 | ```graphql 21 | query list { 22 | listPosts { 23 | id 24 | title 25 | } 26 | } 27 | ``` 28 | 29 |
30 | 31 | ## 👉 Features 32 | 33 | 💎 **Use your ◭ Prisma Schema**
Quickly define your data model and deploy a GraphQL API tailored for AWS AppSync. 34 | 35 | ⚡️ **Auto-generated CRUD operations**
Using Prisma syntax, with a robust TS Client designed for AWS Lambda Resolvers. 36 | 37 | ⛑ **Pre-configured security**
Built-in XSS protection, query depth limitation, and in-memory rate limiting. 38 | 39 | 🔐 **Fine-grained ACL and authorization**
Flexible security options such as API keys, IAM, Cognito, and more. 40 | 41 | 🔌 **Fully extendable features**
Customize your GraphQL schema, API resolvers, and data flow as needed. 42 | 43 | ## 👉 Built around 4 packages 44 | 45 | 46 | 47 | 54 | 55 | 56 | 63 | 64 | 65 | 72 | 73 | 74 | 81 | 82 |
48 | 49 | **`packages/generator`** 50 | 51 | Generator for [Prisma ORM](https://www.prisma.io/), whose role is to parse your Prisma Schema and generate all the necessary components to run and deploy a GraphQL API tailored for AWS AppSync. 52 | 53 |
57 | 58 | **`packages/client`** 59 | 60 | Think of it as [Prisma Client](https://www.prisma.io/client) for GraphQL. Fully typed and designed for AWS Lambda AppSync Resolvers. It can handle CRUD operations with just a single line of code, or be fully extended. 61 | 62 |
66 | 67 | **`packages/installer`** 68 | 69 | Interactive CLI tool that streamlines the setup of new Prisma-AppSync projects, making it as simple as running `npx create-prisma-appsync-app@latest`. 70 | 71 |
75 | 76 | **`packages/server`** 77 | 78 | Local dev environment that mimics running Prisma-AppSync in production. It includes an AppSync simulator, local Lambda resolvers execution, a GraphQL IDE, hot-reloading, and authorizations. 79 | 80 |
83 | -------------------------------------------------------------------------------- /tests/client/mocks/lambda-identity.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AMAZON_COGNITO_USER_POOLS, 3 | API_KEY, 4 | AWS_IAM, 5 | AWS_LAMBDA, 6 | Authorization, 7 | Identity, 8 | OPENID_CONNECT, 9 | } from '../../../packages/client/src' 10 | import { 11 | Authorizations, 12 | } from '../../../packages/client/src' 13 | 14 | export default function mockLambdaIdentity(identity: Authorization, opts?: mockOptions): Identity { 15 | if (identity === Authorizations.AWS_IAM) { 16 | const mock: AWS_IAM = { 17 | accountId: 'string', 18 | cognitoIdentityPoolId: 'string', 19 | cognitoIdentityId: 'string', 20 | sourceIp: [opts?.sourceIp || 'undefined'], 21 | username: opts?.username || 'undefined', 22 | userArn: 'string', 23 | cognitoIdentityAuthType: 'string', 24 | cognitoIdentityAuthProvider: 'string', 25 | } 26 | return mock 27 | } 28 | else if (identity === Authorizations.AMAZON_COGNITO_USER_POOLS) { 29 | // eslint-disable-next-line n/prefer-global/buffer 30 | const decodedJWTToken = opts?.jwt ? JSON.parse(Buffer.from(opts?.jwt?.split('.')[1], 'base64').toString()) : {} 31 | const mock: AMAZON_COGNITO_USER_POOLS = { 32 | sub: decodedJWTToken?.sub || 'undefined', 33 | issuer: 'string', 34 | username: decodedJWTToken?.['cognito:username'] || 'undefined', 35 | claims: decodedJWTToken, 36 | sourceIp: [opts?.sourceIp || 'undefined'], 37 | defaultAuthStrategy: 'string', 38 | groups: ['admin', 'member'], 39 | } 40 | return mock 41 | } 42 | else if (identity === Authorizations.AWS_LAMBDA) { 43 | const mock: AWS_LAMBDA = { 44 | resolverContext: opts?.resolverContext || 'undefined', 45 | } 46 | return mock 47 | } 48 | else if (identity === Authorizations.OPENID_CONNECT) { 49 | const mock: OPENID_CONNECT = { 50 | claims: { 51 | sub: opts?.sub || 'undefined', 52 | aud: 'string', 53 | azp: 'string', 54 | iss: 'string', 55 | exp: 1630923679, 56 | iat: 1630837279, 57 | gty: 'string', 58 | }, 59 | issuer: 'string', 60 | sub: opts?.sub || 'undefined', 61 | } 62 | return mock 63 | } 64 | else { 65 | const mock: API_KEY = null 66 | return mock 67 | } 68 | } 69 | 70 | type mockOptions = { 71 | sub: string 72 | username: string 73 | sourceIp: string 74 | resolverContext: any 75 | jwt?: string 76 | } 77 | -------------------------------------------------------------------------------- /docs/security/shield-acl.md: -------------------------------------------------------------------------------- 1 | # Shield (Access Control Rules) 2 | 3 | Fine-grained access control rules can be used via the `shield` property of Prisma-AppSync client, directly inside the Lambda Handler function. 4 | 5 | ::: warning SECURITY MUST NEVER BE TAKEN FOR GRANTED 6 | Prisma-AppSync implements a basic mechanism to help mitigate some common issues. However, accuracy is not guaranteed and you should always test your own API security implementation. 7 | ::: 8 | 9 | ## 👉 Basic example 10 | 11 | For example, we might want to only allow access to `PUBLISHED` posts: 12 | 13 | ```ts 14 | return await prismaAppSync.resolve({ 15 | event, 16 | shield: () => { 17 | // Prisma filtering syntax 18 | // https://www.prisma.io/docs/concepts/components/prisma-client/filtering-and-sorting 19 | const isPublished = { status: { equals: 'PUBLISHED' } } 20 | 21 | return { 22 | // Micromatch syntax 23 | // https://github.com/micromatch/micromatch 24 | 'getPost{,/**}': { 25 | rule: isPublished, 26 | reason: () => 'Unpublished Posts cannot be accessed.', 27 | }, 28 | } 29 | }, 30 | }) 31 | ``` 32 | 33 | Useful links to create shield rules: 34 | 35 | - [Micromatch syntax](https://github.com/micromatch/micromatch) 36 | - [Micromatch tester](https://globster.xyz/?q=getPost%7B%2C%2F**%7D&f=getPost%2Ftitle%2CgetPost%2Fstatus) 37 | 38 | ## 👉 Usage with AppSync Authorization modes 39 | 40 | Combining fine-grained access control with [AppSync Authorization modes](/security/appsync-authz) allows to implement powerful controls around data. 41 | 42 | Let's assume we want to restrict API access to users logged in via `AMAZON_COGNITO_USER_POOLS` and only allow the owner of a given Post to modify it: 43 | 44 | ```ts 45 | return await prismaAppSync.resolve({ 46 | event, 47 | shield: ({ authorization, identity }: QueryParams) => { 48 | const isCognitoAuth = authorization === Authorizations.AMAZON_COGNITO_USER_POOLS 49 | const isOwner = { owner: { cognitoSub: identity?.sub } } 50 | 51 | return { 52 | '**': { 53 | rule: isCognitoAuth, 54 | reason: ({ model }) => `${model} access is restricted to logged-in users.`, 55 | }, 56 | '{update,upsert,delete}Post{,/**}': { 57 | rule: isOwner, 58 | reason: ({ model }) => `${model} can only be modified by their owner.`, 59 | }, 60 | } 61 | }, 62 | }) 63 | ``` 64 | 65 | > The above example implies using Cognito User Pools Authorization. Plus having set up an `Owner` relation on the `Post` model, and a `cognitoSub` field on the `User` model (containing all users `sub`). 66 | 67 | ## 🚨 Order matters 68 | 69 | The latest matching rule ALWAYS overrides previous ones. 70 | 71 | ```ts 72 | // Bad - Second rule overrides first one 73 | return { 74 | 'listUsers/password': false, 75 | 'listUsers{,/**}': true, 76 | } 77 | 78 | // Good - Always write the more specific rules last 79 | return { 80 | 'listUsers{,/**}': true, 81 | 'listUsers/password': false 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/features/resolvers.md: -------------------------------------------------------------------------------- 1 | # Custom resolvers 2 | 3 | Let's assume we want to extend our GraphQL CRUD API and add a custom mutation `incrementPostsViews` based on our Prisma Schema: 4 | 5 | ```prisma 6 | model Post { 7 | id Int @id @default(autoincrement()) 8 | views Int 9 | } 10 | ``` 11 | 12 | ## 👉 1. Extending our GraphQL Schema 13 | 14 | To extend our auto-generated `schema.gql`, we will create a new `custom-schema.gql` file next to our `schema.prisma` file: 15 | 16 | ```graphql 17 | extend type Mutation { 18 | """ 19 | Increment post views by +1 20 | """ 21 | incrementPostsViews(postId: Int!): Post 22 | } 23 | ``` 24 | 25 | For Prisma-AppSync to merge our `custom-schema.gql` with the auto-generated schema, we edit the `schema.prisma` generator config: 26 | 27 | ```json{3} 28 | generator appsync { 29 | provider = "prisma-appsync" 30 | extendSchema = "./custom-schema.gql" 31 | } 32 | ``` 33 | 34 | ## 👉 2. Extending our Resolvers Config 35 | 36 | For AWS AppSync to be able to use our new `incrementPostsViews` mutation, we also create a new `custom-resolvers.yaml` next to our `schema.prisma` file: 37 | 38 | ```yaml 39 | - typeName: Mutation 40 | fieldName: incrementPostsViews 41 | dataSource: prisma-appsync 42 | ``` 43 | 44 | For Prisma-AppSync to merge our `custom-resolvers.yaml` with the auto-generated resolvers config, we edit the `schema.prisma` generator config: 45 | 46 | ```json{4} 47 | generator appsync { 48 | provider = "prisma-appsync" 49 | extendSchema = "./custom-schema.gql" 50 | extendResolvers = "./custom-resolvers.yaml" 51 | } 52 | ``` 53 | 54 | ## 👉 3. Coding our new Resolver Function 55 | 56 | Querying the `incrementPostsViews` mutation will automatically run a Resolver Function inside our Lambda `handler.ts` file. This is where we will code our custom business logic. 57 | 58 | ```ts 59 | return await prismaAppSync.resolve<'incrementPostsViews'>({ 60 | event, 61 | resolvers: { 62 | // code for our new resolver function 63 | incrementPostsViews: async ({ args, prismaClient }: QueryParamsCustom) => { 64 | return await prismaClient.post.update({ 65 | data: { views: { increment: 1 } }, 66 | where: { id: args.postId } 67 | }) 68 | }, 69 | } 70 | }) 71 | ``` 72 | 73 | ## 👉 4. Updating our CDK file for bundling 74 | 75 | To make sure our `custom-schema.gql` and `custom-resolvers.yaml` are properly bundled and deployed on AWS, we update the `beforeBundling` function inside `cdk/index.ts`: 76 | 77 | ```ts 78 | function: { 79 | bundling: { 80 | commandHooks: { 81 | beforeBundling(inputDir: string, outputDir: string): string[] { 82 | const schema = path.join(inputDir, 'prisma/schema.prisma') 83 | const gql = path.join(inputDir, 'prisma/custom-schema.gql') 84 | const yaml = path.join(inputDir, 'prisma/custom-resolvers.yaml') 85 | 86 | return [ 87 | `cp ${schema} ${outputDir}`, 88 | `cp ${gql} ${outputDir}`, 89 | `cp ${yaml} ${outputDir}`, 90 | ] 91 | }, 92 | }, 93 | } 94 | } 95 | ``` 96 | 97 | 🚀 **Done! Next time we deploy on AWS, we will be able to use our new `incrementPostsViews` mutation.** 98 | -------------------------------------------------------------------------------- /bin/build.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | /* eslint-disable no-console */ 3 | /* eslint-disable n/prefer-global/process */ 4 | import './env.mjs' 5 | 6 | try { 7 | // cleanup previous generated files 8 | console.log(chalk.blue('\n🧹 [chore] cleanup\n')) 9 | await $`rm -rf dist`.quiet() 10 | 11 | if (!argv?.ignoreGenerator) { 12 | console.log(chalk.blue('🛠️ [build] packages/generator')) 13 | 14 | // build Prisma-AppSync Generator 15 | await $`esbuild packages/generator/src/index.ts --bundle --format=cjs --keep-names --platform=node --target=node18 --external:fsevents --external:_http_common --outfile=dist/generator.js --define:import.meta.url='_importMetaUrl' --banner:js="const _importMetaUrl=require('url').pathToFileURL(__filename)"`.quiet() 16 | } 17 | 18 | if (!argv?.ignoreClient) { 19 | console.log(chalk.blue('🛠️ [build] packages/client')) 20 | 21 | // build Prisma-AppSync Client 22 | await $`esbuild packages/client/src/index.ts --bundle '--define:process.env.NODE_ENV="production"' --format=cjs --minify --keep-names --platform=node --target=node18 --external:fsevents --external:@prisma/client --outfile=dist/client/index.js --legal-comments=inline`.quiet() 23 | 24 | console.log(chalk.blue('🛠️ [build] packages/client types')) 25 | 26 | // build Prisma-AppSync Client TS Declarations 27 | await $`tsc packages/client/src/*.ts --outDir dist/client/ --declaration --emitDeclarationOnly --esModuleInterop --downlevelIteration`.nothrow().quiet() 28 | } 29 | 30 | if (!argv?.ignoreInstaller) { 31 | 32 | if (process.env.COMPILE_MODE === 'preview') { 33 | console.log(chalk.blue('🛠️ [build] packages/installer (preview mode)')) 34 | 35 | // build installer (preview mode) 36 | await $`esbuild packages/installer/src/index.ts --bundle '--define:process.env.NODE_ENV="production"' '--define:process.env.COMPILE_MODE="preview"' --format=cjs --minify --keep-names --platform=node --target=node18 --external:fsevents --external:_http_common --outfile=dist/installer/bin/index.js`.quiet() 37 | } 38 | else { 39 | console.log(chalk.blue('🛠️ [build] packages/installer')) 40 | 41 | // build installer (default) 42 | await $`esbuild packages/installer/src/index.ts --bundle '--define:process.env.NODE_ENV="production"' --format=cjs --minify --keep-names --platform=node --target=node18 --external:fsevents --external:_http_common --outfile=dist/installer/bin/index.js`.quiet() 43 | } 44 | } 45 | 46 | if (!argv?.ignoreServer) { 47 | console.log(chalk.blue('🛠️ [build] packages/server')) 48 | 49 | // build server 50 | await $`esbuild packages/server/src/index.ts --bundle --format=cjs --minify --keep-names --platform=node --target=node18 --external:fsevents --external:@prisma/client --external:amplify-appsync-simulator --external:_http_common --outfile=dist/server/index.js`.quiet() 51 | 52 | // build server TS Declarations 53 | await $`cp packages/server/src/index.d.ts dist/server/index.d.ts && chmod -R 755 dist`.quiet() 54 | 55 | // copy server .vtl files into build folder 56 | await $`cp -R packages/server/src/*.vtl dist/server && chmod -R 755 dist`.quiet() 57 | } 58 | } 59 | catch (error) { 60 | console.log(chalk.red(`🚨 [build] error\n\n${error}`)) 61 | } 62 | -------------------------------------------------------------------------------- /packages/generator/src/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // Dependencies 3 | import { generatorHandler } from '@prisma/generator-helper' 4 | import PrismaAppSyncGenerator from './generator' 5 | 6 | // Read Prisma AppSync version 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 8 | const generatorVersion = require('../../../package.json').version 9 | 10 | // Prisma AppSync Generator Handler 11 | generatorHandler({ 12 | onManifest() { 13 | return { 14 | defaultOutput: 'generated/prisma-appsync', 15 | prettyName: 'Prisma-AppSync', 16 | requiresEngines: ['queryEngine'], 17 | version: generatorVersion, 18 | } 19 | }, 20 | async onGenerate(options: any) { 21 | if (options?.generator?.output) { 22 | try { 23 | // Default client generator 24 | const clientGenerator = options?.otherGenerators?.find(g => g?.provider?.value === 'prisma-client-js') 25 | 26 | // Is debug mode enabled? 27 | const debug: boolean = typeof options?.generator?.config?.debug !== 'undefined' 28 | ? Boolean(options.generator.config.debug) 29 | : false 30 | 31 | // Read output dir (ensures previous version of prisma are still supported) 32 | const outputDir: string = options?.generator?.output?.value || String() 33 | 34 | // Read preview features 35 | const previewFeatures: string[] = clientGenerator?.previewFeatures || [] 36 | 37 | if (debug) { 38 | console.log('[Prisma-AppSync] Generator config: ', { 39 | ...options.generator.config, 40 | output: outputDir, 41 | previewFeatures, 42 | }) 43 | } 44 | 45 | // Initiate generator 46 | const generator = new PrismaAppSyncGenerator({ 47 | outputDir, // output directory 48 | prismaDmmf: options.dmmf, // prisma dmmf object 49 | prismaSchemaPath: options.schemaPath, // prisma schema path 50 | userGraphQLPath: options.generator?.config?.extendSchema, // user gql path 51 | userResolversPath: options.generator?.config?.extendResolvers, // user resolvers path 52 | defaultDirective: options?.generator?.config?.defaultDirective, // default directive(s) 53 | }) 54 | 55 | // Make appsync schema 56 | console.log('[Prisma-AppSync] Generating AppSync Schema.') 57 | await generator.makeAppSyncSchema() 58 | 59 | // Make appsync resolvers 60 | console.log('[Prisma-AppSync] Generating AppSync Resolvers.') 61 | await generator.makeAppSyncResolvers() 62 | 63 | // Make client config 64 | console.log('[Prisma-AppSync] Generating Client Runtime Config.') 65 | await generator.makeClientRuntimeConfig() 66 | } 67 | catch (e) { 68 | console.error('Error: unable to compile files for Prisma AppSync Generator') 69 | throw e 70 | } 71 | } 72 | else { 73 | throw new Error('No output was specified for Prisma AppSync Generator') 74 | } 75 | }, 76 | }) 77 | -------------------------------------------------------------------------------- /docs/security/appsync-authz.md: -------------------------------------------------------------------------------- 1 | # AppSync Authorization modes 2 | 3 | AWS AppSync provides [authz directives ↗](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html) for configuring security and data protection. 4 | 5 | ::: warning SECURITY MUST NEVER BE TAKEN FOR GRANTED 6 | Prisma-AppSync implements a basic mechanism to help mitigate some common issues. However, accuracy is not guaranteed and you should always test your own API security implementation. 7 | ::: 8 | 9 | ## 👉 Models directives 10 | 11 | Applying AppSync authorization modes for a given model require to write directives using AST comments (triple-slash `///`). 12 | 13 | ```prisma 14 | /// @auth(model: [{ allow: iam }, { allow: apiKey }]) 15 | model Post { 16 | id Int @id @default(autoincrement()) 17 | title String 18 | } 19 | ``` 20 | 21 | ## 👉 Usage with @auth syntax 22 | 23 | > **Note:** For now, `@auth` only works supports the `allow` key. 24 | 25 | ### Entire model 26 | 27 | ```prisma 28 | // Apply to all queries, mutations and subscriptions 29 | @auth(model: [{ allow: iam }]) 30 | ``` 31 | 32 | ### Queries 33 | 34 | ```prisma 35 | // Apply to all queries (get, list, count, ...) 36 | @auth(queries: [{ allow: iam }]) 37 | 38 | // Apply to granular queries 39 | @auth(queries: { list: [{ allow: iam }] }) 40 | ``` 41 | 42 | ### Mutations 43 | 44 | ```prisma 45 | // Apply to all mutations (create, update, upsert, delete, ...) 46 | @auth(mutations: [{ allow: iam }]) 47 | 48 | // Apply to granular mutations 49 | @auth(mutations: { create: [{ allow: iam }] }) 50 | ``` 51 | 52 | ### Subscriptions 53 | 54 | ```prisma 55 | // Apply to all subscriptions (onCreated, onUpdated, ...) 56 | @auth(subscriptions: [{ allow: iam }]) 57 | 58 | // Apply to granular subscriptions 59 | @auth(subscriptions: { onCreated: [{ allow: iam }] }) 60 | ``` 61 | 62 | ### Fields 63 | 64 | ```prisma 65 | // Apply to specific Type fields 66 | @auth(fields: { password: [{ allow: apiKey }] }) 67 | ``` 68 | 69 | ## 👉 Supported Authorization modes 70 | 71 | 72 | 73 | ```prisma 74 | // API_KEY Authorization 75 | @auth(model: [{ allow: apiKey }]) 76 | 77 | // AWS_IAM 78 | @auth(model: [{ allow: iam }]) 79 | 80 | // OPENID_CONNECT 81 | @auth(model: [{ allow: oidc }]) 82 | 83 | // AWS_LAMBDA 84 | @auth(model: [{ allow: lambda }]) 85 | 86 | // AMAZON_COGNITO_USER_POOLS 87 | @auth(model: [{ allow: userPools }]) 88 | 89 | // AMAZON_COGNITO_USER_POOLS with groups 90 | @auth(model: [{ allow: userPools, groups: ["users", "admins"] }]) 91 | 92 | // Allow multiples 93 | @auth(model: [{ allow: apiKey }, { allow: userPools, groups: ["admins"] }]) 94 | ``` 95 | 96 | ## 👉 Default directive 97 | 98 | It is also possible to set a `defaultDirective`, that will apply to all generated Types: 99 | 100 | ```prisma{3} 101 | generator appsync { 102 | provider = "prisma-appsync" 103 | defaultDirective = "@auth(model: [{ allow: iam }])" 104 | } 105 | ``` 106 | 107 | When provided, `defaultDirective` seamlessly integrates with model-specific directives: 108 | 109 | ```prisma 110 | // specified 'defaultDirective' for all models: 111 | @auth(model: [{ allow: iam }]) 112 | 113 | // additional 'model directive' for enhanced control: 114 | @auth(model: [{ allow: apiKey }]) 115 | 116 | // resulting merged directive for the model: 117 | @auth(model: [{ allow: iam }, { allow: apiKey }]) 118 | ``` 119 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "prisma-appsync", 3 | "projectOwner": "maoosi", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "types": { 12 | "creator": { 13 | "symbol": "🐙", 14 | "description": "Creator & maintainer" 15 | } 16 | }, 17 | "contributors": [ 18 | { 19 | "login": "maoosi", 20 | "name": "Sylvain", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/4679377?v=4", 22 | "profile": "https://sylvainsimao.fr", 23 | "contributions": [ 24 | "creator", 25 | "code", 26 | "ideas", 27 | "doc" 28 | ] 29 | }, 30 | { 31 | "login": "Tenrys", 32 | "name": "Bell", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/3979239?v=4", 34 | "profile": "http://bell.moe", 35 | "contributions": [ 36 | "code", 37 | "ideas" 38 | ] 39 | }, 40 | { 41 | "login": "cipriancaba", 42 | "name": "Ciprian Caba", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/695515?v=4", 44 | "profile": "http://www.cipriancaba.com", 45 | "contributions": [ 46 | "code", 47 | "ideas" 48 | ] 49 | }, 50 | { 51 | "login": "tomschut", 52 | "name": "Tom", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/4933446?v=4", 54 | "profile": "https://github.com/tomschut", 55 | "contributions": [ 56 | "code", 57 | "ideas" 58 | ] 59 | }, 60 | { 61 | "login": "ryparker", 62 | "name": "Ryan Parker", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/17558268?v=4", 64 | "profile": "http://ryanparker.dev", 65 | "contributions": [ 66 | "code" 67 | ] 68 | }, 69 | { 70 | "login": "cjjenkinson", 71 | "name": "Cameron Jenkinson", 72 | "avatar_url": "https://avatars.githubusercontent.com/u/5429478?v=4", 73 | "profile": "https://www.cameronjjenkinson.com", 74 | "contributions": [ 75 | "code" 76 | ] 77 | }, 78 | { 79 | "login": "jeremy-white", 80 | "name": "jeremy-white", 81 | "avatar_url": "https://avatars.githubusercontent.com/u/42325631?v=4", 82 | "profile": "https://github.com/jeremy-white", 83 | "contributions": [ 84 | "code" 85 | ] 86 | }, 87 | { 88 | "login": "max-konin", 89 | "name": "Max Konin", 90 | "avatar_url": "https://avatars.githubusercontent.com/u/1570356?v=4", 91 | "profile": "https://github.com/max-konin", 92 | "contributions": [ 93 | "code" 94 | ] 95 | }, 96 | { 97 | "login": "michachan", 98 | "name": "Michael Chan", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/27760344?v=4", 100 | "profile": "https://github.com/michachan", 101 | "contributions": [ 102 | "code" 103 | ] 104 | }, 105 | { 106 | "login": "nhu-mai-101", 107 | "name": "Nhu Mai", 108 | "avatar_url": "https://avatars.githubusercontent.com/u/84061316?v=4", 109 | "profile": "https://www.linkedin.com/in/nhu-mai/", 110 | "contributions": [ 111 | "code" 112 | ] 113 | } 114 | ], 115 | "commitConvention": "angular", 116 | "contributorsPerLine": 7, 117 | "commitType": "docs" 118 | } 119 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/resolver.d.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaArgs, PrismaClient, PrismaOperator, QueryBuilder, QueryParams } from './types'; 2 | /** 3 | * #### Query Builder 4 | */ 5 | export declare function prismaQueryJoin(queries: PrismaArgs[], operators: PrismaOperator[]): T; 6 | export declare const queryBuilder: QueryBuilder; 7 | /** 8 | * #### Query :: Find Unique 9 | * 10 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findunique 11 | * @param {PrismaClient} prismaClient 12 | * @param {QueryParams} query 13 | */ 14 | export declare function getQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 15 | /** 16 | * #### Query :: Find Many 17 | * 18 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findmany 19 | * @param {PrismaClient} prismaClient 20 | * @param {QueryParams} query 21 | */ 22 | export declare function listQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 23 | /** 24 | * #### Query :: Count 25 | * 26 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#count 27 | * @param {PrismaClient} prismaClient 28 | * @param {QueryParams} query 29 | */ 30 | export declare function countQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 31 | /** 32 | * #### Mutation :: Create 33 | * 34 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#create 35 | * @param {PrismaClient} prismaClient 36 | * @param {QueryParams} query 37 | */ 38 | export declare function createQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 39 | /** 40 | * #### Mutation :: Create Many 41 | * 42 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany 43 | * @param {PrismaClient} prismaClient 44 | * @param {QueryParams} query 45 | */ 46 | export declare function createManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 47 | /** 48 | * #### Mutation :: Update 49 | * 50 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#update 51 | * @param {PrismaClient} prismaClient 52 | * @param {QueryParams} query 53 | */ 54 | export declare function updateQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 55 | /** 56 | * #### Mutation :: Update Many 57 | * 58 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#updatemany 59 | * @param {PrismaClient} prismaClient 60 | * @param {QueryParams} query 61 | */ 62 | export declare function updateManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 63 | /** 64 | * #### Mutation :: Upsert 65 | * 66 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#upsert 67 | * @param {PrismaClient} prismaClient 68 | * @param {QueryParams} query 69 | */ 70 | export declare function upsertQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 71 | /** 72 | * #### Mutation :: Delete 73 | * 74 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#delete 75 | * @param {PrismaClient} prismaClient 76 | * @param {QueryParams} query 77 | */ 78 | export declare function deleteQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 79 | /** 80 | * #### Mutation :: Delete Many 81 | * 82 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#deletemany 83 | * @param {PrismaClient} prismaClient 84 | * @param {QueryParams} query 85 | */ 86 | export declare function deleteManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 87 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/resolver.d.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaArgs, PrismaClient, PrismaOperator, QueryBuilder, QueryParams } from './types'; 2 | /** 3 | * #### Query Builder 4 | */ 5 | export declare function prismaQueryJoin(queries: PrismaArgs[], operators: PrismaOperator[]): T; 6 | export declare const queryBuilder: QueryBuilder; 7 | /** 8 | * #### Query :: Find Unique 9 | * 10 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findunique 11 | * @param {PrismaClient} prismaClient 12 | * @param {QueryParams} query 13 | */ 14 | export declare function getQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 15 | /** 16 | * #### Query :: Find Many 17 | * 18 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findmany 19 | * @param {PrismaClient} prismaClient 20 | * @param {QueryParams} query 21 | */ 22 | export declare function listQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 23 | /** 24 | * #### Query :: Count 25 | * 26 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#count 27 | * @param {PrismaClient} prismaClient 28 | * @param {QueryParams} query 29 | */ 30 | export declare function countQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 31 | /** 32 | * #### Mutation :: Create 33 | * 34 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#create 35 | * @param {PrismaClient} prismaClient 36 | * @param {QueryParams} query 37 | */ 38 | export declare function createQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 39 | /** 40 | * #### Mutation :: Create Many 41 | * 42 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany 43 | * @param {PrismaClient} prismaClient 44 | * @param {QueryParams} query 45 | */ 46 | export declare function createManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 47 | /** 48 | * #### Mutation :: Update 49 | * 50 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#update 51 | * @param {PrismaClient} prismaClient 52 | * @param {QueryParams} query 53 | */ 54 | export declare function updateQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 55 | /** 56 | * #### Mutation :: Update Many 57 | * 58 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#updatemany 59 | * @param {PrismaClient} prismaClient 60 | * @param {QueryParams} query 61 | */ 62 | export declare function updateManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 63 | /** 64 | * #### Mutation :: Upsert 65 | * 66 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#upsert 67 | * @param {PrismaClient} prismaClient 68 | * @param {QueryParams} query 69 | */ 70 | export declare function upsertQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 71 | /** 72 | * #### Mutation :: Delete 73 | * 74 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#delete 75 | * @param {PrismaClient} prismaClient 76 | * @param {QueryParams} query 77 | */ 78 | export declare function deleteQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 79 | /** 80 | * #### Mutation :: Delete Many 81 | * 82 | * https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#deletemany 83 | * @param {PrismaClient} prismaClient 84 | * @param {QueryParams} query 85 | */ 86 | export declare function deleteManyQuery(prismaClient: PrismaClient, query: QueryParams): Promise; 87 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/consts.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum Actions { 2 | get = "get", 3 | list = "list", 4 | count = "count", 5 | createMany = "createMany", 6 | updateMany = "updateMany", 7 | deleteMany = "deleteMany", 8 | create = "create", 9 | update = "update", 10 | upsert = "upsert", 11 | delete = "delete", 12 | onCreatedMany = "onCreatedMany", 13 | onUpdatedMany = "onUpdatedMany", 14 | onDeletedMany = "onDeletedMany", 15 | onMutatedMany = "onMutatedMany", 16 | onCreated = "onCreated", 17 | onUpdated = "onUpdated", 18 | onUpserted = "onUpserted", 19 | onDeleted = "onDeleted", 20 | onMutated = "onMutated" 21 | } 22 | export declare enum ActionsAliases { 23 | access = "access", 24 | batchAccess = "batchAccess", 25 | create = "create", 26 | batchCreate = "batchCreate", 27 | delete = "delete", 28 | batchDelete = "batchDelete", 29 | modify = "modify", 30 | batchModify = "batchModify", 31 | subscribe = "subscribe", 32 | batchSubscribe = "batchSubscribe" 33 | } 34 | /** 35 | * ### Authorizations 36 | * 37 | * - `API_KEY`: Via hard-coded API key passed into `x-api-key` header. 38 | * - `AWS_IAM`: Via IAM identity and associated IAM policy rules. 39 | * - `AMAZON_COGNITO_USER_POOLS`: Via Amazon Cognito user token. 40 | * - `AWS_LAMBDA`: Via an AWS Lambda function. 41 | * - `OPENID_CONNECT`: Via Open ID connect such as Auth0. 42 | * 43 | * https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html 44 | */ 45 | export declare enum Authorizations { 46 | API_KEY = "API_KEY", 47 | AWS_IAM = "AWS_IAM", 48 | AMAZON_COGNITO_USER_POOLS = "AMAZON_COGNITO_USER_POOLS", 49 | AWS_LAMBDA = "AWS_LAMBDA", 50 | OPENID_CONNECT = "OPENID_CONNECT" 51 | } 52 | export declare const Prisma_QueryOptions: string[]; 53 | export declare const Prisma_NestedQueries: string[]; 54 | export declare const Prisma_FilterConditionsAndOperatos: string[]; 55 | export declare const Prisma_FilterRelationFilters: string[]; 56 | export declare const Prisma_ScalarListMethods: string[]; 57 | export declare const Prisma_ScalarListFilters: string[]; 58 | export declare const Prisma_CompositeTypeMethods: string[]; 59 | export declare const Prisma_CompositeTypeFilters: string[]; 60 | export declare const Prisma_AtomicNumberOperations: string[]; 61 | export declare const Prisma_JSONFilters: string[]; 62 | export declare const Prisma_ReservedKeysForPaths: string[]; 63 | export declare const Prisma_ReservedKeys: string[]; 64 | export declare const ActionsAliasesList: { 65 | readonly access: readonly [Actions.get, Actions.list, Actions.count]; 66 | readonly batchAccess: readonly [Actions.list, Actions.count]; 67 | readonly create: readonly [Actions.create, Actions.createMany]; 68 | readonly batchCreate: readonly [Actions.createMany]; 69 | readonly modify: readonly [Actions.upsert, Actions.update, Actions.updateMany, Actions.delete, Actions.deleteMany]; 70 | readonly batchModify: readonly [Actions.updateMany, Actions.deleteMany]; 71 | readonly delete: readonly [Actions.delete, Actions.deleteMany]; 72 | readonly batchDelete: readonly [Actions.deleteMany]; 73 | readonly subscribe: readonly [Actions.onCreatedMany, Actions.onUpdatedMany, Actions.onDeletedMany, Actions.onMutatedMany, Actions.onCreated, Actions.onUpdated, Actions.onUpserted, Actions.onDeleted, Actions.onMutated]; 74 | readonly batchSubscribe: readonly [Actions.onCreatedMany, Actions.onUpdatedMany, Actions.onDeletedMany, Actions.onMutatedMany]; 75 | }; 76 | export declare const ActionsList: string[]; 77 | export declare const BatchActionsList: string[]; 78 | export declare const DebugTestingKey = "__prismaAppsync"; 79 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/consts.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum Actions { 2 | get = "get", 3 | list = "list", 4 | count = "count", 5 | createMany = "createMany", 6 | updateMany = "updateMany", 7 | deleteMany = "deleteMany", 8 | create = "create", 9 | update = "update", 10 | upsert = "upsert", 11 | delete = "delete", 12 | onCreatedMany = "onCreatedMany", 13 | onUpdatedMany = "onUpdatedMany", 14 | onDeletedMany = "onDeletedMany", 15 | onMutatedMany = "onMutatedMany", 16 | onCreated = "onCreated", 17 | onUpdated = "onUpdated", 18 | onUpserted = "onUpserted", 19 | onDeleted = "onDeleted", 20 | onMutated = "onMutated" 21 | } 22 | export declare enum ActionsAliases { 23 | access = "access", 24 | batchAccess = "batchAccess", 25 | create = "create", 26 | batchCreate = "batchCreate", 27 | delete = "delete", 28 | batchDelete = "batchDelete", 29 | modify = "modify", 30 | batchModify = "batchModify", 31 | subscribe = "subscribe", 32 | batchSubscribe = "batchSubscribe" 33 | } 34 | /** 35 | * ### Authorizations 36 | * 37 | * - `API_KEY`: Via hard-coded API key passed into `x-api-key` header. 38 | * - `AWS_IAM`: Via IAM identity and associated IAM policy rules. 39 | * - `AMAZON_COGNITO_USER_POOLS`: Via Amazon Cognito user token. 40 | * - `AWS_LAMBDA`: Via an AWS Lambda function. 41 | * - `OPENID_CONNECT`: Via Open ID connect such as Auth0. 42 | * 43 | * https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html 44 | */ 45 | export declare enum Authorizations { 46 | API_KEY = "API_KEY", 47 | AWS_IAM = "AWS_IAM", 48 | AMAZON_COGNITO_USER_POOLS = "AMAZON_COGNITO_USER_POOLS", 49 | AWS_LAMBDA = "AWS_LAMBDA", 50 | OPENID_CONNECT = "OPENID_CONNECT" 51 | } 52 | export declare const Prisma_QueryOptions: string[]; 53 | export declare const Prisma_NestedQueries: string[]; 54 | export declare const Prisma_FilterConditionsAndOperatos: string[]; 55 | export declare const Prisma_FilterRelationFilters: string[]; 56 | export declare const Prisma_ScalarListMethods: string[]; 57 | export declare const Prisma_ScalarListFilters: string[]; 58 | export declare const Prisma_CompositeTypeMethods: string[]; 59 | export declare const Prisma_CompositeTypeFilters: string[]; 60 | export declare const Prisma_AtomicNumberOperations: string[]; 61 | export declare const Prisma_JSONFilters: string[]; 62 | export declare const Prisma_ReservedKeysForPaths: string[]; 63 | export declare const Prisma_ReservedKeys: string[]; 64 | export declare const ActionsAliasesList: { 65 | readonly access: readonly [Actions.get, Actions.list, Actions.count]; 66 | readonly batchAccess: readonly [Actions.list, Actions.count]; 67 | readonly create: readonly [Actions.create, Actions.createMany]; 68 | readonly batchCreate: readonly [Actions.createMany]; 69 | readonly modify: readonly [Actions.upsert, Actions.update, Actions.updateMany, Actions.delete, Actions.deleteMany]; 70 | readonly batchModify: readonly [Actions.updateMany, Actions.deleteMany]; 71 | readonly delete: readonly [Actions.delete, Actions.deleteMany]; 72 | readonly batchDelete: readonly [Actions.deleteMany]; 73 | readonly subscribe: readonly [Actions.onCreatedMany, Actions.onUpdatedMany, Actions.onDeletedMany, Actions.onMutatedMany, Actions.onCreated, Actions.onUpdated, Actions.onUpserted, Actions.onDeleted, Actions.onMutated]; 74 | readonly batchSubscribe: readonly [Actions.onCreatedMany, Actions.onUpdatedMany, Actions.onDeletedMany, Actions.onMutatedMany]; 75 | }; 76 | export declare const ActionsList: string[]; 77 | export declare const BatchActionsList: string[]; 78 | export declare const DebugTestingKey = "__prismaAppsync"; 79 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/core.d.ts: -------------------------------------------------------------------------------- 1 | import type { Options, PrismaAppSyncOptionsType, ResolveParams } from './types'; 2 | import { Prisma, PrismaClient } from './types'; 3 | /** 4 | * ## Prisma-AppSync Client ʲˢ 5 | * 6 | * Type-safe Prisma AppSync client for TypeScript & Node.js 7 | * @example 8 | * ``` 9 | * const prismaAppSync = new PrismaAppSync() 10 | * 11 | * // lambda handler (AppSync Direct Lambda Resolver) 12 | * export const resolver = async (event: any, context: any) => { 13 | * return await prismaAppSync.resolve({ event }) 14 | * } 15 | * ``` 16 | * 17 | * 18 | * Read more in our [docs](https://prisma-appsync.vercel.app). 19 | */ 20 | export declare class PrismaAppSync { 21 | options: Options; 22 | prismaClient: PrismaClient; 23 | /** 24 | * ### Client Constructor 25 | * 26 | * Instantiate Prisma-AppSync Client. 27 | * @example 28 | * ``` 29 | * const prismaAppSync = new PrismaAppSync() 30 | * ``` 31 | * 32 | * @param {PrismaAppSyncOptionsType} options 33 | * @param {string} options.connectionString? - Prisma connection string (database connection URL). 34 | * @param {boolean} options.sanitize? - Enable sanitize inputs (parse xss + encode html). 35 | * @param {'INFO' | 'WARN' | 'ERROR'} options.logLevel? - Server logs level (visible in CloudWatch). 36 | * @param {number|false} options.defaultPagination? - Default pagination for list Query (items per page). 37 | * @param {number} options.maxDepth? - Maximum allowed GraphQL query depth. 38 | * @param {number} options.maxReqPerUserMinute? - Maximum allowed requests per user, per minute. 39 | * 40 | * @default 41 | * ``` 42 | * { 43 | * connectionString: process.env.DATABASE_URL, 44 | * sanitize: true, 45 | * logLevel: 'INFO', 46 | * defaultPagination: 50, 47 | * maxDepth: 4, 48 | * maxReqPerUserMinute: 200 49 | * } 50 | * ``` 51 | * 52 | * 53 | * Read more in our [docs](https://prisma-appsync.vercel.app). 54 | */ 55 | constructor(options?: PrismaAppSyncOptionsType); 56 | /** 57 | * ### Resolver 58 | * 59 | * Resolve the API request, based on the AppSync `event` received by the Direct Lambda Resolver. 60 | * @example 61 | * ``` 62 | * await prismaAppSync.resolve({ event }) 63 | * 64 | * // custom resolvers 65 | * await prismaAppSync.resolve<'notify'|'listPosts'>( 66 | * event, 67 | * resolvers: { 68 | * // extend CRUD API with a custom `notify` query 69 | * notify: async ({ args }) => { return { message: args.message } }, 70 | * 71 | * // disable one of the generated CRUD API query 72 | * listPosts: false, 73 | * } 74 | * }) 75 | * ``` 76 | * 77 | * @param {ResolveParams} resolveParams 78 | * @param {any} resolveParams.event - AppSync event received by the Direct Lambda Resolver. 79 | * @param {any} resolveParams.resolvers? - Custom resolvers (to extend the CRUD API). 80 | * @param {function} resolveParams.shield? - Shield configuration (to protect your API). 81 | * @param {function} resolveParams.hooks? - Hooks (to trigger functions based on events). 82 | * @returns Promise 83 | * 84 | * 85 | * Read more in our [docs](https://prisma-appsync.vercel.app). 86 | */ 87 | resolve(resolveParams: ResolveParams<"countPosts" | "countUsers" | "createManyPosts" | "createManyUsers" | "createPost" | "createUser" | "deleteManyUsers" | "deleteUser" | "getPost" | "getUser" | "listPosts" | "listUsers" | "updateManyPosts" | "updateManyUsers" | "updatePost" | "updateUser" | "upsertPost" | "upsertUser", Extract>): Promise; 88 | } 89 | -------------------------------------------------------------------------------- /docs/tools/appsync-gql-schema-diff.md: -------------------------------------------------------------------------------- 1 | --- 2 | aside: false 3 | --- 4 | 5 | 56 | 57 | # AppSync GraphQL Schema Diff 58 | 59 | Compare changes between AppSync GraphQL Schemas. 60 | 61 |
62 |
63 | 67 | 71 |
72 |
73 | 74 |
    75 |
  1. 76 | {{ log.level }}: {{ log.message }} 77 |
  2. 78 |
79 | No differences. 80 |
81 |
82 | 83 | 133 | -------------------------------------------------------------------------------- /docs/features/hooks.md: -------------------------------------------------------------------------------- 1 | # Lifecycle hooks 2 | 3 | Hooks let you “hook into” Prisma-AppSync lifecycle to either trigger custom business logic or manipulate data at runtime. 4 | 5 | ## 👉 Example code 6 | 7 | Basic example: 8 | 9 | ```ts 10 | return await prismaAppSync.resolve({ 11 | event, 12 | hooks: { 13 | // Mutate Post title before creation on database 14 | 'before:createPost': async (params: BeforeHookParams) => { 15 | params.prismaArgs.data.title = 'New post title' 16 | return params 17 | }, 18 | // Override query result using always the same Post title 19 | 'after:listPosts': async (params: AfterHookParams) => { 20 | params.result = params.result.map(r => r.title = 'Always the same title') 21 | return params 22 | }, 23 | }, 24 | }) 25 | ``` 26 | 27 | Advanced example: 28 | 29 | ```ts 30 | return await prismaAppSync.resolve<'likePost'>({ 31 | event, 32 | hooks: { 33 | // execute before any query 34 | 'before:**': async (params: BeforeHookParams) => params, 35 | 36 | // execute after any query 37 | 'after:**': async (params: AfterHookParams) => params, 38 | 39 | // execute after custom resolver query `likePost` 40 | // (e.g. `query { likePost(postId: 3) }`) 41 | 'after:likePost': async (params: AfterHookParams) => { 42 | await params.prismaClient.notification.create({ 43 | data: { 44 | event: 'POST_LIKED', 45 | targetId: params.args.postId, 46 | userId: params.authIdentity.sub, 47 | }, 48 | }) 49 | return params 50 | }, 51 | }, 52 | }) 53 | ``` 54 | 55 | ## 👉 Types 56 | 57 | ```ts 58 | export interface QueryParams { 59 | type: GraphQLType 60 | operation: string 61 | context: Context 62 | fields: string[] 63 | paths: string[] 64 | args: any 65 | prismaArgs: PrismaArgs 66 | authorization: Authorization 67 | identity: Identity 68 | headers: any 69 | prismaClient: PrismaClient 70 | } 71 | 72 | type BeforeHookParams = QueryParams 73 | 74 | type AfterHookParams = QueryParams & { 75 | result: any | any[] 76 | } 77 | ``` 78 | 79 | ## 👉 Usage rules 80 | 81 | - Hooks are made of a **Path** (e.g. `after:updatePost`) and an async function. 82 | - **Path** syntax always starts with `before:` or `after:`. 83 | 84 | > `before` or `after` querying data from the database. 85 | 86 | - **Path** syntax after `:` uses [Micromatch syntax](https://github.com/micromatch/micromatch). 87 | - Hooks are fully typed, so VSCode IntelliSense will give you the full list of Hooks Paths you can use while typing. Example: 88 | 89 | ![Prisma-AppSync hooks on VS Code](/guides/hooks-autocompletion.png) 90 | 91 | - Hooks functions all receive a single object as a parameter. Here is an example object received inside `after:getPost`: 92 | 93 | ```json 94 | { 95 | "type": "Query", 96 | "operation": "getPost", 97 | "context": { "action": "get", "alias": "access", "model": "Post" }, 98 | "fields": ["title", "status"], 99 | "paths": ["get/post/title", "get/post/status"], 100 | "args": { "where": { "id": 5 } }, 101 | "prismaArgs": { 102 | "where": { "id": 5 }, 103 | "select": { "title": true, "status": true } 104 | }, 105 | "authorization": "API_KEY", 106 | "identity": {}, 107 | "result": { "title": "My first post", "status": "PUBLISHED" } 108 | } 109 | ``` 110 | 111 | - Key `result` is only available inside `after` hooks. 112 | - Hooks async functions MUST return the object received as a parameter (either mutated or untouched). 113 | - Using hooks on custom resolvers requires explicitly listing resolvers using a TypeScript Generic `prismaAppSync.resolve`: 114 | 115 | ```ts 116 | // Using custom resolver `likePost` 117 | return await prismaAppSync.resolve<'likePost'>({ event, hooks }) 118 | 119 | // Using multiple custom resolvers 120 | return await prismaAppSync.resolve<'likePost' | 'unlikePost'>({ event, hooks }) 121 | ``` 122 | -------------------------------------------------------------------------------- /packages/client/src/inspector.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/prefer-global/process */ 2 | /* eslint-disable no-console */ 3 | import { inspect as nodeInspect } from 'node:util' 4 | import type { logLevel } from './types' 5 | 6 | const errorCodes = { 7 | FORBIDDEN: 401, 8 | BAD_USER_INPUT: 400, 9 | INTERNAL_SERVER_ERROR: 500, 10 | TOO_MANY_REQUESTS: 429, 11 | } 12 | 13 | export type ErrorExtensions = { 14 | type: keyof typeof errorCodes 15 | cause?: any 16 | } 17 | 18 | export type ErrorDetails = { 19 | error: string 20 | type: ErrorExtensions['type'] 21 | code: number 22 | cause?: ErrorExtensions['cause'] 23 | } 24 | 25 | export class CustomError extends Error { 26 | public error: ErrorDetails['error'] 27 | public type: ErrorDetails['type'] 28 | public code: ErrorDetails['code'] 29 | public cause: ErrorDetails['cause'] 30 | public details: ErrorDetails 31 | 32 | constructor(message: string, extensions: ErrorExtensions) { 33 | super(message) 34 | 35 | this.error = message 36 | this.type = extensions.type 37 | this.cause = extensions?.cause?.meta?.cause || extensions?.cause 38 | this.code = typeof errorCodes[this.type] !== 'undefined' ? errorCodes[this.type] : errorCodes.INTERNAL_SERVER_ERROR 39 | 40 | this.message = JSON.stringify({ 41 | error: this.error, 42 | type: this.type, 43 | code: this.code, 44 | }) 45 | 46 | const maxCauseMessageLength = 500 47 | 48 | if (this.cause?.message?.length > maxCauseMessageLength) 49 | this.cause.message = `... ${this.cause.message.slice(this.cause.message.length - maxCauseMessageLength)}` 50 | 51 | this.details = { 52 | error: this.error, 53 | type: this.type, 54 | code: this.code, 55 | ...(this.cause && { cause: this.cause }), 56 | } 57 | 58 | if (!(process?.env?.PRISMA_APPSYNC_TESTING === 'true')) 59 | log(message, this.details, 'ERROR') 60 | } 61 | } 62 | 63 | export function parseError(error: Error): CustomError { 64 | if (error instanceof CustomError) { 65 | return error 66 | } 67 | else { 68 | return new CustomError(error.message, { 69 | type: 'INTERNAL_SERVER_ERROR', 70 | cause: error, 71 | }) 72 | } 73 | } 74 | 75 | export function log(message: string, obj?: any, level?: logLevel): void { 76 | if (canPrintLog(level || 'INFO')) { 77 | printLog(message, level || 'INFO') 78 | 79 | if (obj) { 80 | console.log( 81 | nodeInspect(obj, { 82 | compact: false, 83 | depth: 5, 84 | breakLength: 80, 85 | maxStringLength: 800, 86 | ...(!process.env.LAMBDA_TASK_ROOT && { 87 | colors: true, 88 | }), 89 | }), 90 | ) 91 | } 92 | } 93 | } 94 | 95 | export function printLog(message: any, level: logLevel): void { 96 | const timestamp = new Date().toLocaleString(undefined, { 97 | day: 'numeric', 98 | month: 'numeric', 99 | year: 'numeric', 100 | hour: '2-digit', 101 | minute: '2-digit', 102 | second: '2-digit', 103 | }) 104 | const prefix = `◭ ${timestamp} <<${level}>>` 105 | const log = [prefix, message].join(' ') 106 | 107 | if (level === 'ERROR' && canPrintLog(level)) 108 | console.error(`\x1B[31m${log}`) 109 | else if (level === 'WARN' && canPrintLog(level)) 110 | console.warn(`\x1B[33m${log}`) 111 | else if (level === 'INFO' && canPrintLog(level)) 112 | console.info(`\x1B[36m${log}`) 113 | } 114 | 115 | function canPrintLog(level: logLevel): boolean { 116 | if (process?.env?.PRISMA_APPSYNC_TESTING === 'true') 117 | return false 118 | 119 | const logLevel = String(process.env.PRISMA_APPSYNC_LOG_LEVEL) as logLevel 120 | 121 | return (logLevel === 'ERROR' && level === 'ERROR') 122 | || (logLevel === 'WARN' && ['WARN', 'ERROR'].includes(level)) 123 | || (logLevel === 'INFO') 124 | } 125 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Prisma-AppSync', 3 | description: 'GraphQL API Generator for AWS and ◭ Prisma', 4 | 5 | head: [['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }]], 6 | 7 | vue: { 8 | reactivityTransform: true, 9 | }, 10 | 11 | lastUpdated: true, 12 | 13 | themeConfig: { 14 | logo: '/logo.svg', 15 | 16 | editLink: { 17 | text: 'Suggest changes to this page', 18 | pattern: 'https://github.com/maoosi/prisma-appsync/edit/main/docs/:path', 19 | }, 20 | 21 | socialLinks: [{ icon: 'github', link: 'https://github.com/maoosi/prisma-appsync' }], 22 | 23 | lastUpdatedText: 'Updated Date', 24 | 25 | footer: { 26 | message: 'Released under the BSD 2-Clause License.', 27 | copyright: 'Copyright © 2021-present Sylvain Simao', 28 | }, 29 | 30 | nav: [ 31 | { text: 'Documentation', link: '/quick-start/getting-started' }, 32 | { text: 'Changelog', link: '/changelog/1.0.0' }, 33 | { text: 'Support', link: '/support' }, 34 | { 35 | text: 'Tools', 36 | items: [ 37 | { 38 | text: 'AppSync GraphQL Schema Diff', 39 | link: '/tools/appsync-gql-schema-diff', 40 | }, 41 | ], 42 | }, 43 | { 44 | text: 'Links', 45 | items: [ 46 | { 47 | text: 'Report a bug', 48 | link: 'https://github.com/maoosi/prisma-appsync/issues', 49 | }, 50 | { 51 | text: 'Sponsor', 52 | link: 'https://github.com/sponsors/maoosi', 53 | }, 54 | { 55 | text: 'Roadmap', 56 | link: 'https://github.com/users/maoosi/projects/1', 57 | }, 58 | ], 59 | }, 60 | ], 61 | 62 | sidebar: [ 63 | { 64 | text: 'Quick start', 65 | items: [ 66 | { text: 'Getting started', link: '/quick-start/getting-started' }, 67 | { text: 'Installation', link: '/quick-start/installation' }, 68 | { text: 'Usage', link: '/quick-start/usage' }, 69 | { text: 'Deploy', link: '/quick-start/deploy' }, 70 | ], 71 | }, 72 | { 73 | text: 'Features', 74 | collapsible: true, 75 | items: [ 76 | { text: 'Lifecycle hooks', link: '/features/hooks' }, 77 | { text: 'Custom resolvers', link: '/features/resolvers' }, 78 | { text: 'Tweaking GQL schema', link: '/features/gql-schema' }, 79 | ], 80 | }, 81 | { 82 | text: 'Security', 83 | collapsible: true, 84 | items: [ 85 | { text: 'Authorization', link: '/security/appsync-authz' }, 86 | { text: 'Shield (ACL rules)', link: '/security/shield-acl' }, 87 | { text: 'XSS sanitizer', link: '/security/xss-sanitizer' }, 88 | { text: 'Query depth', link: '/security/query-depth' }, 89 | { text: 'Rate limiter (DOS)', link: '/security/rate-limiter' }, 90 | ], 91 | }, 92 | { 93 | text: 'Contributing', 94 | collapsible: true, 95 | items: [ 96 | { text: 'Contributions guide', link: '/contributing' }, 97 | ], 98 | }, 99 | { 100 | text: 'Changelog', 101 | collapsible: true, 102 | collapsed: true, 103 | items: [ 104 | { text: '(latest) v1.0.0', link: '/changelog/1.0.0' }, 105 | { text: 'Previous', link: '/changelog/' }, 106 | ], 107 | }, 108 | ], 109 | }, 110 | } -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /bin/publish.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import Listr from 'listr' 5 | import prompts from 'prompts' 6 | 7 | await $`zx bin/env.mjs` 8 | 9 | $.verbose = false 10 | 11 | async function getPublishConfig() { 12 | const { tag } = await prompts({ 13 | type: 'select', 14 | name: 'tag', 15 | message: 'Select publish tag', 16 | choices: [ 17 | { title: 'preview', value: 'preview' }, 18 | { title: 'latest', value: 'latest' }, 19 | ], 20 | initial: 0, 21 | }) 22 | 23 | if (!tag) 24 | process.exit() 25 | 26 | let latestPublished = '0.0.9' 27 | 28 | try { 29 | latestPublished = String(await $`npm show prisma-appsync@${tag} version`)?.trim() 30 | } 31 | catch (err) { 32 | try { latestPublished = String(await $`npm show prisma-appsync version`)?.trim() } 33 | catch (err) {} 34 | } 35 | 36 | const minorPos = latestPublished.lastIndexOf('.') 37 | 38 | const possibleFutureVersion = `${latestPublished.slice(0, minorPos)}.${ 39 | parseInt(latestPublished.slice(minorPos + 1)) + 1 40 | }` 41 | 42 | const { publishVersion } = await prompts({ 43 | type: 'text', 44 | name: 'publishVersion', 45 | message: `Enter new version for @${tag}? (latest = "${latestPublished}")`, 46 | initial: possibleFutureVersion, 47 | }) 48 | 49 | if (!publishVersion || publishVersion === latestPublished) 50 | process.exit() 51 | 52 | const { versionOk } = await prompts({ 53 | type: 'confirm', 54 | name: 'versionOk', 55 | message: `Run "pnpm publish --tag ${tag} --no-git-checks" with pkg version "${publishVersion}"?`, 56 | initial: false, 57 | }) 58 | 59 | return { 60 | versionOk, 61 | publishVersion, 62 | tag, 63 | } 64 | } 65 | 66 | async function publishCore({ tag }) { 67 | console.log('Publishing Core...') 68 | 69 | await new Listr([ 70 | { 71 | title: 'Cleansing package.json', 72 | task: async () => { 73 | await $`node bin/publish/_pkg.core.cleanse` 74 | }, 75 | }, 76 | { 77 | title: `Publishing on NPM with tag ${tag}`, 78 | task: async () => await $`pnpm publish --tag ${tag} --no-git-checks`, 79 | }, 80 | { 81 | title: 'Restoring package.json', 82 | task: async () => await $`node bin/publish/_pkg.core.restore`, 83 | }, 84 | ]).run().catch((err) => { 85 | console.error(err) 86 | }) 87 | } 88 | 89 | async function publishInstaller({ tag }) { 90 | console.log('Publishing Installer...') 91 | 92 | await new Listr([ 93 | { 94 | title: 'Copy + Cleanse package.json', 95 | task: async () => await $`node bin/publish/_pkg.installer.cleanse`, 96 | }, 97 | { 98 | title: 'Publishing on NPM', 99 | task: async () => await $`cd ./dist/installer/ && pnpm publish --tag ${tag} --no-git-checks`, 100 | }, 101 | ]).run().catch((err) => { 102 | console.error(err) 103 | }) 104 | } 105 | 106 | const publishConfig = await getPublishConfig() 107 | 108 | if (publishConfig.versionOk) { 109 | const corePkgFile = './package.json' 110 | const installerPkgFile = './packages/installer/package.json' 111 | 112 | // change package.json versions 113 | console.log(`\nSetting publish version to ${publishConfig.publishVersion}...`) 114 | const corePkg = await fs.readJson(corePkgFile) 115 | const installerPkg = await fs.readJson(installerPkgFile) 116 | corePkg.version = publishConfig.publishVersion 117 | installerPkg.version = publishConfig.publishVersion 118 | await fs.writeJson(corePkgFile, corePkg, { spaces: 4 }) 119 | await fs.writeJson(installerPkgFile, installerPkg, { spaces: 4 }) 120 | 121 | // preview? 122 | if (publishConfig.tag === 'preview') 123 | process.env.COMPILE_MODE = "preview" 124 | 125 | // build + test 126 | console.log('Building + Testing...') 127 | await $`zx bin/test.mjs` 128 | 129 | // publish packages 130 | await publishCore(publishConfig) 131 | await publishInstaller(publishConfig) 132 | console.log('Done!') 133 | } 134 | -------------------------------------------------------------------------------- /tests/generator/@gql.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-node-protocol */ 2 | import { readFileSync } from 'fs' 3 | import { join } from 'path' 4 | import { describe, expect, test } from 'vitest' 5 | import EasyGraphQLTester from 'easygraphql-tester' 6 | import { makeExecutableSchema } from '@graphql-tools/schema' 7 | 8 | const appsyncDirectives = readFileSync(join(__dirname, './mock/appsync-directives.gql'), 'utf8') 9 | const appsyncScalars = readFileSync(join(__dirname, './mock/appsync-scalars.gql'), 'utf8') 10 | const generatedSchema = readFileSync(join(__dirname, './schemas/generated/@gql/schema.gql'), 'utf8') 11 | const appsyncSchema = [appsyncDirectives, appsyncScalars, generatedSchema].join('\n\n').replace(/\"\"\"(.|\n)*?\"\"\"\n/gim, '') 12 | const gqlSchema = makeExecutableSchema({ typeDefs: appsyncSchema }) 13 | const tester = new EasyGraphQLTester(appsyncSchema) 14 | 15 | describe('GENERATOR @gql', () => { 16 | describe('disabling entire models', () => { 17 | test('expect Badge queries to be disabled', async () => { 18 | const invalidQuery = 'query { getBadge { level } }' 19 | tester.test(false, invalidQuery) 20 | }) 21 | test('expect Badge mutations to be disabled', async () => { 22 | const invalidQuery = ` 23 | mutation { 24 | createBadge( 25 | data: { level: 1, rank: 1 } 26 | ) { 27 | level 28 | rank 29 | } 30 | }` 31 | tester.test(false, invalidQuery) 32 | }) 33 | }) 34 | 35 | describe('disabling top-level queries, mutations, subscriptions', () => { 36 | test('expect User queries to be enabled', async () => { 37 | const query = 'query { listUsers { email } }' 38 | tester.test(true, query) 39 | }) 40 | test('expect User subscriptions to be disabled', async () => { 41 | const invalidQuery = `subscription { onMutatedUser { id } }` 42 | tester.test(false, invalidQuery) 43 | }) 44 | }) 45 | 46 | describe('disabling granular operations', () => { 47 | test('expect create Post to be enabled', async () => { 48 | const query = ` 49 | mutation { 50 | createPost( 51 | data: { title: "title" } 52 | ) { 53 | title 54 | } 55 | }` 56 | tester.test(true, query) 57 | }) 58 | test('expect delete Post to be disabled', async () => { 59 | const invalidQuery = ` 60 | mutation { 61 | deletePost( 62 | where: { id: 1 } 63 | ) { 64 | title 65 | } 66 | }` 67 | tester.test(false, invalidQuery) 68 | }) 69 | }) 70 | 71 | describe('hidding fields', () => { 72 | test('expect querying password field on User to fail', async () => { 73 | const invalidQuery = 'query { listUsers { password } }' 74 | tester.test(false, invalidQuery) 75 | }) 76 | test('expect querying other fields on User to succeed', async () => { 77 | const query = 'query { listUsers { email } }' 78 | tester.test(true, query) 79 | }) 80 | test('expect password field to still be writable', async () => { 81 | const query = `mutation { 82 | createUser ( 83 | data: { 84 | email: "user@email.com" 85 | password: "123456" 86 | } 87 | ) { 88 | email 89 | } 90 | }` 91 | tester.test(true, query) 92 | }) 93 | }) 94 | 95 | describe('custom scalars', () => { 96 | test('expect source field on Post to be "AWSURL"', async () => { 97 | const postType: any = gqlSchema.getType('Post') 98 | const sourceField = postType.getFields()?.source 99 | const sourceFieldScalar = sourceField?.type?.toString() 100 | expect(sourceFieldScalar).toEqual('AWSURL') 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /docs/changelog/1.0.0-rc.4.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | # 1.0.0-rc.4 6 | 7 | **🌟 Help us spread the word about Prisma-AppSync by starring the repo.** 8 | 9 | ## Highlights 10 | 11 | ### ⚡️ Local dev server is now using `vite-node` instead of `ts-node-dev` 12 | 13 | Due to some incompatibilities between `ts-node-dev` and some of the newest changes, Vite is now used as the underlying Node runtime for the Prisma-AppSync local dev server. 14 | 15 | To migrate an existing project using the local dev server, you'll need to edit the `dev` script inside your `package.json` and replace the following part: 16 | 17 | ```shell 18 | npx ts-node-dev --rs --transpile-only --watch './*.ts' -- ./server.ts 19 | ``` 20 | 21 | with: 22 | 23 | ```shell 24 | npx vite-node ./server.ts --watch -- 25 | ``` 26 | 27 | ### ⚡️ Local dev server upgraded to GraphQL Yoga v3, with the ability to use custom options 28 | 29 | When using Prisma-AppSync local dev server, it is now possible to pass custom options from the `server.ts` file. 30 | 31 | ```ts 32 | createServer({ 33 | yogaServerOptions: { 34 | cors: { 35 | origin: 'http://localhost:4000', 36 | credentials: true, 37 | allowedHeaders: ['X-Custom-Header'], 38 | methods: ['POST'] 39 | } 40 | /* ...other args */ 41 | } 42 | }) 43 | ``` 44 | 45 | For the full list of supported options, please refer to https://the-guild.dev/graphql/yoga-server/docs and the `createYoga` method. 46 | 47 | ## Fixes and improvements 48 | 49 | - [Auto-populated fields (autoincrement, uuid, updatedAt, …) are now visible and directly editable from the GraphQL schema (Issue #70)](https://github.com/maoosi/prisma-appsync/issues/70) 50 | - [Fixed issue with lists (arrays) in Prisma Schema not being properly cast into the GraphQL Schema (PR #78)](https://github.com/maoosi/prisma-appsync/pull/78) 51 | - [Added `cuid` as part of the auto-populated fields (PR #72)](https://github.com/maoosi/prisma-appsync/pull/72) 52 | - [Initialize `prismaArgs` with empty select (PR #69)](https://github.com/maoosi/prisma-appsync/pull/69) 53 | - [Added an optional generic type for QueryParams (PR #74)](https://github.com/maoosi/prisma-appsync/pull/74) 54 | - [Fixed issue with CDK boilerplate policy statements (Issue #64)](https://github.com/maoosi/prisma-appsync/issues/64) 55 | - [Fixed docs using the wrong syntax for fine-grained access control examples (Issue #79)](https://github.com/maoosi/prisma-appsync/issues/79) 56 | - CDK boilerplate Lambda function upgraded to `NODEJS_16_X` 57 | - CDK boilerplate `warmUp(boolean)` parameter becomes `useWarmUp(number)`, allowing to specify the number of warm-up functions to use (default `0`) 58 | 59 | ## Credits 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
Sylvain
Sylvain

🧙‍♂️ 💻 🤔 📖
Ciprian Caba
Ciprian Caba

💻 🤔
Cameron Jenkinson
Cameron Jenkinson

💻
Bell
Bell

💻
71 | 72 | ## Github sponsors 73 | 74 | Enjoy using Prisma-AppSync? Please consider [💛 Github sponsors](https://github.com/sponsors/maoosi). 75 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Visuals 3 | "peacock.color": "#1f08f6", 4 | "workbench.colorCustomizations": { 5 | "activityBar.activeBackground": "#4b38f9", 6 | "activityBar.activeBorder": "#f95745", 7 | "activityBar.background": "#4b38f9", 8 | "activityBar.foreground": "#e7e7e7", 9 | "activityBar.inactiveForeground": "#e7e7e799", 10 | "activityBarBadge.background": "#f95745", 11 | "activityBarBadge.foreground": "#15202b", 12 | "sash.hoverBorder": "#4b38f9", 13 | "statusBar.background": "#1f08f6", 14 | "statusBar.foreground": "#e7e7e7", 15 | "statusBarItem.hoverBackground": "#4b38f9", 16 | "statusBarItem.remoteBackground": "#1f08f6", 17 | "statusBarItem.remoteForeground": "#e7e7e7", 18 | "titleBar.activeBackground": "#1f08f6", 19 | "titleBar.activeForeground": "#e7e7e7", 20 | "titleBar.inactiveBackground": "#1f08f699", 21 | "titleBar.inactiveForeground": "#e7e7e799", 22 | "commandCenter.border": "#e7e7e799" 23 | }, 24 | 25 | // ESLint config 26 | "eslint.codeAction.showDocumentation": { 27 | "enable": true 28 | }, 29 | "eslint.probe": [ 30 | "javascript", 31 | "typescript", 32 | "javascriptreact", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "json5" 40 | ], 41 | "prettier.enable": false, 42 | 43 | // Editor 44 | "editor.formatOnSave": false, 45 | "editor.accessibilitySupport": "off", 46 | "editor.cursorSmoothCaretAnimation": "on", 47 | "editor.find.addExtraSpaceOnTop": false, 48 | "editor.guides.bracketPairs": "active", 49 | "editor.inlineSuggest.enabled": true, 50 | "editor.lineNumbers": "interval", 51 | "editor.multiCursorModifier": "ctrlCmd", 52 | "editor.renderWhitespace": "boundary", 53 | "editor.suggestSelection": "first", 54 | "editor.tabSize": 4, 55 | "editor.unicodeHighlight.invisibleCharacters": false, 56 | "editor.codeActionsOnSave": { 57 | "source.fixAll": "never", 58 | "source.fixAll.eslint": "explicit", 59 | "source.organizeImports": "never" 60 | }, 61 | "[markdown]": { 62 | "editor.formatOnSave": false 63 | }, 64 | "[prisma]": { 65 | "editor.defaultFormatter": "Prisma.prisma", 66 | "editor.formatOnSave": true 67 | }, 68 | "files.eol": "\n", 69 | "typescript.tsdk": "node_modules/typescript/lib", 70 | 71 | // Grammarly 72 | "grammarly.selectors": [ 73 | { 74 | "language": "markdown", 75 | "scheme": "file", 76 | "pattern": "docs/changelog/1.0.0-rc.1.md" 77 | }, 78 | { 79 | "language": "markdown", 80 | "scheme": "file", 81 | "pattern": "docs/contributing.md" 82 | }, 83 | { 84 | "language": "markdown", 85 | "scheme": "file", 86 | "pattern": "docs/essentials/concept.md" 87 | }, 88 | { 89 | "language": "markdown", 90 | "scheme": "file", 91 | "pattern": "docs/essentials/getting-started.md" 92 | }, 93 | { 94 | "language": "markdown", 95 | "scheme": "file", 96 | "pattern": "docs/advanced/securing-api.md" 97 | }, 98 | { 99 | "language": "markdown", 100 | "scheme": "file", 101 | "pattern": "docs/advanced/extending-api.md" 102 | }, 103 | { 104 | "language": "markdown", 105 | "scheme": "file", 106 | "pattern": "README.md" 107 | }, 108 | { 109 | "language": "markdown", 110 | "scheme": "file", 111 | "pattern": "docs/advanced/hooks.md" 112 | }, 113 | { 114 | "language": "markdown", 115 | "scheme": "file", 116 | "pattern": "docs/changelog/1.0.0-rc.4.md" 117 | }, 118 | { 119 | "language": "markdown", 120 | "scheme": "file", 121 | "pattern": "docs/changelog/1.0.0-rc.5.md" 122 | }, 123 | { 124 | "language": "markdown", 125 | "scheme": "file", 126 | "pattern": "docs/changelog/1.0.0-rc.6.md" 127 | } 128 | ] 129 | } 130 | -------------------------------------------------------------------------------- /docs/changelog/1.0.0-rc.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | # 1.0.0-rc.2 6 | 7 | **🌟 Help us spread the word about Prisma-AppSync by starring the repo.** 8 | 9 | ## Major improvements 10 | 11 | ### Support for Prisma Fluent API syntax on Relation Filters 12 | 13 | > 🚨 Breaking change affecting syntax for Relation Filters. 14 | 15 | In this release, we are changing how to write relation filters. We are replacing the [original Prisma Client syntax](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#filter-on--to-one-relations) (using `is` and `isNot` filters) with the newest [Fluent API syntax](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#fluent-api) which feels more natural to write, but also allows using more complex Relation Filters such as `contains`, `endsWith`, `equals`, `gt`, `gte`, `lt`, `lte`, `in`, `not`, `notIn` and `startsWith`. 16 | 17 | **Before** 18 | 19 | ```graphql 20 | query { 21 | listPosts( 22 | where: { 23 | author: { is: { username: "xxx" } } 24 | } 25 | ) { 26 | title 27 | author { username } 28 | } 29 | } 30 | ``` 31 | 32 | **After** 33 | 34 | ```graphql 35 | query { 36 | listPosts( 37 | where: { 38 | author: { username: { equals: "xxx" } } 39 | } 40 | ) { 41 | title 42 | author { username } 43 | } 44 | } 45 | ``` 46 | 47 | ### Improved readability for underlying Prisma client errors 48 | 49 | In this release, we have improved readability for all [known errors](https://www.prisma.io/docs/reference/api-reference/error-reference) thrown by the underlying Prisma Client. For example, using an incorrect connection URL (`DATABASE_URL`) will now return the below message as part of the API response: 50 | 51 | > Error with Prisma client initialization. https://www.prisma.io/docs/reference/api-reference/error-reference#prismaclientinitializationerror 52 | 53 | The full error trace will still appear inside the terminal and/or CloudWatch logs. 54 | 55 | ### New documentation guide: "Adding Hooks" 56 | 57 | In this release, we have added a new guide on "Adding Hooks". Particularly useful to trigger actions and/or manipulate data `before` or `after` queries. 58 | 59 | https://prisma-appsync.vercel.app/advanced/hooks.html 60 | 61 | **Example snippet:** 62 | 63 | ```tsx 64 | return await prismaAppSync.resolve<'likePost'>({ 65 | event, 66 | hooks: { 67 | // execute before any query 68 | 'before:**': async (params: BeforeHookParams) => params, 69 | 70 | // execute after any query 71 | 'after:**': async (params: AfterHookParams) => params, 72 | 73 | // execute after custom resolver query `likePost` 74 | // (e.g. `query { likePost(postId: 3) }`) 75 | 'after:likePost': async (params: AfterHookParams) => { 76 | await params.prismaClient.notification.create({ 77 | data: { 78 | event: 'POST_LIKED', 79 | targetId: params.args.postId, 80 | userId: params.authIdentity.sub, 81 | }, 82 | }) 83 | return params 84 | }, 85 | }, 86 | }) 87 | ``` 88 | 89 | ## Fixes 90 | 91 | - [Issue using the `@aws_auth` directive along with additional authorization modes.](https://github.com/maoosi/prisma-appsync/pull/52) 92 | - [Issue with `before` and `after` hook responses.](https://github.com/maoosi/prisma-appsync/pull/54) 93 | 94 | ## Credits 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
Sylvain
Sylvain

🧙‍♂️ 💻 🤔 📖
Ciprian Caba
Ciprian Caba

💻 🤔
104 | 105 | ## Github sponsors 106 | 107 | Enjoy using Prisma-AppSync? Please consider [💛 Github sponsors](https://github.com/sponsors/maoosi). 108 | -------------------------------------------------------------------------------- /packages/generator/src/generator.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import { copy, outputFile, readFile } from 'fs-extra' 3 | import type { DMMF } from '@prisma/generator-helper' 4 | import SchemaBuilder from './schema' 5 | import ResolversBuilder from './resolvers' 6 | import ClientConfigBuilder from './client' 7 | 8 | export default class PrismaAppSyncGenerator { 9 | private prismaDmmf: GeneratorOption['prismaDmmf'] 10 | private outputDir: GeneratorOption['outputDir'] 11 | private prismaSchemaPath: GeneratorOption['prismaSchemaPath'] 12 | private userGraphQLPath?: GeneratorOption['userGraphQLPath'] 13 | private userResolversPath?: GeneratorOption['userResolversPath'] 14 | private defaultDirective?: GeneratorOption['defaultDirective'] 15 | 16 | constructor(options: GeneratorOption) { 17 | this.outputDir = options.outputDir 18 | this.prismaDmmf = options.prismaDmmf 19 | this.prismaSchemaPath = options.prismaSchemaPath 20 | this.userGraphQLPath = options?.userGraphQLPath 21 | this.userResolversPath = options?.userResolversPath 22 | this.defaultDirective = options?.defaultDirective 23 | } 24 | 25 | public async makeAppSyncSchema() { 26 | let schema: string 27 | 28 | const builder = new SchemaBuilder() 29 | 30 | // create schema from prisma dmmf 31 | schema = await builder.createSchema(this.prismaDmmf, { defaultDirective: this.defaultDirective }) 32 | 33 | // extendSchema option 34 | if (this.userGraphQLPath) { 35 | // read user schema 36 | const userSchema = await readFile(join(dirname(this.prismaSchemaPath), this.userGraphQLPath), { 37 | encoding: 'utf-8', 38 | }) 39 | 40 | // merge with generated schema 41 | schema = await builder.mergeSchemas(schema, userSchema) 42 | } 43 | else { 44 | // merge with nothing 45 | schema = await builder.mergeSchemas(schema, '') 46 | } 47 | 48 | // output graphql schema 49 | await outputFile(join(this.outputDir, 'schema.gql'), schema, 'utf-8') 50 | } 51 | 52 | public async makeAppSyncResolvers() { 53 | let resolvers: string 54 | 55 | const builder = new ResolversBuilder() 56 | 57 | // create schema from prisma dmmf 58 | resolvers = await builder.createResolvers(this.prismaDmmf, { defaultDirective: this.defaultDirective }) 59 | 60 | // extendResolvers option 61 | if (this.userResolversPath) { 62 | // read user resolvers 63 | const userResolvers = await readFile(join(dirname(this.prismaSchemaPath), this.userResolversPath), { 64 | encoding: 'utf8', 65 | }) 66 | 67 | // merge with generated resolvers 68 | resolvers = await builder.mergeResolvers(resolvers, userResolvers) 69 | } 70 | 71 | // output yaml resolvers 72 | await outputFile(join(this.outputDir, 'resolvers.yaml'), resolvers, 'utf-8') 73 | } 74 | 75 | public async makeClientRuntimeConfig() { 76 | const builder = new ClientConfigBuilder() 77 | 78 | // create config from prisma dmmf 79 | const runtimeConfig = await builder.createRuntimeConfig(this.prismaDmmf, { defaultDirective: this.defaultDirective }) 80 | 81 | // copy client to outputDir 82 | await copy(join(__dirname, './client'), join(this.outputDir, 'client')) 83 | 84 | // client files (js + ts defs) 85 | await this.replaceInFile( 86 | join(this.outputDir, 'client', 'index.js'), 87 | /((?: )*{}\;*\s*\/\/\!\s+inject:config)/g, 88 | JSON.stringify(runtimeConfig), 89 | ) 90 | 91 | // inject in client ts defs 92 | await this.replaceInFile( 93 | join(this.outputDir, 'client', 'core.d.ts'), 94 | /((?: )*(\'|\")\/\/\!\s+inject:type:operations(\'|\"))/g, 95 | runtimeConfig.operations 96 | .sort() 97 | .map((o: string) => `"${o}"`) 98 | .join(' | '), 99 | ) 100 | } 101 | 102 | private async replaceInFile(file: string, findRegex: RegExp, replace: string) { 103 | const content = await readFile(file, 'utf-8') 104 | const newContent = content.replace(findRegex, replace) 105 | await outputFile(file, newContent, 'utf-8') 106 | 107 | return newContent 108 | } 109 | } 110 | 111 | export type GeneratorOption = { 112 | outputDir: string 113 | prismaDmmf: DMMF.Document 114 | prismaSchemaPath: string 115 | userGraphQLPath?: string 116 | userResolversPath?: string 117 | defaultDirective?: string 118 | } 119 | -------------------------------------------------------------------------------- /docs/changelog/1.0.0-rc.5.md: -------------------------------------------------------------------------------- 1 | --- 2 | editLink: false 3 | --- 4 | 5 | # 1.0.0-rc.5 6 | 7 | **🌟 Help us spread the word about Prisma-AppSync by starring the repo.** 8 | 9 | ## Highlights 10 | 11 | ### ⚡️ Async shield rules 12 | 13 | Async Shield rules are now supported in Prisma-AppSync, opening up to 3 different ways to define fine-grained access control rules: 14 | 15 | ```ts 16 | return await prismaAppSync.resolve({ 17 | event, 18 | shield: () => { 19 | return { 20 | // Boolean 21 | 'listPosts{,/**}': { rule: true }, 22 | 23 | // Function 24 | 'listPosts{,/**}': { rule: () => true }, 25 | 26 | // (NEW) Async Function 27 | 'listPosts{,/**}': { 28 | rule: async () => { 29 | await sleep(1000) 30 | return true 31 | }, 32 | }, 33 | } 34 | }, 35 | }) 36 | ``` 37 | 38 | ### ⚡️ Support for deeply nested relation filters 39 | 40 | Deeply nested relation filters are now supported in Prisma-AppSync, allowing to perform the following queries: 41 | 42 | ```graphql 43 | query { 44 | listComments( 45 | where: { 46 | author: { 47 | 48 | # deeply nested relation filter 49 | posts: { 50 | every: { 51 | published: { equals: true } 52 | } 53 | } 54 | 55 | } 56 | } 57 | ) 58 | } 59 | ``` 60 | 61 | ```graphql 62 | query { 63 | listUsers( 64 | where: { 65 | posts: { 66 | every: { 67 | 68 | # deeply nested relation filter 69 | comments: { 70 | every: { 71 | message: { startsWith: 'hello' } 72 | } 73 | } 74 | 75 | } 76 | } 77 | } 78 | ) 79 | } 80 | ``` 81 | 82 | ### ⚡️ Support for `extendedWhereUnique` preview feature 83 | 84 | Using the `extendedWhereUnique` preview feature flag will enable filtering on non-unique fields in Prisma-AppSync, allowing to do the following: 85 | 86 | ```prisma 87 | generator client { 88 | provider = "prisma-client-js" 89 | previewFeatures = ["extendedWhereUnique"] 90 | } 91 | ``` 92 | 93 | ```graphql 94 | mutation($id: Int!, $version: Int) { 95 | updatePost( 96 | # version is a non-unique field 97 | where: { id: $id, version: { equals: $version } }, 98 | operation: { version: { increment: 1 } } 99 | ) { 100 | id 101 | version 102 | } 103 | } 104 | ``` 105 | 106 | See [Prisma Docs](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#filter-on-non-unique-fields-with-userwhereuniqueinput) for more details. 107 | 108 | ## Fixes and improvements 109 | 110 | - [`maxDepth` parameter not working properly with Json fields (Issue #71).](https://github.com/maoosi/prisma-appsync/issues/71) 111 | - [Local dev server reads `undefined` when using nested arrays in query (Issue #83).](https://github.com/maoosi/prisma-appsync/issues/81) 112 | - [GraphQL input `WhereUniqueInput` shouldn’t include Relation fields (Issue #83).](https://github.com/maoosi/prisma-appsync/issues/83) 113 | - [Unit tests for Prisma to GraphQL schema conversion (Issue #84).](https://github.com/maoosi/prisma-appsync/issues/84) 114 | - [Local dev server returning `null` for `0` values (PR #82).](https://github.com/maoosi/prisma-appsync/pull/82) 115 | - [Issue: fields with `@default` should appear as required `!` in generated GraphQL schema base type (Issue #91).](https://github.com/maoosi/prisma-appsync/issues/91) 116 | - Improved, more readable, Prisma Client errors logs. 117 | 118 | ## Credits 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
Sylvain
Sylvain

🧙‍♂️ 💻 🤔 📖
Ciprian Caba
Ciprian Caba

💻 🤔
Bell
Bell

💻
129 | 130 | ## Github sponsors 131 | 132 | Enjoy using Prisma-AppSync? Please consider [💛 Github sponsors](https://github.com/sponsors/maoosi). 133 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/adapter.d.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ActionsAlias, AppSyncEvent, Authorization, Context, GraphQLType, Identity, Model, Options, PrismaArgs, QueryParams } from './types'; 2 | /** 3 | * #### Parse AppSync direct resolver `event` and returns Query Params. 4 | * 5 | * @param {AppSyncEvent} appsyncEvent - AppSync event received in Lambda. 6 | * @param {Required} options - PrismaAppSync Client options. 7 | * @param {any|null} customResolvers? - Custom Resolvers. 8 | * @returns `{ type, operation, context, fields, paths, args, prismaArgs, authorization, identity }` - QueryParams 9 | */ 10 | export declare function parseEvent(appsyncEvent: AppSyncEvent, options: Options, customResolvers?: any | null): Promise; 11 | /** 12 | * #### Convert `is: NULL` and `isNot: NULL` to `is: null` and `isNot: null` 13 | * 14 | * @param {any} data 15 | * @returns any 16 | */ 17 | export declare function addNullables(data: any): Promise; 18 | /** 19 | * #### Returns authorization and identity. 20 | * 21 | * @param {any} options 22 | * @param {AppSyncEvent} options.appsyncEvent - AppSync event received in Lambda. 23 | * @returns `{ authorization, identity }` 24 | * 25 | * https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-identity 26 | */ 27 | export declare function getAuthIdentity({ appsyncEvent }: { 28 | appsyncEvent: AppSyncEvent; 29 | }): { 30 | identity: Identity; 31 | authorization: Authorization; 32 | }; 33 | /** 34 | * #### Returns context (`action`, `alias` and `model`). 35 | * 36 | * @param {any} options 37 | * @param {any|null} options.customResolvers 38 | * @param {string} options.operation 39 | * @param {Options} options.options 40 | * @returns Context 41 | */ 42 | export declare function getContext({ customResolvers, operation, options, }: { 43 | customResolvers?: any | null; 44 | operation: string; 45 | options: Options; 46 | }): Context; 47 | /** 48 | * #### Returns operation (`getPost`, `listUsers`, ..). 49 | * 50 | * @param {any} options 51 | * @param {string} options.fieldName 52 | * @returns Operation 53 | */ 54 | export declare function getOperation({ fieldName }: { 55 | fieldName: string; 56 | }): string; 57 | /** 58 | * #### Returns action (`get`, `list`, `create`, ...). 59 | * 60 | * @param {any} options 61 | * @param {string} options.operation 62 | * @returns Action 63 | */ 64 | export declare function getAction({ operation }: { 65 | operation: string; 66 | }): Action; 67 | /** 68 | * #### Returns action alias (`access`, `create`, `modify`, `subscribe`). 69 | * 70 | * @param {any} options 71 | * @param {Action} options.action 72 | * @returns ActionsAlias 73 | */ 74 | export declare function getActionAlias({ action }: { 75 | action: Action; 76 | }): ActionsAlias; 77 | /** 78 | * #### Returns model (`Post`, `User`, ...). 79 | * 80 | * @param {any} options 81 | * @param {string} options.operation 82 | * @param {Action} options.action 83 | * @param {Options} options.options 84 | * @returns Model 85 | */ 86 | export declare function getModel({ operation, action, options }: { 87 | operation: string; 88 | action: Action; 89 | options: Options; 90 | }): Model; 91 | /** 92 | * #### Returns fields (`title`, `author`, ...). 93 | * 94 | * @param {any} options 95 | * @param {string[]} options._selectionSetList 96 | * @returns string[] 97 | */ 98 | export declare function getFields({ _selectionSetList }: { 99 | _selectionSetList: string[]; 100 | }): string[]; 101 | /** 102 | * #### Returns GraphQL type (`Query`, `Mutation` or `Subscription`). 103 | * 104 | * @param {any} options 105 | * @param {string} options._parentTypeName 106 | * @returns GraphQLType 107 | */ 108 | export declare function getType({ _parentTypeName }: { 109 | _parentTypeName: string; 110 | }): GraphQLType; 111 | /** 112 | * #### Returns Prisma args (`where`, `data`, `orderBy`, ...). 113 | * 114 | * @param {any} options 115 | * @param {Action} options.action 116 | * @param {Options['defaultPagination']} options.defaultPagination 117 | * @param {any} options._arguments 118 | * @param {any} options._selectionSetList 119 | * @returns PrismaArgs 120 | */ 121 | export declare function getPrismaArgs({ action, defaultPagination, _arguments, _selectionSetList, }: { 122 | action: Action; 123 | defaultPagination: Options['defaultPagination']; 124 | _arguments: any; 125 | _selectionSetList: any; 126 | }): PrismaArgs; 127 | /** 128 | * #### Returns req and res paths (`updatePost/title`, `getPost/date`, ..). 129 | * 130 | * @param {any} options 131 | * @param {string} options.operation 132 | * @param {Context} options.context 133 | * @param {PrismaArgs} options.prismaArgs 134 | * @returns string[] 135 | */ 136 | export declare function getPaths({ operation, context, prismaArgs, }: { 137 | operation: string; 138 | context: Context; 139 | prismaArgs: PrismaArgs; 140 | }): string[]; 141 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/adapter.d.ts: -------------------------------------------------------------------------------- 1 | import type { Action, ActionsAlias, AppSyncEvent, Authorization, Context, GraphQLType, Identity, Model, Options, PrismaArgs, QueryParams } from './types'; 2 | /** 3 | * #### Parse AppSync direct resolver `event` and returns Query Params. 4 | * 5 | * @param {AppSyncEvent} appsyncEvent - AppSync event received in Lambda. 6 | * @param {Required} options - PrismaAppSync Client options. 7 | * @param {any|null} customResolvers? - Custom Resolvers. 8 | * @returns `{ type, operation, context, fields, paths, args, prismaArgs, authorization, identity }` - QueryParams 9 | */ 10 | export declare function parseEvent(appsyncEvent: AppSyncEvent, options: Options, customResolvers?: any | null): Promise; 11 | /** 12 | * #### Convert `is: NULL` and `isNot: NULL` to `is: null` and `isNot: null` 13 | * 14 | * @param {any} data 15 | * @returns any 16 | */ 17 | export declare function addNullables(data: any): Promise; 18 | /** 19 | * #### Returns authorization and identity. 20 | * 21 | * @param {any} options 22 | * @param {AppSyncEvent} options.appsyncEvent - AppSync event received in Lambda. 23 | * @returns `{ authorization, identity }` 24 | * 25 | * https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-identity 26 | */ 27 | export declare function getAuthIdentity({ appsyncEvent }: { 28 | appsyncEvent: AppSyncEvent; 29 | }): { 30 | identity: Identity; 31 | authorization: Authorization; 32 | }; 33 | /** 34 | * #### Returns context (`action`, `alias` and `model`). 35 | * 36 | * @param {any} options 37 | * @param {any|null} options.customResolvers 38 | * @param {string} options.operation 39 | * @param {Options} options.options 40 | * @returns Context 41 | */ 42 | export declare function getContext({ customResolvers, operation, options, }: { 43 | customResolvers?: any | null; 44 | operation: string; 45 | options: Options; 46 | }): Context; 47 | /** 48 | * #### Returns operation (`getPost`, `listUsers`, ..). 49 | * 50 | * @param {any} options 51 | * @param {string} options.fieldName 52 | * @returns Operation 53 | */ 54 | export declare function getOperation({ fieldName }: { 55 | fieldName: string; 56 | }): string; 57 | /** 58 | * #### Returns action (`get`, `list`, `create`, ...). 59 | * 60 | * @param {any} options 61 | * @param {string} options.operation 62 | * @returns Action 63 | */ 64 | export declare function getAction({ operation }: { 65 | operation: string; 66 | }): Action; 67 | /** 68 | * #### Returns action alias (`access`, `create`, `modify`, `subscribe`). 69 | * 70 | * @param {any} options 71 | * @param {Action} options.action 72 | * @returns ActionsAlias 73 | */ 74 | export declare function getActionAlias({ action }: { 75 | action: Action; 76 | }): ActionsAlias; 77 | /** 78 | * #### Returns model (`Post`, `User`, ...). 79 | * 80 | * @param {any} options 81 | * @param {string} options.operation 82 | * @param {Action} options.action 83 | * @param {Options} options.options 84 | * @returns Model 85 | */ 86 | export declare function getModel({ operation, action, options }: { 87 | operation: string; 88 | action: Action; 89 | options: Options; 90 | }): Model; 91 | /** 92 | * #### Returns fields (`title`, `author`, ...). 93 | * 94 | * @param {any} options 95 | * @param {string[]} options._selectionSetList 96 | * @returns string[] 97 | */ 98 | export declare function getFields({ _selectionSetList }: { 99 | _selectionSetList: string[]; 100 | }): string[]; 101 | /** 102 | * #### Returns GraphQL type (`Query`, `Mutation` or `Subscription`). 103 | * 104 | * @param {any} options 105 | * @param {string} options._parentTypeName 106 | * @returns GraphQLType 107 | */ 108 | export declare function getType({ _parentTypeName }: { 109 | _parentTypeName: string; 110 | }): GraphQLType; 111 | /** 112 | * #### Returns Prisma args (`where`, `data`, `orderBy`, ...). 113 | * 114 | * @param {any} options 115 | * @param {Action} options.action 116 | * @param {Options['defaultPagination']} options.defaultPagination 117 | * @param {any} options._arguments 118 | * @param {any} options._selectionSetList 119 | * @returns PrismaArgs 120 | */ 121 | export declare function getPrismaArgs({ action, defaultPagination, _arguments, _selectionSetList, }: { 122 | action: Action; 123 | defaultPagination: Options['defaultPagination']; 124 | _arguments: any; 125 | _selectionSetList: any; 126 | }): PrismaArgs; 127 | /** 128 | * #### Returns req and res paths (`updatePost/title`, `getPost/date`, ..). 129 | * 130 | * @param {any} options 131 | * @param {string} options.operation 132 | * @param {Context} options.context 133 | * @param {PrismaArgs} options.prismaArgs 134 | * @returns string[] 135 | */ 136 | export declare function getPaths({ operation, context, prismaArgs, }: { 137 | operation: string; 138 | context: Context; 139 | prismaArgs: PrismaArgs; 140 | }): string[]; 141 | -------------------------------------------------------------------------------- /packages/server/src/appsync-simulator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { exec as nodeExec } from 'child_process' 5 | import chokidar from 'chokidar' 6 | import { 7 | type AmplifyAppSyncAPIConfig, 8 | AmplifyAppSyncSimulator, 9 | AmplifyAppSyncSimulatorAuthenticationType, 10 | type AmplifyAppSyncSimulatorConfig, 11 | type AppSyncMockFile, 12 | type AppSyncSimulatorDataSourceConfig, 13 | RESOLVER_KIND, 14 | } from 'amplify-appsync-simulator' 15 | 16 | declare global { 17 | // eslint-disable-next-line no-var, vars-on-top 18 | var __prismaAppSyncServer: any 19 | } 20 | 21 | export function useAppSyncSimulator({ 22 | lambdaHandler, schema, resolvers, port, wsPort, watchers, appSync, dataSources, 23 | }: ServerOptions) { 24 | const mappingTemplates: AppSyncMockFile[] = [{ 25 | path: 'lambdaRequest.vtl', 26 | content: readFileSync(resolve(__dirname, 'lambdaRequest.vtl'), 'utf8'), 27 | }, { 28 | path: 'lambdaResponse.vtl', 29 | content: readFileSync(resolve(__dirname, 'lambdaResponse.vtl'), 'utf8'), 30 | }] 31 | 32 | const appSyncWithDefaults: AmplifyAppSyncAPIConfig = { 33 | name: 'Prisma-AppSync', 34 | defaultAuthenticationType: { 35 | authenticationType: AmplifyAppSyncSimulatorAuthenticationType.API_KEY, 36 | }, 37 | apiKey: 'da2-fakeApiId123456', // this is the default for graphiql 38 | additionalAuthenticationProviders: [], 39 | } 40 | 41 | const simulatorConfig: AmplifyAppSyncSimulatorConfig = { 42 | appSync: appSync || appSyncWithDefaults, 43 | schema: { content: schema }, 44 | mappingTemplates, 45 | dataSources: dataSources || [{ 46 | type: 'AWS_LAMBDA', 47 | name: 'prisma-appsync', 48 | invoke: lambdaHandler.main, 49 | }], 50 | resolvers: resolvers.map(resolver => ({ 51 | ...resolver, 52 | dataSourceName: resolver.dataSource, 53 | kind: RESOLVER_KIND.UNIT, 54 | requestMappingTemplateLocation: 'lambdaRequest.vtl', 55 | responseMappingTemplateLocation: 'lambdaResponse.vtl', 56 | })), 57 | } 58 | 59 | globalThis?.__prismaAppSyncServer?.serverInstance?.stop() 60 | 61 | const serverInstance = new AmplifyAppSyncSimulator({ port, wsPort }) 62 | const watcherInstances: any[] = globalThis?.__prismaAppSyncServer?.watcherInstances || [] 63 | 64 | if (watchers?.length && !globalThis?.__prismaAppSyncServer?.watcherInstances) { 65 | const shell = (command: string): Promise<{ err: any; strdout: any; stderr: any }> => 66 | new Promise((resolve) => { 67 | nodeExec( 68 | command, 69 | (err: any, strdout: any, stderr: any) => { 70 | if (err) 71 | console.error(stderr) 72 | else if (strdout) 73 | console.log(strdout) 74 | 75 | resolve({ err, strdout, stderr }) 76 | }, 77 | ) 78 | }) 79 | 80 | watchers.forEach(({ watch, exec }) => { 81 | const chok = chokidar.watch(watch, { 82 | ignored: '**/node_modules/**', 83 | ignoreInitial: true, 84 | awaitWriteFinish: true, 85 | }) 86 | 87 | chok.on('change', async (path) => { 88 | if (exec === 'manual-restart') { 89 | console.log(`🚨 You need to manually restart the server to apply changes to ${path}`) 90 | } else { 91 | console.log(`Change detected on ${path}`) 92 | console.log(`Executing ${exec}`) 93 | await shell(exec) 94 | } 95 | }) 96 | 97 | watcherInstances.push(chok) 98 | }) 99 | } 100 | 101 | globalThis.__prismaAppSyncServer = { serverInstance, watcherInstances } 102 | 103 | return { 104 | start: async () => { 105 | await globalThis.__prismaAppSyncServer.serverInstance.start() 106 | globalThis.__prismaAppSyncServer.serverInstance.init(simulatorConfig) 107 | }, 108 | stop: () => { 109 | globalThis.__prismaAppSyncServer.serverInstance.stop() 110 | }, 111 | } 112 | } 113 | 114 | export type ServerOptions = { 115 | // required 116 | schema: string 117 | lambdaHandler: any 118 | resolvers: { 119 | typeName: string 120 | fieldName: string 121 | dataSource: string 122 | requestMappingTemplate: string 123 | responseMappingTemplate: string 124 | }[] 125 | port: number 126 | 127 | // optional 128 | wsPort?: number 129 | watchers?: { watch: string | string[]; exec: string }[] 130 | 131 | // advanced 132 | appSync?: AmplifyAppSyncAPIConfig 133 | dataSources?: AppSyncSimulatorDataSourceConfig[] 134 | } 135 | 136 | export { AmplifyAppSyncSimulatorAuthenticationType as AuthenticationType } 137 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/crud/client/core.d.ts: -------------------------------------------------------------------------------- 1 | import type { Options, PrismaAppSyncOptionsType, ResolveParams } from './types'; 2 | import { Prisma, PrismaClient } from './types'; 3 | /** 4 | * ## Prisma-AppSync Client ʲˢ 5 | * 6 | * Type-safe Prisma AppSync client for TypeScript & Node.js 7 | * @example 8 | * ``` 9 | * const prismaAppSync = new PrismaAppSync() 10 | * 11 | * // lambda handler (AppSync Direct Lambda Resolver) 12 | * export const resolver = async (event: any, context: any) => { 13 | * return await prismaAppSync.resolve({ event }) 14 | * } 15 | * ``` 16 | * 17 | * 18 | * Read more in our [docs](https://prisma-appsync.vercel.app). 19 | */ 20 | export declare class PrismaAppSync { 21 | options: Options; 22 | prismaClient: PrismaClient; 23 | /** 24 | * ### Client Constructor 25 | * 26 | * Instantiate Prisma-AppSync Client. 27 | * @example 28 | * ``` 29 | * const prismaAppSync = new PrismaAppSync() 30 | * ``` 31 | * 32 | * @param {PrismaAppSyncOptionsType} options 33 | * @param {string} options.connectionString? - Prisma connection string (database connection URL). 34 | * @param {boolean} options.sanitize? - Enable sanitize inputs (parse xss + encode html). 35 | * @param {'INFO' | 'WARN' | 'ERROR'} options.logLevel? - Server logs level (visible in CloudWatch). 36 | * @param {number|false} options.defaultPagination? - Default pagination for list Query (items per page). 37 | * @param {number} options.maxDepth? - Maximum allowed GraphQL query depth. 38 | * @param {number} options.maxReqPerUserMinute? - Maximum allowed requests per user, per minute. 39 | * 40 | * @default 41 | * ``` 42 | * { 43 | * connectionString: process.env.DATABASE_URL, 44 | * sanitize: true, 45 | * logLevel: 'INFO', 46 | * defaultPagination: 50, 47 | * maxDepth: 4, 48 | * maxReqPerUserMinute: 200 49 | * } 50 | * ``` 51 | * 52 | * 53 | * Read more in our [docs](https://prisma-appsync.vercel.app). 54 | */ 55 | constructor(options?: PrismaAppSyncOptionsType); 56 | /** 57 | * ### Resolver 58 | * 59 | * Resolve the API request, based on the AppSync `event` received by the Direct Lambda Resolver. 60 | * @example 61 | * ``` 62 | * await prismaAppSync.resolve({ event }) 63 | * 64 | * // custom resolvers 65 | * await prismaAppSync.resolve<'notify'|'listPosts'>( 66 | * event, 67 | * resolvers: { 68 | * // extend CRUD API with a custom `notify` query 69 | * notify: async ({ args }) => { return { message: args.message } }, 70 | * 71 | * // disable one of the generated CRUD API query 72 | * listPosts: false, 73 | * } 74 | * }) 75 | * ``` 76 | * 77 | * @param {ResolveParams} resolveParams 78 | * @param {any} resolveParams.event - AppSync event received by the Direct Lambda Resolver. 79 | * @param {any} resolveParams.resolvers? - Custom resolvers (to extend the CRUD API). 80 | * @param {function} resolveParams.shield? - Shield configuration (to protect your API). 81 | * @param {function} resolveParams.hooks? - Hooks (to trigger functions based on events). 82 | * @returns Promise 83 | * 84 | * 85 | * Read more in our [docs](https://prisma-appsync.vercel.app). 86 | */ 87 | resolve(resolveParams: ResolveParams<"countComments" | "countLikes" | "countPosts" | "countProfiles" | "countUsers" | "createComment" | "createLike" | "createManyComments" | "createManyLikes" | "createManyPosts" | "createManyProfiles" | "createManyUsers" | "createPost" | "createProfile" | "createUser" | "deleteComment" | "deleteLike" | "deleteManyComments" | "deleteManyLikes" | "deleteManyPosts" | "deleteManyProfiles" | "deleteManyUsers" | "deletePost" | "deleteProfile" | "deleteUser" | "getComment" | "getLike" | "getPost" | "getProfile" | "getUser" | "listComments" | "listLikes" | "listPosts" | "listProfiles" | "listUsers" | "onCreatedComment" | "onCreatedLike" | "onCreatedManyComments" | "onCreatedManyLikes" | "onCreatedManyPosts" | "onCreatedManyProfiles" | "onCreatedManyUsers" | "onCreatedPost" | "onCreatedProfile" | "onCreatedUser" | "onDeletedComment" | "onDeletedLike" | "onDeletedManyComments" | "onDeletedManyLikes" | "onDeletedManyPosts" | "onDeletedManyProfiles" | "onDeletedManyUsers" | "onDeletedPost" | "onDeletedProfile" | "onDeletedUser" | "onMutatedComment" | "onMutatedLike" | "onMutatedManyComments" | "onMutatedManyLikes" | "onMutatedManyPosts" | "onMutatedManyProfiles" | "onMutatedManyUsers" | "onMutatedPost" | "onMutatedProfile" | "onMutatedUser" | "onUpdatedComment" | "onUpdatedLike" | "onUpdatedManyComments" | "onUpdatedManyLikes" | "onUpdatedManyPosts" | "onUpdatedManyProfiles" | "onUpdatedManyUsers" | "onUpdatedPost" | "onUpdatedProfile" | "onUpdatedUser" | "onUpsertedComment" | "onUpsertedLike" | "onUpsertedPost" | "onUpsertedProfile" | "onUpsertedUser" | "updateComment" | "updateLike" | "updateManyComments" | "updateManyLikes" | "updateManyPosts" | "updateManyProfiles" | "updateManyUsers" | "updatePost" | "updateProfile" | "updateUser" | "upsertComment" | "upsertLike" | "upsertPost" | "upsertProfile" | "upsertUser", Extract>): Promise; 88 | } 89 | -------------------------------------------------------------------------------- /tests/client/resolver.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/prefer-global/process */ 2 | import { describe, expect } from 'vitest' 3 | import * as queries from '@client/resolver' 4 | import type { Model, QueryParams } from '@client/types' 5 | import { Actions, ActionsAliases, Authorizations } from '@client/consts' 6 | import mockLambdaIdentity from './mocks/lambda-identity' 7 | import { testEach } from './utils' 8 | 9 | process.env.PRISMA_APPSYNC_TESTING = 'true' 10 | 11 | const TESTING = { 12 | PostModel: { 13 | prismaRef: 'post', 14 | singular: 'Post', 15 | plural: 'Posts', 16 | }, 17 | } 18 | 19 | const identity = mockLambdaIdentity(Authorizations.AMAZON_COGNITO_USER_POOLS, { 20 | sourceIp: 'xxx.xxx.xxx.x', 21 | username: 'johndoe', 22 | sub: 'xxxxxx', 23 | resolverContext: {}, 24 | }) 25 | 26 | describe('CLIENT #queries', () => { 27 | const query: QueryParams = { 28 | headers: {}, 29 | args: {}, 30 | context: { 31 | action: Actions.get, 32 | alias: ActionsAliases.access, 33 | model: TESTING.PostModel, 34 | }, 35 | prismaArgs: { 36 | data: { title: 'Hello World' }, 37 | select: { title: true }, 38 | where: { id: 2 }, 39 | orderBy: { title: 'DESC' }, 40 | skip: 2, 41 | take: 1, 42 | skipDuplicates: true, 43 | }, 44 | authorization: Authorizations.AMAZON_COGNITO_USER_POOLS, 45 | identity, 46 | operation: 'getPost', 47 | fields: ['title'], 48 | type: 'Query', 49 | paths: [], 50 | } 51 | 52 | const createPrismaClient: any = (model: Model, prismaQuery: string) => { 53 | return { 54 | [model!.prismaRef]: { 55 | [prismaQuery]: (queryObject: any) => { 56 | return queryObject 57 | }, 58 | }, 59 | } 60 | } 61 | 62 | const tests = [ 63 | { 64 | name: 'getQuery', 65 | prismaQuery: 'findUnique', 66 | expectedResult: { 67 | where: query.prismaArgs.where, 68 | select: query.prismaArgs.select, 69 | }, 70 | }, 71 | { 72 | name: 'listQuery', 73 | prismaQuery: 'findMany', 74 | expectedResult: { 75 | where: query.prismaArgs.where, 76 | select: query.prismaArgs.select, 77 | orderBy: query.prismaArgs.orderBy, 78 | skip: query.prismaArgs.skip, 79 | take: query.prismaArgs.take, 80 | }, 81 | }, 82 | { 83 | name: 'countQuery', 84 | prismaQuery: 'count', 85 | expectedResult: { 86 | where: query.prismaArgs.where, 87 | select: query.prismaArgs.select, 88 | orderBy: query.prismaArgs.orderBy, 89 | skip: query.prismaArgs.skip, 90 | take: query.prismaArgs.take, 91 | }, 92 | }, 93 | { 94 | name: 'createQuery', 95 | prismaQuery: 'create', 96 | expectedResult: { 97 | data: query.prismaArgs.data, 98 | select: query.prismaArgs.select, 99 | }, 100 | }, 101 | { 102 | name: 'createManyQuery', 103 | prismaQuery: 'createMany', 104 | expectedResult: { 105 | data: query.prismaArgs.data, 106 | skipDuplicates: query.prismaArgs.skipDuplicates, 107 | }, 108 | }, 109 | { 110 | name: 'updateQuery', 111 | prismaQuery: 'update', 112 | expectedResult: { 113 | data: query.prismaArgs.data, 114 | where: query.prismaArgs.where, 115 | select: query.prismaArgs.select, 116 | }, 117 | }, 118 | { 119 | name: 'updateManyQuery', 120 | prismaQuery: 'updateMany', 121 | expectedResult: { 122 | data: query.prismaArgs.data, 123 | where: query.prismaArgs.where, 124 | }, 125 | }, 126 | { 127 | name: 'upsertQuery', 128 | prismaQuery: 'upsert', 129 | expectedResult: { 130 | create: query.prismaArgs.create, 131 | update: query.prismaArgs.update, 132 | where: query.prismaArgs.where, 133 | select: query.prismaArgs.select, 134 | }, 135 | }, 136 | { 137 | name: 'deleteQuery', 138 | prismaQuery: 'delete', 139 | expectedResult: { 140 | where: query.prismaArgs.where, 141 | select: query.prismaArgs.select, 142 | }, 143 | }, 144 | { 145 | name: 'deleteManyQuery', 146 | prismaQuery: 'deleteMany', 147 | expectedResult: { 148 | where: query.prismaArgs.where, 149 | }, 150 | }, 151 | ] 152 | 153 | const cases = tests.map((test: any) => { 154 | return [test.name, test.prismaQuery, test.expectedResult] 155 | }) 156 | 157 | testEach(cases)('expect "{0}" to call "{1}" Prisma Query', async (queryName, prismaQuery, expectedResult) => { 158 | const result = await queries[queryName](createPrismaClient(query.context.model, prismaQuery), query) 159 | expect(result).toEqual(expectedResult) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributions guide 2 | 3 | Thanks for your interest in contributing! 4 | 5 | ## 👉 Discuss first 6 | 7 | Before starting to work on a pull request, it's always better to open an issue first to confirm its desirability and discuss the approach with the maintainers. 8 | 9 | ## 👉 Project packages 10 | 11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 38 | 39 | 40 | 47 | 48 |
14 | 15 | **`packages/generator`** 16 | 17 | Generator for [Prisma ORM](https://www.prisma.io/), whose role is to parse your Prisma Schema and generate all the necessary components to run and deploy a GraphQL API tailored for AWS AppSync. 18 | 19 |
23 | 24 | **`packages/client`** 25 | 26 | Think of it as [Prisma Client](https://www.prisma.io/client) for GraphQL. Fully typed and designed for AWS Lambda AppSync Resolvers. It can handle CRUD operations with just a single line of code, or be fully extended. 27 | 28 |
32 | 33 | **`packages/installer`** 34 | 35 | Interactive CLI tool that streamlines the setup of new Prisma-AppSync projects, making it as simple as running `npx create-prisma-appsync-app@latest`. 36 | 37 |
41 | 42 | **`packages/server`** 43 | 44 | Local dev environment that mimics running Prisma-AppSync in production. It includes an AppSync simulator, local Lambda resolvers execution, a GraphQL IDE, hot-reloading, and authorizations. 45 | 46 |
49 | 50 | ## 👉 Repository setup 51 | 52 | We use `pnpm` as the core package manager, `yarn` + `docker` for creating the AWS CDK bundle before deployment, `zx` for running scripts, `aws` + `cdk` CLIs for deployment. 53 | 54 | **Start with cloning the repo on your local machine:** 55 | 56 | ```bash 57 | git clone https://github.com/maoosi/prisma-appsync.git 58 | ``` 59 | 60 | **Checkout the `dev` branch (working branch):** 61 | 62 | ```bash 63 | git checkout dev 64 | ``` 65 | 66 | **Install pre-requirements:** 67 | 68 | | Step | 69 | |:-------------| 70 | | 1. Install NodeJS, [latest LTS is recommended ↗](https://nodejs.org/en/about/releases/) | 71 | | 2. Install [pnpm ↗](https://pnpm.js.org/) | 72 | | 3. Install [yarn@1 ↗](https://classic.yarnpkg.com/en/docs/install/) | 73 | | 4. Install [zx ↗](https://github.com/google/zx) | 74 | | 5. Install [docker ↗](https://www.docker.com/products/docker-desktop) | 75 | | 6. Install the [AWS CLI ↗](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) | 76 | | 7. Install the [AWS CDK ↗](https://github.com/aws/aws-cdk) | 77 | 78 | **Verify installation:** 79 | 80 | ```bash 81 | node -v && pnpm --version && yarn --version && zx --version && docker --version && aws --version && cdk --version 82 | ``` 83 | 84 | **Install dependencies:** 85 | 86 | ```bash 87 | pnpm install 88 | ``` 89 | 90 | **Run local dev playground:** 91 | 92 | ```bash 93 | pnpm dev 94 | ``` 95 | 96 | > See list of commands below for more details about `pnpm dev`. 97 | 98 | ## 👉 Commands 99 | 100 | | Command | Description | 101 | | ------------- |:-------------| 102 | | `pnpm install` | Install project dependencies. | 103 | | `pnpm test` | Run all unit tests and e2e tests. | 104 | | `pnpm build` | Build the entire prisma-appsync library. | 105 | | `pnpm dev` | Creates local dev setup, useful for contributing [1]. | 106 | 107 | > [1] Auto-generates a 'playground' folder (if not there already) and launches a local GraphQL + AWS AppSync server. This simulates the Prisma-AppSync AWS environment for local development, with 'playground' contents pointing to local source packages. 108 | 109 | ## 👉 Commit convention 110 | 111 | We use [Conventional Commits ↗](https://www.conventionalcommits.org/) for commit messages such as: 112 | 113 | ```ts 114 | [optional scope]: 115 | ``` 116 | 117 | > - Possible types: `feat` / `fix` / `chore` / `docs` 118 | > - Possible scopes: `client` / `generator` / `cli` / `boilerplate` / `server` 119 | > - Description: Short description, with issue number when relevant. 120 | 121 | Here are some examples: 122 | 123 | | Type | Commit message | 124 | |:------------- |:------------- | 125 | | Bug fix | `fix(client): issue #234 - JEST_WORKER_ID replaced` | 126 | | New feature | `feat(generator): new defaultDirective parameter` | 127 | | Routine task | `chore: deps updated to latest` | 128 | | Docs update | `docs: fix typo inside home` | 129 | 130 | ## 👉 Coding guidelines 131 | 132 | ### ESLint 133 | 134 | We use [ESLint ↗](https://eslint.org/) for both linting and formatting. 135 | 136 |
137 | 138 | #### IDE Setup 139 | 140 | We recommend using [VS Code ↗](https://code.visualstudio.com/) along with the [ESLint extension ↗](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). 141 | 142 | With the settings on the right, you can have auto-fix and formatting when you save the code you are editing. 143 | 144 |
145 | 146 | VS Code's `settings.json` 147 | 148 | ```json 149 | { 150 | "editor.codeActionsOnSave": { 151 | "source.fixAll": false, 152 | "source.fixAll.eslint": true 153 | } 154 | } 155 | ``` 156 | 157 |
158 | 159 | ### No Prettier 160 | 161 | Since ESLint is already configured to format the code, there is no need to duplicate the functionality with Prettier. If you have Prettier installed in your editor, we recommend you disable it when working on the project to avoid conflict. 162 | 163 | ## 👉 License 164 | 165 | When you contribute code to the Prisma-AppSync project, you grant the maintainers permission to use and share your code under the project's BSD 2-Clause License. You also affirm that you are the original author of the code and have the authority to license it. 166 | -------------------------------------------------------------------------------- /packages/client/src/consts.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from './types' 2 | import { uniq } from './utils' 3 | 4 | // Enums 5 | 6 | export enum Actions { 7 | // queries 8 | get = 'get', 9 | list = 'list', 10 | count = 'count', 11 | 12 | // mutations (multiple) 13 | createMany = 'createMany', 14 | updateMany = 'updateMany', 15 | deleteMany = 'deleteMany', 16 | 17 | // mutations (single) 18 | create = 'create', 19 | update = 'update', 20 | upsert = 'upsert', 21 | delete = 'delete', 22 | 23 | // subscriptions (multiple) 24 | onCreatedMany = 'onCreatedMany', 25 | onUpdatedMany = 'onUpdatedMany', 26 | onDeletedMany = 'onDeletedMany', 27 | onMutatedMany = 'onMutatedMany', 28 | 29 | // subscriptions (single) 30 | onCreated = 'onCreated', 31 | onUpdated = 'onUpdated', 32 | onUpserted = 'onUpserted', 33 | onDeleted = 'onDeleted', 34 | onMutated = 'onMutated', 35 | } 36 | 37 | export enum ActionsAliases { 38 | access = 'access', 39 | batchAccess = 'batchAccess', 40 | create = 'create', 41 | batchCreate = 'batchCreate', 42 | delete = 'delete', 43 | batchDelete = 'batchDelete', 44 | modify = 'modify', 45 | batchModify = 'batchModify', 46 | subscribe = 'subscribe', 47 | batchSubscribe = 'batchSubscribe', 48 | } 49 | 50 | /** 51 | * ### Authorizations 52 | * 53 | * - `API_KEY`: Via hard-coded API key passed into `x-api-key` header. 54 | * - `AWS_IAM`: Via IAM identity and associated IAM policy rules. 55 | * - `AMAZON_COGNITO_USER_POOLS`: Via Amazon Cognito user token. 56 | * - `AWS_LAMBDA`: Via an AWS Lambda function. 57 | * - `OPENID_CONNECT`: Via Open ID connect such as Auth0. 58 | * 59 | * https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html 60 | */ 61 | export enum Authorizations { 62 | API_KEY = 'API_KEY', 63 | AWS_IAM = 'AWS_IAM', 64 | AMAZON_COGNITO_USER_POOLS = 'AMAZON_COGNITO_USER_POOLS', 65 | AWS_LAMBDA = 'AWS_LAMBDA', 66 | OPENID_CONNECT = 'OPENID_CONNECT', 67 | } 68 | 69 | // Consts 70 | 71 | export const Prisma_QueryOptions = [ 72 | 'where', 'data', 'select', 'orderBy', 'include', 'distinct', 73 | ] 74 | 75 | export const Prisma_NestedQueries = [ 76 | 'create', 'createMany', 'set', 'connect', 'connectOrCreate', 'disconnect', 'update', 'upsert', 'delete', 'updateMany', 'deleteMany', 77 | ] 78 | 79 | export const Prisma_FilterConditionsAndOperatos = [ 80 | 'equals', 'not', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'contains', 'search', 'mode', 'startsWith', 'endsWith', 'AND', 'OR', 'NOT', 81 | ] 82 | 83 | export const Prisma_FilterRelationFilters = [ 84 | 'some', 'every', 'none', 'is', 'isNot', 85 | ] 86 | 87 | export const Prisma_ScalarListMethods = [ 88 | 'set', 'push', 'unset', 89 | ] 90 | 91 | export const Prisma_ScalarListFilters = [ 92 | 'has', 'hasEvery', 'hasSome', 'isEmpty', 'isSet', 'equals', 93 | ] 94 | 95 | export const Prisma_CompositeTypeMethods = [ 96 | 'set', 'unset', 'update', 'upsert', 'push', 97 | ] 98 | 99 | export const Prisma_CompositeTypeFilters = [ 100 | 'equals', 'is', 'isNot', 'isEmpty', 'every', 'some', 'none', 101 | ] 102 | 103 | export const Prisma_AtomicNumberOperations = [ 104 | 'increment', 'decrement', 'multiply', 'divide', 'set', 105 | ] 106 | 107 | export const Prisma_JSONFilters = [ 108 | 'path', 'string_contains', 'string_starts_with', 'string_ends_with', 'array_contains', 'array_starts_with', 'array_ends_with', 109 | ] 110 | 111 | export const Prisma_ReservedKeysForPaths = uniq([ 112 | ...Prisma_QueryOptions, 113 | ...Prisma_FilterConditionsAndOperatos, 114 | ...Prisma_FilterRelationFilters, 115 | ...Prisma_ScalarListFilters, 116 | ...Prisma_CompositeTypeFilters, 117 | ...Prisma_JSONFilters, 118 | ]) 119 | 120 | export const Prisma_ReservedKeys = uniq([ 121 | ...Prisma_QueryOptions, 122 | ...Prisma_NestedQueries, 123 | ...Prisma_FilterConditionsAndOperatos, 124 | ...Prisma_FilterRelationFilters, 125 | ...Prisma_ScalarListMethods, 126 | ...Prisma_ScalarListFilters, 127 | ...Prisma_CompositeTypeMethods, 128 | ...Prisma_CompositeTypeFilters, 129 | ...Prisma_AtomicNumberOperations, 130 | ...Prisma_JSONFilters, 131 | ]) 132 | 133 | export const ActionsAliasesList = { 134 | access: [Actions.get, Actions.list, Actions.count], 135 | batchAccess: [Actions.list, Actions.count], 136 | create: [Actions.create, Actions.createMany], 137 | batchCreate: [Actions.createMany], 138 | modify: [Actions.upsert, Actions.update, Actions.updateMany, Actions.delete, Actions.deleteMany], 139 | batchModify: [Actions.updateMany, Actions.deleteMany], 140 | delete: [Actions.delete, Actions.deleteMany], 141 | batchDelete: [Actions.deleteMany], 142 | subscribe: [ 143 | Actions.onCreatedMany, 144 | Actions.onUpdatedMany, 145 | Actions.onDeletedMany, 146 | Actions.onMutatedMany, 147 | Actions.onCreated, 148 | Actions.onUpdated, 149 | Actions.onUpserted, 150 | Actions.onDeleted, 151 | Actions.onMutated, 152 | ], 153 | batchSubscribe: [Actions.onCreatedMany, Actions.onUpdatedMany, Actions.onDeletedMany, Actions.onMutatedMany], 154 | } as const 155 | 156 | let actionsListMultiple: Action[] = [] 157 | let actionsListSingle: Action[] = [] 158 | 159 | for (const actionAlias in ActionsAliasesList) { 160 | if (actionAlias.startsWith('batch')) 161 | actionsListMultiple = actionsListMultiple.concat(ActionsAliasesList[actionAlias]) 162 | 163 | else 164 | actionsListSingle = actionsListSingle.concat(ActionsAliasesList[actionAlias]) 165 | } 166 | 167 | export const ActionsList = actionsListSingle.filter((item, pos) => actionsListSingle.indexOf(item) === pos) 168 | 169 | export const BatchActionsList = actionsListMultiple.filter((item, pos) => actionsListMultiple.indexOf(item) === pos) 170 | 171 | export const DebugTestingKey = '__prismaAppsync' 172 | -------------------------------------------------------------------------------- /tests/generator/schemas/generated/@gql/client/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * #### Deep merge objects (without mutating the target object). 3 | * 4 | * @example const newObj = merge(obj1, obj2, obj3) 5 | * 6 | * @param {any[]} sources 7 | * @returns any 8 | */ 9 | export declare function merge(...sources: any[]): any; 10 | /** 11 | * #### Deep clone object. 12 | * 13 | * @example const newObj = clone(sourceObj) 14 | * 15 | * @param {any} source 16 | * @returns any 17 | */ 18 | export declare function clone(source: any): any; 19 | /** 20 | * #### Returns decoded text, replacing HTML special characters. 21 | * 22 | * @example decode('< > " ' & © ∆') 23 | * // returns '< > " \' & © ∆' 24 | * 25 | * @param {string} str 26 | * @returns string 27 | */ 28 | export declare function decode(str: string): string; 29 | /** 30 | * #### Returns encoded text, version of string. 31 | * 32 | * @example encode('