@doswiftly/storefront-operations 6.1.0 → 7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,202 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 227629d: # Storefront GraphQL Operations 7.0 — Breaking changes + nowe rozszerzenia API
8
+
9
+ Major bump wprowadza zmiany breaking w schemie storefront GraphQL plus 3 znaczące nowe funkcjonalności: per-address tax info dla B2B, generic Meta Properties (extensible custom fields), oraz strukturalne userErrors[] z typed codes dla wszystkich mutacji.
10
+
11
+ ## Breaking changes
12
+
13
+ ### 1. `Product.category` (free-text) → `Product.categories` + `Product.primaryCategory` (structured)
14
+
15
+ Wcześniej `Product.category: String` zwracał free-text label — tekst nie był powiązany z entity Category. Klienci nie mogli pobrać slug/name kategorii ani zbudować breadcrumb. Po 7.0:
16
+
17
+ ```graphql
18
+ # PRZED (6.x):
19
+ product { category } # → "Pop Vinyl" (free-text string, brak slug/parent)
20
+
21
+ # PO (7.0):
22
+ product {
23
+ categories { id slug name parent { slug } } # M2M lista (junction table)
24
+ primaryCategory { slug name } # categories[0] dla breadcrumb
25
+ }
26
+ ```
27
+
28
+ **Migration**:
29
+ - Klient renderujący breadcrumb po category name: zamień `product.category` na `product.primaryCategory.name`.
30
+ - Klient filtrujący kategorie z `Product.category` jako string: użyj `product.categories[]` array.
31
+ - Filter `productCategory` w `ProductFilter` usunięty — używaj `category: { id }` (junction lookup, działało wcześniej).
32
+
33
+ ### 2. `categories` query — `CategoryTree` → `CategoryConnection` (Relay)
34
+
35
+ Wcześniej `categories` zwracał outlier shape `{ roots, totalCount }` vs reszta schemy używała Relay Connection. Klienci próbujący `categories { nodes }` dostawali GraphQL error. Po 7.0:
36
+
37
+ ```graphql
38
+ # PRZED (6.x):
39
+ categories(first: 20, rootsOnly: true) {
40
+ roots { id name children { id name } }
41
+ totalCount
42
+ }
43
+
44
+ # PO (7.0):
45
+ categories(first: 20, rootsOnly: true) {
46
+ nodes { id name children { id name } }
47
+ edges { cursor node { id } }
48
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
49
+ totalCount
50
+ }
51
+ ```
52
+
53
+ **Migration**:
54
+ - Zamień `categories.roots` na `categories.nodes`.
55
+ - Tree shape rebuild po stronie klienta — używaj `Category.parent` / `Category.children` field resolvers (DataLoader N+1 safe). Filtr `rootsOnly: true` zwraca tylko top-level (parent IS NULL).
56
+ - Pełen Relay args: `first/after/last/before` zamiast tylko `first/after`.
57
+
58
+ ### 3. `Customer.defaultAddress` — czytelny stan (poprzednio zawsze null)
59
+
60
+ Wcześniej `Customer.defaultAddress` zwracał `null` mimo że adres miał `isDefault: true` w `addresses`. Niespójność łamała storefront breadcrumb i powodowała duplikaty w formularzu dostawy. Po 7.0 zwraca poprawnie wartość — single source of truth z `addresses[].isDefaultShipping` flag.
61
+
62
+ **Migration**: brak — klient po prostu zacznie dostawać poprawne wartości.
63
+
64
+ ### 4. Apollo response — bez `extensions.stacktrace` w produkcji
65
+
66
+ Wcześniej w środowisku produkcyjnym GraphQL response zawierał stacktrace z absolutnymi ścieżkami systemu plików backendu. Po 7.0 production response strippuje wszystko poza `code`, `message`, `locations`, `path` + banner `extensions.environment`. Stacktrace dostępny **tylko** w dev/staging dla DX.
67
+
68
+ **Migration**: brak — klient produkcyjny nie powinien był polegać na stacktrace.
69
+
70
+ ### 5. Validation errors → `userErrors[]` z typed codes (tech debt resolution)
71
+
72
+ Wcześniej walidacja DTO leciała jako `body.errors[].extensions.code = "INTERNAL_SERVER_ERROR"` envelope (wyglądało jak crash). Po 7.0 mutations zwracają strukturalne `userErrors[]` z explicit codes per validation rule:
73
+
74
+ ```graphql
75
+ # Invalid NIP, password too short, malformed email — wszystkie zwracają payload
76
+ mutation {
77
+ customerUpdate(customer: { taxId: "1234567890" }) {
78
+ customer {
79
+ id
80
+ }
81
+ userErrors {
82
+ field
83
+ code
84
+ message
85
+ }
86
+ # → [{ field: ["taxId"], code: "INVALID_TAX_ID_CHECKSUM", message: "..." }]
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Typed codes** dla 25 walidatorów: `INVALID_TAX_ID_CHECKSUM`, `INVALID_VAT_NUMBER`, `INVALID_REGON`, `INVALID_EMAIL_FORMAT`, `INVALID_FORMAT`, `INVALID_PHONE_FORMAT`, `INVALID_UUID`, `INVALID_URL`, `INVALID_DATE`, `INVALID_ENUM_VALUE`, `INVALID_TYPE`, `INVALID_COUNTRY_CODE`, `INVALID_POSTAL_CODE`, `TOO_SHORT`, `TOO_LONG`, `TOO_MANY`, `TOO_FEW`, `OUT_OF_RANGE`, `REQUIRED`, `INVALID_NESTED`, `VALIDATION_ERROR`, etc.
92
+
93
+ **Migration**: klient widzący poprzednio `body.errors[].extensions.code = "INTERNAL_SERVER_ERROR"` — sprawdź `data.<mutation>.userErrors[]` zamiast envelope.
94
+
95
+ ## New features
96
+
97
+ ### 6. `MailingAddress.taxId` + `vatNumber` — per-address B2B tax info
98
+
99
+ Adres ma teraz opcjonalne `taxId` (polski NIP, 10 cyfr z checksumą) + `vatNumber` (VAT UE, np. PL5260250274). Use case: B2B z różnymi danymi firmowymi per adres dostawy/billing (faktura na firmę matkę, dostawa na oddział z osobnym NIP-em).
100
+
101
+ ```graphql
102
+ mutation {
103
+ customerAddAddress(
104
+ address: {
105
+ streetLine1: "ul. Marszałkowska 100"
106
+ city: "Warszawa"
107
+ postalCode: "00-001"
108
+ country: PL
109
+ company: "GameGoods Sp. z o.o."
110
+ taxId: "5260250274"
111
+ vatNumber: "PL5260250274"
112
+ }
113
+ ) {
114
+ address {
115
+ taxId
116
+ vatNumber
117
+ }
118
+ userErrors {
119
+ code
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ Walidacja format/checksum aktywna — invalid NIP zwraca `userErrors` z `INVALID_TAX_ID_CHECKSUM`.
126
+
127
+ ### 7. Meta Properties — extensible custom fields per encji
128
+
129
+ Generic mechanizm rozszerzania encji (Customer/Product/ProductVariant/Order) o storefront-specific dane bez wymogu zmian schematu po stronie platformy. Per-customer birthday, per-product warranty_years, per-order gift_message — wszystko jako typed key-value entries w polymorphic table.
130
+
131
+ ```graphql
132
+ # Customer self-edit (Bearer token klienta)
133
+ mutation {
134
+ customerMetaPropertiesSet(
135
+ properties: [
136
+ {
137
+ namespace: "storefront"
138
+ key: "birthday"
139
+ value: "1990-05-15"
140
+ type: DATE
141
+ }
142
+ {
143
+ namespace: "storefront"
144
+ key: "favorite_color"
145
+ value: "#FF6600"
146
+ type: COLOR
147
+ }
148
+ ]
149
+ ) {
150
+ metaProperties {
151
+ id
152
+ namespace
153
+ key
154
+ value
155
+ type
156
+ }
157
+ userErrors {
158
+ code
159
+ message
160
+ }
161
+ }
162
+ }
163
+
164
+ # Read na każdej encji
165
+ query {
166
+ customer {
167
+ metaProperty(namespace: "storefront", key: "birthday") {
168
+ value
169
+ }
170
+ }
171
+ product(id: "...") {
172
+ metaProperties(first: 10, namespace: "warranty") {
173
+ nodes {
174
+ key
175
+ value
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ 10 typed values: `STRING/TEXT/INTEGER/DECIMAL/BOOLEAN/JSON/DATE/DATE_TIME/URL/COLOR`. Visibility flag `isPrivate` (storefront API filtruje `isPrivate=true` na public encjach jak Product/ProductVariant). Reserved namespace prefix `doswiftly:` dla platform fields.
183
+
184
+ **Faza 1 MVP scope**: Customer self-edit only (`customerMetaPropertiesSet` / `customerMetaPropertyDelete` z auth Bearer token klienta). Cross-resource writes (Product/Order/Variant) wymagają Admin layer (Faza 2).
185
+
186
+ ### 8. NIP/REGON sanity guard — odrzucone all-zeros / repeated-digit patterns
187
+
188
+ Walidator NIP wcześniej akceptował `'0000000000'` (suma kontrolna 0=0). Po 7.0 odrzucamy wszystkie repeated-digit patterns (`'0000000000'`, `'1111111111'`, ..., `'9999999999'`) — to oczywiste fake test data / NULL placeholders, żaden urząd ich nie wystawia. Analogicznie REGON 9-digit i 14-digit.
189
+
190
+ ### 9. Empty string clear-value semantyka (wszystkie nullable input fields)
191
+
192
+ Customer kasujący wartość w UI inputie (np. `<input value="" />`) wysyła `phone: ""` zamiast `null`. Po 7.0 backend traktuje empty string jako "wyczyść pole" (DB → null) — natywne form library behavior, klient nie wymaga specjalnej logiki rozróżniającej `null` vs `""`.
193
+
194
+ Pokrycie: wszystkie nullable string/enum fields w `CustomerCreateInput`, `CustomerUpdateInput`, `MailingAddressInput`.
195
+
196
+ ## Notes
197
+ - 7.0 jest cumulative — kumulacja zmian z 6.x patches (B2B fields w `customerUpdate`, marketing consent flow) + breaking changes opisane wyżej.
198
+ - Wszystkie zmiany pokryte test coverage — backend integration tests gwarantują regression-free contract.
199
+
3
200
  ## 6.1.0
4
201
 
5
202
  ### Minor Changes
package/README.md CHANGED
@@ -96,7 +96,7 @@ export function ProductList() {
96
96
  | `Collection` | Collection with its products |
97
97
  | `Collections` | All collections |
98
98
  | `Category` | Category with hierarchy |
99
- | `Categories` | Category tree |
99
+ | `Categories` | Paginowana lista kategorii (Relay Connection) — drzewo budujesz po stronie klienta z `parent`/`children` |
100
100
  | `Cart` | Cart contents by ID |
101
101
  | `Customer` | Logged-in customer data |
102
102
  | `CustomerOrders` | Customer order history |
@@ -116,23 +116,26 @@ export function ProductList() {
116
116
 
117
117
  **Authentication**
118
118
 
119
- - `CustomerCreate` - Register new customer
119
+ - `CustomerSignup` - Register new customer
120
120
  - `CustomerLogin` - Login and get token
121
121
  - `CustomerLogout` - Logout
122
- - `CustomerTokenRenew` - Refresh auth token
122
+ - `CustomerRefreshToken` - Refresh auth token
123
+ - `CustomerActivate` - Activate account via email link
124
+ - `CustomerMetaPropertiesSet` - Set custom key-value entries on the logged-in customer
125
+ - `CustomerMetaPropertyDelete` - Delete a custom key-value entry from the logged-in customer
123
126
 
124
127
  **Customer Profile**
125
128
 
126
129
  - `CustomerUpdate` - Update profile info
127
- - `CustomerAddressCreate` - Add new address
128
- - `CustomerAddressUpdate` - Update address
129
- - `CustomerAddressDelete` - Delete address
130
- - `CustomerDefaultAddressUpdate` - Set default address
130
+ - `CustomerAddAddress` - Add new address
131
+ - `CustomerUpdateAddress` - Update address
132
+ - `CustomerRemoveAddress` - Remove address
133
+ - `CustomerSetDefaultAddress` - Set default address
131
134
 
132
135
  **Password**
133
136
 
134
- - `CustomerPasswordRecover` - Request password reset email
135
- - `CustomerPasswordReset` - Reset password with token
137
+ - `CustomerRequestPasswordReset` - Request password reset email
138
+ - `CustomerResetPassword` - Reset password with token
136
139
 
137
140
  ## Schema
138
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
package/schema.graphql CHANGED
@@ -1182,11 +1182,14 @@ type Category {
1182
1182
  sortOrder: Int!
1183
1183
  }
1184
1184
 
1185
- """Paginated category list"""
1185
+ """Paginated category list (Relay Connection)"""
1186
1186
  type CategoryConnection {
1187
- """List of category edges"""
1187
+ """List of category edges (cursor + node pairs)"""
1188
1188
  edges: [CategoryEdge!]!
1189
1189
 
1190
+ """List of category nodes (shortcut without cursor)"""
1191
+ nodes: [Category!]!
1192
+
1190
1193
  """Pagination info"""
1191
1194
  pageInfo: PageInfo!
1192
1195
 
@@ -1230,15 +1233,6 @@ type CategoryFilterOption {
1230
1233
  slug: String!
1231
1234
  }
1232
1235
 
1233
- """Category tree structure"""
1234
- type CategoryTree {
1235
- """Root categories"""
1236
- roots: [Category!]!
1237
-
1238
- """Total categories count"""
1239
- totalCount: Int!
1240
- }
1241
-
1242
1236
  """Checkout object"""
1243
1237
  type Checkout {
1244
1238
  """Applied gift cards"""
@@ -2043,6 +2037,16 @@ type Customer implements Node {
2043
2037
  """Last name"""
2044
2038
  lastName: String
2045
2039
 
2040
+ """
2041
+ Lista meta properties (Relay Connection) — opcjonalnie scoped do namespace
2042
+ """
2043
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
2044
+
2045
+ """
2046
+ Pojedyncze meta property po (namespace, key) — dla zalogowanego klienta zwraca także private
2047
+ """
2048
+ metaProperty(key: String!, namespace: String!): MetaProperty
2049
+
2046
2050
  """Total orders count (UnsignedInt64 — BigInt-safe)"""
2047
2051
  orderCount: UnsignedInt64!
2048
2052
 
@@ -3283,6 +3287,16 @@ type MailingAddress implements Node {
3283
3287
 
3284
3288
  """Second line of street address"""
3285
3289
  streetLine2: String
3290
+
3291
+ """
3292
+ Per-address tax ID (Polish NIP, 10 digits z checksum). B2B use case: różne dane firmowe per adres — np. faktura na firmę matkę z NIP A, dostawa na oddział z NIP B. Distinct from `Customer.taxId` (customer-level globally).
3293
+ """
3294
+ taxId: String
3295
+
3296
+ """
3297
+ Per-address EU VAT number (e.g. PL1234567890). B2B cross-border: różny VAT per adres dostawy. Distinct from `Customer.vatNumber` (customer-level globally).
3298
+ """
3299
+ vatNumber: String
3286
3300
  }
3287
3301
 
3288
3302
  """Paginated mailing addresses (Relay Connection)"""
@@ -3340,6 +3354,14 @@ input MailingAddressInput {
3340
3354
 
3341
3355
  """Second line of street address"""
3342
3356
  streetLine2: String
3357
+
3358
+ """
3359
+ Per-address Polish tax ID — NIP (10 digits with checksum). B2B use case: różne dane firmowe per adres dostawy/billing.
3360
+ """
3361
+ taxId: String
3362
+
3363
+ """Per-address EU VAT number (e.g. PL1234567890). Cross-border B2B."""
3364
+ vatNumber: String
3343
3365
  }
3344
3366
 
3345
3367
  """
@@ -3412,6 +3434,119 @@ enum MenuItemType {
3412
3434
  SEARCH
3413
3435
  }
3414
3436
 
3437
+ """
3438
+ Payload `customerMetaPropertiesSet` — bulk upsert własnych meta properties klienta
3439
+ """
3440
+ type MetaPropertiesSetPayload {
3441
+ """Upserted meta properties (puste gdy userErrors)"""
3442
+ metaProperties: [MetaProperty!]!
3443
+
3444
+ """User errors (code z `MetaPropertyErrorCode` enum)"""
3445
+ userErrors: [UserError!]!
3446
+ }
3447
+
3448
+ """Pojedyncze pole dodatkowe (meta property) na encji commerce"""
3449
+ type MetaProperty implements Node {
3450
+ """Created at"""
3451
+ createdAt: DateTime!
3452
+
3453
+ """Unique identifier"""
3454
+ id: ID!
3455
+
3456
+ """
3457
+ true = readable tylko przez authenticated context (admin/customer self). false (default) = readable również przez anonymous Storefront API.
3458
+ """
3459
+ isPrivate: Boolean!
3460
+
3461
+ """
3462
+ Key w obrębie namespace (3-64 chars). Unique per (ownerType, ownerId, namespace).
3463
+ """
3464
+ key: String!
3465
+
3466
+ """
3467
+ Namespace (3-64 chars). Zapobiega kolizji kluczy między aplikacjami. Reserved prefix: `doswiftly:` (platform).
3468
+ """
3469
+ namespace: String!
3470
+
3471
+ """Typ wartości — informuje klienta jak parse value."""
3472
+ type: MetaPropertyValueType!
3473
+
3474
+ """Last updated at"""
3475
+ updatedAt: DateTime!
3476
+
3477
+ """
3478
+ Wartość — zawsze String. Klient parsuje zgodnie z polem `type` (np. INTEGER → parseInt, JSON → JSON.parse).
3479
+ """
3480
+ value: String!
3481
+ }
3482
+
3483
+ """Paginated meta property list (Relay Connection)"""
3484
+ type MetaPropertyConnection {
3485
+ """Edges (cursor + node pairs)"""
3486
+ edges: [MetaPropertyEdge!]!
3487
+
3488
+ """Nodes (shortcut bez cursor)"""
3489
+ nodes: [MetaProperty!]!
3490
+
3491
+ """Page info"""
3492
+ pageInfo: PageInfo!
3493
+
3494
+ """Total count (max po wszystkich pages)"""
3495
+ totalCount: Int!
3496
+ }
3497
+
3498
+ """Payload `customerMetaPropertyDelete`"""
3499
+ type MetaPropertyDeletePayload {
3500
+ """ID usuniętego meta property (null gdy userErrors)"""
3501
+ deletedId: ID
3502
+
3503
+ """User errors (code z `MetaPropertyErrorCode` enum)"""
3504
+ userErrors: [UserError!]!
3505
+ }
3506
+
3507
+ """Meta property edge (Relay pagination)"""
3508
+ type MetaPropertyEdge {
3509
+ """Cursor for pagination"""
3510
+ cursor: String!
3511
+
3512
+ """Meta property node"""
3513
+ node: MetaProperty!
3514
+ }
3515
+
3516
+ """Input dla pojedynczego meta property w bulk set"""
3517
+ input MetaPropertyInput {
3518
+ """Visibility — true = admin/customer-only, false = storefront-readable"""
3519
+ isPrivate: Boolean = false
3520
+
3521
+ """Key (3-64 chars)"""
3522
+ key: String!
3523
+
3524
+ """Namespace (3-64 chars, NOT starting with `doswiftly:`)"""
3525
+ namespace: String!
3526
+
3527
+ """Typ wartości (driver walidacji backend-side)"""
3528
+ type: MetaPropertyValueType!
3529
+
3530
+ """Wartość jako String (parse zgodnie z type)"""
3531
+ value: String!
3532
+ }
3533
+
3534
+ """
3535
+ 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).
3536
+ """
3537
+ enum MetaPropertyValueType {
3538
+ BOOLEAN
3539
+ COLOR
3540
+ DATE
3541
+ DATE_TIME
3542
+ DECIMAL
3543
+ INTEGER
3544
+ JSON
3545
+ STRING
3546
+ TEXT
3547
+ URL
3548
+ }
3549
+
3415
3550
  """Monetary value with currency"""
3416
3551
  type Money {
3417
3552
  """Decimal money amount"""
@@ -3509,6 +3644,12 @@ type Mutation {
3509
3644
  """Logout customer (clears auth cookie)"""
3510
3645
  customerLogout: CustomerLogoutPayload!
3511
3646
 
3647
+ """Bulk upsert własnych meta properties klienta (Bearer token wymagany)"""
3648
+ customerMetaPropertiesSet(properties: [MetaPropertyInput!]!): MetaPropertiesSetPayload!
3649
+
3650
+ """Usuwa pojedyncze meta property klienta po (namespace, key)"""
3651
+ customerMetaPropertyDelete(key: String!, namespace: String!): MetaPropertyDeletePayload!
3652
+
3512
3653
  """Refresh access token"""
3513
3654
  customerRefreshToken: CustomerRefreshTokenPayload!
3514
3655
 
@@ -3624,6 +3765,12 @@ type Order implements Node {
3624
3765
  """Line items count"""
3625
3766
  itemCount: Int!
3626
3767
 
3768
+ """Lista meta properties (Relay Connection)"""
3769
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
3770
+
3771
+ """Pojedyncze meta property po (namespace, key)"""
3772
+ metaProperty(key: String!, namespace: String!): MetaProperty
3773
+
3627
3774
  """Order number (human-readable)"""
3628
3775
  orderNumber: String!
3629
3776
 
@@ -3903,9 +4050,9 @@ type Product implements Node {
3903
4050
  averageRating: Float
3904
4051
 
3905
4052
  """
3906
- Product category (free-text classification, e.g. "Footwear", "Electronics")
4053
+ 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.
3907
4054
  """
3908
- category: String
4055
+ categories: [Category!]!
3909
4056
 
3910
4057
  """
3911
4058
  Compare-at price range (Money pair). Null gdy żaden variant nie ma compareAtPrice.
@@ -3944,6 +4091,12 @@ type Product implements Node {
3944
4091
  """
3945
4092
  isPurchasable: Boolean!
3946
4093
 
4094
+ """Lista meta properties — Storefront API filtruje isPrivate=false"""
4095
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4096
+
4097
+ """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4098
+ metaProperty(key: String!, namespace: String!): MetaProperty
4099
+
3947
4100
  """
3948
4101
  Per-product option definitions (Color, Size, …) with their available values. Use these to build a variant picker without aggregating `selectedOptions` manually.
3949
4102
  """
@@ -3957,6 +4110,11 @@ type Product implements Node {
3957
4110
  """
3958
4111
  priceRangeWithConversion: ConvertedPriceRange
3959
4112
 
4113
+ """
4114
+ Domyślna kategoria dla breadcrumb/nav (= categories[0] po sortOrder). Null gdy produkt nie jest w żadnej kategorii.
4115
+ """
4116
+ primaryCategory: Category
4117
+
3960
4118
  """Similar products recommendations"""
3961
4119
  recommendations(first: Int = 4): ProductRecommendations
3962
4120
 
@@ -4143,11 +4301,6 @@ input ProductFilter {
4143
4301
  """Filter by variant price range"""
4144
4302
  price: PriceRangeFilter
4145
4303
 
4146
- """
4147
- Filter by product category (free-text classification, stored on Product.category). Distinct from `category: CategoryFilter` which selects by structured Category entity.
4148
- """
4149
- productCategory: String
4150
-
4151
4304
  """
4152
4305
  Filter by product type enum (PHYSICAL/DIGITAL/SERVICE/SUBSCRIPTION/GIFT_CARD). Distinct from `productCategory` (free-text classification).
4153
4306
  """
@@ -4345,6 +4498,12 @@ type ProductVariant {
4345
4498
  """Whether variant is available for purchase"""
4346
4499
  isAvailable: Boolean!
4347
4500
 
4501
+ """Lista meta properties — Storefront API filtruje isPrivate=false"""
4502
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4503
+
4504
+ """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4505
+ metaProperty(key: String!, namespace: String!): MetaProperty
4506
+
4348
4507
  """Variant price (Money). Default field — industry-standard schema."""
4349
4508
  price: Money!
4350
4509
 
@@ -4478,20 +4637,30 @@ type Query {
4478
4637
  """Get cart by ID"""
4479
4638
  cart(id: ID!): Cart
4480
4639
 
4481
- """Get category tree"""
4640
+ """
4641
+ Lista kategorii (Relay Connection) z opcjonalnym filtrem rootsOnly/parentId
4642
+ """
4482
4643
  categories(
4483
- """Cursor for pagination"""
4644
+ """Forward pagination cursor (after this element)"""
4484
4645
  after: String
4485
4646
 
4486
- """Number of items to fetch"""
4487
- first: Int! = 20
4647
+ """Backward pagination cursor (before this element)"""
4648
+ before: String
4649
+
4650
+ """Forward pagination — first N elements"""
4651
+ first: Int
4488
4652
 
4489
- """Filter by parent category ID"""
4653
+ """Backward pagination last N elements"""
4654
+ last: Int
4655
+
4656
+ """
4657
+ Filter by parent category ID — używaj `null` semantically dla "roots" via `rootsOnly` flag
4658
+ """
4490
4659
  parentId: ID
4491
4660
 
4492
- """Only root categories"""
4661
+ """Tylko root categories (parentId IS NULL)"""
4493
4662
  rootsOnly: Boolean! = false
4494
- ): CategoryTree!
4663
+ ): CategoryConnection!
4495
4664
 
4496
4665
  """Get category by ID or slug"""
4497
4666
  category(