@happyvertical/smrt-products 0.34.7 → 0.34.8

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 CHANGED
@@ -57,7 +57,7 @@ Same codebase consumed three ways:
57
57
 
58
58
  Auto-generated via Vite plugins: `@happyvertical/smrt-client` (TypeScript client), `@happyvertical/smrt-types`, `@happyvertical/smrt-routes` (Express), `@happyvertical/smrt-mcp`, `@happyvertical/smrt-manifest`.
59
59
 
60
- Svelte 5 stores use runes (`$state`, `$derived`, `$effect`). Separate `product-store.server.svelte.ts` vs `product-store.client.svelte.ts` for SSR safety.
60
+ Svelte 5 stores use runes (`$state`, `$derived`, `$effect`). `product-store.svelte.ts` is the main store (backed by the SMRT client); `product-store.client.svelte.ts` is a virtual-module-free variant for federation builds.
61
61
 
62
62
  ## Schema migrations (Phase 1 release)
63
63
 
@@ -33,12 +33,13 @@ const {
33
33
  }: Props = $props();
34
34
 
35
35
  // Auto-generate label from field name if not provided
36
- const displayLabel =
36
+ const displayLabel = $derived(
37
37
  label ||
38
- fieldName
39
- .replace(/([A-Z])/g, ' $1')
40
- .replace(/^./, (str) => str.toUpperCase());
41
- const fieldId = `field-${fieldName}`;
38
+ fieldName
39
+ .replace(/([A-Z])/g, ' $1')
40
+ .replace(/^./, (str) => str.toUpperCase()),
41
+ );
42
+ const fieldId = $derived(`field-${fieldName}`);
42
43
 
43
44
  function handleUpdate(newValue: any) {
44
45
  if (onUpdate && !readonly) {
@@ -1 +1 @@
1
- {"version":3,"file":"FieldRenderer.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/FieldRenderer.svelte.ts"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChE,KAAK,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAqGD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"FieldRenderer.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/FieldRenderer.svelte.ts"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChE,KAAK,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAsGD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1782290137787,
3
+ "timestamp": 1782318262725,
4
4
  "packageName": "@happyvertical/smrt-products",
5
- "packageVersion": "0.34.7",
5
+ "packageVersion": "0.34.8",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-products:CategoryCollection": {
8
8
  "name": "categorycollection",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-06-24T08:35:38.290Z",
3
+ "generatedAt": "2026-06-24T16:24:23.349Z",
4
4
  "packageName": "@happyvertical/smrt-products",
5
- "packageVersion": "0.34.7",
5
+ "packageVersion": "0.34.8",
6
6
  "sourceManifestPath": "dist/lib/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "30f0edb5a8238da237d80fb012c320c04b2b4ac78e68481089a647961cfc66c4",
10
- "packageJson": "393552fb22b0e6b9bbd15ef79cb29ab70be32c531d3ee31f357d1bab751cdc55",
11
- "agents": "4ca8837df73cf8b966b65cea639a058baa8f92ae536f3cd5036546fcbfeb37c8"
9
+ "manifest": "d88ddc519370ba8d1e115d9e74ca09fc6fc7908bdf17894ae9edb7db1e56517c",
10
+ "packageJson": "b1dea34106ce986acebde25be38c7e1aa7243ab14170453a71a756f6e16031a4",
11
+ "agents": "ede6063b72600b84908e1e32216fbbdd69d70d309f0b23c2add338ad080702d4"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -1572,5 +1572,5 @@
1572
1572
  "polymorphicAssociations": 0,
1573
1573
  "uuidColumns": 21
1574
1574
  },
1575
- "agentDoc": "# @happyvertical/smrt-products\n\nProduct catalog — reference template for triple-consumption: npm package library, module federation, and standalone REST API server.\n\n## Models\n\n- **Product**: STI base. Tenant-scoped (`@TenantScoped({ mode: 'optional' })`, nullable `tenantId`). Knowledge base product with specs, tags, and the `productType` discriminator. Consumers subclass this with vertical-specific subtypes (apparel `Style`, automotive `Model`, furniture `Design`, etc.).\n- **Material**: STI subtype. Raw input consumed by manufacturing (fabric, trim, thread, packaging, component). Meta fields: `materialKind`, `uom`, `costPerUnit`. Materials are first-class products in the catalog — the SAP/NetSuite pattern. Bills of materials in `@happyvertical/smrt-manufacturing` reference Materials by id.\n- **ProductVariant**: standalone (NOT a Product STI subtype). Declarative axis definition: `productId`, `axisName` (e.g. `'size'`, `'color'`, `'finish'`), `allowedValues` (JSON-stored array), optional `label` and `sortOrder`. Per-SKU value pins live on `Sku.attributes`. Lives in its own `product_variants` table; `conflictColumns: ['product_id', 'axis_name', 'tenant_id']`.\n- **Sku**: standalone. The smallest sellable / countable unit. `productId` points at a `Product` or any of its STI subtypes; `code` is the human-meaningful identifier; `attributes` JSON pins each axis value (`{ size: 'M', color: 'navy' }`). Lives in its own `product_skus` table; `conflictColumns: ['code', 'tenant_id']`. Stock balance and movement history for a Sku live in `@happyvertical/smrt-inventory`.\n- **ProductAsset**: dedicated owned-asset join in `product_assets` with `relationship` and `sortOrder`. Tenant-scoped to match Product.\n- **Category**: hierarchical (parentId, level, productCount). STI base, tenant-scoped.\n\n## Vertical-specific Product subtypes\n\nThis package deliberately ships ONLY the generic primitives. Domain-specific top-level item types live in the consumer's template:\n\n- Apparel: `Style extends Product`, `Makeup extends Product`\n- Furniture: `Design extends Product`, `Configuration extends Product`\n- Automotive: `Model extends Product`, `Trim extends Product`\n- CPG: `Brand extends Product`, `Recipe extends Product`\n\nEach is a small subclass: `@smrt()`, override `productType`, add `@meta()` fields. See `packages/template-apparel-erp` for a worked example.\n\n## Variants — the two concepts\n\nThe framework's variant story uses two distinct primitives that look similar but do different jobs:\n\n- **`ProductVariant`** — *axis declaration*. \"For product X, axis `size` accepts the values `[XS, S, M, L, XL]`.\" Drives form/UI choices.\n- **`Sku.attributes`** — *per-unit value pins*. `{ color: 'navy', size: 'M' }` on each concrete sellable SKU.\n\nBoth live in this package — all catalog shapes are here. Stock motion (where the SKU is, how many, history) lives in `@happyvertical/smrt-inventory`.\n\nThere is deliberately no separate \"catalog grouping above SKU\" row (a la a \"Navy colorway\" row sitting between the Product and its Skus). Group SKUs by axis value via `attributes.color = 'navy'` queries; attach per-axis-value assets via `ProductAsset` rows with relationship metadata. Matches how Shopify, Stripe Products, and most e-commerce platforms model the same shape.\n\n## Collections\n\n- **ProductCollection** — base. Polymorphic queries return the correct subclass instance per row.\n- **MaterialCollection** — STI-filtered subclass. Override `_itemClass`; framework auto-filters by `_meta_type`.\n- **ProductVariantCollection** — standalone collection over `product_variants`. Helpers: `findForProduct(productId)`, `findAxis(productId, axisName)`.\n- **SkuCollection** — standalone collection over `product_skus`. Helpers: `findByCode`, `findByBarcode`, `findByProduct`, `findByParent`, `findActive`.\n- **CategoryCollection** — standard Category CRUD with `getRootCategories()`.\n- **ProductAssetCollection** — direct access to `product_assets` rows and product asset helper wrappers.\n\n## Multi-tenancy\n\nOptional. With `withTenant(id, fn)` (from `@happyvertical/smrt-tenancy`), all queries auto-filter and inserts auto-stamp `tenantId`. Without it, models behave globally (`tenantId = null`). This lets the same package serve both shared reference catalogs and per-merchant SaaS catalogs.\n\n## Triple-Consumption Pattern\n\nSame codebase consumed three ways:\n1. **NPM library**: import classes directly\n2. **Module federation**: runtime component sharing (experimental)\n3. **Standalone API**: `startRestServer([Product, Category])`\n\n## Virtual Modules (Vite)\n\nAuto-generated via Vite plugins: `@happyvertical/smrt-client` (TypeScript client), `@happyvertical/smrt-types`, `@happyvertical/smrt-routes` (Express), `@happyvertical/smrt-mcp`, `@happyvertical/smrt-manifest`.\n\nSvelte 5 stores use runes (`$state`, `$derived`, `$effect`). Separate `product-store.server.svelte.ts` vs `product-store.client.svelte.ts` for SSR safety.\n\n## Schema migrations (Phase 1 release)\n\nThis package's schema changed shape between the previous release and the Phase 1 apparel-ERP release. Consumers upgrading need to migrate two tables.\n\n### `Sku` rows moved tables\n\nPreviously `Sku` shipped in `@happyvertical/smrt-inventory` under table `inventory_skus`. It now lives here under `product_skus`. Cross-package refs (`StockLevel.skuId`, `BomLine.componentSkuId`) carry plain string ids that still resolve.\n\n**Upgrade procedure** (works on SQLite + Postgres; does NOT rely on `CREATE TABLE AS` which strips constraints):\n\n1. **Boot the new version once** so the framework's lazy `syncSchema` creates `product_skus` with the right PRIMARY KEY, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes derived from the `Sku` model.\n\n2. **Idempotently copy rows**:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n### `ProductVariant` changed shape entirely\n\nPreviously a Product STI subtype carrying `parentProductId` + `axisValues` JSON inside `_meta_data` on the shared `products` table (`_meta_type='@happyvertical/smrt-products:ProductVariant'`). It is now a **standalone model** on its own `product_variants` table, with columns `productId`, `axisName`, `allowedValues`, `label`, `sortOrder`.\n\n**There is no automatic data conversion.** The old \"catalog grouping above SKU\" concept doesn't map 1:1 to the new \"per-axis declaration\" concept. Recommended procedure:\n\n1. Inspect the old rows: `SELECT * FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`. Treat them as historical reference.\n2. Re-author axis declarations against the new shape (one `ProductVariant` row per `(productId, axisName)` pair, with `allowedValues` listing the values).\n3. Once the new declarations are populated and verified, remove the legacy rows: `DELETE FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`.\n\nIf you had per-colorway / per-variant images attached via `ProductAsset` rows pointing at old ProductVariant ids, repoint those to the parent Product id; group-by-axis-value queries on `Sku.attributes` cover the same use case at the SKU level.\n\n## Gotchas\n\n- **`ProductVariant` and `Sku` are NOT Product STI subtypes** — they each have their own table (`product_variants`, `product_skus`) because their shapes don't fit the Product schema. Don't try to query them via `ProductCollection`.\n- **`npm run build` emits the published library surface directly**: package consumers read model, collection, and helper exports from `dist/lib`. Cross-package imports from this package should target `/models` or `/collections` subpaths (not the main entry) when the consumer isn't a vite app — the main entry pulls in vite virtual modules.\n- **Use `npm run build:all` only when you need standalone or federation bundles** in addition to the library output\n- **Constructor must explicitly assign all properties**: `Object.assign` doesn't work reliably with decorators\n- **STI subtype-specific fields use `Meta<T>`** — declare them as `fieldName: Meta<FieldType> = defaultValue`. The AST scanner detects the `Meta<T>` type wrapper at build time and routes the field through `_meta_data` JSON storage instead of materializing it as a column on the parent's table. Do **NOT** use the runtime `@meta()` decorator on STI children — it never reaches the manifest, so the schema generator treats the field as an ordinary column on the parent table and the framework's hydration path can't tell it's meta. Override `productType` on each subclass.\n- **Tenant-scoped STI children must repeat `@TenantScoped`** — `@TenantScoped` registers per concrete className, so `Material extends Product` inheriting from a tenant-scoped `Product` is NOT automatically tenant-scoped itself. `MaterialCollection.list/save` passes `'Material'` to the tenant interceptor, which looks up `'Material'` (not `'Product'`) in the per-class registry. Without an explicit `@TenantScoped({ mode: 'optional' })` on `Material`, material rows skip the tenant auto-filter and auto-populate.\n- **STI children must repeat `@smrt({ api, mcp, cli })` generation config (S5 #1406)** — like `@TenantScoped`, the generation config is registered per concrete className and is NOT inherited from the STI parent. An empty `@smrt()` on a child resolves `getConfig(child).api/.mcp` to `undefined`, which the REST and MCP generators treat as \"expose EVERYTHING\" — including `delete` and write-capable MCP tools — even when the parent deliberately restricts its surface. The package's `mcp.ts` generator enumerates the whole registry, so an under-configured child ships a wide-open surface silently. Re-declare the parent's `api`/`mcp`/`cli` posture on every subtype (see `Material`). Consumer subtypes (apparel `Style`/`Makeup`, automotive `Model`/`Trim`, …) must do the same.\n- **Two-tenant same-slug is currently a hard error** for tenant-scoped STI bases (`Product`, `Category`). The core schema generator hardcodes the STI unique index as `(slug, context, _meta_type)` and does NOT include `tenant_id` even for `@TenantScoped` classes. Two tenants saving a row with the same slug + context + meta_type collide at the SQL layer. Workaround: namespace slugs per tenant on the application side (e.g. `${tenantId}-widget`) until the upstream framework fix lands.\n- **Module Federation marked experimental**: may change\n"
1575
+ "agentDoc": "# @happyvertical/smrt-products\n\nProduct catalog — reference template for triple-consumption: npm package library, module federation, and standalone REST API server.\n\n## Models\n\n- **Product**: STI base. Tenant-scoped (`@TenantScoped({ mode: 'optional' })`, nullable `tenantId`). Knowledge base product with specs, tags, and the `productType` discriminator. Consumers subclass this with vertical-specific subtypes (apparel `Style`, automotive `Model`, furniture `Design`, etc.).\n- **Material**: STI subtype. Raw input consumed by manufacturing (fabric, trim, thread, packaging, component). Meta fields: `materialKind`, `uom`, `costPerUnit`. Materials are first-class products in the catalog — the SAP/NetSuite pattern. Bills of materials in `@happyvertical/smrt-manufacturing` reference Materials by id.\n- **ProductVariant**: standalone (NOT a Product STI subtype). Declarative axis definition: `productId`, `axisName` (e.g. `'size'`, `'color'`, `'finish'`), `allowedValues` (JSON-stored array), optional `label` and `sortOrder`. Per-SKU value pins live on `Sku.attributes`. Lives in its own `product_variants` table; `conflictColumns: ['product_id', 'axis_name', 'tenant_id']`.\n- **Sku**: standalone. The smallest sellable / countable unit. `productId` points at a `Product` or any of its STI subtypes; `code` is the human-meaningful identifier; `attributes` JSON pins each axis value (`{ size: 'M', color: 'navy' }`). Lives in its own `product_skus` table; `conflictColumns: ['code', 'tenant_id']`. Stock balance and movement history for a Sku live in `@happyvertical/smrt-inventory`.\n- **ProductAsset**: dedicated owned-asset join in `product_assets` with `relationship` and `sortOrder`. Tenant-scoped to match Product.\n- **Category**: hierarchical (parentId, level, productCount). STI base, tenant-scoped.\n\n## Vertical-specific Product subtypes\n\nThis package deliberately ships ONLY the generic primitives. Domain-specific top-level item types live in the consumer's template:\n\n- Apparel: `Style extends Product`, `Makeup extends Product`\n- Furniture: `Design extends Product`, `Configuration extends Product`\n- Automotive: `Model extends Product`, `Trim extends Product`\n- CPG: `Brand extends Product`, `Recipe extends Product`\n\nEach is a small subclass: `@smrt()`, override `productType`, add `@meta()` fields. See `packages/template-apparel-erp` for a worked example.\n\n## Variants — the two concepts\n\nThe framework's variant story uses two distinct primitives that look similar but do different jobs:\n\n- **`ProductVariant`** — *axis declaration*. \"For product X, axis `size` accepts the values `[XS, S, M, L, XL]`.\" Drives form/UI choices.\n- **`Sku.attributes`** — *per-unit value pins*. `{ color: 'navy', size: 'M' }` on each concrete sellable SKU.\n\nBoth live in this package — all catalog shapes are here. Stock motion (where the SKU is, how many, history) lives in `@happyvertical/smrt-inventory`.\n\nThere is deliberately no separate \"catalog grouping above SKU\" row (a la a \"Navy colorway\" row sitting between the Product and its Skus). Group SKUs by axis value via `attributes.color = 'navy'` queries; attach per-axis-value assets via `ProductAsset` rows with relationship metadata. Matches how Shopify, Stripe Products, and most e-commerce platforms model the same shape.\n\n## Collections\n\n- **ProductCollection** — base. Polymorphic queries return the correct subclass instance per row.\n- **MaterialCollection** — STI-filtered subclass. Override `_itemClass`; framework auto-filters by `_meta_type`.\n- **ProductVariantCollection** — standalone collection over `product_variants`. Helpers: `findForProduct(productId)`, `findAxis(productId, axisName)`.\n- **SkuCollection** — standalone collection over `product_skus`. Helpers: `findByCode`, `findByBarcode`, `findByProduct`, `findByParent`, `findActive`.\n- **CategoryCollection** — standard Category CRUD with `getRootCategories()`.\n- **ProductAssetCollection** — direct access to `product_assets` rows and product asset helper wrappers.\n\n## Multi-tenancy\n\nOptional. With `withTenant(id, fn)` (from `@happyvertical/smrt-tenancy`), all queries auto-filter and inserts auto-stamp `tenantId`. Without it, models behave globally (`tenantId = null`). This lets the same package serve both shared reference catalogs and per-merchant SaaS catalogs.\n\n## Triple-Consumption Pattern\n\nSame codebase consumed three ways:\n1. **NPM library**: import classes directly\n2. **Module federation**: runtime component sharing (experimental)\n3. **Standalone API**: `startRestServer([Product, Category])`\n\n## Virtual Modules (Vite)\n\nAuto-generated via Vite plugins: `@happyvertical/smrt-client` (TypeScript client), `@happyvertical/smrt-types`, `@happyvertical/smrt-routes` (Express), `@happyvertical/smrt-mcp`, `@happyvertical/smrt-manifest`.\n\nSvelte 5 stores use runes (`$state`, `$derived`, `$effect`). `product-store.svelte.ts` is the main store (backed by the SMRT client); `product-store.client.svelte.ts` is a virtual-module-free variant for federation builds.\n\n## Schema migrations (Phase 1 release)\n\nThis package's schema changed shape between the previous release and the Phase 1 apparel-ERP release. Consumers upgrading need to migrate two tables.\n\n### `Sku` rows moved tables\n\nPreviously `Sku` shipped in `@happyvertical/smrt-inventory` under table `inventory_skus`. It now lives here under `product_skus`. Cross-package refs (`StockLevel.skuId`, `BomLine.componentSkuId`) carry plain string ids that still resolve.\n\n**Upgrade procedure** (works on SQLite + Postgres; does NOT rely on `CREATE TABLE AS` which strips constraints):\n\n1. **Boot the new version once** so the framework's lazy `syncSchema` creates `product_skus` with the right PRIMARY KEY, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes derived from the `Sku` model.\n\n2. **Idempotently copy rows**:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n### `ProductVariant` changed shape entirely\n\nPreviously a Product STI subtype carrying `parentProductId` + `axisValues` JSON inside `_meta_data` on the shared `products` table (`_meta_type='@happyvertical/smrt-products:ProductVariant'`). It is now a **standalone model** on its own `product_variants` table, with columns `productId`, `axisName`, `allowedValues`, `label`, `sortOrder`.\n\n**There is no automatic data conversion.** The old \"catalog grouping above SKU\" concept doesn't map 1:1 to the new \"per-axis declaration\" concept. Recommended procedure:\n\n1. Inspect the old rows: `SELECT * FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`. Treat them as historical reference.\n2. Re-author axis declarations against the new shape (one `ProductVariant` row per `(productId, axisName)` pair, with `allowedValues` listing the values).\n3. Once the new declarations are populated and verified, remove the legacy rows: `DELETE FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`.\n\nIf you had per-colorway / per-variant images attached via `ProductAsset` rows pointing at old ProductVariant ids, repoint those to the parent Product id; group-by-axis-value queries on `Sku.attributes` cover the same use case at the SKU level.\n\n## Gotchas\n\n- **`ProductVariant` and `Sku` are NOT Product STI subtypes** — they each have their own table (`product_variants`, `product_skus`) because their shapes don't fit the Product schema. Don't try to query them via `ProductCollection`.\n- **`npm run build` emits the published library surface directly**: package consumers read model, collection, and helper exports from `dist/lib`. Cross-package imports from this package should target `/models` or `/collections` subpaths (not the main entry) when the consumer isn't a vite app — the main entry pulls in vite virtual modules.\n- **Use `npm run build:all` only when you need standalone or federation bundles** in addition to the library output\n- **Constructor must explicitly assign all properties**: `Object.assign` doesn't work reliably with decorators\n- **STI subtype-specific fields use `Meta<T>`** — declare them as `fieldName: Meta<FieldType> = defaultValue`. The AST scanner detects the `Meta<T>` type wrapper at build time and routes the field through `_meta_data` JSON storage instead of materializing it as a column on the parent's table. Do **NOT** use the runtime `@meta()` decorator on STI children — it never reaches the manifest, so the schema generator treats the field as an ordinary column on the parent table and the framework's hydration path can't tell it's meta. Override `productType` on each subclass.\n- **Tenant-scoped STI children must repeat `@TenantScoped`** — `@TenantScoped` registers per concrete className, so `Material extends Product` inheriting from a tenant-scoped `Product` is NOT automatically tenant-scoped itself. `MaterialCollection.list/save` passes `'Material'` to the tenant interceptor, which looks up `'Material'` (not `'Product'`) in the per-class registry. Without an explicit `@TenantScoped({ mode: 'optional' })` on `Material`, material rows skip the tenant auto-filter and auto-populate.\n- **STI children must repeat `@smrt({ api, mcp, cli })` generation config (S5 #1406)** — like `@TenantScoped`, the generation config is registered per concrete className and is NOT inherited from the STI parent. An empty `@smrt()` on a child resolves `getConfig(child).api/.mcp` to `undefined`, which the REST and MCP generators treat as \"expose EVERYTHING\" — including `delete` and write-capable MCP tools — even when the parent deliberately restricts its surface. The package's `mcp.ts` generator enumerates the whole registry, so an under-configured child ships a wide-open surface silently. Re-declare the parent's `api`/`mcp`/`cli` posture on every subtype (see `Material`). Consumer subtypes (apparel `Style`/`Makeup`, automotive `Model`/`Trim`, …) must do the same.\n- **Two-tenant same-slug is currently a hard error** for tenant-scoped STI bases (`Product`, `Category`). The core schema generator hardcodes the STI unique index as `(slug, context, _meta_type)` and does NOT include `tenant_id` even for `@TenantScoped` classes. Two tenants saving a row with the same slug + context + meta_type collide at the SQL layer. Workaround: namespace slugs per tenant on the application side (e.g. `${tenantId}-widget`) until the upstream framework fix lands.\n- **Module Federation marked experimental**: may change\n"
1576
1576
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-products",
3
- "version": "0.34.7",
3
+ "version": "0.34.8",
4
4
  "description": "SMRT products module: triple-purpose microservice template for standalone apps, federated modules, and NPM libraries",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -55,11 +55,11 @@
55
55
  "@happyvertical/utils": "^0.74.7",
56
56
  "cors": "^2.8.5",
57
57
  "express": "^5.2.1",
58
- "@happyvertical/smrt-assets": "0.34.7",
59
- "@happyvertical/smrt-core": "0.34.7",
60
- "@happyvertical/smrt-scanner": "0.34.7",
61
- "@happyvertical/smrt-tenancy": "0.34.7",
62
- "@happyvertical/smrt-ui": "0.34.7"
58
+ "@happyvertical/smrt-assets": "0.34.8",
59
+ "@happyvertical/smrt-scanner": "0.34.8",
60
+ "@happyvertical/smrt-core": "0.34.8",
61
+ "@happyvertical/smrt-tenancy": "0.34.8",
62
+ "@happyvertical/smrt-ui": "0.34.8"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "svelte": "^5.46.4"
@@ -82,7 +82,7 @@
82
82
  "typescript": "^5.9.3",
83
83
  "vite": "^7.3.1",
84
84
  "vitest": "^4.0.17",
85
- "@happyvertical/smrt-vitest": "0.34.7"
85
+ "@happyvertical/smrt-vitest": "0.34.8"
86
86
  },
87
87
  "keywords": [
88
88
  "smrt",