@fintekkers/ledger-models 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,20 @@ import { LocalTimestampProto } from '../../../fintekkers/models/util/local_times
2
2
  import { DateTime } from 'luxon';
3
3
  declare class ZonedDateTime {
4
4
  private proto;
5
+ /**
6
+ * Wraps a LocalTimestampProto.
7
+ *
8
+ * Throws if `time_zone` is empty/whitespace — luxon's DateTime would
9
+ * otherwise silently produce an invalid DateTime (isValid=false,
10
+ * year=NaN), which propagates as silent corruption rather than a clear
11
+ * failure. See second-brain#276 for the original report from
12
+ * backend-dev-ledger during #268 verification.
13
+ *
14
+ * Callers with optional/unset timestamps should gate
15
+ * `new ZonedDateTime(parent.getAsOf())` with a `parent.hasAsOf()`
16
+ * check at the call site rather than relying on the constructor to
17
+ * substitute a default.
18
+ */
5
19
  constructor(proto: LocalTimestampProto);
6
20
  getTimezone(): string;
7
21
  getSeconds(): number;
@@ -5,7 +5,27 @@ const local_timestamp_pb_1 = require("../../../fintekkers/models/util/local_time
5
5
  const timestamp_pb_1 = require("google-protobuf/google/protobuf/timestamp_pb");
6
6
  const luxon_1 = require("luxon");
7
7
  class ZonedDateTime {
8
+ /**
9
+ * Wraps a LocalTimestampProto.
10
+ *
11
+ * Throws if `time_zone` is empty/whitespace — luxon's DateTime would
12
+ * otherwise silently produce an invalid DateTime (isValid=false,
13
+ * year=NaN), which propagates as silent corruption rather than a clear
14
+ * failure. See second-brain#276 for the original report from
15
+ * backend-dev-ledger during #268 verification.
16
+ *
17
+ * Callers with optional/unset timestamps should gate
18
+ * `new ZonedDateTime(parent.getAsOf())` with a `parent.hasAsOf()`
19
+ * check at the call site rather than relying on the constructor to
20
+ * substitute a default.
21
+ */
8
22
  constructor(proto) {
23
+ const tz = proto.getTimeZone();
24
+ if (!tz || tz.trim().length === 0) {
25
+ throw new Error("LocalTimestampProto.time_zone is required but was empty. "
26
+ + "Producers must set time_zone (e.g. \"UTC\" or \"America/New_York\") "
27
+ + "when populating LocalTimestampProto. See second-brain#276.");
28
+ }
9
29
  this.proto = proto;
10
30
  }
11
31
  getTimezone() {
@@ -1 +1 @@
1
- {"version":3,"file":"datetime.js","sourceRoot":"","sources":["datetime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"datetime.js","sourceRoot":"","sources":["datetime.ts"],"names":[],"mappings":"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"}
@@ -50,4 +50,42 @@ test('test the date time', () => __awaiter(void 0, void 0, void 0, function* ()
50
50
  //Expect timestamp match
51
51
  expect(timestampStr).toMatch(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/);
52
52
  }));
53
+ // second-brain#276 — lock in that the ZonedDateTime constructor throws on
54
+ // empty time_zone. Pre-fix: luxon's DateTime silently produced
55
+ // isValid=false / year=NaN, indistinguishable from a real value at most
56
+ // call sites until much later. Now: loud failure at construction.
57
+ describe('ZonedDateTime constructor — second-brain#276', () => {
58
+ test('throws when time_zone is empty (proto3 default)', () => {
59
+ const proto = new local_timestamp_pb_1.LocalTimestampProto();
60
+ const ts = new timestamp_pb_js_1.Timestamp();
61
+ ts.setSeconds(1700000000);
62
+ proto.setTimestamp(ts);
63
+ // time_zone left at the proto3 default ""
64
+ expect(() => new datetime_1.ZonedDateTime(proto)).toThrow(/time_zone is required/);
65
+ });
66
+ test('throws when time_zone is whitespace-only', () => {
67
+ const proto = new local_timestamp_pb_1.LocalTimestampProto();
68
+ const ts = new timestamp_pb_js_1.Timestamp();
69
+ ts.setSeconds(1700000000);
70
+ proto.setTimestamp(ts);
71
+ proto.setTimeZone(' ');
72
+ expect(() => new datetime_1.ZonedDateTime(proto)).toThrow(/time_zone is required/);
73
+ });
74
+ test('throws on fully-default LocalTimestampProto', () => {
75
+ // No timestamp, no time_zone — wholly default instance.
76
+ expect(() => new datetime_1.ZonedDateTime(new local_timestamp_pb_1.LocalTimestampProto()))
77
+ .toThrow(/time_zone is required/);
78
+ });
79
+ test('happy path with UTC still constructs successfully', () => {
80
+ const proto = new local_timestamp_pb_1.LocalTimestampProto();
81
+ const ts = new timestamp_pb_js_1.Timestamp();
82
+ ts.setSeconds(1700000000);
83
+ proto.setTimestamp(ts);
84
+ proto.setTimeZone('UTC');
85
+ const zdt = new datetime_1.ZonedDateTime(proto);
86
+ const dt = zdt.toDateTime();
87
+ expect(dt.isValid).toBe(true);
88
+ expect(dt.year).toBe(2023);
89
+ });
90
+ });
53
91
  //# sourceMappingURL=datetime.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"datetime.test.js","sourceRoot":"","sources":["datetime.test.ts"],"names":[],"mappings":";;;;;;;;;;;AACA,2EAA0E;AAC1E,iFAAsH;AACtH,2FAAqF;AACrF,mDAAgD;AAChD,yCAA2C;AAC3C,mEAA6D;AAC7D,2FAAuF;AACvF,qFAA4E;AAC5E,wEAAwE;AAExE,IAAI,CAAC,oBAAoB,EAAE,GAAS,EAAE;IAClC,MAAM,mBAAmB,GAAG,IAAI,wCAAmB,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,IAAI,2BAAS,EAAE,CAAC;IAClC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB;IACpD,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;IAC7C,mBAAmB,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IAEhD,oBAAoB;IAChB,mBAAmB,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,wBAAa,CAAC,mBAAmB,CAAC,CAAC;IAEnD,IAAI,kBAAkB,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC;IACrD,MAAM,CAAC,kBAAkB,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,CAAC,kBAAkB,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAEjD,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,IAAI,YAAG,EAAE,CAAC;IAC5B,SAAS,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACvC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,CAAC;IAE/C,MAAM,QAAQ,GAAG,IAAI,2BAAa,EAAE,CAAC;IAErC,iBAAiB;IACjB,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IACpC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,+BAAiB,CAAC,YAAY,CAAC,CAAC;IACzD,QAAQ,CAAC,eAAe,CAAC,+BAAiB,CAAC,WAAW,CAAC,CAAC;IAExD,aAAa;IACb,MAAM,MAAM,GAAG,IAAI,gCAAa,EAAE,CAAC;IACnC,MAAM,CAAC,QAAQ,CAAC,qBAAU,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE3B,MAAM,GAAG,GAAG,IAAI,mBAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,YAAY,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAEjD,wBAAwB;IACxB,MAAM,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,2DAA2D,CAAC,CAAC;AAC9F,CAAC,CAAA,CAAC,CAAC"}
1
+ {"version":3,"file":"datetime.test.js","sourceRoot":"","sources":["datetime.test.ts"],"names":[],"mappings":"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"}
@@ -48,4 +48,50 @@ test('test the date time', async () => {
48
48
 
49
49
  //Expect timestamp match
50
50
  expect(timestampStr).toMatch(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/);
51
+ });
52
+
53
+ // second-brain#276 — lock in that the ZonedDateTime constructor throws on
54
+ // empty time_zone. Pre-fix: luxon's DateTime silently produced
55
+ // isValid=false / year=NaN, indistinguishable from a real value at most
56
+ // call sites until much later. Now: loud failure at construction.
57
+
58
+ describe('ZonedDateTime constructor — second-brain#276', () => {
59
+ test('throws when time_zone is empty (proto3 default)', () => {
60
+ const proto = new LocalTimestampProto();
61
+ const ts = new Timestamp();
62
+ ts.setSeconds(1_700_000_000);
63
+ proto.setTimestamp(ts);
64
+ // time_zone left at the proto3 default ""
65
+
66
+ expect(() => new ZonedDateTime(proto)).toThrow(/time_zone is required/);
67
+ });
68
+
69
+ test('throws when time_zone is whitespace-only', () => {
70
+ const proto = new LocalTimestampProto();
71
+ const ts = new Timestamp();
72
+ ts.setSeconds(1_700_000_000);
73
+ proto.setTimestamp(ts);
74
+ proto.setTimeZone(' ');
75
+
76
+ expect(() => new ZonedDateTime(proto)).toThrow(/time_zone is required/);
77
+ });
78
+
79
+ test('throws on fully-default LocalTimestampProto', () => {
80
+ // No timestamp, no time_zone — wholly default instance.
81
+ expect(() => new ZonedDateTime(new LocalTimestampProto()))
82
+ .toThrow(/time_zone is required/);
83
+ });
84
+
85
+ test('happy path with UTC still constructs successfully', () => {
86
+ const proto = new LocalTimestampProto();
87
+ const ts = new Timestamp();
88
+ ts.setSeconds(1_700_000_000);
89
+ proto.setTimestamp(ts);
90
+ proto.setTimeZone('UTC');
91
+
92
+ const zdt = new ZonedDateTime(proto);
93
+ const dt = zdt.toDateTime();
94
+ expect(dt.isValid).toBe(true);
95
+ expect(dt.year).toBe(2023);
96
+ });
51
97
  });
@@ -5,7 +5,29 @@ import { DateTime } from 'luxon';
5
5
  class ZonedDateTime {
6
6
  private proto: LocalTimestampProto;
7
7
 
8
+ /**
9
+ * Wraps a LocalTimestampProto.
10
+ *
11
+ * Throws if `time_zone` is empty/whitespace — luxon's DateTime would
12
+ * otherwise silently produce an invalid DateTime (isValid=false,
13
+ * year=NaN), which propagates as silent corruption rather than a clear
14
+ * failure. See second-brain#276 for the original report from
15
+ * backend-dev-ledger during #268 verification.
16
+ *
17
+ * Callers with optional/unset timestamps should gate
18
+ * `new ZonedDateTime(parent.getAsOf())` with a `parent.hasAsOf()`
19
+ * check at the call site rather than relying on the constructor to
20
+ * substitute a default.
21
+ */
8
22
  constructor(proto: LocalTimestampProto) {
23
+ const tz = proto.getTimeZone();
24
+ if (!tz || tz.trim().length === 0) {
25
+ throw new Error(
26
+ "LocalTimestampProto.time_zone is required but was empty. "
27
+ + "Producers must set time_zone (e.g. \"UTC\" or \"America/New_York\") "
28
+ + "when populating LocalTimestampProto. See second-brain#276."
29
+ );
30
+ }
9
31
  this.proto = proto;
10
32
  }
11
33
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fintekkers/ledger-models",
3
3
  "todo": "Replace the version with build script version number",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "ledger model protos ",
6
6
  "authors": [
7
7
  "David Doherty",