Skip to content

Commit 55d4e77

Browse files
authored
feat: strongly typed JSON fields (#107)
1 parent 39cf629 commit 55d4e77

File tree

12 files changed

+376
-66
lines changed

12 files changed

+376
-66
lines changed

packages/language/res/stdlib.zmodel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ function raw(value: String): Any {
701701
/**
702702
* Marks a field to be strong-typed JSON.
703703
*/
704-
attribute @json() @@@targetField([TypeDefField]) @@@deprecated('The "@json" attribute is not needed anymore. ZenStack will automatically use JSON to store typed fields.')
704+
attribute @json() @@@targetField([TypeDefField])
705705

706706
/**
707707
* Marks a field to be computed.

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

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {
44
FieldDef,
55
FieldHasDefault,
66
FieldIsArray,
7-
FieldIsOptional,
87
FieldIsRelation,
98
FieldIsRelationArray,
109
FieldType,
@@ -19,12 +18,14 @@ import type {
1918
GetTypeDefField,
2019
GetTypeDefFields,
2120
GetTypeDefs,
21+
ModelFieldIsOptional,
2222
NonRelationFields,
2323
RelationFields,
2424
RelationFieldType,
2525
RelationInfo,
2626
ScalarFields,
2727
SchemaDef,
28+
TypeDefFieldIsOptional,
2829
} from '../schema';
2930
import type {
3031
AtLeast,
@@ -86,21 +87,21 @@ type ModelSelectResult<Schema extends SchemaDef, Model extends GetModels<Schema>
8687
Schema,
8788
RelationFieldType<Schema, Model, Key>,
8889
Pick<Select[Key], 'select'>,
89-
FieldIsOptional<Schema, Model, Key>,
90+
ModelFieldIsOptional<Schema, Model, Key>,
9091
FieldIsArray<Schema, Model, Key>
9192
>
9293
: ModelResult<
9394
Schema,
9495
RelationFieldType<Schema, Model, Key>,
9596
Pick<Select[Key], 'include' | 'omit'>,
96-
FieldIsOptional<Schema, Model, Key>,
97+
ModelFieldIsOptional<Schema, Model, Key>,
9798
FieldIsArray<Schema, Model, Key>
9899
>
99100
: DefaultModelResult<
100101
Schema,
101102
RelationFieldType<Schema, Model, Key>,
102103
Omit,
103-
FieldIsOptional<Schema, Model, Key>,
104+
ModelFieldIsOptional<Schema, Model, Key>,
104105
FieldIsArray<Schema, Model, Key>
105106
>
106107
: never;
@@ -143,14 +144,14 @@ export type ModelResult<
143144
Schema,
144145
RelationFieldType<Schema, Model, Key>,
145146
I[Key],
146-
FieldIsOptional<Schema, Model, Key>,
147+
ModelFieldIsOptional<Schema, Model, Key>,
147148
FieldIsArray<Schema, Model, Key>
148149
>
149150
: DefaultModelResult<
150151
Schema,
151152
RelationFieldType<Schema, Model, Key>,
152153
undefined,
153-
FieldIsOptional<Schema, Model, Key>,
154+
ModelFieldIsOptional<Schema, Model, Key>,
154155
FieldIsArray<Schema, Model, Key>
155156
>;
156157
}
@@ -169,9 +170,17 @@ export type SimplifiedModelResult<
169170
Array = false,
170171
> = Simplify<ModelResult<Schema, Model, Args, Optional, Array>>;
171172

172-
export type TypeDefResult<Schema extends SchemaDef, TypeDef extends GetTypeDefs<Schema>> = {
173-
[Key in GetTypeDefFields<Schema, TypeDef>]: MapTypeDefFieldType<Schema, TypeDef, Key>;
174-
};
173+
export type TypeDefResult<Schema extends SchemaDef, TypeDef extends GetTypeDefs<Schema>> = Optional<
174+
{
175+
[Key in GetTypeDefFields<Schema, TypeDef>]: MapTypeDefFieldType<Schema, TypeDef, Key>;
176+
},
177+
// optionality
178+
keyof {
179+
[Key in GetTypeDefFields<Schema, TypeDef> as TypeDefFieldIsOptional<Schema, TypeDef, Key> extends true
180+
? Key
181+
: never]: Key;
182+
}
183+
>;
175184

176185
export type BatchResult = { count: number };
177186

@@ -193,11 +202,11 @@ export type WhereInput<
193202
RelationFilter<Schema, Model, Key>
194203
: // enum
195204
GetModelFieldType<Schema, Model, Key> extends GetEnums<Schema>
196-
? EnumFilter<Schema, GetModelFieldType<Schema, Model, Key>, FieldIsOptional<Schema, Model, Key>>
205+
? EnumFilter<Schema, GetModelFieldType<Schema, Model, Key>, ModelFieldIsOptional<Schema, Model, Key>>
197206
: FieldIsArray<Schema, Model, Key> extends true
198207
? ArrayFilter<GetModelFieldType<Schema, Model, Key>>
199208
: // primitive
200-
PrimitiveFilter<GetModelFieldType<Schema, Model, Key>, FieldIsOptional<Schema, Model, Key>>;
209+
PrimitiveFilter<GetModelFieldType<Schema, Model, Key>, ModelFieldIsOptional<Schema, Model, Key>>;
201210
} & {
202211
$expr?: (eb: ExpressionBuilder<ToKyselySchema<Schema>, Model>) => OperandExpression<SqlBool>;
203212
} & {
@@ -290,7 +299,7 @@ export type OrderBy<
290299
WithRelation extends boolean,
291300
WithAggregation extends boolean,
292301
> = {
293-
[Key in NonRelationFields<Schema, Model>]?: FieldIsOptional<Schema, Model, Key> extends true
302+
[Key in NonRelationFields<Schema, Model>]?: ModelFieldIsOptional<Schema, Model, Key> extends true
294303
?
295304
| SortOrder
296305
| {
@@ -391,7 +400,7 @@ export type IncludeInput<Schema extends SchemaDef, Model extends GetModels<Schem
391400
// where clause is allowed only if the relation is array or optional
392401
FieldIsArray<Schema, Model, Key> extends true
393402
? true
394-
: FieldIsOptional<Schema, Model, Key> extends true
403+
: ModelFieldIsOptional<Schema, Model, Key> extends true
395404
? true
396405
: false
397406
>;
@@ -427,14 +436,14 @@ type ToOneRelationFilter<
427436
WhereInput<Schema, RelationFieldType<Schema, Model, Field>> & {
428437
is?: NullableIf<
429438
WhereInput<Schema, RelationFieldType<Schema, Model, Field>>,
430-
FieldIsOptional<Schema, Model, Field>
439+
ModelFieldIsOptional<Schema, Model, Field>
431440
>;
432441
isNot?: NullableIf<
433442
WhereInput<Schema, RelationFieldType<Schema, Model, Field>>,
434-
FieldIsOptional<Schema, Model, Field>
443+
ModelFieldIsOptional<Schema, Model, Field>
435444
>;
436445
},
437-
FieldIsOptional<Schema, Model, Field>
446+
ModelFieldIsOptional<Schema, Model, Field>
438447
>;
439448

440449
type RelationFilter<
@@ -460,23 +469,20 @@ type MapTypeDefFieldType<
460469
Schema extends SchemaDef,
461470
TypeDef extends GetTypeDefs<Schema>,
462471
Field extends GetTypeDefFields<Schema, TypeDef>,
463-
> =
464-
GetTypeDefField<Schema, TypeDef, Field>['type'] extends GetTypeDefs<Schema>
465-
? WrapType<
466-
TypeDefResult<Schema, GetTypeDefField<Schema, TypeDef, Field>['type']>,
467-
GetTypeDefField<Schema, TypeDef, Field>['optional'],
468-
GetTypeDefField<Schema, TypeDef, Field>['array']
469-
>
470-
: MapFieldDefType<Schema, GetTypeDefField<Schema, TypeDef, Field>>;
472+
> = MapFieldDefType<Schema, GetTypeDefField<Schema, TypeDef, Field>>;
471473

472474
type MapFieldDefType<Schema extends SchemaDef, T extends Pick<FieldDef, 'type' | 'optional' | 'array'>> = WrapType<
473-
T['type'] extends GetEnums<Schema> ? keyof GetEnum<Schema, T['type']> : MapBaseType<T['type']>,
475+
T['type'] extends GetEnums<Schema>
476+
? keyof GetEnum<Schema, T['type']>
477+
: T['type'] extends GetTypeDefs<Schema>
478+
? TypeDefResult<Schema, T['type']> & Record<string, unknown>
479+
: MapBaseType<T['type']>,
474480
T['optional'],
475481
T['array']
476482
>;
477483

478484
type OptionalFieldsForCreate<Schema extends SchemaDef, Model extends GetModels<Schema>> = keyof {
479-
[Key in GetModelFields<Schema, Model> as FieldIsOptional<Schema, Model, Key> extends true
485+
[Key in GetModelFields<Schema, Model> as ModelFieldIsOptional<Schema, Model, Key> extends true
480486
? Key
481487
: FieldHasDefault<Schema, Model, Key> extends true
482488
? Key
@@ -752,7 +758,7 @@ type ScalarUpdatePayload<
752758
| MapModelFieldType<Schema, Model, Field>
753759
| (Field extends NumericFields<Schema, Model>
754760
? {
755-
set?: NullableIf<number, FieldIsOptional<Schema, Model, Field>>;
761+
set?: NullableIf<number, ModelFieldIsOptional<Schema, Model, Field>>;
756762
increment?: number;
757763
decrement?: number;
758764
multiply?: number;
@@ -820,7 +826,7 @@ type ToOneRelationUpdateInput<
820826
connectOrCreate?: ConnectOrCreateInput<Schema, Model, Field>;
821827
update?: NestedUpdateInput<Schema, Model, Field>;
822828
upsert?: NestedUpsertInput<Schema, Model, Field>;
823-
} & (FieldIsOptional<Schema, Model, Field> extends true
829+
} & (ModelFieldIsOptional<Schema, Model, Field> extends true
824830
? {
825831
disconnect?: DisconnectInput<Schema, Model, Field>;
826832
delete?: NestedDeleteInput<Schema, Model, Field>;

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,18 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
3434
if (Array.isArray(value)) {
3535
return value.map((v) => this.transformPrimitive(v, type, false));
3636
} else {
37-
return match(type)
38-
.with('Boolean', () => (value ? 1 : 0))
39-
.with('DateTime', () => (value instanceof Date ? value.toISOString() : value))
40-
.with('Decimal', () => (value as Decimal).toString())
41-
.with('Bytes', () => Buffer.from(value as Uint8Array))
42-
.with('Json', () => JSON.stringify(value))
43-
.otherwise(() => value);
37+
if (this.schema.typeDefs && type in this.schema.typeDefs) {
38+
// typed JSON field
39+
return JSON.stringify(value);
40+
} else {
41+
return match(type)
42+
.with('Boolean', () => (value ? 1 : 0))
43+
.with('DateTime', () => (value instanceof Date ? value.toISOString() : value))
44+
.with('Decimal', () => (value as Decimal).toString())
45+
.with('Bytes', () => Buffer.from(value as Uint8Array))
46+
.with('Json', () => JSON.stringify(value))
47+
.otherwise(() => value);
48+
}
4449
}
4550
}
4651

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
493493
const idFields = getIdFields(this.schema, model);
494494
const query = kysely
495495
.insertInto(model)
496-
.values(updatedData)
496+
.$if(Object.keys(updatedData).length === 0, (qb) => qb.defaultValues())
497+
.$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData))
497498
.returning(idFields as any)
498499
.modifyEnd(
499500
this.makeContextComment({

packages/runtime/src/client/crud/validator.ts

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,46 @@ export class InputValidator<Schema extends SchemaDef> {
218218
}
219219

220220
private makePrimitiveSchema(type: string) {
221-
return match(type)
222-
.with('String', () => z.string())
223-
.with('Int', () => z.number())
224-
.with('Float', () => z.number())
225-
.with('Boolean', () => z.boolean())
226-
.with('BigInt', () => z.union([z.number(), z.bigint()]))
227-
.with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()]))
228-
.with('DateTime', () => z.union([z.date(), z.string().datetime()]))
229-
.with('Bytes', () => z.instanceof(Uint8Array))
230-
.otherwise(() => z.unknown());
221+
if (this.schema.typeDefs && type in this.schema.typeDefs) {
222+
return this.makeTypeDefSchema(type);
223+
} else {
224+
return match(type)
225+
.with('String', () => z.string())
226+
.with('Int', () => z.number())
227+
.with('Float', () => z.number())
228+
.with('Boolean', () => z.boolean())
229+
.with('BigInt', () => z.union([z.number(), z.bigint()]))
230+
.with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()]))
231+
.with('DateTime', () => z.union([z.date(), z.string().datetime()]))
232+
.with('Bytes', () => z.instanceof(Uint8Array))
233+
.otherwise(() => z.unknown());
234+
}
235+
}
236+
237+
private makeTypeDefSchema(type: string): z.ZodType {
238+
const key = `$typedef-${type}`;
239+
let schema = this.schemaCache.get(key);
240+
if (schema) {
241+
return schema;
242+
}
243+
const typeDef = this.schema.typeDefs?.[type];
244+
invariant(typeDef, `Type definition "${type}" not found in schema`);
245+
schema = z.looseObject(
246+
Object.fromEntries(
247+
Object.entries(typeDef.fields).map(([field, def]) => {
248+
let fieldSchema = this.makePrimitiveSchema(def.type);
249+
if (def.array) {
250+
fieldSchema = fieldSchema.array();
251+
}
252+
if (def.optional) {
253+
fieldSchema = fieldSchema.optional();
254+
}
255+
return [field, fieldSchema];
256+
}),
257+
),
258+
);
259+
this.schemaCache.set(key, schema);
260+
return schema;
231261
}
232262

233263
private makeWhereSchema(model: string, unique: boolean, withoutRelationFields = false): ZodType {
@@ -396,6 +426,10 @@ export class InputValidator<Schema extends SchemaDef> {
396426
}
397427

398428
private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean) {
429+
if (this.schema.typeDefs && type in this.schema.typeDefs) {
430+
// typed JSON field
431+
return this.makeTypeDefFilterSchema(type, optional);
432+
}
399433
return (
400434
match(type)
401435
.with('String', () => this.makeStringFilterSchema(optional))
@@ -412,6 +446,11 @@ export class InputValidator<Schema extends SchemaDef> {
412446
);
413447
}
414448

449+
private makeTypeDefFilterSchema(_type: string, _optional: boolean) {
450+
// TODO: strong typed JSON filtering
451+
return z.never();
452+
}
453+
415454
private makeDateTimeFilterSchema(optional: boolean): ZodType {
416455
return this.makeCommonPrimitiveFilterSchema(z.union([z.string().datetime(), z.date()]), optional, () =>
417456
z.lazy(() => this.makeDateTimeFilterSchema(optional)),

packages/runtime/src/client/query-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import type Decimal from 'decimal.js';
22
import type { Generated, Kysely } from 'kysely';
33
import type {
44
FieldHasDefault,
5-
FieldIsOptional,
65
ForeignKeyFields,
76
GetModelFields,
87
GetModelFieldType,
98
GetModels,
9+
ModelFieldIsOptional,
1010
ScalarFields,
1111
SchemaDef,
1212
} from '../schema';
@@ -45,7 +45,7 @@ type MapType<
4545
Schema extends SchemaDef,
4646
Model extends GetModels<Schema>,
4747
Field extends GetModelFields<Schema, Model>,
48-
> = WrapNull<MapBaseType<GetModelFieldType<Schema, Model, Field>>, FieldIsOptional<Schema, Model, Field>>;
48+
> = WrapNull<MapBaseType<GetModelFieldType<Schema, Model, Field>>, ModelFieldIsOptional<Schema, Model, Field>>;
4949

5050
type toKyselyFieldType<
5151
Schema extends SchemaDef,

packages/runtime/src/client/result-processor.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,19 @@ export class ResultProcessor<Schema extends SchemaDef> {
8484
}
8585

8686
private transformScalar(value: unknown, type: BuiltinType) {
87-
return match(type)
88-
.with('Boolean', () => this.transformBoolean(value))
89-
.with('DateTime', () => this.transformDate(value))
90-
.with('Bytes', () => this.transformBytes(value))
91-
.with('Decimal', () => this.transformDecimal(value))
92-
.with('BigInt', () => this.transformBigInt(value))
93-
.with('Json', () => this.transformJson(value))
94-
.otherwise(() => value);
87+
if (this.schema.typeDefs && type in this.schema.typeDefs) {
88+
// typed JSON field
89+
return this.transformJson(value);
90+
} else {
91+
return match(type)
92+
.with('Boolean', () => this.transformBoolean(value))
93+
.with('DateTime', () => this.transformDate(value))
94+
.with('Bytes', () => this.transformBytes(value))
95+
.with('Decimal', () => this.transformDecimal(value))
96+
.with('BigInt', () => this.transformBigInt(value))
97+
.with('Json', () => this.transformJson(value))
98+
.otherwise(() => value);
99+
}
95100
}
96101

97102
private transformDecimal(value: unknown) {

0 commit comments

Comments
 (0)