Open some-user123 opened 3 years ago
I would love to have this as well in for MySQL. The Date
column type is extremely useful when time isn't a factor.
I think Prisma should completely avoid JavaScript's native Date
object and treat dates as regular strings (or at least Date ISO strings).
Converting dates to instances of Date
sometimes adds unwanted precision:
new Date("2021-01-01").toISOString() // '2021-01-01T00:00:00.000Z'
2021-01-01
is timezone agnostic and carries a different date information than 2021-01-01T00:00:00.000Z
There are also other issues caused by this behavior: https://github.com/prisma/prisma/issues/7490 https://github.com/prisma/prisma/issues/4355
Is there any solution to this issue yet?
Is there any solution to this issue yet?
Same question and problem
I am still hoping for a clean usage scenario. 🙏
To work around this I started converting the input and output of prisma queries. And to help deal with the extra mess on various date formats I started using the approach communicated in https://github.com/freewind-demos/typescript-typing-string-literal-template-iso-date-demo/blob/master/hello.ts
export type IsoDate = `20${'0' | '1' | '2'}${Digit}-${Month}-${Day}`
export type IsoDateLong = `20${'0' | '1' | '2'}${Digit}-${Month}-${Day}T00:00:00.000Z`
import { z } from "zod";
import { IsoDate, IsoDateLong } from "./IsoDate";
export const $IsoDate = z.string()
.regex(/^\d\d\d\d-\d\d-\d\d$/, 'not valid IsoDate')
.transform(x => x as IsoDate);
export const $IsoDateLong = z.string()
.regex(/^\d\d\d\d-\d\d-\d\dT00:00:00\.000Z$/, 'not valid IsoDateLong')
.transform(x => x as IsoDateLong);
export function getIsoDate(input: IsoDateLong | Date): IsoDate {
if (typeof input === 'string') {
if (process.env.NODE_ENV === 'development') {
$IsoDateLong.parse(input)
}
return input.substring(0, 10) as IsoDate;
}
return input.toISOString().substring(0, 10) as IsoDate;
};
export function getIsoDateLong(input: IsoDate | Date): IsoDateLong {
if (typeof input === 'string') {
if (process.env.NODE_ENV === 'development') {
$IsoDate.parse(input)
}
return (input + 'T00:00:00.000Z') as IsoDateLong;
}
return input.toISOString() as IsoDateLong;
};
On my end, this causes problems & confusion when parsing time in the browser.
I had to work around it by adding a Prisma middleware to sanitize the date values.
import { PrismaClient } from "@prisma/client";
import { formatISO } from "date-fns";
import { flatten, unflatten } from "flat";
import _ from "lodash";
export const prisma = new PrismaClient();
// Prisma middleware to sanitize date values
prisma.$use(async (params, next) => {
let result = await next(params);
if (_.isArray(result) || _.isObject(result))
result = sanitizeDateValuesInObject(result);
return result;
});
/**
*
* @param result any object
* @returns object
* This function remove problematic time component from date only values. It converts Date object to YYYY-MM-DD.
* example of @db.Date column result 1995-05-17T00:00:00.000Z
*/
function sanitizeDateValuesInObject(result: any): any {
if (_.isArray(result))
return result.map((r) => sanitizeDateValuesInObject(r));
if (!_.isObject(result)) return result;
result = flatten(result);
result = _.mapValues(result, (value, key) => {
if (
value instanceof Date &&
value.toISOString().endsWith("T00:00:00.000Z")
) {
return formatISO(value, { representation: "date" });
}
return value;
});
result = unflatten(result);
return result;
}
I did a transformer in order to get primitives values of prisma model objects
export function toPrimitives<T extends {}>(object: T): Primitives<T> {
const keys = Object.keys(object);
const oldObject: any = object;
const newObject: any = {};
keys.forEach((key) => {
if (oldObject[key] instanceof Date) newObject[key] = oldObject[key].toISOString();
else if (oldObject[key] instanceof Array) {
newObject[key] = oldObject[key].map((x: any) =>
x instanceof Date
? x.toISOString()
: x instanceof Decimal
? x.toNumber()
: x instanceof Object
? toPrimitives(x)
: x
);
} else if (oldObject[key] instanceof Decimal) newObject[key] = oldObject[key].toNumber();
else if (oldObject[key] instanceof Object) newObject[key] = toPrimitives(oldObject[key]);
else newObject[key] = oldObject[key];
});
return newObject;
}
And Primitives
import { Decimal } from '@prisma/client/runtime';
type Methods<T> = {
[P in keyof T]: T[P] extends Function ? P : never;
}[keyof T];
type MethodsAndProperties<T> = { [key in keyof T]: T[key] };
type Properties<T> = Omit<MethodsAndProperties<T>, Methods<T>>;
export type Primitives<T extends {}> = {
[key in keyof Properties<T>]: T[key] extends string
? T[key]
: T[key] extends number
? T[key]
: T[key] extends Date
? string
: T[key] extends Date | null
? string | null
: T[key] extends Array<Object>
? Array<Primitives<T[key][number]>>
: T[key] extends Decimal
? number
: T[key] extends Object
? Primitives<T[key]>
: T[key];
};
This way I can do const myObject = await prisma.anything.findFirst() const myPrimitiveObject = usePrimitives(myObject) and myPrimitiveObject contains an exactly copy of myObject but Dates are string and Decimal are number. And Primitives type helps me to not lose the types.
I'm not sure if this is a good way at all, but It works for me
+1 for this also.
+1
+1
I used computed fields for this, it doesn't seem to break type checking.
model Appointment {
...
date DateTime @db.Date
...
}
import { format } from "date-fns";
import { PrismaClient } from "@prisma/client";
const prismaClient = new PrismaClient().$extends({
result: {
appointment: {
dateShort: { // this is the computed field.
needs: { date: true },
compute({ date }) {
return format(date, "MMM yyyy");
},
},
},
},
});
const appointmentList = async () => {
const data = await prismaClient.appointment.findFirst();
console.log(appointment.date); // 2023-06-01T00:00:00.000Z
console.log(appointment.dateShort); // 'Jun 2023'
};
I used computed fields for this, it doesn't seem to break type checking. Actually it does. In your case you adding additional field so it is fine. But if you would try to transform existing field as shown here: https://www.prisma.io/blog/client-extensions-preview-8t3w27xkrxxn#example-transformed-fields then you'll get Date in Prisma generated classes but 'string' in the findFirst or any other finder. The only way it should be fixed is if Prisma team would add @transform element to the schema syntax.
Problem
I'm having a
date
column in PostgreSQL. I.e. it only contains the date, no time, no timezone. In Prisma schema I have to define:In my API I only want to specify the date without time. I have to write a lot of nasty transformation, each time this column is read from or written to the database, e.g.
Suggested solution
AFAIK there is no native "dateonly" type in Javascript. I'd really appreciate, if I could specify in the schema that this column should be handled as string:
Alternative: Transformer
An even more powerful alternative would be to allow transformers (e.g. using
class-transformer
) for each column to be specified, that are executed when transferring data from and to the database. This would allow users to have their columns transformed to the type they like to work with.Alternative: Middleware
It was suggested (https://stackoverflow.com/questions/66109340/retrieve-dateonly-column-as-string-or-map-to-string-without-time-using-prisma) to use middlewares for this purpose. While this works in principle, it breaks the type checking. And the strong type checking is one of the biggest advantages of Prisma over alternative solutions. So, I don't want to lose that.
Example:
prisma.model.findMany
will still tell you the date property isDate
while it actually isString
. Also it is tedious to write transformation for all operations, where this model might be included. I'd prefer to declare this once for a column.Additional context
This problem might be applicable to a wide range of column types where it would be nice to handle it in a different type than Prisma suggests. E.g. users could want to handle
Datetime
as instances ofMomentJs
orDayJs
instead of nativeDate
. Or they would like to handleDecimal
asString
orNumber
orBigJs
. They might have custom implementationsdb.Xml
,inet
,cidr
, ...Please let me know, if I should add more information etc. Or there are simple solutions, I might have missed :)