Skip to content

feat: add "loadPath" options to runtime API and server adapter options #696

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

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/plugins/swr/tests/swr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ ${sharedModel}
{
provider: 'postgresql',
pushDb: false,
extraDependencies: [`${origDir}/dist`, 'react', '@types/react', 'swr'],
extraDependencies: [`${origDir}/dist`, 'react@18.2.0', '@types/react@18.2.0', 'swr@^2'],
compile: true,
}
);
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Default path for loading CLI-generated code
*/
export const DEFAULT_RUNTIME_LOAD_PATH = '.zenstack';

/**
* Default length of password hash salt (used by bcryptjs to hash password)
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/enhancements/enhance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDefaultModelMeta } from './model-meta';
import { getDefaultModelMeta } from '../loader';
import { withOmit, WithOmitOptions } from './omit';
import { withPassword, WithPasswordOptions } from './password';
import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy';
Expand Down Expand Up @@ -29,7 +29,7 @@ export function enhance<DbClient extends object>(
let result = prisma;

if (hasPassword === undefined || hasOmit === undefined) {
const modelMeta = options?.modelMeta ?? getDefaultModelMeta();
const modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath);
const allFields = Object.values(modelMeta.fields).flatMap((modelInfo) => Object.values(modelInfo));
hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password'));
hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit'));
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/enhancements/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './model-meta';
export * from './nested-write-vistor';
export * from './nested-write-visitor';
export * from './omit';
export * from './password';
export * from './policy';
Expand Down
21 changes: 0 additions & 21 deletions packages/runtime/src/enhancements/model-meta.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,8 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { lowerCaseFirst } from 'lower-case-first';
import path from 'path';
import { FieldInfo } from '../types';
import { ModelMeta } from './types';

/**
* Load model meta from standard location.
*/
export function getDefaultModelMeta(): ModelMeta {
try {
// normal load
return require('.zenstack/model-meta').default;
} catch {
if (process.env.ZENSTACK_TEST === '1') {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'model-meta')).default;
} catch {
throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.');
}
}
throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.');
}
}

/**
* Resolves a model field to its metadata. Returns undefined if not found.
*/
Expand Down
11 changes: 6 additions & 5 deletions packages/runtime/src/enhancements/omit.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */

import { getDefaultModelMeta } from '../loader';
import { DbClientContract } from '../types';
import { getDefaultModelMeta, resolveField } from './model-meta';
import { resolveField } from './model-meta';
import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
import { ModelMeta } from './types';
import { CommonEnhancementOptions, ModelMeta } from './types';
import { enumerate, getModelFields } from './utils';

/**
* Options for @see withOmit
*/
export type WithOmitOptions = {
export interface WithOmitOptions extends CommonEnhancementOptions {
/**
* Model metadata
*/
modelMeta?: ModelMeta;
};
}

/**
* Gets an enhanced Prisma client that supports @omit attribute.
*/
export function withOmit<DbClient extends object>(prisma: DbClient, options?: WithOmitOptions): DbClient {
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath);
return makeProxy(
prisma,
_modelMeta,
Expand Down
14 changes: 7 additions & 7 deletions packages/runtime/src/enhancements/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@

import { hash } from 'bcryptjs';
import { DEFAULT_PASSWORD_SALT_LENGTH } from '../constants';
import { getDefaultModelMeta } from '../loader';
import { DbClientContract, PrismaWriteActionType } from '../types';
import { getDefaultModelMeta } from './model-meta';
import { NestedWriteVisitor } from './nested-write-vistor';
import { NestedWriteVisitor } from './nested-write-visitor';
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
import { ModelMeta } from './types';
import { CommonEnhancementOptions, ModelMeta } from './types';

/**
* Options for @see withPassword
*/
export type WithPasswordOptions = {
export interface WithPasswordOptions extends CommonEnhancementOptions {
/**
* Model metatadata
* Model metadata
*/
modelMeta?: ModelMeta;
};
}

/**
* Gets an enhanced Prisma client that supports @password attribute.
*/
export function withPassword<DbClient extends object = any>(prisma: DbClient, options?: WithPasswordOptions): DbClient {
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath);
return makeProxy(
prisma,
_modelMeta,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CrudFailureReason, PRISMA_TX_FLAG } from '../../constants';
import { AuthUser, DbClientContract, DbOperations, FieldInfo, PolicyOperationKind } from '../../types';
import { ModelDataVisitor } from '../model-data-visitor';
import { resolveField } from '../model-meta';
import { NestedWriteVisitor, NestedWriteVisitorContext } from '../nested-write-vistor';
import { NestedWriteVisitor, NestedWriteVisitorContext } from '../nested-write-visitor';
import { PrismaProxyHandler } from '../proxy';
import type { ModelMeta, PolicyDef, ZodSchemas } from '../types';
import { enumerate, formatObject, getIdFields, prismaClientValidationError } from '../utils';
Expand Down
52 changes: 7 additions & 45 deletions packages/runtime/src/enhancements/policy/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-explicit-any */

import path from 'path';
import semver from 'semver';
import { PRISMA_MINIMUM_VERSION } from '../../constants';
import { getDefaultModelMeta, getDefaultPolicy, getDefaultZodSchemas } from '../../loader';
import { AuthUser, DbClientContract } from '../../types';
import { hasAllFields } from '../../validation';
import { getDefaultModelMeta } from '../model-meta';
import { makeProxy } from '../proxy';
import type { ModelMeta, PolicyDef, ZodSchemas } from '../types';
import type { CommonEnhancementOptions, ModelMeta, PolicyDef, ZodSchemas } from '../types';
import { getIdFields } from '../utils';
import { PolicyProxyHandler } from './handler';

Expand All @@ -22,7 +21,7 @@ export type WithPolicyContext = {
/**
* Options for @see withPolicy
*/
export type WithPolicyOptions = {
export interface WithPolicyOptions extends CommonEnhancementOptions {
/**
* Policy definition
*/
Expand All @@ -42,7 +41,7 @@ export type WithPolicyOptions = {
* Whether to log Prisma query
*/
logPrismaQuery?: boolean;
};
}

/**
* Gets an enhanced Prisma client with access policy check.
Expand All @@ -68,9 +67,9 @@ export function withPolicy<DbClient extends object>(
);
}

const _policy = options?.policy ?? getDefaultPolicy();
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta();
const _zodSchemas = options?.zodSchemas ?? getDefaultZodSchemas();
const _policy = options?.policy ?? getDefaultPolicy(options?.loadPath);
const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(options?.loadPath);
const _zodSchemas = options?.zodSchemas ?? getDefaultZodSchemas(options?.loadPath);

// validate user context
if (context?.user) {
Expand Down Expand Up @@ -103,40 +102,3 @@ export function withPolicy<DbClient extends object>(
'policy'
);
}

function getDefaultPolicy(): PolicyDef {
try {
return require('.zenstack/policy').default;
} catch {
if (process.env.ZENSTACK_TEST === '1') {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'policy')).default;
} catch {
throw new Error(
'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.'
);
}
}
throw new Error(
'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.'
);
}
}

function getDefaultZodSchemas(): ZodSchemas | undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('.zenstack/zod');
} catch {
if (process.env.ZENSTACK_TEST === '1') {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', '.zenstack', 'zod'));
} catch {
return undefined;
}
}
return undefined;
}
}
2 changes: 1 addition & 1 deletion packages/runtime/src/enhancements/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { AuthUser, DbClientContract, DbOperations, FieldInfo, PolicyOperationKind } from '../../types';
import { getVersion } from '../../version';
import { getFields, resolveField } from '../model-meta';
import { NestedWriteVisitorContext } from '../nested-write-vistor';
import { NestedWriteVisitorContext } from '../nested-write-visitor';
import type { InputCheckFunc, ModelMeta, PolicyDef, ReadFieldCheckFunc, ZodSchemas } from '../types';
import {
enumerate,
Expand Down
10 changes: 10 additions & 0 deletions packages/runtime/src/enhancements/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import {
HAS_FIELD_LEVEL_POLICY_FLAG,
} from '../constants';

/**
* Common options for PrismaClient enhancements
*/
export interface CommonEnhancementOptions {
/**
* Path for loading CLI-generated code
*/
loadPath?: string;
}

/**
* Metadata for a model-level unique constraint
* e.g.: @@unique([a, b])
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './constants';
export * from './enhancements';
export * from './error';
export * from './loader';
export * from './types';
export * from './validation';
export * from './version';
78 changes: 78 additions & 0 deletions packages/runtime/src/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import path from 'path';
import { DEFAULT_RUNTIME_LOAD_PATH } from './constants';
import { ModelMeta, PolicyDef, ZodSchemas } from './enhancements';

/**
* Load model metadata.
*
* @param loadPath The path to load model metadata from. If not provided,
* will use default load path.
*/
export function getDefaultModelMeta(loadPath: string | undefined): ModelMeta {
const toLoad = loadPath ? path.resolve(loadPath, 'model-meta') : `${DEFAULT_RUNTIME_LOAD_PATH}/model-meta`;
try {
// normal load
return require(toLoad).default;
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(toLoad)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', toLoad)).default;
} catch {
throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.');
}
}
throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.');
}
}

/**
* Load access policies.
*
* @param loadPath The path to load access policies from. If not provided,
* will use default load path.
*/
export function getDefaultPolicy(loadPath: string | undefined): PolicyDef {
const toLoad = loadPath ? path.resolve(loadPath, 'policy') : `${DEFAULT_RUNTIME_LOAD_PATH}/policy`;
try {
return require(toLoad).default;
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(toLoad)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', toLoad)).default;
} catch {
throw new Error(
'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.'
);
}
}
throw new Error(
'Policy definition cannot be loaded from default location. Please make sure "zenstack generate" has been run.'
);
}
}

/**
* Load zod schemas.
*
* @param loadPath The path to load zod schemas from. If not provided,
* will use default load path.
*/
export function getDefaultZodSchemas(loadPath: string | undefined): ZodSchemas | undefined {
const toLoad = loadPath ? path.resolve(loadPath, 'zod') : `${DEFAULT_RUNTIME_LOAD_PATH}/zod`;
try {
return require(toLoad);
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(toLoad)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', toLoad));
} catch {
return undefined;
}
}
return undefined;
}
}
4 changes: 2 additions & 2 deletions packages/schema/src/plugins/plugin-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PolicyOperationKind } from '@zenstackhq/runtime';
import { DEFAULT_RUNTIME_LOAD_PATH, type PolicyOperationKind } from '@zenstackhq/runtime';
import { PluginGlobalOptions } from '@zenstackhq/sdk';
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -73,5 +73,5 @@ export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) {
runtimeModulePath = path.join(runtimeModulePath, '..');
}
const modulesFolder = getNodeModulesFolder(runtimeModulePath);
return modulesFolder ? path.join(modulesFolder, '.zenstack') : undefined;
return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined;
}
Loading