@objectstack/service-messaging 10.2.0 → 11.0.0

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/dist/index.js CHANGED
@@ -892,6 +892,19 @@ function classifyDeliveryAttempt(result, errorClass, attemptsSoFar, now = Date.n
892
892
  return { success: false, error: result.error, nextAttemptAt: now + delay };
893
893
  }
894
894
 
895
+ // src/audit-timestamp.ts
896
+ function toEpochMs(value) {
897
+ if (typeof value === "number") return value;
898
+ if (value instanceof Date) return value.getTime();
899
+ if (typeof value === "string") {
900
+ const parsed = Date.parse(value);
901
+ if (Number.isFinite(parsed)) return parsed;
902
+ const numeric = Number(value);
903
+ if (Number.isFinite(numeric)) return numeric;
904
+ }
905
+ return 0;
906
+ }
907
+
895
908
  // src/sql-outbox.ts
896
909
  var DELIVERY_OBJECT = "sys_notification_delivery";
897
910
  var SqlNotificationOutbox = class {
@@ -910,7 +923,7 @@ var SqlNotificationOutbox = class {
910
923
  const existing = await this.engine.findOne(this.objectName, { where: dedup, fields: ["id"] });
911
924
  if (existing?.id) return String(existing.id);
912
925
  const id = randomUUID();
913
- const now = Date.now();
926
+ const now = /* @__PURE__ */ new Date();
914
927
  const row = {
915
928
  id,
916
929
  notification_id: input.notificationId,
@@ -1071,8 +1084,8 @@ var SqlNotificationOutbox = class {
1071
1084
  lastAttemptedAt: r.last_attempted_at ?? void 0,
1072
1085
  error: r.error ?? void 0,
1073
1086
  digestKey: r.digest_key ?? void 0,
1074
- createdAt: r.created_at,
1075
- updatedAt: r.updated_at
1087
+ createdAt: toEpochMs(r.created_at),
1088
+ updatedAt: toEpochMs(r.updated_at)
1076
1089
  };
1077
1090
  }
1078
1091
  };
@@ -1201,8 +1214,11 @@ var HttpDelivery = ObjectSchema.create({
1201
1214
  response_code: Field.number({ label: "HTTP Status", required: false }),
1202
1215
  response_body: Field.textarea({ label: "Response Body (capped)", required: false }),
1203
1216
  error: Field.textarea({ label: "Error", required: false }),
1204
- created_at: Field.number({ label: "Created At (ms)", required: true }),
1205
- updated_at: Field.number({ label: "Updated At (ms)", required: true })
1217
+ // Builtin audit columns are native TIMESTAMP columns (Postgres/MySQL),
1218
+ // so declare them `datetime` and write `Date`s (not epoch-ms numbers,
1219
+ // which a real timestamp column rejects). See SqlHttpOutbox.
1220
+ created_at: Field.datetime({ label: "Created At", required: true }),
1221
+ updated_at: Field.datetime({ label: "Updated At", required: true })
1206
1222
  },
1207
1223
  indexes: [
1208
1224
  { fields: ["source", "dedup_key"], unique: true },
@@ -1232,7 +1248,7 @@ var SqlHttpOutbox = class {
1232
1248
  });
1233
1249
  if (existing?.id) return existing.id;
1234
1250
  const id = randomUUID2();
1235
- const now = Date.now();
1251
+ const now = /* @__PURE__ */ new Date();
1236
1252
  const row = {
1237
1253
  id,
1238
1254
  source: input.source,
@@ -1401,8 +1417,8 @@ var SqlHttpOutbox = class {
1401
1417
  responseCode: r.response_code ?? void 0,
1402
1418
  responseBody: r.response_body ?? void 0,
1403
1419
  error: r.error ?? void 0,
1404
- createdAt: r.created_at,
1405
- updatedAt: r.updated_at
1420
+ createdAt: toEpochMs(r.created_at),
1421
+ updatedAt: toEpochMs(r.updated_at)
1406
1422
  };
1407
1423
  }
1408
1424
  };
@@ -1876,10 +1892,10 @@ function stableNodeOffset2(nodeId, partitionCount) {
1876
1892
  // src/retention.ts
1877
1893
  var DEFAULT_NOTIFICATION_RETENTION_DAYS = 90;
1878
1894
  var DEFAULT_RETENTION_TARGETS = [
1879
- { object: RECEIPT_OBJECT, tsField: "created_at", format: "iso" },
1880
- { object: INBOX_OBJECT, tsField: "created_at", format: "iso" },
1881
- { object: DELIVERY_OBJECT, tsField: "created_at", format: "epoch" },
1882
- { object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at", format: "iso" }
1895
+ { object: RECEIPT_OBJECT, tsField: "created_at" },
1896
+ { object: INBOX_OBJECT, tsField: "created_at" },
1897
+ { object: DELIVERY_OBJECT, tsField: "created_at" },
1898
+ { object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at" }
1883
1899
  ];
1884
1900
  var NotificationRetention = class {
1885
1901
  constructor(opts) {
@@ -1902,14 +1918,15 @@ var NotificationRetention = class {
1902
1918
  this.opts.logger.warn(`[messaging] retention: invalid retentionDays=${retentionDays}; prune skipped`);
1903
1919
  return [];
1904
1920
  }
1905
- const cutoffMs = this.now() - retentionDays * 864e5;
1906
- const cutoffIso = new Date(cutoffMs).toISOString();
1921
+ const cutoffIso = new Date(this.now() - retentionDays * 864e5).toISOString();
1907
1922
  const outcomes = [];
1908
1923
  for (const t of this.targets) {
1909
- const cutoff = t.format === "epoch" ? cutoffMs : cutoffIso;
1910
1924
  try {
1911
1925
  const res = await data.delete(t.object, {
1912
- where: { [t.tsField]: { $lt: cutoff } },
1926
+ // ISO-8601 cutoff for every target: `created_at` is a native
1927
+ // timestamp column, which rejects a bare epoch-ms number on
1928
+ // Postgres. The driver coerces this per dialect on the way down.
1929
+ where: { [t.tsField]: { $lt: cutoffIso } },
1913
1930
  multi: true,
1914
1931
  // System context: retention is an operator policy that spans
1915
1932
  // tenants, so it must not be scoped by the caller's RLS.
@@ -2261,8 +2278,12 @@ var NotificationDelivery = ObjectSchema4.create({
2261
2278
  next_attempt_at: Field4.number({ label: "Next Attempt At (ms)" }),
2262
2279
  last_attempted_at: Field4.number({ label: "Last Attempted At (ms)" }),
2263
2280
  error: Field4.textarea({ label: "Error" }),
2264
- created_at: Field4.number({ label: "Created At (ms)", readonly: true }),
2265
- updated_at: Field4.number({ label: "Updated At (ms)" })
2281
+ // Builtin audit columns: the SQL driver provisions `created_at` /
2282
+ // `updated_at` as native TIMESTAMP columns (Postgres/MySQL), so they are
2283
+ // declared `datetime` and written as `Date`s — a bare epoch-ms number is
2284
+ // rejected by a real timestamp column. See SqlNotificationOutbox.
2285
+ created_at: Field4.datetime({ label: "Created At", readonly: true }),
2286
+ updated_at: Field4.datetime({ label: "Updated At" })
2266
2287
  },
2267
2288
  indexes: [
2268
2289
  // Dedup: one delivery per (event, recipient, channel).
@@ -2499,6 +2520,12 @@ var MessagingServicePlugin = class {
2499
2520
  }
2500
2521
  ]
2501
2522
  });
2523
+ if (typeof ctx.hook === "function") {
2524
+ ctx.hook("kernel:ready", async () => {
2525
+ const engine = getData();
2526
+ if (engine) await this.provisionSystemTables(engine, ctx);
2527
+ });
2528
+ }
2502
2529
  if (typeof ctx.hook === "function") {
2503
2530
  const templateStore = new NotificationTemplateStore({ getData });
2504
2531
  const getEmail = () => {
@@ -2581,6 +2608,38 @@ var MessagingServicePlugin = class {
2581
2608
  `[messaging] service registered with channels: ${service.getRegisteredChannels().join(", ") || "(none)"}`
2582
2609
  );
2583
2610
  }
2611
+ /**
2612
+ * Provision the physical tables for this service's system objects up-front.
2613
+ *
2614
+ * These objects are lazy-created on first WRITE (the SQL driver issues DDL
2615
+ * when the first row is inserted), so an env that READS them first — the
2616
+ * Console bell / inbox queries sys_inbox_message + sys_notification_receipt
2617
+ * before any notification has been delivered — hits "no such table", which
2618
+ * the engine logs as a `Find operation failed` ERROR on every page load.
2619
+ * Creating the tables at kernel:ready makes a new env consistent from the
2620
+ * start. Idempotent (the driver only creates a table when absent), so it is
2621
+ * safe on every boot; per-object failures are isolated.
2622
+ */
2623
+ async provisionSystemTables(engine, ctx) {
2624
+ const sync = engine.syncObjectSchema;
2625
+ if (typeof sync !== "function") return;
2626
+ const objects = [
2627
+ InboxMessage,
2628
+ NotificationReceipt,
2629
+ NotificationDelivery,
2630
+ NotificationPreference,
2631
+ NotificationSubscription,
2632
+ NotificationTemplate,
2633
+ HttpDelivery
2634
+ ];
2635
+ for (const obj of objects) {
2636
+ try {
2637
+ await sync.call(engine, obj.name);
2638
+ } catch (err) {
2639
+ ctx.logger.warn(`[messaging] could not provision ${obj.name} storage \u2014 ${err?.message ?? err}`);
2640
+ }
2641
+ }
2642
+ }
2584
2643
  /** Stop the dispatcher loop + retention sweep on shutdown. */
2585
2644
  async stop() {
2586
2645
  await this.dispatcher?.stop();