@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.cjs CHANGED
@@ -963,6 +963,19 @@ function classifyDeliveryAttempt(result, errorClass, attemptsSoFar, now = Date.n
963
963
  return { success: false, error: result.error, nextAttemptAt: now + delay };
964
964
  }
965
965
 
966
+ // src/audit-timestamp.ts
967
+ function toEpochMs(value) {
968
+ if (typeof value === "number") return value;
969
+ if (value instanceof Date) return value.getTime();
970
+ if (typeof value === "string") {
971
+ const parsed = Date.parse(value);
972
+ if (Number.isFinite(parsed)) return parsed;
973
+ const numeric = Number(value);
974
+ if (Number.isFinite(numeric)) return numeric;
975
+ }
976
+ return 0;
977
+ }
978
+
966
979
  // src/sql-outbox.ts
967
980
  var DELIVERY_OBJECT = "sys_notification_delivery";
968
981
  var SqlNotificationOutbox = class {
@@ -981,7 +994,7 @@ var SqlNotificationOutbox = class {
981
994
  const existing = await this.engine.findOne(this.objectName, { where: dedup, fields: ["id"] });
982
995
  if (existing?.id) return String(existing.id);
983
996
  const id = (0, import_node_crypto.randomUUID)();
984
- const now = Date.now();
997
+ const now = /* @__PURE__ */ new Date();
985
998
  const row = {
986
999
  id,
987
1000
  notification_id: input.notificationId,
@@ -1142,8 +1155,8 @@ var SqlNotificationOutbox = class {
1142
1155
  lastAttemptedAt: r.last_attempted_at ?? void 0,
1143
1156
  error: r.error ?? void 0,
1144
1157
  digestKey: r.digest_key ?? void 0,
1145
- createdAt: r.created_at,
1146
- updatedAt: r.updated_at
1158
+ createdAt: toEpochMs(r.created_at),
1159
+ updatedAt: toEpochMs(r.updated_at)
1147
1160
  };
1148
1161
  }
1149
1162
  };
@@ -1272,8 +1285,11 @@ var HttpDelivery = import_data.ObjectSchema.create({
1272
1285
  response_code: import_data.Field.number({ label: "HTTP Status", required: false }),
1273
1286
  response_body: import_data.Field.textarea({ label: "Response Body (capped)", required: false }),
1274
1287
  error: import_data.Field.textarea({ label: "Error", required: false }),
1275
- created_at: import_data.Field.number({ label: "Created At (ms)", required: true }),
1276
- updated_at: import_data.Field.number({ label: "Updated At (ms)", required: true })
1288
+ // Builtin audit columns are native TIMESTAMP columns (Postgres/MySQL),
1289
+ // so declare them `datetime` and write `Date`s (not epoch-ms numbers,
1290
+ // which a real timestamp column rejects). See SqlHttpOutbox.
1291
+ created_at: import_data.Field.datetime({ label: "Created At", required: true }),
1292
+ updated_at: import_data.Field.datetime({ label: "Updated At", required: true })
1277
1293
  },
1278
1294
  indexes: [
1279
1295
  { fields: ["source", "dedup_key"], unique: true },
@@ -1303,7 +1319,7 @@ var SqlHttpOutbox = class {
1303
1319
  });
1304
1320
  if (existing?.id) return existing.id;
1305
1321
  const id = (0, import_node_crypto2.randomUUID)();
1306
- const now = Date.now();
1322
+ const now = /* @__PURE__ */ new Date();
1307
1323
  const row = {
1308
1324
  id,
1309
1325
  source: input.source,
@@ -1472,8 +1488,8 @@ var SqlHttpOutbox = class {
1472
1488
  responseCode: r.response_code ?? void 0,
1473
1489
  responseBody: r.response_body ?? void 0,
1474
1490
  error: r.error ?? void 0,
1475
- createdAt: r.created_at,
1476
- updatedAt: r.updated_at
1491
+ createdAt: toEpochMs(r.created_at),
1492
+ updatedAt: toEpochMs(r.updated_at)
1477
1493
  };
1478
1494
  }
1479
1495
  };
@@ -1947,10 +1963,10 @@ function stableNodeOffset2(nodeId, partitionCount) {
1947
1963
  // src/retention.ts
1948
1964
  var DEFAULT_NOTIFICATION_RETENTION_DAYS = 90;
1949
1965
  var DEFAULT_RETENTION_TARGETS = [
1950
- { object: RECEIPT_OBJECT, tsField: "created_at", format: "iso" },
1951
- { object: INBOX_OBJECT, tsField: "created_at", format: "iso" },
1952
- { object: DELIVERY_OBJECT, tsField: "created_at", format: "epoch" },
1953
- { object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at", format: "iso" }
1966
+ { object: RECEIPT_OBJECT, tsField: "created_at" },
1967
+ { object: INBOX_OBJECT, tsField: "created_at" },
1968
+ { object: DELIVERY_OBJECT, tsField: "created_at" },
1969
+ { object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at" }
1954
1970
  ];
1955
1971
  var NotificationRetention = class {
1956
1972
  constructor(opts) {
@@ -1973,14 +1989,15 @@ var NotificationRetention = class {
1973
1989
  this.opts.logger.warn(`[messaging] retention: invalid retentionDays=${retentionDays}; prune skipped`);
1974
1990
  return [];
1975
1991
  }
1976
- const cutoffMs = this.now() - retentionDays * 864e5;
1977
- const cutoffIso = new Date(cutoffMs).toISOString();
1992
+ const cutoffIso = new Date(this.now() - retentionDays * 864e5).toISOString();
1978
1993
  const outcomes = [];
1979
1994
  for (const t of this.targets) {
1980
- const cutoff = t.format === "epoch" ? cutoffMs : cutoffIso;
1981
1995
  try {
1982
1996
  const res = await data.delete(t.object, {
1983
- where: { [t.tsField]: { $lt: cutoff } },
1997
+ // ISO-8601 cutoff for every target: `created_at` is a native
1998
+ // timestamp column, which rejects a bare epoch-ms number on
1999
+ // Postgres. The driver coerces this per dialect on the way down.
2000
+ where: { [t.tsField]: { $lt: cutoffIso } },
1984
2001
  multi: true,
1985
2002
  // System context: retention is an operator policy that spans
1986
2003
  // tenants, so it must not be scoped by the caller's RLS.
@@ -2332,8 +2349,12 @@ var NotificationDelivery = import_data4.ObjectSchema.create({
2332
2349
  next_attempt_at: import_data4.Field.number({ label: "Next Attempt At (ms)" }),
2333
2350
  last_attempted_at: import_data4.Field.number({ label: "Last Attempted At (ms)" }),
2334
2351
  error: import_data4.Field.textarea({ label: "Error" }),
2335
- created_at: import_data4.Field.number({ label: "Created At (ms)", readonly: true }),
2336
- updated_at: import_data4.Field.number({ label: "Updated At (ms)" })
2352
+ // Builtin audit columns: the SQL driver provisions `created_at` /
2353
+ // `updated_at` as native TIMESTAMP columns (Postgres/MySQL), so they are
2354
+ // declared `datetime` and written as `Date`s — a bare epoch-ms number is
2355
+ // rejected by a real timestamp column. See SqlNotificationOutbox.
2356
+ created_at: import_data4.Field.datetime({ label: "Created At", readonly: true }),
2357
+ updated_at: import_data4.Field.datetime({ label: "Updated At" })
2337
2358
  },
2338
2359
  indexes: [
2339
2360
  // Dedup: one delivery per (event, recipient, channel).
@@ -2570,6 +2591,12 @@ var MessagingServicePlugin = class {
2570
2591
  }
2571
2592
  ]
2572
2593
  });
2594
+ if (typeof ctx.hook === "function") {
2595
+ ctx.hook("kernel:ready", async () => {
2596
+ const engine = getData();
2597
+ if (engine) await this.provisionSystemTables(engine, ctx);
2598
+ });
2599
+ }
2573
2600
  if (typeof ctx.hook === "function") {
2574
2601
  const templateStore = new NotificationTemplateStore({ getData });
2575
2602
  const getEmail = () => {
@@ -2652,6 +2679,38 @@ var MessagingServicePlugin = class {
2652
2679
  `[messaging] service registered with channels: ${service.getRegisteredChannels().join(", ") || "(none)"}`
2653
2680
  );
2654
2681
  }
2682
+ /**
2683
+ * Provision the physical tables for this service's system objects up-front.
2684
+ *
2685
+ * These objects are lazy-created on first WRITE (the SQL driver issues DDL
2686
+ * when the first row is inserted), so an env that READS them first — the
2687
+ * Console bell / inbox queries sys_inbox_message + sys_notification_receipt
2688
+ * before any notification has been delivered — hits "no such table", which
2689
+ * the engine logs as a `Find operation failed` ERROR on every page load.
2690
+ * Creating the tables at kernel:ready makes a new env consistent from the
2691
+ * start. Idempotent (the driver only creates a table when absent), so it is
2692
+ * safe on every boot; per-object failures are isolated.
2693
+ */
2694
+ async provisionSystemTables(engine, ctx) {
2695
+ const sync = engine.syncObjectSchema;
2696
+ if (typeof sync !== "function") return;
2697
+ const objects = [
2698
+ InboxMessage,
2699
+ NotificationReceipt,
2700
+ NotificationDelivery,
2701
+ NotificationPreference,
2702
+ NotificationSubscription,
2703
+ NotificationTemplate,
2704
+ HttpDelivery
2705
+ ];
2706
+ for (const obj of objects) {
2707
+ try {
2708
+ await sync.call(engine, obj.name);
2709
+ } catch (err) {
2710
+ ctx.logger.warn(`[messaging] could not provision ${obj.name} storage \u2014 ${err?.message ?? err}`);
2711
+ }
2712
+ }
2713
+ }
2655
2714
  /** Stop the dispatcher loop + retention sweep on shutdown. */
2656
2715
  async stop() {
2657
2716
  await this.dispatcher?.stop();