Skip to content

Commit 653e243

Browse files
committed
several improvements
- transform DateTime values from string to Date before return - support `@default` and `@default(auth()...)`
1 parent 9ac70f7 commit 653e243

File tree

15 files changed

+417
-68
lines changed

15 files changed

+417
-68
lines changed

packages/plugins/swr/src/generator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
resolvePath,
1111
saveProject,
1212
} from '@zenstackhq/sdk';
13-
import { DataModel, DataModelFieldType, Model, isEnum } from '@zenstackhq/sdk/ast';
13+
import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
1414
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
1515
import { paramCase } from 'change-case';
1616
import path from 'path';
@@ -28,8 +28,9 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
2828
const warnings: string[] = [];
2929

3030
const models = getDataModels(model);
31+
const typeDefs = model.declarations.filter(isTypeDef);
3132

32-
await generateModelMeta(project, models, {
33+
await generateModelMeta(project, models, typeDefs, {
3334
output: path.join(outDir, '__model_meta.ts'),
3435
generateAttributes: false,
3536
});

packages/plugins/tanstack-query/src/generator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
resolvePath,
1212
saveProject,
1313
} from '@zenstackhq/sdk';
14-
import { DataModel, DataModelFieldType, Model, isEnum } from '@zenstackhq/sdk/ast';
14+
import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
1515
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
1616
import { paramCase } from 'change-case';
1717
import { lowerCaseFirst } from 'lower-case-first';
@@ -29,6 +29,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
2929
const project = createProject();
3030
const warnings: string[] = [];
3131
const models = getDataModels(model);
32+
const typeDefs = model.declarations.filter(isTypeDef);
3233

3334
const target = requireOption<string>(options, 'target', name);
3435
if (!supportedTargets.includes(target)) {
@@ -44,7 +45,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
4445
outDir = resolvePath(outDir, options);
4546
ensureEmptyDir(outDir);
4647

47-
await generateModelMeta(project, models, {
48+
await generateModelMeta(project, models, typeDefs, {
4849
output: path.join(outDir, '__model_meta.ts'),
4950
generateAttributes: false,
5051
});

packages/runtime/src/cross/model-meta.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type FieldInfo = {
4444
*/
4545
isDataModel?: boolean;
4646

47+
/**
48+
* If the field type is a type def (or an optional/array of type def)
49+
*/
50+
isTypeDef?: boolean;
51+
4752
/**
4853
* If the field is an array
4954
*/
@@ -143,6 +148,21 @@ export type ModelInfo = {
143148
discriminator?: string;
144149
};
145150

151+
/**
152+
* Metadata for a type def
153+
*/
154+
export type TypeDefInfo = {
155+
/**
156+
* TypeDef name
157+
*/
158+
name: string;
159+
160+
/**
161+
* Fields
162+
*/
163+
fields: Record<string, FieldInfo>;
164+
};
165+
146166
/**
147167
* ZModel data model metadata
148168
*/
@@ -152,6 +172,11 @@ export type ModelMeta = {
152172
*/
153173
models: Record<string, ModelInfo>;
154174

175+
/**
176+
* Type defs
177+
*/
178+
typeDefs?: Record<string, TypeDefInfo>;
179+
155180
/**
156181
* Mapping from model name to models that will be deleted because of it due to cascade delete
157182
*/
@@ -171,15 +196,21 @@ export type ModelMeta = {
171196
/**
172197
* Resolves a model field to its metadata. Returns undefined if not found.
173198
*/
174-
export function resolveField(modelMeta: ModelMeta, model: string, field: string): FieldInfo | undefined {
175-
return modelMeta.models[lowerCaseFirst(model)]?.fields?.[field];
199+
export function resolveField(
200+
modelMeta: ModelMeta,
201+
modelOrTypeDef: string,
202+
field: string,
203+
isTypeDef = false
204+
): FieldInfo | undefined {
205+
const container = isTypeDef ? modelMeta.typeDefs : modelMeta.models;
206+
return container?.[lowerCaseFirst(modelOrTypeDef)]?.fields?.[field];
176207
}
177208

178209
/**
179210
* Resolves a model field to its metadata. Throws an error if not found.
180211
*/
181-
export function requireField(modelMeta: ModelMeta, model: string, field: string) {
182-
const f = resolveField(modelMeta, model, field);
212+
export function requireField(modelMeta: ModelMeta, model: string, field: string, isTypeDef = false) {
213+
const f = resolveField(modelMeta, model, field, isTypeDef);
183214
if (!f) {
184215
throw new Error(`Field ${model}.${field} cannot be resolved`);
185216
}

packages/runtime/src/cross/utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { lowerCaseFirst } from 'lower-case-first';
2-
import { requireField, type ModelInfo, type ModelMeta } from '.';
2+
import { requireField, type ModelInfo, type ModelMeta, type TypeDefInfo } from '.';
33

44
/**
55
* Gets field names in a data model entity, filtering out internal fields.
@@ -46,6 +46,9 @@ export function zip<T1, T2>(x: Enumerable<T1>, y: Enumerable<T2>): Array<[T1, T2
4646
}
4747
}
4848

49+
/**
50+
* Gets ID fields of a model.
51+
*/
4952
export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) {
5053
const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {};
5154

@@ -60,6 +63,9 @@ export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound
6063
return entries[0].fields.map((f) => requireField(modelMeta, model, f));
6164
}
6265

66+
/**
67+
* Gets info for a model.
68+
*/
6369
export function getModelInfo<Throw extends boolean = false>(
6470
modelMeta: ModelMeta,
6571
model: string,
@@ -72,6 +78,25 @@ export function getModelInfo<Throw extends boolean = false>(
7278
return info;
7379
}
7480

81+
/**
82+
* Gets info for a type def.
83+
*/
84+
export function getTypeDefInfo<Throw extends boolean = false>(
85+
modelMeta: ModelMeta,
86+
typeDef: string,
87+
throwIfNotFound: Throw = false as Throw
88+
): Throw extends true ? TypeDefInfo : TypeDefInfo | undefined {
89+
const info = modelMeta.typeDefs?.[lowerCaseFirst(typeDef)];
90+
if (!info && throwIfNotFound) {
91+
throw new Error(`Unable to load info for ${typeDef}`);
92+
}
93+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
94+
return info as any;
95+
}
96+
97+
/**
98+
* Checks if a model is a delegate model.
99+
*/
75100
export function isDelegateModel(modelMeta: ModelMeta, model: string) {
76101
return !!getModelInfo(modelMeta, model)?.attributes?.some((attr) => attr.name === '@@delegate');
77102
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../node/json-processor.ts

packages/runtime/src/enhancements/node/create-enhancement.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
} from '../../types';
1111
import { withDefaultAuth } from './default-auth';
1212
import { withDelegate } from './delegate';
13+
import { withJsonProcessor } from './json-processor';
1314
import { Logger } from './logger';
1415
import { withOmit } from './omit';
1516
import { withPassword } from './password';
@@ -90,10 +91,18 @@ export function createEnhancement<DbClient extends object>(
9091

9192
// TODO: move the detection logic into each enhancement
9293
// TODO: how to properly cache the detection result?
94+
9395
const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => Object.values(modelInfo.fields));
96+
if (options.modelMeta.typeDefs) {
97+
allFields.push(
98+
...Object.values(options.modelMeta.typeDefs).flatMap((typeDefInfo) => Object.values(typeDefInfo.fields))
99+
);
100+
}
101+
94102
const hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password'));
95103
const hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit'));
96104
const hasDefaultAuth = allFields.some((field) => field.defaultValueProvider);
105+
const hasTypeDefField = allFields.some((field) => field.isTypeDef);
97106

98107
const kinds = options.kinds ?? ALL_ENHANCEMENTS;
99108
let result = prisma;
@@ -142,5 +151,9 @@ export function createEnhancement<DbClient extends object>(
142151
result = withOmit(result, options);
143152
}
144153

154+
if (hasTypeDefField) {
155+
result = withJsonProcessor(result, options);
156+
}
157+
145158
return result;
146159
}

packages/runtime/src/enhancements/node/default-auth.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
clone,
99
enumerate,
1010
getFields,
11+
getTypeDefInfo,
1112
requireField,
1213
} from '../../cross';
1314
import { DbClientContract, EnhancementContext } from '../../types';
@@ -70,6 +71,11 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
7071
const processCreatePayload = (model: string, data: any) => {
7172
const fields = getFields(this.options.modelMeta, model);
7273
for (const fieldInfo of Object.values(fields)) {
74+
if (fieldInfo.isTypeDef) {
75+
this.setDefaultValueForTypeDefData(fieldInfo.type, data[fieldInfo.name]);
76+
continue;
77+
}
78+
7379
if (fieldInfo.name in data) {
7480
// create payload already sets field value
7581
continue;
@@ -80,10 +86,10 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
8086
continue;
8187
}
8288

83-
const authDefaultValue = this.getDefaultValueFromAuth(fieldInfo);
84-
if (authDefaultValue !== undefined) {
89+
const defaultValue = this.getDefaultValue(fieldInfo);
90+
if (defaultValue !== undefined) {
8591
// set field value extracted from `auth()`
86-
this.setAuthDefaultValue(fieldInfo, model, data, authDefaultValue);
92+
this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue);
8793
}
8894
}
8995
};
@@ -109,7 +115,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
109115
return newArgs;
110116
}
111117

112-
private setAuthDefaultValue(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) {
118+
private setDefaultValueForModelData(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) {
113119
if (fieldInfo.isForeignKey && fieldInfo.relationField && fieldInfo.relationField in data) {
114120
// if the field is a fk, and the relation field is already set, we should not override it
115121
return;
@@ -155,7 +161,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
155161
return entry?.[0];
156162
}
157163

158-
private getDefaultValueFromAuth(fieldInfo: FieldInfo) {
164+
private getDefaultValue(fieldInfo: FieldInfo) {
159165
if (!this.userContext) {
160166
throw prismaClientValidationError(
161167
this.prisma,
@@ -165,4 +171,34 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
165171
}
166172
return fieldInfo.defaultValueProvider?.(this.userContext);
167173
}
174+
175+
private setDefaultValueForTypeDefData(type: string, data: any) {
176+
if (!data || (typeof data !== 'object' && !Array.isArray(data))) {
177+
return;
178+
}
179+
180+
const typeDef = getTypeDefInfo(this.options.modelMeta, type);
181+
if (!typeDef) {
182+
return;
183+
}
184+
185+
enumerate(data).forEach((item) => {
186+
if (!item || typeof item !== 'object') {
187+
return;
188+
}
189+
190+
for (const fieldInfo of Object.values(typeDef.fields)) {
191+
if (fieldInfo.isTypeDef) {
192+
// recurse
193+
this.setDefaultValueForTypeDefData(fieldInfo.type, item[fieldInfo.name]);
194+
} else if (!(fieldInfo.name in item)) {
195+
// set default value if the payload doesn't set the field
196+
const defaultValue = this.getDefaultValue(fieldInfo);
197+
if (defaultValue !== undefined) {
198+
item[fieldInfo.name] = defaultValue;
199+
}
200+
}
201+
}
202+
});
203+
}
168204
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { enumerate, getModelFields, resolveField } from '../../cross';
3+
import { DbClientContract } from '../../types';
4+
import { InternalEnhancementOptions } from './create-enhancement';
5+
import { DefaultPrismaProxyHandler, makeProxy, PrismaProxyActions } from './proxy';
6+
import { QueryUtils } from './query-utils';
7+
8+
/**
9+
* Gets an enhanced Prisma client that post-processes JSON values.
10+
*
11+
* @private
12+
*/
13+
export function withJsonProcessor<DbClient extends object = any>(
14+
prisma: DbClient,
15+
options: InternalEnhancementOptions
16+
): DbClient {
17+
return makeProxy(
18+
prisma,
19+
options.modelMeta,
20+
(_prisma, model) => new JsonProcessorHandler(_prisma as DbClientContract, model, options),
21+
'json-processor'
22+
);
23+
}
24+
25+
class JsonProcessorHandler extends DefaultPrismaProxyHandler {
26+
private queryUtils: QueryUtils;
27+
28+
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
29+
super(prisma, model, options);
30+
this.queryUtils = new QueryUtils(prisma, options);
31+
}
32+
33+
protected override async processResultEntity<T>(_method: PrismaProxyActions, data: T): Promise<T> {
34+
for (const value of enumerate(data)) {
35+
await this.doPostProcess(value, this.model);
36+
}
37+
return data;
38+
}
39+
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
private async doPostProcess(entityData: any, model: string) {
42+
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
43+
44+
for (const field of getModelFields(entityData)) {
45+
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
46+
if (!fieldInfo) {
47+
continue;
48+
}
49+
50+
if (fieldInfo.isTypeDef) {
51+
this.fixJsonDateFields(entityData[field], fieldInfo.type);
52+
} else if (fieldInfo.isDataModel) {
53+
const items =
54+
fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]];
55+
for (const item of items) {
56+
// recurse
57+
await this.doPostProcess(item, fieldInfo.type);
58+
}
59+
}
60+
}
61+
}
62+
63+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
private fixJsonDateFields(entityData: any, typeDef: string) {
65+
if (typeof entityData !== 'object' && !Array.isArray(entityData)) {
66+
return;
67+
}
68+
69+
enumerate(entityData).forEach((item) => {
70+
if (!item || typeof item !== 'object') {
71+
return;
72+
}
73+
74+
for (const [key, value] of Object.entries(item)) {
75+
const fieldInfo = resolveField(this.options.modelMeta, typeDef, key, true);
76+
if (!fieldInfo) {
77+
continue;
78+
}
79+
if (fieldInfo.isTypeDef) {
80+
// recurse
81+
this.fixJsonDateFields(value, fieldInfo.type);
82+
} else if (fieldInfo.type === 'DateTime' && typeof value === 'string') {
83+
// convert to Date
84+
const parsed = Date.parse(value);
85+
if (!isNaN(parsed)) {
86+
item[key] = new Date(parsed);
87+
}
88+
}
89+
}
90+
});
91+
}
92+
}

0 commit comments

Comments
 (0)