Skip to content
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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "ZenStack",
"packageManager": "pnpm@10.12.1",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/config/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"private": true,
"license": "MIT"
}
2 changes: 1 addition & 1 deletion packages/config/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/policy/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/plugin-policy",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "ZenStack Policy Plugin",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/runtime",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.12",
"description": "ZenStack Runtime",
"type": "module",
"scripts": {
Expand Down
102 changes: 77 additions & 25 deletions packages/runtime/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UpdateResult,
type Compilable,
type IsolationLevel,
type QueryResult,
type SelectQueryBuilder,
} from 'kysely';
import { nanoid } from 'nanoid';
Expand Down Expand Up @@ -248,6 +249,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
data: any,
fromRelation?: FromRelationContext<Schema>,
creatingForDelegate = false,
returnFields?: string[],
): Promise<unknown> {
const modelDef = this.requireModel(model);

Expand Down Expand Up @@ -339,12 +341,15 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

const updatedData = this.fillGeneratedAndDefaultValues(modelDef, createFields);
const idFields = requireIdFields(this.schema, model);

// return id fields if no returnFields specified
returnFields = returnFields ?? requireIdFields(this.schema, model);

const query = kysely
.insertInto(model)
.$if(Object.keys(updatedData).length === 0, (qb) => qb.defaultValues())
.$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData))
.returning(idFields as any)
.returning(returnFields as any)
.modifyEnd(
this.makeContextComment({
model,
Expand Down Expand Up @@ -661,6 +666,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
input: { data: any; skipDuplicates?: boolean },
returnData: ReturnData,
fromRelation?: FromRelationContext<Schema>,
fieldsToReturn?: string[],
): Promise<Result> {
if (!input.data || (Array.isArray(input.data) && input.data.length === 0)) {
// nothing todo
Expand Down Expand Up @@ -763,8 +769,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const result = await this.executeQuery(kysely, query, 'createMany');
return { count: Number(result.numAffectedRows) } as Result;
} else {
const idFields = requireIdFields(this.schema, model);
const result = await query.returning(idFields as any).execute();
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
const result = await query.returning(fieldsToReturn as any).execute();
return result as Result;
}
}
Expand Down Expand Up @@ -899,6 +905,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
fromRelation?: FromRelationContext<Schema>,
allowRelationUpdate = true,
throwIfNotFound = true,
fieldsToReturn?: string[],
): Promise<unknown> {
if (!data || typeof data !== 'object') {
throw new InternalError('data must be an object');
Expand Down Expand Up @@ -1044,12 +1051,12 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
// nothing to update, return the filter so that the caller can identify the entity
return combinedWhere;
} else {
const idFields = requireIdFields(this.schema, model);
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
const query = kysely
.updateTable(model)
.where(() => this.dialect.buildFilter(model, model, combinedWhere))
.set(updateFields)
.returning(idFields as any)
.returning(fieldsToReturn as any)
.modifyEnd(
this.makeContextComment({
model,
Expand All @@ -1058,16 +1065,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
);

const updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update');

// try {
// updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update');
// } catch (err) {
// const { sql, parameters } = query.compile();
// throw new QueryError(
// `Error during update: ${err}, sql: ${sql}, parameters: ${parameters}`
// );
// }

if (!updatedEntity) {
if (throwIfNotFound) {
throw new NotFoundError(model);
Expand Down Expand Up @@ -1214,6 +1211,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
limit: number | undefined,
returnData: ReturnData,
filterModel?: GetModels<Schema>,
fieldsToReturn?: string[],
): Promise<Result> {
if (typeof data !== 'object') {
throw new InternalError('data must be an object');
Expand Down Expand Up @@ -1302,8 +1300,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const result = await this.executeQuery(kysely, query, 'update');
return { count: Number(result.numAffectedRows) } as Result;
} else {
const idFields = requireIdFields(this.schema, model);
const finalQuery = query.returning(idFields as any);
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
const finalQuery = query.returning(fieldsToReturn as any);
const result = await this.executeQuery(kysely, finalQuery, 'update');
return result.rows as Result;
}
Expand Down Expand Up @@ -1861,7 +1859,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
expectedDeleteCount = deleteConditions.length;
}

let deleteResult: { count: number };
let deleteResult: QueryResult<unknown>;
let deleteFromModel: GetModels<Schema>;
const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field);

Expand Down Expand Up @@ -1926,7 +1924,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

// validate result
if (throwForNotFound && expectedDeleteCount > deleteResult.count) {
if (throwForNotFound && expectedDeleteCount > deleteResult.rows.length) {
// some entities were not deleted
throw new NotFoundError(deleteFromModel);
}
Expand All @@ -1944,7 +1942,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
where: any,
limit?: number,
filterModel?: GetModels<Schema>,
): Promise<{ count: number }> {
fieldsToReturn?: string[],
): Promise<QueryResult<unknown>> {
filterModel ??= model;

const modelDef = this.requireModel(model);
Expand All @@ -1957,7 +1956,9 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
return this.processBaseModelDelete(kysely, modelDef.baseModel, where, limit, filterModel);
}

let query = kysely.deleteFrom(model);
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
let query = kysely.deleteFrom(model).returning(fieldsToReturn as any);

let needIdFilter = false;

if (limit !== undefined && !this.dialect.supportsDeleteWithLimit) {
Expand Down Expand Up @@ -1999,8 +2000,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
await this.processDelegateRelationDelete(kysely, modelDef, where, limit);

query = query.modifyEnd(this.makeContextComment({ model, operation: 'delete' }));
const result = await this.executeQuery(kysely, query, 'delete');
return { count: Number(result.numAffectedRows) };
return this.executeQuery(kysely, query, 'delete');
}

private async processDelegateRelationDelete(
Expand Down Expand Up @@ -2083,7 +2083,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
} else {
// otherwise, create a new transaction and execute the callback
let txBuilder = this.kysely.transaction();
txBuilder = txBuilder.setIsolationLevel(isolationLevel ?? TransactionIsolationLevel.RepeatableRead);
txBuilder = txBuilder.setIsolationLevel(isolationLevel ?? TransactionIsolationLevel.ReadCommitted);
return txBuilder.execute(callback);
}
}
Expand Down Expand Up @@ -2140,4 +2140,56 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}
return result.rows[0];
}

protected mutationNeedsReadBack(model: string, args: any) {
if (this.hasPolicyEnabled) {
// TODO: refactor this check
// policy enforcement always requires read back
return { needReadBack: true, selectedFields: undefined };
}

if (args.include && typeof args.include === 'object' && Object.keys(args.include).length > 0) {
// includes present, need read back to fetch relations
return { needReadBack: true, selectedFields: undefined };
}

const modelDef = this.requireModel(model);

if (modelDef.baseModel || modelDef.isDelegate) {
// polymorphic model, need read back
return { needReadBack: true, selectedFields: undefined };
}

const allFields = Object.keys(modelDef.fields);
const relationFields = Object.values(modelDef.fields)
.filter((f) => f.relation)
.map((f) => f.name);
const computedFields = Object.values(modelDef.fields)
.filter((f) => f.computed)
.map((f) => f.name);
const omit = Object.entries(args.omit ?? {})
.filter(([, v]) => v)
.map(([k]) => k);

const allFieldsSelected: string[] = [];

if (!args.select || typeof args.select !== 'object') {
// all non-relation fields selected
allFieldsSelected.push(...allFields.filter((f) => !relationFields.includes(f) && !omit.includes(f)));
} else {
// explicit select
allFieldsSelected.push(
...Object.entries(args.select)
.filter(([k, v]) => v && !omit.includes(k))
.map(([k]) => k),
);
}

if (allFieldsSelected.some((f) => relationFields.includes(f) || computedFields.includes(f))) {
// relation or computed field selected, need read back
return { needReadBack: true, selectedFields: undefined };
} else {
return { needReadBack: false, selectedFields: allFieldsSelected };
}
}
}
54 changes: 35 additions & 19 deletions packages/runtime/src/client/crud/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,27 @@ export class CreateOperationHandler<Schema extends SchemaDef> extends BaseOperat
}

private async runCreate(args: CreateArgs<Schema, GetModels<Schema>>) {
// analyze if we need to read back the created record, or just return the create result
const { needReadBack, selectedFields } = this.mutationNeedsReadBack(this.model, args);

// TODO: avoid using transaction for simple create
const result = await this.safeTransaction(async (tx) => {
const createResult = await this.create(tx, this.model, args.data);
return this.readUnique(tx, this.model, {
select: args.select,
include: args.include,
omit: args.omit,
where: getIdValues(this.schema, this.model, createResult) as WhereInput<
Schema,
GetModels<Schema>,
false
>,
});
const createResult = await this.create(tx, this.model, args.data, undefined, false, selectedFields);

if (needReadBack) {
return this.readUnique(tx, this.model, {
select: args.select,
include: args.include,
omit: args.omit,
where: getIdValues(this.schema, this.model, createResult) as WhereInput<
Schema,
GetModels<Schema>,
false
>,
});
} else {
return createResult;
}
});

if (!result && this.hasPolicyEnabled) {
Expand All @@ -62,16 +70,24 @@ export class CreateOperationHandler<Schema extends SchemaDef> extends BaseOperat
return [];
}

// analyze if we need to read back the created record, or just return the create result
const { needReadBack, selectedFields } = this.mutationNeedsReadBack(this.model, args);

// TODO: avoid using transaction for simple create
return this.safeTransaction(async (tx) => {
const createResult = await this.createMany(tx, this.model, args, true);
return this.read(tx, this.model, {
select: args.select,
omit: args.omit,
where: {
OR: createResult.map((item) => getIdValues(this.schema, this.model, item) as any),
} as any, // TODO: fix type
});
const createResult = await this.createMany(tx, this.model, args, true, undefined, selectedFields);

if (needReadBack) {
return this.read(tx, this.model, {
select: args.select,
omit: args.omit,
where: {
OR: createResult.map((item) => getIdValues(this.schema, this.model, item) as any),
} as any, // TODO: fix type
});
} else {
return createResult;
}
});
}
}
Loading
Loading