Skip to content

Commit

Permalink
feat: add "loadPath" options to runtime API and server adapter options
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 committed Sep 18, 2023
1 parent e8f7a2d commit ffdc603
Show file tree
Hide file tree
Showing 32 changed files with 482 additions and 244 deletions.
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 {
loadPath = loadPath ? path.resolve(loadPath, 'model-meta') : `${DEFAULT_RUNTIME_LOAD_PATH}/model-meta`;
try {
// normal load
return require(loadPath).default;
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(loadPath)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', loadPath, '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.');
}
}

/**
* 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 {
loadPath = loadPath ? path.resolve(loadPath, 'policy') : `${DEFAULT_RUNTIME_LOAD_PATH}/policy`;
try {
return require(loadPath).default;
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(loadPath)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', loadPath, '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.'
);
}
}

/**
* 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 {
loadPath = loadPath ? path.resolve(loadPath, 'zod') : `${DEFAULT_RUNTIME_LOAD_PATH}/zod`;
try {
return require(loadPath);
} catch {
if (process.env.ZENSTACK_TEST === '1' && !path.isAbsolute(loadPath)) {
try {
// special handling for running as tests, try resolving relative to CWD
return require(path.join(process.cwd(), 'node_modules', loadPath, 'zod'));
} 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

0 comments on commit ffdc603

Please sign in to comment.