@distrohelena/canton-typescript-sdk 1.0.5 → 1.0.7

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.
package/README.md CHANGED
@@ -839,6 +839,26 @@ uses `[{ createdAt: "desc" }, { contractId: "asc" }]`. `exercises` intentionally
839
839
  because the v1 PQS profile does not declare a stable key. The manager validates
840
840
  the selected PQS schema profile before its first PQS query.
841
841
 
842
+ Payload and JSON-path filters accept `as: "timestamp"` to compare ISO timestamps
843
+ at microsecond precision in both PQS and memory:
844
+
845
+ ```typescript
846
+ where: {
847
+ payload: {
848
+ match: {
849
+ expiresAt: { as: "timestamp", gt: "2026-09-09T12:00:00.000001Z" },
850
+ },
851
+ },
852
+ }
853
+ ```
854
+
855
+ For relational JSON fields, use `{ path: ["expiresAt"], as: "timestamp", gt: value }`.
856
+ Values require a timezone and at most six fractional digits. Equivalent instants
857
+ compare equally regardless of timezone offset or fractional padding. Omit `as`
858
+ or use `as: "text"` for string comparisons; `like` and `ilike` require text.
859
+ This filter option does not change timestamp projections or lifecycle fields
860
+ represented as JavaScript `Date`, which retain millisecond precision.
861
+
842
862
  - Ledger endpoint:
843
863
  - `versionService.getLedgerApiVersionAsync(...)`: `json`, `grpc`
844
864
  - `healthService.checkAsync(...)`: `grpc` only
@@ -120,20 +120,24 @@ function decodeProtobufRecord(record, descriptor, registry, path) {
120
120
  for (const field of descriptor.fields) {
121
121
  const sourceField = fields.find((candidate) => candidate.label === field.damlLabel);
122
122
  if (sourceField === undefined) {
123
- throw materializationError(fieldPath(path, field.propertyName), "required record field is absent");
123
+ output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
124
+ continue;
124
125
  }
125
126
  output[field.propertyName] = decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
126
127
  }
127
- if (fields.length !== descriptor.fields.length) {
128
+ if (fields.some((field) => !descriptor.fields.some((descriptorField) => descriptorField.damlLabel === field.label))) {
128
129
  throw materializationError(path, "record contains an unexpected field");
129
130
  }
130
131
  }
131
132
  else {
132
- if (fields.length !== descriptor.fields.length) {
133
+ if (fields.length > descriptor.fields.length) {
133
134
  throw materializationError(path, "record has the wrong number of positional fields");
134
135
  }
135
136
  descriptor.fields.forEach((field, index) => {
136
- output[field.propertyName] = decodeRequiredProtobufValue(fields[index]?.value, field.type, registry, fieldPath(path, field.propertyName));
137
+ const sourceField = fields[index];
138
+ output[field.propertyName] = sourceField === undefined
139
+ ? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
140
+ : decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
137
141
  });
138
142
  }
139
143
  return new daml_values_js_1.DamlRecord(output);
@@ -212,11 +216,13 @@ function decodeJsonTextMap(value, descriptor, registry, path) {
212
216
  function decodeJsonRecord(value, descriptor, registry, path) {
213
217
  const output = Object.create(null);
214
218
  if (Array.isArray(value)) {
215
- if (value.length !== descriptor.fields.length) {
219
+ if (value.length > descriptor.fields.length) {
216
220
  throw materializationError(path, "record has the wrong number of positional fields");
217
221
  }
218
222
  descriptor.fields.forEach((field, index) => {
219
- output[field.propertyName] = decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
223
+ output[field.propertyName] = index >= value.length
224
+ ? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
225
+ : decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
220
226
  });
221
227
  return new daml_values_js_1.DamlRecord(output);
222
228
  }
@@ -229,12 +235,19 @@ function decodeJsonRecord(value, descriptor, registry, path) {
229
235
  }
230
236
  for (const field of descriptor.fields) {
231
237
  if (!Object.hasOwn(record, field.damlLabel)) {
232
- throw materializationError(fieldPath(path, field.propertyName), "required record field is absent");
238
+ output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
239
+ continue;
233
240
  }
234
241
  output[field.propertyName] = decodeDamlValue({ kind: "json", value: Reflect.get(record, field.damlLabel) }, field.type, registry, fieldPath(path, field.propertyName));
235
242
  }
236
243
  return new daml_values_js_1.DamlRecord(output);
237
244
  }
245
+ function decodeMissingRecordField(field, path) {
246
+ if (field.type.kind === "optional") {
247
+ return undefined;
248
+ }
249
+ throw materializationError(path, "required record field is absent");
250
+ }
238
251
  function decodeJsonVariant(value, descriptor, registry, path) {
239
252
  const envelope = requireObject(value, path, "variant");
240
253
  const tag = requireString(Reflect.get(envelope, "tag"), path, "variant tag");
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryQueryEvaluator = void 0;
4
4
  const query_dataset_js_1 = require("./query-dataset.js");
5
5
  const query_schema_js_1 = require("./query-schema.js");
6
+ const query_timestamp_js_1 = require("./query-timestamp.js");
6
7
  /** Reference evaluator for normalized canonical plans; it intentionally has no transport concerns. */
7
8
  class InMemoryQueryEvaluator {
8
9
  execute(dataset, query) {
@@ -40,8 +41,10 @@ class InMemoryQueryEvaluator {
40
41
  return predicate.quantifier === "one" ? result[0] === true : predicate.quantifier === "some" ? result.some(Boolean) : predicate.quantifier === "none" ? !result.some(Boolean) : result.every(Boolean);
41
42
  }
42
43
  case "scalar": {
43
- const actual = at(row, predicate.path);
44
- const expected = predicate.value;
44
+ const raw = at(row, predicate.path);
45
+ const convert = (value) => predicate.as === "timestamp" && value !== null && value !== undefined ? (0, query_timestamp_js_1.timestampMicroseconds)(value) : value;
46
+ const actual = predicate.operator === "is" || predicate.operator === "isNot" ? raw : convert(raw);
47
+ const expected = Array.isArray(predicate.value) ? predicate.value.map(convert) : convert(predicate.value);
45
48
  switch (predicate.operator) {
46
49
  case "equals": return actual !== null && actual !== undefined && expected !== null && expected !== undefined && equal(actual, expected);
47
50
  case "in": return actual !== null && actual !== undefined && Array.isArray(expected) && expected.some((candidate) => candidate !== null && candidate !== undefined && equal(actual, candidate));
@@ -171,7 +174,10 @@ function freeze(value) {
171
174
  return Object.freeze(value);
172
175
  }
173
176
  function equal(left, right) {
174
- if (left instanceof Date) {
177
+ if (typeof left === "bigint" || typeof right === "bigint") {
178
+ return left === right;
179
+ }
180
+ else if (left instanceof Date) {
175
181
  return left.toISOString() === (right instanceof Date ? right.toISOString() : String(right));
176
182
  }
177
183
  else if (right instanceof Date) {
@@ -196,7 +202,7 @@ function compare(left, right) {
196
202
  return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
197
203
  }
198
204
  function numeric(value) {
199
- return typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
205
+ return typeof value === "bigint" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
200
206
  }
201
207
  function postgresCompare(left, right, direction) {
202
208
  const leftNull = left === null || left === undefined;
@@ -7,6 +7,7 @@ exports.normalizeCount = normalizeCount;
7
7
  exports.normalizeAggregate = normalizeAggregate;
8
8
  exports.normalizeGroupBy = normalizeGroupBy;
9
9
  const model_types_js_1 = require("../model-types.js");
10
+ const query_timestamp_js_1 = require("./query-timestamp.js");
10
11
  const query_schema_js_1 = require("./query-schema.js");
11
12
  const scalarOperators = new Set([
12
13
  "equals", "in", "is", "isNot", "lt", "lte", "gt", "gte", "like", "ilike", "has",
@@ -236,7 +237,8 @@ function normalizeScalarFilter(relation, field, value) {
236
237
  function normalizeJsonFilter(field, value) {
237
238
  const filter = object(value, `${field} JSON filter`);
238
239
  const path = jsonPath(filter.path, `${field}.path`);
239
- const entries = Object.entries(filter).filter(([operator]) => operator !== "path");
240
+ const as = normalizeTimestampFilterType(filter);
241
+ const entries = Object.entries(filter).filter(([operator]) => operator !== "path" && operator !== "as");
240
242
  if (entries.length === 0) {
241
243
  throw new Error(`${field} JSON filter must contain an operator`);
242
244
  }
@@ -245,11 +247,14 @@ function normalizeJsonFilter(field, value) {
245
247
  throw new Error(`${operator} is not supported for ${field}`);
246
248
  }
247
249
  validateOperatorValue(operator, operand, field, "string", true);
248
- return { kind: "scalar", path: [field, ...path], operator: operator, value: operand };
250
+ if (as !== undefined)
251
+ validateTimestampOperand(operator, operand);
252
+ return { kind: "scalar", path: [field, ...path], operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
249
253
  });
250
254
  }
251
255
  function normalizePayloadScalarFilter(path, value) {
252
- const entries = Object.entries(value);
256
+ const as = normalizeTimestampFilterType(value);
257
+ const entries = Object.entries(value).filter(([operator]) => operator !== "as");
253
258
  if (entries.length === 0) {
254
259
  throw new Error("payload filter must not be empty");
255
260
  }
@@ -258,9 +263,26 @@ function normalizePayloadScalarFilter(path, value) {
258
263
  throw new Error(`${operator} is not supported for payload`);
259
264
  }
260
265
  validateOperatorValue(operator, operand, "payload", "string", false);
261
- return { kind: "scalar", path, operator: operator, value: operand };
266
+ if (as !== undefined)
267
+ validateTimestampOperand(operator, operand);
268
+ return { kind: "scalar", path, operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
262
269
  });
263
270
  }
271
+ function normalizeTimestampFilterType(filter) {
272
+ if (filter.as !== undefined && filter.as !== "text" && filter.as !== "timestamp") {
273
+ throw new Error("JSON filter as must be text or timestamp");
274
+ }
275
+ return filter.as === "timestamp" ? "timestamp" : undefined;
276
+ }
277
+ function validateTimestampOperand(operator, value) {
278
+ if (operator === "like" || operator === "ilike") {
279
+ throw new Error(`${operator} is not supported for timestamp filters`);
280
+ }
281
+ for (const entry of Array.isArray(value) ? value : [value]) {
282
+ if (entry !== null)
283
+ (0, query_timestamp_js_1.timestampMicroseconds)(entry);
284
+ }
285
+ }
264
286
  function normalizeTemplateScalarFilter(field, value) {
265
287
  if (!isFilter(value)) {
266
288
  throw new Error(`templateId.${field} must be a filter object`);
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timestampMicroseconds = timestampMicroseconds;
4
+ /** Parse an explicit ISO timestamp without passing its fractional seconds through Date. */
5
+ function timestampMicroseconds(value) {
6
+ const match = typeof value === "string"
7
+ ? /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})$/.exec(value)
8
+ : null;
9
+ if (match === null) {
10
+ throw new Error("Invalid timestamp: expected ISO date-time with a timezone and at most six fractional digits");
11
+ }
12
+ const base = `${match[1]}T${match[2]}`;
13
+ const milliseconds = Date.parse(`${base}Z`);
14
+ if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== base) {
15
+ throw new Error("Invalid timestamp calendar date or time");
16
+ }
17
+ const zone = match[4];
18
+ const hours = zone === "Z" ? 0 : Number(zone.slice(1, 3));
19
+ const minutes = zone === "Z" ? 0 : Number(zone.slice(4, 6));
20
+ if (hours > 15 || minutes > 59) {
21
+ throw new Error("Invalid timestamp timezone offset");
22
+ }
23
+ const offset = BigInt((hours * 60 + minutes) * (zone[0] === "-" ? -1 : 1));
24
+ const micros = BigInt(milliseconds) * 1000n + BigInt((match[3] ?? "").padEnd(6, "0")) - offset * 60000000n;
25
+ if (micros < -62135596800000000n || micros > 253402300799999999n) {
26
+ throw new Error("Timestamp is outside the DAML ledger range");
27
+ }
28
+ return micros;
29
+ }
@@ -257,7 +257,9 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
257
257
  ? `${compileCanonicalPhysicalField(relation, field, alias, profile)} #>> ${add(path)}::text[]`
258
258
  : compileCanonicalPhysicalField(relation, field, alias, profile);
259
259
  const inverseEventType = relation === "__events" && field === "type" && (predicate.operator === "equals" || predicate.operator === "in");
260
- const expression = inverseEventType ? qualified(alias, column) : canonicalExpression;
260
+ const expression = predicate.as === "timestamp" && predicate.operator !== "is" && predicate.operator !== "isNot"
261
+ ? `(${canonicalExpression})::timestamptz`
262
+ : inverseEventType ? qualified(alias, column) : canonicalExpression;
261
263
  const value = inverseEventType ? physicalEventType(predicate.value) : predicate.value;
262
264
  const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[predicate.operator];
263
265
  if (predicate.operator === "is")
@@ -265,7 +267,7 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
265
267
  if (predicate.operator === "isNot")
266
268
  return `${expression} is not null`;
267
269
  if (predicate.operator === "in")
268
- return value.length === 0 ? "false" : `${expression} = any(${add(value)})`;
270
+ return value.length === 0 ? "false" : `${expression} = any(${add(value)}${predicate.as === "timestamp" ? "::timestamptz[]" : ""})`;
269
271
  if (predicate.operator === "has")
270
272
  return `${add(value)} = any(${qualified(alias, column)})`;
271
273
  if (sql === undefined)
@@ -116,20 +116,24 @@ function decodeProtobufRecord(record, descriptor, registry, path) {
116
116
  for (const field of descriptor.fields) {
117
117
  const sourceField = fields.find((candidate) => candidate.label === field.damlLabel);
118
118
  if (sourceField === undefined) {
119
- throw materializationError(fieldPath(path, field.propertyName), "required record field is absent");
119
+ output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
120
+ continue;
120
121
  }
121
122
  output[field.propertyName] = decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
122
123
  }
123
- if (fields.length !== descriptor.fields.length) {
124
+ if (fields.some((field) => !descriptor.fields.some((descriptorField) => descriptorField.damlLabel === field.label))) {
124
125
  throw materializationError(path, "record contains an unexpected field");
125
126
  }
126
127
  }
127
128
  else {
128
- if (fields.length !== descriptor.fields.length) {
129
+ if (fields.length > descriptor.fields.length) {
129
130
  throw materializationError(path, "record has the wrong number of positional fields");
130
131
  }
131
132
  descriptor.fields.forEach((field, index) => {
132
- output[field.propertyName] = decodeRequiredProtobufValue(fields[index]?.value, field.type, registry, fieldPath(path, field.propertyName));
133
+ const sourceField = fields[index];
134
+ output[field.propertyName] = sourceField === undefined
135
+ ? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
136
+ : decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
133
137
  });
134
138
  }
135
139
  return new DamlRecord(output);
@@ -208,11 +212,13 @@ function decodeJsonTextMap(value, descriptor, registry, path) {
208
212
  function decodeJsonRecord(value, descriptor, registry, path) {
209
213
  const output = Object.create(null);
210
214
  if (Array.isArray(value)) {
211
- if (value.length !== descriptor.fields.length) {
215
+ if (value.length > descriptor.fields.length) {
212
216
  throw materializationError(path, "record has the wrong number of positional fields");
213
217
  }
214
218
  descriptor.fields.forEach((field, index) => {
215
- output[field.propertyName] = decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
219
+ output[field.propertyName] = index >= value.length
220
+ ? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
221
+ : decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
216
222
  });
217
223
  return new DamlRecord(output);
218
224
  }
@@ -225,12 +231,19 @@ function decodeJsonRecord(value, descriptor, registry, path) {
225
231
  }
226
232
  for (const field of descriptor.fields) {
227
233
  if (!Object.hasOwn(record, field.damlLabel)) {
228
- throw materializationError(fieldPath(path, field.propertyName), "required record field is absent");
234
+ output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
235
+ continue;
229
236
  }
230
237
  output[field.propertyName] = decodeDamlValue({ kind: "json", value: Reflect.get(record, field.damlLabel) }, field.type, registry, fieldPath(path, field.propertyName));
231
238
  }
232
239
  return new DamlRecord(output);
233
240
  }
241
+ function decodeMissingRecordField(field, path) {
242
+ if (field.type.kind === "optional") {
243
+ return undefined;
244
+ }
245
+ throw materializationError(path, "required record field is absent");
246
+ }
234
247
  function decodeJsonVariant(value, descriptor, registry, path) {
235
248
  const envelope = requireObject(value, path, "variant");
236
249
  const tag = requireString(Reflect.get(envelope, "tag"), path, "variant tag");
@@ -1,5 +1,6 @@
1
1
  import { createQueryDataset, immutableQueryValue, relatedQueryRows } from "./query-dataset.js";
2
2
  import { queryRelationEdges, queryRelationMetadata } from "./query-schema.js";
3
+ import { timestampMicroseconds } from "./query-timestamp.js";
3
4
  /** Reference evaluator for normalized canonical plans; it intentionally has no transport concerns. */
4
5
  export class InMemoryQueryEvaluator {
5
6
  execute(dataset, query) {
@@ -37,8 +38,10 @@ export class InMemoryQueryEvaluator {
37
38
  return predicate.quantifier === "one" ? result[0] === true : predicate.quantifier === "some" ? result.some(Boolean) : predicate.quantifier === "none" ? !result.some(Boolean) : result.every(Boolean);
38
39
  }
39
40
  case "scalar": {
40
- const actual = at(row, predicate.path);
41
- const expected = predicate.value;
41
+ const raw = at(row, predicate.path);
42
+ const convert = (value) => predicate.as === "timestamp" && value !== null && value !== undefined ? timestampMicroseconds(value) : value;
43
+ const actual = predicate.operator === "is" || predicate.operator === "isNot" ? raw : convert(raw);
44
+ const expected = Array.isArray(predicate.value) ? predicate.value.map(convert) : convert(predicate.value);
42
45
  switch (predicate.operator) {
43
46
  case "equals": return actual !== null && actual !== undefined && expected !== null && expected !== undefined && equal(actual, expected);
44
47
  case "in": return actual !== null && actual !== undefined && Array.isArray(expected) && expected.some((candidate) => candidate !== null && candidate !== undefined && equal(actual, candidate));
@@ -167,7 +170,10 @@ function freeze(value) {
167
170
  return Object.freeze(value);
168
171
  }
169
172
  function equal(left, right) {
170
- if (left instanceof Date) {
173
+ if (typeof left === "bigint" || typeof right === "bigint") {
174
+ return left === right;
175
+ }
176
+ else if (left instanceof Date) {
171
177
  return left.toISOString() === (right instanceof Date ? right.toISOString() : String(right));
172
178
  }
173
179
  else if (right instanceof Date) {
@@ -192,7 +198,7 @@ function compare(left, right) {
192
198
  return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
193
199
  }
194
200
  function numeric(value) {
195
- return typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
201
+ return typeof value === "bigint" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
196
202
  }
197
203
  function postgresCompare(left, right, direction) {
198
204
  const leftNull = left === null || left === undefined;
@@ -12,6 +12,7 @@ export type QueryPredicate = {
12
12
  readonly path: readonly string[];
13
13
  readonly operator: ScalarOperator;
14
14
  readonly value: unknown;
15
+ readonly as?: "timestamp";
15
16
  } | {
16
17
  readonly kind: "relation";
17
18
  readonly edge: string;
@@ -1,4 +1,5 @@
1
1
  import { assertQueryOrderBy, assertQueryPageArgs } from "../model-types.js";
2
+ import { timestampMicroseconds } from "./query-timestamp.js";
2
3
  import { queryRelationEdges, queryRelationMetadata, } from "./query-schema.js";
3
4
  const scalarOperators = new Set([
4
5
  "equals", "in", "is", "isNot", "lt", "lte", "gt", "gte", "like", "ilike", "has",
@@ -227,7 +228,8 @@ function normalizeScalarFilter(relation, field, value) {
227
228
  function normalizeJsonFilter(field, value) {
228
229
  const filter = object(value, `${field} JSON filter`);
229
230
  const path = jsonPath(filter.path, `${field}.path`);
230
- const entries = Object.entries(filter).filter(([operator]) => operator !== "path");
231
+ const as = normalizeTimestampFilterType(filter);
232
+ const entries = Object.entries(filter).filter(([operator]) => operator !== "path" && operator !== "as");
231
233
  if (entries.length === 0) {
232
234
  throw new Error(`${field} JSON filter must contain an operator`);
233
235
  }
@@ -236,11 +238,14 @@ function normalizeJsonFilter(field, value) {
236
238
  throw new Error(`${operator} is not supported for ${field}`);
237
239
  }
238
240
  validateOperatorValue(operator, operand, field, "string", true);
239
- return { kind: "scalar", path: [field, ...path], operator: operator, value: operand };
241
+ if (as !== undefined)
242
+ validateTimestampOperand(operator, operand);
243
+ return { kind: "scalar", path: [field, ...path], operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
240
244
  });
241
245
  }
242
246
  function normalizePayloadScalarFilter(path, value) {
243
- const entries = Object.entries(value);
247
+ const as = normalizeTimestampFilterType(value);
248
+ const entries = Object.entries(value).filter(([operator]) => operator !== "as");
244
249
  if (entries.length === 0) {
245
250
  throw new Error("payload filter must not be empty");
246
251
  }
@@ -249,9 +254,26 @@ function normalizePayloadScalarFilter(path, value) {
249
254
  throw new Error(`${operator} is not supported for payload`);
250
255
  }
251
256
  validateOperatorValue(operator, operand, "payload", "string", false);
252
- return { kind: "scalar", path, operator: operator, value: operand };
257
+ if (as !== undefined)
258
+ validateTimestampOperand(operator, operand);
259
+ return { kind: "scalar", path, operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
253
260
  });
254
261
  }
262
+ function normalizeTimestampFilterType(filter) {
263
+ if (filter.as !== undefined && filter.as !== "text" && filter.as !== "timestamp") {
264
+ throw new Error("JSON filter as must be text or timestamp");
265
+ }
266
+ return filter.as === "timestamp" ? "timestamp" : undefined;
267
+ }
268
+ function validateTimestampOperand(operator, value) {
269
+ if (operator === "like" || operator === "ilike") {
270
+ throw new Error(`${operator} is not supported for timestamp filters`);
271
+ }
272
+ for (const entry of Array.isArray(value) ? value : [value]) {
273
+ if (entry !== null)
274
+ timestampMicroseconds(entry);
275
+ }
276
+ }
255
277
  function normalizeTemplateScalarFilter(field, value) {
256
278
  if (!isFilter(value)) {
257
279
  throw new Error(`templateId.${field} must be a filter object`);
@@ -0,0 +1,2 @@
1
+ /** Parse an explicit ISO timestamp without passing its fractional seconds through Date. */
2
+ export declare function timestampMicroseconds(value: unknown): bigint;
@@ -0,0 +1,26 @@
1
+ /** Parse an explicit ISO timestamp without passing its fractional seconds through Date. */
2
+ export function timestampMicroseconds(value) {
3
+ const match = typeof value === "string"
4
+ ? /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})$/.exec(value)
5
+ : null;
6
+ if (match === null) {
7
+ throw new Error("Invalid timestamp: expected ISO date-time with a timezone and at most six fractional digits");
8
+ }
9
+ const base = `${match[1]}T${match[2]}`;
10
+ const milliseconds = Date.parse(`${base}Z`);
11
+ if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== base) {
12
+ throw new Error("Invalid timestamp calendar date or time");
13
+ }
14
+ const zone = match[4];
15
+ const hours = zone === "Z" ? 0 : Number(zone.slice(1, 3));
16
+ const minutes = zone === "Z" ? 0 : Number(zone.slice(4, 6));
17
+ if (hours > 15 || minutes > 59) {
18
+ throw new Error("Invalid timestamp timezone offset");
19
+ }
20
+ const offset = BigInt((hours * 60 + minutes) * (zone[0] === "-" ? -1 : 1));
21
+ const micros = BigInt(milliseconds) * 1000n + BigInt((match[3] ?? "").padEnd(6, "0")) - offset * 60000000n;
22
+ if (micros < -62135596800000000n || micros > 253402300799999999n) {
23
+ throw new Error("Timestamp is outside the DAML ledger range");
24
+ }
25
+ return micros;
26
+ }
@@ -20,6 +20,8 @@ export interface ArrayMembershipFilter {
20
20
  }
21
21
  export interface JsonPathFilter {
22
22
  readonly path: readonly [string, ...readonly string[]];
23
+ /** Compare ISO timestamps at microsecond precision; omitted means text. */
24
+ readonly as?: "text" | "timestamp";
23
25
  readonly equals?: string;
24
26
  readonly in?: readonly string[];
25
27
  readonly lt?: string;
@@ -138,6 +140,8 @@ export interface TemplateId {
138
140
  readonly entityName: string;
139
141
  }
140
142
  type PayloadValueFilter = {
143
+ readonly as?: "text" | "timestamp";
144
+ } & ({
141
145
  readonly equals: string;
142
146
  readonly lt?: never;
143
147
  readonly lte?: never;
@@ -193,7 +197,7 @@ type PayloadValueFilter = {
193
197
  readonly gt?: never;
194
198
  readonly gte?: never;
195
199
  readonly like?: never;
196
- };
200
+ });
197
201
  export type PayloadMatch = {
198
202
  readonly [field: string]: PayloadMatch | PayloadValueFilter;
199
203
  };
@@ -248,7 +248,9 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
248
248
  ? `${compileCanonicalPhysicalField(relation, field, alias, profile)} #>> ${add(path)}::text[]`
249
249
  : compileCanonicalPhysicalField(relation, field, alias, profile);
250
250
  const inverseEventType = relation === "__events" && field === "type" && (predicate.operator === "equals" || predicate.operator === "in");
251
- const expression = inverseEventType ? qualified(alias, column) : canonicalExpression;
251
+ const expression = predicate.as === "timestamp" && predicate.operator !== "is" && predicate.operator !== "isNot"
252
+ ? `(${canonicalExpression})::timestamptz`
253
+ : inverseEventType ? qualified(alias, column) : canonicalExpression;
252
254
  const value = inverseEventType ? physicalEventType(predicate.value) : predicate.value;
253
255
  const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[predicate.operator];
254
256
  if (predicate.operator === "is")
@@ -256,7 +258,7 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
256
258
  if (predicate.operator === "isNot")
257
259
  return `${expression} is not null`;
258
260
  if (predicate.operator === "in")
259
- return value.length === 0 ? "false" : `${expression} = any(${add(value)})`;
261
+ return value.length === 0 ? "false" : `${expression} = any(${add(value)}${predicate.as === "timestamp" ? "::timestamptz[]" : ""})`;
260
262
  if (predicate.operator === "has")
261
263
  return `${add(value)} = any(${qualified(alias, column)})`;
262
264
  if (sql === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distrohelena/canton-typescript-sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",