Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
"version": "2.5.1",
"version": "2.5.2",
"description": "",
"scripts": {
"build": "pnpm -r build",
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
}

group = "dev.zenstack"
version = "2.5.1"
version = "2.5.2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Changelog is severely outdated and missing version 2.5.2

The changelog has not been updated to include version 2.5.2 or any other 2.x versions. In fact, the most recent entry in the main CHANGELOG.md file is for version 0.5.0, dated 2022-12-15. This significant gap in documentation is a critical issue that needs immediate attention.

Action items:

  • Update the CHANGELOG.md file to include all major versions and changes from 0.5.0 up to the current 2.5.2 release.
  • Ensure that the changelog includes a detailed entry for version 2.5.2, highlighting all new features, improvements, and breaking changes.
  • Implement a process to consistently update the changelog with each new release to prevent this issue from recurring.
Analysis chain

LGTM!

The version update aligns with the PR objective of merging dev to main for v2.5.2.

Please verify that the changelog has been updated to reflect the changes included in this release.

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the changelog has been updated for version 2.5.2.

# Test: Search for the version in the changelog. Expect: At least one match.
rg --type markdown $'2\.5\.2' $(fd CHANGELOG.md)

Length of output: 433


Script:

#!/bin/bash
# Description: Search for different version formats in changelog files

# Search for "v2.5.2" (with 'v' prefix)
echo "Searching for 'v2.5.2':"
rg --type markdown 'v2\.5\.2' $(fd CHANGELOG.md)

# Search for "2.5.2" with surrounding characters
echo "Searching for '2.5.2' with surrounding characters:"
rg --type markdown '[^0-9]2\.5\.2[^0-9]' $(fd CHANGELOG.md)

# List the content of the main CHANGELOG.md file
echo "Content of the main CHANGELOG.md file:"
cat CHANGELOG.md

Length of output: 4639


repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/jetbrains/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jetbrains",
"version": "2.5.1",
"version": "2.5.2",
"displayName": "ZenStack JetBrains IDE Plugin",
"description": "ZenStack JetBrains IDE plugin",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
"version": "2.5.1",
"version": "2.5.2",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/misc/redwood/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/redwood",
"displayName": "ZenStack RedwoodJS Integration",
"version": "2.5.1",
"version": "2.5.2",
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/openapi/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/openapi",
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
"version": "2.5.1",
"version": "2.5.2",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/swr/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/swr",
"displayName": "ZenStack plugin for generating SWR hooks",
"version": "2.5.1",
"version": "2.5.2",
"description": "ZenStack plugin for generating SWR hooks",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/tanstack-query/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/tanstack-query",
"displayName": "ZenStack plugin for generating tanstack-query hooks",
"version": "2.5.1",
"version": "2.5.2",
"description": "ZenStack plugin for generating tanstack-query hooks",
"main": "index.js",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/trpc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/trpc",
"displayName": "ZenStack plugin for tRPC",
"version": "2.5.1",
"version": "2.5.2",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
"version": "2.5.1",
"version": "2.5.2",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -98,6 +98,7 @@
"semver": "^7.5.2",
"superjson": "^1.13.0",
"tiny-invariant": "^1.3.1",
"traverse": "^0.6.10",
"ts-pattern": "^4.3.0",
"tslib": "^2.4.1",
"upper-case-first": "^2.0.2",
Expand All @@ -118,6 +119,7 @@
"@types/pluralize": "^0.0.29",
"@types/safe-json-stringify": "^1.1.5",
"@types/semver": "^7.3.13",
"@types/traverse": "^0.6.37",
"@types/uuid": "^8.3.4"
}
}
75 changes: 66 additions & 9 deletions packages/runtime/src/enhancements/node/delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
import { lowerCaseFirst } from 'lower-case-first';
import traverse from 'traverse';
import { DELEGATE_AUX_RELATION_PREFIX } from '../../constants';
import {
FieldInfo,
Expand Down Expand Up @@ -77,6 +78,10 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
this.injectWhereHierarchy(model, args?.where);
this.injectSelectIncludeHierarchy(model, args);

// discriminator field is needed during post process to determine the
// actual concrete model type
this.ensureDiscriminatorSelection(model, args);

if (args.orderBy) {
// `orderBy` may contain fields from base types
this.injectWhereHierarchy(this.model, args.orderBy);
Expand All @@ -94,6 +99,23 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
}

private ensureDiscriminatorSelection(model: string, args: any) {
const modelInfo = getModelInfo(this.options.modelMeta, model);
if (!modelInfo?.discriminator) {
return;
}

if (args.select && typeof args.select === 'object') {
args.select[modelInfo.discriminator] = true;
return;
}

if (args.omit && typeof args.omit === 'object') {
args.omit[modelInfo.discriminator] = false;
return;
}
}
Comment on lines +111 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify the usage of args.omit in ensureDiscriminatorSelection

It appears that args.omit is being checked and modified, but omit is not a standard parameter in Prisma query arguments. Typically, Prisma uses select and include for field selection. Please ensure that omit is a valid and supported parameter in this context.


private injectWhereHierarchy(model: string, where: any) {
if (!where || !isPlainObject(where)) {
return;
Expand Down Expand Up @@ -168,16 +190,20 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
}

if (value !== undefined) {
if (value?.orderBy) {
// refetch the field select/include value because it may have been
// updated during injection
const fieldValue = args[kind][field];

if (fieldValue !== undefined) {
if (fieldValue.orderBy) {
// `orderBy` may contain fields from base types
this.injectWhereHierarchy(fieldInfo.type, value.orderBy);
this.injectWhereHierarchy(fieldInfo.type, fieldValue.orderBy);
}

if (this.injectBaseFieldSelect(model, field, value, args, kind)) {
if (this.injectBaseFieldSelect(model, field, fieldValue, args, kind)) {
delete args[kind][field];
} else if (fieldInfo.isDataModel) {
let nextValue = value;
let nextValue = fieldValue;
if (nextValue === true) {
// make sure the payload is an object
args[kind][field] = nextValue = {};
Expand Down Expand Up @@ -333,6 +359,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (isDelegateModel(this.options.modelMeta, this.model)) {
throw prismaClientValidationError(
this.prisma,
Expand All @@ -348,6 +376,24 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
return this.doCreate(this.prisma, this.model, args);
}

private sanitizeMutationPayload(data: any) {
if (!data) {
return;
}

const prisma = this.prisma;
const prismaModule = this.options.prismaModule;
traverse(data).forEach(function () {
if (this.key?.startsWith(DELEGATE_AUX_RELATION_PREFIX)) {
throw prismaClientValidationError(
prisma,
prismaModule,
`Auxiliary relation field "${this.key}" cannot be set directly`
);
}
});
}

override createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> {
if (!args) {
throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required');
Expand All @@ -360,6 +406,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.createMany(args);
}
Expand Down Expand Up @@ -399,6 +447,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.createManyAndReturn(args);
}
Expand Down Expand Up @@ -585,6 +635,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.update(args);
}
Expand All @@ -604,6 +656,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.updateMany(args);
}
Expand Down Expand Up @@ -631,6 +685,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.update);
this.sanitizeMutationPayload(args.create);

if (isDelegateModel(this.options.modelMeta, this.model)) {
throw prismaClientValidationError(
this.prisma,
Expand Down Expand Up @@ -1158,11 +1215,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
const base = this.getBaseModel(model);

if (base) {
// merge base fields
// fully merge base fields
const baseRelationName = this.makeAuxRelationName(base);
const baseData = entity[baseRelationName];
if (baseData && typeof baseData === 'object') {
const baseAssembled = this.assembleUp(base.name, baseData);
const baseAssembled = this.assembleHierarchy(base.name, baseData);
Object.assign(result, baseAssembled);
}
}
Expand Down Expand Up @@ -1209,14 +1266,14 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
const modelInfo = getModelInfo(this.options.modelMeta, model, true);

if (modelInfo.discriminator) {
// model is a delegate, merge sub model fields
// model is a delegate, fully merge concrete model fields
const subModelName = entity[modelInfo.discriminator];
if (subModelName) {
const subModel = getModelInfo(this.options.modelMeta, subModelName, true);
const subRelationName = this.makeAuxRelationName(subModel);
const subData = entity[subRelationName];
if (subData && typeof subData === 'object') {
const subAssembled = this.assembleDown(subModel.name, subData);
const subAssembled = this.assembleHierarchy(subModel.name, subData);
Object.assign(result, subAssembled);
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/runtime/src/enhancements/node/omit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { enumerate, getModelFields, resolveField } from '../../cross';
import { DbClientContract } from '../../types';
import { InternalEnhancementOptions } from './create-enhancement';
import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
import { QueryUtils } from './query-utils';

/**
* Gets an enhanced Prisma client that supports `@omit` attribute.
Expand All @@ -21,8 +22,11 @@ export function withOmit<DbClient extends object>(prisma: DbClient, options: Int
}

class OmitHandler extends DefaultPrismaProxyHandler {
private queryUtils: QueryUtils;

constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
super(prisma, model, options);
this.queryUtils = new QueryUtils(prisma, options);
}

// base override
Expand Down Expand Up @@ -67,8 +71,10 @@ class OmitHandler extends DefaultPrismaProxyHandler {
}

private async doPostProcess(entityData: any, model: string) {
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);

for (const field of getModelFields(entityData)) {
const fieldInfo = await resolveField(this.options.modelMeta, model, field);
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
Comment on lines +74 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure realModel is valid before proceeding

When calling this.queryUtils.getDelegateConcreteModel(model, entityData);, there is a potential for realModel to be undefined or null. This could lead to errors when resolveField is called with an invalid realModel. Consider adding a check to verify that realModel is valid before proceeding.

Apply this diff to add a validation check:

private async doPostProcess(entityData: any, model: string) {
    const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);

+   if (!realModel) {
+       // Handle the error appropriately
+       return;
+   }

    for (const field of getModelFields(entityData)) {
        const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
for (const field of getModelFields(entityData)) {
const fieldInfo = await resolveField(this.options.modelMeta, model, field);
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
if (!realModel) {
// Handle the error appropriately
return;
}
for (const field of getModelFields(entityData)) {
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);

if (!fieldInfo) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,10 @@ export class PolicyUtil extends QueryUtils {
// preserve the original data as it may be needed for checking field-level readability,
// while the "data" will be manipulated during traversal (deleting unreadable fields)
const origData = this.safeClone(data);
return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));

// use the concrete model if the data is a polymorphic entity
const realModel = this.getDelegateConcreteModel(model, data);
return this.doPostProcessForRead(data, realModel, origData, queryArgs, this.hasFieldLevelPolicy(realModel));
}

private doPostProcessForRead(
Expand Down
18 changes: 18 additions & 0 deletions packages/runtime/src/enhancements/node/query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,22 @@ export class QueryUtils {
safeClone(value: unknown): any {
return value ? clone(value) : value === undefined || value === null ? {} : value;
}

getDelegateConcreteModel(model: string, data: any) {
if (!data || typeof data !== 'object') {
return model;
}
Comment on lines +219 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure data is not an array to prevent unexpected behavior

Currently, the condition if (!data || typeof data !== 'object') does not exclude arrays since typeof [] === 'object'. If data is expected to be a plain object, consider adding a check to exclude arrays:

 if (!data || typeof data !== 'object' || Array.isArray(data)) {
     return model;
 }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!data || typeof data !== 'object') {
return model;
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return model;
}


const modelInfo = getModelInfo(this.options.modelMeta, model);
if (modelInfo?.discriminator) {
Comment on lines +223 to +224
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential undefined from getModelInfo

getModelInfo might return undefined if the model is not found in this.options.modelMeta. Accessing modelInfo.discriminator without verifying modelInfo could lead to a runtime error. Consider adding a null check for modelInfo:

 const modelInfo = getModelInfo(this.options.modelMeta, model);
-if (modelInfo?.discriminator) {
+if (modelInfo && modelInfo.discriminator) {
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const modelInfo = getModelInfo(this.options.modelMeta, model);
if (modelInfo?.discriminator) {
const modelInfo = getModelInfo(this.options.modelMeta, model);
if (modelInfo && modelInfo.discriminator) {

// model has a discriminator so it can be a polymorphic base,
// need to find the concrete model
const concreteModelName = data[modelInfo.discriminator];
if (concreteModelName) {
return concreteModelName;
}
}

return model;
}
}
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI",
"version": "2.5.1",
"version": "2.5.2",
"author": {
"name": "ZenStack Team"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/language-server/zmodel-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
private createScopeForContainingModel(node: AstNode, globalScope: Scope) {
const model = getContainerOfType(node, isDataModel);
if (model) {
return this.createScopeForNodes(model.fields, globalScope);
return this.createScopeForModel(model, globalScope);
} else {
return EMPTY_SCOPE;
}
Expand Down
Loading