@happyvertical/smrt-core 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +38 -0
- package/dist/decorators/compatibility.d.ts +46 -0
- package/dist/decorators/compatibility.d.ts.map +1 -1
- package/dist/decorators/compatibility.js +48 -5
- package/dist/decorators/compatibility.js.map +1 -1
- package/dist/decorators/index.d.ts +119 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +52 -8
- package/dist/decorators/index.js.map +1 -1
- package/dist/generators/custom-action.d.ts +379 -2
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +694 -17
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -2
- package/dist/generators/preflight-route.d.ts +12 -10
- package/dist/generators/preflight-route.d.ts.map +1 -1
- package/dist/generators/preflight-route.js +46 -14
- package/dist/generators/preflight-route.js.map +1 -1
- package/dist/generators/rest.d.ts +44 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +84 -9
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +59 -15
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +125 -53
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +184 -53
- package/dist/postgres-permissions.d.ts +6 -1
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +78 -7
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/shared-state.d.ts +19 -0
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +16 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry.d.ts +136 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +231 -1
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +18 -16
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +59 -6
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/smrt-knowledge.json +33 -32
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +10 -8
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +1 -1
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +121 -51
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.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 +10 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/decorators/index.ts"],"sourcesContent":["/**\n * Field decorators for SMRT objects\n *\n * Modern decorator-based API for defining SMRT object properties.\n * Properties are typed as primitives with decorator metadata.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldUIHints } from '../scanner/types.js';\nimport type { SQLDataType } from '../schema/types.js';\nimport {\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n} from './compatibility.js';\n\nexport type { FieldUIHints } from '../scanner/types.js';\n\n/**\n * Meta type wrapper for STI (Single Table Inheritance) meta fields\n *\n * Fields typed as Meta<T> are stored in the _meta_data JSONB column\n * rather than as direct table columns. Used for child-specific fields\n * in STI hierarchies.\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * // Stored in _meta_data JSONB column\n * roomNumber: Meta<string> = '';\n * attendees: Meta<string[]> = [];\n * }\n * ```\n */\nexport type Meta<T> = T;\n\n/**\n * Base field options\n */\nexport type PrimitiveFieldType =\n | 'text'\n | 'integer'\n | 'decimal'\n | 'boolean'\n | 'datetime'\n | 'json';\n\nexport type FieldType =\n | PrimitiveFieldType\n | 'meta'\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\nexport interface FieldOptions {\n /** Explicit field type for runtime-only registration paths */\n type?: FieldType;\n /** Explicit SQL storage type when runtime and persistence contracts differ */\n sqlType?: SQLDataType;\n /** Whether the field is required */\n required?: boolean;\n /** Default value for the field */\n default?: unknown;\n /** Whether the field is unique */\n unique?: boolean;\n /**\n * When `true`, the schema emits a database index targeting this field.\n *\n * For regular (column-backed) fields the index is a plain column index.\n * For `@meta()` fields stored inside `_meta_data` JSONB, the index targets\n * the JSON path — `json_extract(_meta_data, '$.fieldName')` on SQLite,\n * `(_meta_data->>'fieldName')` on Postgres.\n *\n * Note that `collection.list({ where })` cannot currently reach a JSON-path\n * index: dot-notation keys such as `_meta_data.fieldName` are rejected,\n * because nothing rewrites them into the matching extraction expression\n * (#2276, tracked in #2282). The index is still emitted and still serves\n * hand-written SQL that spells the expression out; it just has no\n * collection-level query to accelerate yet.\n */\n indexed?: boolean;\n /** Whether the field is nullable */\n nullable?: boolean;\n /** Whether the field should be excluded from database */\n transient?: boolean;\n /**\n * Marks the field as sensitive (e.g. API secrets, credentials, tax IDs).\n *\n * Sensitive fields are still persisted to the database, but the framework:\n * - excludes them from `toPublicJSON()` (the serializer used by generated\n * REST/MCP/SvelteKit routes), so they never appear in API responses; and\n * - rejects them as `where`-clause filter keys, closing the\n * `?secret[like]=...` value-probing oracle.\n *\n * Use this for any column that holds a secret value that must never be\n * read back over a generated network surface.\n */\n sensitive?: boolean;\n /**\n * Marks the field as read-only over generated write surfaces.\n *\n * Read-only fields are stripped from the request body before\n * `create`/`update` in generated REST/MCP/SvelteKit routes, so callers\n * cannot mass-assign them. Server-side code can still set them directly.\n */\n readonly?: boolean;\n /**\n * Permission slug required to include this field in public/read responses.\n *\n * Fields with a read permission are fail-closed: generated serializers omit\n * them unless the caller's resolved permission set contains this slug.\n * `sensitive: true` still wins and omits the field for every caller.\n */\n readPermission?: string;\n /** Field description */\n description?: string;\n /**\n * Static UI hints for the field-policy rail (#2046, epic #2045).\n *\n * A pure presentation seed — carried in the manifest under the field's\n * `_meta.ui`, readable at runtime via `getAllFields()` at `field._meta.ui`,\n * and emitted to the browser in generated web-collection definitions. Has no\n * schema, persistence, or security effect; `sensitive`/`readPermission`\n * remain the security rail.\n */\n ui?: FieldUIHints;\n /**\n * Controls whether the field is included in JSON exports.\n * - `true`: Always exported (unless site explicitly excludes it)\n * - `false`: Never exported (cannot be overridden by site config)\n * - `undefined`: Uses site's fieldExportDefault setting\n */\n exported?: boolean;\n}\n\n/**\n * Options for text fields\n */\nexport interface TextFieldOptions extends FieldOptions {\n /** Minimum length for text fields */\n minLength?: number;\n /** Maximum length for text fields */\n maxLength?: number;\n /** Regex pattern for validation */\n pattern?: RegExp | string;\n}\n\n/**\n * Options for numeric fields\n */\nexport interface NumericFieldOptions extends FieldOptions {\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n}\n\n/**\n * Options for relationship fields\n */\nexport interface RelationshipFieldOptions extends FieldOptions {\n /** Related class name */\n related?: string;\n /** Foreign key field name */\n foreignKey?: string;\n /** Through table for many-to-many */\n through?: string;\n /** Relationship type */\n type?: 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany';\n /**\n * What happens to this row when the referenced object is deleted (#2371).\n *\n * Same-package references emit this DB-level action and `SmrtObject.delete()`\n * enforces the same policy in the application layer:\n *\n * - `'CASCADE'` — this row is deleted with the target.\n * - `'SET NULL'` — this column is set to `NULL`. The field must be nullable;\n * declaring it on a `required` field throws `ConfigurationError`.\n * - `'RESTRICT'` — deleting the target throws `DatabaseError` while any row\n * still points at it.\n *\n * When omitted, a column that is part of this class's `conflictColumns`\n * (junction and association rows, which are *identified* by the reference)\n * defaults to `CASCADE`; every other same-package column defaults to\n * immediate `NO ACTION`.\n * `@tenantId()` fields are the one exception: `smrt-tenancy` leads a\n * tenant-scoped class's default `conflictColumns` with the tenant column,\n * but that column scopes ownership rather than identifying the row, so it\n * is never defaulted to `CASCADE` — deleting a `Tenant` must not cascade\n * through every tenant-scoped table.\n *\n * Cascaded rows are removed set-based: their `beforeDelete`/`afterDelete`\n * hooks and interceptors do not run and no change-feed entry is written, the\n * same as a database-level `ON DELETE CASCADE`.\n */\n onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';\n /**\n * Set to `false` only for an app-side relationship whose target identifier\n * must deliberately survive without a database parent row (for example an\n * immutable audit/event record retained after its parent is pruned).\n *\n * The field remains a typed `foreignKey` relationship for loading and\n * indexes, but no physical DDL constraint, schema dependency, or app-side\n * delete action is emitted. The stored identifier is deliberately preserved\n * after the parent is deleted. Ordinary same-package relationships must leave\n * this enabled (the default).\n */\n constraint?:\n | boolean\n | {\n /**\n * Database engines on which SMRT emits the physical constraint.\n *\n * Relationship loading, UUID storage, indexes, and application-side\n * delete enforcement remain active on every engine. Use this narrow\n * allowlist only when an engine cannot faithfully enforce a supported\n * relationship shape (for example a DuckDB self-reference).\n */\n engines: Array<'postgres' | 'sqlite' | 'duckdb' | 'json'>;\n };\n}\n\n/**\n * Options specific to cross-package references.\n */\nexport interface CrossPackageRefOptions\n extends Omit<RelationshipFieldOptions, 'related' | 'type'> {\n /**\n * Storage type for the referenced target id. Defaults to 'uuid'.\n *\n * Use 'text' only when the external target model declares\n * `@smrt({ idType: 'text' })`.\n */\n idType?: 'uuid' | 'text';\n\n /**\n * When `true`, the framework verifies the referenced object exists at save time.\n * Validation uses the target package's manifest (loaded on demand via\n * `ObjectRegistry.ensureManifestLoaded()`), so this requires the target manifest\n * to be discoverable at runtime.\n *\n * Empty/null values are always allowed (treated as \"no reference set\").\n *\n * Defaults to `false` — same behavior as a plain string field today.\n */\n validate?: boolean;\n}\n\n/**\n * Resolve a relationship decorator's target argument to a class name.\n *\n * Accepted target forms:\n * - `'Target'` — class name string; resolves lazily, immune to import cycles.\n * - `Target` — class constructor.\n * - `() => Target` — thunk (inline or a named `const`), invoked here to read\n * the name, so its target must already be initialized.\n *\n * A thunk's own `.name` is `''`, so reading `relatedClass.name` used to register\n * `related: ''` (issue #2379): the field kept `type: 'foreignKey'` but lost its\n * target, which silently dropped the relationship edge, `loadRelated()`, and any\n * FK-derived index. Thunks are therefore invoked at decoration time and an\n * unresolvable target throws with the string form as the remedy — an empty\n * `related` is never registered.\n *\n * Resolution runs inside the field-registration callback, which is the latest\n * point in the decorator lifecycle (after the class binding exists for legacy\n * decorators, and at `@smrt()` application time for standard decorators), so a\n * self-referential `() => Self` thunk resolves rather than hitting the TDZ.\n * A thunk pointing at a class declared LATER in the same module is still in\n * that class's temporal dead zone when the decorators of the earlier class run;\n * that now fails loudly, naming the string form, instead of silently\n * registering an empty target.\n */\nfunction resolveRelatedClassName(\n decoratorName: 'foreignKey' | 'oneToMany' | 'manyToMany',\n relatedClass: string | Function,\n className: string,\n propertyKey: string,\n): string {\n const where = `@${decoratorName}() on ${className}.${propertyKey}`;\n const remedy =\n `Pass the target class name as a string instead — ` +\n `\\`@${decoratorName}('Target')\\` resolves lazily and is immune to import cycles.`;\n\n if (typeof relatedClass === 'string') {\n const name = relatedClass.trim();\n if (!name) {\n throw new Error(\n `${where}: target class name is empty. Pass a class, a class name, or a \\`() => Target\\` thunk.`,\n );\n }\n return name;\n }\n\n if (typeof relatedClass !== 'function') {\n throw new Error(\n `${where}: expected a class, a class name, or a \\`() => Target\\` thunk, received ${relatedClass === null ? 'null' : typeof relatedClass}. ${remedy}`,\n );\n }\n\n // Class/function reference — the common `@foreignKey(Target)` form. Arrow\n // functions have no `prototype`, so a *named* thunk (`const lazyTarget = () =>\n // Target`) is still routed to the thunk branch below instead of registering\n // the variable name as the target class.\n if (relatedClass.name && relatedClass.prototype !== undefined) {\n return relatedClass.name;\n }\n\n // Thunk (`() => Target`, named or inline) — invoke it for the target.\n let resolved: unknown;\n try {\n resolved = (relatedClass as () => unknown)();\n } catch (error) {\n throw new Error(\n `${where}: the \\`() => Target\\` thunk threw while resolving its target (${\n error instanceof Error ? error.message : String(error)\n }). ${remedy}`,\n { cause: error },\n );\n }\n\n if (typeof resolved === 'function' && resolved.name) {\n return resolved.name;\n }\n if (typeof resolved === 'string' && resolved.trim()) {\n return resolved.trim();\n }\n\n throw new Error(\n `${where}: the \\`() => Target\\` thunk resolved to ${\n resolved === null ? 'null' : typeof resolved\n } instead of a named class. ${remedy}`,\n );\n}\n\n/**\n * Marks a class property with validation constraints and metadata options.\n *\n * Use `@field()` when you need options beyond what plain TypeScript initializers\n * express — required validation, numeric ranges, string length limits, uniqueness,\n * or transient (non-persisted) computed properties.\n *\n * For plain persisted fields with no constraints, no decorator is needed: just\n * declare the property with a TypeScript initializer and the framework will infer\n * the column type from the default value (`0` → INTEGER, `0.0` → DECIMAL, `''` → TEXT).\n *\n * @param options - Field configuration options\n * @param options.required - If `true`, `save()` throws `ValidationError` when empty/null\n * @param options.unique - Enforces a UNIQUE database constraint\n * @param options.nullable - If `true`, the column accepts NULL (default depends on type)\n * @param options.transient - If `true`, the property is not persisted to the database\n * @param options.default - Default value applied at the database level\n * @param options.description - Human-readable description used in generated API docs\n * @param options.exported - Controls JSON export visibility (see `FieldOptions`)\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @field({ required: true, maxLength: 100 })\n * name: string = '';\n *\n * @field({ min: 0 })\n * stock: number = 0;\n *\n * @field({ transient: true })\n * get displayPrice(): string { return `$${this.price.toFixed(2)}`; }\n * }\n * ```\n *\n * @see {@link meta} for STI child-specific fields stored in `_meta_data` JSON\n * @see {@link foreignKey} for typed relationship fields\n */\nexport function field(\n options: FieldOptions | NumericFieldOptions | TextFieldOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, options);\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-one (foreign key) relationship to another `SmrtObject` class.\n *\n * The decorated property stores the UUID of the related object. At runtime, call\n * `instance.loadRelated('fieldName')` to lazy-load (and cache) the related object,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading.\n *\n * Cross-package rule: Use `@foreignKey()` only for same-package references.\n * Use `@crossPackageRef()` for cross-package relationships.\n *\n * A named database constraint is emitted on supported engines by default, and\n * the same action is enforced by `SmrtObject.delete()` before the engine sees\n * it. Exceptional archival/audit references that intentionally outlive their\n * parent may declare `{ constraint: false }`; they retain relationship loading\n * and indexing while deliberately preserving the identifier after parent\n * deletion and omitting physical DDL. The\n * decorator also enables `loadRelated()` and eager `include:` loading:\n *\n * ```typescript\n * @foreignKey(Order, { onDelete: 'CASCADE' }) // deleted with the order\n * orderId: string = '';\n *\n * @foreignKey(Customer, { onDelete: 'RESTRICT' }) // blocks the customer delete\n * customerId: string = '';\n * ```\n *\n * Without an `onDelete`, a column that is part of this class's\n * `conflictColumns` defaults to `CASCADE` (this is what cleans up junction\n * rows); any other column defaults to immediate `NO ACTION`.\n *\n * @param relatedClass - The target class constructor, its name as a string, or a\n * `() => Target` thunk. A thunk is **invoked at decoration time**, so its\n * target must already be initialized: a class from an already-evaluated module\n * or the decorated class itself. Use the string form for a class declared\n * later in the same module or reached through an import cycle — it is never\n * evaluated, so it cannot hit the temporal dead zone.\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\n * @throws Error when the target cannot be resolved to a class name (an empty\n * string, a thunk that throws — including on an uninitialized target — or a\n * thunk returning an anonymous value)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * // Same-package FK — enables loadRelated() and eager loading\n * @foreignKey(Customer)\n * customerId: string = '';\n *\n * // Self-reference: the class binding exists when its decorators run\n * @foreignKey(() => Order)\n * parentOrderId: string = '';\n *\n * // Declared later in this module — name string, never evaluated\n * @foreignKey('Invoice')\n * invoiceId: string = '';\n *\n * // Physical constraint only where the engine can enforce this shape;\n * // relationship loading and app-side delete policy remain cross-engine.\n * @foreignKey('Order', {\n * constraint: { engines: ['postgres', 'sqlite'] },\n * })\n * hierarchyParentId: string | null = null;\n * }\n *\n * // Cross-package: runtime relationship, index, and loading; no physical FK\n * @smrt()\n * class Post extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-users:User')\n * authorId: string = '';\n * }\n * ```\n *\n * @see {@link oneToMany} for the inverse (parent) side of the relationship\n * @see SmrtObject.loadRelated for lazy-loading the related object at runtime\n */\nexport function foreignKey(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: resolveRelatedClassName(\n 'foreignKey',\n relatedClass,\n className,\n propertyKey,\n ),\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a cross-package foreign key reference.\n *\n * Use this for relationships that point to a `SmrtObject` in a *different* package\n * (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).\n *\n * Unlike same-package `@foreignKey()`, this emits **no** DDL `FOREIGN KEY`\n * constraint, keeping package schemas independently installable. The property is a\n * `UUID` column on PostgreSQL/DuckDB and `TEXT` on SQLite, matching the target's\n * id type; pass `{ idType: 'text' }` when the target declares\n * `@smrt({ idType: 'text' })`.\n *\n * What you get over a plain string field:\n * - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`\n * and `Collection.list({ include })` can resolve it once the target package's\n * manifest is loaded.\n * - No physical database constraint is emitted; package schemas remain\n * independently installable.\n * - Optional save-time validation (`validate: true`) confirms the referenced\n * object exists, catching typos and stale IDs before they hit the database.\n * - `onDelete` is honoured by `SmrtObject.delete()` when the target package's\n * manifest is loaded in the same runtime.\n *\n * The `qualifiedName` is a fully-qualified class identifier in the form\n * `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.\n *\n * @param qualifiedName - Qualified name of the target class\n * @param options - Optional field constraints and `validate` flag\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Customer extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n * profileId: string = '';\n *\n * // With save-time validation\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile', { validate: true })\n * primaryContactId: string = '';\n * }\n * ```\n *\n * @see {@link foreignKey} for same-package relationships\n * @see SmrtObject.loadRelated for runtime resolution\n */\nexport function crossPackageRef(\n qualifiedName: string,\n options: CrossPackageRefOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'crossPackageRef',\n related: qualifiedName,\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a one-to-many relationship from this object to a collection of related objects.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * At runtime, call `instance.loadRelatedMany('fieldName')` to load the related objects,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading (issues\n * a single batched query for all instances instead of N individual queries).\n *\n * The inverse side (`@foreignKey`) must exist on the `relatedClass` pointing back to this\n * class. The framework discovers it automatically via `ObjectRegistry.getInverseRelationships()`.\n *\n * **Generated accessor (R10):** registering the class installs a consistent\n * `get<FieldName>()` instance method (e.g. `items` → `order.getItems()`) that\n * delegates to `loadRelatedMany('items')`. Generation is additive — a\n * hand-rolled method of the same name is never overwritten.\n *\n * **Disambiguation:** when `relatedClass` declares more than one `@foreignKey`\n * back to this class, pass `{ foreignKey: '<inverseFieldName>' }` so both\n * `loadRelatedMany` and the generated accessor resolve the intended inverse\n * side. Without it the first matching foreign key is used.\n *\n * **Delete behaviour is declared on the child, not here.** `@oneToMany` is a\n * transient read-side accessor; to have children removed with their parent,\n * put `onDelete: 'CASCADE'` on the inverse `@foreignKey` (#2371):\n *\n * ```typescript\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order, { onDelete: 'CASCADE' })\n * orderId: string = '';\n * }\n * ```\n *\n * @param relatedClass - The class constructor of the child/related objects\n * @param options - Optional relationship options. `foreignKey` selects the\n * inverse foreign-key field on `relatedClass` when it has more than one.\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * @oneToMany(OrderItem)\n * items: OrderItem[] = [];\n * }\n *\n * @smrt()\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order)\n * orderId: string = '';\n * }\n *\n * const order = await orders.get({ id });\n * const items = await order.getItems(); // generated; === loadRelatedMany('items')\n * ```\n *\n * @example\n * ```typescript\n * // Multiple inverse foreign keys → disambiguate explicitly.\n * @smrt()\n * class Profile extends SmrtObject {\n * @oneToMany(ProfileRelationship, { foreignKey: 'fromProfileId' })\n * relationshipsFrom: ProfileRelationship[] = [];\n * @oneToMany(ProfileRelationship, { foreignKey: 'toProfileId' })\n * relationshipsTo: ProfileRelationship[] = [];\n * }\n * ```\n *\n * @see {@link foreignKey} for the many-to-one (child) side of the relationship\n * @see SmrtObject.loadRelatedMany for lazy-loading at runtime\n */\nexport function oneToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: resolveRelatedClassName(\n 'oneToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-many relationship between two `SmrtObject` classes via a join table.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * The `through` option specifies the junction table name. The join table model must\n * be decorated with `@smrt({ conflictColumns: ['...', '...'] })` to use the natural\n * key columns for upsert operations.\n *\n * Runtime loading: call `instance.loadRelatedMany('field')` to lazy-load, or\n * pass `include: ['field']` to `collection.list()` for batched eager loading.\n *\n * @param relatedClass - The class constructor of the related objects\n * @param options - Relationship options; `through` specifies the junction table name\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @manyToMany(Tag, { through: 'product_tags' })\n * tags: Tag[] = [];\n * }\n * ```\n *\n * @see {@link oneToMany} for one-to-many relationships\n */\nexport function manyToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: resolveRelatedClassName(\n 'manyToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Marks a field as a Single Table Inheritance (STI) meta field.\n *\n * Meta fields are stored in the `_meta_data` JSONB column on the shared STI\n * table rather than as dedicated table columns. Use this decorator for fields\n * that are specific to an STI child class and should not pollute the shared\n * table schema with child-specific columns.\n *\n * The `@smrt({ tableStrategy: 'sti' })` decorator must be set on the base class.\n * All child-specific fields should use `@meta()` (or the `Meta<T>` type alias).\n *\n * @param options - Standard field options (required, nullable, description, etc.)\n * @returns A TypeScript property decorator (registers field with `type: 'meta'`)\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = ''; // shared column on events table\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * @meta()\n * roomNumber: string = ''; // stored in _meta_data JSON, not a column\n *\n * @meta({ required: true })\n * durationMinutes: number = 60;\n * }\n * ```\n *\n * @see {@link Meta} for the equivalent type alias approach\n * @see {@link field} for regular (non-STI) field declarations\n */\nexport function meta(options: FieldOptions = {}) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'meta', // Mark this field as a meta field for STI\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"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 type {\n CustomActionScope,\n ToolEffect,\n} from '../generators/custom-action.js';\nimport type { ApiHttpMethod } from '../registry/types.js';\nimport { ObjectRegistry } from '../registry.js';\nimport type { FieldUIHints } from '../scanner/types.js';\nimport type { SQLDataType } from '../schema/types.js';\nimport {\n type CompatibleMethodDecorator,\n type CompatibleMethodDecoratorContext,\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n registerCompatibleFieldDecorator,\n registerCompatibleMethodDecorator,\n} from './compatibility.js';\n\nexport type { FieldUIHints } from '../scanner/types.js';\n\n/**\n * Meta type wrapper for STI (Single Table Inheritance) meta fields\n *\n * Fields typed as Meta<T> are stored in the _meta_data JSONB column\n * rather than as direct table columns. Used for child-specific fields\n * in STI hierarchies.\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = '';\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * // Stored in _meta_data JSONB column\n * roomNumber: Meta<string> = '';\n * attendees: Meta<string[]> = [];\n * }\n * ```\n */\nexport type Meta<T> = T;\n\n/**\n * Base field options\n */\nexport type PrimitiveFieldType =\n | 'text'\n | 'integer'\n | 'decimal'\n | 'boolean'\n | 'datetime'\n | 'json';\n\nexport type FieldType =\n | PrimitiveFieldType\n | 'meta'\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\nexport interface FieldOptions {\n /** Explicit field type for runtime-only registration paths */\n type?: FieldType;\n /** Explicit SQL storage type when runtime and persistence contracts differ */\n sqlType?: SQLDataType;\n /** Whether the field is required */\n required?: boolean;\n /** Default value for the field */\n default?: unknown;\n /** Whether the field is unique */\n unique?: boolean;\n /**\n * When `true`, the schema emits a database index targeting this field.\n *\n * For regular (column-backed) fields the index is a plain column index.\n * For `@meta()` fields stored inside `_meta_data` JSONB, the index targets\n * the JSON path — `json_extract(_meta_data, '$.fieldName')` on SQLite,\n * `(_meta_data->>'fieldName')` on Postgres.\n *\n * Note that `collection.list({ where })` cannot currently reach a JSON-path\n * index: dot-notation keys such as `_meta_data.fieldName` are rejected,\n * because nothing rewrites them into the matching extraction expression\n * (#2276, tracked in #2282). The index is still emitted and still serves\n * hand-written SQL that spells the expression out; it just has no\n * collection-level query to accelerate yet.\n */\n indexed?: boolean;\n /** Whether the field is nullable */\n nullable?: boolean;\n /** Whether the field should be excluded from database */\n transient?: boolean;\n /**\n * Marks the field as sensitive (e.g. API secrets, credentials, tax IDs).\n *\n * Sensitive fields are still persisted to the database, but the framework:\n * - excludes them from `toPublicJSON()` (the serializer used by generated\n * REST/MCP/SvelteKit routes), so they never appear in API responses; and\n * - rejects them as `where`-clause filter keys, closing the\n * `?secret[like]=...` value-probing oracle.\n *\n * Use this for any column that holds a secret value that must never be\n * read back over a generated network surface.\n */\n sensitive?: boolean;\n /**\n * Marks the field as read-only over generated write surfaces.\n *\n * Read-only fields are stripped from the request body before\n * `create`/`update` in generated REST/MCP/SvelteKit routes, so callers\n * cannot mass-assign them. Server-side code can still set them directly.\n */\n readonly?: boolean;\n /**\n * Permission slug required to include this field in public/read responses.\n *\n * Fields with a read permission are fail-closed: generated serializers omit\n * them unless the caller's resolved permission set contains this slug.\n * `sensitive: true` still wins and omits the field for every caller.\n */\n readPermission?: string;\n /** Field description */\n description?: string;\n /**\n * Static UI hints for the field-policy rail (#2046, epic #2045).\n *\n * A pure presentation seed — carried in the manifest under the field's\n * `_meta.ui`, readable at runtime via `getAllFields()` at `field._meta.ui`,\n * and emitted to the browser in generated web-collection definitions. Has no\n * schema, persistence, or security effect; `sensitive`/`readPermission`\n * remain the security rail.\n */\n ui?: FieldUIHints;\n /**\n * Controls whether the field is included in JSON exports.\n * - `true`: Always exported (unless site explicitly excludes it)\n * - `false`: Never exported (cannot be overridden by site config)\n * - `undefined`: Uses site's fieldExportDefault setting\n */\n exported?: boolean;\n}\n\n/**\n * Options for text fields\n */\nexport interface TextFieldOptions extends FieldOptions {\n /** Minimum length for text fields */\n minLength?: number;\n /** Maximum length for text fields */\n maxLength?: number;\n /** Regex pattern for validation */\n pattern?: RegExp | string;\n}\n\n/**\n * Options for numeric fields\n */\nexport interface NumericFieldOptions extends FieldOptions {\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n}\n\n/**\n * Options for relationship fields\n */\nexport interface RelationshipFieldOptions extends FieldOptions {\n /** Related class name */\n related?: string;\n /** Foreign key field name */\n foreignKey?: string;\n /** Through table for many-to-many */\n through?: string;\n /** Relationship type */\n type?: 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany';\n /**\n * What happens to this row when the referenced object is deleted (#2371).\n *\n * Same-package references emit this DB-level action and `SmrtObject.delete()`\n * enforces the same policy in the application layer:\n *\n * - `'CASCADE'` — this row is deleted with the target.\n * - `'SET NULL'` — this column is set to `NULL`. The field must be nullable;\n * declaring it on a `required` field throws `ConfigurationError`.\n * - `'RESTRICT'` — deleting the target throws `DatabaseError` while any row\n * still points at it.\n *\n * When omitted, a column that is part of this class's `conflictColumns`\n * (junction and association rows, which are *identified* by the reference)\n * defaults to `CASCADE`; every other same-package column defaults to\n * immediate `NO ACTION`.\n * `@tenantId()` fields are the one exception: `smrt-tenancy` leads a\n * tenant-scoped class's default `conflictColumns` with the tenant column,\n * but that column scopes ownership rather than identifying the row, so it\n * is never defaulted to `CASCADE` — deleting a `Tenant` must not cascade\n * through every tenant-scoped table.\n *\n * Cascaded rows are removed set-based: their `beforeDelete`/`afterDelete`\n * hooks and interceptors do not run and no change-feed entry is written, the\n * same as a database-level `ON DELETE CASCADE`.\n */\n onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';\n /**\n * Set to `false` only for an app-side relationship whose target identifier\n * must deliberately survive without a database parent row (for example an\n * immutable audit/event record retained after its parent is pruned).\n *\n * The field remains a typed `foreignKey` relationship for loading and\n * indexes, but no physical DDL constraint, schema dependency, or app-side\n * delete action is emitted. The stored identifier is deliberately preserved\n * after the parent is deleted. Ordinary same-package relationships must leave\n * this enabled (the default).\n */\n constraint?:\n | boolean\n | {\n /**\n * Database engines on which SMRT emits the physical constraint.\n *\n * Relationship loading, UUID storage, indexes, and application-side\n * delete enforcement remain active on every engine. Use this narrow\n * allowlist only when an engine cannot faithfully enforce a supported\n * relationship shape (for example a DuckDB self-reference).\n */\n engines: Array<'postgres' | 'sqlite' | 'duckdb' | 'json'>;\n };\n}\n\n/**\n * Options specific to cross-package references.\n */\nexport interface CrossPackageRefOptions\n extends Omit<RelationshipFieldOptions, 'related' | 'type'> {\n /**\n * Storage type for the referenced target id. Defaults to 'uuid'.\n *\n * Use 'text' only when the external target model declares\n * `@smrt({ idType: 'text' })`.\n */\n idType?: 'uuid' | 'text';\n\n /**\n * When `true`, the framework verifies the referenced object exists at save time.\n * Validation uses the target package's manifest (loaded on demand via\n * `ObjectRegistry.ensureManifestLoaded()`), so this requires the target manifest\n * to be discoverable at runtime.\n *\n * Empty/null values are always allowed (treated as \"no reference set\").\n *\n * Defaults to `false` — same behavior as a plain string field today.\n */\n validate?: boolean;\n}\n\n/**\n * Resolve a relationship decorator's target argument to a class name.\n *\n * Accepted target forms:\n * - `'Target'` — class name string; resolves lazily, immune to import cycles.\n * - `Target` — class constructor.\n * - `() => Target` — thunk (inline or a named `const`), invoked here to read\n * the name, so its target must already be initialized.\n *\n * A thunk's own `.name` is `''`, so reading `relatedClass.name` used to register\n * `related: ''` (issue #2379): the field kept `type: 'foreignKey'` but lost its\n * target, which silently dropped the relationship edge, `loadRelated()`, and any\n * FK-derived index. Thunks are therefore invoked at decoration time and an\n * unresolvable target throws with the string form as the remedy — an empty\n * `related` is never registered.\n *\n * Resolution runs inside the field-registration callback, which is the latest\n * point in the decorator lifecycle (after the class binding exists for legacy\n * decorators, and at `@smrt()` application time for standard decorators), so a\n * self-referential `() => Self` thunk resolves rather than hitting the TDZ.\n * A thunk pointing at a class declared LATER in the same module is still in\n * that class's temporal dead zone when the decorators of the earlier class run;\n * that now fails loudly, naming the string form, instead of silently\n * registering an empty target.\n */\nfunction resolveRelatedClassName(\n decoratorName: 'foreignKey' | 'oneToMany' | 'manyToMany',\n relatedClass: string | Function,\n className: string,\n propertyKey: string,\n): string {\n const where = `@${decoratorName}() on ${className}.${propertyKey}`;\n const remedy =\n `Pass the target class name as a string instead — ` +\n `\\`@${decoratorName}('Target')\\` resolves lazily and is immune to import cycles.`;\n\n if (typeof relatedClass === 'string') {\n const name = relatedClass.trim();\n if (!name) {\n throw new Error(\n `${where}: target class name is empty. Pass a class, a class name, or a \\`() => Target\\` thunk.`,\n );\n }\n return name;\n }\n\n if (typeof relatedClass !== 'function') {\n throw new Error(\n `${where}: expected a class, a class name, or a \\`() => Target\\` thunk, received ${relatedClass === null ? 'null' : typeof relatedClass}. ${remedy}`,\n );\n }\n\n // Class/function reference — the common `@foreignKey(Target)` form. Arrow\n // functions have no `prototype`, so a *named* thunk (`const lazyTarget = () =>\n // Target`) is still routed to the thunk branch below instead of registering\n // the variable name as the target class.\n if (relatedClass.name && relatedClass.prototype !== undefined) {\n return relatedClass.name;\n }\n\n // Thunk (`() => Target`, named or inline) — invoke it for the target.\n let resolved: unknown;\n try {\n resolved = (relatedClass as () => unknown)();\n } catch (error) {\n throw new Error(\n `${where}: the \\`() => Target\\` thunk threw while resolving its target (${\n error instanceof Error ? error.message : String(error)\n }). ${remedy}`,\n { cause: error },\n );\n }\n\n if (typeof resolved === 'function' && resolved.name) {\n return resolved.name;\n }\n if (typeof resolved === 'string' && resolved.trim()) {\n return resolved.trim();\n }\n\n throw new Error(\n `${where}: the \\`() => Target\\` thunk resolved to ${\n resolved === null ? 'null' : typeof resolved\n } instead of a named class. ${remedy}`,\n );\n}\n\n/**\n * Marks a class property with validation constraints and metadata options.\n *\n * Use `@field()` when you need options beyond what plain TypeScript initializers\n * express — required validation, numeric ranges, string length limits, uniqueness,\n * or transient (non-persisted) computed properties.\n *\n * For plain persisted fields with no constraints, no decorator is needed: just\n * declare the property with a TypeScript initializer and the framework will infer\n * the column type from the default value (`0` → INTEGER, `0.0` → DECIMAL, `''` → TEXT).\n *\n * @param options - Field configuration options\n * @param options.required - If `true`, `save()` throws `ValidationError` when empty/null\n * @param options.unique - Enforces a UNIQUE database constraint\n * @param options.nullable - If `true`, the column accepts NULL (default depends on type)\n * @param options.transient - If `true`, the property is not persisted to the database\n * @param options.default - Default value applied at the database level\n * @param options.description - Human-readable description used in generated API docs\n * @param options.exported - Controls JSON export visibility (see `FieldOptions`)\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @field({ required: true, maxLength: 100 })\n * name: string = '';\n *\n * @field({ min: 0 })\n * stock: number = 0;\n *\n * @field({ transient: true })\n * get displayPrice(): string { return `$${this.price.toFixed(2)}`; }\n * }\n * ```\n *\n * @see {@link meta} for STI child-specific fields stored in `_meta_data` JSON\n * @see {@link foreignKey} for typed relationship fields\n */\nexport function field(\n options: FieldOptions | NumericFieldOptions | TextFieldOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, options);\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-one (foreign key) relationship to another `SmrtObject` class.\n *\n * The decorated property stores the UUID of the related object. At runtime, call\n * `instance.loadRelated('fieldName')` to lazy-load (and cache) the related object,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading.\n *\n * Cross-package rule: Use `@foreignKey()` only for same-package references.\n * Use `@crossPackageRef()` for cross-package relationships.\n *\n * A named database constraint is emitted on supported engines by default, and\n * the same action is enforced by `SmrtObject.delete()` before the engine sees\n * it. Exceptional archival/audit references that intentionally outlive their\n * parent may declare `{ constraint: false }`; they retain relationship loading\n * and indexing while deliberately preserving the identifier after parent\n * deletion and omitting physical DDL. The\n * decorator also enables `loadRelated()` and eager `include:` loading:\n *\n * ```typescript\n * @foreignKey(Order, { onDelete: 'CASCADE' }) // deleted with the order\n * orderId: string = '';\n *\n * @foreignKey(Customer, { onDelete: 'RESTRICT' }) // blocks the customer delete\n * customerId: string = '';\n * ```\n *\n * Without an `onDelete`, a column that is part of this class's\n * `conflictColumns` defaults to `CASCADE` (this is what cleans up junction\n * rows); any other column defaults to immediate `NO ACTION`.\n *\n * @param relatedClass - The target class constructor, its name as a string, or a\n * `() => Target` thunk. A thunk is **invoked at decoration time**, so its\n * target must already be initialized: a class from an already-evaluated module\n * or the decorated class itself. Use the string form for a class declared\n * later in the same module or reached through an import cycle — it is never\n * evaluated, so it cannot hit the temporal dead zone.\n * @param options - Optional field constraints (required, nullable, etc.)\n * @returns A TypeScript property decorator\n * @throws Error when the target cannot be resolved to a class name (an empty\n * string, a thunk that throws — including on an uninitialized target — or a\n * thunk returning an anonymous value)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * // Same-package FK — enables loadRelated() and eager loading\n * @foreignKey(Customer)\n * customerId: string = '';\n *\n * // Self-reference: the class binding exists when its decorators run\n * @foreignKey(() => Order)\n * parentOrderId: string = '';\n *\n * // Declared later in this module — name string, never evaluated\n * @foreignKey('Invoice')\n * invoiceId: string = '';\n *\n * // Physical constraint only where the engine can enforce this shape;\n * // relationship loading and app-side delete policy remain cross-engine.\n * @foreignKey('Order', {\n * constraint: { engines: ['postgres', 'sqlite'] },\n * })\n * hierarchyParentId: string | null = null;\n * }\n *\n * // Cross-package: runtime relationship, index, and loading; no physical FK\n * @smrt()\n * class Post extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-users:User')\n * authorId: string = '';\n * }\n * ```\n *\n * @see {@link oneToMany} for the inverse (parent) side of the relationship\n * @see SmrtObject.loadRelated for lazy-loading the related object at runtime\n */\nexport function foreignKey(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'foreignKey',\n related: resolveRelatedClassName(\n 'foreignKey',\n relatedClass,\n className,\n propertyKey,\n ),\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a cross-package foreign key reference.\n *\n * Use this for relationships that point to a `SmrtObject` in a *different* package\n * (e.g. `Customer.profileId` pointing at `@happyvertical/smrt-profiles:Profile`).\n *\n * Unlike same-package `@foreignKey()`, this emits **no** DDL `FOREIGN KEY`\n * constraint, keeping package schemas independently installable. The property is a\n * `UUID` column on PostgreSQL/DuckDB and `TEXT` on SQLite, matching the target's\n * id type; pass `{ idType: 'text' }` when the target declares\n * `@smrt({ idType: 'text' })`.\n *\n * What you get over a plain string field:\n * - The relationship is registered with the `ObjectRegistry`, so `loadRelated()`\n * and `Collection.list({ include })` can resolve it once the target package's\n * manifest is loaded.\n * - No physical database constraint is emitted; package schemas remain\n * independently installable.\n * - Optional save-time validation (`validate: true`) confirms the referenced\n * object exists, catching typos and stale IDs before they hit the database.\n * - `onDelete` is honoured by `SmrtObject.delete()` when the target package's\n * manifest is loaded in the same runtime.\n *\n * The `qualifiedName` is a fully-qualified class identifier in the form\n * `@package/scope:ClassName` — for example `@happyvertical/smrt-profiles:Profile`.\n *\n * @param qualifiedName - Qualified name of the target class\n * @param options - Optional field constraints and `validate` flag\n * @returns A TypeScript property decorator\n *\n * @example\n * ```typescript\n * @smrt()\n * class Customer extends SmrtObject {\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n * profileId: string = '';\n *\n * // With save-time validation\n * @crossPackageRef('@happyvertical/smrt-profiles:Profile', { validate: true })\n * primaryContactId: string = '';\n * }\n * ```\n *\n * @see {@link foreignKey} for same-package relationships\n * @see SmrtObject.loadRelated for runtime resolution\n */\nexport function crossPackageRef(\n qualifiedName: string,\n options: CrossPackageRefOptions = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'crossPackageRef',\n related: qualifiedName,\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a one-to-many relationship from this object to a collection of related objects.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * At runtime, call `instance.loadRelatedMany('fieldName')` to load the related objects,\n * or pass `include: ['fieldName']` to `collection.list()` for batch eager loading (issues\n * a single batched query for all instances instead of N individual queries).\n *\n * The inverse side (`@foreignKey`) must exist on the `relatedClass` pointing back to this\n * class. The framework discovers it automatically via `ObjectRegistry.getInverseRelationships()`.\n *\n * **Generated accessor (R10):** registering the class installs a consistent\n * `get<FieldName>()` instance method (e.g. `items` → `order.getItems()`) that\n * delegates to `loadRelatedMany('items')`. Generation is additive — a\n * hand-rolled method of the same name is never overwritten.\n *\n * **Disambiguation:** when `relatedClass` declares more than one `@foreignKey`\n * back to this class, pass `{ foreignKey: '<inverseFieldName>' }` so both\n * `loadRelatedMany` and the generated accessor resolve the intended inverse\n * side. Without it the first matching foreign key is used.\n *\n * **Delete behaviour is declared on the child, not here.** `@oneToMany` is a\n * transient read-side accessor; to have children removed with their parent,\n * put `onDelete: 'CASCADE'` on the inverse `@foreignKey` (#2371):\n *\n * ```typescript\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order, { onDelete: 'CASCADE' })\n * orderId: string = '';\n * }\n * ```\n *\n * @param relatedClass - The class constructor of the child/related objects\n * @param options - Optional relationship options. `foreignKey` selects the\n * inverse foreign-key field on `relatedClass` when it has more than one.\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Order extends SmrtObject {\n * @oneToMany(OrderItem)\n * items: OrderItem[] = [];\n * }\n *\n * @smrt()\n * class OrderItem extends SmrtObject {\n * @foreignKey(Order)\n * orderId: string = '';\n * }\n *\n * const order = await orders.get({ id });\n * const items = await order.getItems(); // generated; === loadRelatedMany('items')\n * ```\n *\n * @example\n * ```typescript\n * // Multiple inverse foreign keys → disambiguate explicitly.\n * @smrt()\n * class Profile extends SmrtObject {\n * @oneToMany(ProfileRelationship, { foreignKey: 'fromProfileId' })\n * relationshipsFrom: ProfileRelationship[] = [];\n * @oneToMany(ProfileRelationship, { foreignKey: 'toProfileId' })\n * relationshipsTo: ProfileRelationship[] = [];\n * }\n * ```\n *\n * @see {@link foreignKey} for the many-to-one (child) side of the relationship\n * @see SmrtObject.loadRelatedMany for lazy-loading at runtime\n */\nexport function oneToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'oneToMany',\n related: resolveRelatedClassName(\n 'oneToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Declares a many-to-many relationship between two `SmrtObject` classes via a join table.\n *\n * The decorated property is `transient` — it is not persisted as a database column.\n * The `through` option specifies the junction table name. The join table model must\n * be decorated with `@smrt({ conflictColumns: ['...', '...'] })` to use the natural\n * key columns for upsert operations.\n *\n * Runtime loading: call `instance.loadRelatedMany('field')` to lazy-load, or\n * pass `include: ['field']` to `collection.list()` for batched eager loading.\n *\n * @param relatedClass - The class constructor of the related objects\n * @param options - Relationship options; `through` specifies the junction table name\n * @returns A TypeScript property decorator (sets `transient: true` automatically)\n *\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * @manyToMany(Tag, { through: 'product_tags' })\n * tags: Tag[] = [];\n * }\n * ```\n *\n * @see {@link oneToMany} for one-to-many relationships\n */\nexport function manyToMany(\n relatedClass: string | Function,\n options: Omit<RelationshipFieldOptions, 'related'> = {},\n) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'manyToMany',\n related: resolveRelatedClassName(\n 'manyToMany',\n relatedClass,\n className,\n propertyKey,\n ),\n transient: true, // Relationship fields are not database columns\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Marks a field as a Single Table Inheritance (STI) meta field.\n *\n * Meta fields are stored in the `_meta_data` JSONB column on the shared STI\n * table rather than as dedicated table columns. Use this decorator for fields\n * that are specific to an STI child class and should not pollute the shared\n * table schema with child-specific columns.\n *\n * The `@smrt({ tableStrategy: 'sti' })` decorator must be set on the base class.\n * All child-specific fields should use `@meta()` (or the `Meta<T>` type alias).\n *\n * @param options - Standard field options (required, nullable, description, etc.)\n * @returns A TypeScript property decorator (registers field with `type: 'meta'`)\n *\n * @example\n * ```typescript\n * @smrt({ tableStrategy: 'sti' })\n * class Event extends SmrtObject {\n * title: string = ''; // shared column on events table\n * }\n *\n * @smrt()\n * class Meeting extends Event {\n * @meta()\n * roomNumber: string = ''; // stored in _meta_data JSON, not a column\n *\n * @meta({ required: true })\n * durationMinutes: number = 60;\n * }\n * ```\n *\n * @see {@link Meta} for the equivalent type alias approach\n * @see {@link field} for regular (non-STI) field declarations\n */\nexport function meta(options: FieldOptions = {}) {\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<unknown, unknown>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n ...options,\n type: 'meta', // Mark this field as a meta field for STI\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n\n/**\n * Options for {@link method}.\n *\n * Every option is optional, and an omitted one falls back to the class-level\n * `api.routes[methodName]` entry (or `ai.descriptions[methodName]` for\n * `description`) when one exists. The merge is FIELD BY FIELD, so adding\n * `@method({ description })` to a class that already declares a route verb and\n * path does not reset them (#2686).\n */\nexport interface MethodOptions {\n /**\n * Override the default routing decision.\n *\n * Public methods are routed by default when they are WIRE-ABLE: every\n * parameter can be built from a JSON request body or query string. Set\n * `false` to withhold a method the heuristic accepted, `true` to expose one\n * it rejected.\n *\n * `true` bypasses the heuristic and NOTHING else. It cannot manufacture a\n * receiver, undo `api: false`, cross an `include`/`exclude` boundary, reach a\n * non-public method, claim a CRUD verb, or hydrate a value the transport\n * cannot build — a model-instance parameter still arrives as whatever JSON\n * the caller sent.\n */\n expose?: boolean;\n\n /**\n * Why the method is withheld. Recorded in the knowledge artifact so\n * `smrt doctor` and agents read an explanation instead of silence.\n */\n reason?: string;\n\n /**\n * HTTP verb for the generated route. Defaults to `POST`.\n *\n * Named `httpMethod`, not `method`: RFC 9110 calls this the request\n * \"method\", so the protocol word is exactly the one that collides with the\n * decorator's own name, and `@method({ method: 'POST' })` stutters.\n * `httpMethod` is also already this framework's name for it — `ApiHttpMethod`\n * is carried across the whole discovery/invoke path.\n *\n * Migrates from `api.routes[methodName].method`.\n */\n httpMethod?: ApiHttpMethod;\n\n /**\n * Route path segment(s), used verbatim and split on `/`.\n * Defaults to the method name. Migrates from `api.routes[methodName].path`.\n */\n path?: string;\n\n /**\n * Declared receiver scope. Migrates from `api.routes[methodName].scope`.\n *\n * A DECLARATION checked against the method's executable receiver, not a\n * relocation: an instance method is item-scoped and a static (or\n * collection-class) method is collection-scoped, and no config can change\n * that. A contradicting value is reported at build time and ignored.\n */\n scope?: CustomActionScope;\n\n /**\n * Agent-visible effect classification. Omitted actions are treated as\n * `destructive`, so missing metadata can never widen browser capability\n * exposure. Migrates from `api.routes[methodName].effect`.\n */\n effect?: ToolEffect;\n\n /** Whether repeating the action with the same arguments is safe. */\n idempotent?: boolean;\n\n /** Whether the action may interact outside the SMRT application. */\n openWorld?: boolean;\n\n /**\n * Description used by AI/tool surfaces, overriding the method's JSDoc.\n * Migrates from `ai.descriptions[methodName]`.\n */\n description?: string;\n}\n\n/**\n * Refines how a method is exposed on generated surfaces.\n *\n * The method counterpart of {@link field}, and symmetric with it: a manifest\n * records exactly two collections per object, `fields` and `methods`. Neither\n * decorator DECLARES its member — a property is a field because it is a\n * property, and a method is a candidate action because it is public — both\n * REFINE what the framework already inferred. `@field()` refines an inferred\n * column type and constraints; `@method()` refines inferred exposure, its\n * route shape, and its tool semantics.\n *\n * The name is `@method()` rather than `@action()` because of the negative\n * case: withholding a method has to read correctly, and\n * `@action({ expose: false })` contradicts itself — declaring something an\n * action in order to say it is not one. \"Action\" keeps its established meaning\n * in the generators (a non-CRUD method exposed on a surface); this decorator\n * is how a method BECOMES one.\n *\n * Metadata only: the decorated method is not wrapped and keeps its identity,\n * so it stays directly callable.\n *\n * @example Shape the generated route\n * ```typescript\n * @method({ httpMethod: 'POST', path: 'reviews', effect: 'write' })\n * async runReview(options?: RunContentReviewOptions) { … }\n * ```\n *\n * @example Withhold a method the heuristic would have routed\n * ```typescript\n * @method({ expose: false, reason: 'callback registration, not a wire operation' })\n * static registerValidator(validator: ValidatorFunction) { … }\n * ```\n *\n * @example Force a route the heuristic rejected\n * ```typescript\n * // `expose: true` bypasses the heuristic only — this method must still\n * // accept whatever JSON the caller sends for `asset`.\n * @method({ expose: true })\n * async addAsset(asset: Asset) { … }\n * ```\n *\n * @see {@link field} for the property-side refinement decorator\n */\nexport function method(options: MethodOptions = {}) {\n return ((\n targetOrValue: unknown,\n propertyKeyOrContext: CompatibleMethodDecoratorContext<\n unknown,\n // biome-ignore lint/suspicious/noExplicitAny: mirrors the lib's own `ClassMethodDecoratorContext` constraint; see `AnyMethodOf` in `compatibility.ts`. S4 #1579.\n (this: unknown, ...args: any) => any\n >,\n ) => {\n registerCompatibleMethodDecorator(\n targetOrValue as LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext,\n (ctor, methodName, isStatic) => {\n ObjectRegistry.registerMethodDecorator(\n ctor,\n methodName,\n options,\n isStatic,\n );\n },\n );\n }) as CompatibleMethodDecorator;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ToolEffect } from '../registry/types.js';
|
|
2
|
-
import { MethodDefinition } from '../scanner/types.js';
|
|
1
|
+
import { ApiHttpMethod, ToolEffect } from '../registry/types.js';
|
|
2
|
+
import { MethodDefinition, SmartObjectManifest } from '../scanner/types.js';
|
|
3
3
|
export type CustomActionScope = 'item' | 'collection';
|
|
4
4
|
export type { ToolEffect } from '../registry/types.js';
|
|
5
5
|
/**
|
|
@@ -279,6 +279,11 @@ export interface ResolveCustomActionMetadataOptions {
|
|
|
279
279
|
method?: {
|
|
280
280
|
isStatic?: boolean;
|
|
281
281
|
parameters?: MethodDefinition['parameters'];
|
|
282
|
+
/**
|
|
283
|
+
* The method's `@method()` config, whose options win field by field over
|
|
284
|
+
* the class-level `api.routes` entry for the same action (#2686).
|
|
285
|
+
*/
|
|
286
|
+
decoratorConfig?: Record<string, unknown>;
|
|
282
287
|
};
|
|
283
288
|
apiConfig?: unknown;
|
|
284
289
|
/** Collection-class actions have a collection receiver even when non-static. */
|
|
@@ -292,6 +297,23 @@ type ToolArgs = Record<string, unknown>;
|
|
|
292
297
|
* schema; runtime callers may still provide their legacy collection fallback.
|
|
293
298
|
*/
|
|
294
299
|
export declare function resolveCustomActionMetadata(options: ResolveCustomActionMetadataOptions): ResolvedCustomActionMetadata;
|
|
300
|
+
/**
|
|
301
|
+
* The declared scope, when it contradicts the receiver the method actually
|
|
302
|
+
* has; `undefined` when there is no declaration or it agrees.
|
|
303
|
+
*
|
|
304
|
+
* A scope is a DECLARATION about a method, not a relocation of it: nothing in
|
|
305
|
+
* a config can move an instance method onto the class. Silently ignoring a
|
|
306
|
+
* contradiction leaves an author believing a route exists at a collection URL
|
|
307
|
+
* that was never written, so the generators report this at build time
|
|
308
|
+
* (#2686).
|
|
309
|
+
*/
|
|
310
|
+
export declare function resolveDeclaredScopeMismatch(options: {
|
|
311
|
+
actionName: string;
|
|
312
|
+
method?: ExposableMethod;
|
|
313
|
+
apiConfig?: unknown;
|
|
314
|
+
/** The receiver-derived scope, as the emitter computed it. */
|
|
315
|
+
effectiveScope: CustomActionScope;
|
|
316
|
+
}): CustomActionScope | undefined;
|
|
295
317
|
/** Build the custom-action portion of an MCP/WebMCP JSON Schema. */
|
|
296
318
|
export declare function buildCustomActionInputSchema(metadata: CustomActionMetadata): JsonSchema;
|
|
297
319
|
/**
|
|
@@ -322,4 +344,359 @@ export declare const SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY = "io.happyvertical/s
|
|
|
322
344
|
* REST callers receive non-2xx semantics even when an adapter omits it.
|
|
323
345
|
*/
|
|
324
346
|
export declare function normalizeCustomActionFailure(value: unknown): CustomActionFailure | undefined;
|
|
347
|
+
/**
|
|
348
|
+
* The `@method()` decorator's options, as they reach a consumer.
|
|
349
|
+
*
|
|
350
|
+
* Narrowed from the manifest's untyped `MethodDefinition.decoratorConfig` by
|
|
351
|
+
* {@link readMethodDecoratorConfig}. The authoring type is `MethodOptions` in
|
|
352
|
+
* `decorators/index.ts`; this is the read side, and it is deliberately
|
|
353
|
+
* defensive — every field is validated, and a malformed one is dropped rather
|
|
354
|
+
* than trusted, the same stance `readConfiguredToolMetadata` takes for a
|
|
355
|
+
* scanned `api.routes` entry.
|
|
356
|
+
*/
|
|
357
|
+
export interface MethodDecoratorConfig {
|
|
358
|
+
/**
|
|
359
|
+
* `false` withholds a method the wire-ability heuristic accepted; `true`
|
|
360
|
+
* exposes one it rejected.
|
|
361
|
+
*
|
|
362
|
+
* `true` bypasses the HEURISTIC only. It cannot manufacture a receiver, undo
|
|
363
|
+
* `api: false`, escape an `include`/`exclude` boundary, reach a non-public
|
|
364
|
+
* method, or claim a CRUD verb the generated operation already owns — and it
|
|
365
|
+
* does not hydrate a parameter the transport cannot build (a model instance
|
|
366
|
+
* still arrives as whatever JSON the caller sent).
|
|
367
|
+
*/
|
|
368
|
+
expose?: boolean;
|
|
369
|
+
/** Why the method is withheld. Reported by the knowledge artifact. */
|
|
370
|
+
reason?: string;
|
|
371
|
+
/** HTTP verb for the generated route. Migrates from `api.routes[m].method`. */
|
|
372
|
+
httpMethod?: ApiHttpMethod;
|
|
373
|
+
/** Route path segment(s). Migrates from `api.routes[m].path`. */
|
|
374
|
+
path?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Declared receiver scope. Migrates from `api.routes[m].scope`.
|
|
377
|
+
*
|
|
378
|
+
* DECLARATIVE, not relocating: the executable receiver decides (an instance
|
|
379
|
+
* method is item-scoped, a static or collection-class method is
|
|
380
|
+
* collection-scoped), exactly as `api.routes[m].scope` already behaves. A
|
|
381
|
+
* mismatch keeps the receiver and reports a diagnostic.
|
|
382
|
+
*/
|
|
383
|
+
scope?: CustomActionScope;
|
|
384
|
+
/** Browser/agent-visible effect. Migrates from `api.routes[m].effect`. */
|
|
385
|
+
effect?: ToolEffect;
|
|
386
|
+
/** Whether repeating the action with the same arguments is safe. */
|
|
387
|
+
idempotent?: boolean;
|
|
388
|
+
/** Whether the action may interact outside the SMRT application. */
|
|
389
|
+
openWorld?: boolean;
|
|
390
|
+
/** AI/tool description. Migrates from `ai.descriptions[m]`. */
|
|
391
|
+
description?: string;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The only parameter facts the wire-ability test reads.
|
|
395
|
+
*
|
|
396
|
+
* Deliberately looser than the manifest's `MethodParameterDefinition`: callers
|
|
397
|
+
* outside the vite plugin hold their own structural view of a registered
|
|
398
|
+
* method (`@happyvertical/smrt-users`' `MethodLike`, for one) and must be able
|
|
399
|
+
* to ask this question without first widening their type to the manifest's.
|
|
400
|
+
*/
|
|
401
|
+
export interface WireableParameter {
|
|
402
|
+
name: string;
|
|
403
|
+
type?: string | undefined;
|
|
404
|
+
/** See `MethodParameterDefinition.typeUnresolved`. */
|
|
405
|
+
typeUnresolved?: boolean | undefined;
|
|
406
|
+
/** See `MethodParameterDefinition.memberTypes`. */
|
|
407
|
+
memberTypes?: readonly string[] | undefined;
|
|
408
|
+
/**
|
|
409
|
+
* See `MethodParameterDefinition.unionBranches`. Preferred over
|
|
410
|
+
* `memberTypes` when present: it keeps each union branch's members attached
|
|
411
|
+
* to that branch instead of flattening them together (#2686).
|
|
412
|
+
*/
|
|
413
|
+
unionBranches?: readonly {
|
|
414
|
+
type: string;
|
|
415
|
+
memberTypes?: readonly string[] | undefined;
|
|
416
|
+
}[] | undefined;
|
|
417
|
+
}
|
|
418
|
+
/** Minimal method shape the exposure resolver reads. */
|
|
419
|
+
export interface ExposableMethod {
|
|
420
|
+
isPublic?: boolean;
|
|
421
|
+
isStatic?: boolean;
|
|
422
|
+
parameters?: readonly WireableParameter[] | undefined;
|
|
423
|
+
decoratorConfig?: Record<string, unknown>;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Read and validate the `@method()` config the scanner put on a manifest
|
|
427
|
+
* method. Returns `undefined` for an undecorated method.
|
|
428
|
+
*/
|
|
429
|
+
export declare function readMethodDecoratorConfig(method: ExposableMethod | undefined): MethodDecoratorConfig | undefined;
|
|
430
|
+
/** Options that let the wire-ability test consult the surrounding manifest. */
|
|
431
|
+
export interface WireabilityOptions {
|
|
432
|
+
/**
|
|
433
|
+
* True when `name` identifies a class the manifest knows about — a model,
|
|
434
|
+
* collection, or junction. Such a parameter wants a live instance with
|
|
435
|
+
* methods and a database binding; JSON cannot produce one.
|
|
436
|
+
*
|
|
437
|
+
* OPTIONAL, and its absence is a documented widening: without a class
|
|
438
|
+
* inventory the test cannot distinguish `Asset` from `AssetOptions`, so it
|
|
439
|
+
* accepts both — and because every OTHER rejection still applies, the caller
|
|
440
|
+
* then disagrees with the emitters on exactly the largest group of withheld
|
|
441
|
+
* methods. Build it with {@link createManifestClassNamePredicate} from a
|
|
442
|
+
* manifest, or {@link createClassNamePredicate} from a live registry's class
|
|
443
|
+
* names. The parameter stays optional only because `resolveApiActionSet`'s
|
|
444
|
+
* arguments are public API and optional there.
|
|
445
|
+
*/
|
|
446
|
+
isModelClassName?: (name: string) => boolean;
|
|
447
|
+
}
|
|
448
|
+
/** Result of testing one method (or one parameter) for wire-ability. */
|
|
449
|
+
export interface WireabilityVerdict {
|
|
450
|
+
wireable: boolean;
|
|
451
|
+
/** Present only when `wireable` is false. */
|
|
452
|
+
reason?: string;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Build the `isModelClassName` predicate {@link classifyMethodWireability}
|
|
456
|
+
* needs, from a manifest.
|
|
457
|
+
*
|
|
458
|
+
* Both the SIMPLE and QUALIFIED name of every manifest class are registered: a
|
|
459
|
+
* parameter is annotated with the simple name in source, but a qualified name
|
|
460
|
+
* can reach the predicate through a `TSQualifiedName` annotation.
|
|
461
|
+
*
|
|
462
|
+
* Returns `undefined` for a missing manifest, which widens the gate — see
|
|
463
|
+
* {@link WireabilityOptions.isModelClassName}.
|
|
464
|
+
*/
|
|
465
|
+
export declare function createManifestClassNamePredicate(manifest: SmartObjectManifest | undefined): ((name: string) => boolean) | undefined;
|
|
466
|
+
/**
|
|
467
|
+
* The same predicate from a bare name list, for a caller whose class inventory
|
|
468
|
+
* is the live `ObjectRegistry` rather than a manifest — notably
|
|
469
|
+
* `@happyvertical/smrt-users`' CLI resource listing, which iterates
|
|
470
|
+
* `ObjectRegistry.getAllClasses()` and has no manifest to hand.
|
|
471
|
+
*
|
|
472
|
+
* Exported so that caller does not grow its own copy: without a predicate the
|
|
473
|
+
* gate half-applies (every rejection EXCEPT model instances), which is worse
|
|
474
|
+
* than either extreme because the consumer then disagrees with the emitters on
|
|
475
|
+
* exactly the largest group of withheld methods.
|
|
476
|
+
*/
|
|
477
|
+
export declare function createClassNamePredicate(names: Iterable<string>): (name: string) => boolean;
|
|
478
|
+
/**
|
|
479
|
+
* Whether one declared parameter can be built from a JSON request body or
|
|
480
|
+
* query string.
|
|
481
|
+
*/
|
|
482
|
+
export declare function classifyParameterWireability(parameter: WireableParameter, options?: WireabilityOptions): WireabilityVerdict;
|
|
483
|
+
/**
|
|
484
|
+
* Whether every declared parameter of a method can be built from a JSON
|
|
485
|
+
* request body or query string.
|
|
486
|
+
*
|
|
487
|
+
* A method with NO manifest parameter metadata is wire-able: that is the
|
|
488
|
+
* legacy options-bag contract every transport already supports, and absent
|
|
489
|
+
* metadata is not evidence of a hostile signature.
|
|
490
|
+
*/
|
|
491
|
+
export declare function classifyMethodWireability(method: Pick<ExposableMethod, 'parameters'>, options?: WireabilityOptions): WireabilityVerdict;
|
|
492
|
+
/**
|
|
493
|
+
* Why a public method is not reachable as a generated API action.
|
|
494
|
+
*
|
|
495
|
+
* Machine-readable so callers can react differently per cause: the route
|
|
496
|
+
* emitters warn on `no-receiver` (a configuration mistake worth shouting
|
|
497
|
+
* about) and stay quiet on the rest, while the knowledge artifact reports the
|
|
498
|
+
* accompanying `reason` text for every one of them (#2686).
|
|
499
|
+
*/
|
|
500
|
+
export type ApiMethodRejectionCode = 'api-disabled' | 'crud-reserved' | 'not-public' | 'lifecycle-method' | 'excluded' | 'not-included' | 'withheld' | 'not-wireable' | 'no-receiver';
|
|
501
|
+
/** Verdict of {@link resolveApiMethodExposure}. */
|
|
502
|
+
export interface ApiMethodExposure {
|
|
503
|
+
exposed: boolean;
|
|
504
|
+
/** Present only when `exposed` is false. */
|
|
505
|
+
code?: ApiMethodRejectionCode;
|
|
506
|
+
/** Human-readable explanation, present only when `exposed` is false. */
|
|
507
|
+
reason?: string;
|
|
508
|
+
}
|
|
509
|
+
export interface ResolveApiMethodExposureOptions extends WireabilityOptions {
|
|
510
|
+
actionName: string;
|
|
511
|
+
method: ExposableMethod;
|
|
512
|
+
/** The class's scanned `api` config (`decoratorConfig.api`). */
|
|
513
|
+
apiConfig?: unknown;
|
|
514
|
+
/**
|
|
515
|
+
* True when the HOST is a collection class, which emits only
|
|
516
|
+
* collection-scoped routes. Drives the receiver check.
|
|
517
|
+
*/
|
|
518
|
+
isCollectionClass?: boolean;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The single decision every generated-API consumer asks: is this method
|
|
522
|
+
* reachable as a custom REST action, and if not, why?
|
|
523
|
+
*
|
|
524
|
+
* ONE resolver, four consumers — both SvelteKit route emitters
|
|
525
|
+
* (`generateRoutesForObject`, `generateCollectionRoutesForObject`), the
|
|
526
|
+
* cli↔api coherence resolver (`resolveApiActionSet`), and the knowledge
|
|
527
|
+
* artifact's API projection. They previously each re-derived a subset: the
|
|
528
|
+
* emitters filtered on `shouldIncludeInApi` plus their own receiver skip,
|
|
529
|
+
* `resolveApiActionSet` mirrored both, and `knowledge.ts` mirrored the
|
|
530
|
+
* receiver half a third time. A gate added to only one of them would report a
|
|
531
|
+
* method as unavailable while still writing its route file, which is the exact
|
|
532
|
+
* incoherence this issue exists to close (#2686).
|
|
533
|
+
*
|
|
534
|
+
* Order matters, and is the tested precedence contract:
|
|
535
|
+
*
|
|
536
|
+
* 1. `api: false` — the class has no REST surface at all.
|
|
537
|
+
* 2. A CRUD verb — the generated operation already owns the name (#2646).
|
|
538
|
+
* 3. Non-public — never a surface.
|
|
539
|
+
* 4. A framework lifecycle method (`save`, `initialize`, `toJSON`, ...) — the
|
|
540
|
+
* mechanism behind generated CRUD, not a distinct operation, even when a
|
|
541
|
+
* subclass declares its own override. `CLIGenerator` and `MCPGenerator`
|
|
542
|
+
* already gate on this; REST did not, and this is where it joins them
|
|
543
|
+
* (#2638, #2657).
|
|
544
|
+
* 5. `api.exclude` — an explicit withdrawal.
|
|
545
|
+
* 6. `api.include` — an explicit allowlist boundary.
|
|
546
|
+
* 7. `@method({ expose: false })` — an explicit withdrawal that outranks every
|
|
547
|
+
* remaining rule, including a legacy `api.routes` entry for the same
|
|
548
|
+
* method. This is why the decorator is `@method()` and not `@action()`:
|
|
549
|
+
* declaring something an action in order to say it is not one contradicts
|
|
550
|
+
* itself.
|
|
551
|
+
* 8. Explicit legacy exposure — a name listed in `api.include` or carrying an
|
|
552
|
+
* `api.routes` entry is a DECLARATION that this method is a route, made
|
|
553
|
+
* before the heuristic existed. It bypasses the heuristic. This is the
|
|
554
|
+
* documented compatibility exception that makes "nothing breaks" true for
|
|
555
|
+
* the 42 existing route entries; without it, migrating a class to the new
|
|
556
|
+
* gate could silently drop a route its author had spelled out.
|
|
557
|
+
* 9. `@method({ expose: true })` — bypasses the heuristic, and NOTHING else.
|
|
558
|
+
* It cannot manufacture a receiver (step 10 still applies), reach a
|
|
559
|
+
* non-public method, or hydrate a parameter the transport cannot build.
|
|
560
|
+
* 10. Wire-ability — every parameter must be constructible from JSON.
|
|
561
|
+
* 11. Receiver — a collection class emits only collection-scoped routes, and a
|
|
562
|
+
* model class cannot host a collection-scoped instance method.
|
|
563
|
+
*/
|
|
564
|
+
export declare function resolveApiMethodExposure(options: ResolveApiMethodExposureOptions): ApiMethodExposure;
|
|
565
|
+
/**
|
|
566
|
+
* The effective route/tool metadata for one custom action, with `@method()`
|
|
567
|
+
* winning FIELD BY FIELD over the class-level `api.routes` map and
|
|
568
|
+
* `ai.descriptions`.
|
|
569
|
+
*
|
|
570
|
+
* Field-by-field, not wholesale: `@method({ description: '...' })` on a class
|
|
571
|
+
* that already declares `routes: { runReview: { method: 'POST', path:
|
|
572
|
+
* 'reviews' } }` must not silently reset that verb and path to their defaults.
|
|
573
|
+
* Only options the decorator actually supplies override their legacy
|
|
574
|
+
* counterparts (#2686).
|
|
575
|
+
*/
|
|
576
|
+
export interface EffectiveActionMetadata {
|
|
577
|
+
httpMethod?: ApiHttpMethod;
|
|
578
|
+
path?: string;
|
|
579
|
+
scope?: CustomActionScope;
|
|
580
|
+
effect?: ToolEffect;
|
|
581
|
+
idempotent?: boolean;
|
|
582
|
+
openWorld?: boolean;
|
|
583
|
+
description?: string;
|
|
584
|
+
}
|
|
585
|
+
export declare function resolveEffectiveActionMetadata(options: {
|
|
586
|
+
actionName: string;
|
|
587
|
+
method?: ExposableMethod;
|
|
588
|
+
apiConfig?: unknown;
|
|
589
|
+
aiConfig?: unknown;
|
|
590
|
+
}): EffectiveActionMetadata;
|
|
591
|
+
/**
|
|
592
|
+
* Whether a method's `@method()` declaration is also a RUNTIME REST route
|
|
593
|
+
* declaration, the way an `api.routes[m]` entry is.
|
|
594
|
+
*
|
|
595
|
+
* The runtime `APIGenerator` transport is deliberately declaration-gated: it
|
|
596
|
+
* serves a custom collection action only where one was declared, because its URL
|
|
597
|
+
* shape supports a single segment and an undeclared public method has never had
|
|
598
|
+
* a route there. `dispatchCustomCollectionAction` and the `isRestActionRoutable`
|
|
599
|
+
* preflight prediction must agree on that gate exactly, so both read this (#2686).
|
|
600
|
+
*
|
|
601
|
+
* True for any option that migrates from `ApiCustomRouteConfig` — its complete
|
|
602
|
+
* field set is `scope`, `method`, `path`, `effect`, `idempotent`, `openWorld` —
|
|
603
|
+
* because a legacy `routes: { m: { effect: 'write' } }` entry with no path or
|
|
604
|
+
* verb already dispatches at `POST /<collection>/m`, and migrating it onto the
|
|
605
|
+
* method must not silently delete that endpoint. Also true for an explicit
|
|
606
|
+
* `expose: true`, which is a stronger statement that the method is an action
|
|
607
|
+
* than an empty route entry is.
|
|
608
|
+
*
|
|
609
|
+
* FALSE for a bare `@method()` and for a `description`-only one. Neither
|
|
610
|
+
* migrates from a route entry — `description` migrates from `ai.descriptions`,
|
|
611
|
+
* and a bare decorator is a review marker — so counting them would hand the
|
|
612
|
+
* runtime transport endpoints it never served.
|
|
613
|
+
*/
|
|
614
|
+
export declare function declaresRuntimeRestRoute(method: ExposableMethod | undefined): boolean;
|
|
615
|
+
/**
|
|
616
|
+
* Whether the author WROTE a runtime REST route declaration on this method,
|
|
617
|
+
* ignoring whether they then withheld it.
|
|
618
|
+
*
|
|
619
|
+
* Deliberately distinct from {@link declaresRuntimeRestRoute}: the dispatcher
|
|
620
|
+
* must still SEE a withheld declaration in order to refuse it explicitly. This
|
|
621
|
+
* router resolves `POST /<collection>/<segment>` to `create` when nothing
|
|
622
|
+
* claims the segment, so dropping a withheld action from the candidate set
|
|
623
|
+
* would turn a request aimed at an explicitly withheld operation into a silent
|
|
624
|
+
* row insert. The candidate set reads this; the preflight PREDICTION reads
|
|
625
|
+
* {@link declaresRuntimeRestRoute}, which adds the `expose: false` veto —
|
|
626
|
+
* "there is a declaration here" and "it is reachable" are different questions.
|
|
627
|
+
*/
|
|
628
|
+
export declare function declaresRuntimeRestRouteShape(method: ExposableMethod | undefined): boolean;
|
|
629
|
+
/**
|
|
630
|
+
* Coerce one transport-supplied argument into the runtime value the declared
|
|
631
|
+
* parameter type needs.
|
|
632
|
+
*
|
|
633
|
+
* Today that means exactly one conversion: a `Date` parameter, which the
|
|
634
|
+
* wire-ability heuristic accepts as JSON-shaped. JSON has no date type, so a
|
|
635
|
+
* caller can only send an ISO string (or an epoch number) and the receiving
|
|
636
|
+
* method — which calls `getTime()`, or hands the value to a query builder that
|
|
637
|
+
* expects a `Date` — would otherwise get a string. Accepting `Date` as
|
|
638
|
+
* wire-able and NOT hydrating it here would generate a route that 500s, so the
|
|
639
|
+
* two are one decision (#2686).
|
|
640
|
+
*
|
|
641
|
+
* Deliberately narrow:
|
|
642
|
+
* - Only a TOP-LEVEL declared parameter is converted. A `Date` nested inside a
|
|
643
|
+
* named options bag is invisible to the manifest (the bag is accepted
|
|
644
|
+
* heuristically, its members unresolved), so it is not hydrated and the
|
|
645
|
+
* method must accept the string itself.
|
|
646
|
+
* - An already-`Date` value, and anything that is not a string or finite
|
|
647
|
+
* number, passes through untouched, so a runtime caller invoking the same
|
|
648
|
+
* helper is never degraded.
|
|
649
|
+
* - An unparseable string passes through as-is rather than becoming an
|
|
650
|
+
* `Invalid Date`, leaving the method's own validation in charge of the error
|
|
651
|
+
* message.
|
|
652
|
+
*/
|
|
653
|
+
export declare function coerceCustomActionArgument(value: unknown, declaredType: string | undefined): unknown;
|
|
654
|
+
/**
|
|
655
|
+
* The `Date` half of {@link coerceCustomActionArgument}, exported on its own
|
|
656
|
+
* because generated SvelteKit route code calls it directly: the generator
|
|
657
|
+
* already knows at build time which parameters are `Date`-typed, so the
|
|
658
|
+
* emitted handler names the conversion rather than re-deriving it from a type
|
|
659
|
+
* string at runtime. Both paths share this one implementation so the two
|
|
660
|
+
* transports cannot drift.
|
|
661
|
+
*/
|
|
662
|
+
export declare function toCustomActionDate(value: unknown): unknown;
|
|
663
|
+
/**
|
|
664
|
+
* Decode a `number`-typed action argument that arrived over a QUERY STRING.
|
|
665
|
+
*
|
|
666
|
+
* A GET handler builds its options from `URLSearchParams`, so every value is a
|
|
667
|
+
* string: `limit: number` reached the method as `'2'` and any arithmetic on it
|
|
668
|
+
* silently produced string concatenation or `NaN` (#2686). A JSON body needs no
|
|
669
|
+
* such repair, which is why this is emitted only on GET routes.
|
|
670
|
+
*
|
|
671
|
+
* Leaves anything it cannot decode alone, so a malformed value reaches the
|
|
672
|
+
* method's own validation rather than becoming a silent `NaN`.
|
|
673
|
+
*/
|
|
674
|
+
export declare function toCustomActionNumber(value: unknown): unknown;
|
|
675
|
+
/**
|
|
676
|
+
* The `boolean` counterpart to {@link toCustomActionNumber}. A query string
|
|
677
|
+
* carries `?active=false`, and the bare string `'false'` is TRUTHY — the most
|
|
678
|
+
* dangerous of these coercions, since it inverts a guard rather than degrading
|
|
679
|
+
* it. Only the four canonical spellings decode; anything else is left for the
|
|
680
|
+
* method's own validation.
|
|
681
|
+
*/
|
|
682
|
+
export declare function toCustomActionBoolean(value: unknown): unknown;
|
|
683
|
+
/**
|
|
684
|
+
* The query-string decoder a GET route should apply to one parameter, or
|
|
685
|
+
* `undefined` when the value passes through untouched.
|
|
686
|
+
*
|
|
687
|
+
* Mirrors {@link declaredTypeAcceptsDate}: a nullish branch does not change the
|
|
688
|
+
* representation, but a genuine alternative (`number | string`) means the
|
|
689
|
+
* method already accepts what the query string sends, so nothing is decoded.
|
|
690
|
+
*/
|
|
691
|
+
export declare function queryStringDecoderFor(declaredType: string | undefined): 'toCustomActionDate' | 'toCustomActionNumber' | 'toCustomActionBoolean' | undefined;
|
|
692
|
+
/**
|
|
693
|
+
* True when the declared type is a `Date` and nothing else.
|
|
694
|
+
*
|
|
695
|
+
* `Date | null` and `Date | undefined` qualify — a nullish branch is not an
|
|
696
|
+
* alternative representation. `Date | string` deliberately does NOT: that
|
|
697
|
+
* signature already accepts the string a JSON caller sends, so the method's
|
|
698
|
+
* own handling is authoritative and converting behind its back would change
|
|
699
|
+
* which branch it takes.
|
|
700
|
+
*/
|
|
701
|
+
export declare function declaredTypeAcceptsDate(declaredType: string): boolean;
|
|
325
702
|
//# sourceMappingURL=custom-action.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"custom-action.d.ts","sourceRoot":"","sources":["../../src/generators/custom-action.ts"],"names":[],"mappings":"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"}
|