@pattern-stack/codegen 0.10.0 → 0.10.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/consumer-skills/events/typed-bus-and-outbox.md +1 -1
  3. package/consumer-skills/subsystems/SKILL.md +56 -0
  4. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +0 -1
  5. package/dist/runtime/subsystems/bridge/bridge.module.js +294 -710
  6. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  7. package/dist/runtime/subsystems/bridge/index.d.ts +0 -1
  8. package/dist/runtime/subsystems/bridge/index.js +248 -664
  9. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  10. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +18 -10
  11. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  12. package/dist/runtime/subsystems/events/events.module.js +43 -244
  13. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  14. package/dist/runtime/subsystems/events/index.d.ts +0 -1
  15. package/dist/runtime/subsystems/events/index.js +39 -241
  16. package/dist/runtime/subsystems/events/index.js.map +1 -1
  17. package/dist/runtime/subsystems/index.js +174 -791
  18. package/dist/runtime/subsystems/index.js.map +1 -1
  19. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +22 -3
  20. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
  21. package/dist/runtime/subsystems/jobs/index.d.ts +1 -4
  22. package/dist/runtime/subsystems/jobs/index.js +87 -506
  23. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  24. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  25. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -0
  26. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
  27. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +11 -4
  28. package/dist/runtime/subsystems/jobs/job-worker.module.js +248 -664
  29. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  30. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +0 -1
  31. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +89 -391
  32. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  33. package/dist/src/cli/index.js +152 -35
  34. package/dist/src/cli/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +32 -10
  37. package/runtime/subsystems/events/events.module.ts +38 -6
  38. package/runtime/subsystems/events/index.ts +7 -1
  39. package/runtime/subsystems/jobs/bullmq.config.ts +23 -3
  40. package/runtime/subsystems/jobs/index.ts +13 -8
  41. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +5 -2
  42. package/runtime/subsystems/jobs/job-worker.module.ts +27 -7
  43. package/runtime/subsystems/jobs/jobs-domain.module.ts +27 -2
  44. package/templates/subsystem/events/domain-events.schema.ejs.t +43 -2
@@ -359,13 +359,12 @@ var BRIDGE_OUTBOX_DRAIN_HOOK = "BRIDGE_OUTBOX_DRAIN_HOOK";
359
359
  // runtime/subsystems/events/event-bus.drizzle-backend.ts
360
360
  var POLL_INTERVAL_MS = 1e3;
361
361
  var POLL_BATCH_SIZE = 50;
362
- function toInsertValues(event) {
362
+ function toInsertValues(event, multiTenant) {
363
363
  const metadata = event.metadata ?? void 0;
364
364
  const pool = metadata?.["pool"] ?? null;
365
365
  const direction = metadata?.["direction"] ?? null;
366
- const tenantId = metadata?.["tenantId"] ?? null;
367
366
  const tier = metadata?.["tier"] ?? "domain";
368
- return {
367
+ const base = {
369
368
  id: event.id,
370
369
  type: event.type,
371
370
  aggregateId: event.aggregateId,
@@ -377,9 +376,11 @@ function toInsertValues(event) {
377
376
  metadata: event.metadata,
378
377
  pool,
379
378
  direction,
380
- tier,
381
- tenantId
379
+ tier
382
380
  };
381
+ if (!multiTenant) return base;
382
+ const tenantId = metadata?.["tenantId"] ?? null;
383
+ return { ...base, tenantId };
383
384
  }
384
385
  function toEventSummary(r) {
385
386
  const metadata = r.metadata ?? void 0;
@@ -394,7 +395,11 @@ function toEventSummary(r) {
394
395
  direction: r.direction,
395
396
  tier: r.tier,
396
397
  rootRunId: typeof rootRunId === "string" ? rootRunId : null,
397
- tenantId: r.tenantId,
398
+ // EVT-8: `tenant_id` is a scaffold-time conditional column. Read it
399
+ // structurally so this projection typechecks against both the
400
+ // multi-tenant schema (column present) and the single-tenant schema
401
+ // (column absent → undefined → null).
402
+ tenantId: r.tenantId ?? null,
398
403
  occurredAt: r.occurredAt instanceof Date ? r.occurredAt : new Date(r.occurredAt),
399
404
  processedAt: r.processedAt == null ? null : r.processedAt instanceof Date ? r.processedAt : new Date(r.processedAt)
400
405
  };
@@ -431,12 +436,14 @@ var DrizzleEventBus = class {
431
436
  // ============================================================================
432
437
  async publish(event, tx) {
433
438
  const client = tx ?? this.db;
434
- await client.insert(domainEvents).values(toInsertValues(event));
439
+ const multiTenant = this.opts.multiTenant ?? false;
440
+ await client.insert(domainEvents).values(toInsertValues(event, multiTenant));
435
441
  }
436
442
  async publishMany(events, tx) {
437
443
  if (events.length === 0) return;
438
444
  const client = tx ?? this.db;
439
- await client.insert(domainEvents).values(events.map(toInsertValues));
445
+ const multiTenant = this.opts.multiTenant ?? false;
446
+ await client.insert(domainEvents).values(events.map((e) => toInsertValues(e, multiTenant)));
440
447
  }
441
448
  async findById(eventId) {
442
449
  const rows = await this.db.select().from(domainEvents).where(eq(domainEvents.id, eventId)).limit(1);
@@ -478,9 +485,10 @@ var DrizzleEventBus = class {
478
485
  sql2`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`
479
486
  );
480
487
  }
481
- if (query.tenantId !== void 0) {
488
+ if (this.opts.multiTenant && query.tenantId !== void 0) {
489
+ const tenantIdColumn = domainEvents.tenantId;
482
490
  conditions.push(
483
- query.tenantId === null ? sql2`${domainEvents.tenantId} is null` : eq(domainEvents.tenantId, query.tenantId)
491
+ query.tenantId === null ? sql2`${tenantIdColumn} is null` : eq(tenantIdColumn, query.tenantId)
484
492
  );
485
493
  }
486
494
  if (query.cursor) {
@@ -811,233 +819,12 @@ MemoryEventBus = __decorateClass([
811
819
  __decorateParam(0, Inject3(EVENTS_MODULE_OPTIONS))
812
820
  ], MemoryEventBus);
813
821
 
814
- // runtime/subsystems/events/event-bus.redis-backend.ts
815
- import { Injectable as Injectable4, Inject as Inject4, Logger as Logger3 } from "@nestjs/common";
816
- var CHANNEL_PREFIX = "events:";
817
- async function createRedisClient(url) {
818
- let Redis;
819
- try {
820
- const mod = await import("ioredis");
821
- Redis = mod.default ?? mod;
822
- } catch {
823
- throw new Error(
824
- 'RedisEventBus requires the "ioredis" package. Install it with: npm install ioredis'
825
- );
826
- }
827
- return new Redis(url);
828
- }
829
- var RedisEventBus = class {
830
- constructor(redisUrl) {
831
- this.redisUrl = redisUrl;
832
- }
833
- redisUrl;
834
- logger = new Logger3(RedisEventBus.name);
835
- publisher = null;
836
- subscriber = null;
837
- connected = false;
838
- /**
839
- * In-process subscriber registry. Handlers registered here are called when
840
- * a message arrives on the subscriber client — keeping fan-out within the
841
- * same process without an extra round-trip through Redis.
842
- */
843
- handlers = /* @__PURE__ */ new Map();
844
- /**
845
- * Track which event types have active Redis subscriptions.
846
- * Used to avoid subscribing multiple times to the same type channel.
847
- */
848
- subscribedTypes = /* @__PURE__ */ new Set();
849
- // ============================================================================
850
- // Lifecycle
851
- // ============================================================================
852
- async onModuleInit() {
853
- this.publisher = await createRedisClient(this.redisUrl);
854
- this.subscriber = await createRedisClient(this.redisUrl);
855
- this.publisher.on(
856
- "error",
857
- (err) => this.logger.error(`Redis publisher error: ${err.message}`, err.stack)
858
- );
859
- this.subscriber.on(
860
- "error",
861
- (err) => this.logger.error(`Redis subscriber error: ${err.message}`, err.stack)
862
- );
863
- this.subscriber.on("message", (channel, message) => {
864
- void this.handleMessage(channel, message);
865
- });
866
- this.connected = true;
867
- this.logger.log(`RedisEventBus connected to ${this.redisUrl}`);
868
- }
869
- async onModuleDestroy() {
870
- this.connected = false;
871
- if (this.subscriber) {
872
- await this.subscriber.unsubscribe();
873
- this.subscriber.disconnect();
874
- this.subscriber = null;
875
- }
876
- if (this.publisher) {
877
- this.publisher.disconnect();
878
- this.publisher = null;
879
- }
880
- this.subscribedTypes.clear();
881
- this.logger.log("RedisEventBus disconnected");
882
- }
883
- // ============================================================================
884
- // IEventBus
885
- // ============================================================================
886
- /**
887
- * Publish a single event.
888
- *
889
- * `tx` is accepted but ignored — see module-level JSDoc for details.
890
- */
891
- async publish(event, tx) {
892
- void tx;
893
- this.assertConnected();
894
- const payload = this.serialize(event);
895
- const channel = `${CHANNEL_PREFIX}${event.type}`;
896
- await this.publisher.publish(channel, payload);
897
- }
898
- /**
899
- * Publish multiple events using a pipeline so all PUBLISH commands are sent
900
- * in a single round-trip.
901
- *
902
- * `tx` is accepted but ignored — see module-level JSDoc for details.
903
- */
904
- async publishMany(events, tx) {
905
- void tx;
906
- if (events.length === 0) return;
907
- this.assertConnected();
908
- const pipeline = this.publisher.pipeline();
909
- for (const event of events) {
910
- const payload = this.serialize(event);
911
- const channel = `${CHANNEL_PREFIX}${event.type}`;
912
- pipeline.publish(channel, payload);
913
- }
914
- await pipeline.exec();
915
- }
916
- /**
917
- * Register a handler for a specific event type.
918
- * Returns an unsubscribe function — call it to remove the handler.
919
- *
920
- * On first handler for a type, subscribes to the per-type Redis channel.
921
- * On removal of the last handler for a type, unsubscribes from the channel.
922
- */
923
- /**
924
- * Lookup by id is unsupported on the Redis Pub/Sub backend — Pub/Sub
925
- * does not retain history. Always returns `null`. Logs a warning the
926
- * first time it's called so a misconfiguration surfaces visibly. Using
927
- * the bridge with the Redis backend is unsupported (the bridge requires
928
- * a durable event store).
929
- */
930
- warnedFindById = false;
931
- async findById(_eventId) {
932
- if (!this.warnedFindById) {
933
- this.warnedFindById = true;
934
- this.logger.warn(
935
- "RedisEventBus.findById is unsupported (Pub/Sub has no history). The bridge subsystem requires a durable event store; switch to DrizzleEventBus if you need bridge fanout."
936
- );
937
- }
938
- return null;
939
- }
940
- subscribe(eventType, handler) {
941
- if (!this.handlers.has(eventType)) {
942
- this.handlers.set(eventType, /* @__PURE__ */ new Set());
943
- void this.subscribeToType(eventType);
944
- }
945
- const set = this.handlers.get(eventType);
946
- const h = handler;
947
- set.add(h);
948
- return () => {
949
- set.delete(h);
950
- if (set.size === 0) {
951
- this.handlers.delete(eventType);
952
- void this.unsubscribeFromType(eventType);
953
- }
954
- };
955
- }
956
- // ============================================================================
957
- // Internal helpers
958
- // ============================================================================
959
- assertConnected() {
960
- if (!this.connected || !this.publisher) {
961
- throw new Error(
962
- "RedisEventBus is not connected. Ensure the module has been initialised before publishing."
963
- );
964
- }
965
- }
966
- serialize(event) {
967
- return JSON.stringify({
968
- ...event,
969
- occurredAt: event.occurredAt.toISOString()
970
- });
971
- }
972
- deserialize(raw) {
973
- const parsed = JSON.parse(raw);
974
- return {
975
- ...parsed,
976
- occurredAt: new Date(parsed.occurredAt)
977
- };
978
- }
979
- async handleMessage(channel, message) {
980
- let event;
981
- try {
982
- event = this.deserialize(message);
983
- } catch (err) {
984
- this.logger.warn(`Failed to deserialize event on channel "${channel}": ${err}`);
985
- return;
986
- }
987
- await this.dispatch(event);
988
- }
989
- async dispatch(event) {
990
- const set = this.handlers.get(event.type);
991
- if (!set) return;
992
- for (const handler of set) {
993
- try {
994
- await handler(event);
995
- } catch (err) {
996
- this.logger.error(
997
- `Handler error for event type "${event.type}" (id: ${event.id}): ${err}`
998
- );
999
- }
1000
- }
1001
- }
1002
- /**
1003
- * Subscribe to a per-type Redis channel.
1004
- * Called lazily when the first handler is registered for a type.
1005
- */
1006
- async subscribeToType(eventType) {
1007
- if (this.subscribedTypes.has(eventType)) {
1008
- return;
1009
- }
1010
- const channel = `${CHANNEL_PREFIX}${eventType}`;
1011
- try {
1012
- await this.subscriber.subscribe(channel);
1013
- this.subscribedTypes.add(eventType);
1014
- } catch (err) {
1015
- this.logger.error(`Failed to subscribe to channel "${channel}": ${err}`);
1016
- }
1017
- }
1018
- /**
1019
- * Unsubscribe from a per-type Redis channel.
1020
- * Called when the last handler for a type is removed.
1021
- */
1022
- async unsubscribeFromType(eventType) {
1023
- if (!this.subscribedTypes.has(eventType)) {
1024
- return;
1025
- }
1026
- const channel = `${CHANNEL_PREFIX}${eventType}`;
1027
- try {
1028
- await this.subscriber.unsubscribe(channel);
1029
- this.subscribedTypes.delete(eventType);
1030
- } catch (err) {
1031
- this.logger.error(`Failed to unsubscribe from channel "${channel}": ${err}`);
1032
- }
1033
- }
1034
- };
1035
- RedisEventBus = __decorateClass([
1036
- Injectable4(),
1037
- __decorateParam(0, Inject4(REDIS_URL))
1038
- ], RedisEventBus);
1039
-
1040
822
  // runtime/subsystems/events/events.module.ts
823
+ async function loadRedisEventBus() {
824
+ const specifier = "./event-bus.redis-backend";
825
+ const mod = await import(specifier);
826
+ return mod.RedisEventBus;
827
+ }
1041
828
  function buildTypedBusProviders(multiTenant) {
1042
829
  return [
1043
830
  TypedEventBus,
@@ -1045,7 +832,7 @@ function buildTypedBusProviders(multiTenant) {
1045
832
  { provide: EVENTS_MULTI_TENANT, useValue: multiTenant }
1046
833
  ];
1047
834
  }
1048
- function buildEventBusAsync(options, db, redisUrl) {
835
+ async function buildEventBusAsync(options, db, redisUrl) {
1049
836
  if (options.backend === "drizzle") {
1050
837
  if (!db) {
1051
838
  throw new Error(
@@ -1055,6 +842,7 @@ function buildEventBusAsync(options, db, redisUrl) {
1055
842
  return new DrizzleEventBus(db, options);
1056
843
  }
1057
844
  if (options.backend === "redis") {
845
+ const RedisEventBus = await loadRedisEventBus();
1058
846
  return new RedisEventBus(redisUrl);
1059
847
  }
1060
848
  return new MemoryEventBus(options);
@@ -1116,9 +904,20 @@ var EventsModule = class {
1116
904
  providers: [
1117
905
  { provide: EVENTS_MODULE_OPTIONS, useValue: options },
1118
906
  { provide: REDIS_URL, useValue: resolvedUrl },
1119
- { provide: EVENT_BUS, useClass: RedisEventBus },
1120
- // Register concrete class so NestJS can resolve lifecycle hooks
1121
- RedisEventBus,
907
+ {
908
+ // #6: useFactory + dynamic import so the consumer's tsc never
909
+ // needs to resolve `event-bus.redis-backend.ts` for drizzle/
910
+ // memory installs (the file is filtered out by
911
+ // `backendFileFilter`). Nest awaits async factories + manages
912
+ // lifecycle on the returned instance, so we drop the old bare
913
+ // `RedisEventBus` provider entry.
914
+ provide: EVENT_BUS,
915
+ useFactory: async (url) => {
916
+ const RedisEventBus = await loadRedisEventBus();
917
+ return new RedisEventBus(url);
918
+ },
919
+ inject: [REDIS_URL]
920
+ },
1122
921
  ...buildTypedBusProviders(multiTenant)
1123
922
  ],
1124
923
  exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
@@ -1153,7 +952,6 @@ export {
1153
952
  EventsModule,
1154
953
  MemoryEventBus,
1155
954
  MissingTenantIdError,
1156
- RedisEventBus,
1157
955
  TYPED_EVENT_BUS,
1158
956
  TypedEventBus,
1159
957
  domainEvents