From dc840985d2afdd85e0b311c55aa831bac5da8ce4 Mon Sep 17 00:00:00 2001 From: le0nik <12472674+le0nik@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:36:41 +0400 Subject: [PATCH] feat: add useDate=string-nano option (#981) --- README.markdown | 8 +- integration/emit-default-values-json/test.ts | 2 +- integration/file-suffix/parent.pb.ts | 2 +- .../grpc-js-use-date-false.ts | 2 +- .../google/protobuf/timestamp.ts | 214 ++++++++++++++++++ .../grpc-js-use-date-string-nano-test.ts | 25 ++ .../grpc-js-use-date-string-nano.bin | Bin 0 -> 6956 bytes .../grpc-js-use-date-string-nano.proto | 12 + .../grpc-js-use-date-string-nano.ts | 182 +++++++++++++++ .../parameters.txt | 1 + .../grpc-js-use-date-string.ts | 2 +- .../grpc-js-use-date-true.ts | 2 +- integration/grpc-js/simple.ts | 2 +- integration/import-mapping/mapping.ts | 2 +- integration/import-suffix/parent.pb.ts | 2 +- integration/meta-typings/import_dir/thing.ts | 2 +- integration/meta-typings/simple.ts | 2 +- integration/nice-grpc/simple.ts | 2 +- integration/optional-long/test.ts | 2 +- integration/simple-json-name/simple.ts | 2 +- .../simple-optionals/import_dir/thing.ts | 2 +- integration/simple-optionals/simple.ts | 2 +- integration/simple-optionals/thing.ts | 2 +- .../import_dir/thing.ts | 2 +- .../simple-prototype-defaults/simple.ts | 2 +- integration/simple-snake/import_dir/thing.ts | 2 +- integration/simple-snake/simple.ts | 2 +- .../import_dir/thing.ts | 2 +- .../simple-unrecognized-enum/simple.ts | 2 +- integration/simple/import_dir/thing.ts | 2 +- integration/simple/simple.ts | 2 +- integration/static-only-type-registry/foo.ts | 2 +- integration/static-only/foo.ts | 2 +- integration/type-annotations/foo.ts | 2 +- integration/type-registry/foo.ts | 2 +- integration/use-date-false/metadata.ts | 2 +- integration/use-date-false/use-date-test.ts | 2 +- .../use-date-string/use-date-string.ts | 2 +- integration/use-date-true/use-date-true.ts | 2 +- integration/use-json-name/use-json-name.ts | 2 +- integration/use-map-type/use-map-type.ts | 2 +- integration/use-optionals-all/test.ts | 2 +- package.json | 1 + src/encode.ts | 12 +- src/main.ts | 95 ++++++-- src/options.ts | 1 + src/types.ts | 2 +- yarn.lock | 34 +++ 48 files changed, 600 insertions(+), 57 deletions(-) create mode 100644 integration/grpc-js-use-date-string-nano/google/protobuf/timestamp.ts create mode 100644 integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano-test.ts create mode 100644 integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.bin create mode 100644 integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.proto create mode 100644 integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.ts create mode 100644 integration/grpc-js-use-date-string-nano/parameters.txt diff --git a/README.markdown b/README.markdown index c9a1727f5..f3327f1ef 100644 --- a/README.markdown +++ b/README.markdown @@ -899,12 +899,14 @@ ExampleMessage.encode({ anything: true }); The representation of `google.protobuf.Timestamp` is configurable by the `useDate` flag. The `useJsonTimestamp` flag controls precision when `useDate` is `false`. -| Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` | -| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | -| `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` | +| Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` | `useDate=string-nano` | +| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | --------------------- | +| `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` | `string` | When using `useDate=false` and `useJsonTimestamp=raw` timestamp is represented as `{ seconds: number, nanos: number }`, but has nanosecond precision. +When using `useDate=string-nano` timestamp is represented as an ISO string with nanosecond precision `1970-01-01T14:27:59.987654321Z` and relies on [nano-date](https://www.npmjs.com/package/nano-date) library for conversion. You'll need to install it in your project. + # Number Types Numbers are by default assumed to be plain JavaScript `number`s. diff --git a/integration/emit-default-values-json/test.ts b/integration/emit-default-values-json/test.ts index a2aef5f94..9f1eddf25 100644 --- a/integration/emit-default-values-json/test.ts +++ b/integration/emit-default-values-json/test.ts @@ -726,7 +726,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/file-suffix/parent.pb.ts b/integration/file-suffix/parent.pb.ts index d7a0ebb8c..e7502489d 100644 --- a/integration/file-suffix/parent.pb.ts +++ b/integration/file-suffix/parent.pb.ts @@ -113,7 +113,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/grpc-js-use-date-false/grpc-js-use-date-false.ts b/integration/grpc-js-use-date-false/grpc-js-use-date-false.ts index 577ebe509..304c723d3 100644 --- a/integration/grpc-js-use-date-false/grpc-js-use-date-false.ts +++ b/integration/grpc-js-use-date-false/grpc-js-use-date-false.ts @@ -152,7 +152,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/grpc-js-use-date-string-nano/google/protobuf/timestamp.ts b/integration/grpc-js-use-date-string-nano/google/protobuf/timestamp.ts new file mode 100644 index 000000000..0cf501b86 --- /dev/null +++ b/integration/grpc-js-use-date-string-nano/google/protobuf/timestamp.ts @@ -0,0 +1,214 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; +import Long = require("long"); + +export const protobufPackage = "google.protobuf"; + +/** + * A Timestamp represents a point in time independent of any time zone or local + * calendar, encoded as a count of seconds and fractions of seconds at + * nanosecond resolution. The count is relative to an epoch at UTC midnight on + * January 1, 1970, in the proleptic Gregorian calendar which extends the + * Gregorian calendar backwards to year one. + * + * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + * second table is needed for interpretation, using a [24-hour linear + * smear](https://developers.google.com/time/smear). + * + * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + * restricting to that range, we ensure that we can convert to and from [RFC + * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + * + * # Examples + * + * Example 1: Compute Timestamp from POSIX `time()`. + * + * Timestamp timestamp; + * timestamp.set_seconds(time(NULL)); + * timestamp.set_nanos(0); + * + * Example 2: Compute Timestamp from POSIX `gettimeofday()`. + * + * struct timeval tv; + * gettimeofday(&tv, NULL); + * + * Timestamp timestamp; + * timestamp.set_seconds(tv.tv_sec); + * timestamp.set_nanos(tv.tv_usec * 1000); + * + * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + * + * FILETIME ft; + * GetSystemTimeAsFileTime(&ft); + * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + * + * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + * Timestamp timestamp; + * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + * + * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + * + * long millis = System.currentTimeMillis(); + * + * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + * .setNanos((int) ((millis % 1000) * 1000000)).build(); + * + * Example 5: Compute Timestamp from Java `Instant.now()`. + * + * Instant now = Instant.now(); + * + * Timestamp timestamp = + * Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + * .setNanos(now.getNano()).build(); + * + * Example 6: Compute Timestamp from current time in Python. + * + * timestamp = Timestamp() + * timestamp.GetCurrentTime() + * + * # JSON Mapping + * + * In JSON format, the Timestamp type is encoded as a string in the + * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + * where {year} is always expressed using four digits while {month}, {day}, + * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + * is required. A proto3 JSON serializer should always use UTC (as indicated by + * "Z") when printing the Timestamp type and a proto3 JSON parser should be + * able to accept both UTC and other timezones (as indicated by an offset). + * + * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + * 01:30 UTC on January 15, 2017. + * + * In JavaScript, one can convert a Date object to this format using the + * standard + * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + * method. In Python, a standard `datetime.datetime` object can be converted + * to this format using + * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + * the Joda Time's [`ISODateTimeFormat.dateTime()`]( + * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + * ) to obtain a formatter capable of generating timestamps in this format. + */ +export interface Timestamp { + /** + * Represents seconds of UTC time since Unix epoch + * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59Z inclusive. + */ + seconds: number; + /** + * Non-negative fractions of a second at nanosecond resolution. Negative + * second values with fractions must still have non-negative nanos values + * that count forward in time. Must be from 0 to 999,999,999 + * inclusive. + */ + nanos: number; +} + +function createBaseTimestamp(): Timestamp { + return { seconds: 0, nanos: 0 }; +} + +export const Timestamp = { + encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.seconds !== 0) { + writer.uint32(8).int64(message.seconds); + } + if (message.nanos !== 0) { + writer.uint32(16).int32(message.nanos); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Timestamp { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTimestamp(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.seconds = longToNumber(reader.int64() as Long); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.nanos = reader.int32(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Timestamp { + return { + seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0, + nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0, + }; + }, + + toJSON(message: Timestamp): unknown { + const obj: any = {}; + if (message.seconds !== 0) { + obj.seconds = Math.round(message.seconds); + } + if (message.nanos !== 0) { + obj.nanos = Math.round(message.nanos); + } + return obj; + }, + + create, I>>(base?: I): Timestamp { + return Timestamp.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Timestamp { + const message = createBaseTimestamp(); + message.seconds = object.seconds ?? 0; + message.nanos = object.nanos ?? 0; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(long: Long): number { + if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + return long.toNumber(); +} + +if (_m0.util.Long !== Long) { + _m0.util.Long = Long as any; + _m0.configure(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano-test.ts b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano-test.ts new file mode 100644 index 000000000..192cdbf20 --- /dev/null +++ b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano-test.ts @@ -0,0 +1,25 @@ +/** + * @jest-environment node + */ +import { TestService, TimestampMessage } from "./grpc-js-use-date-string-nano"; + +const jan1 = "1970-01-01T14:27:59.987654321Z"; + +describe("grpc-js-use-date-nano", () => { + it("compiles", () => { + expect(TestService).not.toBeUndefined(); + }); + + it("returns simple date string", async () => { + const encoded = TestService.simpleNow.requestSerialize(jan1); + const decoded = TestService.simpleNow.responseDeserialize(encoded); + expect(decoded).toStrictEqual(jan1); + }); + + it("returns wrapped date string", async () => { + const data: TimestampMessage = { timestamp: jan1 }; + const encoded = TestService.wrappedNow.requestSerialize(data); + const decoded = TestService.wrappedNow.responseDeserialize(encoded); + expect(decoded).toStrictEqual(data); + }); +}); diff --git a/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.bin b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.bin new file mode 100644 index 0000000000000000000000000000000000000000..0c2facb636f5d1a32ee464936a7d80c36e7ff554 GIT binary patch literal 6956 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 PB`013wTrclG-mz{DV;!R literal 0 HcmV?d00001 diff --git a/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.proto b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.proto new file mode 100644 index 000000000..d6fab88e6 --- /dev/null +++ b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package simple; + +service Test { + rpc SimpleNow(google.protobuf.Timestamp) returns (google.protobuf.Timestamp); + rpc WrappedNow(TimestampMessage) returns (TimestampMessage); +} + +message TimestampMessage { google.protobuf.Timestamp timestamp = 1; } diff --git a/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.ts b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.ts new file mode 100644 index 000000000..f89033f9c --- /dev/null +++ b/integration/grpc-js-use-date-string-nano/grpc-js-use-date-string-nano.ts @@ -0,0 +1,182 @@ +/* eslint-disable */ +import { ChannelCredentials, Client, makeGenericClientConstructor, Metadata } from "@grpc/grpc-js"; +import type { + CallOptions, + ClientOptions, + ClientUnaryCall, + handleUnaryCall, + ServiceError, + UntypedServiceImplementation, +} from "@grpc/grpc-js"; +import NanoDate from "nano-date"; +import * as _m0 from "protobufjs/minimal"; +import { Timestamp } from "./google/protobuf/timestamp"; + +export const protobufPackage = "simple"; + +export interface TimestampMessage { + timestamp: string | undefined; +} + +function createBaseTimestampMessage(): TimestampMessage { + return { timestamp: undefined }; +} + +export const TimestampMessage = { + encode(message: TimestampMessage, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.timestamp !== undefined) { + Timestamp.encode(toTimestamp(message.timestamp), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): TimestampMessage { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTimestampMessage(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.timestamp = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): TimestampMessage { + return { timestamp: isSet(object.timestamp) ? globalThis.String(object.timestamp) : undefined }; + }, + + toJSON(message: TimestampMessage): unknown { + const obj: any = {}; + if (message.timestamp !== undefined) { + obj.timestamp = message.timestamp; + } + return obj; + }, + + create, I>>(base?: I): TimestampMessage { + return TimestampMessage.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): TimestampMessage { + const message = createBaseTimestampMessage(); + message.timestamp = object.timestamp ?? undefined; + return message; + }, +}; + +export type TestService = typeof TestService; +export const TestService = { + simpleNow: { + path: "/simple.Test/SimpleNow", + requestStream: false, + responseStream: false, + requestSerialize: (value: string) => Buffer.from(Timestamp.encode(toTimestamp(value)).finish()), + requestDeserialize: (value: Buffer) => fromTimestamp(Timestamp.decode(value)), + responseSerialize: (value: string) => Buffer.from(Timestamp.encode(toTimestamp(value)).finish()), + responseDeserialize: (value: Buffer) => fromTimestamp(Timestamp.decode(value)), + }, + wrappedNow: { + path: "/simple.Test/WrappedNow", + requestStream: false, + responseStream: false, + requestSerialize: (value: TimestampMessage) => Buffer.from(TimestampMessage.encode(value).finish()), + requestDeserialize: (value: Buffer) => TimestampMessage.decode(value), + responseSerialize: (value: TimestampMessage) => Buffer.from(TimestampMessage.encode(value).finish()), + responseDeserialize: (value: Buffer) => TimestampMessage.decode(value), + }, +} as const; + +export interface TestServer extends UntypedServiceImplementation { + simpleNow: handleUnaryCall; + wrappedNow: handleUnaryCall; +} + +export interface TestClient extends Client { + simpleNow(request: string, callback: (error: ServiceError | null, response: string) => void): ClientUnaryCall; + simpleNow( + request: string, + metadata: Metadata, + callback: (error: ServiceError | null, response: string) => void, + ): ClientUnaryCall; + simpleNow( + request: string, + metadata: Metadata, + options: Partial, + callback: (error: ServiceError | null, response: string) => void, + ): ClientUnaryCall; + wrappedNow( + request: TimestampMessage, + callback: (error: ServiceError | null, response: TimestampMessage) => void, + ): ClientUnaryCall; + wrappedNow( + request: TimestampMessage, + metadata: Metadata, + callback: (error: ServiceError | null, response: TimestampMessage) => void, + ): ClientUnaryCall; + wrappedNow( + request: TimestampMessage, + metadata: Metadata, + options: Partial, + callback: (error: ServiceError | null, response: TimestampMessage) => void, + ): ClientUnaryCall; +} + +export const TestClient = makeGenericClientConstructor(TestService, "simple.Test") as unknown as { + new (address: string, credentials: ChannelCredentials, options?: Partial): TestClient; + service: typeof TestService; + serviceName: string; +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function toTimestamp(dateStr: string): Timestamp { + const nanoDate = new NanoDate(dateStr); + + const date = { getTime: (): number => nanoDate.valueOf() } as const; + const seconds = Math.trunc(date.getTime() / 1_000); + + let nanos = nanoDate.getMilliseconds() * 1_000_000; + nanos += nanoDate.getMicroseconds() * 1_000; + nanos += nanoDate.getNanoseconds(); + + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): string { + const seconds = t.seconds || 0; + const nanos = (t.nanos || 0) % 1_000; + const micros = Math.trunc(((t.nanos || 0) % 1_000_000) / 1_000); + let millis = seconds * 1_000; + millis += Math.trunc((t.nanos || 0) / 1_000_000); + + const nanoDate = new NanoDate(millis); + nanoDate.setMicroseconds(micros); + nanoDate.setNanoseconds(nanos); + + return nanoDate.toISOStringFull(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/grpc-js-use-date-string-nano/parameters.txt b/integration/grpc-js-use-date-string-nano/parameters.txt new file mode 100644 index 000000000..693c75e45 --- /dev/null +++ b/integration/grpc-js-use-date-string-nano/parameters.txt @@ -0,0 +1 @@ +outputServices=grpc-js,useDate=string-nano \ No newline at end of file diff --git a/integration/grpc-js-use-date-string/grpc-js-use-date-string.ts b/integration/grpc-js-use-date-string/grpc-js-use-date-string.ts index b0c924dc9..9b5a4faa0 100644 --- a/integration/grpc-js-use-date-string/grpc-js-use-date-string.ts +++ b/integration/grpc-js-use-date-string/grpc-js-use-date-string.ts @@ -151,7 +151,7 @@ export type Exact = P extends Builtin ? P function toTimestamp(dateStr: string): Timestamp { const date = new globalThis.Date(dateStr); - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/grpc-js-use-date-true/grpc-js-use-date-true.ts b/integration/grpc-js-use-date-true/grpc-js-use-date-true.ts index c21eac1a8..40334bf08 100644 --- a/integration/grpc-js-use-date-true/grpc-js-use-date-true.ts +++ b/integration/grpc-js-use-date-true/grpc-js-use-date-true.ts @@ -150,7 +150,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/grpc-js/simple.ts b/integration/grpc-js/simple.ts index 353610cf6..4a136e9aa 100644 --- a/integration/grpc-js/simple.ts +++ b/integration/grpc-js/simple.ts @@ -647,7 +647,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/import-mapping/mapping.ts b/integration/import-mapping/mapping.ts index 8800ed517..44ee57ef2 100644 --- a/integration/import-mapping/mapping.ts +++ b/integration/import-mapping/mapping.ts @@ -335,7 +335,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/import-suffix/parent.pb.ts b/integration/import-suffix/parent.pb.ts index 48210d877..f48d139ea 100644 --- a/integration/import-suffix/parent.pb.ts +++ b/integration/import-suffix/parent.pb.ts @@ -113,7 +113,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/meta-typings/import_dir/thing.ts b/integration/meta-typings/import_dir/thing.ts index 6e0f7a227..af0ebba06 100644 --- a/integration/meta-typings/import_dir/thing.ts +++ b/integration/meta-typings/import_dir/thing.ts @@ -109,7 +109,7 @@ export const protoMetadata: ProtoMetadata = { }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/meta-typings/simple.ts b/integration/meta-typings/simple.ts index 8c39b5b49..937a59762 100644 --- a/integration/meta-typings/simple.ts +++ b/integration/meta-typings/simple.ts @@ -2951,7 +2951,7 @@ export const protoMetadata: ProtoMetadata = { }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/nice-grpc/simple.ts b/integration/nice-grpc/simple.ts index c9824f0a8..cd2f18ad5 100644 --- a/integration/nice-grpc/simple.ts +++ b/integration/nice-grpc/simple.ts @@ -380,7 +380,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/optional-long/test.ts b/integration/optional-long/test.ts index 8dc400d03..ca44c6796 100644 --- a/integration/optional-long/test.ts +++ b/integration/optional-long/test.ts @@ -79,7 +79,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = numberToLong(date.getTime() / 1_000); + const seconds = numberToLong(Math.trunc(date.getTime() / 1_000)); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-json-name/simple.ts b/integration/simple-json-name/simple.ts index f89562f37..7646692af 100644 --- a/integration/simple-json-name/simple.ts +++ b/integration/simple-json-name/simple.ts @@ -203,7 +203,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-optionals/import_dir/thing.ts b/integration/simple-optionals/import_dir/thing.ts index f4e27d406..49ce0ed45 100644 --- a/integration/simple-optionals/import_dir/thing.ts +++ b/integration/simple-optionals/import_dir/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-optionals/simple.ts b/integration/simple-optionals/simple.ts index 8e446f144..a360451d0 100644 --- a/integration/simple-optionals/simple.ts +++ b/integration/simple-optionals/simple.ts @@ -1955,7 +1955,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-optionals/thing.ts b/integration/simple-optionals/thing.ts index b5da9edcc..469d11393 100644 --- a/integration/simple-optionals/thing.ts +++ b/integration/simple-optionals/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-prototype-defaults/import_dir/thing.ts b/integration/simple-prototype-defaults/import_dir/thing.ts index 426053cff..29074100c 100644 --- a/integration/simple-prototype-defaults/import_dir/thing.ts +++ b/integration/simple-prototype-defaults/import_dir/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-prototype-defaults/simple.ts b/integration/simple-prototype-defaults/simple.ts index 7d9bdb633..bd2662386 100644 --- a/integration/simple-prototype-defaults/simple.ts +++ b/integration/simple-prototype-defaults/simple.ts @@ -2950,7 +2950,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-snake/import_dir/thing.ts b/integration/simple-snake/import_dir/thing.ts index 5f80dc135..190d6d8f3 100644 --- a/integration/simple-snake/import_dir/thing.ts +++ b/integration/simple-snake/import_dir/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-snake/simple.ts b/integration/simple-snake/simple.ts index 2fc3b1294..642443709 100644 --- a/integration/simple-snake/simple.ts +++ b/integration/simple-snake/simple.ts @@ -2017,7 +2017,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-unrecognized-enum/import_dir/thing.ts b/integration/simple-unrecognized-enum/import_dir/thing.ts index 0fe76ebd1..6f3f3a7ca 100644 --- a/integration/simple-unrecognized-enum/import_dir/thing.ts +++ b/integration/simple-unrecognized-enum/import_dir/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple-unrecognized-enum/simple.ts b/integration/simple-unrecognized-enum/simple.ts index fe8a7b066..2025b503d 100644 --- a/integration/simple-unrecognized-enum/simple.ts +++ b/integration/simple-unrecognized-enum/simple.ts @@ -1943,7 +1943,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple/import_dir/thing.ts b/integration/simple/import_dir/thing.ts index 0fe76ebd1..6f3f3a7ca 100644 --- a/integration/simple/import_dir/thing.ts +++ b/integration/simple/import_dir/thing.ts @@ -78,7 +78,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/simple/simple.ts b/integration/simple/simple.ts index a611e5a7b..85c8a2adf 100644 --- a/integration/simple/simple.ts +++ b/integration/simple/simple.ts @@ -3072,7 +3072,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/static-only-type-registry/foo.ts b/integration/static-only-type-registry/foo.ts index 6941ffc8b..88479ce04 100644 --- a/integration/static-only-type-registry/foo.ts +++ b/integration/static-only-type-registry/foo.ts @@ -214,7 +214,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/static-only/foo.ts b/integration/static-only/foo.ts index fc9400375..49d3417d0 100644 --- a/integration/static-only/foo.ts +++ b/integration/static-only/foo.ts @@ -207,7 +207,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/type-annotations/foo.ts b/integration/type-annotations/foo.ts index ef4e39ae6..5720cf796 100644 --- a/integration/type-annotations/foo.ts +++ b/integration/type-annotations/foo.ts @@ -210,7 +210,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude | "$type">]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { $type: "google.protobuf.Timestamp", seconds, nanos }; } diff --git a/integration/type-registry/foo.ts b/integration/type-registry/foo.ts index 619415d64..01d135889 100644 --- a/integration/type-registry/foo.ts +++ b/integration/type-registry/foo.ts @@ -217,7 +217,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude | "$type">]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { $type: "google.protobuf.Timestamp", seconds, nanos }; } diff --git a/integration/use-date-false/metadata.ts b/integration/use-date-false/metadata.ts index fd39acde6..ef69aaa8b 100644 --- a/integration/use-date-false/metadata.ts +++ b/integration/use-date-false/metadata.ts @@ -80,7 +80,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/use-date-false/use-date-test.ts b/integration/use-date-false/use-date-test.ts index e81c22ceb..58f599aea 100644 --- a/integration/use-date-false/use-date-test.ts +++ b/integration/use-date-false/use-date-test.ts @@ -25,7 +25,7 @@ describe("useDate=false", () => { expect(Metadata.fromJSON(json).lastEdited).toMatchInlineSnapshot(` { "nanos": 234000000, - "seconds": 123456789.234, + "seconds": 123456789, } `); }); diff --git a/integration/use-date-string/use-date-string.ts b/integration/use-date-string/use-date-string.ts index d20076369..597094714 100644 --- a/integration/use-date-string/use-date-string.ts +++ b/integration/use-date-string/use-date-string.ts @@ -248,7 +248,7 @@ export type Exact = P extends Builtin ? P function toTimestamp(dateStr: string): Timestamp { const date = new globalThis.Date(dateStr); - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/use-date-true/use-date-true.ts b/integration/use-date-true/use-date-true.ts index a5d2b4050..e47a9fd29 100644 --- a/integration/use-date-true/use-date-true.ts +++ b/integration/use-date-true/use-date-true.ts @@ -288,7 +288,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/use-json-name/use-json-name.ts b/integration/use-json-name/use-json-name.ts index d299271a3..bb82ad4da 100644 --- a/integration/use-json-name/use-json-name.ts +++ b/integration/use-json-name/use-json-name.ts @@ -351,7 +351,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/use-map-type/use-map-type.ts b/integration/use-map-type/use-map-type.ts index d577b240f..3934c16bf 100644 --- a/integration/use-map-type/use-map-type.ts +++ b/integration/use-map-type/use-map-type.ts @@ -747,7 +747,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/integration/use-optionals-all/test.ts b/integration/use-optionals-all/test.ts index 831a7a4ec..d1279d8e5 100644 --- a/integration/use-optionals-all/test.ts +++ b/integration/use-optionals-all/test.ts @@ -738,7 +738,7 @@ export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function toTimestamp(date: Date): Timestamp { - const seconds = date.getTime() / 1_000; + const seconds = Math.trunc(date.getTime() / 1_000); const nanos = (date.getTime() % 1_000) * 1_000_000; return { seconds, nanos }; } diff --git a/package.json b/package.json index 08bfa7403..9b8edca61 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "jest": "^29.6.1", "jest-ts-webcompat-resolver": "^1.0.0", "mongodb": "^5.7.0", + "nano-date": "^4.1.0", "nice-grpc": "^2.1.4", "object-hash": "^3.0.0", "prettier": "^2.8.8", diff --git a/src/encode.ts b/src/encode.ts index 0399ff23b..41ff98143 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -14,7 +14,11 @@ export function generateEncoder(ctx: Context, typeName: string): Code { const TimestampValue = impProto(ctx.options, "google/protobuf/timestamp", name); let value = code`value`; - if (ctx.options.useDate === DateOption.DATE || ctx.options.useDate === DateOption.STRING) { + if ( + ctx.options.useDate === DateOption.DATE || + ctx.options.useDate === DateOption.STRING || + ctx.options.useDate === DateOption.STRING_NANO + ) { value = code`${ctx.utils.toTimestamp}(${value})`; } return code`${TimestampValue}.encode(${value}).finish()`; @@ -69,7 +73,11 @@ export function generateDecoder(ctx: Context, typeName: string): Code { TypeValue = impProto(ctx.options, "google/protobuf/timestamp", name); const decoder = code`${TypeValue}.decode(value)`; - if (ctx.options.useDate === DateOption.DATE || ctx.options.useDate === DateOption.STRING) { + if ( + ctx.options.useDate === DateOption.DATE || + ctx.options.useDate === DateOption.STRING || + ctx.options.useDate === DateOption.STRING_NANO + ) { return code`${ctx.utils.fromTimestamp}(${decoder})`; } return decoder; diff --git a/src/main.ts b/src/main.ts index 1b5108a7f..f8c73b30a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -680,24 +680,22 @@ function makeTimestampMethods( bytes: ReturnType, ) { const Timestamp = impProto(options, "google/protobuf/timestamp", "Timestamp"); + const NanoDate = imp("NanoDate=nano-date"); - let seconds: string | Code = "date.getTime() / 1_000"; + let seconds: string | Code = "Math.trunc(date.getTime() / 1_000)"; let toNumberCode: string | Code = "t.seconds"; const makeToNumberCode = (methodCall: string) => `t.seconds${options.useOptionals === "all" ? "?" : ""}.${methodCall}`; if (options.forceLong === LongOption.LONG) { toNumberCode = makeToNumberCode("toNumber()"); - seconds = code`${longs.numberToLong}(date.getTime() / 1_000)`; + seconds = code`${longs.numberToLong}(${seconds})`; } else if (options.forceLong === LongOption.BIGINT) { toNumberCode = code`${bytes.globalThis}.Number(${makeToNumberCode("toString()")})`; - seconds = code`BigInt(Math.trunc(date.getTime() / 1_000))`; + seconds = code`BigInt(${seconds})`; } else if (options.forceLong === LongOption.STRING) { toNumberCode = code`${bytes.globalThis}.Number(t.seconds)`; - // Must discard the fractional piece here - // Otherwise the fraction ends up on the seconds when parsed as a Long - // (note this only occurs when the string is > 8 characters) - seconds = "Math.trunc(date.getTime() / 1_000).toString()"; + seconds = code`${seconds}.toString()`; } const maybeTypeField = addTypeToMessages(options) ? `$type: 'google.protobuf.Timestamp',` : ""; @@ -710,6 +708,23 @@ function makeTimestampMethods( const date = new ${bytes.globalThis}.Date(dateStr); const seconds = ${seconds}; const nanos = (date.getTime() % 1_000) * 1_000_000; + return { ${maybeTypeField} seconds, nanos }; + } + ` + : options.useDate === DateOption.STRING_NANO + ? code` + function toTimestamp(dateStr: string): ${Timestamp} { + const nanoDate = new ${NanoDate}(dateStr); + + const date = { + getTime: (): number => nanoDate.valueOf(), + } as const; + const seconds = ${seconds}; + + let nanos = nanoDate.getMilliseconds() * 1_000_000; + nanos += nanoDate.getMicroseconds() * 1_000; + nanos += nanoDate.getNanoseconds(); + return { ${maybeTypeField} seconds, nanos }; } ` @@ -732,6 +747,22 @@ function makeTimestampMethods( return new ${bytes.globalThis}.Date(millis).toISOString(); } ` + : options.useDate === DateOption.STRING_NANO + ? code` + function fromTimestamp(t: ${Timestamp}): string { + const seconds = ${toNumberCode} || 0; + const nanos = (t.nanos || 0) % 1_000; + const micros = Math.trunc(((t.nanos || 0) % 1_000_000) / 1_000) + let millis = seconds * 1_000; + millis += Math.trunc((t.nanos || 0) / 1_000_000); + + const nanoDate = new ${NanoDate}(millis); + nanoDate.setMicroseconds(micros); + nanoDate.setNanoseconds(nanos); + + return nanoDate.toISOStringFull(); + } + ` : code` function fromTimestamp(t: ${Timestamp}): Date { let millis = (${toNumberCode} || 0) * 1_000; @@ -1043,7 +1074,12 @@ function getDecodeReadSnippet(ctx: Context, field: FieldDescriptorProto) { }; const decoder = code`${type}.decode(reader, reader.uint32())`; readSnippet = code`${unwrap(decoder)}`; - } else if (isTimestamp(field) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING)) { + } else if ( + isTimestamp(field) && + (options.useDate === DateOption.DATE || + options.useDate === DateOption.STRING || + options.useDate === DateOption.STRING_NANO) + ) { const type = basicTypeName(ctx, field, { keepValueType: true }); readSnippet = code`${utils.fromTimestamp}(${type}.decode(reader, reader.uint32()))`; } else if (isObjectId(field) && options.useMongoObjectId) { @@ -1283,7 +1319,12 @@ function getEncodeWriteSnippet(ctx: Context, field: FieldDescriptorProto): (plac const tag = ((field.number << 3) | 2) >>> 0; const type = basicTypeName(ctx, field, { keepValueType: true }); return (place) => code`${type}.encode(${utils.toProtoObjectId}(${place}), writer.uint32(${tag}).fork()).ldelim()`; - } else if (isTimestamp(field) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING)) { + } else if ( + isTimestamp(field) && + (options.useDate === DateOption.DATE || + options.useDate === DateOption.STRING || + options.useDate === DateOption.STRING_NANO) + ) { const tag = ((field.number << 3) | 2) >>> 0; const type = basicTypeName(ctx, field, { keepValueType: true }); return (place) => code`${type}.encode(${utils.toTimestamp}(${place}), writer.uint32(${tag}).fork()).ldelim()`; @@ -1635,7 +1676,12 @@ function generateExtension(ctx: Context, message: DescriptorProto | undefined, e } else if (isObjectId(field) && options.useMongoObjectId) { const type = basicTypeName(ctx, field, { keepValueType: true }); return (place) => code`${type}.encode(${utils.toProtoObjectId}(${place}), writer.fork()).ldelim()`; - } else if (isTimestamp(field) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING)) { + } else if ( + isTimestamp(field) && + (options.useDate === DateOption.DATE || + options.useDate === DateOption.STRING || + options.useDate === DateOption.STRING_NANO) + ) { const type = basicTypeName(ctx, field, { keepValueType: true }); return (place) => code`${type}.encode(${utils.toTimestamp}(${place}), writer.fork()).ldelim()`; } else if (isValueType(ctx, field)) { @@ -1834,7 +1880,10 @@ function generateFromJson(ctx: Context, fullName: string, fullTypeName: string, } } else if (isObjectId(field) && options.useMongoObjectId) { return code`${utils.fromJsonObjectId}(${from})`; - } else if (isTimestamp(field) && options.useDate === DateOption.STRING) { + } else if ( + isTimestamp(field) && + (options.useDate === DateOption.STRING || options.useDate === DateOption.STRING_NANO) + ) { return code`${utils.globalThis}.String(${from})`; } else if ( isTimestamp(field) && @@ -1883,7 +1932,10 @@ function generateFromJson(ctx: Context, fullName: string, fullTypeName: string, } } else if (isObjectId(valueField) && options.useMongoObjectId) { return code`${utils.fromJsonObjectId}(${from})`; - } else if (isTimestamp(valueField) && options.useDate === DateOption.STRING) { + } else if ( + isTimestamp(valueField) && + (options.useDate === DateOption.STRING || options.useDate === DateOption.STRING_NANO) + ) { return code`${utils.globalThis}.String(${from})`; } else if ( isTimestamp(valueField) && @@ -2058,7 +2110,10 @@ function generateToJson( return code`${from}.toString()`; } else if (isTimestamp(field) && options.useDate === DateOption.DATE) { return code`${from}.toISOString()`; - } else if (isTimestamp(field) && options.useDate === DateOption.STRING) { + } else if ( + isTimestamp(field) && + (options.useDate === DateOption.STRING || options.useDate === DateOption.STRING_NANO) + ) { return code`${from}`; } else if (isTimestamp(field) && options.useDate === DateOption.TIMESTAMP) { if (options.useJsonTimestamp === JsonTimestampOption.RAW) { @@ -2077,7 +2132,10 @@ function generateToJson( return code`${from}.toString()`; } else if (isTimestamp(valueType) && options.useDate === DateOption.DATE) { return code`${from}.toISOString()`; - } else if (isTimestamp(valueType) && options.useDate === DateOption.STRING) { + } else if ( + isTimestamp(valueType) && + (options.useDate === DateOption.STRING || options.useDate === DateOption.STRING_NANO) + ) { return code`${from}`; } else if (isTimestamp(valueType) && options.useDate === DateOption.TIMESTAMP) { return code`${utils.fromTimestamp}(${from}).toISOString()`; @@ -2232,7 +2290,10 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri return code`${from} as mongodb.ObjectId`; } else if ( isPrimitive(field) || - (isTimestamp(field) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING)) || + (isTimestamp(field) && + (options.useDate === DateOption.DATE || + options.useDate === DateOption.STRING || + options.useDate === DateOption.STRING_NANO)) || isValueType(ctx, field) ) { return code`${from}`; @@ -2258,7 +2319,9 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri return code`${from} as mongodb.ObjectId`; } else if ( isTimestamp(valueField) && - (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING) + (options.useDate === DateOption.DATE || + options.useDate === DateOption.STRING || + options.useDate === DateOption.STRING_NANO) ) { return code`${from}`; } else if (isValueType(ctx, valueField)) { diff --git a/src/options.ts b/src/options.ts index 0e5661435..5f37bc7a8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -12,6 +12,7 @@ export enum LongOption { export enum DateOption { DATE = "date", STRING = "string", + STRING_NANO = "string-nano", TIMESTAMP = "timestamp", } diff --git a/src/types.ts b/src/types.ts index 5ab96c83c..576150cd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -579,7 +579,7 @@ export function messageToTypeName( return code`Date`; } - if (options.useDate == DateOption.STRING) { + if (options.useDate == DateOption.STRING || options.useDate == DateOption.STRING_NANO) { return code`string`; } } diff --git a/yarn.lock b/yarn.lock index c25b14787..c71b7ec58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,6 +2334,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "bin-links@npm:^4.0.1": version: 4.0.1 resolution: "bin-links@npm:4.0.1" @@ -5428,6 +5435,13 @@ __metadata: languageName: node linkType: hard +"map-or-similar@npm:^1.5.0": + version: 1.5.0 + resolution: "map-or-similar@npm:1.5.0" + checksum: f65c0d420e272d0fce4e24db35f6a08109218480bca1d61eaa442cbe6cf46270b840218d3b5e94e4bfcc2595f1d0a1fa5885df750b52aac9ab1d437b29dcce38 + languageName: node + linkType: hard + "markdown-it-anchor@npm:^8.4.1": version: 8.6.7 resolution: "markdown-it-anchor@npm:8.6.7" @@ -5494,6 +5508,15 @@ __metadata: languageName: node linkType: hard +"memoizerific@npm:^1.11.2": + version: 1.11.3 + resolution: "memoizerific@npm:1.11.3" + dependencies: + map-or-similar: ^1.5.0 + checksum: d51bdc3ed8c39b4b73845c90eb62d243ddf21899914352d0c303f5e1d477abcb192f4c605e008caa4a31d823225eeb22a99ba5ee825fb88d0c33382db3aee95a + languageName: node + linkType: hard + "memory-pager@npm:^1.0.2": version: 1.5.0 resolution: "memory-pager@npm:1.5.0" @@ -5807,6 +5830,16 @@ __metadata: languageName: node linkType: hard +"nano-date@npm:^4.1.0": + version: 4.1.0 + resolution: "nano-date@npm:4.1.0" + dependencies: + bignumber.js: ^9.0.0 + memoizerific: ^1.11.2 + checksum: 7029b3c1db439e612ce4fdf426b33d4d930bfa682edf6c4c8d3d30b66a89d1f995215ebc06b04a53d2b8c4b5a95d0a25250fb7d66f067f9eca0b546fe918698b + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -7852,6 +7885,7 @@ __metadata: jest: ^29.6.1 jest-ts-webcompat-resolver: ^1.0.0 mongodb: ^5.7.0 + nano-date: ^4.1.0 nice-grpc: ^2.1.4 object-hash: ^3.0.0 prettier: ^2.8.8