cabloy 5.1.113 → 5.1.114
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +4 -0
- package/.github/workflows/vona-test-mysql.yml +12 -0
- package/.github/workflows/vona-test-pg.yml +5 -1
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +4 -0
- package/cabloy-docs/backend/internal-aop-guide.md +35 -0
- package/cabloy-docs/backend/unit-testing.md +72 -1
- package/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +26 -13
- package/vona/packages-utils/compose/package.json +1 -1
- package/vona/packages-utils/compose/src/index.ts +19 -11
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/beanContainer.ts +15 -3
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +8 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuAttribute.tsx +17 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +10 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +228 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +34 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAttempt.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +4 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +533 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +24 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.version.ts +137 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/ssrMenu.couponTemplate.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/coupon.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/couponTemplate.ts +65 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponMineItem.tsx +32 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +43 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectReq.tsx +28 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +39 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponAudit.tsx +73 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponGrant.tsx +83 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +82 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponGrant.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponTemplate.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +397 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/couponTemplate.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +162 -0
- package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +8 -10
- package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +277 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/bean.scheduleOrderExpiry.ts +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +75 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +32 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAddressSnapshot.tsx +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderCouponSnapshot.tsx +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderLineSkuAttributeSnapshot.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +90 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +36 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderLine.tsx +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderLine.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +556 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +15 -20
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +8 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +158 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +203 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +163 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +11 -6
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +11 -6
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +10 -7
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-aspect/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-aspect/src/types/aopMethod.ts +16 -3
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/bean/bean.core.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/fetch.ts +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/lib/utils.ts +5 -3
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/package.json +50 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/index.ts +55 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/bean/aopMethod.retryable.ts +45 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-vona/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
- package/zova/packages-utils/zova-jsx/package.json +3 -3
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/src/suite/a-commerce/modules/commerce-promotion/cli/openapi.config.ts +9 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +2 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +115 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/commercePromotionCoupon.ts +24 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/baseURL.ts +5 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/index.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +478 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +8187 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/apiSchema/commercePromotionCoupon.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +30 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +27 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +92 -52
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +1389 -848
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +13 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.114
|
|
@@ -43,6 +43,10 @@ After generating or extending a backend thread, check which follow-up layers app
|
|
|
43
43
|
- retain exact IDs or entities for every test-local persisted resource and delete them from `finally`
|
|
44
44
|
- delete joins, children, derived records, and other dependents before their owners, in the same active tenant/instance scope
|
|
45
45
|
- do not use unscoped, timestamp-, prefix-, or broad business-condition deletion when an exact test-owned identity is available
|
|
46
|
+
- treat the application lifecycle as runner-owned; do not create or close the shared `app` in an individual test
|
|
47
|
+
- give each scoped test or operation its own `app.bean.executor.mockCtx(...)` boundary
|
|
48
|
+
- for a business race, give every contender a separate `mockCtx(...)`, launch the competing operations explicitly, wait for all branches to settle, and assert the combined durable state
|
|
49
|
+
- gate lock- or isolation-sensitive contention tests on supporting database capabilities; runner scheduling is never race-condition evidence
|
|
46
50
|
|
|
47
51
|
## Module composition and dependency intent
|
|
48
52
|
|
|
@@ -34,6 +34,18 @@ jobs:
|
|
|
34
34
|
version: 11.5.2
|
|
35
35
|
- name: init
|
|
36
36
|
run: npm run init
|
|
37
|
+
- name: run Commerce lifecycle gate
|
|
38
|
+
run: >-
|
|
39
|
+
DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test --
|
|
40
|
+
commerce-trade/test/stockBalance.test.ts
|
|
41
|
+
commerce-trade/test/stockReservation.test.ts
|
|
42
|
+
commerce-trade/test/stockAudit.test.ts
|
|
43
|
+
commerce-promotion/test/couponReservation.test.ts
|
|
44
|
+
commerce-payment/test/paymentAttempt.test.ts
|
|
45
|
+
commerce-trade/test/checkoutReservation.test.ts
|
|
46
|
+
commerce-trade/test/reservationExpiry.test.ts
|
|
47
|
+
--flavor=normal
|
|
48
|
+
working-directory: vona
|
|
37
49
|
- run: DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run test
|
|
38
50
|
working-directory: vona
|
|
39
51
|
- run: DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run build -- --flavor=ci
|
|
@@ -36,10 +36,14 @@ jobs:
|
|
|
36
36
|
run: npm run init
|
|
37
37
|
- name: run Commerce stock contention gate
|
|
38
38
|
run: >-
|
|
39
|
-
|
|
39
|
+
DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test --
|
|
40
40
|
commerce-trade/test/stockBalance.test.ts
|
|
41
41
|
commerce-trade/test/stockReservation.test.ts
|
|
42
42
|
commerce-trade/test/stockAudit.test.ts
|
|
43
|
+
commerce-promotion/test/couponReservation.test.ts
|
|
44
|
+
commerce-payment/test/paymentAttempt.test.ts
|
|
45
|
+
commerce-trade/test/checkoutReservation.test.ts
|
|
46
|
+
commerce-trade/test/reservationExpiry.test.ts
|
|
43
47
|
--flavor=normal
|
|
44
48
|
working-directory: vona
|
|
45
49
|
- run: DATABASE_DEFAULT_CLIENT=pg npm run test
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.114
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add retryable AOP infrastructure for Commerce transactions.
|
|
8
|
+
- Add a foundational Commerce coupon lifecycle.
|
|
9
|
+
- Add a foundational Commerce order snapshot capability.
|
|
10
|
+
- Add `ctx.state.fetch` support.
|
|
11
|
+
- Add checkout functionality.
|
|
12
|
+
|
|
13
|
+
### Improvements
|
|
14
|
+
|
|
15
|
+
- Decorate Commerce transaction retries.
|
|
16
|
+
|
|
3
17
|
## 5.1.113
|
|
4
18
|
|
|
5
19
|
### Features
|
package/CLAUDE.md
CHANGED
|
@@ -72,6 +72,10 @@ Before inventing a custom implementation path:
|
|
|
72
72
|
- In Vona, a tenant corresponds to an instance. Ordinary resource-model CRUD is automatically scoped to the active instance; treat records absent from that scope as absent, and do not use raw cross-instance probes merely to choose between `403` and not-found behavior. Model future multi-merchant boundaries explicitly inside an instance.
|
|
73
73
|
- Model cross-Model query-cache dependencies as one directed, acyclic `modelsClear` / `modelsClearedBy` graph, and verify source mutations refresh warmed dependent queries; read `.docs-internal/architecture/vona-cross-model-query-cache-dependencies.md` before designing a nontrivial graph.
|
|
74
74
|
- For `@Api.field(...)` and related schemaLike composition, framework guards now preserve previously attached OpenAPI metadata across schema rebuilds, but structure-shaping schemaLike is still order-sensitive. Treat `v.object(...)`, `v.array(...)`, `v.optional()`, `v.nullable()`, `v.default(...)`, and preprocess/transform wrappers as structure-shaping; keep the final structure-defining schemaLike last and verify emitted schema/OpenAPI output after such edits.
|
|
75
|
+
- `@Core.transaction(...)` defaults to `REQUIRED`: it starts a transaction only when none exists and otherwise joins the current datasource transaction without upgrading its isolation. Prefer it over manual `inTransaction` wrappers for atomic service methods; use `REQUIRES_NEW` only when an independently committed boundary is explicitly required.
|
|
76
|
+
- For replay-safe transient failures, use `@Core.retryable(...)` with an explicit `errorCodes` allowlist. It retries the downstream AOP suffix, so place it closest to the method when it must wrap and retry a `@Core.transaction(...)` boundary; use `ownerOnly: true` for dual-role leaves that must not retry inside a caller-owned transaction; do not retry external side effects or infer retryability from isolation level.
|
|
77
|
+
- Backend tests that use scoped Vona state must own an `app.bean.executor.mockCtx(...)` boundary; every intentionally competing operation must run in a separate `mockCtx(...)`.
|
|
78
|
+
- Do not set `TEST_CONCURRENCY=false` for normal `npm run test` or CI runs: an unset value defaults to concurrent execution. For stateful `node:test` suites, declare `{ concurrency: false }` explicitly. Prove business contention by explicitly launching competing operations and asserting their combined durable outcome, never through runner parallelism or scheduling.
|
|
75
79
|
- Unit tests must delete every test-owned persisted resource in `finally`, using precise owned identities and reverse dependency order.
|
|
76
80
|
- Shared durable test or local-development fixtures must be created through the owning module's `meta.version.ts` `seed()` hook and treated as read-only by tests; the managed seed path starts from a newly recreated database rather than repeating against one database.
|
|
77
81
|
|
|
@@ -82,6 +82,7 @@ Representative examples include:
|
|
|
82
82
|
|
|
83
83
|
- `a-logger:log`
|
|
84
84
|
- `a-orm:transaction`
|
|
85
|
+
- `a-retryable:retryable`
|
|
85
86
|
- `a-caching:cachingGet`
|
|
86
87
|
- `a-caching:cachingSet`
|
|
87
88
|
- `a-caching:cachingDel`
|
|
@@ -92,9 +93,43 @@ These built-ins also expose shorthand decorators such as:
|
|
|
92
93
|
```typescript
|
|
93
94
|
@Core.log({ level: 'info' })
|
|
94
95
|
@Core.transaction({ isolationLevel: 'READ_COMMITTED', propagation: 'REQUIRED' })
|
|
96
|
+
@Core.retryable({
|
|
97
|
+
retries: 1,
|
|
98
|
+
minTimeout: 0,
|
|
99
|
+
maxTimeout: 0,
|
|
100
|
+
errorCodes: ['40001'],
|
|
101
|
+
})
|
|
95
102
|
@Caching.get({ cacheName: 'module-name:xxx' })
|
|
96
103
|
```
|
|
97
104
|
|
|
105
|
+
`@Core.retryable(...)` retries only errors whose string `code` is explicitly listed in `errorCodes`. It replays the downstream AOP suffix; to retry a complete transaction, place `@Core.retryable(...)` closest to the method so it runs outside the transaction decorator:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
109
|
+
@Core.retryable({ retries: 1, minTimeout: 0, maxTimeout: 0, errorCodes: ['40001'] })
|
|
110
|
+
async reserve() {
|
|
111
|
+
// transactional writes
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use `ownerOnly: true` for a dual-role leaf method that may either own a transaction when called directly or join an aggregate transaction owned by its caller. It retries only when no current-datasource transaction existed when the method began; when nested, it executes once and lets the aggregate owner decide whether to replay the complete unit of work:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
119
|
+
@Core.retryable({
|
|
120
|
+
retries: 1,
|
|
121
|
+
minTimeout: 0,
|
|
122
|
+
maxTimeout: 0,
|
|
123
|
+
errorCodes: ['40001'],
|
|
124
|
+
ownerOnly: true,
|
|
125
|
+
})
|
|
126
|
+
async reserveLeaf() {
|
|
127
|
+
// transactional writes
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Retried methods must be replay-safe: defer external side effects until commit, and do not use this helper to imply cross-datasource or independent `REQUIRES_NEW` retry semantics.
|
|
132
|
+
|
|
98
133
|
For the broader logger-client, rotation, and level model behind `@Core.log(...)`, see [Logger Guide](/backend/logger-guide).
|
|
99
134
|
|
|
100
135
|
## Magic Method
|
|
@@ -46,11 +46,18 @@ npm run test
|
|
|
46
46
|
|
|
47
47
|
A typical Vona test flow includes:
|
|
48
48
|
|
|
49
|
-
1. create
|
|
49
|
+
1. create one shared global `app` object for the test run
|
|
50
50
|
2. clean Redis data
|
|
51
51
|
3. recreate the database
|
|
52
52
|
4. execute migration code
|
|
53
53
|
5. run the test files
|
|
54
|
+
6. close the shared application after the test run
|
|
55
|
+
|
|
56
|
+
The runner owns this application lifecycle. Test files import the shared app from `vona-mock` and must not create or close it themselves:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { app } from 'vona-mock';
|
|
60
|
+
```
|
|
54
61
|
|
|
55
62
|
This is one of the most important distinctions from ordinary app flow: test execution rebuilds and verifies the framework lifecycle, not only the target function.
|
|
56
63
|
|
|
@@ -72,6 +79,33 @@ Representative command:
|
|
|
72
79
|
cd vona && npm run cov
|
|
73
80
|
```
|
|
74
81
|
|
|
82
|
+
## Runner concurrency
|
|
83
|
+
|
|
84
|
+
The Vona runner uses Node's built-in test runner with one shared application and process isolation disabled. `TEST_CONCURRENCY` controls runner scheduling:
|
|
85
|
+
|
|
86
|
+
- `true` uses the available CPU count;
|
|
87
|
+
- `false` forces serial execution;
|
|
88
|
+
- a positive integer sets an explicit concurrency limit.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
TEST_CONCURRENCY=false npm run test
|
|
92
|
+
TEST_CONCURRENCY=4 npm run test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Cabloy Basic requests concurrency by default with `TEST_CONCURRENCY=true`. When the active database dialect does not advertise the required concurrency capability, the runner falls back to serial execution. The default SQLite path therefore runs serially.
|
|
96
|
+
|
|
97
|
+
Runner concurrency schedules ordinary tests; it does not prove a business-level race condition. Tests must not rely on scheduling, timing, or execution order for correctness.
|
|
98
|
+
|
|
99
|
+
A suite can also make sibling-test scheduling explicit:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
describe('resource.test.ts', { concurrency: false }, () => {
|
|
103
|
+
// sibling tests in this suite are serialized
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This setting controls test scheduling only. It is not a substitute for explicitly creating competing business operations.
|
|
108
|
+
|
|
75
109
|
## Mock request context
|
|
76
110
|
|
|
77
111
|
One of the most important Vona testing patterns is simulating a request context.
|
|
@@ -84,6 +118,10 @@ await app.bean.executor.mockCtx(async () => {
|
|
|
84
118
|
});
|
|
85
119
|
```
|
|
86
120
|
|
|
121
|
+
Treat one `mockCtx(...)` callback as one simulated request boundary. Keep `app.ctx`, authentication, current database selection, and context-dependent service, model, or action work inside that callback, and always await it. Do not retain request-context state after the callback returns.
|
|
122
|
+
|
|
123
|
+
`mockCtx(...)` isolates request-local context. It does not isolate committed persisted records, app-global state, external caches, or shared durable fixtures. For independent concurrent requests, create one `mockCtx(...)` for each branch; parallel work started inside a single `mockCtx(...)` shares that request context.
|
|
124
|
+
|
|
87
125
|
Locale-sensitive variants and additional request-context helpers are also available.
|
|
88
126
|
|
|
89
127
|
## Persisted fixture lifecycle
|
|
@@ -111,6 +149,37 @@ try {
|
|
|
111
149
|
|
|
112
150
|
Treat shared `seed()` records as read-only. If a scenario needs to change a record, create a separate test-local fixture instead and clean it up. See [Migration and Changes](/backend/migration-and-changes#seed-test-environment-baseline-data) for the durable seed lifecycle.
|
|
113
151
|
|
|
152
|
+
Application shutdown is runner-owned; authentication and fixture cleanup are test-owned. Do not call `app.close()` from a test. Keep `signout()` and exact-identity deletion in `finally` inside an appropriate request context.
|
|
153
|
+
|
|
154
|
+
## Testing concurrent behavior explicitly
|
|
155
|
+
|
|
156
|
+
A concurrency test creates competing business operations inside one test; it does not depend on the runner to happen to schedule tests at the same time.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const attempt = async () => {
|
|
160
|
+
return await app.bean.executor.mockCtx(async () => {
|
|
161
|
+
await app.bean.passport.signinMock();
|
|
162
|
+
try {
|
|
163
|
+
return await reserve(resourceId);
|
|
164
|
+
} finally {
|
|
165
|
+
await app.bean.passport.signout();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const results = await Promise.allSettled([attempt(), attempt()]);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use this sequence:
|
|
174
|
+
|
|
175
|
+
1. create a dedicated test-local fixture in an appropriate `mockCtx(...)`;
|
|
176
|
+
2. give every contender its own `mockCtx(...)` and authentication lifecycle;
|
|
177
|
+
3. launch the contenders explicitly and wait for all of them to settle;
|
|
178
|
+
4. use a fresh `mockCtx(...)` to assert both individual outcomes and the combined durable invariant;
|
|
179
|
+
5. clean up exact owned fixtures only after every branch has settled, in the appropriate tenant/instance context.
|
|
180
|
+
|
|
181
|
+
For example, a one-winner reservation test should verify not only the fulfilled and rejected operations but also the final balance, surviving reservation, and audit records. If the invariant depends on row locks, transaction isolation, or another database-specific capability, skip the test on unsupported dialects rather than weakening its assertions. See [stockReservation.test.ts](../../vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts) for this pattern.
|
|
182
|
+
|
|
114
183
|
## Working with module scope in tests
|
|
115
184
|
|
|
116
185
|
Representative pattern:
|
|
@@ -218,6 +287,8 @@ Read this guide together with:
|
|
|
218
287
|
- [CRUD Workflow](/backend/crud-workflow)
|
|
219
288
|
- [Migration and Changes](/backend/migration-and-changes)
|
|
220
289
|
- [Controller Guide](/backend/controller-guide)
|
|
290
|
+
- [Transaction Guide](/backend/transaction-guide)
|
|
291
|
+
- [Redlock Guide](/backend/redlock-guide)
|
|
221
292
|
|
|
222
293
|
A practical split is:
|
|
223
294
|
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { globby } from 'globby';
|
|
|
7
7
|
import { createWriteStream } from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
11
|
import { run } from 'node:test';
|
|
11
12
|
import { lcov, spec } from 'node:test/reporters';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -104,19 +105,25 @@ async function testRun(projectPath: string, coverage: boolean, patterns: string[
|
|
|
104
105
|
}
|
|
105
106
|
});
|
|
106
107
|
const summaryPromise = waitForTestSummary(testStream);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
const reporterPromise = coverage
|
|
109
|
+
? (() => {
|
|
110
|
+
const reporterDir = path.join(projectPath, 'coverage');
|
|
111
|
+
fse.ensureDirSync(reporterDir);
|
|
112
|
+
const reporterLcov = createWriteStream(path.join(reporterDir, 'lcov.info'));
|
|
113
|
+
return pipeline(testStream.compose(lcov), reporterLcov);
|
|
114
|
+
})()
|
|
115
|
+
: pipeline(testStream.compose(spec), process.stdout, { end: false });
|
|
115
116
|
|
|
116
|
-
const summarySuccess = await
|
|
117
|
+
const [summarySuccess, [, reporterError]] = await Promise.all([
|
|
118
|
+
summaryPromise,
|
|
119
|
+
catchError(() => reporterPromise),
|
|
120
|
+
]);
|
|
117
121
|
if (!summarySuccess) {
|
|
118
122
|
throw new Error('node:test reported failed tests');
|
|
119
123
|
}
|
|
124
|
+
if (reporterError) {
|
|
125
|
+
throw reporterError;
|
|
126
|
+
}
|
|
120
127
|
} catch (error) {
|
|
121
128
|
testError = error;
|
|
122
129
|
} finally {
|
|
@@ -143,13 +150,19 @@ function waitForTestSummary(testStream: ReturnType<typeof run>) {
|
|
|
143
150
|
|
|
144
151
|
async function prepareConcurrency(app: VonaApplication) {
|
|
145
152
|
// check
|
|
153
|
+
const configured = process.env.TEST_CONCURRENCY;
|
|
146
154
|
let concurrency = 1;
|
|
147
|
-
if (
|
|
148
|
-
concurrency = os.cpus().length;
|
|
149
|
-
} else if (process.env.TEST_CONCURRENCY === 'false') {
|
|
155
|
+
if (configured === 'false') {
|
|
150
156
|
concurrency = 1;
|
|
157
|
+
} else if (configured === undefined || configured === 'true') {
|
|
158
|
+
concurrency = Math.max(1, os.cpus().length);
|
|
151
159
|
} else {
|
|
152
|
-
concurrency = Number.parseInt(
|
|
160
|
+
concurrency = Number.parseInt(configured, 10);
|
|
161
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`TEST_CONCURRENCY must be true, false, or a positive integer; received ${configured}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
153
166
|
}
|
|
154
167
|
if (concurrency === 1) return concurrency;
|
|
155
168
|
// check again
|
|
@@ -10,32 +10,40 @@ function __adapterDefault(_context, chain) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type TypeComposeNext = ((context?: any) => any) & {
|
|
14
|
+
replay: (context?: any) => any;
|
|
15
|
+
};
|
|
16
|
+
|
|
13
17
|
export function compose(chains?: any[], adapter?: TypeComposeAdapter) {
|
|
14
18
|
if (!adapter) adapter = __adapterDefault;
|
|
15
19
|
if (!chains) chains = [];
|
|
16
20
|
return function (context, next?) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
index = i;
|
|
21
|
+
return dispatch(0, context, { index: -1 });
|
|
22
|
+
|
|
23
|
+
function dispatch(i, context, state: { index: number }) {
|
|
24
|
+
if (i <= state.index) throw new Error('next() called multiple times');
|
|
25
|
+
state.index = i;
|
|
23
26
|
let receiver;
|
|
24
27
|
let fn;
|
|
25
28
|
const chain = chains![i];
|
|
26
29
|
if (chain) {
|
|
27
30
|
const obj = adapter!(context, chain);
|
|
28
|
-
if (!obj) return dispatch(i + 1, context);
|
|
31
|
+
if (!obj) return dispatch(i + 1, context, state);
|
|
29
32
|
receiver = obj.receiver;
|
|
30
33
|
fn = obj.fn;
|
|
31
34
|
if (!fn) throw new Error('fn is not defined');
|
|
32
35
|
}
|
|
33
36
|
if (i === chains!.length) fn = next;
|
|
34
37
|
if (!fn) return context;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return dispatch(i + 1,
|
|
38
|
-
});
|
|
38
|
+
const nextPatched = ((...args) => {
|
|
39
|
+
const nextContext = args.length === 0 ? context : args[0];
|
|
40
|
+
return dispatch(i + 1, nextContext, state);
|
|
41
|
+
}) as TypeComposeNext;
|
|
42
|
+
nextPatched.replay = (...args) => {
|
|
43
|
+
const nextContext = args.length === 0 ? context : args[0];
|
|
44
|
+
return dispatch(i + 1, nextContext, { index: i });
|
|
45
|
+
};
|
|
46
|
+
return fn.call(receiver, context, nextPatched);
|
|
39
47
|
}
|
|
40
48
|
};
|
|
41
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.79",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
|
|
6
6
|
"keywords": [
|
|
@@ -614,7 +614,7 @@ export class BeanContainer {
|
|
|
614
614
|
throw new Error(`get property accessor not exists: ${aopMethod.onionName}`);
|
|
615
615
|
return beanInstance.get(
|
|
616
616
|
aopMethod.options,
|
|
617
|
-
|
|
617
|
+
_patchAopMethodNext([receiver, _], next),
|
|
618
618
|
receiver,
|
|
619
619
|
prop,
|
|
620
620
|
);
|
|
@@ -627,7 +627,7 @@ export class BeanContainer {
|
|
|
627
627
|
return beanInstance.set(
|
|
628
628
|
aopMethod.options,
|
|
629
629
|
value,
|
|
630
|
-
|
|
630
|
+
_patchAopMethodNext([receiver, value], next),
|
|
631
631
|
receiver,
|
|
632
632
|
prop,
|
|
633
633
|
);
|
|
@@ -640,7 +640,7 @@ export class BeanContainer {
|
|
|
640
640
|
return beanInstance.execute(
|
|
641
641
|
aopMethod.options,
|
|
642
642
|
args,
|
|
643
|
-
|
|
643
|
+
_patchAopMethodNext([receiver, args], next),
|
|
644
644
|
receiver,
|
|
645
645
|
prop,
|
|
646
646
|
);
|
|
@@ -673,6 +673,18 @@ function _patchAopNext([receiver, context], next) {
|
|
|
673
673
|
};
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
+
function _patchAopMethodNext([receiver, context], next) {
|
|
677
|
+
const nextPatched = (...args) => {
|
|
678
|
+
context = args.length === 0 ? context : args[0];
|
|
679
|
+
return next([receiver, context]);
|
|
680
|
+
};
|
|
681
|
+
nextPatched.replay = (...args) => {
|
|
682
|
+
const nextContext = args.length === 0 ? context : args[0];
|
|
683
|
+
return next.replay([receiver, nextContext]);
|
|
684
|
+
};
|
|
685
|
+
return nextPatched;
|
|
686
|
+
}
|
|
687
|
+
|
|
676
688
|
function __checkAopOfDescriptorInfo(descriptorInfo) {
|
|
677
689
|
if (!descriptorInfo) return true;
|
|
678
690
|
return !descriptorInfo.dynamic && !descriptorInfo.ofBeanBase;
|
package/vona/pnpm-lock.yaml
CHANGED
|
@@ -253,6 +253,9 @@ importers:
|
|
|
253
253
|
vona-module-a-redlock:
|
|
254
254
|
specifier: workspace:^
|
|
255
255
|
version: link:src/suite-vendor/a-vona/modules/a-redlock
|
|
256
|
+
vona-module-a-retryable:
|
|
257
|
+
specifier: workspace:^
|
|
258
|
+
version: link:src/suite-vendor/a-vona/modules/a-retryable
|
|
256
259
|
vona-module-a-runtime:
|
|
257
260
|
specifier: workspace:^
|
|
258
261
|
version: link:src/suite-vendor/a-vona/modules/a-runtime
|
|
@@ -1991,6 +1994,9 @@ importers:
|
|
|
1991
1994
|
vona-module-a-redlock:
|
|
1992
1995
|
specifier: workspace:^
|
|
1993
1996
|
version: link:modules/a-redlock
|
|
1997
|
+
vona-module-a-retryable:
|
|
1998
|
+
specifier: workspace:^
|
|
1999
|
+
version: link:modules/a-retryable
|
|
1994
2000
|
vona-module-a-runtime:
|
|
1995
2001
|
specifier: workspace:^
|
|
1996
2002
|
version: link:modules/a-runtime
|
|
@@ -2618,6 +2624,15 @@ importers:
|
|
|
2618
2624
|
specifier: ^6.1.3
|
|
2619
2625
|
version: 6.1.3
|
|
2620
2626
|
|
|
2627
|
+
src/suite-vendor/a-vona/modules/a-retryable:
|
|
2628
|
+
devDependencies:
|
|
2629
|
+
clean-package:
|
|
2630
|
+
specifier: ^2.2.0
|
|
2631
|
+
version: 2.2.0
|
|
2632
|
+
rimraf:
|
|
2633
|
+
specifier: ^6.1.3
|
|
2634
|
+
version: 6.1.3
|
|
2635
|
+
|
|
2621
2636
|
src/suite-vendor/a-vona/modules/a-runtime:
|
|
2622
2637
|
devDependencies:
|
|
2623
2638
|
clean-package:
|
|
@@ -377,6 +377,7 @@ export * from '../dto/productSelectRes.tsx';
|
|
|
377
377
|
export * from '../dto/productSelectResItem.tsx';
|
|
378
378
|
export * from '../dto/productUpdate.tsx';
|
|
379
379
|
export * from '../dto/productView.tsx';
|
|
380
|
+
export * from '../dto/skuAttribute.tsx';
|
|
380
381
|
export * from '../dto/skuCreate.tsx';
|
|
381
382
|
export * from '../dto/skuSelectReq.tsx';
|
|
382
383
|
export * from '../dto/skuSelectRes.tsx';
|
|
@@ -399,6 +400,7 @@ import type { IDtoOptionsProductSelectRes } from '../dto/productSelectRes.tsx';
|
|
|
399
400
|
import type { IDtoOptionsProductSelectResItem } from '../dto/productSelectResItem.tsx';
|
|
400
401
|
import type { IDtoOptionsProductUpdate } from '../dto/productUpdate.tsx';
|
|
401
402
|
import type { IDtoOptionsProductView } from '../dto/productView.tsx';
|
|
403
|
+
import type { IDtoOptionsSkuAttribute } from '../dto/skuAttribute.tsx';
|
|
402
404
|
import type { IDtoOptionsSkuCreate } from '../dto/skuCreate.tsx';
|
|
403
405
|
import type { IDtoOptionsSkuSelectReq } from '../dto/skuSelectReq.tsx';
|
|
404
406
|
import type { IDtoOptionsSkuSelectRes } from '../dto/skuSelectRes.tsx';
|
|
@@ -425,6 +427,7 @@ declare module 'vona-module-a-web' {
|
|
|
425
427
|
'commerce-catalog:productSelectResItem': IDtoOptionsProductSelectResItem;
|
|
426
428
|
'commerce-catalog:productUpdate': IDtoOptionsProductUpdate;
|
|
427
429
|
'commerce-catalog:productView': IDtoOptionsProductView;
|
|
430
|
+
'commerce-catalog:skuAttribute': IDtoOptionsSkuAttribute;
|
|
428
431
|
'commerce-catalog:skuCreate': IDtoOptionsSkuCreate;
|
|
429
432
|
'commerce-catalog:skuSelectReq': IDtoOptionsSkuSelectReq;
|
|
430
433
|
'commerce-catalog:skuSelectRes': IDtoOptionsSkuSelectRes;
|
|
@@ -456,6 +459,7 @@ import type { DtoProductSelectRes } from '../dto/productSelectRes.tsx';
|
|
|
456
459
|
import type { DtoProductSelectResItem } from '../dto/productSelectResItem.tsx';
|
|
457
460
|
import type { DtoProductUpdate } from '../dto/productUpdate.tsx';
|
|
458
461
|
import type { DtoProductView } from '../dto/productView.tsx';
|
|
462
|
+
import type { DtoSkuAttribute } from '../dto/skuAttribute.tsx';
|
|
459
463
|
import type { DtoSkuCreate } from '../dto/skuCreate.tsx';
|
|
460
464
|
import type { DtoSkuSelectReq } from '../dto/skuSelectReq.tsx';
|
|
461
465
|
import type { DtoSkuSelectRes } from '../dto/skuSelectRes.tsx';
|
|
@@ -528,6 +532,10 @@ declare module 'vona-module-commerce-catalog' {
|
|
|
528
532
|
fields?: TypeEntityOptionsFields<DtoProductView, IDtoOptionsProductView[TypeSymbolKeyFieldsMore]>;
|
|
529
533
|
}
|
|
530
534
|
|
|
535
|
+
export interface IDtoOptionsSkuAttribute {
|
|
536
|
+
fields?: TypeEntityOptionsFields<DtoSkuAttribute, IDtoOptionsSkuAttribute[TypeSymbolKeyFieldsMore]>;
|
|
537
|
+
}
|
|
538
|
+
|
|
531
539
|
export interface IDtoOptionsSkuCreate {
|
|
532
540
|
fields?: TypeEntityOptionsFields<DtoSkuCreate, IDtoOptionsSkuCreate[TypeSymbolKeyFieldsMore]>;
|
|
533
541
|
}
|
|
@@ -42,6 +42,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
42
42
|
table.tableIdentity(entitySku.productId).comment(entitySku.$comment.productId);
|
|
43
43
|
table.string(entitySku.code, 100).comment(entitySku.$comment.code);
|
|
44
44
|
table.integer(entitySku.priceCents).comment(entitySku.$comment.priceCents);
|
|
45
|
+
table.json(entitySku.attributes).comment(entitySku.$comment.attributes);
|
|
45
46
|
table.string(entitySku.lifecycle, 20).comment(entitySku.$comment.lifecycle);
|
|
46
47
|
table.index([entitySku.productId], `idx_${entitySku.$table}_productId`);
|
|
47
48
|
table.index([entitySku.lifecycle], `idx_${entitySku.$table}_lifecycle`);
|
|
@@ -10,6 +10,9 @@ export default {
|
|
|
10
10
|
ProductTitle: 'Product title',
|
|
11
11
|
Published: 'Published',
|
|
12
12
|
Sku: 'SKU',
|
|
13
|
+
SkuAttributeName: 'SKU attribute name',
|
|
14
|
+
SkuAttributeValue: 'SKU attribute value',
|
|
15
|
+
SkuAttributes: 'SKU attributes',
|
|
13
16
|
SkuCode: 'SKU code',
|
|
14
17
|
SkuLifecycle: 'SKU lifecycle',
|
|
15
18
|
SkuLifecycleActive: 'Active',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
2
|
+
|
|
3
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
4
|
+
import { Dto } from 'vona-module-a-web';
|
|
5
|
+
|
|
6
|
+
import { $locale } from '../.metadata/locales.ts';
|
|
7
|
+
|
|
8
|
+
export interface IDtoOptionsSkuAttribute extends IDecoratorDtoOptions {}
|
|
9
|
+
|
|
10
|
+
@Dto<IDtoOptionsSkuAttribute>()
|
|
11
|
+
export class DtoSkuAttribute {
|
|
12
|
+
@Api.field(v.title($locale('SkuAttributeName')), v.required(), v.min(1), v.max(100))
|
|
13
|
+
name: string;
|
|
14
|
+
|
|
15
|
+
@Api.field(v.title($locale('SkuAttributeValue')), v.required(), v.min(1), v.max(255))
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
@@ -7,6 +7,7 @@ import z from 'zod';
|
|
|
7
7
|
import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
|
|
8
8
|
|
|
9
9
|
import { $locale } from '../.metadata/locales.ts';
|
|
10
|
+
import { DtoSkuAttribute } from '../dto/skuAttribute.tsx';
|
|
10
11
|
|
|
11
12
|
export const skuLifecycleItems = [
|
|
12
13
|
{ value: 'draft', title: $locale('SkuLifecycleDraft') },
|
|
@@ -56,10 +57,18 @@ export class EntitySku extends EntityBase {
|
|
|
56
57
|
)
|
|
57
58
|
priceCents: number;
|
|
58
59
|
|
|
60
|
+
@Api.field(
|
|
61
|
+
v.title($locale('SkuAttributes')),
|
|
62
|
+
v.default([]),
|
|
63
|
+
v.array(v.object(DtoSkuAttribute)),
|
|
64
|
+
ZovaRender.order(4),
|
|
65
|
+
)
|
|
66
|
+
attributes: DtoSkuAttribute[];
|
|
67
|
+
|
|
59
68
|
@Api.field(
|
|
60
69
|
v.title($locale('SkuLifecycle')),
|
|
61
70
|
v.default('draft'),
|
|
62
|
-
ZovaRender.order(
|
|
71
|
+
ZovaRender.order(5),
|
|
63
72
|
z.enum(['draft', 'active', 'inactive', 'archived']),
|
|
64
73
|
)
|
|
65
74
|
lifecycle: 'draft' | 'active' | 'inactive' | 'archived';
|
|
@@ -448,7 +448,7 @@ describe('catalog.test.ts', () => {
|
|
|
448
448
|
let foreignCatalog!: Awaited<ReturnType<typeof createCatalog>>;
|
|
449
449
|
await app.bean.executor.mockCtx(
|
|
450
450
|
async () => {
|
|
451
|
-
foreignCatalog = await createCatalog(`
|
|
451
|
+
foreignCatalog = await createCatalog(`crf-${suffix}`, 'active');
|
|
452
452
|
},
|
|
453
453
|
{ instanceName: 'shareTest' as any },
|
|
454
454
|
);
|