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: support for edge runtime #1209

Merged
merged 12 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function createTRPCNext<
TRouter extends AnyRouter,
TPath extends string | undefined = undefined,
TSSRContext extends NextPageContext = NextPageContext,
TFlags = null,
TFlags = null
>(opts: Parameters<typeof _createTRPCNext>[0]) {
const r: CreateTRPCNext<TRouter, TSSRContext, TFlags> = _createTRPCNext<TRouter, TSSRContext, TFlags>(opts);
return r as DeepOverrideAtPath<CreateTRPCNext<TRouter, TSSRContext, TFlags>, ClientType<TRouter>, TPath>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export type DeepOverride<T, R> = T extends Primitive
: R extends Primitive
? R
: {
[K in keyof T]: K extends keyof R ? DeepOverride<T[K], R[K]> : T[K];
} & {
[K in Exclude<keyof R, keyof T>]: R[K];
};
[K in keyof T]: K extends keyof R ? DeepOverride<T[K], R[K]> : T[K];
} & {
[K in Exclude<keyof R, keyof T>]: R[K];
};

/**
* Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there,
Expand All @@ -25,8 +25,8 @@ export type DeepOverrideAtPath<T, R, Path extends string | undefined = undefined
? DeepOverride<T, R>
: Path extends `${infer P1}.${infer P2}`
? P1 extends keyof T
? Omit<T, P1> & Record<P1, DeepOverride<T[P1], DeepOverrideAtPath<T[P1], R, P2>>>
: never
? Omit<T, P1> & Record<P1, DeepOverride<T[P1], DeepOverrideAtPath<T[P1], R, P2>>>
: never
: Path extends keyof T
? Omit<T, Path> & Record<Path, DeepOverride<T[Path], R>>
: never;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function checkMutate<T>(promise: Promise<T>): Promise<T | undefined
throw err;
}
}

}

export async function checkRead<T>(promise: Promise<T>): Promise<T> {
Expand Down Expand Up @@ -58,10 +59,11 @@ export async function checkRead<T>(promise: Promise<T>): Promise<T> {
code: 'BAD_REQUEST',
message: err.message,
cause: err,
});
})
}
} else {
throw err;
}
}

}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
/* eslint-disable */
import {
unsetMarker,
type AnyRouter,
type AnyRootConfig,
type CreateRouterInner,
type Procedure,
type ProcedureBuilder,
type ProcedureParams,
type ProcedureRouterRecord,
type ProcedureType,
} from '@trpc/server';
import { type PrismaClient } from '@prisma/client';
import createUserRouter from './User.router';
import createPostRouter from './Post.router';
import { ClientType as UserClientType } from './User.router';
import { ClientType as PostClientType } from './Post.router';
import { unsetMarker, type AnyRouter, type AnyRootConfig, type CreateRouterInner, type Procedure, type ProcedureBuilder, type ProcedureParams, type ProcedureRouterRecord, type ProcedureType } from "@trpc/server";
import { type PrismaClient } from "@prisma/client";
import createUserRouter from "./User.router";
import createPostRouter from "./Post.router";
import { ClientType as UserClientType } from "./User.router";
import { ClientType as PostClientType } from "./Post.router";

export type BaseConfig = AnyRootConfig;

export type RouterFactory<Config extends BaseConfig> = <ProcRouterRecord extends ProcedureRouterRecord>(
procedures: ProcRouterRecord,
export type RouterFactory<Config extends BaseConfig> = <
ProcRouterRecord extends ProcedureRouterRecord
>(
procedures: ProcRouterRecord
) => CreateRouterInner<Config, ProcRouterRecord>;

export type UnsetMarker = typeof unsetMarker;
Expand All @@ -39,7 +31,8 @@ export function createRouter<Config extends BaseConfig>(router: RouterFactory<Co
return router({
user: createUserRouter(router, procedure),
post: createPostRouter(router, procedure),
});
}
);
}

export interface ClientType<AppRouter extends AnyRouter> {
Expand Down
5 changes: 4 additions & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
"require": "./cross/index.js",
"default": "./cross/index.js"
},
"./model-meta": {
"types": "./model-meta.d.ts",
"default": "./model-meta.js"
},
"./prisma": {
"types": "./prisma.d.ts"
}
Expand All @@ -59,7 +63,6 @@
"bcryptjs": "^2.4.3",
"buffer": "^6.0.3",
"change-case": "^4.1.2",
"colors": "1.4.0",
"decimal.js": "^10.4.2",
"deepcopy": "^2.1.0",
"deepmerge": "^4.3.1",
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/res/model-meta.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '.zenstack/model-meta';
10 changes: 10 additions & 0 deletions packages/runtime/res/model-meta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });

try {
exports.default = require('.zenstack/model-meta').default;
ymc9 marked this conversation as resolved.
Show resolved Hide resolved
} catch {
exports.default = function () {
throw new Error('Generated model meta not found. Please run `zenstack generate` first.');
};
}
35 changes: 11 additions & 24 deletions packages/runtime/src/enhancements/create-enhancement.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import colors from 'colors';
import semver from 'semver';
import { PRISMA_MINIMUM_VERSION } from '../constants';
import { isDelegateModel, type ModelMeta } from '../cross';
import type { AuthUser } from '../types';
import { withDefaultAuth } from './default-auth';
import { withDelegate } from './delegate';
import { Logger } from './logger';
import { withOmit } from './omit';
import { withPassword } from './password';
import { withPolicy } from './policy';
Expand Down Expand Up @@ -98,10 +98,6 @@ export type EnhancementContext<User extends AuthUser = AuthUser> = {
user?: User;
};

let hasPassword: boolean | undefined = undefined;
let hasOmit: boolean | undefined = undefined;
let hasDefaultAuth: boolean | undefined = undefined;

/**
* Gets a Prisma client enhanced with all enhancement behaviors, including access
* policy, field validation, field omission and password hashing.
Expand Down Expand Up @@ -129,32 +125,23 @@ export function createEnhancement<DbClient extends object>(
);
}

let result = prisma;

if (
process.env.ZENSTACK_TEST === '1' || // avoid caching in tests
hasPassword === undefined ||
hasOmit === undefined ||
hasDefaultAuth === undefined
) {
const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) =>
Object.values(modelInfo.fields)
);
hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password'));
hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit'));
hasDefaultAuth = allFields.some((field) => field.defaultValueProvider);
}
// TODO: move the detection logic into each enhancement
// TODO: how to properly cache the detection result?
const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => Object.values(modelInfo.fields));
const hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password'));
const hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit'));
const hasDefaultAuth = allFields.some((field) => field.defaultValueProvider);

const kinds = options.kinds ?? ALL_ENHANCEMENTS;
let result = prisma;

// delegate proxy needs to be wrapped inside policy proxy, since it may translate `deleteMany`
// and `updateMany` to plain `delete` and `update`
if (Object.values(options.modelMeta.models).some((model) => isDelegateModel(options.modelMeta, model.name))) {
if (!kinds.includes('delegate')) {
console.warn(
colors.yellow(
'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.'
)
const logger = new Logger(prisma);
logger.warn(
'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.'
);
} else {
result = withDelegate(result, options);
Expand Down
10 changes: 9 additions & 1 deletion packages/runtime/src/enhancements/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ export class Logger {
const engine = (this.prisma as any)._engine;
this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined;
if (this.emitter) {
this.eventNames = this.emitter.eventNames();
if (typeof this.emitter.eventNames === 'function') {
// Node.js
this.eventNames = this.emitter.eventNames();
} else if ('events' in this.emitter && this.emitter.events && typeof this.emitter.events === 'object') {
// edge runtime
this.eventNames = Object.keys((this.emitter as any).events);
} else {
this.eventNames = [];
}
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/runtime/src/enhancements/password.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */

import { hash } from 'bcryptjs';
import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants';
import { NestedWriteVisitor, type PrismaWriteActionType } from '../cross';
import { DbClientContract } from '../types';
Expand All @@ -25,6 +24,14 @@ export function withPassword<DbClient extends object = any>(
);
}

// `bcryptjs.hash` is good for performance but it doesn't work in edge runtime,
// so we fall back to `bcrypt.hash` in that case.

// eslint-disable-next-line no-var
declare var EdgeRuntime: any;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const hashFunc = typeof EdgeRuntime === 'string' ? require('bcryptjs').hashSync : require('bcryptjs').hash;

class PasswordHandler extends DefaultPrismaProxyHandler {
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
super(prisma, model, options);
Expand Down Expand Up @@ -53,7 +60,7 @@ class PasswordHandler extends DefaultPrismaProxyHandler {
if (!salt) {
salt = DEFAULT_PASSWORD_SALT_LENGTH;
}
context.parent[field.name] = await hash(data, salt);
context.parent[field.name] = await hashFunc(data, salt);
}
},
});
Expand Down
3 changes: 1 addition & 2 deletions packages/runtime/src/enhancements/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as util from 'util';
import { FieldInfo, ModelMeta, resolveField } from '..';
import type { DbClientContract } from '../types';

/**
* Formats an object for pretty printing.
*/
export function formatObject(value: unknown) {
return util.formatWithOptions({ depth: 20 }, value);
return JSON.stringify(value, undefined, 2);
}
ymc9 marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
15 changes: 14 additions & 1 deletion packages/schema/src/plugins/plugin-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export function getNodeModulesFolder(startPath?: string): string | undefined {
*/
export function ensureDefaultOutputFolder(options: PluginRunnerOptions) {
const output = options.output ? path.resolve(options.output) : getDefaultOutputFolder();
if (output && !fs.existsSync(output)) {
if (output) {
if (fs.existsSync(output)) {
fs.rmSync(output, { recursive: true });
}
fs.mkdirSync(output, { recursive: true });
if (!options.output) {
const pkgJson = {
Expand All @@ -55,11 +58,21 @@ export function ensureDefaultOutputFolder(options: PluginRunnerOptions) {
types: './zod/objects/index.d.ts',
default: './zod/objects/index.js',
},
'./model-meta': {
types: './model-meta.d.ts',
default: './model-meta.js',
},
'./prisma': {
types: './prisma.d.ts',
},
},
};

for (const zodFolder of ['models', 'input', 'objects']) {
fs.mkdirSync(path.join(output, 'zod', zodFolder), { recursive: true });
fs.writeFileSync(path.join(output, 'zod', zodFolder, 'index.js'), '');
}

fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4));
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export abstract class APIHandlerBase {

constructor() {
try {
this.defaultModelMeta = getDefaultModelMeta(undefined);
this.defaultModelMeta = getDefaultModelMeta();
} catch {
// noop
}
Expand Down
Loading
Loading