Skip to content
Merged
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
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
- [x] Nested to-many
- [x] Nested to-one
- [x] Incremental update for numeric fields
- [ ] Array update
- [x] Array update
- [x] Upsert
- [x] Delete
- [x] Aggregation
Expand Down
38 changes: 34 additions & 4 deletions packages/runtime/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export type WhereInput<
GetFieldType<Schema, Model, Key>,
FieldIsOptional<Schema, Model, Key>
>
: FieldIsArray<Schema, Model, Key> extends true
? ArrayFilter<GetFieldType<Schema, Model, Key>>
: // primitive
PrimitiveFilter<
GetFieldType<Schema, Model, Key>,
Expand All @@ -183,7 +185,7 @@ export type WhereInput<
NOT?: OrArray<WhereInput<Schema, Model, ScalarOnly>>;
};

export type EnumFilter<
type EnumFilter<
Schema extends SchemaDef,
T extends GetEnums<Schema>,
Nullable extends boolean
Expand All @@ -196,7 +198,15 @@ export type EnumFilter<
not?: EnumFilter<Schema, T, Nullable>;
};

export type PrimitiveFilter<
type ArrayFilter<T extends string> = {
equals?: MapBaseType<T>[];
has?: MapBaseType<T>;
hasEvery?: MapBaseType<T>[];
hasSome?: MapBaseType<T>[];
isEmpty?: boolean;
};

type PrimitiveFilter<
T extends string,
Nullable extends boolean
> = T extends 'String'
Expand Down Expand Up @@ -622,14 +632,26 @@ type CreateScalarPayload<
Schema,
Model,
{
[Key in ScalarFields<Schema, Model, false>]: MapFieldType<
[Key in ScalarFields<Schema, Model, false>]: ScalarCreatePayload<
Schema,
Model,
Key
>;
}
>;

type ScalarCreatePayload<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends ScalarFields<Schema, Model, false>
> =
| MapFieldType<Schema, Model, Field>
| (FieldIsArray<Schema, Model, Field> extends true
? {
set?: MapFieldType<Schema, Model, Field>[];
}
: never);

type CreateFKPayload<
Schema extends SchemaDef,
Model extends GetModels<Schema>
Expand Down Expand Up @@ -802,6 +824,12 @@ type ScalarUpdatePayload<
multiply?: number;
divide?: number;
}
: never)
| (FieldIsArray<Schema, Model, Field> extends true
? {
set?: MapFieldType<Schema, Model, Field>[];
push?: OrArray<MapFieldType<Schema, Model, Field>, true>;
}
: never);

export type UpdateRelationInput<
Expand Down Expand Up @@ -937,7 +965,9 @@ type NumericFields<
Model,
Key
> extends 'Int' | 'Float' | 'BigInt' | 'Decimal'
? Key
? FieldIsArray<Schema, Model, Key> extends true
? never
: Key
: never]: GetField<Schema, Model, Key>;
};

Expand Down
112 changes: 104 additions & 8 deletions packages/runtime/src/client/crud/dialects/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
payload
)
);
} else if (fieldDef.array) {
result = this.and(
eb,
result,
this.buildArrayFilter(
eb,
model,
modelAlias,
key,
fieldDef,
payload
)
);
} else {
result = this.and(
eb,
Expand Down Expand Up @@ -477,37 +490,120 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
return result;
}

private buildArrayFilter(
eb: ExpressionBuilder<any, any>,
model: string,
modelAlias: string,
field: string,
fieldDef: FieldDef,
payload: any
) {
const clauses: Expression<SqlBool>[] = [];
const fieldType = fieldDef.type as BuiltinType;
const fieldRef = buildFieldRef(
this.schema,
model,
field,
this.options,
eb,
modelAlias
);

for (const [key, _value] of Object.entries(payload)) {
if (_value === undefined) {
continue;
}

const value = this.transformPrimitive(_value, fieldType);

switch (key) {
case 'equals': {
clauses.push(
this.buildLiteralFilter(
eb,
fieldRef,
fieldType,
eb.val(value)
)
);
break;
}

case 'has': {
clauses.push(eb(fieldRef, '@>', eb.val([value])));
break;
}

case 'hasEvery': {
clauses.push(eb(fieldRef, '@>', eb.val(value)));
break;
}

case 'hasSome': {
clauses.push(eb(fieldRef, '&&', eb.val(value)));
break;
}

case 'isEmpty': {
clauses.push(
eb(fieldRef, value === true ? '=' : '!=', eb.val([]))
);
break;
}

default: {
throw new InternalError(`Invalid array filter key: ${key}`);
}
}
}

return this.and(eb, ...clauses);
}

buildPrimitiveFilter(
eb: ExpressionBuilder<any, any>,
model: string,
table: string,
modelAlias: string,
field: string,
fieldDef: FieldDef,
payload: any
) {
if (payload === null) {
return eb(sql.ref(`${table}.${field}`), 'is', null);
return eb(sql.ref(`${modelAlias}.${field}`), 'is', null);
}

if (isEnum(this.schema, fieldDef.type)) {
return this.buildEnumFilter(eb, table, field, fieldDef, payload);
return this.buildEnumFilter(
eb,
modelAlias,
field,
fieldDef,
payload
);
}

return match(fieldDef.type as BuiltinType)
.with('String', () =>
this.buildStringFilter(eb, table, field, payload)
this.buildStringFilter(eb, modelAlias, field, payload)
)
.with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) =>
this.buildNumberFilter(eb, model, table, field, type, payload)
this.buildNumberFilter(
eb,
model,
modelAlias,
field,
type,
payload
)
)
.with('Boolean', () =>
this.buildBooleanFilter(eb, table, field, payload)
this.buildBooleanFilter(eb, modelAlias, field, payload)
)
.with('DateTime', () =>
this.buildDateTimeFilter(eb, table, field, payload)
this.buildDateTimeFilter(eb, modelAlias, field, payload)
)
.with('Bytes', () =>
this.buildBytesFilter(eb, table, field, payload)
this.buildBytesFilter(eb, modelAlias, field, payload)
)
.exhaustive();
}
Expand Down
38 changes: 25 additions & 13 deletions packages/runtime/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,24 @@ export class PostgresCrudDialect<
return 'postgresql' as const;
}

override transformPrimitive(value: unknown, type: BuiltinType) {
return match(type)
.with('DateTime', () =>
value instanceof Date
? value
: typeof value === 'string'
? new Date(value)
: value
)
.otherwise(() => value);
override transformPrimitive(value: unknown, type: BuiltinType): unknown {
if (value === undefined) {
return value;
}

if (Array.isArray(value)) {
return value.map((v) => this.transformPrimitive(v, type));
} else {
return match(type)
.with('DateTime', () =>
value instanceof Date
? value
: typeof value === 'string'
? new Date(value)
: value
)
.otherwise(() => value);
}
}

override buildRelationSelection(
Expand Down Expand Up @@ -347,8 +355,12 @@ export class PostgresCrudDialect<
}

override buildArrayLiteralSQL(values: unknown[]): string {
return `ARRAY[${values.map((v) =>
typeof v === 'string' ? `'${v}'` : v
)}]`;
if (values.length === 0) {
return '{}';
} else {
return `ARRAY[${values.map((v) =>
typeof v === 'string' ? `'${v}'` : v
)}]`;
}
}
}
22 changes: 13 additions & 9 deletions packages/runtime/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,23 @@ export class SqliteCrudDialect<
return 'sqlite' as const;
}

override transformPrimitive(value: unknown, type: BuiltinType) {
override transformPrimitive(value: unknown, type: BuiltinType): unknown {
if (value === undefined) {
return value;
}

return match(type)
.with('Boolean', () => (value ? 1 : 0))
.with('DateTime', () =>
value instanceof Date ? value.toISOString() : value
)
.with('Decimal', () => (value as Decimal).toString())
.with('Bytes', () => Buffer.from(value as Uint8Array))
.otherwise(() => value);
if (Array.isArray(value)) {
return value.map((v) => this.transformPrimitive(v, type));
} else {
return match(type)
.with('Boolean', () => (value ? 1 : 0))
.with('DateTime', () =>
value instanceof Date ? value.toISOString() : value
)
.with('Decimal', () => (value as Decimal).toString())
.with('Bytes', () => Buffer.from(value as Uint8Array))
.otherwise(() => value);
}
}

override buildRelationSelection(
Expand Down
Loading