Skip to content

Commit c768af7

Browse files
authored
fix(orm): exclude Unsupported fields from ORM client (#2468)
1 parent 12aeb7b commit c768af7

File tree

19 files changed

+906
-132
lines changed

19 files changed

+906
-132
lines changed

packages/clients/tanstack-query/src/common/types.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import type { FetchFn } from '@zenstackhq/client-helpers/fetch';
33
import type {
44
GetProcedureNames,
55
GetSlicedOperations,
6-
OperationsIneligibleForDelegateModels,
6+
ModelAllowsCreate,
7+
OperationsRequiringCreate,
78
ProcedureFunc,
89
QueryOptions,
910
} from '@zenstackhq/orm';
10-
import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema';
11+
import type { GetModels, SchemaDef } from '@zenstackhq/schema';
1112

1213
/**
1314
* Context type for configuring the hooks.
@@ -59,8 +60,8 @@ export type ExtraMutationOptions = {
5960
optimisticDataProvider?: OptimisticDataProvider;
6061
} & QueryContext;
6162

62-
type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any
63-
? `use${Capitalize<OperationsIneligibleForDelegateModels>}`
63+
type HooksOperationsRequiringCreate = OperationsRequiringCreate extends any
64+
? `use${Capitalize<OperationsRequiringCreate>}`
6465
: never;
6566

6667
type Modifiers = '' | 'Suspense' | 'Infinite' | 'SuspenseInfinite';
@@ -76,12 +77,12 @@ export type TrimSlicedOperations<
7677
> = {
7778
// trim operations based on slicing options
7879
[Key in keyof T as Key extends `use${Modifiers}${Capitalize<GetSlicedOperations<Schema, Model, Options>>}`
79-
? IsDelegateModel<Schema, Model> extends true
80-
? // trim operations ineligible for delegate models
81-
Key extends HooksOperationsIneligibleForDelegateModels
82-
? never
83-
: Key
84-
: Key
80+
? ModelAllowsCreate<Schema, Model> extends true
81+
? Key
82+
: // trim create operations for models that don't allow create
83+
Key extends HooksOperationsRequiringCreate
84+
? never
85+
: Key
8586
: never]: T[Key];
8687
};
8788

packages/orm/src/client/client-impl.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type ExtResultFieldDef = {
4848
};
4949
import { getField } from './query-utils';
5050
import { createZenStackPromise, type ZenStackPromise } from './promise';
51+
import { fieldHasDefaultValue, isUnsupportedField, requireModel } from './query-utils';
5152
import { ResultProcessor } from './result-processor';
5253

5354
/**
@@ -880,6 +881,14 @@ function createModelCrudHandler(
880881
}
881882
}
882883

884+
// Remove create/upsert operations for models with required Unsupported fields
885+
const modelDef = requireModel(client.$schema, model);
886+
if (Object.values(modelDef.fields).some((f) => isUnsupportedField(f) && !f.optional && !fieldHasDefaultValue(f))) {
887+
for (const op of ['create', 'createMany', 'createManyAndReturn', 'upsert'] as const) {
888+
delete (operations as any)[op];
889+
}
890+
}
891+
883892
return operations as ModelOperations<any, any>;
884893
}
885894

packages/orm/src/client/contract.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
type FieldIsArray,
33
type GetModels,
44
type GetTypeDefs,
5-
type IsDelegateModel,
65
type ProcedureDef,
76
type RelationFields,
87
type RelationFieldType,
@@ -44,7 +43,12 @@ import type { ClientOptions, QueryOptions } from './options';
4443
import type { ExtClientMembersBase, ExtQueryArgsBase, ExtResultBase, RuntimePlugin } from './plugin';
4544
import type { ZenStackPromise } from './promise';
4645
import type { ToKysely } from './query-builder';
47-
import type { GetSlicedModels, GetSlicedOperations, GetSlicedProcedures } from './type-utils';
46+
import type {
47+
GetSlicedModels,
48+
GetSlicedOperations,
49+
GetSlicedProcedures,
50+
ModelAllowsCreate,
51+
} from './type-utils';
4852
import type { ZodSchemaFactory } from './zod/factory';
4953

5054
type TransactionUnsupportedMethods = (typeof TRANSACTION_UNSUPPORTED_METHODS)[number];
@@ -292,8 +296,8 @@ type SliceOperations<
292296
// keep only operations included by slicing options
293297
[Key in keyof T as Key extends GetSlicedOperations<Schema, Model, Options> ? Key : never]: T[Key];
294298
},
295-
// exclude operations not applicable to delegate models
296-
IsDelegateModel<Schema, Model> extends true ? OperationsIneligibleForDelegateModels : never
299+
// exclude create operations for models that don't allow create (delegate models, required Unsupported fields)
300+
| (ModelAllowsCreate<Schema, Model> extends true ? never : OperationsRequiringCreate)
297301
>;
298302

299303
export type AllModelOperations<
@@ -890,7 +894,7 @@ type CommonModelOperations<
890894
): ZenStackPromise<Schema, boolean>;
891895
};
892896

893-
export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';
897+
export type OperationsRequiringCreate = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert';
894898

895899
export type ModelOperations<
896900
Schema extends SchemaDef,

packages/orm/src/client/crud-types.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {
66
FieldHasDefault,
77
FieldIsArray,
88
FieldIsDelegateDiscriminator,
9-
FieldIsDelegateRelation,
109
FieldIsRelation,
1110
FieldType,
1211
ForeignKeyFields,
@@ -60,7 +59,7 @@ import type {
6059
import type { FilterKind, QueryOptions } from './options';
6160
import type { ExtQueryArgsBase, ExtResultBase } from './plugin';
6261
import type { ToKyselySchema } from './query-builder';
63-
import type { GetSlicedFilterKindsForField, GetSlicedModels } from './type-utils';
62+
import type { GetSlicedFilterKindsForField, GetSlicedModels, ModelAllowsCreate } from './type-utils';
6463

6564
//#region Query results
6665

@@ -1351,6 +1350,15 @@ type CreateFKPayload<Schema extends SchemaDef, Model extends GetModels<Schema>>
13511350
}
13521351
>;
13531352

1353+
type RelationModelAllowsCreate<
1354+
Schema extends SchemaDef,
1355+
Model extends GetModels<Schema>,
1356+
Field extends RelationFields<Schema, Model>,
1357+
> =
1358+
GetModelFieldType<Schema, Model, Field> extends GetModels<Schema>
1359+
? ModelAllowsCreate<Schema, GetModelFieldType<Schema, Model, Field>>
1360+
: false;
1361+
13541362
type CreateRelationFieldPayload<
13551363
Schema extends SchemaDef,
13561364
Model extends GetModels<Schema>,
@@ -1380,8 +1388,8 @@ type CreateRelationFieldPayload<
13801388
},
13811389
// no "createMany" for non-array fields
13821390
| (FieldIsArray<Schema, Model, Field> extends true ? never : 'createMany')
1383-
// exclude operations not applicable to delegate models
1384-
| (FieldIsDelegateRelation<Schema, Model, Field> extends true ? 'create' | 'createMany' | 'connectOrCreate' : never)
1391+
// exclude create operations for models that don't allow create
1392+
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'createMany' | 'connectOrCreate')
13851393
>;
13861394

13871395
type CreateRelationPayload<
@@ -1738,10 +1746,8 @@ type ToManyRelationUpdateInput<
17381746
*/
17391747
set?: SetRelationInput<Schema, Model, Field, Options>;
17401748
},
1741-
// exclude
1742-
FieldIsDelegateRelation<Schema, Model, Field> extends true
1743-
? 'create' | 'createMany' | 'connectOrCreate' | 'upsert'
1744-
: never
1749+
// exclude create operations for models that don't allow create
1750+
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'createMany' | 'connectOrCreate' | 'upsert')
17451751
>;
17461752

17471753
type ToOneRelationUpdateInput<
@@ -1788,7 +1794,8 @@ type ToOneRelationUpdateInput<
17881794
delete?: NestedDeleteInput<Schema, Model, Field, Options>;
17891795
}
17901796
: {}),
1791-
FieldIsDelegateRelation<Schema, Model, Field> extends true ? 'create' | 'connectOrCreate' | 'upsert' : never
1797+
// exclude create operations for models that don't allow create
1798+
| (RelationModelAllowsCreate<Schema, Model, Field> extends true ? never : 'create' | 'connectOrCreate' | 'upsert')
17921799
>;
17931800

17941801
// #endregion

packages/orm/src/client/crud/dialects/base-dialect.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ import {
2626
getManyToManyRelation,
2727
getRelationForeignKeyFieldPairs,
2828
isEnum,
29-
isInheritedField,
30-
isRelationField,
3129
isTypeDef,
30+
getModelFields,
3231
makeDefaultOrderBy,
3332
requireField,
3433
requireIdFields,
@@ -1127,33 +1126,26 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
11271126
omit: Record<string, boolean | undefined> | undefined | null,
11281127
modelAlias: string,
11291128
) {
1130-
const modelDef = requireModel(this.schema, model);
11311129
let result = query;
11321130

1133-
for (const field of Object.keys(modelDef.fields)) {
1134-
if (isRelationField(this.schema, model, field)) {
1135-
continue;
1136-
}
1137-
if (this.shouldOmitField(omit, model, field)) {
1131+
for (const fieldDef of getModelFields(this.schema, model, { inherited: true, computed: true })) {
1132+
if (this.shouldOmitField(omit, model, fieldDef.name)) {
11381133
continue;
11391134
}
1140-
result = this.buildSelectField(result, model, modelAlias, field);
1135+
result = this.buildSelectField(result, model, modelAlias, fieldDef.name);
11411136
}
11421137

11431138
// select all fields from delegate descendants and pack into a JSON field `$delegate$Model`
11441139
const descendants = getDelegateDescendantModels(this.schema, model);
11451140
for (const subModel of descendants) {
11461141
result = this.buildDelegateJoin(model, modelAlias, subModel.name, result);
1147-
result = result.select((eb) => {
1142+
result = result.select(() => {
11481143
const jsonObject: Record<string, Expression<any>> = {};
1149-
for (const field of Object.keys(subModel.fields)) {
1150-
if (
1151-
isRelationField(this.schema, subModel.name, field) ||
1152-
isInheritedField(this.schema, subModel.name, field)
1153-
) {
1144+
for (const fieldDef of getModelFields(this.schema, subModel.name, { computed: true })) {
1145+
if (this.shouldOmitField(omit, subModel.name, fieldDef.name)) {
11541146
continue;
11551147
}
1156-
jsonObject[field] = eb.ref(`${subModel.name}.${field}`);
1148+
jsonObject[fieldDef.name] = this.fieldRef(subModel.name, fieldDef.name, subModel.name);
11571149
}
11581150
return this.buildJsonObject(jsonObject).as(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`);
11591151
});

packages/orm/src/client/executor/name-mapper.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import {
3737
getEnum,
3838
getField,
3939
getModel,
40+
getModelFields,
4041
isEnum,
41-
requireModel,
4242
stripAlias,
4343
} from '../query-utils';
4444

@@ -66,7 +66,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
6666
this.modelToTableMap.set(modelName, mappedName);
6767
}
6868

69-
for (const fieldDef of this.getModelFields(modelDef)) {
69+
for (const fieldDef of getModelFields(this.schema, modelName)) {
7070
const mappedName = this.getMappedName(fieldDef);
7171
if (mappedName) {
7272
this.fieldToColumnMap.set(`${modelName}.${fieldDef.name}`, mappedName);
@@ -431,7 +431,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
431431
if (!modelDef) {
432432
continue;
433433
}
434-
if (this.getModelFields(modelDef).some((f) => f.name === name)) {
434+
if (getModelFields(this.schema, scope.model).some((f) => f.name === name)) {
435435
return scope;
436436
}
437437
}
@@ -560,8 +560,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
560560
}
561561

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

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

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

629625
// expand select all to a list of selections with name mapping
630-
const modelDef = requireModel(this.schema, scope.model);
631-
return this.getModelFields(modelDef).map((fieldDef) => {
632-
const columnName = this.mapFieldName(modelDef.name, fieldDef.name);
626+
return getModelFields(this.schema, scope.model).map((fieldDef) => {
627+
const columnName = this.mapFieldName(scope.model!, fieldDef.name);
633628
const columnRef = ReferenceNode.create(ColumnNode.create(columnName));
634629

635630
// process enum value mapping
@@ -660,7 +655,7 @@ export class QueryNameMapper extends OperationNodeTransformer {
660655
if (!modelDef) {
661656
return false;
662657
}
663-
return this.getModelFields(modelDef).some((fieldDef) => {
658+
return getModelFields(this.schema, model).some((fieldDef) => {
664659
const enumDef = getEnum(this.schema, fieldDef.type);
665660
if (!enumDef) {
666661
return false;

packages/orm/src/client/helpers/schema-db-pusher.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type SchemaDef,
1313
} from '../../schema';
1414
import type { ToKysely } from '../query-builder';
15-
import { requireModel } from '../query-utils';
15+
import { isUnsupportedField, requireModel } from '../query-utils';
1616

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

120+
if (isUnsupportedField(fieldDef)) {
121+
// Unsupported fields cannot be represented in the ORM's schema pusher
122+
continue;
123+
}
124+
120125
if (fieldDef.relation) {
121126
table = this.addForeignKeyConstraint(table, modelDef.name, fieldName, fieldDef);
122127
} else if (!this.isComputedField(fieldDef)) {

packages/orm/src/client/query-utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ export function requireField(schema: SchemaDef, modelOrType: string, field: stri
7070
}
7171

7272
/**
73-
* Gets all model fields, by default non-relation, non-computed, non-inherited fields only.
73+
* Gets all model fields, by default non-relation, non-computed, non-inherited, non-unsupported fields only.
7474
*/
7575
export function getModelFields(
7676
schema: SchemaDef,
7777
model: string,
78-
options?: { relations?: boolean; computed?: boolean; inherited?: boolean },
78+
options?: { relations?: boolean; computed?: boolean; inherited?: boolean; unsupported?: boolean },
7979
) {
8080
const modelDef = requireModel(schema, model);
8181
return Object.values(modelDef.fields).filter((f) => {
@@ -88,10 +88,20 @@ export function getModelFields(
8888
if (f.originModel && !options?.inherited) {
8989
return false;
9090
}
91+
if (f.type === 'Unsupported' && !options?.unsupported) {
92+
return false;
93+
}
9194
return true;
9295
});
9396
}
9497

98+
/**
99+
* Checks if a field is of `Unsupported` type.
100+
*/
101+
export function isUnsupportedField(fieldDef: FieldDef) {
102+
return fieldDef.type === 'Unsupported';
103+
}
104+
95105
export function getIdFields<Schema extends SchemaDef>(schema: SchemaDef, model: GetModels<Schema>) {
96106
const modelDef = getModel(schema, model);
97107
return modelDef?.idFields;

packages/orm/src/client/type-utils.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,39 @@
1-
import type { GetModels, SchemaDef } from '@zenstackhq/schema';
1+
import type { FieldDef, GetModel, GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema';
22
import type { GetProcedureNames } from './crud-types';
33
import type { AllCrudOperations } from './crud/operations/base';
44
import type { FilterKind, QueryOptions, SlicingOptions } from './options';
55

6+
/**
7+
* Checks if a model has any required Unsupported fields (non-optional, no default).
8+
* Uses raw field access since `GetModelFields` excludes Unsupported fields.
9+
*/
10+
export type ModelHasRequiredUnsupportedField<Schema extends SchemaDef, Model extends GetModels<Schema>> = true extends {
11+
[Key in Extract<keyof GetModel<Schema, Model>['fields'], string>]: GetModel<
12+
Schema,
13+
Model
14+
>['fields'][Key] extends infer F extends FieldDef
15+
? F['type'] extends 'Unsupported'
16+
? F['optional'] extends true
17+
? false
18+
: 'default' extends keyof F
19+
? false
20+
: true
21+
: false
22+
: false;
23+
}[Extract<keyof GetModel<Schema, Model>['fields'], string>]
24+
? true
25+
: false;
26+
27+
/**
28+
* Checks if a model allows create operations (not a delegate model and has no required Unsupported fields).
29+
*/
30+
export type ModelAllowsCreate<Schema extends SchemaDef, Model extends GetModels<Schema>> =
31+
IsDelegateModel<Schema, Model> extends true
32+
? false
33+
: ModelHasRequiredUnsupportedField<Schema, Model> extends true
34+
? false
35+
: true;
36+
637
type IsNever<T> = [T] extends [never] ? true : false;
738

839
// #region Model slicing

0 commit comments

Comments
 (0)