@doswiftly/storefront-operations 12.0.0 → 13.0.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 CHANGED
@@ -27,7 +27,7 @@ consumer's `codegen.ts` references this package's `.graphql` files as
27
27
  live in the consumer's repo.
28
28
 
29
29
  <!-- AUTOGEN:STATS:BEGIN — auto-regenerated, do not edit by hand -->
30
- - **Schema version**: 12.0.0
30
+ - **Schema version**: 13.0.0
31
31
  - **Queries**: 49
32
32
  - **Mutations**: 40
33
33
  - **Fragments**: 100
package/CHANGELOG.md CHANGED
@@ -1,5 +1,181 @@
1
1
  # Changelog
2
2
 
3
+ ## 13.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 783ce01: Faceted aggregation accuracy + DX naming consolidation w `productFilters` API. Plus boolean facet count dla "availableForSale" + payload cleanup w Cart query.
8
+
9
+ ## BREAKING — `AvailableFilters.matchCount` removed → `totalCount`
10
+
11
+ Pole `matchCount` zostało usunięte. Użyj `totalCount` — semantyka identyczna (produkty w current context PRZED zaaplikowaniem faceted filters), nazwa zaligned z `ProductConnection.totalCount` (Relay Connection spec).
12
+
13
+ **Migration** (1:1 rename):
14
+
15
+ ```graphql
16
+ # PRZED
17
+ query Listing {
18
+ productFilters {
19
+ matchCount # ❌ removed
20
+ activeCount
21
+ }
22
+ }
23
+
24
+ # PO
25
+ query Listing {
26
+ productFilters {
27
+ totalCount # ✅ same semantics, Relay-aligned
28
+ activeCount
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## BREAKING — `ProductFilter.available: true` semantics
34
+
35
+ Filter teraz includuje untracked produkty (gift cards, digital, made-to-order — wszystko z `trackQuantity = false`). Semantyka zgodna z `Product.isAvailable` resolver — single source of truth dla "co jest sellable".
36
+
37
+ **Pre-fix**: `available: true` zwracał tylko produkty z fizycznym stockiem (`available > 0`), wykluczał untracked. Storefront widział "Dostępne (3)" w sidebar (computed via `isAvailable`), po kliku dostawał 2 produkty (SQL filter różny).
38
+
39
+ **Post-fix**: oba źródła zwracają to samo.
40
+
41
+ Jeśli polegałeś na exclusion untracked przez `available: true` (np. żeby ukryć gift cards) — użyj `ProductFilter.type` lub osobnej logiki klienckiej.
42
+
43
+ ## NEW — `AvailableFilters.availableCount: Int!`
44
+
45
+ Nowy boolean facet count — liczba produktów spełniających `Product.isAvailable` w current context. Storefront UI renderuje "Dostępne (N)" w sidebar facet panel.
46
+
47
+ Exclude-self: gdy `AvailableFiltersInput.available` jest provided w `currentFilters`, `availableCount` IGNORUJE ten filter (pokazuje "ile produktów ten facet odsłoni gdy włączysz") — inne facets aplikują go normalnie.
48
+
49
+ ## NEW — `AvailableFiltersInput.available: Boolean`
50
+
51
+ Storefront może passować obecny `available` filter context do `productFilters` query — wymagane dla exclude-self semantyki. Pomiń (null/undefined) gdy filtr "Dostępne" nie jest aktywny w UI.
52
+
53
+ ## IMPROVEMENT — Faceted count accuracy (`productCount` mismatch eliminated)
54
+
55
+ Per-attribute / per-brand / per-category `productCount` teraz używa exclude-self aggregation pattern. Facet count MATCHUJE `products(filters).totalCount` gdy storefront aplikuje facet jako filter.
56
+
57
+ **Pre-fix**: `productFilters({categoryId, available: true}).attributes[producent].filterValues[Funko].productCount = 4` (ignoring `available`), `products({categoryId, available: true, attributes: [Funko]}).totalCount = 3` (3 sellable + 1 OOS Funko). Storefront pokazywał "Funko (4)", user dostawał 3 — utracone zaufanie do countów.
58
+
59
+ **Post-fix**: oba zwracają 3. Facet ignoruje WYŁĄCZNIE swój wymiar w `currentFilters`, aplikuje pozostałe + `available` + context (categoryId/collectionId/searchQuery).
60
+
61
+ ## IMPROVEMENT — Cart query payload size
62
+
63
+ `Cart.lines` query w SDK używał obu form (`edges + nodes`) z których konsumowane były wyłącznie `nodes`. Backend serializował `CartLine` fragment 2× per request. Usunięcie duplikatu — redukcja payload typowo 5-20KB per cart request (proportional do liczby pozycji).
64
+
65
+ Brak API change widzialnej dla consumer — SDK używa `cart.lines.nodes` jak wcześniej. Internal SDK improvement.
66
+
67
+ ## Example: end-to-end facet sidebar query (post-rename)
68
+
69
+ ```graphql
70
+ query ProductListing($input: AvailableFiltersInput) {
71
+ productFilters(input: $input) {
72
+ totalCount # ile produktów w current context (przed faceted filters)
73
+ availableCount # ile sellable (boolean facet, exclude-self)
74
+ activeCount # input.currentFilters.length
75
+ attributes {
76
+ handle
77
+ filterValues {
78
+ value
79
+ productCount
80
+ } # per-value facet count (exclude-self per attrDef)
81
+ }
82
+ brands {
83
+ slug
84
+ name
85
+ productCount
86
+ } # per-brand facet count
87
+ categories {
88
+ slug
89
+ name
90
+ productCount
91
+ } # per-category facet count
92
+ priceRange {
93
+ min {
94
+ amount
95
+ currencyCode
96
+ }
97
+ max {
98
+ amount
99
+ currencyCode
100
+ }
101
+ }
102
+ }
103
+
104
+ products(
105
+ filters: [
106
+ { available: true }
107
+ { attributes: [{ attributeId: "producent", values: ["Funko"] }] }
108
+ ]
109
+ ) {
110
+ totalCount
111
+ nodes {
112
+ id
113
+ title
114
+ isAvailable
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ Gdy storefront aplikuje "Dostępne + Producent=Funko" jako filter w UI:
121
+
122
+ ```graphql
123
+ variables: {
124
+ input: {
125
+ available: true,
126
+ currentFilters: [{ attributeId: "producent", values: ["Funko"] }]
127
+ }
128
+ }
129
+ ```
130
+
131
+ - `availableCount` = liczba sellable Funko w context (ignoring `input.available`, applying `producent: Funko`)
132
+ - `attributes[producent].filterValues[Funko].productCount` = liczba sellable produktów (ignoring `producent`, applying `available: true`) = `products(filters).totalCount`
133
+
134
+ ### Minor Changes
135
+
136
+ - e64cfc5: Brand entity available in Storefront API: filter products by canonical brand + read brand details per product.
137
+
138
+ **What's new in Storefront API**
139
+ - **`Product.brand: BrandSummary`** — every product now exposes its canonical brand (when assigned). Returns `{ id, name, slug, logo }` or `null` if the product has no brand. Resolved via DataLoader so listing 50 products incurs at most 1 brand query (no N+1).
140
+ - **`ProductFilter.brand: BrandFilter`** — new filter input accepting either `{ id: "uuid" }` or `{ slug: "funko" }`. Multiple `brand` entries in `filters[]` use OR semantics. Combine with other filters (price, tags, attributes) using AND across different field names.
141
+ - **`AvailableFilters.brands: [BrandFilterValue!]!`** — `productFilters` query now returns aggregated brands present in the current product set, each with `productCount`. Build a brand facet (checkbox list or dropdown with logo) directly from this response. Only active brands with at least one product in context are included.
142
+
143
+ **Example: brand facet + filter on a listing page**
144
+
145
+ ```graphql
146
+ query Listing {
147
+ productFilters {
148
+ brands {
149
+ id
150
+ name
151
+ slug
152
+ logo
153
+ productCount
154
+ }
155
+ }
156
+ products(filters: [{ brand: { slug: "funko" } }]) {
157
+ totalCount
158
+ nodes {
159
+ id
160
+ title
161
+ brand {
162
+ name
163
+ slug
164
+ logo
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ **Migration**
172
+ - Existing `Product.vendor: String` (legacy free-text) remains supported. New brand-based filtering uses `ProductFilter.brand` instead of `productVendor`.
173
+ - No breaking changes. All new fields are nullable / optional — queries written before this release continue to work.
174
+
175
+ **SDK**
176
+
177
+ Regenerated types include `BrandSummary`, `BrandFilter`, `BrandFilterValue`, and `Product.brand`. Import them from `@doswiftly/storefront-operations` schema types.
178
+
3
179
  ## 12.0.0
4
180
 
5
181
  ### Major Changes
package/README.md CHANGED
@@ -247,7 +247,7 @@ full executable body of each operation.
247
247
 
248
248
  | Operation | Description |
249
249
  | --- | --- |
250
- | `ProductFilters` | Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, or `searchQuery`. For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, per-category counts, `activeCount` (length of `currentFilters` input), and `matchCount` (total products in the context). Use to render filter sidebars on listing/search pages. |
250
+ | `ProductFilters` | Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, `searchQuery`, optional `available` (boolean for the availability facet), and optional `currentFilters` (array of attribute filters currently applied by the UI). For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, `brands`, per-category counts, `activeCount` (length of `currentFilters`), `totalCount` (products in context — Relay-aligned), and `availableCount` (boolean facet count for `availableForSale`). All per-facet counts use exclude-self aggregation: a facet's count IGNORES its own currently-applied filter and APPLIES other filters — so the count reflects "products if this facet were toggled" rather than "products currently visible". Untracked inventory (gift cards, digital, made-to-order) is always counted as available. Use to render filter sidebars on listing/search pages. |
251
251
 
252
252
  #### Loyalty Program
253
253
 
@@ -554,7 +554,7 @@ full executable body of each operation.
554
554
  | `AttributeDefinition` | `AttributeDefinition` | Filterable attribute exposed on the storefront — name, type (SELECT / CHECKBOX / SLIDER / etc.), filterability flags, display order, and either discrete `filterValues[]` or numeric `rangeBounds`. Spread inside `availableFilters.attributes[]`. |
555
555
  | `PriceRangeFilter` | `PriceRange` | Min / max price range across products in the current listing context. Use to bound the price slider. |
556
556
  | `CategoryFilterOption` | `CategoryFilterOption` | One category option in the categories filter — id, name, slug, count of products in this category within the current listing context, level + parentId for tree rendering. |
557
- | `AvailableFilters` | `AvailableFilters` | Full result of the `productFilters($input)` query — attribute filters, price range, category filters, count of currently active filters, total products matching the context. Spread on listing pages to render filter sidebars. |
557
+ | `AvailableFilters` | `AvailableFilters` | Full result of the `productFilters($input)` query — attribute filters, price range, category filters, brands, count of currently active filters, total products in context (Relay-aligned `totalCount`), and boolean facet count for `availableForSale` (`availableCount`). Spread on listing pages to render filter sidebars. Per-facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`, `availableCount`) use exclude-self aggregation: when a dimension is in `input.currentFilters` / `input.available`, its own facet count IGNORES that filter (shows "how many products would appear if this facet were toggled"), while OTHER dimensions APPLY their filters. This matches industry convention for accurate facet UX. |
558
558
 
559
559
  #### Loyalty Program
560
560
 
package/fragments.graphql CHANGED
@@ -1012,7 +1012,7 @@ fragment CategoryFilterOption on CategoryFilterOption {
1012
1012
  parentId
1013
1013
  }
1014
1014
 
1015
- # Full result of the `productFilters($input)` query — attribute filters, price range, category filters, count of currently active filters, total products matching the context. Spread on listing pages to render filter sidebars.
1015
+ # Full result of the `productFilters($input)` query — attribute filters, price range, category filters, brands, count of currently active filters, total products in context (Relay-aligned `totalCount`), and boolean facet count for `availableForSale` (`availableCount`). Spread on listing pages to render filter sidebars. Per-facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`, `availableCount`) use exclude-self aggregation: when a dimension is in `input.currentFilters` / `input.available`, its own facet count IGNORES that filter (shows "how many products would appear if this facet were toggled"), while OTHER dimensions APPLY their filters. This matches industry convention for accurate facet UX.
1016
1016
  fragment AvailableFilters on AvailableFilters {
1017
1017
  attributes {
1018
1018
  ...AttributeDefinition
@@ -1024,7 +1024,8 @@ fragment AvailableFilters on AvailableFilters {
1024
1024
  ...CategoryFilterOption
1025
1025
  }
1026
1026
  activeCount
1027
- matchCount
1027
+ totalCount
1028
+ availableCount
1028
1029
  }
1029
1030
 
1030
1031
  # ============================================
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # DoSwiftly Storefront Operations — Full Reference
2
2
 
3
- > Schema version: **12.0.0**
3
+ > Schema version: **13.0.0**
4
4
  > 49 queries · 40 mutations · 100 fragments
5
5
 
6
6
  Auto-generated from `.graphql` source files. Do not edit by hand — this file is
@@ -705,7 +705,7 @@ query AvailableShippingMethods($address: ShippingAddressInput!, $cart: CartShipp
705
705
 
706
706
  **Section**: Attribute Filters
707
707
 
708
- **Description**: Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, or `searchQuery`. For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, per-category counts, `activeCount` (length of `currentFilters` input), and `matchCount` (total products in the context). Use to render filter sidebars on listing/search pages.
708
+ **Description**: Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, `searchQuery`, optional `available` (boolean for the availability facet), and optional `currentFilters` (array of attribute filters currently applied by the UI). For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, `brands`, per-category counts, `activeCount` (length of `currentFilters`), `totalCount` (products in context — Relay-aligned), and `availableCount` (boolean facet count for `availableForSale`). All per-facet counts use exclude-self aggregation: a facet's count IGNORES its own currently-applied filter and APPLIES other filters — so the count reflects "products if this facet were toggled" rather than "products currently visible". Untracked inventory (gift cards, digital, made-to-order) is always counted as available. Use to render filter sidebars on listing/search pages.
709
709
 
710
710
  **Variables**:
711
711
  - `$input`: `AvailableFiltersInput`
@@ -3887,7 +3887,7 @@ fragment CategoryFilterOption on CategoryFilterOption {
3887
3887
 
3888
3888
  **Section**: Attribute Filters
3889
3889
 
3890
- **Description**: Full result of the `productFilters($input)` query — attribute filters, price range, category filters, count of currently active filters, total products matching the context. Spread on listing pages to render filter sidebars.
3890
+ **Description**: Full result of the `productFilters($input)` query — attribute filters, price range, category filters, brands, count of currently active filters, total products in context (Relay-aligned `totalCount`), and boolean facet count for `availableForSale` (`availableCount`). Spread on listing pages to render filter sidebars. Per-facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`, `availableCount`) use exclude-self aggregation: when a dimension is in `input.currentFilters` / `input.available`, its own facet count IGNORES that filter (shows "how many products would appear if this facet were toggled"), while OTHER dimensions APPLY their filters. This matches industry convention for accurate facet UX.
3891
3891
 
3892
3892
  **Uses fragments**: `AttributeDefinition`, `CategoryFilterOption`, `PriceRangeFilter`
3893
3893
 
@@ -3904,7 +3904,8 @@ fragment AvailableFilters on AvailableFilters {
3904
3904
  ...CategoryFilterOption
3905
3905
  }
3906
3906
  activeCount
3907
- matchCount
3907
+ totalCount
3908
+ availableCount
3908
3909
  }
3909
3910
  ```
3910
3911
 
package/operations.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": "12.0.0",
2
+ "schemaVersion": "13.0.0",
3
3
  "queries": [
4
4
  {
5
5
  "name": "Shop",
@@ -528,7 +528,7 @@
528
528
  "name": "ProductFilters",
529
529
  "kind": "query",
530
530
  "section": "Attribute Filters",
531
- "description": "Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, or `searchQuery`. For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, per-category counts, `activeCount` (length of `currentFilters` input), and `matchCount` (total products in the context). Use to render filter sidebars on listing/search pages.",
531
+ "description": "Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, `searchQuery`, optional `available` (boolean for the availability facet), and optional `currentFilters` (array of attribute filters currently applied by the UI). For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, `brands`, per-category counts, `activeCount` (length of `currentFilters`), `totalCount` (products in context — Relay-aligned), and `availableCount` (boolean facet count for `availableForSale`). All per-facet counts use exclude-self aggregation: a facet's count IGNORES its own currently-applied filter and APPLIES other filters — so the count reflects \"products if this facet were toggled\" rather than \"products currently visible\". Untracked inventory (gift cards, digital, made-to-order) is always counted as available. Use to render filter sidebars on listing/search pages.",
532
532
  "variables": [
533
533
  {
534
534
  "name": "input",
@@ -2557,14 +2557,14 @@
2557
2557
  "name": "AvailableFilters",
2558
2558
  "kind": "fragment",
2559
2559
  "section": "Attribute Filters",
2560
- "description": "Full result of the `productFilters($input)` query — attribute filters, price range, category filters, count of currently active filters, total products matching the context. Spread on listing pages to render filter sidebars.",
2560
+ "description": "Full result of the `productFilters($input)` query — attribute filters, price range, category filters, brands, count of currently active filters, total products in context (Relay-aligned `totalCount`), and boolean facet count for `availableForSale` (`availableCount`). Spread on listing pages to render filter sidebars. Per-facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`, `availableCount`) use exclude-self aggregation: when a dimension is in `input.currentFilters` / `input.available`, its own facet count IGNORES that filter (shows \"how many products would appear if this facet were toggled\"), while OTHER dimensions APPLY their filters. This matches industry convention for accurate facet UX.",
2561
2561
  "variables": [],
2562
2562
  "fragmentRefs": [
2563
2563
  "AttributeDefinition",
2564
2564
  "CategoryFilterOption",
2565
2565
  "PriceRangeFilter"
2566
2566
  ],
2567
- "body": "fragment AvailableFilters on AvailableFilters {\n attributes {\n ...AttributeDefinition\n }\n priceRange {\n ...PriceRangeFilter\n }\n categories {\n ...CategoryFilterOption\n }\n activeCount\n matchCount\n}",
2567
+ "body": "fragment AvailableFilters on AvailableFilters {\n attributes {\n ...AttributeDefinition\n }\n priceRange {\n ...PriceRangeFilter\n }\n categories {\n ...CategoryFilterOption\n }\n activeCount\n totalCount\n availableCount\n}",
2568
2568
  "onType": "AvailableFilters"
2569
2569
  },
2570
2570
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
package/queries.graphql CHANGED
@@ -387,7 +387,7 @@ query AvailableShippingMethods($address: ShippingAddressInput!, $cart: CartShipp
387
387
  # Attribute Filters
388
388
  # ============================================
389
389
 
390
- # Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, or `searchQuery`. For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, per-category counts, `activeCount` (length of `currentFilters` input), and `matchCount` (total products in the context). Use to render filter sidebars on listing/search pages.
390
+ # Returns the dynamic facet filters available for a listing context — pass `collectionId`, `categoryId`, `searchQuery`, optional `available` (boolean for the availability facet), and optional `currentFilters` (array of attribute filters currently applied by the UI). For each visible & filterable attribute, returns either discrete value counts (for `SELECT` / `CHECKBOX` types) or numeric range bounds (for `SLIDER` types). Plus `priceRange`, `brands`, per-category counts, `activeCount` (length of `currentFilters`), `totalCount` (products in context — Relay-aligned), and `availableCount` (boolean facet count for `availableForSale`). All per-facet counts use exclude-self aggregation: a facet's count IGNORES its own currently-applied filter and APPLIES other filters — so the count reflects "products if this facet were toggled" rather than "products currently visible". Untracked inventory (gift cards, digital, made-to-order) is always counted as available. Use to render filter sidebars on listing/search pages.
391
391
  query ProductFilters($input: AvailableFiltersInput) {
392
392
  productFilters(input: $input) {
393
393
  ...AvailableFilters
package/schema.graphql CHANGED
@@ -72,6 +72,11 @@ type AttributeDefinition {
72
72
  """Attribute name (e.g., "Color", "Size")"""
73
73
  name: String!
74
74
 
75
+ """
76
+ Faza 3 Opcja A (2026-05-17): opcjonalne grupowanie atrybutu (np. "inventory", "marketing", "content"). NULL = atrybut bez grupy. Pomaga storefront-devom rozpoznać "te atrybuty są custom dane ERP" vs "te są filtrowalne cechy katalogu".
77
+ """
78
+ namespace: String
79
+
75
80
  """Range bounds (for NUMBER, CURRENCY)"""
76
81
  rangeBounds: AttributeRangeBounds
77
82
 
@@ -99,6 +104,11 @@ input AttributeFilterInput {
99
104
  """Minimum value (for NUMBER, CURRENCY)"""
100
105
  minValue: Float
101
106
 
107
+ """
108
+ Opcjonalny dyskryminator grupy atrybutów (np. "inventory", "marketing"). Faza 3 Opcja A (2026-05-17): konsolidacja MetaProperty-style custom fields pod AttributeDefinition — namespace pozwala filtrować "Marka z grupy XYZ" gdy multiple shopów ma `Marka` w różnych grupach.
109
+ """
110
+ namespace: String
111
+
102
112
  """Text search (for TEXT, TEXTAREA)"""
103
113
  textSearch: String
104
114
 
@@ -248,34 +258,51 @@ enum AttributeType {
248
258
  """Available filters for product listing"""
249
259
  type AvailableFilters {
250
260
  """
251
- Number of currently active filters (renamed from activeFilterCount in Storefront 5.0)
261
+ Liczba currently applied filters w `currentFilters` (Storefront UI renderuje "Filtry (3)" w sidebar header).
252
262
  """
253
263
  activeCount: Int!
254
264
 
255
265
  """Filterable attributes with values"""
256
266
  attributes: [AttributeDefinition!]!
257
267
 
258
- """Categories available for filtering"""
259
- categories: [CategoryFilterOption!]
268
+ """
269
+ Boolean facet count: liczba produktów w current context spełniających `Product.isAvailable` (przynajmniej jeden aktywny wariant z untracked inventory LUB available>0). Exclude-self: jeśli `available: true` jest w `currentFilters`, count IGNORUJE ten filtr i pokazuje "ile produktów dostępnych w bazowym kontekście" (nie "ile teraz widać"). Spójne semantically z `attributes[].filterValues[].productCount` (per-enum-value facet) i `brands[].productCount` (per-brand facet) — wszystkie używają exclude-self per dimension. Untracked inventory (gift cards, digital, na-zamówienie) zawsze liczy się jako available.
270
+ """
271
+ availableCount: Int!
260
272
 
261
273
  """
262
- Total products matching current context, before filters (renamed from totalProducts in Storefront 5.0)
274
+ Faza 3 (2026-05-17): canonical Brand entities z product counts w current context. Storefront UI renderuje checkbox list / dropdown z logo per brand. Klik → `filters: [{brand: {slug}}]`. Tylko brandy z >0 produktów w current product set + isActive=true.
263
275
  """
264
- matchCount: Int!
276
+ brands: [BrandFilterValue!]
277
+
278
+ """Categories available for filtering"""
279
+ categories: [CategoryFilterOption!]
265
280
 
266
281
  """Price range for filtering"""
267
282
  priceRange: PriceRange
283
+
284
+ """
285
+ Total products w current context (categoryId/collectionId/searchQuery) PRZED zaaplikowaniem faceted filters z `currentFilters`. Spójne z `ProductConnection.totalCount` Relay Connection spec — gdy `currentFilters` jest pusty, `AvailableFilters.totalCount` === `products(filters).totalCount`. Użyj do "Wszystkie produkty (N)" w sidebar header.
286
+ """
287
+ totalCount: Int!
268
288
  }
269
289
 
270
290
  """Context for available filters query"""
271
291
  input AvailableFiltersInput {
292
+ """
293
+ Czy filtr "dostępne" jest currently zaaplikowany. Mirror `ProductFilter.available`. Backend używa do exclude-self semantyki: gdy `available: true` (lub `false`) tutaj, `AvailableFilters.availableCount` IGNORUJE ten filtr (pokazuje "ile produktów ten facet odsłoni gdy włączysz"), pozostałe facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`) APLIKUJĄ go (pokazują "ile produktów w obecnym kontekście + tym facet"). Pomiń (null/undefined) gdy storefront nie aplikuje "dostępne" filter.
294
+ """
295
+ available: Boolean
296
+
272
297
  """Category ID context (supports comma-separated UUIDs)"""
273
298
  categoryId: ID
274
299
 
275
300
  """Collection ID context (supports comma-separated UUIDs)"""
276
301
  collectionId: ID
277
302
 
278
- """Currently applied filters (to update counts)"""
303
+ """
304
+ Currently applied attribute filters (to update facet counts). Per facet `productCount` używa exclude-self pattern: gdy attrId X jest w currentFilters, facet values X.* są liczone IGNORUJĄC X (pokazują "ile produktów dla X=Y w pozostałym kontekście"). Pozostałe attrIds są aplikowane.
305
+ """
279
306
  currentFilters: [AttributeFilterInput!]
280
307
 
281
308
  """Search query context"""
@@ -598,6 +625,52 @@ type BrandColors {
598
625
  secondary: BrandColorGroup
599
626
  }
600
627
 
628
+ """Filter products po brand (canonical entity)"""
629
+ input BrandFilter {
630
+ """Brand ID — exact UUID match"""
631
+ id: ID
632
+
633
+ """
634
+ Brand slug — URL-friendly identifier. Preferowany over `id` dla storefront refetch (stable po URL share).
635
+ """
636
+ slug: String
637
+ }
638
+
639
+ """Brand filter value z product count"""
640
+ type BrandFilterValue {
641
+ """Brand ID"""
642
+ id: ID!
643
+
644
+ """Brand logo URL (UI rendering)"""
645
+ logo: String
646
+
647
+ """Brand name"""
648
+ name: String!
649
+
650
+ """Liczba produktów z tym brandem w current product set"""
651
+ productCount: Int!
652
+
653
+ """Brand slug (refetch payload — paste w ProductFilter.brand.slug)"""
654
+ slug: String!
655
+ }
656
+
657
+ """
658
+ Brand summary — minimalna projekcja dla storefront catalog + filter rendering
659
+ """
660
+ type BrandSummary {
661
+ """Brand unique identifier"""
662
+ id: ID!
663
+
664
+ """Brand logo URL (CDN-served, storefront-rendered)"""
665
+ logo: String
666
+
667
+ """Brand display name (e.g. "Funko", "Nintendo")"""
668
+ name: String!
669
+
670
+ """URL-friendly slug — używane dla future /marka/[slug] landing pages"""
671
+ slug: String!
672
+ }
673
+
601
674
  """Business hours for a day"""
602
675
  type BusinessHour {
603
676
  """Closing time (HH:MM format)"""
@@ -1904,6 +1977,9 @@ type Customer implements Node {
1904
1977
  """Saved addresses (Relay Connection)"""
1905
1978
  addresses(after: String, before: String, first: Int, last: Int): MailingAddressConnection!
1906
1979
 
1980
+ """Customer custom field values (post-Opcja A unified custom fields)."""
1981
+ attributes(namespace: String): [EntityAttributeField!]!
1982
+
1907
1983
  """Company name (populated for COMPANY type)"""
1908
1984
  companyName: String
1909
1985
 
@@ -1937,16 +2013,6 @@ type Customer implements Node {
1937
2013
  """Last name"""
1938
2014
  lastName: String
1939
2015
 
1940
- """
1941
- Lista meta properties (Relay Connection) — opcjonalnie scoped do namespace
1942
- """
1943
- metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
1944
-
1945
- """
1946
- Pojedyncze meta property po (namespace, key) — dla zalogowanego klienta zwraca także private
1947
- """
1948
- metaProperty(key: String!, namespace: String!): MetaProperty
1949
-
1950
2016
  """Total orders count (UnsignedInt64 — BigInt-safe)"""
1951
2017
  orderCount: UnsignedInt64!
1952
2018
 
@@ -2347,6 +2413,39 @@ enum EmailMarketingState {
2347
2413
  UNSUBSCRIBED
2348
2414
  }
2349
2415
 
2416
+ """Attribute assignment per entity (polymorphic owner)"""
2417
+ type EntityAttributeField {
2418
+ """AttributeDefinition ID"""
2419
+ definitionId: ID!
2420
+
2421
+ """Attribute handle (URL-friendly identifier)"""
2422
+ handle: String!
2423
+
2424
+ """EntityAttribute row ID"""
2425
+ id: ID!
2426
+
2427
+ """
2428
+ Per-entity visibility override (NULL = use AttributeDefinition.isVisible).
2429
+ """
2430
+ isVisibleOverride: Boolean
2431
+
2432
+ """Attribute display name"""
2433
+ name: String!
2434
+
2435
+ """
2436
+ Optional grouping namespace (e.g. "inventory", "marketing"). NULL = ungrouped.
2437
+ """
2438
+ namespace: String
2439
+
2440
+ """Attribute data type"""
2441
+ type: AttributeType!
2442
+
2443
+ """
2444
+ Value serialized as JSON string. Storefront-dev parses according to `type` (TEXT/NUMBER/BOOLEAN/JSON/etc.). Null values serialize as JSON `null`.
2445
+ """
2446
+ value: String!
2447
+ }
2448
+
2350
2449
  """Exchange rate between currencies"""
2351
2450
  type ExchangeRate {
2352
2451
  """Source currency code"""
@@ -3269,119 +3368,6 @@ enum MenuItemType {
3269
3368
  SEARCH
3270
3369
  }
3271
3370
 
3272
- """
3273
- Payload `customerMetaPropertiesSet` — bulk upsert własnych meta properties klienta
3274
- """
3275
- type MetaPropertiesSetPayload {
3276
- """Upserted meta properties (puste gdy userErrors)"""
3277
- metaProperties: [MetaProperty!]!
3278
-
3279
- """User errors (code z `MetaPropertyErrorCode` enum)"""
3280
- userErrors: [UserError!]!
3281
- }
3282
-
3283
- """Pojedyncze pole dodatkowe (meta property) na encji commerce"""
3284
- type MetaProperty implements Node {
3285
- """Created at"""
3286
- createdAt: DateTime!
3287
-
3288
- """Unique identifier"""
3289
- id: ID!
3290
-
3291
- """
3292
- true = readable tylko przez authenticated context (admin/customer self). false (default) = readable również przez anonymous Storefront API.
3293
- """
3294
- isPrivate: Boolean!
3295
-
3296
- """
3297
- Key w obrębie namespace (3-64 chars). Unique per (ownerType, ownerId, namespace).
3298
- """
3299
- key: String!
3300
-
3301
- """
3302
- Namespace (3-64 chars). Zapobiega kolizji kluczy między aplikacjami. Reserved prefix: `doswiftly:` (platform).
3303
- """
3304
- namespace: String!
3305
-
3306
- """Typ wartości — informuje klienta jak parse value."""
3307
- type: MetaPropertyValueType!
3308
-
3309
- """Last updated at"""
3310
- updatedAt: DateTime!
3311
-
3312
- """
3313
- Wartość — zawsze String. Klient parsuje zgodnie z polem `type` (np. INTEGER → parseInt, JSON → JSON.parse).
3314
- """
3315
- value: String!
3316
- }
3317
-
3318
- """Paginated meta property list (Relay Connection)"""
3319
- type MetaPropertyConnection {
3320
- """Edges (cursor + node pairs)"""
3321
- edges: [MetaPropertyEdge!]!
3322
-
3323
- """Nodes (shortcut bez cursor)"""
3324
- nodes: [MetaProperty!]!
3325
-
3326
- """Page info"""
3327
- pageInfo: PageInfo!
3328
-
3329
- """Total count (max po wszystkich pages)"""
3330
- totalCount: Int!
3331
- }
3332
-
3333
- """Payload `customerMetaPropertyDelete`"""
3334
- type MetaPropertyDeletePayload {
3335
- """ID usuniętego meta property (null gdy userErrors)"""
3336
- deletedId: ID
3337
-
3338
- """User errors (code z `MetaPropertyErrorCode` enum)"""
3339
- userErrors: [UserError!]!
3340
- }
3341
-
3342
- """Meta property edge (Relay pagination)"""
3343
- type MetaPropertyEdge {
3344
- """Cursor for pagination"""
3345
- cursor: String!
3346
-
3347
- """Meta property node"""
3348
- node: MetaProperty!
3349
- }
3350
-
3351
- """Input dla pojedynczego meta property w bulk set"""
3352
- input MetaPropertyInput {
3353
- """Visibility — true = admin/customer-only, false = storefront-readable"""
3354
- isPrivate: Boolean = false
3355
-
3356
- """Key (3-64 chars)"""
3357
- key: String!
3358
-
3359
- """Namespace (3-64 chars, NOT starting with `doswiftly:`)"""
3360
- namespace: String!
3361
-
3362
- """Typ wartości (driver walidacji backend-side)"""
3363
- type: MetaPropertyValueType!
3364
-
3365
- """Wartość jako String (parse zgodnie z type)"""
3366
- value: String!
3367
- }
3368
-
3369
- """
3370
- Typy wartości — STRING (max 255 chars), TEXT (no cap), INTEGER, DECIMAL, BOOLEAN ("true"/"false"), JSON (stringified), DATE (YYYY-MM-DD), DATE_TIME (ISO 8601), URL (http(s)://...), COLOR (#RRGGBB / #RRGGBBAA hex).
3371
- """
3372
- enum MetaPropertyValueType {
3373
- BOOLEAN
3374
- COLOR
3375
- DATE
3376
- DATE_TIME
3377
- DECIMAL
3378
- INTEGER
3379
- JSON
3380
- STRING
3381
- TEXT
3382
- URL
3383
- }
3384
-
3385
3371
  """Monetary value with currency"""
3386
3372
  type Money {
3387
3373
  """Decimal money amount"""
@@ -3463,12 +3449,6 @@ type Mutation {
3463
3449
  """Logout customer (clears auth cookie)"""
3464
3450
  customerLogout: CustomerLogoutPayload!
3465
3451
 
3466
- """Bulk upsert własnych meta properties klienta (Bearer token wymagany)"""
3467
- customerMetaPropertiesSet(properties: [MetaPropertyInput!]!): MetaPropertiesSetPayload!
3468
-
3469
- """Usuwa pojedyncze meta property klienta po (namespace, key)"""
3470
- customerMetaPropertyDelete(key: String!, namespace: String!): MetaPropertyDeletePayload!
3471
-
3472
3452
  """Refresh access token"""
3473
3453
  customerRefreshToken: CustomerRefreshTokenPayload!
3474
3454
 
@@ -3576,6 +3556,9 @@ type Order implements Node {
3576
3556
  """
3577
3557
  accessToken: String!
3578
3558
 
3559
+ """Order custom field values (post-Opcja A unified custom fields)."""
3560
+ attributes(namespace: String): [EntityAttributeField!]!
3561
+
3579
3562
  """Czy storefront może zainicjować płatność dla tego order"""
3580
3563
  canCreatePayment: Boolean!
3581
3564
 
@@ -3602,12 +3585,6 @@ type Order implements Node {
3602
3585
  """
3603
3586
  lineItems(after: String, first: Int = 10): OrderLineItemConnection!
3604
3587
 
3605
- """Lista meta properties (Relay Connection)"""
3606
- metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
3607
-
3608
- """Pojedyncze meta property po (namespace, key)"""
3609
- metaProperty(key: String!, namespace: String!): MetaProperty
3610
-
3611
3588
  """Order number (human-readable)"""
3612
3589
  orderNumber: String!
3613
3590
 
@@ -4014,6 +3991,11 @@ type Product implements Node {
4014
3991
  """Average rating (1-5)"""
4015
3992
  averageRating: Float
4016
3993
 
3994
+ """
3995
+ Canonical brand entity (Faza 3, 2026-05-17). Resolved via DataLoader (N+1 safe dla list queries). Null gdy product nie ma przypisanego brand_id.
3996
+ """
3997
+ brand: BrandSummary
3998
+
4017
3999
  """
4018
4000
  Wszystkie kategorie do których produkt należy (M2M przez ProductCategory junction). Posortowane po junction.sortOrder ASC. Empty list gdy produkt nie jest w żadnej kategorii.
4019
4001
  """
@@ -4056,12 +4038,6 @@ type Product implements Node {
4056
4038
  """
4057
4039
  isPurchasable: Boolean!
4058
4040
 
4059
- """Lista meta properties — Storefront API filtruje isPrivate=false"""
4060
- metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4061
-
4062
- """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4063
- metaProperty(key: String!, namespace: String!): MetaProperty
4064
-
4065
4041
  """
4066
4042
  Per-product option definitions (Color, Size, …) with their available values. Use these to build a variant picker without aggregating `selectedOptions` manually.
4067
4043
  """
@@ -4109,7 +4085,9 @@ type Product implements Node {
4109
4085
  """Product variants (Relay Connection)"""
4110
4086
  variants(after: String, before: String, first: Float, last: Float): ProductVariantConnection!
4111
4087
 
4112
- """Vendor/brand name"""
4088
+ """
4089
+ Legacy vendor/brand name (free-text). Preferred: `brand` field (canonical Brand entity, Faza 3).
4090
+ """
4113
4091
  vendor: String
4114
4092
 
4115
4093
  """Catalog visibility (Faza 1.5) — PUBLIC | HIDDEN | BUNDLE_ONLY"""
@@ -4245,13 +4223,18 @@ type ProductEdge {
4245
4223
  """Single product filter"""
4246
4224
  input ProductFilter {
4247
4225
  """
4248
- DoSwiftly: dynamic attribute filters (configurator system). Product/variant metafield filters will be added in Wave 5.
4226
+ DoSwiftly: dynamic attribute filters (configurator + custom fields system). Optional `namespace` w AttributeFilterInput dyskryminuje per grupa atrybutów.
4249
4227
  """
4250
4228
  attributes: [AttributeFilterInput!]
4251
4229
 
4252
4230
  """Filter by availability for sale"""
4253
4231
  available: Boolean
4254
4232
 
4233
+ """
4234
+ Faza 3 (2026-05-17): filter by canonical Brand entity. Wybierz po `slug` (URL-stable) lub `id` (raw UUID). Multiple `brand` entries w `filters[]` array = OR semantics. Vendor (legacy free-text) wciąż wspierany via `productVendor`.
4235
+ """
4236
+ brand: BrandFilter
4237
+
4255
4238
  """Filter by category"""
4256
4239
  category: CategoryFilter
4257
4240
 
@@ -4437,6 +4420,9 @@ enum ProductTypeEnum {
4437
4420
 
4438
4421
  """Product variant - purchasable unit"""
4439
4422
  type ProductVariant {
4423
+ """Variant custom field values (post-Opcja A unified custom fields)."""
4424
+ attributes(namespace: String): [EntityAttributeField!]!
4425
+
4440
4426
  """
4441
4427
  Available stock (computed: stock - reserved). Null when track is disabled.
4442
4428
  """
@@ -4460,12 +4446,6 @@ type ProductVariant {
4460
4446
  """Whether variant is available for purchase"""
4461
4447
  isAvailable: Boolean!
4462
4448
 
4463
- """Lista meta properties — Storefront API filtruje isPrivate=false"""
4464
- metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4465
-
4466
- """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4467
- metaProperty(key: String!, namespace: String!): MetaProperty
4468
-
4469
4449
  """Variant price (Money). Default field — industry-standard schema."""
4470
4450
  price: Money!
4471
4451