Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore(client): fix isolated data proxy issues [DPGA, 3] #13599

Merged
merged 9 commits into from Jun 7, 2022
13 changes: 6 additions & 7 deletions packages/client/src/generation/TSClient/TSClient.ts
Expand Up @@ -39,7 +39,7 @@ export interface TSClientOptions {
generator?: GeneratorConfig
platforms?: Platform[] // TODO: consider making it non-nullable
sqliteDatasourceOverrides?: DatasourceOverwrite[]
schemaDir: string
schemaPath: string
outputDir: string
activeProvider: string
dataProxy?: boolean
Expand All @@ -59,13 +59,12 @@ export class TSClient implements Generatable {
generator,
sqliteDatasourceOverrides,
outputDir,
schemaDir,
schemaPath,
runtimeDir,
runtimeName,
datasources,
dataProxy,
} = this.options
const schemaPath = path.join(schemaDir, 'schema.prisma')
millsp marked this conversation as resolved.
Show resolved Hide resolved
const envPaths = getEnvPaths(schemaPath, { cwd: outputDir })

const relativeEnvPaths = {
Expand All @@ -83,7 +82,7 @@ export class TSClient implements Generatable {
generator,
relativeEnvPaths,
sqliteDatasourceOverrides,
relativePath: path.relative(outputDir, schemaDir),
relativePath: path.relative(outputDir, path.dirname(schemaPath)),
millsp marked this conversation as resolved.
Show resolved Hide resolved
clientVersion: this.options.clientVersion,
engineVersion: this.options.engineVersion,
datasourceNames: datasources.map((d) => d.name),
Expand All @@ -96,8 +95,8 @@ export class TSClient implements Generatable {
const relativeOutdir = path.relative(process.cwd(), outputDir)

const code = `${commonCodeJS({ ...this.options, browser: false })}
${buildRequirePath(dataProxy)}
${buildDirname(dataProxy, relativeOutdir, runtimeDir)}
${buildRequirePath(edge)}
millsp marked this conversation as resolved.
Show resolved Hide resolved
${buildDirname(edge, relativeOutdir, runtimeDir)}
millsp marked this conversation as resolved.
Show resolved Hide resolved
/**
* Enums
*/
Expand Down Expand Up @@ -142,7 +141,7 @@ ${buildNFTAnnotations(dataProxy, engineType, platforms, relativeOutdir)}
this.options.browser,
this.options.generator,
this.options.sqliteDatasourceOverrides,
this.options.schemaDir,
path.dirname(this.options.schemaPath),
)

const collector = new ExportCollector()
Expand Down
25 changes: 11 additions & 14 deletions packages/client/src/generation/generateClient.ts
Expand Up @@ -33,8 +33,7 @@ export class DenylistError extends Error {
export interface GenerateClientOptions {
projectRoot?: string
datamodel: string
datamodelPath: string
schemaDir?: string
schemaPath: string
transpile?: boolean
runtimeDirs?: { node: string; edge: string }
outputDir: string
Expand All @@ -57,7 +56,7 @@ export interface BuildClientResult {

// eslint-disable-next-line @typescript-eslint/require-await
export async function buildClient({
schemaDir,
schemaPath,
runtimeDirs,
binaryPaths,
outputDir,
Expand All @@ -69,7 +68,7 @@ export async function buildClient({
projectRoot,
activeProvider,
dataProxy,
}: O.Required<GenerateClientOptions, 'schemaDir' | 'runtimeDirs'>): Promise<BuildClientResult> {
}: O.Required<GenerateClientOptions, 'runtimeDirs'>): Promise<BuildClientResult> {
// we define the basic options for the client generation
const document = getPrismaClientDMMF(dmmf)
const clientEngineType = getClientEngineType(generator!)
Expand All @@ -81,7 +80,7 @@ export async function buildClient({
clientEngineType === ClientEngineType.Library
? (Object.keys(binaryPaths.libqueryEngine ?? {}) as Platform[])
: (Object.keys(binaryPaths.queryEngine ?? {}) as Platform[]),
schemaDir,
schemaPath,
outputDir,
clientVersion,
engineVersion,
Expand Down Expand Up @@ -171,8 +170,7 @@ async function getDefaultOutdir(outputDir: string): Promise<string> {
export async function generateClient(options: GenerateClientOptions): Promise<void> {
const {
datamodel,
datamodelPath,
schemaDir = datamodelPath ? path.dirname(datamodelPath) : process.cwd(),
schemaPath,
outputDir,
transpile,
generator,
Expand All @@ -192,8 +190,7 @@ export async function generateClient(options: GenerateClientOptions): Promise<vo

const { prismaClientDmmf, fileMap } = await buildClient({
datamodel,
datamodelPath,
schemaDir,
schemaPath,
transpile,
runtimeDirs,
outputDir: finalOutputDir,
Expand All @@ -213,7 +210,7 @@ export async function generateClient(options: GenerateClientOptions): Promise<vo
if (denylistsErrors) {
let message = `${chalk.redBright.bold(
'Error: ',
)}The schema at "${datamodelPath}" contains reserved keywords.\n Rename the following items:`
)}The schema at "${schemaPath}" contains reserved keywords.\n Rename the following items:`

for (const error of denylistsErrors) {
message += '\n - ' + error.message
Expand Down Expand Up @@ -316,9 +313,9 @@ export async function generateClient(options: GenerateClientOptions): Promise<vo
}
}

const datamodelTargetPath = path.join(finalOutputDir, 'schema.prisma')
if (datamodelPath !== datamodelTargetPath) {
await copyFile(datamodelPath, datamodelTargetPath)
const schemaTargetPath = path.join(finalOutputDir, 'schema.prisma')
millsp marked this conversation as resolved.
Show resolved Hide resolved
if (schemaPath !== schemaTargetPath) {
await copyFile(schemaPath, schemaTargetPath)
}

const proxyIndexJsPath = path.join(outputDir, 'index.js')
Expand Down Expand Up @@ -436,7 +433,7 @@ async function getGenerationDirs({ testMode, runtimeDirs, generator, outputDir }
const useDefaultOutdir = testMode ? !runtimeDirs : !generator?.isCustomOutput

const _runtimeDirs = {
// if we have an override we use it, but if not then use the defaults
// if we have an override, we use it, but if not then use the defaults
node: runtimeDirs?.node || (useDefaultOutdir ? '@prisma/client/runtime' : './runtime'),
edge: runtimeDirs?.edge || (useDefaultOutdir ? '@prisma/client/runtime' : '../runtime'),
}
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/generation/generator.ts
Expand Up @@ -40,7 +40,7 @@ if (process.argv[1] === __filename) {

return generateClient({
datamodel: options.datamodel,
datamodelPath: options.schemaPath,
schemaPath: options.schemaPath,
binaryPaths: options.binaryPaths!,
datasources: options.datasources,
outputDir,
Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/generation/utils/buildDirname.ts
Expand Up @@ -2,13 +2,13 @@ import path from 'path'

/**
* Builds a `dirname` variable that holds the location of the generated client.
* @param dataProxy
* @param edge
* @param relativeOutdir
* @param runtimeDir
* @returns
*/
export function buildDirname(dataProxy: boolean | undefined, relativeOutdir: string, runtimeDir: string) {
if (dataProxy === true) {
export function buildDirname(edge: boolean | undefined, relativeOutdir: string, runtimeDir: string) {
if (edge === true) {
millsp marked this conversation as resolved.
Show resolved Hide resolved
return buildDirnameDefault()
}

Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/generation/utils/buildRequirePath.ts
@@ -1,10 +1,10 @@
/**
* Builds a require statement for `path`.
* @param dataProxy
* @param edge
* @returns
*/
export function buildRequirePath(dataProxy: boolean | undefined) {
if (dataProxy === true) return ''
export function buildRequirePath(edge: boolean | undefined) {
if (edge === true) return ''

return `
const path = require('path')`
Expand Down
3 changes: 1 addition & 2 deletions packages/client/src/utils/generateInFolder.ts
Expand Up @@ -133,11 +133,10 @@ export async function generateInFolder({
dmmf,
...config,
outputDir,
schemaDir: path.dirname(schemaPath),
runtimeDirs,
transpile,
testMode: true,
datamodelPath: schemaPath,
schemaPath,
copyRuntime: false,
generator: config.generators[0],
clientVersion: 'local',
Expand Down
Expand Up @@ -35,7 +35,7 @@ export async function setupTestSuiteClient(suiteMeta: TestSuiteMeta, suiteConfig

await generateClient({
datamodel: schema,
datamodelPath: getTestSuiteSchemaPath(suiteMeta, suiteConfig),
schemaPath: getTestSuiteSchemaPath(suiteMeta, suiteConfig),
binaryPaths: { libqueryEngine: {}, queryEngine: {} },
datasources: config.datasources,
outputDir: path.join(suiteFolderPath, 'node_modules/@prisma/client'),
Expand Down
51 changes: 13 additions & 38 deletions packages/engine-core/src/data-proxy/DataProxyEngine.ts
Expand Up @@ -18,7 +18,6 @@ import { request } from './utils/request'
const MAX_RETRIES = 10

export class DataProxyEngine extends Engine {
private pushPromise: Promise<void>
private inlineSchema: string
private inlineSchemaHash: string
private inlineDatasources: any
Expand Down Expand Up @@ -48,24 +47,6 @@ export class DataProxyEngine extends Engine {
this.remoteClientVersion = getClientVersion(this.config)
this.headers = { Authorization: `Bearer ${apiKey}` }
this.host = host

millsp marked this conversation as resolved.
Show resolved Hide resolved
// hack for Cloudflare
// That's because we instantiate the client outside of the request handler. This essentially prevents immediate execution of the promise.
// Removing this will produce the following error
// [Error] Some functionality, such as asynchronous I/O, timeouts, and generating random values, can only be performed while handling a request.
const promise = Promise.resolve()
this.pushPromise = promise.then(() => this.pushSchema())
}

private async pushSchema() {
const response = await request(this.url('schema'), {
method: 'GET',
headers: this.headers,
})

if (response.status === 404) {
await this.uploadSchema()
}
}

version() {
Expand Down Expand Up @@ -108,6 +89,7 @@ export class DataProxyEngine extends Engine {
method: 'PUT',
headers: this.headers,
body: this.inlineSchema,
clientVersion: this.clientVersion,
})

const err = await responseToError(response, this.clientVersion)
Expand Down Expand Up @@ -144,8 +126,6 @@ export class DataProxyEngine extends Engine {
}

private async requestInternal<T>(body: Record<string, any>, headers: Record<string, string>, attempt: number) {
await this.pushPromise

try {
this.logEmitter.emit('info', {
message: `Calling ${this.url('graphql')} (n=${attempt})`,
Expand All @@ -155,21 +135,20 @@ export class DataProxyEngine extends Engine {
method: 'POST',
headers: { ...headers, ...this.headers },
body: JSON.stringify(body),
clientVersion: this.clientVersion,
})

const err = await responseToError(response, this.clientVersion)
const e = await responseToError(response, this.clientVersion)

if (err instanceof SchemaMissingError) {
if (e instanceof SchemaMissingError) {
await this.uploadSchema()
throw new ForcedRetryError({
clientVersion: this.clientVersion,
cause: err,
cause: e,
})
}

if (err) {
throw err
}
if (e) throw e

const data = await response.json()

Expand All @@ -180,22 +159,18 @@ export class DataProxyEngine extends Engine {
}

return data
} catch (err) {
} catch (e) {
this.logEmitter.emit('error', {
message: `Error while querying: ${err.message ?? '(unknown)'}`,
message: `Error while querying: ${e.message ?? '(unknown)'}`,
})

if (!(err instanceof DataProxyError)) {
throw err
}
if (!err.isRetryable) {
throw err
}
if (!(e instanceof DataProxyError)) throw e
if (!e.isRetryable) throw e
if (attempt >= MAX_RETRIES) {
if (err instanceof ForcedRetryError) {
throw err.cause
if (e instanceof ForcedRetryError) {
throw e.cause
} else {
throw err
throw e
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/engine-core/src/data-proxy/errors/NetworkError.ts
@@ -0,0 +1,14 @@
import type { DataProxyErrorInfo } from './DataProxyError'
import { DataProxyError } from './DataProxyError'
import { setRetryable } from './utils/setRetryable'

export interface NetworkErrorInfo extends DataProxyErrorInfo {}

export class NetworkError extends DataProxyError {
public name = 'NetworkError'
public code = 'P5010'

constructor(info: NetworkErrorInfo) {
super('Cannot fetch data from service', setRetryable(info, true))
millsp marked this conversation as resolved.
Show resolved Hide resolved
}
}
22 changes: 15 additions & 7 deletions packages/engine-core/src/data-proxy/utils/request.ts
@@ -1,8 +1,9 @@
import { IncomingMessage } from 'http'
import type { IncomingMessage } from 'http'
import type Https from 'https'
import type { RequestInit, Response } from 'node-fetch'
import { O } from 'ts-toolbelt'
import type { O } from 'ts-toolbelt'

import { NetworkError } from '../errors/NetworkError'
import { getJSRuntimeName } from './getJSRuntimeName'

// our implementation handles less
Expand All @@ -18,13 +19,20 @@ declare let fetch: typeof nodeFetch
* @param options
* @returns
*/
export async function request(url: string, options: RequestOptions = {}): Promise<RequestResponse> {
export async function request(
url: string,
options: RequestOptions & { clientVersion: string },
): Promise<RequestResponse> {
const jsRuntimeName = getJSRuntimeName()

if (jsRuntimeName === 'browser') {
return fetch(url, options)
} else {
return nodeFetch(url, options)
try {
if (jsRuntimeName === 'browser') {
return await fetch(url, options)
} else {
return await nodeFetch(url, options)
}
} catch (e) {
throw new NetworkError({ clientVersion: options.clientVersion })
millsp marked this conversation as resolved.
Show resolved Hide resolved
millsp marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down