@happyvertical/smrt-core 0.40.70 → 0.41.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/AGENTS.md +2 -2
- package/agents/change-feed.md +1 -1
- package/agents/schema-paths.md +487 -29
- package/dist/browser.js +5 -4
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +59 -5
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +29 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +66 -14
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decorators/index.d.ts +63 -5
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +37 -5
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +19 -19
- package/dist/migrations/differ.d.ts +9 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +10 -5
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations.js +2 -1
- package/dist/object.d.ts +98 -5
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +138 -30
- package/dist/object.js.map +1 -1
- package/dist/query-bounds.d.ts +5 -1
- package/dist/query-bounds.d.ts.map +1 -1
- package/dist/query-bounds.js +5 -1
- package/dist/query-bounds.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +3 -2
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +252 -16
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +411 -106
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.js.map +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -2
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +4 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +13 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -1
- package/dist/schema/system-table-shapes.js +14 -1
- package/dist/schema/system-table-shapes.js.map +1 -1
- package/dist/schema/utils.d.ts +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/smrt-knowledge.json +9 -9
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { LoggerConfig } from '@happyvertical/logger';
|
|
|
2
2
|
import { AiUsageHandler, SignalAdapter } from '@happyvertical/smrt-types';
|
|
3
3
|
import { SignalBus } from './signals/bus.js';
|
|
4
4
|
import { SanitizationConfig } from './signals/sanitizer.js';
|
|
5
|
+
import { RetentionPolicy } from './system/retention.js';
|
|
5
6
|
/**
|
|
6
7
|
* Metrics adapter configuration
|
|
7
8
|
*/
|
|
@@ -88,6 +89,15 @@ export interface GlobalSignalConfig {
|
|
|
88
89
|
* Set to false to disable sanitization
|
|
89
90
|
*/
|
|
90
91
|
sanitization?: SanitizationConfig | false;
|
|
92
|
+
/**
|
|
93
|
+
* System-table retention policy (#2375).
|
|
94
|
+
*
|
|
95
|
+
* Consumed by `smrt db:prune` and by the jobs `TaskRunner`'s periodic
|
|
96
|
+
* sweep. Defaults come from `DEFAULT_RETENTION_POLICY`; set
|
|
97
|
+
* `retention: { enabled: false }` to opt a deployment out entirely, or an
|
|
98
|
+
* individual table to `false` to keep only that one unbounded.
|
|
99
|
+
*/
|
|
100
|
+
retention?: RetentionPolicy;
|
|
91
101
|
/** Custom signal configuration */
|
|
92
102
|
signals?: {
|
|
93
103
|
/** Shared signal bus instance */
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,yDAAyD;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE9D,gCAAgC;IAChC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,YAAY,CAAC;IAEtB;;;OAGG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IAEd;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;;OAGG;IACH,YAAY,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAAC;IAE1C;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B,kCAAkC;IAClC,OAAO,CAAC,EAAE;QACR,iCAAiC;QACjC,GAAG,CAAC,EAAE,SAAS,CAAC;QAChB,iCAAiC;QACjC,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;KAC5B,CAAC;CACH;AAmDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,iBAAS,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAEjD;kBAFQ,MAAM;qBAOI,IAAI;sBAUH,kBAAkB;wBAQhB,MAAM;;AAI5B,OAAO,EACL,KAAK,aAAa,EAClB,WAAW,EACX,aAAa,EACb,WAAW,EACX,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Global SMRT configuration system\n *\n * Provides application-level defaults for signal adapters.\n * Configuration follows a three-tier pattern:\n * 1. Global defaults (via smrt.configure())\n * 2. Per-instance overrides (via SmrtClassOptions)\n * 3. Runtime behavior (from merged config)\n */\n\nimport type { LoggerConfig } from '@happyvertical/logger';\nimport type { AiUsageHandler, SignalAdapter } from '@happyvertical/smrt-types';\nimport type { SignalBus } from './signals/bus.js';\nimport type { SanitizationConfig } from './signals/sanitizer.js';\n\n/**\n * Metrics adapter configuration\n */\nexport interface MetricsConfig {\n /** Enable metrics tracking */\n enabled: boolean;\n}\n\n/**\n * Pub/Sub adapter configuration\n */\nexport interface PubSubConfig {\n /** Enable pub/sub broadcasting */\n enabled: boolean;\n}\n\n/**\n * AI provider configuration\n *\n * Global defaults for AI client initialization.\n * Provides fallback values when AI options are not specified per-instance.\n */\nexport interface AIConfig {\n /**\n * Default AI provider to use\n * Examples: 'openai', 'anthropic', 'claude-cli', 'gemini', etc.\n */\n provider?: string;\n\n /**\n * Default model to use with the provider\n * Examples: 'gpt-4', 'claude-3-opus', 'sonnet', etc.\n */\n model?: string;\n\n /**\n * Default API key for the provider\n * Can be overridden by environment variables or instance options\n */\n apiKey?: string;\n\n /**\n * Additional provider-specific options\n */\n [key: string]: unknown;\n}\n\n/**\n * AI usage tracking configuration\n */\nexport interface AiUsageConfig {\n /** Enable normalized AI usage tracking (default: true) */\n enabled?: boolean;\n\n /** Enable persistence to _smrt_ai_usage when a DB exists (default: true) */\n persist?: boolean;\n\n /** Enable best-effort cost estimation (default: true) */\n estimateCosts?: boolean;\n\n /** Override USD-per-1K-token cost rates */\n costRates?: Record<string, { input: number; output: number }>;\n\n /** Additional usage handlers */\n handlers?: AiUsageHandler[];\n}\n\n/**\n * Global signal configuration\n *\n * Application-level defaults for signal adapters.\n * These can be overridden per-instance via SmrtClassOptions.\n */\nexport interface GlobalSignalConfig {\n /** Logging configuration (default: true with console, info level) */\n logging?: LoggerConfig;\n\n /** Metrics configuration (default: undefined/disabled) */\n metrics?: MetricsConfig;\n\n /** Pub/Sub configuration (default: undefined/disabled) */\n pubsub?: PubSubConfig;\n\n /**\n * AI provider configuration (default: undefined)\n * Provides global defaults for AI client initialization\n */\n ai?: AIConfig;\n\n /**\n * AI usage tracking configuration (default: enabled)\n */\n usage?: AiUsageConfig;\n\n /**\n * Signal sanitization configuration (default: enabled with standard redactions)\n * Set to false to disable sanitization\n */\n sanitization?: SanitizationConfig | false;\n\n /** Custom signal configuration */\n signals?: {\n /** Shared signal bus instance */\n bus?: SignalBus;\n /** Additional custom adapters */\n adapters?: SignalAdapter[];\n };\n}\n\n/**\n * Singleton configuration manager\n *\n * Manages global SMRT configuration with sensible defaults.\n */\nclass SmrtConfig {\n private static instance: SmrtConfig;\n private config: GlobalSignalConfig = {\n logging: true, // Default: console logging at info level\n };\n\n private constructor() {}\n\n /**\n * Get singleton instance\n */\n static getInstance(): SmrtConfig {\n if (!SmrtConfig.instance) {\n SmrtConfig.instance = new SmrtConfig();\n }\n return SmrtConfig.instance;\n }\n\n /**\n * Configure global defaults\n *\n * @param config - Configuration to apply\n */\n configure(config: GlobalSignalConfig): void {\n this.config = { ...this.config, ...config };\n }\n\n /**\n * Get current configuration\n *\n * @returns Current global configuration\n */\n getConfig(): GlobalSignalConfig {\n return { ...this.config };\n }\n\n /**\n * Reset to default configuration\n */\n reset(): void {\n this.config = { logging: true };\n }\n}\n\n/**\n * Global configuration API\n *\n * Callable function with attached methods for managing SMRT configuration.\n *\n * @example\n * ```typescript\n * import { config } from '@happyvertical/smrt-core';\n *\n * // Set application-level defaults\n * config({\n * logging: { level: 'debug' },\n * metrics: { enabled: true },\n * pubsub: { enabled: false },\n * ai: {\n * provider: 'claude-cli',\n * model: 'sonnet'\n * }\n * });\n *\n * // Reset to defaults\n * config.reset();\n *\n * // Get current configuration\n * const current = config.toJSON();\n *\n * // Auto-convert to string\n * console.log(`Config: ${config}`);\n *\n * // Auto-convert to JSON\n * JSON.stringify(config);\n *\n * // All SmrtClass instances now use these defaults\n * const product = new Product({ name: 'Widget' });\n * await product.initialize();\n * // product has logging at debug level, metrics enabled, and uses claude-cli by default\n * ```\n */\nfunction config(options: GlobalSignalConfig): void {\n SmrtConfig.getInstance().configure(options);\n}\n\n/**\n * Reset configuration to defaults\n */\nconfig.reset = (): void => {\n SmrtConfig.getInstance().reset();\n};\n\n/**\n * Get current configuration as object\n * Called automatically by JSON.stringify()\n *\n * @returns Current global configuration\n */\nconfig.toJSON = (): GlobalSignalConfig => SmrtConfig.getInstance().getConfig();\n\n/**\n * Convert configuration to string\n * Called automatically in string contexts\n *\n * @returns JSON string representation of configuration\n */\nconfig.toString = (): string =>\n JSON.stringify(SmrtConfig.getInstance().getConfig(), null, 2);\n\n// Re-export env-config utilities from @happyvertical/utils\nexport {\n type ConfigOptions,\n convertType,\n loadEnvConfig,\n toCamelCase,\n toScreamingSnakeCase,\n} from '@happyvertical/utils';\nexport { config };\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Global SMRT configuration system\n *\n * Provides application-level defaults for signal adapters.\n * Configuration follows a three-tier pattern:\n * 1. Global defaults (via smrt.configure())\n * 2. Per-instance overrides (via SmrtClassOptions)\n * 3. Runtime behavior (from merged config)\n */\n\nimport type { LoggerConfig } from '@happyvertical/logger';\nimport type { AiUsageHandler, SignalAdapter } from '@happyvertical/smrt-types';\nimport type { SignalBus } from './signals/bus.js';\nimport type { SanitizationConfig } from './signals/sanitizer.js';\nimport type { RetentionPolicy } from './system/retention.js';\n\n/**\n * Metrics adapter configuration\n */\nexport interface MetricsConfig {\n /** Enable metrics tracking */\n enabled: boolean;\n}\n\n/**\n * Pub/Sub adapter configuration\n */\nexport interface PubSubConfig {\n /** Enable pub/sub broadcasting */\n enabled: boolean;\n}\n\n/**\n * AI provider configuration\n *\n * Global defaults for AI client initialization.\n * Provides fallback values when AI options are not specified per-instance.\n */\nexport interface AIConfig {\n /**\n * Default AI provider to use\n * Examples: 'openai', 'anthropic', 'claude-cli', 'gemini', etc.\n */\n provider?: string;\n\n /**\n * Default model to use with the provider\n * Examples: 'gpt-4', 'claude-3-opus', 'sonnet', etc.\n */\n model?: string;\n\n /**\n * Default API key for the provider\n * Can be overridden by environment variables or instance options\n */\n apiKey?: string;\n\n /**\n * Additional provider-specific options\n */\n [key: string]: unknown;\n}\n\n/**\n * AI usage tracking configuration\n */\nexport interface AiUsageConfig {\n /** Enable normalized AI usage tracking (default: true) */\n enabled?: boolean;\n\n /** Enable persistence to _smrt_ai_usage when a DB exists (default: true) */\n persist?: boolean;\n\n /** Enable best-effort cost estimation (default: true) */\n estimateCosts?: boolean;\n\n /** Override USD-per-1K-token cost rates */\n costRates?: Record<string, { input: number; output: number }>;\n\n /** Additional usage handlers */\n handlers?: AiUsageHandler[];\n}\n\n/**\n * Global signal configuration\n *\n * Application-level defaults for signal adapters.\n * These can be overridden per-instance via SmrtClassOptions.\n */\nexport interface GlobalSignalConfig {\n /** Logging configuration (default: true with console, info level) */\n logging?: LoggerConfig;\n\n /** Metrics configuration (default: undefined/disabled) */\n metrics?: MetricsConfig;\n\n /** Pub/Sub configuration (default: undefined/disabled) */\n pubsub?: PubSubConfig;\n\n /**\n * AI provider configuration (default: undefined)\n * Provides global defaults for AI client initialization\n */\n ai?: AIConfig;\n\n /**\n * AI usage tracking configuration (default: enabled)\n */\n usage?: AiUsageConfig;\n\n /**\n * Signal sanitization configuration (default: enabled with standard redactions)\n * Set to false to disable sanitization\n */\n sanitization?: SanitizationConfig | false;\n\n /**\n * System-table retention policy (#2375).\n *\n * Consumed by `smrt db:prune` and by the jobs `TaskRunner`'s periodic\n * sweep. Defaults come from `DEFAULT_RETENTION_POLICY`; set\n * `retention: { enabled: false }` to opt a deployment out entirely, or an\n * individual table to `false` to keep only that one unbounded.\n */\n retention?: RetentionPolicy;\n\n /** Custom signal configuration */\n signals?: {\n /** Shared signal bus instance */\n bus?: SignalBus;\n /** Additional custom adapters */\n adapters?: SignalAdapter[];\n };\n}\n\n/**\n * Singleton configuration manager\n *\n * Manages global SMRT configuration with sensible defaults.\n */\nclass SmrtConfig {\n private static instance: SmrtConfig;\n private config: GlobalSignalConfig = {\n logging: true, // Default: console logging at info level\n };\n\n private constructor() {}\n\n /**\n * Get singleton instance\n */\n static getInstance(): SmrtConfig {\n if (!SmrtConfig.instance) {\n SmrtConfig.instance = new SmrtConfig();\n }\n return SmrtConfig.instance;\n }\n\n /**\n * Configure global defaults\n *\n * @param config - Configuration to apply\n */\n configure(config: GlobalSignalConfig): void {\n this.config = { ...this.config, ...config };\n }\n\n /**\n * Get current configuration\n *\n * @returns Current global configuration\n */\n getConfig(): GlobalSignalConfig {\n return { ...this.config };\n }\n\n /**\n * Reset to default configuration\n */\n reset(): void {\n this.config = { logging: true };\n }\n}\n\n/**\n * Global configuration API\n *\n * Callable function with attached methods for managing SMRT configuration.\n *\n * @example\n * ```typescript\n * import { config } from '@happyvertical/smrt-core';\n *\n * // Set application-level defaults\n * config({\n * logging: { level: 'debug' },\n * metrics: { enabled: true },\n * pubsub: { enabled: false },\n * ai: {\n * provider: 'claude-cli',\n * model: 'sonnet'\n * }\n * });\n *\n * // Reset to defaults\n * config.reset();\n *\n * // Get current configuration\n * const current = config.toJSON();\n *\n * // Auto-convert to string\n * console.log(`Config: ${config}`);\n *\n * // Auto-convert to JSON\n * JSON.stringify(config);\n *\n * // All SmrtClass instances now use these defaults\n * const product = new Product({ name: 'Widget' });\n * await product.initialize();\n * // product has logging at debug level, metrics enabled, and uses claude-cli by default\n * ```\n */\nfunction config(options: GlobalSignalConfig): void {\n SmrtConfig.getInstance().configure(options);\n}\n\n/**\n * Reset configuration to defaults\n */\nconfig.reset = (): void => {\n SmrtConfig.getInstance().reset();\n};\n\n/**\n * Get current configuration as object\n * Called automatically by JSON.stringify()\n *\n * @returns Current global configuration\n */\nconfig.toJSON = (): GlobalSignalConfig => SmrtConfig.getInstance().getConfig();\n\n/**\n * Convert configuration to string\n * Called automatically in string contexts\n *\n * @returns JSON string representation of configuration\n */\nconfig.toString = (): string =>\n JSON.stringify(SmrtConfig.getInstance().getConfig(), null, 2);\n\n// Re-export env-config utilities from @happyvertical/utils\nexport {\n type ConfigOptions,\n convertType,\n loadEnvConfig,\n toCamelCase,\n toScreamingSnakeCase,\n} from '@happyvertical/utils';\nexport { config };\n"],"mappings":";;;;;;;AA4IA,IAAM,aAAN,MAAM,WAAW;CACf,OAAe;CACf,SAAqC,EACnC,SAAS,KACX;CAEA,cAAsB,CAAC;;;;CAKvB,OAAO,cAA0B;EAC/B,IAAI,CAAC,WAAW,UACd,WAAW,WAAW,IAAI,WAAW;EAEvC,OAAO,WAAW;CACpB;;;;;;CAOA,UAAU,QAAkC;EAC1C,KAAK,SAAS;GAAE,GAAG,KAAK;GAAQ,GAAG;EAAO;CAC5C;;;;;;CAOA,YAAgC;EAC9B,OAAO,EAAE,GAAG,KAAK,OAAO;CAC1B;;;;CAKA,QAAc;EACZ,KAAK,SAAS,EAAE,SAAS,KAAK;CAChC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,OAAO,SAAmC;CACjD,WAAW,YAAY,CAAC,CAAC,UAAU,OAAO;AAC5C;;;;AAKA,OAAO,cAAoB;CACzB,WAAW,YAAY,CAAC,CAAC,MAAM;AACjC;;;;;;;AAQA,OAAO,eAAmC,WAAW,YAAY,CAAC,CAAC,UAAU;;;;;;;AAQ7E,OAAO,iBACL,KAAK,UAAU,WAAW,YAAY,CAAC,CAAC,UAAU,GAAG,MAAM,CAAC"}
|
|
@@ -142,6 +142,32 @@ export interface RelationshipFieldOptions extends FieldOptions {
|
|
|
142
142
|
through?: string;
|
|
143
143
|
/** Relationship type */
|
|
144
144
|
type?: 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany';
|
|
145
|
+
/**
|
|
146
|
+
* What happens to this row when the referenced object is deleted (#2371).
|
|
147
|
+
*
|
|
148
|
+
* SMRT emits no DB-level `FOREIGN KEY` constraints, so this is enforced by
|
|
149
|
+
* `SmrtObject.delete()` in the application layer rather than by the engine:
|
|
150
|
+
*
|
|
151
|
+
* - `'CASCADE'` — this row is deleted with the target.
|
|
152
|
+
* - `'SET NULL'` — this column is set to `NULL`. The field must be nullable;
|
|
153
|
+
* declaring it on a `required` field throws `ConfigurationError`.
|
|
154
|
+
* - `'RESTRICT'` — deleting the target throws `DatabaseError` while any row
|
|
155
|
+
* still points at it.
|
|
156
|
+
*
|
|
157
|
+
* When omitted, a column that is part of this class's `conflictColumns`
|
|
158
|
+
* (junction and association rows, which are *identified* by the reference)
|
|
159
|
+
* defaults to `CASCADE`; every other column is left untouched on delete.
|
|
160
|
+
* `@tenantId()` fields are the one exception: `smrt-tenancy` leads a
|
|
161
|
+
* tenant-scoped class's default `conflictColumns` with the tenant column,
|
|
162
|
+
* but that column scopes ownership rather than identifying the row, so it
|
|
163
|
+
* is never defaulted to `CASCADE` — deleting a `Tenant` must not cascade
|
|
164
|
+
* through every tenant-scoped table.
|
|
165
|
+
*
|
|
166
|
+
* Cascaded rows are removed set-based: their `beforeDelete`/`afterDelete`
|
|
167
|
+
* hooks and interceptors do not run and no change-feed entry is written, the
|
|
168
|
+
* same as a database-level `ON DELETE CASCADE`.
|
|
169
|
+
*/
|
|
170
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
145
171
|
}
|
|
146
172
|
/**
|
|
147
173
|
* Options specific to cross-package references.
|
|
@@ -217,6 +243,23 @@ export declare function field(options?: FieldOptions | NumericFieldOptions | Tex
|
|
|
217
243
|
* For cross-package foreign keys, use a plain `string` property instead to avoid
|
|
218
244
|
* circular dependencies between packages.
|
|
219
245
|
*
|
|
246
|
+
* **No DDL constraint is emitted.** SMRT generates no `FOREIGN KEY` clause on
|
|
247
|
+
* any engine — the relationship is enforced by the framework, not the database
|
|
248
|
+
* (#2371). What that buys you is `loadRelated()`, eager `include:` loading, and
|
|
249
|
+
* the `onDelete` behaviour applied by `SmrtObject.delete()`:
|
|
250
|
+
*
|
|
251
|
+
* ```typescript
|
|
252
|
+
* @foreignKey(Order, { onDelete: 'CASCADE' }) // deleted with the order
|
|
253
|
+
* orderId: string = '';
|
|
254
|
+
*
|
|
255
|
+
* @foreignKey(Customer, { onDelete: 'RESTRICT' }) // blocks the customer delete
|
|
256
|
+
* customerId: string = '';
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* Without an `onDelete`, a column that is part of this class's
|
|
260
|
+
* `conflictColumns` defaults to `CASCADE` (this is what cleans up junction
|
|
261
|
+
* rows); any other column is left untouched when the target is deleted.
|
|
262
|
+
*
|
|
220
263
|
* @param relatedClass - The target class constructor, its name as a string, or a
|
|
221
264
|
* `() => Target` thunk. A thunk is **invoked at decoration time**, so its
|
|
222
265
|
* target must already be initialized: a class from an already-evaluated module
|
|
@@ -262,10 +305,12 @@ export declare function foreignKey(relatedClass: string | Function, options?: Om
|
|
|
262
305
|
*
|
|
263
306
|
* Use this for relationships that point to a `SmrtObject` in a *different* package
|
|
264
307
|
* (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
308
|
+
*
|
|
309
|
+
* Like `@foreignKey()`, this emits **no** DDL `FOREIGN KEY` constraint — SMRT
|
|
310
|
+
* does not emit them on any engine (#2371). The decorated property is a
|
|
311
|
+
* `UUID` column on PostgreSQL/DuckDB and `TEXT` on SQLite, matching the target's
|
|
312
|
+
* id type; pass `{ idType: 'text' }` when the target declares
|
|
313
|
+
* `@smrt({ idType: 'text' })`.
|
|
269
314
|
*
|
|
270
315
|
* What you get over a plain string field:
|
|
271
316
|
* - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`
|
|
@@ -273,6 +318,8 @@ export declare function foreignKey(relatedClass: string | Function, options?: Om
|
|
|
273
318
|
* manifest is loaded.
|
|
274
319
|
* - Optional save-time validation (`validate: true`) confirms the referenced
|
|
275
320
|
* object exists, catching typos and stale IDs before they hit the database.
|
|
321
|
+
* - `onDelete` is honoured by `SmrtObject.delete()` when the target package's
|
|
322
|
+
* manifest is loaded in the same runtime.
|
|
276
323
|
*
|
|
277
324
|
* The `qualifiedName` is a fully-qualified class identifier in the form
|
|
278
325
|
* `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.
|
|
@@ -294,7 +341,7 @@ export declare function foreignKey(relatedClass: string | Function, options?: Om
|
|
|
294
341
|
* }
|
|
295
342
|
* ```
|
|
296
343
|
*
|
|
297
|
-
* @see {@link foreignKey} for same-package relationships
|
|
344
|
+
* @see {@link foreignKey} for same-package relationships
|
|
298
345
|
* @see SmrtObject.loadRelated for runtime resolution
|
|
299
346
|
*/
|
|
300
347
|
export declare function crossPackageRef(qualifiedName: string, options?: CrossPackageRefOptions): CompatiblePropertyDecorator;
|
|
@@ -319,6 +366,17 @@ export declare function crossPackageRef(qualifiedName: string, options?: CrossPa
|
|
|
319
366
|
* `loadRelatedMany` and the generated accessor resolve the intended inverse
|
|
320
367
|
* side. Without it the first matching foreign key is used.
|
|
321
368
|
*
|
|
369
|
+
* **Delete behaviour is declared on the child, not here.** `@oneToMany` is a
|
|
370
|
+
* transient read-side accessor; to have children removed with their parent,
|
|
371
|
+
* put `onDelete: 'CASCADE'` on the inverse `@foreignKey` (#2371):
|
|
372
|
+
*
|
|
373
|
+
* ```typescript
|
|
374
|
+
* class OrderItem extends SmrtObject {
|
|
375
|
+
* @foreignKey(Order, { onDelete: 'CASCADE' })
|
|
376
|
+
* orderId: string = '';
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
322
380
|
* @param relatedClass - The class constructor of the child/related objects
|
|
323
381
|
* @param options - Optional relationship options. `foreignKey` selects the
|
|
324
382
|
* inverse foreign-key field on `relatedClass` when it has more than one.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,KAAK,2BAA2B,EAIjC,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAExB;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,SAAS,GACT,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,MAAM,SAAS,GACjB,kBAAkB,GAClB,MAAM,GACN,YAAY,GACZ,iBAAiB,GACjB,WAAW,GACX,YAAY,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,EAAE,CAAC,EAAE,YAAY,CAAC;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,YAAY;IAC5D,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,IAAI,CAAC,EAAE,YAAY,GAAG,iBAAiB,GAAG,WAAW,GAAG,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"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"}
|
package/dist/decorators/index.js
CHANGED
|
@@ -109,6 +109,23 @@ function field(options = {}) {
|
|
|
109
109
|
* For cross-package foreign keys, use a plain `string` property instead to avoid
|
|
110
110
|
* circular dependencies between packages.
|
|
111
111
|
*
|
|
112
|
+
* **No DDL constraint is emitted.** SMRT generates no `FOREIGN KEY` clause on
|
|
113
|
+
* any engine — the relationship is enforced by the framework, not the database
|
|
114
|
+
* (#2371). What that buys you is `loadRelated()`, eager `include:` loading, and
|
|
115
|
+
* the `onDelete` behaviour applied by `SmrtObject.delete()`:
|
|
116
|
+
*
|
|
117
|
+
* ```typescript
|
|
118
|
+
* @foreignKey(Order, { onDelete: 'CASCADE' }) // deleted with the order
|
|
119
|
+
* orderId: string = '';
|
|
120
|
+
*
|
|
121
|
+
* @foreignKey(Customer, { onDelete: 'RESTRICT' }) // blocks the customer delete
|
|
122
|
+
* customerId: string = '';
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* Without an `onDelete`, a column that is part of this class's
|
|
126
|
+
* `conflictColumns` defaults to `CASCADE` (this is what cleans up junction
|
|
127
|
+
* rows); any other column is left untouched when the target is deleted.
|
|
128
|
+
*
|
|
112
129
|
* @param relatedClass - The target class constructor, its name as a string, or a
|
|
113
130
|
* `() => Target` thunk. A thunk is **invoked at decoration time**, so its
|
|
114
131
|
* target must already be initialized: a class from an already-evaluated module
|
|
@@ -164,10 +181,12 @@ function foreignKey(relatedClass, options = {}) {
|
|
|
164
181
|
*
|
|
165
182
|
* Use this for relationships that point to a `SmrtObject` in a *different* package
|
|
166
183
|
* (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
184
|
+
*
|
|
185
|
+
* Like `@foreignKey()`, this emits **no** DDL `FOREIGN KEY` constraint — SMRT
|
|
186
|
+
* does not emit them on any engine (#2371). The decorated property is a
|
|
187
|
+
* `UUID` column on PostgreSQL/DuckDB and `TEXT` on SQLite, matching the target's
|
|
188
|
+
* id type; pass `{ idType: 'text' }` when the target declares
|
|
189
|
+
* `@smrt({ idType: 'text' })`.
|
|
171
190
|
*
|
|
172
191
|
* What you get over a plain string field:
|
|
173
192
|
* - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`
|
|
@@ -175,6 +194,8 @@ function foreignKey(relatedClass, options = {}) {
|
|
|
175
194
|
* manifest is loaded.
|
|
176
195
|
* - Optional save-time validation (`validate: true`) confirms the referenced
|
|
177
196
|
* object exists, catching typos and stale IDs before they hit the database.
|
|
197
|
+
* - `onDelete` is honoured by `SmrtObject.delete()` when the target package's
|
|
198
|
+
* manifest is loaded in the same runtime.
|
|
178
199
|
*
|
|
179
200
|
* The `qualifiedName` is a fully-qualified class identifier in the form
|
|
180
201
|
* `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.
|
|
@@ -196,7 +217,7 @@ function foreignKey(relatedClass, options = {}) {
|
|
|
196
217
|
* }
|
|
197
218
|
* ```
|
|
198
219
|
*
|
|
199
|
-
* @see {@link foreignKey} for same-package relationships
|
|
220
|
+
* @see {@link foreignKey} for same-package relationships
|
|
200
221
|
* @see SmrtObject.loadRelated for runtime resolution
|
|
201
222
|
*/
|
|
202
223
|
function crossPackageRef(qualifiedName, options = {}) {
|
|
@@ -231,6 +252,17 @@ function crossPackageRef(qualifiedName, options = {}) {
|
|
|
231
252
|
* `loadRelatedMany` and the generated accessor resolve the intended inverse
|
|
232
253
|
* side. Without it the first matching foreign key is used.
|
|
233
254
|
*
|
|
255
|
+
* **Delete behaviour is declared on the child, not here.** `@oneToMany` is a
|
|
256
|
+
* transient read-side accessor; to have children removed with their parent,
|
|
257
|
+
* put `onDelete: 'CASCADE'` on the inverse `@foreignKey` (#2371):
|
|
258
|
+
*
|
|
259
|
+
* ```typescript
|
|
260
|
+
* class OrderItem extends SmrtObject {
|
|
261
|
+
* @foreignKey(Order, { onDelete: 'CASCADE' })
|
|
262
|
+
* orderId: string = '';
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
234
266
|
* @param relatedClass - The class constructor of the child/related objects
|
|
235
267
|
* @param options - Optional relationship options. `foreignKey` selects the
|
|
236
268
|
* inverse foreign-key field on `relatedClass` when it has more than one.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/decorators/index.ts"],"sourcesContent":["/**\n * Field decorators for SMRT objects\n *\n * Modern decorator-based API for defining SMRT object properties.\n * Properties are typed as primitives with decorator metadata.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldUIHints } from '../scanner/types.js';\nimport type { SQLDataType } from '../schema/types.js';\nimport {\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n} from './compatibility.js';\n\nexport type { FieldUIHints } from '../scanner/types.js';\n\n/**\n * Meta type wrapper for STI (Single Table Inheritance) meta fields\n *\n * Fields typed as Meta<T> are stored in the _meta_data JSONB column\n * rather than as direct table columns. Used for child-specific fields\n * in STI hierarchies.\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * // Stored in _meta_data JSONB column\n * roomNumber: Meta<string> = '';\n * attendees: Meta<string[]> = [];\n * }\n * ```\n */\nexport type Meta<T> = T;\n\n/**\n * Base field options\n */\nexport type PrimitiveFieldType =\n | 'text'\n | 'integer'\n | 'decimal'\n | 'boolean'\n | 'datetime'\n | 'json';\n\nexport type FieldType =\n | PrimitiveFieldType\n | 'meta'\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\nexport interface FieldOptions {\n /** Explicit field type for runtime-only registration paths */\n type?: FieldType;\n /** Explicit SQL storage type when runtime and persistence contracts differ */\n sqlType?: SQLDataType;\n /** Whether the field is required */\n required?: boolean;\n /** Default value for the field */\n default?: unknown;\n /** Whether the field is unique */\n unique?: boolean;\n /**\n * When `true`, the schema emits a database index targeting this field.\n *\n * For regular (column-backed) fields the index is a plain column index.\n * For `@meta()` fields stored inside `_meta_data` JSONB, the index targets\n * the JSON path — `json_extract(_meta_data, '$.fieldName')` on SQLite,\n * `(_meta_data->>'fieldName')` on Postgres.\n *\n * Note that `collection.list({ where })` cannot currently reach a JSON-path\n * index: dot-notation keys such as `_meta_data.fieldName` are rejected,\n * because nothing rewrites them into the matching extraction expression\n * (#2276, tracked in #2282). The index is still emitted and still serves\n * hand-written SQL that spells the expression out; it just has no\n * collection-level query to accelerate yet.\n */\n indexed?: boolean;\n /** Whether the field is nullable */\n nullable?: boolean;\n /** Whether the field should be excluded from database */\n transient?: boolean;\n /**\n * Marks the field as sensitive (e.g. API secrets, credentials, tax IDs).\n *\n * Sensitive fields are still persisted to the database, but the framework:\n * - excludes them from `toPublicJSON()` (the serializer used by generated\n * REST/MCP/SvelteKit routes), so they never appear in API responses; and\n * - rejects them as `where`-clause filter keys, closing the\n * `?secret[like]=...` value-probing oracle.\n *\n * Use this for any column that holds a secret value that must never be\n * read back over a generated network surface.\n */\n sensitive?: boolean;\n /**\n * Marks the field as read-only over generated write surfaces.\n *\n * Read-only fields are stripped from the request body before\n * `create`/`update` in generated REST/MCP/SvelteKit routes, so callers\n * cannot mass-assign them. Server-side code can still set them directly.\n */\n readonly?: boolean;\n /**\n * Permission slug required to include this field in public/read responses.\n *\n * Fields with a read permission are fail-closed: generated serializers omit\n * them unless the caller's resolved permission set contains this slug.\n * `sensitive: true` still wins and omits the field for every caller.\n */\n readPermission?: string;\n /** Field description */\n description?: string;\n /**\n * Static UI hints for the field-policy rail (#2046, epic #2045).\n *\n * A pure presentation seed — carried in the manifest under the field's\n * `_meta.ui`, readable at runtime via `getAllFields()` at `field._meta.ui`,\n * and emitted to the browser in generated web-collection definitions. Has no\n * schema, persistence, or security effect; `sensitive`/`readPermission`\n * remain the security rail.\n */\n ui?: FieldUIHints;\n /**\n * Controls whether the field is included in JSON exports.\n * - `true`: Always exported (unless site explicitly excludes it)\n * - `false`: Never exported (cannot be overridden by site config)\n * - `undefined`: Uses site's fieldExportDefault setting\n */\n exported?: boolean;\n}\n\n/**\n * Options for text fields\n */\nexport interface TextFieldOptions extends FieldOptions {\n /** Minimum length for text fields */\n minLength?: number;\n /** Maximum length for text fields */\n maxLength?: number;\n /** Regex pattern for validation */\n pattern?: RegExp | string;\n}\n\n/**\n * Options for numeric fields\n */\nexport interface NumericFieldOptions extends FieldOptions {\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n}\n\n/**\n * Options for relationship fields\n */\nexport interface RelationshipFieldOptions extends FieldOptions {\n /** Related class name */\n related?: string;\n /** Foreign key field name */\n foreignKey?: string;\n /** Through table for many-to-many */\n through?: string;\n /** Relationship type */\n type?: 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany';\n}\n\n/**\n * Options specific to cross-package references.\n */\nexport interface CrossPackageRefOptions\n extends Omit<RelationshipFieldOptions, 'related' | 'type'> {\n /**\n * Storage type for the referenced target id. Defaults to 'uuid'.\n *\n * Use 'text' only when the external target model declares\n * `@smrt({ idType: 'text' })`.\n */\n idType?: 'uuid' | 'text';\n\n /**\n * When `true`, the framework verifies the referenced object exists at save time.\n * Validation uses the target package's manifest (loaded on demand via\n * `ObjectRegistry.ensureManifestLoaded()`), so this requires the target manifest\n * to be discoverable at runtime.\n *\n * Empty/null values are always allowed (treated as \"no reference set\").\n *\n * Defaults to `false` — same behavior as a plain string field today.\n */\n validate?: boolean;\n}\n\n/**\n * Resolve a relationship decorator's target argument to a class name.\n *\n * Accepted target forms:\n * - `'Target'` — class name string; resolves lazily, immune to import cycles.\n * - `Target` — class constructor.\n * - `() => Target` — thunk (inline or a named `const`), invoked here to read\n * the name, so its target must already be initialized.\n *\n * A thunk's own `.name` is `''`, so reading `relatedClass.name` used to register\n * `related: ''` (issue #2379): the field kept `type: 'foreignKey'` but lost its\n * target, which silently dropped the relationship edge, `loadRelated()`, and any\n * FK-derived index. Thunks are therefore invoked at decoration time and an\n * unresolvable target throws with the string form as the remedy — an empty\n * `related` is never registered.\n *\n * Resolution runs inside the field-registration callback, which is the latest\n * point in the decorator lifecycle (after the class binding exists for legacy\n * decorators, and at `@smrt()` application time for standard decorators), so a\n * self-referential `() => Self` thunk resolves rather than hitting the TDZ.\n * A thunk pointing at a class declared LATER in the same module is still in\n * that class's temporal dead zone when the decorators of the earlier class run;\n * that now fails loudly, naming the string form, instead of silently\n * registering an empty target.\n */\nfunction resolveRelatedClassName(\n decoratorName: 'foreignKey' | 'oneToMany' | 'manyToMany',\n relatedClass: string | Function,\n className: string,\n propertyKey: string,\n): string {\n const where = `@${decoratorName}() on ${className}.${propertyKey}`;\n const remedy =\n `Pass the target class name as a string instead — ` +\n `\\`@${decoratorName}('Target')\\` resolves lazily and is immune to import cycles.`;\n\n if (typeof relatedClass === 'string') {\n const name = relatedClass.trim();\n if (!name) {\n throw new Error(\n `${where}: target class name is empty. Pass a class, a class name, or a \\`() => Target\\` thunk.`,\n );\n }\n return name;\n }\n\n if (typeof relatedClass !== 'function') {\n throw new Error(\n `${where}: expected a class, a class name, or a \\`() => Target\\` thunk, received ${relatedClass === null ? 'null' : typeof relatedClass}. ${remedy}`,\n );\n }\n\n // Class/function reference — the common `@foreignKey(Target)` form. Arrow\n // functions have no `prototype`, so a *named* thunk (`const lazyTarget = () =>\n // Target`) is still routed to the thunk branch below instead of registering\n // the variable name as the target class.\n if (relatedClass.name && relatedClass.prototype !== undefined) {\n return relatedClass.name;\n }\n\n // Thunk (`() => Target`, named or inline) — invoke it for the target.\n let resolved: unknown;\n try {\n resolved = (relatedClass as () => unknown)();\n } catch (error) {\n throw new Error(\n `${where}: the \\`() => Target\\` thunk threw while resolving its target (${\n error instanceof Error ? error.message : String(error)\n }). ${remedy}`,\n { cause: error },\n );\n }\n\n if (typeof resolved === 'function' && resolved.name) {\n return resolved.name;\n }\n if (typeof resolved === 'string' && resolved.trim()) {\n return resolved.trim();\n }\n\n throw new Error(\n `${where}: the \\`() => Target\\` thunk resolved to ${\n resolved === null ? 'null' : typeof resolved\n } instead of a named class. ${remedy}`,\n );\n}\n\n/**\n * Marks a class property with validation constraints and metadata options.\n *\n * Use `@field()` when you need options beyond what plain TypeScript initializers\n * express — required validation, numeric ranges, string length limits, uniqueness,\n * or transient (non-persisted) computed properties.\n *\n * For plain persisted fields with no constraints, no decorator is needed: just\n * declare the property with a TypeScript initializer and the framework will infer\n * the column type from the default value (`0` → INTEGER, `0.0` → DECIMAL, `''` → TEXT).\n *\n * @param options - Field configuration options\n * @param options.required - If `true`, `save()` throws `ValidationError` when empty/null\n * @param options.unique - Enforces a UNIQUE database constraint\n * @param options.nullable - If `true`, the column accepts NULL (default depends on type)\n * @param options.transient - If `true`, the property is not persisted to the database\n * @param options.default - Default value applied at the database level\n * @param options.description - Human-readable description used in generated API docs\n * @param options.exported - Controls JSON export visibility (see `FieldOptions`)\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @field({ required: true, maxLength: 100 })\n * name: string = '';\n *\n * @field({ min: 0 })\n * stock: number = 0;\n *\n * @field({ transient: true })\n * get displayPrice(): string { return `$${this.price.toFixed(2)}`; }\n * }\n * ```\n *\n * @see {@link meta} for STI child-specific fields stored in `_meta_data` JSON\n * @see {@link foreignKey} for typed relationship fields\n */\nexport function field(\n options: FieldOptions | NumericFieldOptions | TextFieldOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, options);\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-one (foreign key) relationship to another `SmrtObject` class.\n *\n * The decorated property stores the UUID of the related object. At runtime, call\n * `instance.loadRelated('fieldName')` to lazy-load (and cache) the related object,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading.\n *\n * Cross-package rule: Use `@foreignKey()` only for same-package references.\n * For cross-package foreign keys, use a plain `string` property instead to avoid\n * circular dependencies between packages.\n *\n * @param relatedClass - The target class constructor, its name as a string, or a\n * `() => Target` thunk. A thunk is **invoked at decoration time**, so its\n * target must already be initialized: a class from an already-evaluated module\n * or the decorated class itself. Use the string form for a class declared\n * later in the same module or reached through an import cycle — it is never\n * evaluated, so it cannot hit the temporal dead zone.\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\n * @throws Error when the target cannot be resolved to a class name (an empty\n * string, a thunk that throws — including on an uninitialized target — or a\n * thunk returning an anonymous value)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * // Same-package FK — enables loadRelated() and eager loading\n * @foreignKey(Customer)\n * customerId: string = '';\n *\n * // Self-reference: the class binding exists when its decorators run\n * @foreignKey(() => Order)\n * parentOrderId: string = '';\n *\n * // Declared later in this module — name string, never evaluated\n * @foreignKey('Invoice')\n * invoiceId: string = '';\n * }\n *\n * // Cross-package: use a plain string instead\n * @smrt()\n * class Post extends SmrtObject {\n * authorId: string = ''; // plain string — no circular dep\n * }\n * ```\n *\n * @see {@link oneToMany} for the inverse (parent) side of the relationship\n * @see SmrtObject.loadRelated for lazy-loading the related object at runtime\n */\nexport function foreignKey(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: resolveRelatedClassName(\n 'foreignKey',\n relatedClass,\n className,\n propertyKey,\n ),\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a cross-package foreign key reference.\n *\n * Use this for relationships that point to a `SmrtObject` in a *different* package\n * (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).\n * Unlike `@foreignKey()`, this decorator does **not** emit a DDL `FOREIGN KEY`\n * constraint — cross-package classes are not visible at schema-generation time and\n * adding a constraint would force a circular package dependency. The decorated\n * property remains a plain `TEXT` column at the database level.\n *\n * What you get over a plain string field:\n * - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`\n * and `Collection.list({ include })` can resolve it once the target package's\n * manifest is loaded.\n * - Optional save-time validation (`validate: true`) confirms the referenced\n * object exists, catching typos and stale IDs before they hit the database.\n *\n * The `qualifiedName` is a fully-qualified class identifier in the form\n * `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.\n *\n * @param qualifiedName - Qualified name of the target class\n * @param options - Optional field constraints and `validate` flag\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Customer extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n * profileId: string = '';\n *\n * // With save-time validation\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile', { validate: true })\n * primaryContactId: string = '';\n * }\n * ```\n *\n * @see {@link foreignKey} for same-package relationships (emits FK constraint)\n * @see SmrtObject.loadRelated for runtime resolution\n */\nexport function crossPackageRef(\n qualifiedName: string,\n options: CrossPackageRefOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'crossPackageRef',\n related: qualifiedName,\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a one-to-many relationship from this object to a collection of related objects.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * At runtime, call `instance.loadRelatedMany('fieldName')` to load the related objects,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading (issues\n * a single batched query for all instances instead of N individual queries).\n *\n * The inverse side (`@foreignKey`) must exist on the `relatedClass` pointing back to this\n * class. The framework discovers it automatically via `ObjectRegistry.getInverseRelationships()`.\n *\n * **Generated accessor (R10):** registering the class installs a consistent\n * `get<FieldName>()` instance method (e.g. `items` → `order.getItems()`) that\n * delegates to `loadRelatedMany('items')`. Generation is additive — a\n * hand-rolled method of the same name is never overwritten.\n *\n * **Disambiguation:** when `relatedClass` declares more than one `@foreignKey`\n * back to this class, pass `{ foreignKey: '<inverseFieldName>' }` so both\n * `loadRelatedMany` and the generated accessor resolve the intended inverse\n * side. Without it the first matching foreign key is used.\n *\n * @param relatedClass - The class constructor of the child/related objects\n * @param options - Optional relationship options. `foreignKey` selects the\n * inverse foreign-key field on `relatedClass` when it has more than one.\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * @oneToMany(OrderItem)\n * items: OrderItem[] = [];\n * }\n *\n * @smrt()\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order)\n * orderId: string = '';\n * }\n *\n * const order = await orders.get({ id });\n * const items = await order.getItems(); // generated; === loadRelatedMany('items')\n * ```\n *\n * @example\n * ```typescript\n * // Multiple inverse foreign keys → disambiguate explicitly.\n * @smrt()\n * class Profile extends SmrtObject {\n * @oneToMany(ProfileRelationship, { foreignKey: 'fromProfileId' })\n * relationshipsFrom: ProfileRelationship[] = [];\n * @oneToMany(ProfileRelationship, { foreignKey: 'toProfileId' })\n * relationshipsTo: ProfileRelationship[] = [];\n * }\n * ```\n *\n * @see {@link foreignKey} for the many-to-one (child) side of the relationship\n * @see SmrtObject.loadRelatedMany for lazy-loading at runtime\n */\nexport function oneToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: resolveRelatedClassName(\n 'oneToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-many relationship between two `SmrtObject` classes via a join table.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * The `through` option specifies the junction table name. The join table model must\n * be decorated with `@smrt({ conflictColumns: ['...', '...'] })` to use the natural\n * key columns for upsert operations.\n *\n * Runtime loading: call `instance.loadRelatedMany('field')` to lazy-load, or\n * pass `include: ['field']` to `collection.list()` for batched eager loading.\n *\n * @param relatedClass - The class constructor of the related objects\n * @param options - Relationship options; `through` specifies the junction table name\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @manyToMany(Tag, { through: 'product_tags' })\n * tags: Tag[] = [];\n * }\n * ```\n *\n * @see {@link oneToMany} for one-to-many relationships\n */\nexport function manyToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: resolveRelatedClassName(\n 'manyToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Marks a field as a Single Table Inheritance (STI) meta field.\n *\n * Meta fields are stored in the `_meta_data` JSONB column on the shared STI\n * table rather than as dedicated table columns. Use this decorator for fields\n * that are specific to an STI child class and should not pollute the shared\n * table schema with child-specific columns.\n *\n * The `@smrt({ tableStrategy: 'sti' })` decorator must be set on the base class.\n * All child-specific fields should use `@meta()` (or the `Meta<T>` type alias).\n *\n * @param options - Standard field options (required, nullable, description, etc.)\n * @returns A TypeScript property decorator (registers field with `type: 'meta'`)\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = ''; // shared column on events table\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * @meta()\n * roomNumber: string = ''; // stored in _meta_data JSON, not a column\n *\n * @meta({ required: true })\n * durationMinutes: number = 60;\n * }\n * ```\n *\n * @see {@link Meta} for the equivalent type alias approach\n * @see {@link field} for regular (non-STI) field declarations\n */\nexport function meta(options: FieldOptions = {}) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'meta', // Mark this field as a meta field for STI\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/decorators/index.ts"],"sourcesContent":["/**\n * Field decorators for SMRT objects\n *\n * Modern decorator-based API for defining SMRT object properties.\n * Properties are typed as primitives with decorator metadata.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldUIHints } from '../scanner/types.js';\nimport type { SQLDataType } from '../schema/types.js';\nimport {\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n} from './compatibility.js';\n\nexport type { FieldUIHints } from '../scanner/types.js';\n\n/**\n * Meta type wrapper for STI (Single Table Inheritance) meta fields\n *\n * Fields typed as Meta<T> are stored in the _meta_data JSONB column\n * rather than as direct table columns. Used for child-specific fields\n * in STI hierarchies.\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * // Stored in _meta_data JSONB column\n * roomNumber: Meta<string> = '';\n * attendees: Meta<string[]> = [];\n * }\n * ```\n */\nexport type Meta<T> = T;\n\n/**\n * Base field options\n */\nexport type PrimitiveFieldType =\n | 'text'\n | 'integer'\n | 'decimal'\n | 'boolean'\n | 'datetime'\n | 'json';\n\nexport type FieldType =\n | PrimitiveFieldType\n | 'meta'\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\nexport interface FieldOptions {\n /** Explicit field type for runtime-only registration paths */\n type?: FieldType;\n /** Explicit SQL storage type when runtime and persistence contracts differ */\n sqlType?: SQLDataType;\n /** Whether the field is required */\n required?: boolean;\n /** Default value for the field */\n default?: unknown;\n /** Whether the field is unique */\n unique?: boolean;\n /**\n * When `true`, the schema emits a database index targeting this field.\n *\n * For regular (column-backed) fields the index is a plain column index.\n * For `@meta()` fields stored inside `_meta_data` JSONB, the index targets\n * the JSON path — `json_extract(_meta_data, '$.fieldName')` on SQLite,\n * `(_meta_data->>'fieldName')` on Postgres.\n *\n * Note that `collection.list({ where })` cannot currently reach a JSON-path\n * index: dot-notation keys such as `_meta_data.fieldName` are rejected,\n * because nothing rewrites them into the matching extraction expression\n * (#2276, tracked in #2282). The index is still emitted and still serves\n * hand-written SQL that spells the expression out; it just has no\n * collection-level query to accelerate yet.\n */\n indexed?: boolean;\n /** Whether the field is nullable */\n nullable?: boolean;\n /** Whether the field should be excluded from database */\n transient?: boolean;\n /**\n * Marks the field as sensitive (e.g. API secrets, credentials, tax IDs).\n *\n * Sensitive fields are still persisted to the database, but the framework:\n * - excludes them from `toPublicJSON()` (the serializer used by generated\n * REST/MCP/SvelteKit routes), so they never appear in API responses; and\n * - rejects them as `where`-clause filter keys, closing the\n * `?secret[like]=...` value-probing oracle.\n *\n * Use this for any column that holds a secret value that must never be\n * read back over a generated network surface.\n */\n sensitive?: boolean;\n /**\n * Marks the field as read-only over generated write surfaces.\n *\n * Read-only fields are stripped from the request body before\n * `create`/`update` in generated REST/MCP/SvelteKit routes, so callers\n * cannot mass-assign them. Server-side code can still set them directly.\n */\n readonly?: boolean;\n /**\n * Permission slug required to include this field in public/read responses.\n *\n * Fields with a read permission are fail-closed: generated serializers omit\n * them unless the caller's resolved permission set contains this slug.\n * `sensitive: true` still wins and omits the field for every caller.\n */\n readPermission?: string;\n /** Field description */\n description?: string;\n /**\n * Static UI hints for the field-policy rail (#2046, epic #2045).\n *\n * A pure presentation seed — carried in the manifest under the field's\n * `_meta.ui`, readable at runtime via `getAllFields()` at `field._meta.ui`,\n * and emitted to the browser in generated web-collection definitions. Has no\n * schema, persistence, or security effect; `sensitive`/`readPermission`\n * remain the security rail.\n */\n ui?: FieldUIHints;\n /**\n * Controls whether the field is included in JSON exports.\n * - `true`: Always exported (unless site explicitly excludes it)\n * - `false`: Never exported (cannot be overridden by site config)\n * - `undefined`: Uses site's fieldExportDefault setting\n */\n exported?: boolean;\n}\n\n/**\n * Options for text fields\n */\nexport interface TextFieldOptions extends FieldOptions {\n /** Minimum length for text fields */\n minLength?: number;\n /** Maximum length for text fields */\n maxLength?: number;\n /** Regex pattern for validation */\n pattern?: RegExp | string;\n}\n\n/**\n * Options for numeric fields\n */\nexport interface NumericFieldOptions extends FieldOptions {\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n}\n\n/**\n * Options for relationship fields\n */\nexport interface RelationshipFieldOptions extends FieldOptions {\n /** Related class name */\n related?: string;\n /** Foreign key field name */\n foreignKey?: string;\n /** Through table for many-to-many */\n through?: string;\n /** Relationship type */\n type?: 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany';\n /**\n * What happens to this row when the referenced object is deleted (#2371).\n *\n * SMRT emits no DB-level `FOREIGN KEY` constraints, so this is enforced by\n * `SmrtObject.delete()` in the application layer rather than by the engine:\n *\n * - `'CASCADE'` — this row is deleted with the target.\n * - `'SET NULL'` — this column is set to `NULL`. The field must be nullable;\n * declaring it on a `required` field throws `ConfigurationError`.\n * - `'RESTRICT'` — deleting the target throws `DatabaseError` while any row\n * still points at it.\n *\n * When omitted, a column that is part of this class's `conflictColumns`\n * (junction and association rows, which are *identified* by the reference)\n * defaults to `CASCADE`; every other column is left untouched on delete.\n * `@tenantId()` fields are the one exception: `smrt-tenancy` leads a\n * tenant-scoped class's default `conflictColumns` with the tenant column,\n * but that column scopes ownership rather than identifying the row, so it\n * is never defaulted to `CASCADE` — deleting a `Tenant` must not cascade\n * through every tenant-scoped table.\n *\n * Cascaded rows are removed set-based: their `beforeDelete`/`afterDelete`\n * hooks and interceptors do not run and no change-feed entry is written, the\n * same as a database-level `ON DELETE CASCADE`.\n */\n onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';\n}\n\n/**\n * Options specific to cross-package references.\n */\nexport interface CrossPackageRefOptions\n extends Omit<RelationshipFieldOptions, 'related' | 'type'> {\n /**\n * Storage type for the referenced target id. Defaults to 'uuid'.\n *\n * Use 'text' only when the external target model declares\n * `@smrt({ idType: 'text' })`.\n */\n idType?: 'uuid' | 'text';\n\n /**\n * When `true`, the framework verifies the referenced object exists at save time.\n * Validation uses the target package's manifest (loaded on demand via\n * `ObjectRegistry.ensureManifestLoaded()`), so this requires the target manifest\n * to be discoverable at runtime.\n *\n * Empty/null values are always allowed (treated as \"no reference set\").\n *\n * Defaults to `false` — same behavior as a plain string field today.\n */\n validate?: boolean;\n}\n\n/**\n * Resolve a relationship decorator's target argument to a class name.\n *\n * Accepted target forms:\n * - `'Target'` — class name string; resolves lazily, immune to import cycles.\n * - `Target` — class constructor.\n * - `() => Target` — thunk (inline or a named `const`), invoked here to read\n * the name, so its target must already be initialized.\n *\n * A thunk's own `.name` is `''`, so reading `relatedClass.name` used to register\n * `related: ''` (issue #2379): the field kept `type: 'foreignKey'` but lost its\n * target, which silently dropped the relationship edge, `loadRelated()`, and any\n * FK-derived index. Thunks are therefore invoked at decoration time and an\n * unresolvable target throws with the string form as the remedy — an empty\n * `related` is never registered.\n *\n * Resolution runs inside the field-registration callback, which is the latest\n * point in the decorator lifecycle (after the class binding exists for legacy\n * decorators, and at `@smrt()` application time for standard decorators), so a\n * self-referential `() => Self` thunk resolves rather than hitting the TDZ.\n * A thunk pointing at a class declared LATER in the same module is still in\n * that class's temporal dead zone when the decorators of the earlier class run;\n * that now fails loudly, naming the string form, instead of silently\n * registering an empty target.\n */\nfunction resolveRelatedClassName(\n decoratorName: 'foreignKey' | 'oneToMany' | 'manyToMany',\n relatedClass: string | Function,\n className: string,\n propertyKey: string,\n): string {\n const where = `@${decoratorName}() on ${className}.${propertyKey}`;\n const remedy =\n `Pass the target class name as a string instead — ` +\n `\\`@${decoratorName}('Target')\\` resolves lazily and is immune to import cycles.`;\n\n if (typeof relatedClass === 'string') {\n const name = relatedClass.trim();\n if (!name) {\n throw new Error(\n `${where}: target class name is empty. Pass a class, a class name, or a \\`() => Target\\` thunk.`,\n );\n }\n return name;\n }\n\n if (typeof relatedClass !== 'function') {\n throw new Error(\n `${where}: expected a class, a class name, or a \\`() => Target\\` thunk, received ${relatedClass === null ? 'null' : typeof relatedClass}. ${remedy}`,\n );\n }\n\n // Class/function reference — the common `@foreignKey(Target)` form. Arrow\n // functions have no `prototype`, so a *named* thunk (`const lazyTarget = () =>\n // Target`) is still routed to the thunk branch below instead of registering\n // the variable name as the target class.\n if (relatedClass.name && relatedClass.prototype !== undefined) {\n return relatedClass.name;\n }\n\n // Thunk (`() => Target`, named or inline) — invoke it for the target.\n let resolved: unknown;\n try {\n resolved = (relatedClass as () => unknown)();\n } catch (error) {\n throw new Error(\n `${where}: the \\`() => Target\\` thunk threw while resolving its target (${\n error instanceof Error ? error.message : String(error)\n }). ${remedy}`,\n { cause: error },\n );\n }\n\n if (typeof resolved === 'function' && resolved.name) {\n return resolved.name;\n }\n if (typeof resolved === 'string' && resolved.trim()) {\n return resolved.trim();\n }\n\n throw new Error(\n `${where}: the \\`() => Target\\` thunk resolved to ${\n resolved === null ? 'null' : typeof resolved\n } instead of a named class. ${remedy}`,\n );\n}\n\n/**\n * Marks a class property with validation constraints and metadata options.\n *\n * Use `@field()` when you need options beyond what plain TypeScript initializers\n * express — required validation, numeric ranges, string length limits, uniqueness,\n * or transient (non-persisted) computed properties.\n *\n * For plain persisted fields with no constraints, no decorator is needed: just\n * declare the property with a TypeScript initializer and the framework will infer\n * the column type from the default value (`0` → INTEGER, `0.0` → DECIMAL, `''` → TEXT).\n *\n * @param options - Field configuration options\n * @param options.required - If `true`, `save()` throws `ValidationError` when empty/null\n * @param options.unique - Enforces a UNIQUE database constraint\n * @param options.nullable - If `true`, the column accepts NULL (default depends on type)\n * @param options.transient - If `true`, the property is not persisted to the database\n * @param options.default - Default value applied at the database level\n * @param options.description - Human-readable description used in generated API docs\n * @param options.exported - Controls JSON export visibility (see `FieldOptions`)\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @field({ required: true, maxLength: 100 })\n * name: string = '';\n *\n * @field({ min: 0 })\n * stock: number = 0;\n *\n * @field({ transient: true })\n * get displayPrice(): string { return `$${this.price.toFixed(2)}`; }\n * }\n * ```\n *\n * @see {@link meta} for STI child-specific fields stored in `_meta_data` JSON\n * @see {@link foreignKey} for typed relationship fields\n */\nexport function field(\n options: FieldOptions | NumericFieldOptions | TextFieldOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, options);\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-one (foreign key) relationship to another `SmrtObject` class.\n *\n * The decorated property stores the UUID of the related object. At runtime, call\n * `instance.loadRelated('fieldName')` to lazy-load (and cache) the related object,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading.\n *\n * Cross-package rule: Use `@foreignKey()` only for same-package references.\n * For cross-package foreign keys, use a plain `string` property instead to avoid\n * circular dependencies between packages.\n *\n * **No DDL constraint is emitted.** SMRT generates no `FOREIGN KEY` clause on\n * any engine — the relationship is enforced by the framework, not the database\n * (#2371). What that buys you is `loadRelated()`, eager `include:` loading, and\n * the `onDelete` behaviour applied by `SmrtObject.delete()`:\n *\n * ```typescript\n * @foreignKey(Order, { onDelete: 'CASCADE' }) // deleted with the order\n * orderId: string = '';\n *\n * @foreignKey(Customer, { onDelete: 'RESTRICT' }) // blocks the customer delete\n * customerId: string = '';\n * ```\n *\n * Without an `onDelete`, a column that is part of this class's\n * `conflictColumns` defaults to `CASCADE` (this is what cleans up junction\n * rows); any other column is left untouched when the target is deleted.\n *\n * @param relatedClass - The target class constructor, its name as a string, or a\n * `() => Target` thunk. A thunk is **invoked at decoration time**, so its\n * target must already be initialized: a class from an already-evaluated module\n * or the decorated class itself. Use the string form for a class declared\n * later in the same module or reached through an import cycle — it is never\n * evaluated, so it cannot hit the temporal dead zone.\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\n * @throws Error when the target cannot be resolved to a class name (an empty\n * string, a thunk that throws — including on an uninitialized target — or a\n * thunk returning an anonymous value)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * // Same-package FK — enables loadRelated() and eager loading\n * @foreignKey(Customer)\n * customerId: string = '';\n *\n * // Self-reference: the class binding exists when its decorators run\n * @foreignKey(() => Order)\n * parentOrderId: string = '';\n *\n * // Declared later in this module — name string, never evaluated\n * @foreignKey('Invoice')\n * invoiceId: string = '';\n * }\n *\n * // Cross-package: use a plain string instead\n * @smrt()\n * class Post extends SmrtObject {\n * authorId: string = ''; // plain string — no circular dep\n * }\n * ```\n *\n * @see {@link oneToMany} for the inverse (parent) side of the relationship\n * @see SmrtObject.loadRelated for lazy-loading the related object at runtime\n */\nexport function foreignKey(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: resolveRelatedClassName(\n 'foreignKey',\n relatedClass,\n className,\n propertyKey,\n ),\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a cross-package foreign key reference.\n *\n * Use this for relationships that point to a `SmrtObject` in a *different* package\n * (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).\n *\n * Like `@foreignKey()`, this emits **no** DDL `FOREIGN KEY` constraint — SMRT\n * does not emit them on any engine (#2371). The decorated property is a\n * `UUID` column on PostgreSQL/DuckDB and `TEXT` on SQLite, matching the target's\n * id type; pass `{ idType: 'text' }` when the target declares\n * `@smrt({ idType: 'text' })`.\n *\n * What you get over a plain string field:\n * - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`\n * and `Collection.list({ include })` can resolve it once the target package's\n * manifest is loaded.\n * - Optional save-time validation (`validate: true`) confirms the referenced\n * object exists, catching typos and stale IDs before they hit the database.\n * - `onDelete` is honoured by `SmrtObject.delete()` when the target package's\n * manifest is loaded in the same runtime.\n *\n * The `qualifiedName` is a fully-qualified class identifier in the form\n * `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.\n *\n * @param qualifiedName - Qualified name of the target class\n * @param options - Optional field constraints and `validate` flag\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Customer extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n * profileId: string = '';\n *\n * // With save-time validation\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile', { validate: true })\n * primaryContactId: string = '';\n * }\n * ```\n *\n * @see {@link foreignKey} for same-package relationships\n * @see SmrtObject.loadRelated for runtime resolution\n */\nexport function crossPackageRef(\n qualifiedName: string,\n options: CrossPackageRefOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'crossPackageRef',\n related: qualifiedName,\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a one-to-many relationship from this object to a collection of related objects.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * At runtime, call `instance.loadRelatedMany('fieldName')` to load the related objects,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading (issues\n * a single batched query for all instances instead of N individual queries).\n *\n * The inverse side (`@foreignKey`) must exist on the `relatedClass` pointing back to this\n * class. The framework discovers it automatically via `ObjectRegistry.getInverseRelationships()`.\n *\n * **Generated accessor (R10):** registering the class installs a consistent\n * `get<FieldName>()` instance method (e.g. `items` → `order.getItems()`) that\n * delegates to `loadRelatedMany('items')`. Generation is additive — a\n * hand-rolled method of the same name is never overwritten.\n *\n * **Disambiguation:** when `relatedClass` declares more than one `@foreignKey`\n * back to this class, pass `{ foreignKey: '<inverseFieldName>' }` so both\n * `loadRelatedMany` and the generated accessor resolve the intended inverse\n * side. Without it the first matching foreign key is used.\n *\n * **Delete behaviour is declared on the child, not here.** `@oneToMany` is a\n * transient read-side accessor; to have children removed with their parent,\n * put `onDelete: 'CASCADE'` on the inverse `@foreignKey` (#2371):\n *\n * ```typescript\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order, { onDelete: 'CASCADE' })\n * orderId: string = '';\n * }\n * ```\n *\n * @param relatedClass - The class constructor of the child/related objects\n * @param options - Optional relationship options. `foreignKey` selects the\n * inverse foreign-key field on `relatedClass` when it has more than one.\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * @oneToMany(OrderItem)\n * items: OrderItem[] = [];\n * }\n *\n * @smrt()\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order)\n * orderId: string = '';\n * }\n *\n * const order = await orders.get({ id });\n * const items = await order.getItems(); // generated; === loadRelatedMany('items')\n * ```\n *\n * @example\n * ```typescript\n * // Multiple inverse foreign keys → disambiguate explicitly.\n * @smrt()\n * class Profile extends SmrtObject {\n * @oneToMany(ProfileRelationship, { foreignKey: 'fromProfileId' })\n * relationshipsFrom: ProfileRelationship[] = [];\n * @oneToMany(ProfileRelationship, { foreignKey: 'toProfileId' })\n * relationshipsTo: ProfileRelationship[] = [];\n * }\n * ```\n *\n * @see {@link foreignKey} for the many-to-one (child) side of the relationship\n * @see SmrtObject.loadRelatedMany for lazy-loading at runtime\n */\nexport function oneToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: resolveRelatedClassName(\n 'oneToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-many relationship between two `SmrtObject` classes via a join table.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * The `through` option specifies the junction table name. The join table model must\n * be decorated with `@smrt({ conflictColumns: ['...', '...'] })` to use the natural\n * key columns for upsert operations.\n *\n * Runtime loading: call `instance.loadRelatedMany('field')` to lazy-load, or\n * pass `include: ['field']` to `collection.list()` for batched eager loading.\n *\n * @param relatedClass - The class constructor of the related objects\n * @param options - Relationship options; `through` specifies the junction table name\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @manyToMany(Tag, { through: 'product_tags' })\n * tags: Tag[] = [];\n * }\n * ```\n *\n * @see {@link oneToMany} for one-to-many relationships\n */\nexport function manyToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: resolveRelatedClassName(\n 'manyToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Marks a field as a Single Table Inheritance (STI) meta field.\n *\n * Meta fields are stored in the `_meta_data` JSONB column on the shared STI\n * table rather than as dedicated table columns. Use this decorator for fields\n * that are specific to an STI child class and should not pollute the shared\n * table schema with child-specific columns.\n *\n * The `@smrt({ tableStrategy: 'sti' })` decorator must be set on the base class.\n * All child-specific fields should use `@meta()` (or the `Meta<T>` type alias).\n *\n * @param options - Standard field options (required, nullable, description, etc.)\n * @returns A TypeScript property decorator (registers field with `type: 'meta'`)\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = ''; // shared column on events table\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * @meta()\n * roomNumber: string = ''; // stored in _meta_data JSON, not a column\n *\n * @meta({ required: true })\n * durationMinutes: number = 60;\n * }\n * ```\n *\n * @see {@link Meta} for the equivalent type alias approach\n * @see {@link field} for regular (non-STI) field declarations\n */\nexport function meta(options: FieldOptions = {}) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'meta', // Mark this field as a meta field for STI\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatches.d.ts","sourceRoot":"","sources":["../../../src/dispatch/collections/Dispatches.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAqB,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACf,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Dispatches.d.ts","sourceRoot":"","sources":["../../../src/dispatch/collections/Dispatches.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAqB,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACf,MAAM,aAAa,CAAC;AAiErB;;GAEG;AACH,qBAAa,kBAAkB;IAC7B;;OAEG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC;IA0BhB;;OAEG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC;IAyBhB;;;;;;OAMG;WACU,GAAG,CACd,EAAE,EAAE,iBAAiB,EACrB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAS3B;;;;;;OAMG;WACU,SAAS,CACpB,EAAE,EAAE,iBAAiB,EACrB,EAAE,EAAE,MAAM,EACV,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAe3B;;OAEG;WACU,IAAI,CACf,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuFtB;;;;;;;;;;OAUG;WACU,WAAW,CACtB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,GAAE,MAAY,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAsCtB;;;;;;;;;;;;;;;;OAgBG;WACU,KAAK,CAChB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,QAAQ,EAClB,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,OAAO,CAAC;IA6BnB;;;;;;;OAOG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,oBAAyB,EAClC,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuCtB;;OAEG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC;IAOlB;;OAEG;WACU,MAAM,CAAC,EAAE,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrE;;;;;;;OAOG;WACU,OAAO,CAClB,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,sBAA2B,EACpC,WAAW,CAAC,EAAE,mBAAmB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IA4CjC;;OAEG;WACU,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;CAQlE"}
|
|
@@ -26,6 +26,23 @@ function pushTenantPredicate(conditions, params, scope, placeholder) {
|
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
+
* Count the rows a cleanup predicate selects, then delete them (#2375).
|
|
30
|
+
*
|
|
31
|
+
* Counting first gives a usable figure on every adapter — `rowCount` is not
|
|
32
|
+
* reliably populated across the engines SMRT supports — and is what lets
|
|
33
|
+
* `dryRun` preview the very same predicate the real cleanup runs. The two
|
|
34
|
+
* statements are not transactional, so the figure is approximate under
|
|
35
|
+
* concurrent writers.
|
|
36
|
+
*/
|
|
37
|
+
async function deleteOrCount(db, conditions, params, dryRun) {
|
|
38
|
+
const where = conditions.join(" AND ");
|
|
39
|
+
const countResult = await db.query(`SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`, ...params);
|
|
40
|
+
const total = Number(countResult.rows?.[0]?.total ?? 0);
|
|
41
|
+
if (!Number.isFinite(total) || total <= 0) return 0;
|
|
42
|
+
if (!dryRun) await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);
|
|
43
|
+
return total;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
29
46
|
* Storage operations for dispatches in _smrt_dispatch table
|
|
30
47
|
*/
|
|
31
48
|
var DispatchCollection = class {
|
|
@@ -260,8 +277,7 @@ var DispatchCollection = class {
|
|
|
260
277
|
const conditions = ["status = 'completed'", "processed_at < $1"];
|
|
261
278
|
const params = [cutoff.toISOString()];
|
|
262
279
|
pushTenantPredicate(conditions, params, tenantScope, "$2");
|
|
263
|
-
|
|
264
|
-
result.completedDeleted = rowCount || 0;
|
|
280
|
+
result.completedDeleted = await deleteOrCount(db, conditions, params, options.dryRun ?? false);
|
|
265
281
|
}
|
|
266
282
|
if (options.failedOlderThanDays) {
|
|
267
283
|
const cutoff = /* @__PURE__ */ new Date();
|
|
@@ -269,8 +285,7 @@ var DispatchCollection = class {
|
|
|
269
285
|
const conditions = ["status = 'failed'", "updated_at < $1"];
|
|
270
286
|
const params = [cutoff.toISOString()];
|
|
271
287
|
pushTenantPredicate(conditions, params, tenantScope, "$2");
|
|
272
|
-
|
|
273
|
-
result.failedDeleted = rowCount || 0;
|
|
288
|
+
result.failedDeleted = await deleteOrCount(db, conditions, params, options.dryRun ?? false);
|
|
274
289
|
}
|
|
275
290
|
return result;
|
|
276
291
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return (result as { count: number })?.count || 0;\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rowCount } = await db.query(\n `DELETE FROM _smrt_dispatch WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n result.completedDeleted = rowCount || 0;\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rowCount } = await db.query(\n `DELETE FROM _smrt_dispatch WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n result.failedDeleted = rowCount || 0;\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Count the rows a cleanup predicate selects, then delete them (#2375).\n *\n * Counting first gives a usable figure on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what lets\n * `dryRun` preview the very same predicate the real cleanup runs. The two\n * statements are not transactional, so the figure is approximate under\n * concurrent writers.\n */\nasync function deleteOrCount(\n db: DatabaseInterface,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = conditions.join(' AND ');\n const countResult = await db.query(\n `SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`,\n ...params,\n );\n const total = Number(countResult.rows?.[0]?.total ?? 0);\n if (!Number.isFinite(total) || total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);\n }\n\n return total;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return (result as { count: number })?.count || 0;\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.completedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.failedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/dispatch/types.d.ts
CHANGED
|
@@ -121,6 +121,11 @@ export interface DispatchCleanupOptions {
|
|
|
121
121
|
completedOlderThanDays?: number;
|
|
122
122
|
/** Delete failed dispatches older than this many days */
|
|
123
123
|
failedOlderThanDays?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Count the dispatches the cutoffs select without deleting them (#2375).
|
|
126
|
+
* The result reports what a real cleanup with the same options would remove.
|
|
127
|
+
*/
|
|
128
|
+
dryRun?: boolean;
|
|
124
129
|
}
|
|
125
130
|
/**
|
|
126
131
|
* Result of dispatch cleanup
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dispatch/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dispatch/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,SAAS,EAAE,IAAI,CAAC;IAChB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qGAAqG;IACrG,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uBAAuB;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,yDAAyD;IACzD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,EAAE,CAAC,EACC,iBAAiB,GACjB;QACE,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC;QAChD,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACN,qDAAqD;IACrD,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;QAChC,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;CACH"}
|