@happyvertical/smrt-core 0.38.2 → 0.38.3
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 +11 -2
- package/dist/change-feed.d.ts +19 -10
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +43 -13
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts +83 -0
- package/dist/change-signals.d.ts.map +1 -0
- package/dist/change-signals.js +272 -0
- package/dist/change-signals.js.map +1 -0
- package/dist/collection-cache.d.ts +21 -0
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts +15 -27
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +17 -7
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/cli.d.ts +3 -0
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +7 -4
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/conditional-get.d.ts +17 -6
- package/dist/generators/conditional-get.d.ts.map +1 -1
- package/dist/generators/conditional-get.js +19 -13
- package/dist/generators/conditional-get.js.map +1 -1
- package/dist/generators/events-route.d.ts +72 -0
- package/dist/generators/events-route.d.ts.map +1 -0
- package/dist/generators/events-route.js +234 -0
- package/dist/generators/events-route.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +11 -5
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +3 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +18 -9
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +7 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +77 -13
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +7 -3
- 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/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1050 -124
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +9 -3
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
- package/dist/mcp-advisor/types.d.ts +3 -0
- package/dist/mcp-advisor/types.d.ts.map +1 -1
- package/dist/migrations/tracker.js +5 -4
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +1 -1
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/object.d.ts +17 -8
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +42 -26
- package/dist/object.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +12 -0
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/shared-state.d.ts +2 -1
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js.map +1 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +7 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +1 -1
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
- package/dist/signals/sanitizer.d.ts.map +1 -1
- package/dist/signals/sanitizer.js.map +1 -1
- package/dist/smrt-knowledge.json +8 -8
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/notifications-stub.d.ts +24 -0
- package/dist/testing/notifications-stub.d.ts.map +1 -0
- package/dist/testing/notifications-stub.js +62 -0
- package/dist/testing/notifications-stub.js.map +1 -0
- package/dist/testing.js +2 -1
- package/dist/tools/tool-generator.js +1 -1
- package/dist/tools/tool-generator.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts +14 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +2 -2
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts +9 -0
- package/dist/vite-plugin/events-route.d.ts.map +1 -0
- package/dist/vite-plugin/events-route.js +155 -0
- package/dist/vite-plugin/events-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +8 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +8 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +193 -31
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +12 -12
|
@@ -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 { SQLDataType } from '../schema/types.js';\nimport {\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n} from './compatibility.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 — giving WHERE clauses on that\n * meta key the same performance as a real column.\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 /** Field description */\n description?: string;\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 * 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 (or class name string)\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\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 *\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: relatedClassName,\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: relatedClassName,\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: relatedClassName,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyNA,SAAgB,MACd,UAAiE,CAAC,GAClE;CACA,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa,OAAO;EACvE,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,WACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;GACX,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,gBACd,eACA,UAAkC,CAAC,GACnC;CACA,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;GACX,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,UACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;IACT,WAAW;GACb,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,WACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;IACT,WAAW;GACb,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,KAAK,UAAwB,CAAC,GAAG;CAC/C,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;GACR,CAAC;EACH,CACF;CACF;AACF"}
|
|
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 { SQLDataType } from '../schema/types.js';\nimport {\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n} from './compatibility.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 — giving WHERE clauses on that\n * meta key the same performance as a real column.\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 * 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 * 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 (or class name string)\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\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 *\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: relatedClassName,\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: relatedClassName,\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 const relatedClassName =\n typeof relatedClass === 'string' ? relatedClass : relatedClass.name;\n\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: relatedClassName,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiOA,SAAgB,MACd,UAAiE,CAAC,GAClE;CACA,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa,OAAO;EACvE,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,WACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;GACX,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,gBACd,eACA,UAAkC,CAAC,GACnC;CACA,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;GACX,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,UACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;IACT,WAAW;GACb,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,WACd,cACA,UAAqD,CAAC,GACtD;CACA,SACE,eACA,yBACG;EACH,MAAM,mBACJ,OAAO,iBAAiB,WAAW,eAAe,aAAa;EAEjE,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;IACN,SAAS;IACT,WAAW;GACb,CAAC;EACH,CACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,KAAK,UAAwB,CAAC,GAAG;CAC/C,SACE,eACA,yBACG;EACH,iCACE,eACA,uBACC,WAAW,gBAAgB;GAC1B,eAAe,uBAAuB,WAAW,aAAa;IAC5D,GAAG;IACH,MAAM;GACR,CAAC;EACH,CACF;CACF;AACF"}
|
package/dist/errors.js
CHANGED
|
@@ -264,7 +264,8 @@ var ValidationError = class ValidationError extends SmrtError {
|
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
266
|
static rangeError(fieldName, value, min, max) {
|
|
267
|
-
|
|
267
|
+
const range = min !== void 0 && max !== void 0 ? `between ${min} and ${max}` : min !== void 0 ? `>= ${min}` : `<= ${max}`;
|
|
268
|
+
return new ValidationError(`Value for field '${fieldName}' must be ${range}, got: ${value}`, "VALIDATION_RANGE_ERROR", {
|
|
268
269
|
fieldName,
|
|
269
270
|
value,
|
|
270
271
|
min,
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Comprehensive error handling system for SMRT framework\n *\n * Provides specialized error types for different failure scenarios\n * with proper error codes, messages, and debugging information.\n */\n\n/**\n * Abstract base class for all SMRT framework errors.\n *\n * Adds a structured `code` (machine-readable string constant), a `category`\n * (coarse error domain), optional structured `details`, and an optional\n * causal `Error` chain on top of the standard `Error` class.\n *\n * Never throw `SmrtError` directly — use one of the concrete subclasses\n * (`DatabaseError`, `AIError`, `ValidationError`, etc.) or their static\n * factory methods for consistent error codes and messages.\n *\n * @example\n * ```typescript\n * try {\n * await product.save();\n * } catch (err) {\n * if (err instanceof ValidationError) {\n * console.error(err.code, err.details); // 'VALIDATION_REQUIRED_FIELD', { fieldName, objectType }\n * }\n * if (err instanceof SmrtError) {\n * logger.error(ErrorUtils.sanitizeError(err));\n * }\n * }\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\n\nconst logger = createLogger({ level: 'info' });\n\nexport abstract class SmrtError extends Error {\n public readonly code: string;\n public readonly category:\n | 'database'\n | 'ai'\n | 'filesystem'\n | 'validation'\n | 'network'\n | 'configuration'\n | 'runtime';\n public readonly details?: Record<string, unknown>;\n public readonly cause?: Error;\n\n constructor(\n message: string,\n code: string,\n category: SmrtError['category'],\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message);\n this.name = this.constructor.name;\n this.code = code;\n this.category = category;\n this.details = details;\n this.cause = cause;\n\n // Maintain proper stack trace for V8\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n\n /**\n * Converts error to a serializable object for logging/debugging\n */\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n category: this.category,\n details: this.details,\n stack: this.stack,\n cause: this.cause\n ? {\n name: this.cause.name,\n message: this.cause.message,\n stack: this.cause.stack,\n }\n : undefined,\n };\n }\n}\n\ntype ErrorLikeWithContext = Error & {\n cause?: unknown;\n context?: {\n originalError?: unknown;\n };\n};\n\nfunction collectErrorMessages(\n value: unknown,\n messages: string[],\n visited: Set<unknown>,\n depth = 0,\n): void {\n if (!value || visited.has(value) || depth > 10) {\n return;\n }\n\n visited.add(value);\n\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (trimmed) {\n messages.push(trimmed);\n }\n return;\n }\n\n if (!(value instanceof Error)) {\n return;\n }\n\n const message = value.message?.trim();\n if (message) {\n messages.push(message);\n }\n\n const errorWithContext = value as ErrorLikeWithContext;\n collectErrorMessages(\n errorWithContext.context?.originalError,\n messages,\n visited,\n depth + 1,\n );\n collectErrorMessages(errorWithContext.cause, messages, visited, depth + 1);\n}\n\nfunction getPrimaryCauseMessage(cause?: Error): {\n message?: string;\n messages?: string[];\n} {\n if (!cause) {\n return {};\n }\n\n const collected: string[] = [];\n collectErrorMessages(cause, collected, new Set<unknown>());\n\n const uniqueMessages = [...new Set(collected.filter(Boolean))];\n if (uniqueMessages.length === 0) {\n return {};\n }\n\n return {\n message: uniqueMessages[uniqueMessages.length - 1],\n messages: uniqueMessages,\n };\n}\n\n/**\n * Errors originating from database operations.\n *\n * Use the static factory methods rather than the constructor directly:\n * - `DatabaseError.connectionFailed(url, cause)` — DB connection failure\n * - `DatabaseError.queryFailed(query, cause)` — SQL execution error\n * - `DatabaseError.schemaError(table, op, cause)` — DDL/migration error\n * - `DatabaseError.constraintViolation(constraint, value, cause)` — FK/CHECK/UNIQUE\n * - `DatabaseError.corruptedData(field, class, cause)` — unparse-able column data\n * - `DatabaseError.missingDiscriminator(class, rowId)` — STI row missing `_meta_type`\n * - `DatabaseError.stiDiscriminatorConflict(...)` — legacy STI discriminator collides during qualification\n * - `DatabaseError.schemaMissing(table, class)` — table not yet migrated\n *\n * All errors have `category: 'database'` and codes prefixed with `DB_`.\n */\nexport class DatabaseError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'database', details, cause);\n }\n\n static connectionFailed(dbUrl: string, cause?: Error): DatabaseError {\n return new DatabaseError(\n `Failed to connect to database: ${dbUrl}`,\n 'DB_CONNECTION_FAILED',\n { dbUrl },\n cause,\n );\n }\n\n static queryFailed(query: string, cause?: Error): DatabaseError {\n // Include the deepest actionable cause message for better debugging.\n const causeInfo = getPrimaryCauseMessage(cause);\n const causeMsg = causeInfo.message ? `\\nCause: ${causeInfo.message}` : '';\n return new DatabaseError(\n `Database query failed: ${query.substring(0, 100)}${query.length > 100 ? '...' : ''}${causeMsg}`,\n 'DB_QUERY_FAILED',\n {\n query,\n causeMessage: causeInfo.message,\n causeMessages: causeInfo.messages,\n },\n cause,\n );\n }\n\n static schemaError(\n tableName: string,\n operation: string,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Schema operation failed for table '${tableName}': ${operation}`,\n 'DB_SCHEMA_ERROR',\n { tableName, operation },\n cause,\n );\n }\n\n static constraintViolation(\n constraint: string,\n value: unknown,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Database constraint violation: ${constraint}`,\n 'DB_CONSTRAINT_VIOLATION',\n { constraint, value },\n cause,\n );\n }\n\n static corruptedData(\n fieldName: string,\n className: string,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Corrupted data in field '${fieldName}' for ${className}. ` +\n `The data cannot be parsed or is malformed. ` +\n `This may indicate database corruption or incompatible schema changes.`,\n 'DB_CORRUPTED_DATA',\n { fieldName, className },\n cause,\n );\n }\n\n static missingDiscriminator(\n className: string,\n rowId?: string,\n ): DatabaseError {\n return new DatabaseError(\n `Missing discriminator (_meta_type) for STI class ${className}${rowId ? ` (row id: ${rowId})` : ''}. ` +\n `STI classes require a discriminator column to determine the correct subclass. ` +\n `This may indicate a schema mismatch or manual database modification.`,\n 'DB_MISSING_DISCRIMINATOR',\n { className, rowId },\n );\n }\n\n static stiDiscriminatorConflict(details: {\n className: string;\n tableName: string;\n id: string;\n slug: string;\n context: string;\n conflictIdentity: Record<string, unknown>;\n legacyMetaType: string;\n qualifiedMetaType: string;\n duplicateId: string;\n }): DatabaseError {\n const identityText = Object.entries(details.conflictIdentity)\n .map(([key, value]) => `${key} '${String(value)}'`)\n .join(', ');\n\n return new DatabaseError(\n `Legacy STI discriminator collision for ${details.className} (${details.tableName}). ` +\n `Row '${details.id}' would upgrade _meta_type from '${details.legacyMetaType}' to '${details.qualifiedMetaType}', ` +\n `but row '${details.duplicateId}' already uses the qualified discriminator for ${identityText || 'the same conflict identity'}. ` +\n `Merge or remove the duplicate legacy/qualified rows before saving.`,\n 'DB_STI_DISCRIMINATOR_CONFLICT',\n details,\n );\n }\n\n static schemaMissing(tableName: string, className: string): DatabaseError {\n return new DatabaseError(\n `Table '${tableName}' does not exist for class '${className}'. ` +\n `Run 'smrt db:migrate' to create database schema.`,\n 'DB_SCHEMA_MISSING',\n { tableName, className },\n );\n }\n}\n\n/**\n * Errors from AI provider integrations.\n *\n * Use the static factory methods:\n * - `AIError.providerError(provider, operation, cause)` — generic provider failure\n * - `AIError.rateLimitExceeded(provider, retryAfter)` — rate limit hit\n * - `AIError.invalidResponse(provider, response)` — unexpected response shape\n * - `AIError.authenticationFailed(provider)` — bad API key / credentials\n *\n * All errors have `category: 'ai'` and codes prefixed with `AI_`.\n * AI errors are considered retryable by `ErrorUtils.isRetryable()`.\n */\nexport class AIError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'ai', details, cause);\n }\n\n static providerError(\n provider: string,\n operation: string,\n cause?: Error,\n ): AIError {\n return new AIError(\n `AI provider '${provider}' failed during ${operation}`,\n 'AI_PROVIDER_ERROR',\n { provider, operation },\n cause,\n );\n }\n\n static rateLimitExceeded(provider: string, retryAfter?: number): AIError {\n return new AIError(\n `AI provider '${provider}' rate limit exceeded`,\n 'AI_RATE_LIMIT',\n { provider, retryAfter },\n );\n }\n\n static invalidResponse(provider: string, response: unknown): AIError {\n return new AIError(\n `AI provider '${provider}' returned invalid response`,\n 'AI_INVALID_RESPONSE',\n { provider, response },\n );\n }\n\n static authenticationFailed(provider: string): AIError {\n return new AIError(\n `AI provider '${provider}' authentication failed`,\n 'AI_AUTH_FAILED',\n { provider },\n );\n }\n}\n\n/**\n * Errors from filesystem operations.\n *\n * Use the static factory methods:\n * - `FilesystemError.fileNotFound(path)` — file does not exist\n * - `FilesystemError.permissionDenied(path, operation)` — access denied\n * - `FilesystemError.diskSpaceExceeded(path, requiredBytes)` — insufficient space\n *\n * All errors have `category: 'filesystem'` and codes prefixed with `FS_`.\n */\nexport class FilesystemError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'filesystem', details, cause);\n }\n\n static fileNotFound(path: string): FilesystemError {\n return new FilesystemError(`File not found: ${path}`, 'FS_FILE_NOT_FOUND', {\n path,\n });\n }\n\n static permissionDenied(path: string, operation: string): FilesystemError {\n return new FilesystemError(\n `Permission denied for ${operation} on: ${path}`,\n 'FS_PERMISSION_DENIED',\n { path, operation },\n );\n }\n\n static diskSpaceExceeded(\n path: string,\n requiredBytes: number,\n ): FilesystemError {\n return new FilesystemError(\n `Insufficient disk space for operation on: ${path}`,\n 'FS_DISK_SPACE_EXCEEDED',\n { path, requiredBytes },\n );\n }\n}\n\n/**\n * Input/data validation errors thrown before or during a database operation.\n *\n * `save()` throws `ValidationError` when field validation fails. The collection's\n * `convertWhereKeys()` throws it for invalid WHERE clause operators or field names.\n * `ValidationError` is **not** retried by `ErrorUtils.withRetry()`.\n *\n * Use the static factory methods:\n * - `ValidationError.requiredField(field, objectType)` — missing required field\n * - `ValidationError.invalidValue(field, value, expected)` — wrong type/format\n * - `ValidationError.uniqueConstraint(field, value)` — duplicate unique value\n * - `ValidationError.rangeError(field, value, min?, max?)` — out of allowed range\n *\n * All errors have `category: 'validation'` and codes prefixed with `VALIDATION_`.\n */\nexport class ValidationError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'validation', details, cause);\n }\n\n static requiredField(fieldName: string, objectType: string): ValidationError {\n return new ValidationError(\n `Required field '${fieldName}' is missing for ${objectType}`,\n 'VALIDATION_REQUIRED_FIELD',\n { fieldName, objectType },\n );\n }\n\n static invalidValue(\n fieldName: string,\n value: unknown,\n expectedType: string,\n ): ValidationError {\n return new ValidationError(\n `Invalid value for field '${fieldName}': expected ${expectedType}, got ${typeof value}`,\n 'VALIDATION_INVALID_VALUE',\n { fieldName, value, expectedType },\n );\n }\n\n static uniqueConstraint(fieldName: string, value: unknown): ValidationError {\n return new ValidationError(\n `Unique constraint violation for field '${fieldName}' with value: ${String(value)}`,\n 'VALIDATION_UNIQUE_CONSTRAINT',\n { fieldName, value },\n );\n }\n\n static rangeError(\n fieldName: string,\n value: number,\n min?: number,\n max?: number,\n ): ValidationError {\n const range =\n min !== undefined && max !== undefined\n ? `between ${min} and ${max}`\n : min !== undefined\n ? `>= ${min}`\n : `<= ${max}`;\n\n return new ValidationError(\n `Value for field '${fieldName}' must be ${range}, got: ${value}`,\n 'VALIDATION_RANGE_ERROR',\n { fieldName, value, min, max },\n );\n }\n}\n\n/**\n * Errors from HTTP and external network operations.\n *\n * Use the static factory methods:\n * - `NetworkError.requestFailed(url, status?, body?)` — non-2xx response or connection failure\n * - `NetworkError.timeout(url, timeoutMs)` — request exceeded timeout\n * - `NetworkError.serviceUnavailable(service, reason?)` — external service down\n *\n * All errors have `category: 'network'` and codes prefixed with `NETWORK_`.\n * Network errors are considered retryable by `ErrorUtils.isRetryable()`.\n */\nexport class NetworkError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'network', details, cause);\n }\n\n static requestFailed(\n url: string,\n status?: number,\n responseBody?: string | Error,\n ): NetworkError {\n const cause = responseBody instanceof Error ? responseBody : undefined;\n const body = typeof responseBody === 'string' ? responseBody : undefined;\n return new NetworkError(\n `Network request failed: ${url}${status ? ` (Status: ${status})` : ''}${body ? ` - ${body.substring(0, 200)}` : ''}`,\n 'NETWORK_REQUEST_FAILED',\n { url, status, responseBody: body },\n cause,\n );\n }\n\n static timeout(url: string, timeoutMs: number): NetworkError {\n return new NetworkError(\n `Network request timed out after ${timeoutMs}ms: ${url}`,\n 'NETWORK_TIMEOUT',\n { url, timeoutMs },\n );\n }\n\n static serviceUnavailable(service: string, reason?: string): NetworkError {\n return new NetworkError(\n reason\n ? `External service unavailable: ${service} - ${reason}`\n : `External service unavailable: ${service}`,\n 'NETWORK_SERVICE_UNAVAILABLE',\n { service, reason },\n );\n }\n}\n\n/**\n * Errors from misconfigured or incompatible class/framework setup.\n *\n * These are typically thrown during class registration (i.e. at module load time),\n * not during normal request handling.\n *\n * Use the static factory methods:\n * - `ConfigurationError.missingConfiguration(key, context?)` — missing required config\n * - `ConfigurationError.invalidConfiguration(key, value, expected)` — wrong config type/value\n * - `ConfigurationError.initializationFailed(component, cause?)` — component failed to start\n * - `ConfigurationError.circularInheritance(class, chain)` — circular class inheritance\n * - `ConfigurationError.incompatibleStrategy(class, strategy, parent, parentStrategy)` — STI mismatch\n * - `ConfigurationError.unregisteredBaseClass(child, base)` — STI base not yet registered\n *\n * All errors have `category: 'configuration'` and codes prefixed with `CONFIG_`.\n * Configuration errors are **not** retried by `ErrorUtils.withRetry()`.\n */\nexport class ConfigurationError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'configuration', details, cause);\n }\n\n static missingConfiguration(\n configKey: string,\n context?: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Missing required configuration: ${configKey}${context ? ` in ${context}` : ''}`,\n 'CONFIG_MISSING',\n { configKey, context },\n );\n }\n\n static invalidConfiguration(\n configKey: string,\n value: unknown,\n expected: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Invalid configuration for ${configKey}: expected ${expected}, got ${typeof value}`,\n 'CONFIG_INVALID',\n { configKey, value, expected },\n );\n }\n\n static initializationFailed(\n component: string,\n cause?: Error,\n ): ConfigurationError {\n return new ConfigurationError(\n `Failed to initialize component: ${component}`,\n 'CONFIG_INIT_FAILED',\n { component },\n cause,\n );\n }\n\n static circularInheritance(\n className: string,\n inheritanceChain: string[],\n ): ConfigurationError {\n return new ConfigurationError(\n `Circular inheritance detected for class '${className}'. ` +\n `Inheritance chain: ${inheritanceChain.join(' → ')} → ${className}. ` +\n `Classes cannot inherit from themselves directly or indirectly.`,\n 'CONFIG_CIRCULAR_INHERITANCE',\n { className, inheritanceChain },\n );\n }\n\n static incompatibleStrategy(\n className: string,\n classStrategy: string,\n parentClass: string,\n parentStrategy: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Incompatible table strategy for class '${className}' (${classStrategy}). ` +\n `Parent class '${parentClass}' uses ${parentStrategy} strategy. ` +\n `Child classes must use the same table strategy as their parent. ` +\n `Either change ${className} to use ${parentStrategy}, or remove the inheritance.`,\n 'CONFIG_INCOMPATIBLE_STRATEGY',\n { className, classStrategy, parentClass, parentStrategy },\n );\n }\n\n static unregisteredBaseClass(\n childClass: string,\n baseClass: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `STI base class '${baseClass}' is not registered for child class '${childClass}'. ` +\n `When using Single Table Inheritance, the base class must be registered before any child classes. ` +\n `Ensure ${baseClass} is decorated with @smrt({ tableStrategy: 'sti' }) and imported before ${childClass}.`,\n 'CONFIG_UNREGISTERED_BASE',\n { childClass, baseClass },\n );\n }\n}\n\n/**\n * Errors representing unexpected runtime failures not covered by other categories.\n *\n * `RuntimeError` is the catch-all for internal framework errors — invalid object\n * state, exhausted resources, or failures in operations like `save()` and `loadFromId()`\n * that propagate from an unknown cause.\n *\n * Use the static factory methods:\n * - `RuntimeError.operationFailed(operation, context?, cause?)` — generic operation failure\n * - `RuntimeError.invalidState(message, context?)` — unexpected object/system state\n * - `RuntimeError.resourceExhausted(resource, limit)` — limit exceeded (e.g. connections)\n *\n * All errors have `category: 'runtime'` and codes prefixed with `RUNTIME_`.\n */\nexport class RuntimeError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'runtime', details, cause);\n }\n\n static operationFailed(\n operation: string,\n context?: string,\n cause?: Error,\n ): RuntimeError {\n return new RuntimeError(\n `Operation failed: ${operation}${context ? ` in ${context}` : ''}`,\n 'RUNTIME_OPERATION_FAILED',\n { operation, context },\n cause,\n );\n }\n\n static invalidState(\n message: string,\n context?: Record<string, unknown>,\n ): RuntimeError {\n return new RuntimeError(message, 'RUNTIME_INVALID_STATE', context);\n }\n\n static resourceExhausted(resource: string, limit: number): RuntimeError {\n return new RuntimeError(\n `Resource exhausted: ${resource} exceeded limit of ${limit}`,\n 'RUNTIME_RESOURCE_EXHAUSTED',\n { resource, limit },\n );\n }\n}\n\n/**\n * Error thrown when a tenant isolation boundary is crossed while resolving a\n * relationship.\n *\n * Raised by {@link SmrtObject.loadRelated} / {@link SmrtObject.loadRelatedMany}\n * (and {@link SmrtObject.getRelated}, which delegates to them) when a\n * tenant-scoped object resolves a relationship to an object belonging to a\n * *different*, non-null tenant — the genuine cross-tenant data leak. The guard\n * is a no-op when either side has a `null` tenant (global / non-tenant-scoped\n * models) and when both sides share the same tenant, so it only fires on real\n * leaks. Pass `{ allowCrossTenant: true }` to the loader to deliberately opt out.\n *\n * The `code` is always `'TENANT_ISOLATION_VIOLATION'` and the category is\n * `'validation'`. It is never retried — `ErrorUtils.withRetry()` rethrows it\n * immediately and `ErrorUtils.isRetryable()` returns `false` — because a tenant\n * boundary violation is deterministic. `tenantId` is the owning object's tenant\n * and `attemptedTenantId` is the tenant of the object that was reached.\n *\n * This shares its stable `code`, `name`, `tenantId`, and `attemptedTenantId`\n * shape with the interceptor-level `TenantIsolationError` in\n * `@happyvertical/smrt-tenancy`, so cross-cutting handlers can match either via\n * `err.code === 'TENANT_ISOLATION_VIOLATION'`. They are intentionally distinct\n * classes because `@happyvertical/smrt-core` cannot depend on the tenancy\n * package (the dependency runs the other way).\n *\n * @example\n * ```typescript\n * try {\n * await order.loadRelated('customerId');\n * } catch (err) {\n * if (err instanceof TenantIsolationError) {\n * // err.tenantId — the order's tenant\n * // err.attemptedTenantId — the customer's tenant\n * }\n * }\n * ```\n *\n * @see SmrtObject.loadRelated\n * @see SmrtObject.loadRelatedMany\n */\nexport class TenantIsolationError extends SmrtError {\n /** The tenant ID of the object that owns the relationship. */\n public readonly tenantId?: string;\n /** The tenant ID of the related object that was reached (and rejected). */\n public readonly attemptedTenantId?: string;\n\n constructor(\n message: string,\n details?: {\n tenantId?: string;\n attemptedTenantId?: string;\n [key: string]: unknown;\n },\n cause?: Error,\n ) {\n super(message, 'TENANT_ISOLATION_VIOLATION', 'validation', details, cause);\n this.tenantId = details?.tenantId;\n this.attemptedTenantId = details?.attemptedTenantId;\n }\n\n /**\n * Builds a {@link TenantIsolationError} for a blocked cross-tenant\n * relationship resolution, with a descriptive message and structured details.\n */\n static crossTenantReference(details: {\n sourceClass: string;\n fieldName: string;\n sourceTenantId: string;\n targetClass?: string;\n targetTenantId: string;\n }): TenantIsolationError {\n const target = details.targetClass\n ? `${details.targetClass} (tenant '${details.targetTenantId}')`\n : `tenant '${details.targetTenantId}'`;\n return new TenantIsolationError(\n `Cross-tenant relationship access blocked on ${details.sourceClass}.${details.fieldName}: ` +\n `owning tenant '${details.sourceTenantId}' does not match ${target}. ` +\n `Pass { allowCrossTenant: true } to loadRelated()/loadRelatedMany()/getRelated() to override.`,\n {\n tenantId: details.sourceTenantId,\n attemptedTenantId: details.targetTenantId,\n sourceClass: details.sourceClass,\n fieldName: details.fieldName,\n targetClass: details.targetClass,\n },\n );\n }\n}\n\n/**\n * Utility functions for error handling\n */\nexport class ErrorUtils {\n /**\n * Wraps a function with error handling and automatic retry logic\n */\n static async withRetry<T>(\n operation: () => Promise<T>,\n maxRetries = 3,\n delay = 1000,\n backoffMultiplier = 2,\n ): Promise<T> {\n let lastError: Error = new Error('Operation failed without error details');\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await operation();\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n if (attempt === maxRetries) {\n throw lastError;\n }\n\n // Skip retry for certain error types. A tenant isolation violation is\n // deterministic — retrying re-fetches the same cross-tenant target — and\n // is a security boundary, so it must never be retried.\n if (\n error instanceof ValidationError ||\n error instanceof ConfigurationError ||\n error instanceof TenantIsolationError\n ) {\n throw error;\n }\n\n // Wait before retrying with exponential backoff\n // Wrap in try-catch to handle any potential timer errors\n try {\n await new Promise<void>((resolve) => {\n setTimeout(() => resolve(), delay * backoffMultiplier ** attempt);\n });\n } catch (timerError) {\n // Log timer error but don't fail the retry\n logger.error('Timer error during retry', { error: timerError });\n }\n }\n }\n\n throw lastError;\n }\n\n /**\n * Checks if an error is retryable\n */\n static isRetryable(error: Error): boolean {\n if (error instanceof SmrtError) {\n return error.category === 'network' || error.category === 'ai';\n }\n\n // Check for common retryable error patterns\n const retryablePatterns = [\n /ECONNRESET/,\n /ETIMEDOUT/,\n /ENOTFOUND/,\n /rate.?limit/i,\n /timeout/i,\n /503/,\n /502/,\n /500/,\n ];\n\n return retryablePatterns.some((pattern) => pattern.test(error.message));\n }\n\n /**\n * Sanitizes an error for safe logging (removes sensitive information)\n */\n static sanitizeError(error: Error): Record<string, unknown> {\n const sanitized: Record<string, unknown> = {\n name: error.name,\n message: error.message,\n stack: error.stack,\n };\n\n if (error instanceof SmrtError) {\n sanitized.code = error.code;\n sanitized.category = error.category;\n\n // Sanitize details to remove potential sensitive information\n if (error.details) {\n const details: Record<string, unknown> = { ...error.details };\n sanitized.details = details;\n\n // Remove common sensitive fields\n const sensitiveFields = [\n 'password',\n 'token',\n 'key',\n 'secret',\n 'apiKey',\n ];\n for (const field of sensitiveFields) {\n if (details[field]) {\n details[field] = '[REDACTED]';\n }\n }\n }\n }\n\n return sanitized;\n }\n}\n\n/**\n * Validation report that collects multiple validation errors\n *\n * Useful for validating an entire object and reporting all errors\n * at once rather than stopping at the first error.\n *\n * @example\n * ```typescript\n * const report = new ValidationReport('Product');\n * report.addError(ValidationError.requiredField('name', 'Product'));\n * report.addError(ValidationError.rangeError('price', -10, 0));\n *\n * if (report.hasErrors()) {\n * console.error(report.toString());\n * // Output:\n * // Validation failed for Product with 2 errors:\n * // - name: Required field 'name' is missing for Product\n * // - price: Value -10 for field 'price' is outside allowed range [0, undefined]\n * }\n * ```\n */\nexport class ValidationReport {\n private errors: ValidationError[] = [];\n private objectType: string;\n\n constructor(objectType: string) {\n this.objectType = objectType;\n }\n\n /**\n * Add a validation error to the report\n */\n addError(error: ValidationError): void {\n this.errors.push(error);\n }\n\n /**\n * Check if there are any validation errors\n */\n hasErrors(): boolean {\n return this.errors.length > 0;\n }\n\n /**\n * Get all validation errors\n */\n getErrors(): ValidationError[] {\n return [...this.errors];\n }\n\n /**\n * Get the number of validation errors\n */\n getErrorCount(): number {\n return this.errors.length;\n }\n\n /**\n * Convert to a human-readable string\n */\n toString(): string {\n if (this.errors.length === 0) {\n return `Validation passed for ${this.objectType}`;\n }\n\n const errorList = this.errors\n .map((err, idx) => ` ${idx + 1}. ${err.message}`)\n .join('\\n');\n\n return `Validation failed for ${this.objectType} with ${this.errors.length} error(s):\\n${errorList}`;\n }\n\n /**\n * Convert to JSON format\n */\n toJSON(): object {\n return {\n objectType: this.objectType,\n errorCount: this.errors.length,\n errors: this.errors.map((err) => err.toJSON()),\n };\n }\n\n /**\n * Throw the first error if there are any errors\n */\n throwIfErrors(): void {\n if (this.errors.length > 0) {\n throw this.errors[0];\n }\n }\n\n /**\n * Clear all errors\n */\n clear(): void {\n this.errors = [];\n }\n}\n\n/**\n * Validation utility functions\n */\nexport class ValidationUtils {\n /**\n * Validate a single field value\n *\n * @param fieldName - Name of the field\n * @param value - Value to validate\n * @param options - Validation options (required, min, max, etc.)\n * @returns ValidationError if validation fails, null otherwise\n */\n static async validateField(\n fieldName: string,\n value: unknown,\n options: {\n required?: boolean;\n min?: number;\n max?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string | RegExp;\n type?: string;\n customValidator?: (value: unknown) => boolean | Promise<boolean>;\n customMessage?: string;\n },\n objectType: string = 'Object',\n ): Promise<ValidationError | null> {\n // Required check\n if (\n options.required &&\n (value === null || value === undefined || value === '')\n ) {\n return ValidationError.requiredField(fieldName, objectType);\n }\n\n // Skip further validation if value is null/undefined and not required\n if (value === null || value === undefined) {\n return null;\n }\n\n // Numeric range validation\n if (typeof value === 'number') {\n if (options.min !== undefined && value < options.min) {\n return ValidationError.rangeError(\n fieldName,\n value,\n options.min,\n options.max,\n );\n }\n if (options.max !== undefined && value > options.max) {\n return ValidationError.rangeError(\n fieldName,\n value,\n options.min,\n options.max,\n );\n }\n }\n\n // String length validation\n if (typeof value === 'string') {\n if (options.minLength !== undefined && value.length < options.minLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with minimum length ${options.minLength}`,\n );\n }\n if (options.maxLength !== undefined && value.length > options.maxLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with maximum length ${options.maxLength}`,\n );\n }\n\n // Pattern validation\n if (options.pattern) {\n const regex =\n typeof options.pattern === 'string'\n ? new RegExp(options.pattern)\n : options.pattern;\n if (!regex.test(value)) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string matching pattern ${options.pattern}`,\n );\n }\n }\n }\n\n // Custom validator\n if (options.customValidator) {\n try {\n const isValid = await options.customValidator(value);\n if (!isValid) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n options.customMessage || 'custom validation failed',\n );\n }\n } catch (error) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `custom validation error: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return null;\n }\n\n /**\n * Validate required field\n */\n static validateRequired(\n fieldName: string,\n value: unknown,\n objectType: string = 'Object',\n ): ValidationError | null {\n if (value === null || value === undefined || value === '') {\n return ValidationError.requiredField(fieldName, objectType);\n }\n return null;\n }\n\n /**\n * Validate numeric range\n */\n static validateRange(\n fieldName: string,\n value: number,\n min?: number,\n max?: number,\n ): ValidationError | null {\n if (min !== undefined && value < min) {\n return ValidationError.rangeError(fieldName, value, min, max);\n }\n if (max !== undefined && value > max) {\n return ValidationError.rangeError(fieldName, value, min, max);\n }\n return null;\n }\n\n /**\n * Validate string length\n */\n static validateLength(\n fieldName: string,\n value: string,\n minLength?: number,\n maxLength?: number,\n ): ValidationError | null {\n if (minLength !== undefined && value.length < minLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with minimum length ${minLength}`,\n );\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with maximum length ${maxLength}`,\n );\n }\n return null;\n }\n\n /**\n * Validate string pattern\n */\n static validatePattern(\n fieldName: string,\n value: string,\n pattern: string | RegExp,\n ): ValidationError | null {\n const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;\n if (!regex.test(value)) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string matching pattern ${pattern}`,\n );\n }\n return null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Comprehensive error handling system for SMRT framework\n *\n * Provides specialized error types for different failure scenarios\n * with proper error codes, messages, and debugging information.\n */\n\n/**\n * Abstract base class for all SMRT framework errors.\n *\n * Adds a structured `code` (machine-readable string constant), a `category`\n * (coarse error domain), optional structured `details`, and an optional\n * causal `Error` chain on top of the standard `Error` class.\n *\n * Never throw `SmrtError` directly — use one of the concrete subclasses\n * (`DatabaseError`, `AIError`, `ValidationError`, etc.) or their static\n * factory methods for consistent error codes and messages.\n *\n * @example\n * ```typescript\n * try {\n * await product.save();\n * } catch (err) {\n * if (err instanceof ValidationError) {\n * console.error(err.code, err.details); // 'VALIDATION_REQUIRED_FIELD', { fieldName, objectType }\n * }\n * if (err instanceof SmrtError) {\n * logger.error(ErrorUtils.sanitizeError(err));\n * }\n * }\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\n\nconst logger = createLogger({ level: 'info' });\n\nexport abstract class SmrtError extends Error {\n public readonly code: string;\n public readonly category:\n | 'database'\n | 'ai'\n | 'filesystem'\n | 'validation'\n | 'network'\n | 'configuration'\n | 'runtime';\n public readonly details?: Record<string, unknown>;\n public readonly cause?: Error;\n\n constructor(\n message: string,\n code: string,\n category: SmrtError['category'],\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message);\n this.name = this.constructor.name;\n this.code = code;\n this.category = category;\n this.details = details;\n this.cause = cause;\n\n // Maintain proper stack trace for V8\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n\n /**\n * Converts error to a serializable object for logging/debugging\n */\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n category: this.category,\n details: this.details,\n stack: this.stack,\n cause: this.cause\n ? {\n name: this.cause.name,\n message: this.cause.message,\n stack: this.cause.stack,\n }\n : undefined,\n };\n }\n}\n\ntype ErrorLikeWithContext = Error & {\n cause?: unknown;\n context?: {\n originalError?: unknown;\n };\n};\n\nfunction collectErrorMessages(\n value: unknown,\n messages: string[],\n visited: Set<unknown>,\n depth = 0,\n): void {\n if (!value || visited.has(value) || depth > 10) {\n return;\n }\n\n visited.add(value);\n\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (trimmed) {\n messages.push(trimmed);\n }\n return;\n }\n\n if (!(value instanceof Error)) {\n return;\n }\n\n const message = value.message?.trim();\n if (message) {\n messages.push(message);\n }\n\n const errorWithContext = value as ErrorLikeWithContext;\n collectErrorMessages(\n errorWithContext.context?.originalError,\n messages,\n visited,\n depth + 1,\n );\n collectErrorMessages(errorWithContext.cause, messages, visited, depth + 1);\n}\n\nfunction getPrimaryCauseMessage(cause?: Error): {\n message?: string;\n messages?: string[];\n} {\n if (!cause) {\n return {};\n }\n\n const collected: string[] = [];\n collectErrorMessages(cause, collected, new Set<unknown>());\n\n const uniqueMessages = [...new Set(collected.filter(Boolean))];\n if (uniqueMessages.length === 0) {\n return {};\n }\n\n return {\n message: uniqueMessages[uniqueMessages.length - 1],\n messages: uniqueMessages,\n };\n}\n\n/**\n * Errors originating from database operations.\n *\n * Use the static factory methods rather than the constructor directly:\n * - `DatabaseError.connectionFailed(url, cause)` — DB connection failure\n * - `DatabaseError.queryFailed(query, cause)` — SQL execution error\n * - `DatabaseError.schemaError(table, op, cause)` — DDL/migration error\n * - `DatabaseError.constraintViolation(constraint, value, cause)` — FK/CHECK/UNIQUE\n * - `DatabaseError.corruptedData(field, class, cause)` — unparse-able column data\n * - `DatabaseError.missingDiscriminator(class, rowId)` — STI row missing `_meta_type`\n * - `DatabaseError.stiDiscriminatorConflict(...)` — legacy STI discriminator collides during qualification\n * - `DatabaseError.schemaMissing(table, class)` — table not yet migrated\n *\n * All errors have `category: 'database'` and codes prefixed with `DB_`.\n */\nexport class DatabaseError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'database', details, cause);\n }\n\n static connectionFailed(dbUrl: string, cause?: Error): DatabaseError {\n return new DatabaseError(\n `Failed to connect to database: ${dbUrl}`,\n 'DB_CONNECTION_FAILED',\n { dbUrl },\n cause,\n );\n }\n\n static queryFailed(query: string, cause?: Error): DatabaseError {\n // Include the deepest actionable cause message for better debugging.\n const causeInfo = getPrimaryCauseMessage(cause);\n const causeMsg = causeInfo.message ? `\\nCause: ${causeInfo.message}` : '';\n return new DatabaseError(\n `Database query failed: ${query.substring(0, 100)}${query.length > 100 ? '...' : ''}${causeMsg}`,\n 'DB_QUERY_FAILED',\n {\n query,\n causeMessage: causeInfo.message,\n causeMessages: causeInfo.messages,\n },\n cause,\n );\n }\n\n static schemaError(\n tableName: string,\n operation: string,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Schema operation failed for table '${tableName}': ${operation}`,\n 'DB_SCHEMA_ERROR',\n { tableName, operation },\n cause,\n );\n }\n\n static constraintViolation(\n constraint: string,\n value: unknown,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Database constraint violation: ${constraint}`,\n 'DB_CONSTRAINT_VIOLATION',\n { constraint, value },\n cause,\n );\n }\n\n static corruptedData(\n fieldName: string,\n className: string,\n cause?: Error,\n ): DatabaseError {\n return new DatabaseError(\n `Corrupted data in field '${fieldName}' for ${className}. ` +\n `The data cannot be parsed or is malformed. ` +\n `This may indicate database corruption or incompatible schema changes.`,\n 'DB_CORRUPTED_DATA',\n { fieldName, className },\n cause,\n );\n }\n\n static missingDiscriminator(\n className: string,\n rowId?: string,\n ): DatabaseError {\n return new DatabaseError(\n `Missing discriminator (_meta_type) for STI class ${className}${rowId ? ` (row id: ${rowId})` : ''}. ` +\n `STI classes require a discriminator column to determine the correct subclass. ` +\n `This may indicate a schema mismatch or manual database modification.`,\n 'DB_MISSING_DISCRIMINATOR',\n { className, rowId },\n );\n }\n\n static stiDiscriminatorConflict(details: {\n className: string;\n tableName: string;\n id: string;\n slug: string;\n context: string;\n conflictIdentity: Record<string, unknown>;\n legacyMetaType: string;\n qualifiedMetaType: string;\n duplicateId: string;\n }): DatabaseError {\n const identityText = Object.entries(details.conflictIdentity)\n .map(([key, value]) => `${key} '${String(value)}'`)\n .join(', ');\n\n return new DatabaseError(\n `Legacy STI discriminator collision for ${details.className} (${details.tableName}). ` +\n `Row '${details.id}' would upgrade _meta_type from '${details.legacyMetaType}' to '${details.qualifiedMetaType}', ` +\n `but row '${details.duplicateId}' already uses the qualified discriminator for ${identityText || 'the same conflict identity'}. ` +\n `Merge or remove the duplicate legacy/qualified rows before saving.`,\n 'DB_STI_DISCRIMINATOR_CONFLICT',\n details,\n );\n }\n\n static schemaMissing(tableName: string, className: string): DatabaseError {\n return new DatabaseError(\n `Table '${tableName}' does not exist for class '${className}'. ` +\n `Run 'smrt db:migrate' to create database schema.`,\n 'DB_SCHEMA_MISSING',\n { tableName, className },\n );\n }\n}\n\n/**\n * Errors from AI provider integrations.\n *\n * Use the static factory methods:\n * - `AIError.providerError(provider, operation, cause)` — generic provider failure\n * - `AIError.rateLimitExceeded(provider, retryAfter)` — rate limit hit\n * - `AIError.invalidResponse(provider, response)` — unexpected response shape\n * - `AIError.authenticationFailed(provider)` — bad API key / credentials\n *\n * All errors have `category: 'ai'` and codes prefixed with `AI_`.\n * AI errors are considered retryable by `ErrorUtils.isRetryable()`.\n */\nexport class AIError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'ai', details, cause);\n }\n\n static providerError(\n provider: string,\n operation: string,\n cause?: Error,\n ): AIError {\n return new AIError(\n `AI provider '${provider}' failed during ${operation}`,\n 'AI_PROVIDER_ERROR',\n { provider, operation },\n cause,\n );\n }\n\n static rateLimitExceeded(provider: string, retryAfter?: number): AIError {\n return new AIError(\n `AI provider '${provider}' rate limit exceeded`,\n 'AI_RATE_LIMIT',\n { provider, retryAfter },\n );\n }\n\n static invalidResponse(provider: string, response: unknown): AIError {\n return new AIError(\n `AI provider '${provider}' returned invalid response`,\n 'AI_INVALID_RESPONSE',\n { provider, response },\n );\n }\n\n static authenticationFailed(provider: string): AIError {\n return new AIError(\n `AI provider '${provider}' authentication failed`,\n 'AI_AUTH_FAILED',\n { provider },\n );\n }\n}\n\n/**\n * Errors from filesystem operations.\n *\n * Use the static factory methods:\n * - `FilesystemError.fileNotFound(path)` — file does not exist\n * - `FilesystemError.permissionDenied(path, operation)` — access denied\n * - `FilesystemError.diskSpaceExceeded(path, requiredBytes)` — insufficient space\n *\n * All errors have `category: 'filesystem'` and codes prefixed with `FS_`.\n */\nexport class FilesystemError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'filesystem', details, cause);\n }\n\n static fileNotFound(path: string): FilesystemError {\n return new FilesystemError(`File not found: ${path}`, 'FS_FILE_NOT_FOUND', {\n path,\n });\n }\n\n static permissionDenied(path: string, operation: string): FilesystemError {\n return new FilesystemError(\n `Permission denied for ${operation} on: ${path}`,\n 'FS_PERMISSION_DENIED',\n { path, operation },\n );\n }\n\n static diskSpaceExceeded(\n path: string,\n requiredBytes: number,\n ): FilesystemError {\n return new FilesystemError(\n `Insufficient disk space for operation on: ${path}`,\n 'FS_DISK_SPACE_EXCEEDED',\n { path, requiredBytes },\n );\n }\n}\n\n/**\n * Input/data validation errors thrown before or during a database operation.\n *\n * `save()` throws `ValidationError` when field validation fails. The collection's\n * `convertWhereKeys()` throws it for invalid WHERE clause operators or field names.\n * `ValidationError` is **not** retried by `ErrorUtils.withRetry()`.\n *\n * Use the static factory methods:\n * - `ValidationError.requiredField(field, objectType)` — missing required field\n * - `ValidationError.invalidValue(field, value, expected)` — wrong type/format\n * - `ValidationError.uniqueConstraint(field, value)` — duplicate unique value\n * - `ValidationError.rangeError(field, value, min?, max?)` — out of allowed range\n *\n * All errors have `category: 'validation'` and codes prefixed with `VALIDATION_`.\n */\nexport class ValidationError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'validation', details, cause);\n }\n\n static requiredField(fieldName: string, objectType: string): ValidationError {\n return new ValidationError(\n `Required field '${fieldName}' is missing for ${objectType}`,\n 'VALIDATION_REQUIRED_FIELD',\n { fieldName, objectType },\n );\n }\n\n static invalidValue(\n fieldName: string,\n value: unknown,\n expectedType: string,\n ): ValidationError {\n return new ValidationError(\n `Invalid value for field '${fieldName}': expected ${expectedType}, got ${typeof value}`,\n 'VALIDATION_INVALID_VALUE',\n { fieldName, value, expectedType },\n );\n }\n\n static uniqueConstraint(fieldName: string, value: unknown): ValidationError {\n return new ValidationError(\n `Unique constraint violation for field '${fieldName}' with value: ${String(value)}`,\n 'VALIDATION_UNIQUE_CONSTRAINT',\n { fieldName, value },\n );\n }\n\n static rangeError(\n fieldName: string,\n value: number,\n min?: number,\n max?: number,\n ): ValidationError {\n const range =\n min !== undefined && max !== undefined\n ? `between ${min} and ${max}`\n : min !== undefined\n ? `>= ${min}`\n : `<= ${max}`;\n\n return new ValidationError(\n `Value for field '${fieldName}' must be ${range}, got: ${value}`,\n 'VALIDATION_RANGE_ERROR',\n { fieldName, value, min, max },\n );\n }\n}\n\n/**\n * Errors from HTTP and external network operations.\n *\n * Use the static factory methods:\n * - `NetworkError.requestFailed(url, status?, body?)` — non-2xx response or connection failure\n * - `NetworkError.timeout(url, timeoutMs)` — request exceeded timeout\n * - `NetworkError.serviceUnavailable(service, reason?)` — external service down\n *\n * All errors have `category: 'network'` and codes prefixed with `NETWORK_`.\n * Network errors are considered retryable by `ErrorUtils.isRetryable()`.\n */\nexport class NetworkError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'network', details, cause);\n }\n\n static requestFailed(\n url: string,\n status?: number,\n responseBody?: string | Error,\n ): NetworkError {\n const cause = responseBody instanceof Error ? responseBody : undefined;\n const body = typeof responseBody === 'string' ? responseBody : undefined;\n return new NetworkError(\n `Network request failed: ${url}${status ? ` (Status: ${status})` : ''}${body ? ` - ${body.substring(0, 200)}` : ''}`,\n 'NETWORK_REQUEST_FAILED',\n { url, status, responseBody: body },\n cause,\n );\n }\n\n static timeout(url: string, timeoutMs: number): NetworkError {\n return new NetworkError(\n `Network request timed out after ${timeoutMs}ms: ${url}`,\n 'NETWORK_TIMEOUT',\n { url, timeoutMs },\n );\n }\n\n static serviceUnavailable(service: string, reason?: string): NetworkError {\n return new NetworkError(\n reason\n ? `External service unavailable: ${service} - ${reason}`\n : `External service unavailable: ${service}`,\n 'NETWORK_SERVICE_UNAVAILABLE',\n { service, reason },\n );\n }\n}\n\n/**\n * Errors from misconfigured or incompatible class/framework setup.\n *\n * These are typically thrown during class registration (i.e. at module load time),\n * not during normal request handling.\n *\n * Use the static factory methods:\n * - `ConfigurationError.missingConfiguration(key, context?)` — missing required config\n * - `ConfigurationError.invalidConfiguration(key, value, expected)` — wrong config type/value\n * - `ConfigurationError.initializationFailed(component, cause?)` — component failed to start\n * - `ConfigurationError.circularInheritance(class, chain)` — circular class inheritance\n * - `ConfigurationError.incompatibleStrategy(class, strategy, parent, parentStrategy)` — STI mismatch\n * - `ConfigurationError.unregisteredBaseClass(child, base)` — STI base not yet registered\n *\n * All errors have `category: 'configuration'` and codes prefixed with `CONFIG_`.\n * Configuration errors are **not** retried by `ErrorUtils.withRetry()`.\n */\nexport class ConfigurationError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'configuration', details, cause);\n }\n\n static missingConfiguration(\n configKey: string,\n context?: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Missing required configuration: ${configKey}${context ? ` in ${context}` : ''}`,\n 'CONFIG_MISSING',\n { configKey, context },\n );\n }\n\n static invalidConfiguration(\n configKey: string,\n value: unknown,\n expected: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Invalid configuration for ${configKey}: expected ${expected}, got ${typeof value}`,\n 'CONFIG_INVALID',\n { configKey, value, expected },\n );\n }\n\n static initializationFailed(\n component: string,\n cause?: Error,\n ): ConfigurationError {\n return new ConfigurationError(\n `Failed to initialize component: ${component}`,\n 'CONFIG_INIT_FAILED',\n { component },\n cause,\n );\n }\n\n static circularInheritance(\n className: string,\n inheritanceChain: string[],\n ): ConfigurationError {\n return new ConfigurationError(\n `Circular inheritance detected for class '${className}'. ` +\n `Inheritance chain: ${inheritanceChain.join(' → ')} → ${className}. ` +\n `Classes cannot inherit from themselves directly or indirectly.`,\n 'CONFIG_CIRCULAR_INHERITANCE',\n { className, inheritanceChain },\n );\n }\n\n static incompatibleStrategy(\n className: string,\n classStrategy: string,\n parentClass: string,\n parentStrategy: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `Incompatible table strategy for class '${className}' (${classStrategy}). ` +\n `Parent class '${parentClass}' uses ${parentStrategy} strategy. ` +\n `Child classes must use the same table strategy as their parent. ` +\n `Either change ${className} to use ${parentStrategy}, or remove the inheritance.`,\n 'CONFIG_INCOMPATIBLE_STRATEGY',\n { className, classStrategy, parentClass, parentStrategy },\n );\n }\n\n static unregisteredBaseClass(\n childClass: string,\n baseClass: string,\n ): ConfigurationError {\n return new ConfigurationError(\n `STI base class '${baseClass}' is not registered for child class '${childClass}'. ` +\n `When using Single Table Inheritance, the base class must be registered before any child classes. ` +\n `Ensure ${baseClass} is decorated with @smrt({ tableStrategy: 'sti' }) and imported before ${childClass}.`,\n 'CONFIG_UNREGISTERED_BASE',\n { childClass, baseClass },\n );\n }\n}\n\n/**\n * Errors representing unexpected runtime failures not covered by other categories.\n *\n * `RuntimeError` is the catch-all for internal framework errors — invalid object\n * state, exhausted resources, or failures in operations like `save()` and `loadFromId()`\n * that propagate from an unknown cause.\n *\n * Use the static factory methods:\n * - `RuntimeError.operationFailed(operation, context?, cause?)` — generic operation failure\n * - `RuntimeError.invalidState(message, context?)` — unexpected object/system state\n * - `RuntimeError.resourceExhausted(resource, limit)` — limit exceeded (e.g. connections)\n *\n * All errors have `category: 'runtime'` and codes prefixed with `RUNTIME_`.\n */\nexport class RuntimeError extends SmrtError {\n constructor(\n message: string,\n code: string,\n details?: Record<string, unknown>,\n cause?: Error,\n ) {\n super(message, code, 'runtime', details, cause);\n }\n\n static operationFailed(\n operation: string,\n context?: string,\n cause?: Error,\n ): RuntimeError {\n return new RuntimeError(\n `Operation failed: ${operation}${context ? ` in ${context}` : ''}`,\n 'RUNTIME_OPERATION_FAILED',\n { operation, context },\n cause,\n );\n }\n\n static invalidState(\n message: string,\n context?: Record<string, unknown>,\n ): RuntimeError {\n return new RuntimeError(message, 'RUNTIME_INVALID_STATE', context);\n }\n\n static resourceExhausted(resource: string, limit: number): RuntimeError {\n return new RuntimeError(\n `Resource exhausted: ${resource} exceeded limit of ${limit}`,\n 'RUNTIME_RESOURCE_EXHAUSTED',\n { resource, limit },\n );\n }\n}\n\n/**\n * Error thrown when a tenant isolation boundary is crossed while resolving a\n * relationship.\n *\n * Raised by {@link SmrtObject.loadRelated} / {@link SmrtObject.loadRelatedMany}\n * (and {@link SmrtObject.getRelated}, which delegates to them) when a\n * tenant-scoped object resolves a relationship to an object belonging to a\n * *different*, non-null tenant — the genuine cross-tenant data leak. The guard\n * is a no-op when either side has a `null` tenant (global / non-tenant-scoped\n * models) and when both sides share the same tenant, so it only fires on real\n * leaks. Pass `{ allowCrossTenant: true }` to the loader to deliberately opt out.\n *\n * The `code` is always `'TENANT_ISOLATION_VIOLATION'` and the category is\n * `'validation'`. It is never retried — `ErrorUtils.withRetry()` rethrows it\n * immediately and `ErrorUtils.isRetryable()` returns `false` — because a tenant\n * boundary violation is deterministic. `tenantId` is the owning object's tenant\n * and `attemptedTenantId` is the tenant of the object that was reached.\n *\n * This shares its stable `code`, `name`, `tenantId`, and `attemptedTenantId`\n * shape with the interceptor-level `TenantIsolationError` in\n * `@happyvertical/smrt-tenancy`, so cross-cutting handlers can match either via\n * `err.code === 'TENANT_ISOLATION_VIOLATION'`. They are intentionally distinct\n * classes because `@happyvertical/smrt-core` cannot depend on the tenancy\n * package (the dependency runs the other way).\n *\n * @example\n * ```typescript\n * try {\n * await order.loadRelated('customerId');\n * } catch (err) {\n * if (err instanceof TenantIsolationError) {\n * // err.tenantId — the order's tenant\n * // err.attemptedTenantId — the customer's tenant\n * }\n * }\n * ```\n *\n * @see SmrtObject.loadRelated\n * @see SmrtObject.loadRelatedMany\n */\nexport class TenantIsolationError extends SmrtError {\n /** The tenant ID of the object that owns the relationship. */\n public readonly tenantId?: string;\n /** The tenant ID of the related object that was reached (and rejected). */\n public readonly attemptedTenantId?: string;\n\n constructor(\n message: string,\n details?: {\n tenantId?: string;\n attemptedTenantId?: string;\n [key: string]: unknown;\n },\n cause?: Error,\n ) {\n super(message, 'TENANT_ISOLATION_VIOLATION', 'validation', details, cause);\n this.tenantId = details?.tenantId;\n this.attemptedTenantId = details?.attemptedTenantId;\n }\n\n /**\n * Builds a {@link TenantIsolationError} for a blocked cross-tenant\n * relationship resolution, with a descriptive message and structured details.\n */\n static crossTenantReference(details: {\n sourceClass: string;\n fieldName: string;\n sourceTenantId: string;\n targetClass?: string;\n targetTenantId: string;\n }): TenantIsolationError {\n const target = details.targetClass\n ? `${details.targetClass} (tenant '${details.targetTenantId}')`\n : `tenant '${details.targetTenantId}'`;\n return new TenantIsolationError(\n `Cross-tenant relationship access blocked on ${details.sourceClass}.${details.fieldName}: ` +\n `owning tenant '${details.sourceTenantId}' does not match ${target}. ` +\n `Pass { allowCrossTenant: true } to loadRelated()/loadRelatedMany()/getRelated() to override.`,\n {\n tenantId: details.sourceTenantId,\n attemptedTenantId: details.targetTenantId,\n sourceClass: details.sourceClass,\n fieldName: details.fieldName,\n targetClass: details.targetClass,\n },\n );\n }\n}\n\n/**\n * Utility functions for error handling\n */\nexport class ErrorUtils {\n /**\n * Wraps a function with error handling and automatic retry logic\n */\n static async withRetry<T>(\n operation: () => Promise<T>,\n maxRetries = 3,\n delay = 1000,\n backoffMultiplier = 2,\n ): Promise<T> {\n let lastError: Error = new Error('Operation failed without error details');\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await operation();\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n if (attempt === maxRetries) {\n throw lastError;\n }\n\n // Skip retry for certain error types. A tenant isolation violation is\n // deterministic — retrying re-fetches the same cross-tenant target — and\n // is a security boundary, so it must never be retried.\n if (\n error instanceof ValidationError ||\n error instanceof ConfigurationError ||\n error instanceof TenantIsolationError\n ) {\n throw error;\n }\n\n // Wait before retrying with exponential backoff\n // Wrap in try-catch to handle any potential timer errors\n try {\n await new Promise<void>((resolve) => {\n setTimeout(() => resolve(), delay * backoffMultiplier ** attempt);\n });\n } catch (timerError) {\n // Log timer error but don't fail the retry\n logger.error('Timer error during retry', { error: timerError });\n }\n }\n }\n\n throw lastError;\n }\n\n /**\n * Checks if an error is retryable\n */\n static isRetryable(error: Error): boolean {\n if (error instanceof SmrtError) {\n return error.category === 'network' || error.category === 'ai';\n }\n\n // Check for common retryable error patterns\n const retryablePatterns = [\n /ECONNRESET/,\n /ETIMEDOUT/,\n /ENOTFOUND/,\n /rate.?limit/i,\n /timeout/i,\n /503/,\n /502/,\n /500/,\n ];\n\n return retryablePatterns.some((pattern) => pattern.test(error.message));\n }\n\n /**\n * Sanitizes an error for safe logging (removes sensitive information)\n */\n static sanitizeError(error: Error): Record<string, unknown> {\n const sanitized: Record<string, unknown> = {\n name: error.name,\n message: error.message,\n stack: error.stack,\n };\n\n if (error instanceof SmrtError) {\n sanitized.code = error.code;\n sanitized.category = error.category;\n\n // Sanitize details to remove potential sensitive information\n if (error.details) {\n const details: Record<string, unknown> = { ...error.details };\n sanitized.details = details;\n\n // Remove common sensitive fields\n const sensitiveFields = [\n 'password',\n 'token',\n 'key',\n 'secret',\n 'apiKey',\n ];\n for (const field of sensitiveFields) {\n if (details[field]) {\n details[field] = '[REDACTED]';\n }\n }\n }\n }\n\n return sanitized;\n }\n}\n\n/**\n * Validation report that collects multiple validation errors\n *\n * Useful for validating an entire object and reporting all errors\n * at once rather than stopping at the first error.\n *\n * @example\n * ```typescript\n * const report = new ValidationReport('Product');\n * report.addError(ValidationError.requiredField('name', 'Product'));\n * report.addError(ValidationError.rangeError('price', -10, 0));\n *\n * if (report.hasErrors()) {\n * console.error(report.toString());\n * // Output:\n * // Validation failed for Product with 2 errors:\n * // - name: Required field 'name' is missing for Product\n * // - price: Value -10 for field 'price' is outside allowed range [0, undefined]\n * }\n * ```\n */\nexport class ValidationReport {\n private errors: ValidationError[] = [];\n private objectType: string;\n\n constructor(objectType: string) {\n this.objectType = objectType;\n }\n\n /**\n * Add a validation error to the report\n */\n addError(error: ValidationError): void {\n this.errors.push(error);\n }\n\n /**\n * Check if there are any validation errors\n */\n hasErrors(): boolean {\n return this.errors.length > 0;\n }\n\n /**\n * Get all validation errors\n */\n getErrors(): ValidationError[] {\n return [...this.errors];\n }\n\n /**\n * Get the number of validation errors\n */\n getErrorCount(): number {\n return this.errors.length;\n }\n\n /**\n * Convert to a human-readable string\n */\n toString(): string {\n if (this.errors.length === 0) {\n return `Validation passed for ${this.objectType}`;\n }\n\n const errorList = this.errors\n .map((err, idx) => ` ${idx + 1}. ${err.message}`)\n .join('\\n');\n\n return `Validation failed for ${this.objectType} with ${this.errors.length} error(s):\\n${errorList}`;\n }\n\n /**\n * Convert to JSON format\n */\n toJSON(): object {\n return {\n objectType: this.objectType,\n errorCount: this.errors.length,\n errors: this.errors.map((err) => err.toJSON()),\n };\n }\n\n /**\n * Throw the first error if there are any errors\n */\n throwIfErrors(): void {\n if (this.errors.length > 0) {\n throw this.errors[0];\n }\n }\n\n /**\n * Clear all errors\n */\n clear(): void {\n this.errors = [];\n }\n}\n\n/**\n * Validation utility functions\n */\nexport class ValidationUtils {\n /**\n * Validate a single field value\n *\n * @param fieldName - Name of the field\n * @param value - Value to validate\n * @param options - Validation options (required, min, max, etc.)\n * @returns ValidationError if validation fails, null otherwise\n */\n static async validateField(\n fieldName: string,\n value: unknown,\n options: {\n required?: boolean;\n min?: number;\n max?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string | RegExp;\n type?: string;\n customValidator?: (value: unknown) => boolean | Promise<boolean>;\n customMessage?: string;\n },\n objectType: string = 'Object',\n ): Promise<ValidationError | null> {\n // Required check\n if (\n options.required &&\n (value === null || value === undefined || value === '')\n ) {\n return ValidationError.requiredField(fieldName, objectType);\n }\n\n // Skip further validation if value is null/undefined and not required\n if (value === null || value === undefined) {\n return null;\n }\n\n // Numeric range validation\n if (typeof value === 'number') {\n if (options.min !== undefined && value < options.min) {\n return ValidationError.rangeError(\n fieldName,\n value,\n options.min,\n options.max,\n );\n }\n if (options.max !== undefined && value > options.max) {\n return ValidationError.rangeError(\n fieldName,\n value,\n options.min,\n options.max,\n );\n }\n }\n\n // String length validation\n if (typeof value === 'string') {\n if (options.minLength !== undefined && value.length < options.minLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with minimum length ${options.minLength}`,\n );\n }\n if (options.maxLength !== undefined && value.length > options.maxLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with maximum length ${options.maxLength}`,\n );\n }\n\n // Pattern validation\n if (options.pattern) {\n const regex =\n typeof options.pattern === 'string'\n ? new RegExp(options.pattern)\n : options.pattern;\n if (!regex.test(value)) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string matching pattern ${options.pattern}`,\n );\n }\n }\n }\n\n // Custom validator\n if (options.customValidator) {\n try {\n const isValid = await options.customValidator(value);\n if (!isValid) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n options.customMessage || 'custom validation failed',\n );\n }\n } catch (error) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `custom validation error: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return null;\n }\n\n /**\n * Validate required field\n */\n static validateRequired(\n fieldName: string,\n value: unknown,\n objectType: string = 'Object',\n ): ValidationError | null {\n if (value === null || value === undefined || value === '') {\n return ValidationError.requiredField(fieldName, objectType);\n }\n return null;\n }\n\n /**\n * Validate numeric range\n */\n static validateRange(\n fieldName: string,\n value: number,\n min?: number,\n max?: number,\n ): ValidationError | null {\n if (min !== undefined && value < min) {\n return ValidationError.rangeError(fieldName, value, min, max);\n }\n if (max !== undefined && value > max) {\n return ValidationError.rangeError(fieldName, value, min, max);\n }\n return null;\n }\n\n /**\n * Validate string length\n */\n static validateLength(\n fieldName: string,\n value: string,\n minLength?: number,\n maxLength?: number,\n ): ValidationError | null {\n if (minLength !== undefined && value.length < minLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with minimum length ${minLength}`,\n );\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string with maximum length ${maxLength}`,\n );\n }\n return null;\n }\n\n /**\n * Validate string pattern\n */\n static validatePattern(\n fieldName: string,\n value: string,\n pattern: string | RegExp,\n ): ValidationError | null {\n const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;\n if (!regex.test(value)) {\n return ValidationError.invalidValue(\n fieldName,\n value,\n `string matching pattern ${pattern}`,\n );\n }\n return null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,33 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
* Generated `_changes` HTTP route for the change feed (issue #1758).
|
|
3
|
-
*
|
|
4
|
-
* Handles `GET {basePath}/_changes` in the REST generator: an auth-guarded,
|
|
5
|
-
* tenant-scoped cursor read over the `_smrt_changes` log. This module keeps
|
|
6
|
-
* the feed logic out of `rest.ts` (which only registers the path) so
|
|
7
|
-
* sibling generator changes stay conflict-free.
|
|
8
|
-
*
|
|
9
|
-
* Contract (part of the client/mobile sync contract, PRD #1755):
|
|
10
|
-
* - `GET {basePath}/_changes?since=<cursor>&tables=<a,b>&limit=<n>` returns
|
|
11
|
-
* `{ changes, cursor, resyncRequired? }` — see `getChangesSince` for the
|
|
12
|
-
* exact cursor guarantee (strictly monotonic; reads miss no committed
|
|
13
|
-
* changes under concurrent writers). `resyncRequired: true` — served as
|
|
14
|
-
* HTTP 200, it is protocol state rather than an error — means the cursor
|
|
15
|
-
* cannot be served incrementally (pruned out of the retained window, or
|
|
16
|
-
* foreign/reset) and the client must re-fetch its data in full.
|
|
17
|
-
* - **Auth is fail-closed** (#1540 posture): the route requires the
|
|
18
|
-
* generator's `authMiddleware`. Without one configured, every request is
|
|
19
|
-
* refused with 401 — the feed spans all tables, so per-model
|
|
20
|
-
* `api: { public }` opt-outs deliberately do NOT apply to it.
|
|
21
|
-
* - **Tenant scoping** follows the active tenant context through the same
|
|
22
|
-
* dependency-inversion hook the DispatchBus uses: with tenancy enabled, a
|
|
23
|
-
* request only ever sees its own tenant's changes plus global rows; with
|
|
24
|
-
* tenancy enabled but no active tenant, only global rows (fail-closed).
|
|
25
|
-
*/
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
26
2
|
/**
|
|
27
3
|
* Structural copy of `APIConfig['authMiddleware']` (from `rest.ts`) so this
|
|
28
4
|
* module never imports the generator back (keeps the dependency one-way).
|
|
5
|
+
*
|
|
6
|
+
* Exported so the sibling `_events` route (#1763) reuses the exact same
|
|
7
|
+
* fail-closed auth contract without redeclaring it.
|
|
29
8
|
*/
|
|
30
|
-
type ChangesAuthMiddleware = (objectName: string, action: string) => (req: Request) => Promise<Request | Response>;
|
|
9
|
+
export type ChangesAuthMiddleware = (objectName: string, action: string) => (req: Request) => Promise<Request | Response>;
|
|
31
10
|
export interface ChangesRouteOptions {
|
|
32
11
|
/** The generator's configured auth middleware, if any. */
|
|
33
12
|
authMiddleware?: ChangesAuthMiddleware;
|
|
@@ -39,6 +18,16 @@ export interface ChangesRouteOptions {
|
|
|
39
18
|
* middlewares can recognize and specially authorize it if they want to.
|
|
40
19
|
*/
|
|
41
20
|
export declare const CHANGES_ROUTE_OBJECT_NAME = "_changes";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the generator's `db` option (instance, config object, or URL string)
|
|
23
|
+
* to a single shared {@link DatabaseInterface} per distinct option value.
|
|
24
|
+
*
|
|
25
|
+
* Exported so the sibling `_events` route (#1763) resolves its database
|
|
26
|
+
* identically — sharing this WeakMap/Map means both routes read the same
|
|
27
|
+
* underlying handle for a given option, which matters for `:memory:` databases
|
|
28
|
+
* where a fresh `getDatabase()` per call would mint a separate database.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveChangesDb(dbOption: unknown): Promise<DatabaseInterface>;
|
|
42
31
|
/**
|
|
43
32
|
* Handle a request against the generated `_changes` route.
|
|
44
33
|
*
|
|
@@ -48,5 +37,4 @@ export declare const CHANGES_ROUTE_OBJECT_NAME = "_changes";
|
|
|
48
37
|
* `{ changes, cursor }` page scoped to the active tenant context.
|
|
49
38
|
*/
|
|
50
39
|
export declare function handleChangesRoute(req: Request, options: ChangesRouteOptions): Promise<Response>;
|
|
51
|
-
export {};
|
|
52
40
|
//# sourceMappingURL=changes-route.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/generators/changes-route.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"changes-route.d.ts","sourceRoot":"","sources":["../../src/generators/changes-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAS5D;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,KACX,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,gFAAgF;IAChF,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAa,CAAC;AAQpD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAmC5B;AAaD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAmEnB"}
|
|
@@ -12,12 +12,13 @@ import { getDatabase } from "@happyvertical/sql";
|
|
|
12
12
|
*
|
|
13
13
|
* Contract (part of the client/mobile sync contract, PRD #1755):
|
|
14
14
|
* - `GET {basePath}/_changes?since=<cursor>&tables=<a,b>&limit=<n>` returns
|
|
15
|
-
* `{ changes, cursor, resyncRequired? }` — see
|
|
16
|
-
* exact cursor guarantee (strictly monotonic;
|
|
17
|
-
* changes under concurrent writers).
|
|
18
|
-
* HTTP 200, it is protocol state rather
|
|
19
|
-
* cannot be served incrementally (pruned
|
|
20
|
-
* foreign/reset) and the client must
|
|
15
|
+
* `{ changes, cursor, resyncRequired?, resyncCursor? }` — see
|
|
16
|
+
* `getChangesSince` for the exact cursor guarantee (strictly monotonic;
|
|
17
|
+
* reads miss no committed changes under concurrent writers).
|
|
18
|
+
* `resyncRequired: true` — served as HTTP 200, it is protocol state rather
|
|
19
|
+
* than an error — means the cursor cannot be served incrementally (pruned
|
|
20
|
+
* out of the retained window, or foreign/reset) and the client must
|
|
21
|
+
* re-fetch its data in full, then resume from `resyncCursor`.
|
|
21
22
|
* - **Auth is fail-closed** (#1540 posture): the route requires the
|
|
22
23
|
* generator's `authMiddleware`. Without one configured, every request is
|
|
23
24
|
* refused with 401 — the feed spans all tables, so per-model
|
|
@@ -35,6 +36,15 @@ var logger = createLogger({ level: "info" });
|
|
|
35
36
|
var CHANGES_ROUTE_OBJECT_NAME = "_changes";
|
|
36
37
|
var resolvedInstanceDbs = /* @__PURE__ */ new WeakMap();
|
|
37
38
|
var resolvedUrlDbs = /* @__PURE__ */ new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the generator's `db` option (instance, config object, or URL string)
|
|
41
|
+
* to a single shared {@link DatabaseInterface} per distinct option value.
|
|
42
|
+
*
|
|
43
|
+
* Exported so the sibling `_events` route (#1763) resolves its database
|
|
44
|
+
* identically — sharing this WeakMap/Map means both routes read the same
|
|
45
|
+
* underlying handle for a given option, which matters for `:memory:` databases
|
|
46
|
+
* where a fresh `getDatabase()` per call would mint a separate database.
|
|
47
|
+
*/
|
|
38
48
|
function resolveChangesDb(dbOption) {
|
|
39
49
|
if (dbOption && typeof dbOption === "object") {
|
|
40
50
|
if ("query" in dbOption && typeof dbOption.query === "function") return Promise.resolve(dbOption);
|
|
@@ -106,6 +116,6 @@ async function handleChangesRoute(req, options) {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
//#endregion
|
|
109
|
-
export { CHANGES_ROUTE_OBJECT_NAME, handleChangesRoute };
|
|
119
|
+
export { CHANGES_ROUTE_OBJECT_NAME, handleChangesRoute, resolveChangesDb };
|
|
110
120
|
|
|
111
121
|
//# sourceMappingURL=changes-route.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/generators/changes-route.ts"],"sourcesContent":["/**\n * Generated `_changes` HTTP route for the change feed (issue #1758).\n *\n * Handles `GET {basePath}/_changes` in the REST generator: an auth-guarded,\n * tenant-scoped cursor read over the `_smrt_changes` log. This module keeps\n * the feed logic out of `rest.ts` (which only registers the path) so\n * sibling generator changes stay conflict-free.\n *\n * Contract (part of the client/mobile sync contract, PRD #1755):\n * - `GET {basePath}/_changes?since=<cursor>&tables=<a,b>&limit=<n>` returns\n * `{ changes, cursor, resyncRequired? }` — see `getChangesSince` for the
|
|
1
|
+
{"version":3,"file":"changes-route.js","names":[],"sources":["../../src/generators/changes-route.ts"],"sourcesContent":["/**\n * Generated `_changes` HTTP route for the change feed (issue #1758).\n *\n * Handles `GET {basePath}/_changes` in the REST generator: an auth-guarded,\n * tenant-scoped cursor read over the `_smrt_changes` log. This module keeps\n * the feed logic out of `rest.ts` (which only registers the path) so\n * sibling generator changes stay conflict-free.\n *\n * Contract (part of the client/mobile sync contract, PRD #1755):\n * - `GET {basePath}/_changes?since=<cursor>&tables=<a,b>&limit=<n>` returns\n * `{ changes, cursor, resyncRequired?, resyncCursor? }` — see\n * `getChangesSince` for the exact cursor guarantee (strictly monotonic;\n * reads miss no committed changes under concurrent writers).\n * `resyncRequired: true` — served as HTTP 200, it is protocol state rather\n * than an error — means the cursor cannot be served incrementally (pruned\n * out of the retained window, or foreign/reset) and the client must\n * re-fetch its data in full, then resume from `resyncCursor`.\n * - **Auth is fail-closed** (#1540 posture): the route requires the\n * generator's `authMiddleware`. Without one configured, every request is\n * refused with 401 — the feed spans all tables, so per-model\n * `api: { public }` opt-outs deliberately do NOT apply to it.\n * - **Tenant scoping** follows the active tenant context through the same\n * dependency-inversion hook the DispatchBus uses: with tenancy enabled, a\n * request only ever sees its own tenant's changes plus global rows; with\n * tenancy enabled but no active tenant, only global rows (fail-closed).\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n ensureChangeFeedTable,\n getTenantScopedChangesSince,\n} from '../change-feed.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Structural copy of `APIConfig['authMiddleware']` (from `rest.ts`) so this\n * module never imports the generator back (keeps the dependency one-way).\n *\n * Exported so the sibling `_events` route (#1763) reuses the exact same\n * fail-closed auth contract without redeclaring it.\n */\nexport type ChangesAuthMiddleware = (\n objectName: string,\n action: string,\n) => (req: Request) => Promise<Request | Response>;\n\nexport interface ChangesRouteOptions {\n /** The generator's configured auth middleware, if any. */\n authMiddleware?: ChangesAuthMiddleware;\n /** The generator's `APIContext.db` (instance, config object, or URL string). */\n db?: unknown;\n}\n\n/**\n * Pseudo object name passed to the auth middleware for the feed route, so\n * middlewares can recognize and specially authorize it if they want to.\n */\nexport const CHANGES_ROUTE_OBJECT_NAME = '_changes';\n\n// Resolve the APIContext db option once per distinct option value so every\n// request reads the same underlying database (getDatabase() would otherwise\n// mint a fresh instance per call for config objects / `:memory:` URLs).\nconst resolvedInstanceDbs = new WeakMap<object, Promise<DatabaseInterface>>();\nconst resolvedUrlDbs = new Map<string, Promise<DatabaseInterface>>();\n\n/**\n * Resolve the generator's `db` option (instance, config object, or URL string)\n * to a single shared {@link DatabaseInterface} per distinct option value.\n *\n * Exported so the sibling `_events` route (#1763) resolves its database\n * identically — sharing this WeakMap/Map means both routes read the same\n * underlying handle for a given option, which matters for `:memory:` databases\n * where a fresh `getDatabase()` per call would mint a separate database.\n */\nexport function resolveChangesDb(\n dbOption: unknown,\n): Promise<DatabaseInterface> {\n if (dbOption && typeof dbOption === 'object') {\n if (\n 'query' in dbOption &&\n typeof (dbOption as { query?: unknown }).query === 'function'\n ) {\n return Promise.resolve(dbOption as DatabaseInterface);\n }\n let resolved = resolvedInstanceDbs.get(dbOption);\n if (!resolved) {\n resolved = getDatabase(\n dbOption as Parameters<typeof getDatabase>[0],\n ) as Promise<DatabaseInterface>;\n resolvedInstanceDbs.set(dbOption, resolved);\n }\n return resolved;\n }\n\n if (typeof dbOption === 'string' && dbOption) {\n let resolved = resolvedUrlDbs.get(dbOption);\n if (!resolved) {\n // Match SmrtClass's connection-sharing convention for file-backed URLs.\n const isMemoryDb = dbOption === ':memory:';\n resolved = getDatabase({\n url: dbOption,\n ...(isMemoryDb ? {} : { dbid: `smrt:${dbOption}` }),\n }) as Promise<DatabaseInterface>;\n resolvedUrlDbs.set(dbOption, resolved);\n }\n return resolved;\n }\n\n return Promise.reject(\n new Error('The _changes route requires a database in APIContext'),\n );\n}\n\nfunction jsonResponse(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n\nfunction errorResponse(status: number, message: string): Response {\n return jsonResponse({ error: message }, status);\n}\n\n/**\n * Handle a request against the generated `_changes` route.\n *\n * Returns 401 when no auth middleware is configured (fail-closed) or the\n * middleware rejects; 405 for non-GET methods; 400 for malformed `since`/\n * `limit`; 503 when the generator has no database; otherwise a\n * `{ changes, cursor }` page scoped to the active tenant context.\n */\nexport async function handleChangesRoute(\n req: Request,\n options: ChangesRouteOptions,\n): Promise<Response> {\n if (req.method !== 'GET') {\n return errorResponse(405, 'Method not allowed');\n }\n\n // Fail-closed (#1540): the change feed spans every table, so it is never\n // public — an auth middleware must be configured and must pass. The action\n // is the lowercased HTTP method, matching the generator's other call sites.\n if (!options.authMiddleware) {\n return errorResponse(401, 'Authentication required');\n }\n const authCheck = options.authMiddleware(\n CHANGES_ROUTE_OBJECT_NAME,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult;\n }\n\n if (options.db == null) {\n return errorResponse(\n 503,\n 'Change feed unavailable: no database configured for the API generator',\n );\n }\n\n const url = new URL(authResult.url);\n const since = Number(url.searchParams.get('since') ?? '0');\n if (!Number.isFinite(since) || since < 0) {\n return errorResponse(400, \"'since' must be a non-negative number\");\n }\n\n let limit: number | undefined;\n const limitParam = url.searchParams.get('limit');\n if (limitParam !== null) {\n limit = Number(limitParam);\n if (!Number.isFinite(limit) || limit < 1) {\n return errorResponse(400, \"'limit' must be a positive number\");\n }\n }\n\n const tablesParam = url.searchParams.get('tables');\n const tables = tablesParam\n ? tablesParam\n .split(',')\n .map((table) => table.trim())\n .filter(Boolean)\n : undefined;\n\n try {\n const db = await resolveChangesDb(options.db);\n // System tables are runtime-ensured; a raw handle passed straight to the\n // generator may not have gone through framework init yet.\n await ensureChangeFeedTable(db);\n const page = await getTenantScopedChangesSince(db, {\n since,\n tables,\n limit,\n });\n return jsonResponse(page);\n } catch (error) {\n logger.error('Change feed route failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n return errorResponse(500, 'Internal server error');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;;;;;AAyB7C,IAAa,4BAA4B;AAKzC,IAAM,sCAAsB,IAAI,QAA4C;AAC5E,IAAM,iCAAiB,IAAI,IAAwC;;;;;;;;;;AAWnE,SAAgB,iBACd,UAC4B;CAC5B,IAAI,YAAY,OAAO,aAAa,UAAU;EAC5C,IACE,WAAW,YACX,OAAQ,SAAiC,UAAU,YAEnD,OAAO,QAAQ,QAAQ,QAA6B;EAEtD,IAAI,WAAW,oBAAoB,IAAI,QAAQ;EAC/C,IAAI,CAAC,UAAU;GACb,WAAW,YACT,QACF;GACA,oBAAoB,IAAI,UAAU,QAAQ;EAC5C;EACA,OAAO;CACT;CAEA,IAAI,OAAO,aAAa,YAAY,UAAU;EAC5C,IAAI,WAAW,eAAe,IAAI,QAAQ;EAC1C,IAAI,CAAC,UAAU;GAGb,WAAW,YAAY;IACrB,KAAK;IACL,GAHiB,aAAa,aAGb,CAAC,IAAI,EAAE,MAAM,QAAQ,WAAW;GACnD,CAAC;GACD,eAAe,IAAI,UAAU,QAAQ;EACvC;EACA,OAAO;CACT;CAEA,OAAO,QAAQ,uBACb,IAAI,MAAM,sDAAsD,CAClE;AACF;AAEA,SAAS,aAAa,MAAe,SAAS,KAAe;CAC3D,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH;AAEA,SAAS,cAAc,QAAgB,SAA2B;CAChE,OAAO,aAAa,EAAE,OAAO,QAAQ,GAAG,MAAM;AAChD;;;;;;;;;AAUA,eAAsB,mBACpB,KACA,SACmB;CACnB,IAAI,IAAI,WAAW,OACjB,OAAO,cAAc,KAAK,oBAAoB;CAMhD,IAAI,CAAC,QAAQ,gBACX,OAAO,cAAc,KAAK,yBAAyB;CAMrD,MAAM,aAAa,MAJD,QAAQ,eACxB,2BACA,IAAI,OAAO,YAAY,CAEA,CAAA,CAAU,GAAG;CACtC,IAAI,sBAAsB,UACxB,OAAO;CAGT,IAAI,QAAQ,MAAM,MAChB,OAAO,cACL,KACA,uEACF;CAGF,MAAM,MAAM,IAAI,IAAI,WAAW,GAAG;CAClC,MAAM,QAAQ,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK,GAAG;CACzD,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GACrC,OAAO,cAAc,KAAK,uCAAuC;CAGnE,IAAI;CACJ,MAAM,aAAa,IAAI,aAAa,IAAI,OAAO;CAC/C,IAAI,eAAe,MAAM;EACvB,QAAQ,OAAO,UAAU;EACzB,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GACrC,OAAO,cAAc,KAAK,mCAAmC;CAEjE;CAEA,MAAM,cAAc,IAAI,aAAa,IAAI,QAAQ;CACjD,MAAM,SAAS,cACX,YACG,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,IACjB,KAAA;CAEJ,IAAI;EACF,MAAM,KAAK,MAAM,iBAAiB,QAAQ,EAAE;EAG5C,MAAM,sBAAsB,EAAE;EAM9B,OAAO,aAAa,MALD,4BAA4B,IAAI;GACjD;GACA;GACA;EACF,CAAC,CACuB;CAC1B,SAAS,OAAO;EACd,OAAO,MAAM,4BAA4B,EACvC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACD,OAAO,cAAc,KAAK,uBAAuB;CACnD;AACF"}
|
package/dist/generators/cli.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface CLIContext {
|
|
|
24
24
|
id: string;
|
|
25
25
|
roles?: string[];
|
|
26
26
|
};
|
|
27
|
+
/** Resolved permission slugs held by the operator. */
|
|
28
|
+
permissions?: Iterable<string>;
|
|
27
29
|
/**
|
|
28
30
|
* Default tenant for tenant-scoped commands when no `--tenant` flag is given.
|
|
29
31
|
* Hosts that authenticate an operator may set this from the principal.
|
|
@@ -113,6 +115,7 @@ export declare class CLIGenerator {
|
|
|
113
115
|
* result is stripped too; a cycle guard prevents infinite loops.
|
|
114
116
|
*/
|
|
115
117
|
private toPublicData;
|
|
118
|
+
private getPublicJsonOptions;
|
|
116
119
|
/** List the runnable commands grouped by object, for help output. */
|
|
117
120
|
listCommands(): Promise<string[]>;
|
|
118
121
|
private helpText;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/generators/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAwBrD;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,yCAAyC;IACzC,EAAE,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAoCD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;gBAEhB,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;IAU5D,wBAAwB;IACxB,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,mBAAmB;IACnB,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;;;OAIG;IACH,eAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC;IAMpD;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CxC;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAkDjB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAWpB;;;;OAIG;YACW,oBAAoB;IA+ClC;;;;;;OAMG;YACW,aAAa;IAS3B;;OAEG;YACW,aAAa;IAkF3B;;;OAGG;YACW,mBAAmB;IAgCjC;;;;OAIG;YACW,WAAW;IAsBzB,2DAA2D;IAC3D,OAAO,CAAC,aAAa;IAWrB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA+C3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/generators/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAwBrD;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,yCAAyC;IACzC,EAAE,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IAChC,6DAA6D;IAC7D,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,sDAAsD;IACtD,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAoCD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;gBAEhB,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;IAU5D,wBAAwB;IACxB,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,mBAAmB;IACnB,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;;;OAIG;IACH,eAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC;IAMpD;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CxC;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAkDjB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAWpB;;;;OAIG;YACW,oBAAoB;IA+ClC;;;;;;OAMG;YACW,aAAa;IAS3B;;OAEG;YACW,aAAa;IAkF3B;;;OAGG;YACW,mBAAmB;IAgCjC;;;;OAIG;YACW,WAAW;IAsBzB,2DAA2D;IAC3D,OAAO,CAAC,aAAa;IAWrB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA+C3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,oBAAoB;IAI5B,qEAAqE;IAC/D,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAmCzB,QAAQ;IAoBtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IAIlB,2EAA2E;IAC3E,OAAO,CAAC,WAAW;CAiBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,SAAc,EAAE,OAAO,GAAE,UAAe;gBAGnD,MAAM,EAAE;;EAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,GAAE,SAAc,EACtB,OAAO,GAAE,UAAe,UA/iBE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAmjBrD"}
|
package/dist/generators/cli.js
CHANGED
|
@@ -322,22 +322,25 @@ var CLIGenerator = class {
|
|
|
322
322
|
* Recurses arrays/plain objects so a SmrtObject nested in a custom-action
|
|
323
323
|
* result is stripped too; a cycle guard prevents infinite loops.
|
|
324
324
|
*/
|
|
325
|
-
toPublicData(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
325
|
+
toPublicData(value, seen = /* @__PURE__ */ new WeakSet(), options = this.getPublicJsonOptions()) {
|
|
326
326
|
if (value === null || typeof value !== "object") return value;
|
|
327
|
-
if (typeof value.toPublicJSON === "function") return value.toPublicJSON();
|
|
327
|
+
if (typeof value.toPublicJSON === "function") return value.toPublicJSON(options);
|
|
328
328
|
if (Array.isArray(value)) {
|
|
329
329
|
if (seen.has(value)) return value;
|
|
330
330
|
seen.add(value);
|
|
331
|
-
return value.map((entry) => this.toPublicData(entry, seen));
|
|
331
|
+
return value.map((entry) => this.toPublicData(entry, seen, options));
|
|
332
332
|
}
|
|
333
333
|
const proto = Object.getPrototypeOf(value);
|
|
334
334
|
if (proto !== Object.prototype && proto !== null) return value;
|
|
335
335
|
if (seen.has(value)) return value;
|
|
336
336
|
seen.add(value);
|
|
337
337
|
const out = {};
|
|
338
|
-
for (const [key, entry] of Object.entries(value)) out[key] = this.toPublicData(entry, seen);
|
|
338
|
+
for (const [key, entry] of Object.entries(value)) out[key] = this.toPublicData(entry, seen, options);
|
|
339
339
|
return out;
|
|
340
340
|
}
|
|
341
|
+
getPublicJsonOptions() {
|
|
342
|
+
return { permissions: this.context.permissions };
|
|
343
|
+
}
|
|
341
344
|
/** List the runnable commands grouped by object, for help output. */
|
|
342
345
|
async listCommands() {
|
|
343
346
|
const commands = [];
|