@pattern-stack/codegen 0.21.0 → 0.23.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -1
  3. package/dist/{chunk-3A34R6CI.js → chunk-3VEVGL74.js} +4 -4
  4. package/dist/{chunk-G3IKPDTP.js → chunk-42763UEE.js} +2 -2
  5. package/dist/{chunk-524YKITE.js → chunk-4M66MQYA.js} +50 -6
  6. package/dist/chunk-4M66MQYA.js.map +1 -0
  7. package/dist/{chunk-EEJC66ZF.js → chunk-6XP2Q5SS.js} +3 -3
  8. package/dist/{chunk-6CJRZHV4.js → chunk-7B7MMDOJ.js} +57 -4
  9. package/dist/chunk-7B7MMDOJ.js.map +1 -0
  10. package/dist/{chunk-NXHL5YII.js → chunk-7LKAMLV4.js} +4 -4
  11. package/dist/{chunk-7625PLY7.js → chunk-COGHTKXY.js} +4 -4
  12. package/dist/{chunk-GV337QP3.js → chunk-E5FJWOMP.js} +7 -7
  13. package/dist/{chunk-TKU6VYG3.js → chunk-E6PLM6QG.js} +6 -6
  14. package/dist/{chunk-GMRTI7AK.js → chunk-FIUC6QB5.js} +3 -3
  15. package/dist/chunk-FIUC6QB5.js.map +1 -0
  16. package/dist/{chunk-YXI7K4MJ.js → chunk-PNCOUFFI.js} +7 -5
  17. package/dist/chunk-PNCOUFFI.js.map +1 -0
  18. package/dist/{chunk-MBFSG4KQ.js → chunk-SH76CFAY.js} +9 -4
  19. package/dist/chunk-SH76CFAY.js.map +1 -0
  20. package/dist/runtime/subsystems/auth/auth.module.js +3 -3
  21. package/dist/runtime/subsystems/auth/index.js +7 -7
  22. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +1 -1
  23. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +4 -4
  24. package/dist/runtime/subsystems/bridge/bridge.module.js +10 -10
  25. package/dist/runtime/subsystems/bridge/index.js +13 -13
  26. package/dist/runtime/subsystems/cache/cache.module.js +2 -2
  27. package/dist/runtime/subsystems/cache/index.js +4 -4
  28. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +2 -2
  29. package/dist/runtime/subsystems/events/events.module.js +3 -3
  30. package/dist/runtime/subsystems/events/index.js +5 -5
  31. package/dist/runtime/subsystems/index.js +48 -48
  32. package/dist/runtime/subsystems/jobs/index.js +18 -18
  33. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +5 -5
  34. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +2 -2
  35. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  36. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
  37. package/dist/runtime/subsystems/jobs/job-worker.d.ts +592 -4
  38. package/dist/runtime/subsystems/jobs/job-worker.js +4 -2
  39. package/dist/runtime/subsystems/jobs/job-worker.module.js +6 -6
  40. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +19 -0
  41. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +4 -4
  42. package/dist/runtime/subsystems/storage/index.js +1 -1
  43. package/dist/runtime/subsystems/storage/storage.module.js +1 -1
  44. package/dist/src/cli/index.js +226 -65
  45. package/dist/src/cli/index.js.map +1 -1
  46. package/dist/src/index.d.ts +477 -1
  47. package/dist/src/index.js +1 -1
  48. package/package.json +1 -1
  49. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +23 -7
  50. package/runtime/subsystems/jobs/job-worker.module.ts +5 -0
  51. package/runtime/subsystems/jobs/job-worker.ts +126 -12
  52. package/runtime/subsystems/jobs/jobs-domain.module.ts +19 -0
  53. package/templates/entity/new/clean-lite-ps/prompt-extension.js +59 -10
  54. package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +11 -0
  55. package/dist/chunk-524YKITE.js.map +0 -1
  56. package/dist/chunk-6CJRZHV4.js.map +0 -1
  57. package/dist/chunk-GMRTI7AK.js.map +0 -1
  58. package/dist/chunk-MBFSG4KQ.js.map +0 -1
  59. package/dist/chunk-YXI7K4MJ.js.map +0 -1
  60. /package/dist/{chunk-3A34R6CI.js.map → chunk-3VEVGL74.js.map} +0 -0
  61. /package/dist/{chunk-G3IKPDTP.js.map → chunk-42763UEE.js.map} +0 -0
  62. /package/dist/{chunk-EEJC66ZF.js.map → chunk-6XP2Q5SS.js.map} +0 -0
  63. /package/dist/{chunk-NXHL5YII.js.map → chunk-7LKAMLV4.js.map} +0 -0
  64. /package/dist/{chunk-7625PLY7.js.map → chunk-COGHTKXY.js.map} +0 -0
  65. /package/dist/{chunk-GV337QP3.js.map → chunk-E5FJWOMP.js.map} +0 -0
  66. /package/dist/{chunk-TKU6VYG3.js.map → chunk-E6PLM6QG.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+
9
+ - **Jobs: claim heartbeat (CLAIM-HB-1) — long-running handlers are no longer
10
+ swept mid-flight.** The drizzle `JobWorker` stamped `claimed_at` once at claim
11
+ and never renewed it, while `sweepStaleClaims` reset any `running` row whose
12
+ `claimed_at` aged past `staleThresholdMs` (default 5 min) back to `pending`.
13
+ Consequence: ANY handler that legitimately ran longer than the threshold was
14
+ silently re-queued and re-claimed by a second worker, running CONCURRENTLY
15
+ with the still-live (uncancellable) original. Discovered by the swe-brain
16
+ dogfood: a 365-day Gmail backfill could never finish inside 5 min, so it
17
+ re-spawned a fresh concurrent mailbox walk every ~6 min for 5 days (writes
18
+ were idempotent upserts, so no corruption — but a non-idempotent handler would
19
+ have corrupted). Fix: a live worker now tracks its in-flight run IDs and bumps
20
+ `claimed_at = now()` for them every `claimHeartbeatIntervalMs` (new
21
+ `JobWorkerOptions` knob, default `staleThresholdMs / 3`). The sweeper now
22
+ fires only for genuinely dead workers (renewal stopped) — its documented
23
+ "stranded by a crashed worker" intent.
24
+
25
+ ### Added
26
+
27
+ - **Jobs: consumer-threadable lease tuning.** `jobs.extensions.drizzle` now
28
+ accepts `stale_threshold_ms`, `stale_sweeper_interval_ms`, and
29
+ `claim_heartbeat_interval_ms`, threaded through the subsystem barrel generator
30
+ into both `JobsDomainModule.forRoot` and `JobWorkerModule.forRoot` (camelCase
31
+ runtime keys). All optional; the worker defaults the heartbeat to a third of
32
+ the stale threshold.
33
+
34
+ > **Deferred (CLAIM-HB-1 follow-up):** fencing — a claim token on `job_run` so a
35
+ > swept-and-reclaimed run cannot be double-completed by a zombie attempt that
36
+ > finishes after the sweep. Needs a schema/migration change + write-site guards;
37
+ > tracked as issue #501. The heartbeat closes the practical
38
+ > re-claim-loop bug; fencing hardens the residual crash-recovery race.
39
+
7
40
  ## [0.21.0] — 2026-06-06
8
41
 
9
42
  **FieldMeta enrichment (ADR-040, Phase A of type-aware rendering).** The
package/README.md CHANGED
@@ -209,11 +209,15 @@ frontend:
209
209
  columnMapperNeedsCall: true # call the mapper (fn()) vs reference (fn)
210
210
  apiBaseUrlImport: null # when set, import API_BASE_URL from it as baseURL
211
211
  apiUrl: /api # REST base path when no apiBaseUrlImport
212
+ fields:
213
+ textareaThreshold: 500 # string→textarea cutoff (strict >); null DISABLES
212
214
  ```
213
215
 
214
216
  `null`-disables convention: an **absent** `auth.function` defaults to
215
217
  `getAuthorizationHeader`; an **explicit `null`** disables it entirely (no header
216
- lines emitted). Likewise `sync.columnMapper: null` omits the Electric mapper.
218
+ lines emitted). Likewise `sync.columnMapper: null` omits the Electric mapper, and
219
+ `fields.textareaThreshold: null` disables the string→textarea heuristic entirely
220
+ (bounded strings always render as `text` unless the author sets `ui_type: textarea`).
217
221
 
218
222
  ### Per-entity sync mode (`entity.sync`)
219
223
 
@@ -7,13 +7,13 @@ import {
7
7
  import {
8
8
  MissingTenantIdError
9
9
  } from "./chunk-T4BIIU5E.js";
10
- import {
11
- jobRuns
12
- } from "./chunk-OKXZ63IA.js";
13
10
  import {
14
11
  JOBS_MULTI_TENANT,
15
12
  JOB_ORCHESTRATOR
16
13
  } from "./chunk-ZPL74UQN.js";
14
+ import {
15
+ jobRuns
16
+ } from "./chunk-OKXZ63IA.js";
17
17
  import {
18
18
  DRIZZLE
19
19
  } from "./chunk-U64T4YZE.js";
@@ -198,4 +198,4 @@ DrizzleJobRunService = __decorateClass([
198
198
  export {
199
199
  DrizzleJobRunService
200
200
  };
201
- //# sourceMappingURL=chunk-3A34R6CI.js.map
201
+ //# sourceMappingURL=chunk-3VEVGL74.js.map
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-DUUCU77W.js";
8
8
  import {
9
9
  DrizzleEventBus
10
- } from "./chunk-YXI7K4MJ.js";
10
+ } from "./chunk-PNCOUFFI.js";
11
11
  import {
12
12
  MemoryEventBus
13
13
  } from "./chunk-GOO5ZMYO.js";
@@ -200,4 +200,4 @@ export {
200
200
  EventSchedulerLifecycle,
201
201
  EventsModule
202
202
  };
203
- //# sourceMappingURL=chunk-G3IKPDTP.js.map
203
+ //# sourceMappingURL=chunk-42763UEE.js.map
@@ -396,9 +396,15 @@ var ProviderIntegrationSchema = z.object({
396
396
  field_mapping: z.record(z.string(), z.string()).optional(),
397
397
  read_only_fields: z.array(z.string()).optional()
398
398
  });
399
+ var SinkPolicySchema = z.object({
400
+ delete: z.enum(["soft", "tombstone", "noop"]).optional(),
401
+ // NO .default() — see default fence in spec §Goal
402
+ exclude_fields: z.array(z.string()).optional()
403
+ }).strict();
399
404
  var IntegrationConfigSchema = z.object({
400
405
  electric: z.boolean().optional().default(false),
401
- providers: z.record(z.string(), ProviderIntegrationSchema).optional()
406
+ providers: z.record(z.string(), ProviderIntegrationSchema).optional(),
407
+ sink: SinkPolicySchema.optional()
402
408
  });
403
409
  var EventDeclarationSchema = z.object({
404
410
  name: z.string().regex(/^[a-z][a-z0-9_]*$/, "Event name must be snake_case"),
@@ -567,6 +573,42 @@ var EntityDefinitionSchema = z.object({
567
573
  });
568
574
  }
569
575
  }
576
+ }).superRefine((entity, ctx) => {
577
+ const excludeFields = entity.integration?.sink?.exclude_fields;
578
+ if (!excludeFields || excludeFields.length === 0) return;
579
+ const declaredFields = new Set(Object.keys(entity.fields ?? {}));
580
+ const fkColumns = /* @__PURE__ */ new Set();
581
+ for (const rel of Object.values(entity.relationships ?? {})) {
582
+ if (rel.type === "belongs_to" && typeof rel.foreign_key === "string") {
583
+ fkColumns.add(rel.foreign_key);
584
+ }
585
+ }
586
+ for (let i = 0; i < excludeFields.length; i++) {
587
+ const name = excludeFields[i];
588
+ if (!declaredFields.has(name)) {
589
+ ctx.addIssue({
590
+ code: "custom",
591
+ path: ["integration", "sink", "exclude_fields", i],
592
+ message: `exclude_fields: '${name}' is not a declared field. Declared fields: ${[...declaredFields].join(", ")}`
593
+ });
594
+ continue;
595
+ }
596
+ if (fkColumns.has(name)) {
597
+ ctx.addIssue({
598
+ code: "custom",
599
+ path: ["integration", "sink", "exclude_fields", i],
600
+ message: `exclude_fields: '${name}' is a FK column (belongs_to foreign_key). Excluding FK columns corrupts the FK-resolver path \u2014 exclude FK columns is not supported. Declare it in exclude_fields only for copy-through scalars.`
601
+ });
602
+ continue;
603
+ }
604
+ if (name === "user_id") {
605
+ ctx.addIssue({
606
+ code: "custom",
607
+ path: ["integration", "sink", "exclude_fields", i],
608
+ message: `exclude_fields: 'user_id' cannot be excluded. It is used for user-scoping and EAV dual-write; excluding it would break those mechanisms.`
609
+ });
610
+ }
611
+ }
570
612
  });
571
613
 
572
614
  // src/schema/event-definition.schema.ts
@@ -4072,11 +4114,12 @@ registerLibraryPattern(MetadataPattern);
4072
4114
  registerLibraryPattern(JunctionPattern);
4073
4115
 
4074
4116
  // src/emitters/frontend/field-meta.ts
4117
+ var DEFAULT_TEXTAREA_THRESHOLD = 500;
4075
4118
  var CAMEL = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
4076
4119
  function formatLabel(fieldName) {
4077
4120
  return fieldName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4078
4121
  }
4079
- function inferUiType(field) {
4122
+ function inferUiType(field, opts = {}) {
4080
4123
  if (field.ui.type) return field.ui.type;
4081
4124
  if (Array.isArray(field.choices) && field.choices.length > 0) return "enum";
4082
4125
  if (field.foreignKey) return "reference";
@@ -4090,9 +4133,10 @@ function inferUiType(field) {
4090
4133
  if (nameLower.includes("percent") || nameLower.includes("rate")) {
4091
4134
  return "percentage";
4092
4135
  }
4136
+ const threshold = opts.textareaThreshold === void 0 ? DEFAULT_TEXTAREA_THRESHOLD : opts.textareaThreshold;
4093
4137
  switch (field.type) {
4094
4138
  case "string":
4095
- return field.constraints.maxLength && field.constraints.maxLength > 500 ? "textarea" : "text";
4139
+ return threshold !== null && field.constraints.maxLength && field.constraints.maxLength > threshold ? "textarea" : "text";
4096
4140
  case "integer":
4097
4141
  case "decimal":
4098
4142
  return "number";
@@ -4125,12 +4169,12 @@ function isEntityRefField(field) {
4125
4169
  if (field.type === "entity_ref") return true;
4126
4170
  return field.name.endsWith("_entity_type") || field.name.endsWith("_entity_id");
4127
4171
  }
4128
- function deriveFieldMeta(field, defaults = {}) {
4172
+ function deriveFieldMeta(field, defaults = {}, opts = {}) {
4129
4173
  const hasChoices = Array.isArray(field.choices) && field.choices.length > 0;
4130
4174
  const meta = {
4131
4175
  field: CAMEL(field.name),
4132
4176
  label: field.ui.label ?? formatLabel(field.name),
4133
- type: inferUiType(field),
4177
+ type: inferUiType(field, opts),
4134
4178
  importance: inferUiImportance(field),
4135
4179
  sortable: field.ui.sortable ?? false,
4136
4180
  filterable: field.ui.filterable ?? false
@@ -4303,4 +4347,4 @@ export {
4303
4347
  analyzeDomain,
4304
4348
  validateEntities
4305
4349
  };
4306
- //# sourceMappingURL=chunk-524YKITE.js.map
4350
+ //# sourceMappingURL=chunk-4M66MQYA.js.map