Skip to content
Open
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
21 changes: 11 additions & 10 deletions packages/clients/tanstack-query/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { FetchFn } from '@zenstackhq/client-helpers/fetch';
import type {
GetProcedureNames,
GetSlicedOperations,
OperationsIneligibleForDelegateModels,
ModelAllowsCreate,
OperationsRequiringCreate,
ProcedureFunc,
QueryOptions,
} from '@zenstackhq/orm';
import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema';
import type { GetModels, SchemaDef } from '@zenstackhq/schema';

/**
* Context type for configuring the hooks.
Expand Down Expand Up @@ -59,8 +60,8 @@ export type ExtraMutationOptions = {
optimisticDataProvider?: OptimisticDataProvider;
} & QueryContext;

type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any
? `use${Capitalize<OperationsIneligibleForDelegateModels>}`
type HooksOperationsRequiringCreate = OperationsRequiringCreate extends any
? `use${Capitalize<OperationsRequiringCreate>}`
: never;

type Modifiers = '' | 'Suspense' | 'Infinite' | 'SuspenseInfinite';
Expand All @@ -76,12 +77,12 @@ export type TrimSlicedOperations<
> = {
// trim operations based on slicing options
[Key in keyof T as Key extends `use${Modifiers}${Capitalize<GetSlicedOperations<Schema, Model, Options>>}`
? IsDelegateModel<Schema, Model> extends true
? // trim operations ineligible for delegate models
Key extends HooksOperationsIneligibleForDelegateModels
? never
: Key
: Key
? ModelAllowsCreate<Schema, Model> extends true
? Key
: // trim create operations for models that don't allow create
Key extends HooksOperationsRequiringCreate
? never
: Key
: never]: T[Key];
};

Expand Down
9 changes: 9 additions & 0 deletions packages/orm/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { SchemaDbPusher } from './helpers/schema-db-pusher';
import type { ClientOptions, ProceduresOptions } from './options';
import type { AnyPlugin } from './plugin';
import { createZenStackPromise, type ZenStackPromise } from './promise';
import { fieldHasDefaultValue, isUnsupportedField, requireModel } from './query-utils';
import { ResultProcessor } from './result-processor';

/**
Expand Down Expand Up @@ -821,5 +822,13 @@ function createModelCrudHandler(
}
}

// Remove create/upsert operations for models with required Unsupported fields
const modelDef = requireModel(client.$schema, model);
if (Object.values(modelDef.fields).some((f) => isUnsupportedField(f) && !f.optional && !fieldHasDefaultValue(f))) {
for (const op of ['create', 'createMany', 'createManyAndReturn', 'upsert'] as const) {
delete (operations as any)[op];
}
}

return operations as ModelOperations<any, any>;
}
14 changes: 9 additions & 5 deletions packages/orm/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
type FieldIsArray,
type GetModels,
type GetTypeDefs,
type IsDelegateModel,
type ProcedureDef,
type RelationFields,
type RelationFieldType,
Expand Down Expand Up @@ -43,7 +42,12 @@ import type { ClientOptions, QueryOptions } from './options';
import type { ExtClientMembersBase, ExtQueryArgsBase, RuntimePlugin } from './plugin';
import type { ZenStackPromise } from './promise';
import type { ToKysely } from './query-builder';
import type { GetSlicedModels, GetSlicedOperations, GetSlicedProcedures } from './type-utils';
import type {
GetSlicedModels,
GetSlicedOperations,
GetSlicedProcedures,
ModelAllowsCreate,
} from './type-utils';
import type { ZodSchemaFactory } from './zod/factory';

type TransactionUnsupportedMethods = (typeof TRANSACTION_UNSUPPORTED_METHODS)[number];
Expand Down Expand Up @@ -284,8 +288,8 @@ type SliceOperations<
// keep only operations included by slicing options
[Key in keyof T as Key extends GetSlicedOperations<Schema, Model, Options> ? Key : never]: T[Key];
},
// exclude operations not applicable to delegate models
IsDelegateModel<Schema, Model> extends true ? OperationsIneligibleForDelegateModels : never
// exclude create operations for models that don't allow create (delegate models, required Unsupported fields)
| (ModelAllowsCreate<Schema, Model> extends true ? never : OperationsRequiringCreate)
>;

export type AllModelOperations<
Expand Down Expand Up @@ -880,7 +884,7 @@ type CommonModelOperations<
): ZenStackPromise<Schema, boolean>;
};

export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';
export type OperationsRequiringCreate = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';

export type ModelOperations<
Schema extends SchemaDef,
Expand Down
25 changes: 16 additions & 9 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
FieldHasDefault,
FieldIsArray,
FieldIsDelegateDiscriminator,
FieldIsDelegateRelation,
FieldIsRelation,
FieldType,
ForeignKeyFields,
Expand Down Expand Up @@ -60,7 +59,7 @@ import type {
import type { FilterKind, QueryOptions } from './options';
import type { ExtQueryArgsBase } from './plugin';
import type { ToKyselySchema } from './query-builder';
import type { GetSlicedFilterKindsForField, GetSlicedModels } from './type-utils';
import type { GetSlicedFilterKindsForField, GetSlicedModels, ModelAllowsCreate } from './type-utils';

//#region Query results

Expand Down Expand Up @@ -1331,6 +1330,15 @@ type CreateFKPayload<Schema extends SchemaDef, Model extends GetModels<Schema>>
}
>;

type RelationModelAllowsCreate<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends RelationFields<Schema, Model>,
> =
GetModelFieldType<Schema, Model, Field> extends GetModels<Schema>
? ModelAllowsCreate<Schema, GetModelFieldType<Schema, Model, Field>>
: false;

type CreateRelationFieldPayload<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Expand Down Expand Up @@ -1360,8 +1368,8 @@ type CreateRelationFieldPayload<
},
// no "createMany" for non-array fields
| (FieldIsArray<Schema, Model, Field> extends true ? never : 'createMany')
// exclude operations not applicable to delegate models
| (FieldIsDelegateRelation<Schema, Model, Field> extends true ? 'create' | 'createMany' | 'connectOrCreate' : never)
// exclude create operations for models that don't allow create
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'createMany' | 'connectOrCreate')
>;

type CreateRelationPayload<
Expand Down Expand Up @@ -1715,10 +1723,8 @@ type ToManyRelationUpdateInput<
*/
set?: SetRelationInput<Schema, Model, Field, Options>;
},
// exclude
FieldIsDelegateRelation<Schema, Model, Field> extends true
? 'create' | 'createMany' | 'connectOrCreate' | 'upsert'
: never
// exclude create operations for models that don't allow create
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'createMany' | 'connectOrCreate' | 'upsert')
>;

type ToOneRelationUpdateInput<
Expand Down Expand Up @@ -1765,7 +1771,8 @@ type ToOneRelationUpdateInput<
delete?: NestedDeleteInput<Schema, Model, Field, Options>;
}
: {}),
FieldIsDelegateRelation<Schema, Model, Field> extends true ? 'create' | 'connectOrCreate' | 'upsert' : never
// exclude create operations for models that don't allow create
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'connectOrCreate' | 'upsert')
>;

// #endregion
Expand Down
11 changes: 4 additions & 7 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
isInheritedField,
isRelationField,
isTypeDef,
getModelFields,
makeDefaultOrderBy,
requireField,
requireIdFields,
Expand Down Expand Up @@ -1117,17 +1118,13 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
omit: Record<string, boolean | undefined> | undefined | null,
modelAlias: string,
) {
const modelDef = requireModel(this.schema, model);
let result = query;

for (const field of Object.keys(modelDef.fields)) {
if (isRelationField(this.schema, model, field)) {
continue;
}
if (this.shouldOmitField(omit, model, field)) {
for (const fieldDef of getModelFields(this.schema, model, { inherited: true, computed: true })) {
if (this.shouldOmitField(omit, model, fieldDef.name)) {
continue;
}
result = this.buildSelectField(result, model, modelAlias, field);
result = this.buildSelectField(result, model, modelAlias, fieldDef.name);
}

// select all fields from delegate descendants and pack into a JSON field `$delegate$Model`
Expand Down
5 changes: 3 additions & 2 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {

const parentWhere = await this.buildUpdateParentRelationFilter(kysely, fromRelation);

let combinedWhere: WhereInput<Schema, GetModels<Schema>, any, false> = where ?? {};
let combinedWhere: Record<string, any> = where ?? {};
if (Object.keys(parentWhere).length > 0) {
combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere;
}
Expand Down Expand Up @@ -1210,7 +1210,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {

if (needIdRead) {
const readResult = await this.readUnique(kysely, model, {
where: combinedWhere,
where: combinedWhere as WhereInput<Schema, GetModels<Schema>>,
select: this.makeIdSelect(model),
});
if (!readResult && throwIfNotFound) {
Expand Down Expand Up @@ -2507,6 +2507,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
return newArgs;
}


private doNormalizeArgs(args: unknown) {
if (args && typeof args === 'object') {
for (const [key, value] of Object.entries(args)) {
Expand Down
19 changes: 7 additions & 12 deletions packages/orm/src/client/executor/name-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import {
getEnum,
getField,
getModel,
getModelFields,
isEnum,
requireModel,
stripAlias,
} from '../query-utils';

Expand Down Expand Up @@ -66,7 +66,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
this.modelToTableMap.set(modelName, mappedName);
}

for (const fieldDef of this.getModelFields(modelDef)) {
for (const fieldDef of getModelFields(this.schema, modelName)) {
const mappedName = this.getMappedName(fieldDef);
if (mappedName) {
this.fieldToColumnMap.set(`${modelName}.${fieldDef.name}`, mappedName);
Expand Down Expand Up @@ -431,7 +431,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
if (!modelDef) {
continue;
}
if (this.getModelFields(modelDef).some((f) => f.name === name)) {
if (getModelFields(this.schema, scope.model).some((f) => f.name === name)) {
return scope;
}
}
Expand Down Expand Up @@ -560,8 +560,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
}

private createSelectAllFields(model: string, alias: OperationNode | undefined) {
const modelDef = requireModel(this.schema, model);
return this.getModelFields(modelDef).map((fieldDef) => {
return getModelFields(this.schema, model).map((fieldDef) => {
const columnName = this.mapFieldName(model, fieldDef.name);
const columnRef = ReferenceNode.create(
ColumnNode.create(columnName),
Expand All @@ -576,9 +575,6 @@ export class QueryNameMapper extends OperationNodeTransformer {
});
}

private getModelFields(modelDef: ModelDef) {
return Object.values(modelDef.fields).filter((f) => !f.relation && !f.computed && !f.originModel);
}

private processSelections(selections: readonly SelectionNode[]) {
const result: SelectionNode[] = [];
Expand Down Expand Up @@ -627,9 +623,8 @@ export class QueryNameMapper extends OperationNodeTransformer {
}

// expand select all to a list of selections with name mapping
const modelDef = requireModel(this.schema, scope.model);
return this.getModelFields(modelDef).map((fieldDef) => {
const columnName = this.mapFieldName(modelDef.name, fieldDef.name);
return getModelFields(this.schema, scope.model).map((fieldDef) => {
const columnName = this.mapFieldName(scope.model!, fieldDef.name);
const columnRef = ReferenceNode.create(ColumnNode.create(columnName));

// process enum value mapping
Expand Down Expand Up @@ -660,7 +655,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
if (!modelDef) {
return false;
}
return this.getModelFields(modelDef).some((fieldDef) => {
return getModelFields(this.schema, model).some((fieldDef) => {
const enumDef = getEnum(this.schema, fieldDef.type);
if (!enumDef) {
return false;
Expand Down
7 changes: 6 additions & 1 deletion packages/orm/src/client/helpers/schema-db-pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type SchemaDef,
} from '../../schema';
import type { ToKysely } from '../query-builder';
import { requireModel } from '../query-utils';
import { isUnsupportedField, requireModel } from '../query-utils';

/**
* This class is for testing purposes only. It should never be used in production.
Expand Down Expand Up @@ -117,6 +117,11 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
continue;
}

if (isUnsupportedField(fieldDef)) {
// Unsupported fields cannot be represented in the ORM's schema pusher
continue;
}

if (fieldDef.relation) {
table = this.addForeignKeyConstraint(table, modelDef.name, fieldName, fieldDef);
} else if (!this.isComputedField(fieldDef)) {
Expand Down
14 changes: 12 additions & 2 deletions packages/orm/src/client/query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ export function requireField(schema: SchemaDef, modelOrType: string, field: stri
}

/**
* Gets all model fields, by default non-relation, non-computed, non-inherited fields only.
* Gets all model fields, by default non-relation, non-computed, non-inherited, non-unsupported fields only.
*/
export function getModelFields(
schema: SchemaDef,
model: string,
options?: { relations?: boolean; computed?: boolean; inherited?: boolean },
options?: { relations?: boolean; computed?: boolean; inherited?: boolean; unsupported?: boolean },
) {
const modelDef = requireModel(schema, model);
return Object.values(modelDef.fields).filter((f) => {
Expand All @@ -88,10 +88,20 @@ export function getModelFields(
if (f.originModel && !options?.inherited) {
return false;
}
if (f.type === 'Unsupported' && !options?.unsupported) {
return false;
}
return true;
});
}

/**
* Checks if a field is of `Unsupported` type.
*/
export function isUnsupportedField(fieldDef: FieldDef) {
return fieldDef.type === 'Unsupported';
}

export function getIdFields<Schema extends SchemaDef>(schema: SchemaDef, model: GetModels<Schema>) {
const modelDef = getModel(schema, model);
return modelDef?.idFields;
Expand Down
33 changes: 32 additions & 1 deletion packages/orm/src/client/type-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
import type { GetModels, SchemaDef } from '@zenstackhq/schema';
import type { FieldDef, GetModel, GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema';
import type { GetProcedureNames } from './crud-types';
import type { AllCrudOperations } from './crud/operations/base';
import type { FilterKind, QueryOptions, SlicingOptions } from './options';

/**
* Checks if a model has any required Unsupported fields (non-optional, no default).
* Uses raw field access since `GetModelFields` excludes Unsupported fields.
*/
export type ModelHasRequiredUnsupportedField<Schema extends SchemaDef, Model extends GetModels<Schema>> = true extends {
[Key in Extract<keyof GetModel<Schema, Model>['fields'], string>]: GetModel<
Schema,
Model
>['fields'][Key] extends infer F extends FieldDef
? F['type'] extends 'Unsupported'
? F['optional'] extends true
? false
: 'default' extends keyof F
? false
: true
: false
: false;
}[Extract<keyof GetModel<Schema, Model>['fields'], string>]
? true
: false;

/**
* Checks if a model allows create operations (not a delegate model and has no required Unsupported fields).
*/
export type ModelAllowsCreate<Schema extends SchemaDef, Model extends GetModels<Schema>> =
IsDelegateModel<Schema, Model> extends true
? false
: ModelHasRequiredUnsupportedField<Schema, Model> extends true
? false
: true;

type IsNever<T> = [T] extends [never] ? true : false;

// #region Model slicing
Expand Down
Loading
Loading