Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/amplify appsync simulator #147

Merged
merged 13 commits into from
Jun 10, 2023
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"ignoredNodes": ["TemplateLiteral *", "TSTypeParameterInstantiation"],
"SwitchCase": 1
}
]
],
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
},
"globals": {
"$": true,
Expand Down
23 changes: 5 additions & 18 deletions packages/boilerplate/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import { join } from 'path'
import { readFileSync } from 'fs'
import { load } from 'js-yaml'
import { argv, createServer } from 'prisma-appsync/dist/server'

(async () => {
const schema = readFileSync(join(process.cwd(), argv.flags.schema), { encoding: 'utf-8' })
const lambdaHandler = await import(join(process.cwd(), argv.flags.handler))
const resolvers = load(readFileSync(join(process.cwd(), argv.flags.resolvers), { encoding: 'utf-8' }))
const port = argv.flags.port
const wsPort = argv.flags.wsPort
const watchers = argv.flags.watchers ? JSON.parse(argv.flags.watchers) : []
const headers = argv.flags.headers ? JSON.parse(argv.flags.headers) : {}

const defaultQuery = /* GraphQL */`
query listPosts {
listPosts {
id
title
}
}

mutation createPost {
createPost(data:{ title: "My first post" }) {
title
}
}
`

createServer({
schema,
lambdaHandler,
defaultQuery,
resolvers,
port,
wsPort,
watchers,
headers,
})
})()
19 changes: 11 additions & 8 deletions packages/client/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,14 +552,17 @@ export function getPaths({
context: Context
prismaArgs: PrismaArgs
}): string[] {
const paths: string[] = objectToPaths({
...(prismaArgs?.data && {
data: prismaArgs.data,
}),
...(prismaArgs?.select && {
select: prismaArgs.select,
const paths: string[] = [
operation,
...objectToPaths({
...(prismaArgs?.data && {
data: prismaArgs.data,
}),
...(prismaArgs?.select && {
select: prismaArgs.select,
}),
}),
})
]

paths.forEach((path: string, index: number) => {
if (path.startsWith('data')) {
Expand All @@ -584,6 +587,6 @@ export function getPaths({
.split('/')
.filter(k => !Prisma_ReservedKeysForPaths.includes(k))
.join('/'),
),
).filter(Boolean),
)
}
15 changes: 13 additions & 2 deletions packages/installer/src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export class Installer {
...this.installConfig.dependencies,
...[
{ package: '@types/node', dev: true },
{ package: 'js-yaml', dev: true },
],
]

Expand All @@ -326,10 +327,15 @@ export class Installer {
path.join(path.dirname(this.userChoices.prismaSchemaPath), 'generated/prisma-appsync/schema.gql'),
)

const yamlResolversPath = path.relative(
path.join(this.detected.rootPath),
path.join(path.dirname(this.userChoices.prismaSchemaPath), 'generated/prisma-appsync/resolvers.yaml'),
)

const devCmd = [
'npx prisma generate',
`DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss`,
`DATABASE_URL='${databaseUrl}' npx vite-node ./server.ts --watch -- --schema ${gqlSchemaPath} --watchers '[{"watch":["**/*.prisma","*.prisma"],"exec":"npx prisma generate && DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss && touch ./server.ts"}]'`,
`DATABASE_URL='${databaseUrl}' npx vite-node ./server.ts --watch -- --schema ${gqlSchemaPath} --resolvers ${yamlResolversPath} --watchers '[{"watch":["**/*.prisma","*.prisma"],"exec":"npx prisma generate && DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss && touch ./server.ts"}]'`,
]

this.installConfig.scripts.push({
Expand All @@ -355,11 +361,16 @@ export class Installer {
path.join(path.dirname(this.userChoices.prismaSchemaPath), 'generated/prisma-appsync/schema.gql'),
)

const yamlResolversPath = path.relative(
path.join(this.detected.rootPath),
path.join(path.dirname(this.userChoices.prismaSchemaPath), 'generated/prisma-appsync/resolvers.yaml'),
)

const devCmd = [
'docker-compose up -d',
'npx prisma generate',
`DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss`,
`DATABASE_URL='${databaseUrl}' npx vite-node ./server.ts --watch -- --schema ${gqlSchemaPath} --watchers '[{"watch":"../packages/(client|generator)/**","exec":"cd ../ && pnpm run build --ignoreServer && cd playground && npx prisma generate && touch ./server.ts"},{"watch":["**/*.prisma","*.prisma"],"exec":"npx prisma generate && DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss && touch ./server.ts"}]'`,
`DATABASE_URL='${databaseUrl}' npx vite-node ./server.ts --watch -- --schema ${gqlSchemaPath} --resolvers ${yamlResolversPath} --watchers '[{"watch":"../packages/(client|generator)/**","exec":"cd ../ && pnpm run build --ignoreServer && cd playground && npx prisma generate && touch ./server.ts"},{"watch":["**/*.prisma","*.prisma"],"exec":"npx prisma generate && DATABASE_URL='${databaseUrl}' npx prisma db push --accept-data-loss && touch ./server.ts"}]'`,
]

this.installConfig.scripts.push({
Expand Down
11 changes: 2 additions & 9 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,8 @@
"author": "maoosi <hello@sylvainsimao.fr>",
"license": "BSD-2-Clause",
"devDependencies": {
"@envelop/types": "^3.0.0",
"@types/lodash": "^4.14.186",
"@types/prettier": "^2.7.1",
"amplify-appsync-simulator": "^2.4.1",
"chokidar": "^3.5.3",
"cleye": "^1.2.1",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"graphql-yoga": "^3.1.1",
"lodash": "^4.17.21",
"prettier": "^2.7.1"
"cleye": "^1.2.1"
}
}
136 changes: 136 additions & 0 deletions packages/server/src/appsync-simulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-disable no-console */
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { exec as nodeExec } from 'child_process'
import chokidar from 'chokidar'
import {
type AmplifyAppSyncAPIConfig,
AmplifyAppSyncSimulator,
AmplifyAppSyncSimulatorAuthenticationType,
type AmplifyAppSyncSimulatorConfig,
type AppSyncMockFile,
type AppSyncSimulatorDataSourceConfig,
RESOLVER_KIND,
} from 'amplify-appsync-simulator'

declare global {
// eslint-disable-next-line no-var, vars-on-top
var __prismaAppSyncServer: any
}

export function useAppSyncSimulator({
lambdaHandler, schema, resolvers, port, wsPort, watchers, appSync, dataSources,
}: ServerOptions) {
const mappingTemplates: AppSyncMockFile[] = [{
path: 'lambdaRequest.vtl',
content: readFileSync(resolve(__dirname, 'lambdaRequest.vtl'), 'utf8'),
}, {
path: 'lambdaResponse.vtl',
content: readFileSync(resolve(__dirname, 'lambdaResponse.vtl'), 'utf8'),
}]

const appSyncWithDefaults: AmplifyAppSyncAPIConfig = {
name: 'Prisma-AppSync',
defaultAuthenticationType: {
authenticationType: AmplifyAppSyncSimulatorAuthenticationType.API_KEY,
},
apiKey: 'da2-fakeApiId123456', // this is the default for graphiql
additionalAuthenticationProviders: [],
}

const simulatorConfig: AmplifyAppSyncSimulatorConfig = {
appSync: appSync || appSyncWithDefaults,
schema: { content: schema },
mappingTemplates,
dataSources: dataSources || [{
type: 'AWS_LAMBDA',
name: 'prisma-appsync',
invoke: lambdaHandler.main,
}],
resolvers: resolvers.map(resolver => ({
...resolver,
dataSourceName: resolver.dataSource,
kind: RESOLVER_KIND.UNIT,
requestMappingTemplateLocation: 'lambdaRequest.vtl',
responseMappingTemplateLocation: 'lambdaResponse.vtl',
})),
}

if (globalThis?.__prismaAppSyncServer?.serverInstance)
globalThis?.__prismaAppSyncServer?.serverInstance?.stop()

const serverInstance = new AmplifyAppSyncSimulator({ port, wsPort })
const watcherInstances: any[] = globalThis?.__prismaAppSyncServer?.watcherInstances || []

if (watchers?.length && !globalThis?.__prismaAppSyncServer?.watcherInstances) {
const shell = (command: string): Promise<{ err: any; strdout: any; stderr: any }> =>
new Promise((resolve) => {
nodeExec(
command,
(err: any, strdout: any, stderr: any) => {
if (err)
console.error(stderr)
else if (strdout)
console.log(strdout)

resolve({ err, strdout, stderr })
},
)
})

watchers.forEach(({ watch, exec }) => {
const chok = chokidar.watch(watch, {
ignored: '**/node_modules/**',
ignoreInitial: true,
awaitWriteFinish: true,
})

chok.on('change', async (path) => {
console.log(`Change detected on ${path}`)

if (exec) {
console.log(`Executing ${exec}`)
await shell(exec)
}
})

watcherInstances.push(chok)
})
}

globalThis.__prismaAppSyncServer = { serverInstance, watcherInstances }

return {
start: async () => {
await globalThis.__prismaAppSyncServer.serverInstance.start()
globalThis.__prismaAppSyncServer.serverInstance.init(simulatorConfig)
},
stop: () => {
globalThis.__prismaAppSyncServer.serverInstance.stop()
},
}
}

export type ServerOptions = {
// required
schema: string
lambdaHandler: any
resolvers: {
typeName: string
fieldName: string
dataSource: string
requestMappingTemplate: string
responseMappingTemplate: string
}[]
port: number

// optional
wsPort?: number
watchers?: { watch: string | string[]; exec: string }[]

// advanced
appSync?: AmplifyAppSyncAPIConfig
dataSources?: AppSyncSimulatorDataSourceConfig[]
}

export { AmplifyAppSyncSimulatorAuthenticationType as AuthenticationType }
22 changes: 1 addition & 21 deletions packages/server/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1 @@
import type { YogaServerOptions } from 'graphql-yoga'

export const argv: any

export function createServer({
schema,
lambdaHandler,
port,
defaultQuery,
watchers
}: {
schema: string
lambdaHandler: any
port: number
defaultQuery?: string
headers?: any
watchers?: { watch: string | string[]; exec: string }[]
yogaServerOptions?: YogaServerOptions<{}, {}>
}): void


export { createServer, argv } from './index'
Loading