@happyvertical/smrt-products 0.57.30 → 0.57.31

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.
@@ -1 +1 @@
1
- {"version":3,"file":"collections-BSdsxJCV.js","names":[],"sources":["../../../src/lib/collections/CategoryCollection.ts","../../../src/lib/collections/MaterialCollection.ts","../../../src/lib/collections/ProductVariantCollection.ts","../../../src/lib/collections/SkuCollection.ts"],"sourcesContent":["/**\n * CategoryCollection — Collection manager for {@link Category} objects.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Category } from '../models/Category';\n\nexport class CategoryCollection extends SmrtCollection<Category> {\n static readonly _itemClass = Category;\n\n /**\n * Return every top-level category. `parentId` on `Category` is typed\n * `string | undefined`; SMRT serializes undefined string fields as `''`\n * rather than `NULL`, so a category created without an explicit\n * `parentId` lands in the DB with `parent_id = ''`, not `parent_id IS NULL`.\n *\n * We accept both shapes — historical data, migrations, and direct DB\n * imports can produce either — so consumers don't have to normalize\n * `parentId` on write to be discoverable as a root. SQL's `IN` operator\n * does NOT match `NULL` (NULL is not equal to anything, including itself\n * inside an IN list), so the empty-string and NULL cases must run as\n * separate queries and merge.\n */\n async getRootCategories(): Promise<Category[]> {\n const [emptyParents, nullParents] = await Promise.all([\n this.list({ where: { parentId: '' } }),\n this.list({ where: { parentId: null } }),\n ]);\n\n if (emptyParents.length === 0) return nullParents;\n if (nullParents.length === 0) return emptyParents;\n\n // De-dupe by id in case a row somehow shows up in both lists.\n const seen = new Set<string>();\n const merged: Category[] = [];\n for (const row of [...emptyParents, ...nullParents]) {\n const id = row.id ?? '';\n if (!id || seen.has(id)) continue;\n seen.add(id);\n merged.push(row);\n }\n return merged;\n }\n}\n","/**\n * MaterialCollection — Collection manager for {@link Material} objects.\n *\n * STI framework auto-filters by `_meta_type` to return only Material rows\n * from the shared `products` table.\n */\n\nimport { Material } from '../models/Material';\nimport type { MaterialKind } from '../models/types';\nimport { ProductCollection } from './ProductCollection';\n\nexport class MaterialCollection extends ProductCollection {\n static override readonly _itemClass = Material;\n\n /**\n * Find every {@link Material} with the given kind.\n *\n * `materialKind` is an `@meta()` field — it lives inside the shared\n * `_meta_data` JSON column on `products`, not as its own SQL column.\n * That means a naive `list({ where: { materialKind } })` would generate\n * a WHERE on a non-existent column. We list every Material STI row and\n * filter the hydrated instances in JS.\n *\n * For very large catalogs, consumers should add a generated column or\n * an expression index over the JSON path and override this helper.\n */\n async findByKind(materialKind: MaterialKind): Promise<Material[]> {\n const items = (await this.list({})) as Material[];\n return items.filter((m) => m.materialKind === materialKind);\n }\n}\n","/**\n * ProductVariantCollection — read helpers for {@link ProductVariant} rows.\n *\n * {@link ProductVariant} is a standalone model (not a Product STI subtype),\n * so this collection drives its own table (`product_variants`). Helper\n * methods cover the two common query patterns: \"what axes does this\n * product vary along?\" and \"tell me about the size axis specifically.\"\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { ProductVariant } from '../models/ProductVariant';\n\nexport class ProductVariantCollection extends SmrtCollection<ProductVariant> {\n static readonly _itemClass = ProductVariant;\n\n /** Every axis declaration for a given product, in display order. */\n async findForProduct(productId: string): Promise<ProductVariant[]> {\n return this.list({\n where: { productId },\n orderBy: 'sortOrder ASC',\n });\n }\n\n /** The axis declaration for a `(productId, axisName)` pair, or null. */\n async findAxis(\n productId: string,\n axisName: string,\n ): Promise<ProductVariant | null> {\n const matches = await this.list({\n where: { productId, axisName },\n limit: 1,\n });\n return matches[0] ?? null;\n }\n}\n","/**\n * SkuCollection — query helpers for {@link Sku} rows.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Sku } from '../models/Sku';\n\nexport class SkuCollection extends SmrtCollection<Sku> {\n static readonly _itemClass = Sku;\n\n /**\n * Look up a SKU by its tenant-scoped `code` (UPC, internal part\n * number, etc.). Returns `null` when no row matches.\n */\n async findByCode(code: string): Promise<Sku | null> {\n const matches = await this.list({ where: { code }, limit: 1 });\n return matches[0] ?? null;\n }\n\n /**\n * Look up a SKU by its scannable barcode. Returns `null` when no row\n * matches; pass `''` to skip the query early.\n */\n async findByBarcode(barcode: string): Promise<Sku | null> {\n if (!barcode) return null;\n const matches = await this.list({ where: { barcode }, limit: 1 });\n return matches[0] ?? null;\n }\n\n /**\n * Find every SKU pointing at the given `Product` id (or any Product\n * STI subtype id — `Material` upstream, vertical subtypes defined in\n * templates). Useful for listing the SKUs that back a single product\n * card.\n */\n async findByProduct(productId: string): Promise<Sku[]> {\n return this.list({ where: { productId }, orderBy: 'code ASC' });\n }\n\n /**\n * Find every SKU that is a component of the given parent SKU (for\n * bundles / kits). Returns an empty array when the parent has no\n * children.\n */\n async findByParent(parentSkuId: string): Promise<Sku[]> {\n return this.list({ where: { parentSkuId }, orderBy: 'code ASC' });\n }\n\n /** Find every active SKU, optionally narrowed by parent product id. */\n async findActive(productId?: string): Promise<Sku[]> {\n const where: Record<string, unknown> = { active: true };\n if (productId) where.productId = productId;\n return this.list({ where, orderBy: 'code ASC' });\n }\n}\n"],"mappings":";;;;;;;;;AAOA,IAAa,qBAAb,cAAwC,eAAyB;CAC/D,OAAgB,aAAa;;;;;;;;;;;;;;CAe7B,MAAM,oBAAyC;EAC7C,MAAM,CAAC,cAAc,eAAe,MAAM,QAAQ,IAAI,CACpD,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,GAAG,EAAE,CAAC,GACrC,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,KAAK,EAAE,CAAC,CACzC,CAAC;EAED,IAAI,aAAa,WAAW,GAAG,OAAO;EACtC,IAAI,YAAY,WAAW,GAAG,OAAO;EAGrC,MAAM,uBAAO,IAAI,IAAY;EAC7B,MAAM,SAAqB,CAAC;EAC5B,KAAK,MAAM,OAAO,CAAC,GAAG,cAAc,GAAG,WAAW,GAAG;GACnD,MAAM,KAAK,IAAI,MAAM;GACrB,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,GAAG;GACzB,KAAK,IAAI,EAAE;GACX,OAAO,KAAK,GAAG;EACjB;EACA,OAAO;CACT;AACF;;;;;;;;;AChCA,IAAa,qBAAb,cAAwC,kBAAkB;CACxD,OAAyB,aAAa;;;;;;;;;;;;;CActC,MAAM,WAAW,cAAiD;EAEhE,QAAO,MADc,KAAK,KAAK,CAAC,CAAC,EAAA,CACpB,QAAQ,MAAM,EAAE,iBAAiB,YAAY;CAC5D;AACF;;;;;;;;;;;AClBA,IAAa,2BAAb,cAA8C,eAA+B;CAC3E,OAAgB,aAAa;;CAG7B,MAAM,eAAe,WAA8C;EACjE,OAAO,KAAK,KAAK;GACf,OAAO,EAAE,UAAU;GACnB,SAAS;EACX,CAAC;CACH;;CAGA,MAAM,SACJ,WACA,UACgC;EAKhC,QAAO,MAJe,KAAK,KAAK;GAC9B,OAAO;IAAE;IAAW;GAAS;GAC7B,OAAO;EACT,CAAC,EAAA,CACc,MAAM;CACvB;AACF;;;;;;;;ACzBA,IAAa,gBAAb,cAAmC,eAAoB;CACrD,OAAgB,aAAa;;;;;CAM7B,MAAM,WAAW,MAAmC;EAElD,QAAO,MADe,KAAK,KAAK;GAAE,OAAO,EAAE,KAAK;GAAG,OAAO;EAAE,CAAC,EAAA,CAC9C,MAAM;CACvB;;;;;CAMA,MAAM,cAAc,SAAsC;EACxD,IAAI,CAAC,SAAS,OAAO;EAErB,QAAO,MADe,KAAK,KAAK;GAAE,OAAO,EAAE,QAAQ;GAAG,OAAO;EAAE,CAAC,EAAA,CACjD,MAAM;CACvB;;;;;;;CAQA,MAAM,cAAc,WAAmC;EACrD,OAAO,KAAK,KAAK;GAAE,OAAO,EAAE,UAAU;GAAG,SAAS;EAAW,CAAC;CAChE;;;;;;CAOA,MAAM,aAAa,aAAqC;EACtD,OAAO,KAAK,KAAK;GAAE,OAAO,EAAE,YAAY;GAAG,SAAS;EAAW,CAAC;CAClE;;CAGA,MAAM,WAAW,WAAoC;EACnD,MAAM,QAAiC,EAAE,QAAQ,KAAK;EACtD,IAAI,WAAW,MAAM,YAAY;EACjC,OAAO,KAAK,KAAK;GAAE;GAAO,SAAS;EAAW,CAAC;CACjD;AACF"}
1
+ {"version":3,"file":"collections-D_K3tSyK.js","names":[],"sources":["../../../src/lib/collections/CategoryCollection.ts","../../../src/lib/collections/MaterialCollection.ts","../../../src/lib/collections/ProductVariantCollection.ts","../../../src/lib/collections/SkuCollection.ts"],"sourcesContent":["/**\n * CategoryCollection — Collection manager for {@link Category} objects.\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Category } from '../models/Category';\n\nexport class CategoryCollection extends SmrtCollection<Category> {\n static readonly _itemClass = Category;\n\n /**\n * Return every top-level category. `parentId` on `Category` is typed\n * `string | undefined`; SMRT serializes undefined string fields as `''`\n * rather than `NULL`, so a category created without an explicit\n * `parentId` lands in the DB with `parent_id = ''`, not `parent_id IS NULL`.\n *\n * We accept both shapes — historical data, migrations, and direct DB\n * imports can produce either — so consumers don't have to normalize\n * `parentId` on write to be discoverable as a root. SQL's `IN` operator\n * does NOT match `NULL` (NULL is not equal to anything, including itself\n * inside an IN list), so the empty-string and NULL cases must run as\n * separate queries and merge.\n */\n async getRootCategories(): Promise<Category[]> {\n const [emptyParents, nullParents] = await Promise.all([\n this.list({ where: { parentId: '' } }),\n this.list({ where: { parentId: null } }),\n ]);\n\n if (emptyParents.length === 0) return nullParents;\n if (nullParents.length === 0) return emptyParents;\n\n // De-dupe by id in case a row somehow shows up in both lists.\n const seen = new Set<string>();\n const merged: Category[] = [];\n for (const row of [...emptyParents, ...nullParents]) {\n const id = row.id ?? '';\n if (!id || seen.has(id)) continue;\n seen.add(id);\n merged.push(row);\n }\n return merged;\n }\n}\n","/**\n * MaterialCollection — Collection manager for {@link Material} objects.\n *\n * STI framework auto-filters by `_meta_type` to return only Material rows\n * from the shared `products` table.\n */\n\nimport { Material } from '../models/Material';\nimport type { MaterialKind } from '../models/types';\nimport { ProductCollection } from './ProductCollection';\n\nexport class MaterialCollection extends ProductCollection {\n static override readonly _itemClass = Material;\n\n /**\n * Find every {@link Material} with the given kind.\n *\n * `materialKind` is an `@meta()` field — it lives inside the shared\n * `_meta_data` JSON column on `products`, not as its own SQL column.\n * That means a naive `list({ where: { materialKind } })` would generate\n * a WHERE on a non-existent column. We list every Material STI row and\n * filter the hydrated instances in JS.\n *\n * For very large catalogs, consumers should add a generated column or\n * an expression index over the JSON path and override this helper.\n */\n async findByKind(materialKind: MaterialKind): Promise<Material[]> {\n const items = (await this.list({})) as Material[];\n return items.filter((m) => m.materialKind === materialKind);\n }\n}\n","/**\n * ProductVariantCollection — read helpers for {@link ProductVariant} rows.\n *\n * {@link ProductVariant} is a standalone model (not a Product STI subtype),\n * so this collection drives its own table (`product_variants`). Helper\n * methods cover the two common query patterns: \"what axes does this\n * product vary along?\" and \"tell me about the size axis specifically.\"\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { ProductVariant } from '../models/ProductVariant';\n\nexport class ProductVariantCollection extends SmrtCollection<ProductVariant> {\n static readonly _itemClass = ProductVariant;\n\n /** Every axis declaration for a given product, in display order. */\n async findForProduct(productId: string): Promise<ProductVariant[]> {\n return this.list({\n where: { productId },\n orderBy: 'sortOrder ASC',\n });\n }\n\n /** The axis declaration for a `(productId, axisName)` pair, or null. */\n async findAxis(\n productId: string,\n axisName: string,\n ): Promise<ProductVariant | null> {\n const matches = await this.list({\n where: { productId, axisName },\n limit: 1,\n });\n return matches[0] ?? null;\n }\n}\n","/**\n * SkuCollection — query helpers for {@link Sku} rows.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Sku } from '../models/Sku';\n\nexport class SkuCollection extends SmrtCollection<Sku> {\n static readonly _itemClass = Sku;\n\n /**\n * Look up a SKU by its tenant-scoped `code` (UPC, internal part\n * number, etc.). Returns `null` when no row matches.\n */\n async findByCode(code: string): Promise<Sku | null> {\n const matches = await this.list({ where: { code }, limit: 1 });\n return matches[0] ?? null;\n }\n\n /**\n * Look up a SKU by its scannable barcode. Returns `null` when no row\n * matches; pass `''` to skip the query early.\n */\n async findByBarcode(barcode: string): Promise<Sku | null> {\n if (!barcode) return null;\n const matches = await this.list({ where: { barcode }, limit: 1 });\n return matches[0] ?? null;\n }\n\n /**\n * Find every SKU pointing at the given `Product` id (or any Product\n * STI subtype id — `Material` upstream, vertical subtypes defined in\n * templates). Useful for listing the SKUs that back a single product\n * card.\n */\n async findByProduct(productId: string): Promise<Sku[]> {\n return this.list({ where: { productId }, orderBy: 'code ASC' });\n }\n\n /**\n * Find every SKU that is a component of the given parent SKU (for\n * bundles / kits). Returns an empty array when the parent has no\n * children.\n */\n async findByParent(parentSkuId: string): Promise<Sku[]> {\n return this.list({ where: { parentSkuId }, orderBy: 'code ASC' });\n }\n\n /** Find every active SKU, optionally narrowed by parent product id. */\n async findActive(productId?: string): Promise<Sku[]> {\n const where: Record<string, unknown> = { active: true };\n if (productId) where.productId = productId;\n return this.list({ where, orderBy: 'code ASC' });\n }\n}\n"],"mappings":";;;;;;;;;AAOA,IAAa,qBAAb,cAAwC,eAAyB;CAC/D,OAAgB,aAAa;;;;;;;;;;;;;;CAe7B,MAAM,oBAAyC;EAC7C,MAAM,CAAC,cAAc,eAAe,MAAM,QAAQ,IAAI,CACpD,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,GAAG,EAAE,CAAC,GACrC,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,KAAK,EAAE,CAAC,CACzC,CAAC;EAED,IAAI,aAAa,WAAW,GAAG,OAAO;EACtC,IAAI,YAAY,WAAW,GAAG,OAAO;EAGrC,MAAM,uBAAO,IAAI,IAAY;EAC7B,MAAM,SAAqB,CAAC;EAC5B,KAAK,MAAM,OAAO,CAAC,GAAG,cAAc,GAAG,WAAW,GAAG;GACnD,MAAM,KAAK,IAAI,MAAM;GACrB,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,GAAG;GACzB,KAAK,IAAI,EAAE;GACX,OAAO,KAAK,GAAG;EACjB;EACA,OAAO;CACT;AACF;;;;;;;;;AChCA,IAAa,qBAAb,cAAwC,kBAAkB;CACxD,OAAyB,aAAa;;;;;;;;;;;;;CActC,MAAM,WAAW,cAAiD;EAEhE,QAAO,MADc,KAAK,KAAK,CAAC,CAAC,EAAA,CACpB,QAAQ,MAAM,EAAE,iBAAiB,YAAY;CAC5D;AACF;;;;;;;;;;;AClBA,IAAa,2BAAb,cAA8C,eAA+B;CAC3E,OAAgB,aAAa;;CAG7B,MAAM,eAAe,WAA8C;EACjE,OAAO,KAAK,KAAK;GACf,OAAO,EAAE,UAAU;GACnB,SAAS;EACX,CAAC;CACH;;CAGA,MAAM,SACJ,WACA,UACgC;EAKhC,QAAO,MAJe,KAAK,KAAK;GAC9B,OAAO;IAAE;IAAW;GAAS;GAC7B,OAAO;EACT,CAAC,EAAA,CACc,MAAM;CACvB;AACF;;;;;;;;ACzBA,IAAa,gBAAb,cAAmC,eAAoB;CACrD,OAAgB,aAAa;;;;;CAM7B,MAAM,WAAW,MAAmC;EAElD,QAAO,MADe,KAAK,KAAK;GAAE,OAAO,EAAE,KAAK;GAAG,OAAO;EAAE,CAAC,EAAA,CAC9C,MAAM;CACvB;;;;;CAMA,MAAM,cAAc,SAAsC;EACxD,IAAI,CAAC,SAAS,OAAO;EAErB,QAAO,MADe,KAAK,KAAK;GAAE,OAAO,EAAE,QAAQ;GAAG,OAAO;EAAE,CAAC,EAAA,CACjD,MAAM;CACvB;;;;;;;CAQA,MAAM,cAAc,WAAmC;EACrD,OAAO,KAAK,KAAK;GAAE,OAAO,EAAE,UAAU;GAAG,SAAS;EAAW,CAAC;CAChE;;;;;;CAOA,MAAM,aAAa,aAAqC;EACtD,OAAO,KAAK,KAAK;GAAE,OAAO,EAAE,YAAY;GAAG,SAAS;EAAW,CAAC;CAClE;;CAGA,MAAM,WAAW,WAAoC;EACnD,MAAM,QAAiC,EAAE,QAAQ,KAAK;EACtD,IAAI,WAAW,MAAM,YAAY;EACjC,OAAO,KAAK,KAAK;GAAE;GAAO,SAAS;EAAW,CAAC;CACjD;AACF"}
@@ -1,7 +1,7 @@
1
- import "./__smrt-register__-KK4pvFca.js";
1
+ import "./__smrt-register__-S_wj3PPh.js";
2
2
  //#region src/lib/generated/index.ts
3
3
  var autoGeneratedComponents = {};
4
4
  //#endregion
5
5
  export { autoGeneratedComponents as t };
6
6
 
7
- //# sourceMappingURL=generated-6eQW9AVZ.js.map
7
+ //# sourceMappingURL=generated-DdzAc1k6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"generated-6eQW9AVZ.js","names":[],"sources":["../../../src/lib/generated/index.ts"],"sourcesContent":["/**\n * Auto-generated SMRT Components\n *\n * This file is automatically generated by the SMRT Vite plugin.\n * It exports UI components that are auto-generated from @smrt() decorated classes.\n *\n * DO NOT EDIT THIS FILE MANUALLY - it will be overwritten.\n */\n\n// Self-register this package's manifest for consumers that import via this\n// subpath without the main entry. See src/__smrt-register__.ts (issue #1132).\nimport '../../__smrt-register__.js';\n\n// Auto-generated components will be exported here by the SMRT Vite plugin\n// Examples:\n// export { default as SmrtProductForm } from './SmrtProductForm.svelte';\n// export { default as SmrtProductTable } from './SmrtProductTable.svelte';\n// export { default as SmrtCategoryForm } from './SmrtCategoryForm.svelte';\n\n// For now, export an empty object to prevent import errors\nexport const autoGeneratedComponents = {};\n"],"mappings":";;AAoBA,IAAa,0BAA0B,CAAC"}
1
+ {"version":3,"file":"generated-DdzAc1k6.js","names":[],"sources":["../../../src/lib/generated/index.ts"],"sourcesContent":["/**\n * Auto-generated SMRT Components\n *\n * This file is automatically generated by the SMRT Vite plugin.\n * It exports UI components that are auto-generated from @smrt() decorated classes.\n *\n * DO NOT EDIT THIS FILE MANUALLY - it will be overwritten.\n */\n\n// Self-register this package's manifest for consumers that import via this\n// subpath without the main entry. See src/__smrt-register__.ts (issue #1132).\nimport '../../__smrt-register__.js';\n\n// Auto-generated components will be exported here by the SMRT Vite plugin\n// Examples:\n// export { default as SmrtProductForm } from './SmrtProductForm.svelte';\n// export { default as SmrtProductTable } from './SmrtProductTable.svelte';\n// export { default as SmrtCategoryForm } from './SmrtCategoryForm.svelte';\n\n// For now, export an empty object to prevent import errors\nexport const autoGeneratedComponents = {};\n"],"mappings":";;AAoBA,IAAa,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ import "./__smrt-register__-S_wj3PPh.js";
2
+ import "./Sku-D9O8I6NV.js";
@@ -1,4 +1,4 @@
1
- import { i as CategoryCollection, n as ProductVariantCollection, r as MaterialCollection, t as SkuCollection } from "./chunks/collections-BSdsxJCV.js";
1
+ import { i as CategoryCollection, n as ProductVariantCollection, r as MaterialCollection, t as SkuCollection } from "./chunks/collections-D_K3tSyK.js";
2
2
  import { t as ProductCollection } from "./chunks/ProductCollection-bcMIqMpy.js";
3
3
  import { t as ProductAssetCollection } from "./chunks/ProductAssetCollection-CDwWlUxj.js";
4
4
  export { CategoryCollection, MaterialCollection, ProductAssetCollection, ProductCollection, ProductVariantCollection, SkuCollection };
@@ -1,2 +1,2 @@
1
- import { t as autoGeneratedComponents } from "./chunks/generated-6eQW9AVZ.js";
1
+ import { t as autoGeneratedComponents } from "./chunks/generated-DdzAc1k6.js";
2
2
  export { autoGeneratedComponents };
package/dist/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "./chunks/rolldown-runtime-D7D4PA-g.js";
2
- import "./chunks/__smrt-register__-KK4pvFca.js";
2
+ import "./chunks/__smrt-register__-S_wj3PPh.js";
3
3
  import { a as Product, i as Material, n as ProductVariant, o as ProductType, r as ProductAsset, s as Category, t as Sku } from "./chunks/Sku-D9O8I6NV.js";
4
- import { i as CategoryCollection, n as ProductVariantCollection, r as MaterialCollection, t as SkuCollection } from "./chunks/collections-BSdsxJCV.js";
4
+ import { i as CategoryCollection, n as ProductVariantCollection, r as MaterialCollection, t as SkuCollection } from "./chunks/collections-D_K3tSyK.js";
5
5
  import { t as ProductCollection } from "./chunks/ProductCollection-bcMIqMpy.js";
6
6
  import { t as ProductAssetCollection } from "./chunks/ProductAssetCollection-CDwWlUxj.js";
7
7
  import { n as ProductCard, t as ProductForm } from "./chunks/components-DMJz1geh.js";
8
- import { t as autoGeneratedComponents } from "./chunks/generated-6eQW9AVZ.js";
9
- import "./chunks/models-DdWvzYB1.js";
8
+ import { t as autoGeneratedComponents } from "./chunks/generated-DdzAc1k6.js";
9
+ import "./chunks/models-BA1FXAcw.js";
10
10
  import { n as productStore, t as ProductStoreClass } from "./chunks/stores-CB0SI1c5.js";
11
11
  import { i as slugify, n as formatPrice, r as generateId, t as formatDate } from "./chunks/utils-BgTqlQoH.js";
12
12
  import { SmrtRecipe, startRestServer } from "@happyvertical/smrt-core";
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-products",
5
- "packageVersion": "0.57.30",
5
+ "packageVersion": "0.57.31",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-products:CategoryCollection": {
8
8
  "name": "categorycollection",
@@ -1,3 +1,3 @@
1
1
  import { a as Product, i as Material, n as ProductVariant, o as ProductType, r as ProductAsset, s as Category, t as Sku } from "./chunks/Sku-D9O8I6NV.js";
2
- import "./chunks/models-DdWvzYB1.js";
2
+ import "./chunks/models-BA1FXAcw.js";
3
3
  export { Category, Material, Product, ProductAsset, ProductType, ProductVariant, Sku };
@@ -3,13 +3,14 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-products",
6
- "packageVersion": "0.57.30",
6
+ "packageVersion": "0.57.31",
7
7
  "sourceManifestPath": "dist/lib/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "756fd18bcf45939cf7003a8301c66962f025a935d304d6929c82d40026346014",
11
- "packageJson": "9ea2bf81a04fcb849930becfafbf7ba5a1f3ec7b699d84734a1bcbd7e1c73a80",
12
- "agents": "b5aa86a9c02bb81be5621ae3397ff2dedcef972a520c2ba2f56ee7abf5613b8c"
10
+ "manifest": "4b8532409940dd845caa683bb00ac821ece7ef1496852899d1af93f5cacf5974",
11
+ "packageJson": "05688e95fcf1b2be0dab249b78c6e06235ec4402e2e691d4df4a382c62e81545",
12
+ "agents": "39bae43f14d7f80933a11fc1087d5bf376d8e0d6f4eae3a9a658185bf54508cf",
13
+ "moduleDoc:agents/install-lifecycle.md": "0b9b63732ff27f7435e68a04f481d4d4f03974065a39e46f0960964d4fac49df"
13
14
  },
14
15
  "exports": [
15
16
  ".",
@@ -2719,7 +2720,14 @@
2719
2720
  "polymorphicAssociations": 0,
2720
2721
  "uuidColumns": 38
2721
2722
  },
2722
- "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- **Assembly** (`ProductType.ASSEMBLY`, `'assembly'`): the made counterpart to `Material`, defined in `@happyvertical/smrt-manufacturing` as a cross-package STI subtype. Its own fields are columns on `products`; the enum value lives here so catalog code can filter by type without importing manufacturing.\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## Client-data runtime — reference store (#1761)\n\nProducts is the reference consumer of the `@happyvertical/smrt-web` browser client-data runtime.\n\n- **Store** — `src/lib/stores/product-collection.ts` (subpath `@happyvertical/smrt-products/stores/product-collection`). `createProductCollection(definition, options)` builds a cached, reactive `SmrtWebCollection<ProductData>` over the generated `/api/v1/products` REST surface via `createSmrtCollection`. It is plugin-agnostic on purpose: the `products` definition is a PARAMETER (not a top-level import), so importing the store never eagerly resolves `@happyvertical/smrt-virt-web` (which exists only under the SMRT Vite plugin) — a unit test / npm consumer supplies its own definition. `productsCollectionDefinition()` lazily resolves the plugin definition (dynamic `import()`; resolved for `tsc` via the `@smrt/web` ambient alias). Deliberately NOT re-exported from the always-loaded `./stores` barrel — importing it is an explicit opt-in to the engine.\n- **Live surface** — `src/lib/components/LiveProductList.svelte` binds the store with `liveCollection` from `@happyvertical/smrt-svelte/web` (live rows + optimistic insert); it takes the definition as a prop. `src/app/pages/LiveProductsPage.svelte` (route `#live`) resolves the component AND the definition via `import()` and passes the definition down.\n- **Code-split (ratified condition ①)** — the ~76 kB TanStack engine must NEVER load on public / smrt-sites pages. `LiveProductsPage` reaches `LiveProductList` and the store ONLY through dynamic `import()`, so the engine lands in a lazily-loaded chunk; the app entry stays engine-free. NEVER import the store or live component statically from an app-root / public entry. `scripts/check-web-engine-code-split.mjs` (wrapped by `src/web-engine-code-split.spec.ts`, `pnpm test:code-split`) proves this against the REAL app graph: it runs the `--mode standalone` vite build and asserts, over the emitted rollup chunk graph, that the entry's static-import closure has zero `@tanstack/*` and the engine is reached only via `dynamicImports`. If the build hits the pre-existing nested-worktree `Tsconfig not found` failure locally, it falls back to a static-import-graph walk of the real `src/app/main.ts` (CI runs the real build).\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 tenants can share a slug on `Product`/`Category` (smrt#2360)** — a tenant-scoped STI base keys on `(tenant_id, slug, context, _meta_type)` by default, on the schema index and the runtime upsert alike, so no per-tenant slug namespacing and no `conflictColumns` override is needed. Global rows (`tenantId = null`) still dedup among themselves through the SDK's null-aware upsert. Existing databases pick the new key up through `smrt db:migrate` (same-name index swap) — deploy the code and the migration together, since neither version's create binds against the other's index.\n- **`price` is integer minor units** (cents, satoshis) — `$19.99` is `1999`. Money is exact, so `price` initializes `= 0`; writing `19.99` is the bug, and PostgreSQL rejects it with `22P02` while SQLite's affinity stores it and hides it from this package's SQLite suites. `pnpm --filter @happyvertical/smrt-products test:postgres` is the lane that holds the line (#2361). Because `Product` is the STI base, the column is shared by every consumer subtype. `Material.costPerUnit` is a `Meta<number>` in `_meta_data` rather than a column, so it is outside this rule.\n- **Module Federation marked experimental**: may change\n",
2723
+ "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- **Assembly** (`ProductType.ASSEMBLY`, `'assembly'`): the made counterpart to `Material`, defined in `@happyvertical/smrt-manufacturing` as a cross-package STI subtype. Its own fields are columns on `products`; the enum value lives here so catalog code can filter by type without importing manufacturing.\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## Client-data runtime — reference store (#1761)\n\nProducts is the reference consumer of the `@happyvertical/smrt-web` browser client-data runtime.\n\n- **Store** — `src/lib/stores/product-collection.ts` (subpath `@happyvertical/smrt-products/stores/product-collection`). `createProductCollection(definition, options)` builds a cached, reactive `SmrtWebCollection<ProductData>` over the generated `/api/v1/products` REST surface via `createSmrtCollection`. It is plugin-agnostic on purpose: the `products` definition is a PARAMETER (not a top-level import), so importing the store never eagerly resolves `@happyvertical/smrt-virt-web` (which exists only under the SMRT Vite plugin) — a unit test / npm consumer supplies its own definition. `productsCollectionDefinition()` lazily resolves the plugin definition (dynamic `import()`; resolved for `tsc` via the `@smrt/web` ambient alias). Deliberately NOT re-exported from the always-loaded `./stores` barrel — importing it is an explicit opt-in to the engine.\n- **Live surface** — `src/lib/components/LiveProductList.svelte` binds the store with `liveCollection` from `@happyvertical/smrt-svelte/web` (live rows + optimistic insert); it takes the definition as a prop. `src/app/pages/LiveProductsPage.svelte` (route `#live`) resolves the component AND the definition via `import()` and passes the definition down.\n- **Code-split (ratified condition ①)** — the ~76 kB TanStack engine must NEVER load on public / smrt-sites pages. `LiveProductsPage` reaches `LiveProductList` and the store ONLY through dynamic `import()`, so the engine lands in a lazily-loaded chunk; the app entry stays engine-free. NEVER import the store or live component statically from an app-root / public entry. `scripts/check-web-engine-code-split.mjs` (wrapped by `src/web-engine-code-split.spec.ts`, `pnpm test:code-split`) proves this against the REAL app graph: it runs the `--mode standalone` vite build and asserts, over the emitted rollup chunk graph, that the entry's static-import closure has zero `@tanstack/*` and the engine is reached only via `dynamicImports`. If the build hits the pre-existing nested-worktree `Tsconfig not found` failure locally, it falls back to a static-import-graph walk of the real `src/app/main.ts` (CI runs the real build).\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. **Run the reviewed application schema migration under the migration identity before runtime startup** to create `product_skus` with the right PRIMARY KEY, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes derived from the `Sku` model. Runtime [verifies existing tables](https://github.com/happyvertical/smrt/blob/main/packages/core/src/schema/table-verifier.ts); it does not create application schema.\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- **Workspace install lifecycle**: `scripts/prepare.mjs` distinguishes unbuilt prerequisites from build errors; `pnpm test` runs real pnpm lifecycle fixtures before Vitest. See [install lifecycle and test contract](agents/install-lifecycle.md).\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 tenants can share a slug on `Product`/`Category` (smrt#2360)** — a tenant-scoped STI base keys on `(tenant_id, slug, context, _meta_type)` by default, on the schema index and the runtime upsert alike, so no per-tenant slug namespacing and no `conflictColumns` override is needed. Global rows (`tenantId = null`) still dedup among themselves through the SDK's null-aware upsert. Existing databases pick the new key up through `smrt db:migrate` (same-name index swap) — deploy the code and the migration together, since neither version's create binds against the other's index.\n- **`price` is integer minor units** (cents, satoshis) — `$19.99` is `1999`. Money is exact, so `price` initializes `= 0`; writing `19.99` is the bug, and PostgreSQL rejects it with `22P02` while SQLite's affinity stores it and hides it from this package's SQLite suites. `pnpm --filter @happyvertical/smrt-products test:postgres` is the lane that holds the line (#2361). Because `Product` is the STI base, the column is shared by every consumer subtype. `Material.costPerUnit` is a `Meta<number>` in `_meta_data` rather than a column, so it is outside this rule.\n- **Module Federation marked experimental**: may change\n",
2724
+ "moduleDocs": [
2725
+ {
2726
+ "path": "agents/install-lifecycle.md",
2727
+ "module": "install-lifecycle",
2728
+ "content": "# Products install lifecycle (#4316)\n\n`scripts/prepare.mjs` defers its opportunistic source build while declared\nproduction workspace dependencies lack compiled `main`/`types` files or core's\npublic `./vite-plugin` import/types files. A directory containing only manifest\nmetadata is not a completed build. Deferral names the missing prerequisites;\n`pnpm build` performs the normal dependency-ordered workspace build.\n\nMissing, malformed or incorrectly linked installed dependency manifests are\nerrors. Once prerequisites exist, prepare invokes the real build and propagates\nits exit status or signal. It never converts a failed build into success.\n\nPacked manifests use compiled `main`/`types` artifacts without rebuilding source;\nmissing artifacts fail. Keep the helper in the published `files` allow-list.\nThe fixtures also verify pnpm's actual packed lifecycle metadata.\n\nRun `node --test packages/products/scripts/prepare.test.mjs` from the repository\nroot, or the package's normal `pnpm test` (lifecycle checks followed by Vitest).\n\n| Behavior | Trigger | Positive | Negative | Actor/context | Executor/transaction | Runtime/dialect | External edge | Level | Command |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| Defer only for unbuilt prerequisites | Cold or core-only install | Install names missing files and defers | Complete prerequisites run the build | Developer/package installer | Filesystem and child process; no SQL transaction | Node 26/pnpm 11; no DB | Real workspace install lifecycle | Integration | `node --test packages/products/scripts/prepare.test.mjs` |\n| Metadata is not compiled readiness | Metadata-only dist or missing types/plugin output | Explicit deferral until required files exist | Dist path that is a regular file fails | Installer in partially built workspace | Filesystem; no SQL transaction | Node 26/pnpm 11; no DB | Declared package entrypoints | Integration | `node --test packages/products/scripts/prepare.test.mjs` |\n| Invalid installs remain errors | Dependency absent, malformed or mislinked | Valid dependency manifests proceed | Invalid installed identity/JSON fails before deferral | Installer | Filesystem; no SQL transaction | Node 26/pnpm 11; no DB | Installed workspace links | Integration | `node --test packages/products/scripts/prepare.test.mjs` |\n| Preserve real build failures | Prepare starts after readiness | Successful child emits output | Nonzero exit or signal fails install | Installer with complete prerequisites | npm child; no SQL transaction | Node 26/pnpm 11; no DB | Child lifecycle termination | Integration | `node --test packages/products/scripts/prepare.test.mjs` |\n| Preserve compiled consumers | Pack/unpack or non-workspace install | Compiled artifacts are retained without rebuilding | Missing compiled artifact is actionable | Package publisher/consumer | Tarball and filesystem; no SQL transaction | Node 26/pnpm 11; no DB | Packed metadata and files allow-list | Packaging integration | `node --test packages/products/scripts/prepare.test.mjs` |\n"
2729
+ }
2730
+ ],
2723
2731
  "recipes": [
2724
2732
  {
2725
2733
  "id": "products.clothing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-products",
3
- "version": "0.57.30",
3
+ "version": "0.57.31",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "SMRT products module: triple-purpose microservice template for standalone apps, federated modules, and NPM libraries",
6
6
  "author": "HappyVertical",
@@ -13,7 +13,9 @@
13
13
  "dist/app",
14
14
  "dist/federation",
15
15
  "dist/lib",
16
- "AGENTS.md"
16
+ "AGENTS.md",
17
+ "agents",
18
+ "scripts/prepare.mjs"
17
19
  ],
18
20
  "exports": {
19
21
  ".": {
@@ -27,10 +29,12 @@
27
29
  },
28
30
  "./components": {
29
31
  "types": "./dist/lib/components.d.ts",
32
+ "svelte": "./dist/lib/components.js",
30
33
  "default": "./dist/lib/components.js"
31
34
  },
32
35
  "./stores": {
33
36
  "types": "./dist/lib/stores.d.ts",
37
+ "svelte": "./dist/lib/stores.js",
34
38
  "default": "./dist/lib/stores.js"
35
39
  },
36
40
  "./stores/product-collection": {
@@ -57,13 +61,13 @@
57
61
  "@happyvertical/ai": "^0.108.0",
58
62
  "@happyvertical/files": "^0.108.0",
59
63
  "@happyvertical/logger": "^0.108.0",
60
- "@happyvertical/smrt-assets": "0.57.30",
61
- "@happyvertical/smrt-core": "0.57.30",
62
- "@happyvertical/smrt-scanner": "0.57.30",
63
- "@happyvertical/smrt-svelte": "0.57.30",
64
- "@happyvertical/smrt-tenancy": "0.57.30",
65
- "@happyvertical/smrt-ui": "0.57.30",
66
- "@happyvertical/smrt-web": "0.57.30",
64
+ "@happyvertical/smrt-assets": "0.57.31",
65
+ "@happyvertical/smrt-core": "0.57.31",
66
+ "@happyvertical/smrt-scanner": "0.57.31",
67
+ "@happyvertical/smrt-svelte": "0.57.31",
68
+ "@happyvertical/smrt-tenancy": "0.57.31",
69
+ "@happyvertical/smrt-ui": "0.57.31",
70
+ "@happyvertical/smrt-web": "0.57.31",
67
71
  "@happyvertical/sql": "^0.108.0",
68
72
  "@happyvertical/utils": "^0.108.0",
69
73
  "cors": "^2.8.6",
@@ -84,7 +88,7 @@
84
88
  "@types/cors": "2.8.19",
85
89
  "@types/express": "^5.0.6",
86
90
  "@types/node": "24.13.6",
87
- "@happyvertical/smrt-vitest": "0.57.30",
91
+ "@happyvertical/smrt-vitest": "0.57.31",
88
92
  "concurrently": "^10.0.5",
89
93
  "svelte": "^5.57.1",
90
94
  "svelte-check": "^4.7.6",
@@ -128,7 +132,7 @@
128
132
  "start:standalone": "node dist/app/main.js",
129
133
  "start:federation": "node dist/federation/main.js",
130
134
  "preview": "vite preview",
131
- "test": "vitest run",
135
+ "test": "node --test scripts/prepare.test.mjs && vitest run",
132
136
  "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/price-field.postgres.test.ts",
133
137
  "test:watch": "vitest",
134
138
  "test:code-split": "node scripts/check-web-engine-code-split.mjs",
@@ -0,0 +1,67 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const packageDir = fileURLToPath(new URL('../', import.meta.url));
7
+ const manifest = JSON.parse(
8
+ readFileSync(resolve(packageDir, 'package.json'), 'utf8'),
9
+ );
10
+ const workspaceDependencies = Object.entries(manifest.dependencies ?? {}).filter(
11
+ ([, version]) => version.startsWith('workspace:'),
12
+ );
13
+
14
+ if (workspaceDependencies.length === 0) {
15
+ // pnpm rewrites workspace ranges when packing. Published installs use dist.
16
+ for (const field of ['main', 'types']) {
17
+ const artifact = manifest[field];
18
+ if (
19
+ typeof artifact !== 'string' ||
20
+ !statSync(resolve(packageDir, artifact), { throwIfNoEntry: false })?.isFile()
21
+ ) {
22
+ throw new Error(`Products prepare: missing compiled ${field} artifact (${artifact}); install a complete published package or build from the workspace source.`);
23
+ }
24
+ }
25
+ } else {
26
+ const missing = [];
27
+ for (const [name] of workspaceDependencies) {
28
+ const dependencyDir = resolve(packageDir, 'node_modules', name);
29
+ // Read directly: package exports may hide package.json, and missing installed
30
+ // dependencies are errors rather than a reason to defer the build.
31
+ const dependency = JSON.parse(
32
+ readFileSync(resolve(dependencyDir, 'package.json'), 'utf8'),
33
+ );
34
+ if (dependency.name !== name) {
35
+ throw new Error(`Products prepare: expected installed workspace dependency ${name}, received ${dependency.name}`);
36
+ }
37
+ const output = resolve(dependencyDir, 'dist');
38
+ const stats = statSync(output, { throwIfNoEntry: false });
39
+ if (stats && !stats.isDirectory()) {
40
+ throw new Error(`Products prepare: ${name}/dist must be a build output directory`);
41
+ }
42
+ const artifacts = [dependency.main, dependency.types];
43
+ if (name === '@happyvertical/smrt-core') {
44
+ const plugin = dependency.exports?.['./vite-plugin'];
45
+ artifacts.push(plugin?.import, plugin?.types);
46
+ }
47
+ for (const artifact of artifacts) {
48
+ if (typeof artifact !== 'string' || artifact.length === 0) {
49
+ throw new Error(`Products prepare: ${name} must declare main/types build outputs and core must declare vite-plugin import/types exports`);
50
+ }
51
+ if (!statSync(resolve(dependencyDir, artifact), { throwIfNoEntry: false })?.isFile()) {
52
+ missing.push(`${name}/${artifact.replace(/^\.\//, '')}`);
53
+ }
54
+ }
55
+ }
56
+ if (missing.length > 0) {
57
+ console.log(`Deferring products build; missing workspace build outputs: ${missing.join(', ')}. Run pnpm build after installation.`);
58
+ } else {
59
+ const result = spawnSync('npm', ['run', 'build'], {
60
+ cwd: packageDir,
61
+ stdio: 'inherit',
62
+ });
63
+ if (result.error) throw result.error;
64
+ if (result.signal) process.kill(process.pid, result.signal);
65
+ else process.exitCode = result.status;
66
+ }
67
+ }
@@ -1,2 +0,0 @@
1
- import "./__smrt-register__-KK4pvFca.js";
2
- import "./Sku-D9O8I6NV.js";