diff --git a/packages/clients/tanstack-query/src/common/types.ts b/packages/clients/tanstack-query/src/common/types.ts index f745ceeb4..a967869ed 100644 --- a/packages/clients/tanstack-query/src/common/types.ts +++ b/packages/clients/tanstack-query/src/common/types.ts @@ -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. @@ -59,8 +60,8 @@ export type ExtraMutationOptions = { optimisticDataProvider?: OptimisticDataProvider; } & QueryContext; -type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any - ? `use${Capitalize}` +type HooksOperationsRequiringCreate = OperationsRequiringCreate extends any + ? `use${Capitalize}` : never; type Modifiers = '' | 'Suspense' | 'Infinite' | 'SuspenseInfinite'; @@ -76,12 +77,12 @@ export type TrimSlicedOperations< > = { // trim operations based on slicing options [Key in keyof T as Key extends `use${Modifiers}${Capitalize>}` - ? IsDelegateModel extends true - ? // trim operations ineligible for delegate models - Key extends HooksOperationsIneligibleForDelegateModels - ? never - : Key - : Key + ? ModelAllowsCreate extends true + ? Key + : // trim create operations for models that don't allow create + Key extends HooksOperationsRequiringCreate + ? never + : Key : never]: T[Key]; }; diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 1046d0ca8..92dddebb8 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -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'; /** @@ -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; } diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index c6b772aa4..23c2367fd 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -2,7 +2,6 @@ import { type FieldIsArray, type GetModels, type GetTypeDefs, - type IsDelegateModel, type ProcedureDef, type RelationFields, type RelationFieldType, @@ -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]; @@ -284,8 +288,8 @@ type SliceOperations< // keep only operations included by slicing options [Key in keyof T as Key extends GetSlicedOperations ? Key : never]: T[Key]; }, - // exclude operations not applicable to delegate models - IsDelegateModel extends true ? OperationsIneligibleForDelegateModels : never + // exclude create operations for models that don't allow create (delegate models, required Unsupported fields) + | (ModelAllowsCreate extends true ? never : OperationsRequiringCreate) >; export type AllModelOperations< @@ -880,7 +884,7 @@ type CommonModelOperations< ): ZenStackPromise; }; -export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; +export type OperationsRequiringCreate = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; export type ModelOperations< Schema extends SchemaDef, diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 6c335cf93..d5d6e8c27 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -6,7 +6,6 @@ import type { FieldHasDefault, FieldIsArray, FieldIsDelegateDiscriminator, - FieldIsDelegateRelation, FieldIsRelation, FieldType, ForeignKeyFields, @@ -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 @@ -1331,6 +1330,15 @@ type CreateFKPayload> } >; +type RelationModelAllowsCreate< + Schema extends SchemaDef, + Model extends GetModels, + Field extends RelationFields, +> = + GetModelFieldType extends GetModels + ? ModelAllowsCreate> + : false; + type CreateRelationFieldPayload< Schema extends SchemaDef, Model extends GetModels, @@ -1360,8 +1368,8 @@ type CreateRelationFieldPayload< }, // no "createMany" for non-array fields | (FieldIsArray extends true ? never : 'createMany') - // exclude operations not applicable to delegate models - | (FieldIsDelegateRelation extends true ? 'create' | 'createMany' | 'connectOrCreate' : never) + // exclude create operations for models that don't allow create + | (RelationModelAllowsCreate extends true ? never : 'create' | 'createMany' | 'connectOrCreate') >; type CreateRelationPayload< @@ -1715,10 +1723,8 @@ type ToManyRelationUpdateInput< */ set?: SetRelationInput; }, - // exclude - FieldIsDelegateRelation extends true - ? 'create' | 'createMany' | 'connectOrCreate' | 'upsert' - : never + // exclude create operations for models that don't allow create + | (RelationModelAllowsCreate extends true ? never : 'create' | 'createMany' | 'connectOrCreate' | 'upsert') >; type ToOneRelationUpdateInput< @@ -1765,7 +1771,8 @@ type ToOneRelationUpdateInput< delete?: NestedDeleteInput; } : {}), - FieldIsDelegateRelation extends true ? 'create' | 'connectOrCreate' | 'upsert' : never + // exclude create operations for models that don't allow create + | (RelationModelAllowsCreate extends true ? never : 'create' | 'connectOrCreate' | 'upsert') >; // #endregion diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 1f5102121..24081069b 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -29,6 +29,7 @@ import { isInheritedField, isRelationField, isTypeDef, + getModelFields, makeDefaultOrderBy, requireField, requireIdFields, @@ -1117,17 +1118,13 @@ export abstract class BaseCrudDialect { omit: Record | 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` diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index a12f0fe2a..d05485d5d 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1148,7 +1148,7 @@ export abstract class BaseOperationHandler { const parentWhere = await this.buildUpdateParentRelationFilter(kysely, fromRelation); - let combinedWhere: WhereInput, any, false> = where ?? {}; + let combinedWhere: Record = where ?? {}; if (Object.keys(parentWhere).length > 0) { combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere; } @@ -1210,7 +1210,7 @@ export abstract class BaseOperationHandler { if (needIdRead) { const readResult = await this.readUnique(kysely, model, { - where: combinedWhere, + where: combinedWhere as WhereInput>, select: this.makeIdSelect(model), }); if (!readResult && throwIfNotFound) { @@ -2507,6 +2507,7 @@ export abstract class BaseOperationHandler { return newArgs; } + private doNormalizeArgs(args: unknown) { if (args && typeof args === 'object') { for (const [key, value] of Object.entries(args)) { diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 6a7535fe0..369526cd6 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -37,8 +37,8 @@ import { getEnum, getField, getModel, + getModelFields, isEnum, - requireModel, stripAlias, } from '../query-utils'; @@ -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); @@ -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; } } @@ -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), @@ -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[] = []; @@ -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 @@ -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; diff --git a/packages/orm/src/client/helpers/schema-db-pusher.ts b/packages/orm/src/client/helpers/schema-db-pusher.ts index 4886687ef..f84a29004 100644 --- a/packages/orm/src/client/helpers/schema-db-pusher.ts +++ b/packages/orm/src/client/helpers/schema-db-pusher.ts @@ -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. @@ -117,6 +117,11 @@ export class SchemaDbPusher { 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)) { diff --git a/packages/orm/src/client/query-utils.ts b/packages/orm/src/client/query-utils.ts index 0ea7ea58a..2e8b134b3 100644 --- a/packages/orm/src/client/query-utils.ts +++ b/packages/orm/src/client/query-utils.ts @@ -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) => { @@ -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: SchemaDef, model: GetModels) { const modelDef = getModel(schema, model); return modelDef?.idFields; diff --git a/packages/orm/src/client/type-utils.ts b/packages/orm/src/client/type-utils.ts index 3a4589f71..68e79a6c0 100644 --- a/packages/orm/src/client/type-utils.ts +++ b/packages/orm/src/client/type-utils.ts @@ -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> = true extends { + [Key in Extract['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['fields'], string>] + ? true + : false; + +/** + * Checks if a model allows create operations (not a delegate model and has no required Unsupported fields). + */ +export type ModelAllowsCreate> = + IsDelegateModel extends true + ? false + : ModelHasRequiredUnsupportedField extends true + ? false + : true; + type IsNever = [T] extends [never] ? true : false; // #region Model slicing diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 0f0eb61e8..6e1c5db2c 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -129,6 +129,14 @@ export class ZodSchemaFactory< return this.options.validateInput !== false; } + /** + * Returns model field entries, excluding Unsupported fields. + */ + private getModelFields(model: string): [string, FieldDef][] { + const modelDef = requireModel(this.schema, model); + return Object.entries(modelDef.fields).filter(([, def]) => def.type !== 'Unsupported'); + } + private shouldIncludeRelations(options?: CreateSchemaOptions): boolean { return options?.relationDepth === undefined || options.relationDepth > 0; } @@ -337,8 +345,6 @@ export class ZodSchemaFactory< withAggregations = false, options?: CreateSchemaOptions, ): ZodType { - const modelDef = requireModel(this.schema, model); - // unique field used in unique filters bypass filter slicing const uniqueFieldNames = unique ? getUniqueFields(this.schema, model) @@ -353,8 +359,7 @@ export class ZodSchemaFactory< const nextOpts = this.nextOptions(options); const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); + for (const [field, fieldDef] of this.getModelFields(model)) { let fieldSchema: ZodType | undefined; if (fieldDef.relation) { @@ -877,10 +882,8 @@ export class ZodSchemaFactory< @cache() private makeSelectSchema(model: string, options?: CreateSchemaOptions) { - const modelDef = requireModel(this.schema, model); const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); + for (const [field, fieldDef] of this.getModelFields(model)) { if (fieldDef.relation) { if (!this.shouldIncludeRelations(options)) { continue; @@ -992,10 +995,8 @@ export class ZodSchemaFactory< @cache() private makeOmitSchema(model: string) { - const modelDef = requireModel(this.schema, model); const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); + for (const [field, fieldDef] of this.getModelFields(model)) { if (!fieldDef.relation) { if (this.options.allowQueryTimeOmitOverride !== false) { // if override is allowed, use boolean @@ -1043,12 +1044,10 @@ export class ZodSchemaFactory< WithAggregation: boolean, options?: CreateSchemaOptions, ) { - const modelDef = requireModel(this.schema, model); const fields: Record = {}; const sort = z.union([z.literal('asc'), z.literal('desc')]); const nextOpts = this.nextOptions(options); - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); + for (const [field, fieldDef] of this.getModelFields(model)) { if (fieldDef.relation) { // relations if (withRelation && this.shouldIncludeRelations(options)) { @@ -1098,8 +1097,9 @@ export class ZodSchemaFactory< @cache() private makeDistinctSchema(model: string) { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); + const nonRelationFields = this.getModelFields(model) + .filter(([, def]) => !def.relation) + .map(([name]) => name); return nonRelationFields.length > 0 ? this.orArray(z.enum(nonRelationFields as any), true) : z.never(); } @@ -1170,20 +1170,19 @@ export class ZodSchemaFactory< const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; const modelDef = requireModel(this.schema, model); + const modelFields = this.getModelFields(model); const hasRelation = !skipRelations && - Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); + modelFields.some(([f, def]) => !withoutFields.includes(f) && def.relation); const nextOpts = this.nextOptions(options); - Object.keys(modelDef.fields).forEach((field) => { + modelFields.forEach(([field, fieldDef]) => { if (withoutFields.includes(field)) { return; } - const fieldDef = requireField(this.schema, model, field); - - // skip computed fields and discriminator fields, they cannot be set on create + // skip computed fields and discriminator fields if (fieldDef.computed || fieldDef.isDiscriminator) { return; } @@ -1302,21 +1301,28 @@ export class ZodSchemaFactory< const fieldDef = requireField(this.schema, model, field); const fieldType = fieldDef.type; const array = !!fieldDef.array; + const canCreateModel = this.canCreateModel(fieldType); const fields: Record = { - create: this.makeCreateDataSchema( + connect: this.makeConnectDataSchema(fieldType, array, options).optional(), + }; + + if (canCreateModel) { + fields['create'] = this.makeCreateDataSchema( fieldDef.type, !!fieldDef.array, withoutFields, false, options, - ).optional(), - - connect: this.makeConnectDataSchema(fieldType, array, options).optional(), - - connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields, options).optional(), - }; + ).optional(); + fields['connectOrCreate'] = this.makeConnectOrCreateDataSchema( + fieldType, + array, + withoutFields, + options, + ).optional(); + } - if (array) { + if (array && canCreateModel) { fields['createMany'] = this.makeCreateManyPayloadSchema(fieldType, withoutFields, options).optional(); } @@ -1346,19 +1352,21 @@ export class ZodSchemaFactory< ]) .optional(); - let upsertWhere = this.makeWhereSchema(fieldType, true, false, false, options); - if (!fieldDef.array) { - // to-one relation, can upsert without where clause - upsertWhere = upsertWhere.optional(); + if (canCreateModel) { + let upsertWhere = this.makeWhereSchema(fieldType, true, false, false, options); + if (!fieldDef.array) { + // to-one relation, can upsert without where clause + upsertWhere = upsertWhere.optional(); + } + fields['upsert'] = this.orArray( + z.strictObject({ + where: upsertWhere, + create: this.makeCreateDataSchema(fieldType, false, withoutFields, false, options), + update: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), + }), + true, + ).optional(); } - fields['upsert'] = this.orArray( - z.strictObject({ - where: upsertWhere, - create: this.makeCreateDataSchema(fieldType, false, withoutFields, false, options), - update: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), - }), - true, - ).optional(); if (array) { // to-many relation specifics @@ -1524,21 +1532,20 @@ export class ZodSchemaFactory< const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; const modelDef = requireModel(this.schema, model); + const modelFields = this.getModelFields(model); const hasRelation = !skipRelations && - Object.entries(modelDef.fields).some(([key, value]) => value.relation && !withoutFields.includes(key)); + modelFields.some(([key, value]) => value.relation && !withoutFields.includes(key)); const nextOpts = this.nextOptions(options); - Object.keys(modelDef.fields).forEach((field) => { + modelFields.forEach(([field, fieldDef]) => { if (withoutFields.includes(field)) { return; } - const fieldDef = requireField(this.schema, model, field); - + // skip computed fields and discriminator fields if (fieldDef.computed || fieldDef.isDiscriminator) { - // skip computed fields and discriminator fields, they cannot be updated return; } @@ -1698,13 +1705,12 @@ export class ZodSchemaFactory< @cache() private makeCountAggregateInputSchema(model: string) { - const modelDef = requireModel(this.schema, model); return z.union([ z.literal(true), z.strictObject({ _all: z.literal(true).optional(), - ...Object.keys(modelDef.fields).reduce( - (acc, field) => { + ...this.getModelFields(model).reduce( + (acc, [field]) => { acc[field] = z.literal(true).optional(); return acc; }, @@ -1741,11 +1747,9 @@ export class ZodSchemaFactory< @cache() private makeSumAvgInputSchema(model: string) { - const modelDef = requireModel(this.schema, model); return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); + this.getModelFields(model).reduce( + (acc, [field, fieldDef]) => { if (this.isNumericField(fieldDef)) { acc[field] = z.literal(true).optional(); } @@ -1758,11 +1762,9 @@ export class ZodSchemaFactory< @cache() private makeMinMaxInputSchema(model: string) { - const modelDef = requireModel(this.schema, model); return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); + this.getModelFields(model).reduce( + (acc, [field, fieldDef]) => { if (!fieldDef.relation && !fieldDef.array) { acc[field] = z.literal(true).optional(); } @@ -1782,8 +1784,9 @@ export class ZodSchemaFactory< model: Model, options?: CreateSchemaOptions, ): ZodType> { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); + const nonRelationFields = this.getModelFields(model) + .filter(([, def]) => !def.relation) + .map(([name]) => name); const bySchema = nonRelationFields.length > 0 ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) @@ -2185,10 +2188,14 @@ export class ZodSchemaFactory< } } - /** - * Checks if a model is included in the slicing configuration. - * Returns true if the model is allowed, false if it's excluded. - */ + private canCreateModel(model: string) { + const modelDef = requireModel(this.schema, model); + const hasRequiredUnsupportedFields = Object.values(modelDef.fields).some( + (fieldDef) => fieldDef.type === 'Unsupported' && !fieldDef.optional && !fieldHasDefaultValue(fieldDef), + ); + return !hasRequiredUnsupportedFields; + } + private isModelAllowed(targetModel: string): boolean { const slicing = this.options.slicing; if (!slicing) { diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index e21b5e30e..b93ea9355 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -150,10 +150,15 @@ export type GetTypeDefs = Extract> = Schema['typeDefs'] extends Record ? Schema['typeDefs'][TypeDef] : never; -export type GetModelFields> = Extract< - keyof GetModel['fields'], - string ->; +export type GetModelFields> = keyof { + [Key in Extract['fields'], string> as FieldIsUnsupported< + Schema, + Model, + Key + > extends true + ? never + : Key]: never; +}; export type GetModelField< Schema extends SchemaDef, @@ -281,6 +286,16 @@ export type FieldIsComputed< Field extends GetModelFields, > = GetModelField['computed'] extends true ? true : false; +export type FieldIsUnsupported< + Schema extends SchemaDef, + Model extends GetModels, + Field extends string, +> = Field extends keyof GetModel['fields'] + ? GetModel['fields'][Field]['type'] extends 'Unsupported' + ? true + : false + : never; + export type FieldHasDefault< Schema extends SchemaDef, Model extends GetModels, diff --git a/tests/e2e/orm/client-api/unsupported.test-d.ts b/tests/e2e/orm/client-api/unsupported.test-d.ts new file mode 100644 index 000000000..a9797cc90 --- /dev/null +++ b/tests/e2e/orm/client-api/unsupported.test-d.ts @@ -0,0 +1,134 @@ +import type { ClientContract, CreateArgs, FindManyArgs, ModelResult, UpdateArgs } from '@zenstackhq/orm'; +import { describe, expectTypeOf, it } from 'vitest'; +import z from 'zod'; +import { schema } from '../schemas/unsupported/schema'; + +type Schema = typeof schema; + +declare const client: ClientContract; + +describe('Unsupported field exclusion - typing', () => { + // #region Result types + + it('excludes Unsupported fields from result type (optional Unsupported)', () => { + type ItemResult = ModelResult; + expectTypeOf().toHaveProperty('id'); + expectTypeOf().toHaveProperty('name'); + // Unsupported field should be excluded + expectTypeOf().not.toHaveProperty('data'); + }); + + it('excludes Unsupported fields from result type (required Unsupported)', () => { + type GeoResult = ModelResult; + expectTypeOf().toHaveProperty('id'); + expectTypeOf().toHaveProperty('title'); + // Unsupported field should be excluded + expectTypeOf().not.toHaveProperty('extra'); + }); + + // #endregion + + // #region Find/Where types + + it('excludes Unsupported fields from where filter', () => { + type FindArgs = FindManyArgs; + type Where = NonNullable; + expectTypeOf().toHaveProperty('id'); + expectTypeOf().toHaveProperty('name'); + // Unsupported field should not be filterable + expectTypeOf().not.toHaveProperty('data'); + }); + + // #endregion + + // #region Select/Omit types + + it('excludes Unsupported fields from select', () => { + type FindArgs = FindManyArgs; + type Select = NonNullable; + expectTypeOf().toHaveProperty('name'); + // Unsupported field should not be selectable + expectTypeOf