cabloy 5.1.115 → 5.1.117
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-contract-loop/SKILL.md +8 -2
- package/.claude/skills/cabloy-contract-loop/evals/evals.json +6 -0
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +13 -0
- package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +16 -3
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +12 -0
- package/.github/workflows/vona-test-mysql.yml +6 -10
- package/.github/workflows/vona-test-pg.yml +9 -10
- package/.github/workflows/vona-test-sqlite3.yml +3 -0
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +2 -0
- package/cabloy-docs/.vitepress/config.mjs +6 -0
- package/cabloy-docs/ai/docs-skills-rules-mapping.md +7 -0
- package/cabloy-docs/backend/aop-overview.md +16 -24
- package/cabloy-docs/backend/backend-source-reading-roadmap.md +26 -2
- package/cabloy-docs/backend/controller-aop-guide.md +38 -14
- package/cabloy-docs/backend/logger-guide.md +1 -0
- package/cabloy-docs/backend/menu-guide.md +1 -1
- package/cabloy-docs/backend/orm-mutation-guide.md +36 -10
- package/cabloy-docs/backend/orm-select-guide.md +15 -10
- package/cabloy-docs/backend/rate-limit-guide.md +97 -0
- package/cabloy-docs/backend/telemetry-guide.md +93 -0
- package/cabloy-docs/backend/vona-source-reading-map.md +44 -1
- package/cabloy-docs/frontend/model-resource-owner-pattern.md +2 -0
- package/cabloy-docs/frontend/ssr-overview.md +2 -0
- package/cabloy-docs/fullstack/admin-resource-and-web-self-service.md +329 -0
- package/cabloy-docs/fullstack/contract-loop-playbook.md +1 -1
- package/cabloy-docs/fullstack/introduction.md +1 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +479 -1
- package/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoBaseName%>.tsx_ +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoMutateName%>.tsx_ +2 -2
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoResItemName%>.tsx_ +10 -10
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoViewName%>.tsx_ +2 -2
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +13 -4
- 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/core/logger/utils.ts +12 -3
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +237 -164
- package/vona/src/backend/config/config/config.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +81 -25
- package/vona/src/suite/a-commerce/modules/commerce-member/src/bean/ssrMenu.address.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/controller/address.ts +42 -20
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +39 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineReq.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineView.tsx +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +1 -7
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +43 -42
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +168 -127
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +76 -3
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAudit.tsx +54 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/mockPaymentAdapter.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +22 -7
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +480 -8
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/ssrMenu.test.ts +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +309 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/ssrMenu.order.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +69 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/payment.ts +23 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +4 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderDetail.tsx +72 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderMineReq.tsx +21 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderMineRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectReq.tsx +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +38 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderShip.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +26 -0
- package/vona/src/suite/a-commerce/modules/{commerce-member/src/dto/addressCreate.tsx → commerce-trade/src/dto/orderView.tsx} +5 -17
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/paymentOutcomeCreate.tsx +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/paymentOutcomeResult.tsx +29 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +13 -7
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/shipment.tsx +28 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/shipment.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/cart.ts +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +436 -34
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +4 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +160 -80
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +773 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +212 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +29 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +714 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +282 -9
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipment.test.ts +260 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +15 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +20 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/testLock.ts +11 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +21 -0
- package/vona/src/suite/a-home/modules/home-user/src/controller/passportTest.ts +21 -0
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +77 -0
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/src/service/broadcast.ts +84 -23
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/src/types/broadcast.ts +7 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +8 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +68 -53
- package/vona/src/suite-vendor/a-vona/modules/a-orm/cli/model/metadata/generate.ts +10 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +12 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +3 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_utils.ts +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-queue/src/service/queue.ts +103 -54
- package/vona/src/suite-vendor/a-vona/modules/a-queue/src/types/queue.ts +5 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +48 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/.metadata/index.ts +100 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/bean/interceptor.rateLimit.ts +72 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/index.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/main.ts +28 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/service/rateLimit.ts +75 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/rateLimit.ts +19 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +1 -2
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/types/redis.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +63 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/.metadata/index.ts +132 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +71 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +73 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/config/config.ts +83 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/index.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/ingress.ts +34 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/monkey.ts +22 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +201 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/types/telemetry.ts +22 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/test/telemetry.test.ts +339 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/package.json +3 -1
- package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +3 -2
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +87 -127
- package/zova/src/front/config/config/config.cabloyCommerce.ts +4 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/cli/openapi.config.ts +9 -1
- package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +16 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/commerceMemberAddress.ts +94 -50
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +162 -52
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +2656 -1298
- package/zova/src/suite/a-commerce/modules/commerce-member/src/apiSchema/commerceMemberAddress.ts +23 -13
- package/zova/src/suite/a-commerce/modules/commerce-member/src/model/addressMine.ts +67 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +21 -11
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +40 -32
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +638 -633
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +6 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionShip.ts +31 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +248 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/checkout.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/order.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/orders.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/payment.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +114 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradePayment.ts +36 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +75 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +762 -46
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +35 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradePayment.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionShip.tsx +41 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx +66 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +2 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +2 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +38 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +31 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/payment.ts +25 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +14 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +124 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +105 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +95 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +88 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +28 -0
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/command.delete.tsx +3 -2
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionView.tsx +1 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +4 -0
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressUpdate.tsx +0 -39
|
@@ -122,29 +122,34 @@ A practical operator-family reading is:
|
|
|
122
122
|
- composition/subquery: `_and_`, `_or_`, `_not_`, `_exists_`, `_notExists_`
|
|
123
123
|
- identifier helper: `_ref_`
|
|
124
124
|
|
|
125
|
-
###
|
|
125
|
+
### Absent, `undefined`, and `null` in `where`
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
A direct `where` object distinguishes omitted conditions from SQL `NULL` checks:
|
|
128
|
+
|
|
129
|
+
| Field value | Meaning |
|
|
130
|
+
| -------------------------- | --------------------------------- |
|
|
131
|
+
| Field is absent | Do not add a condition |
|
|
132
|
+
| Own field with `undefined` | Do not add a condition |
|
|
133
|
+
| `Op.omit` | Explicitly do not add a condition |
|
|
134
|
+
| `null` | Match SQL `NULL` with `IS NULL` |
|
|
135
|
+
|
|
136
|
+
An own `undefined` means the property exists on the JavaScript object but its value is `undefined`, such as `{ title: undefined }`. It has the same query result as an absent field, while `Op.omit` is the readable way to make that omission intentional in a dynamically composed query.
|
|
128
137
|
|
|
129
138
|
Representative pattern:
|
|
130
139
|
|
|
131
140
|
```typescript
|
|
132
141
|
import { Op } from 'vona-module-a-orm';
|
|
133
142
|
|
|
134
|
-
const where = {
|
|
135
|
-
title: { _includes_: 'ai' },
|
|
136
|
-
stars: { _gt_: 20 },
|
|
137
|
-
};
|
|
138
|
-
|
|
139
143
|
await this.scope.model.post.select({
|
|
140
144
|
where: {
|
|
141
|
-
|
|
142
|
-
stars: Op.omit,
|
|
145
|
+
title: undefined, // omitted
|
|
146
|
+
stars: Op.omit, // explicitly omitted
|
|
147
|
+
publishedAt: null, // SQL IS NULL
|
|
143
148
|
},
|
|
144
149
|
});
|
|
145
150
|
```
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
The same direct-`where` rule applies to filters used by mutation, aggregate, and group operations.
|
|
148
153
|
|
|
149
154
|
That distinction also matters at the request-contract layer: omitting a nullable filter and passing a real `null` are different operations. When a query DTO needs `IS NULL` behavior from frontend query params, the parsing contract must explicitly preserve `null` instead of collapsing it to omission.
|
|
150
155
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Rate Limit Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`a-ratelimit` limits matched Vona HTTP API actions across all application workers. It runs as the global `a-ratelimit:rateLimit` interceptor after Passport has resolved an authenticated or anonymous subject, and before request-body parsing, pipes, local middleware, and controller code. See [Controller AOP Guide](/backend/controller-aop-guide) for the general request-stage diagram and the distinction between a local aspect and a route override of an existing global onion.
|
|
6
|
+
|
|
7
|
+
It protects controller APIs. Put volumetric, static-file, unknown-route, and network-edge protection in the reverse proxy, WAF, or gateway.
|
|
8
|
+
|
|
9
|
+
## Enablement and rollout
|
|
10
|
+
|
|
11
|
+
The interceptor ships with `enable: false` to preserve compatibility. Enable the global interceptor through standard onion configuration and begin in observe mode:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
config.onions.interceptor['a-ratelimit:rateLimit'] = {
|
|
15
|
+
enable: true,
|
|
16
|
+
rateLimit: {
|
|
17
|
+
mode: 'observe',
|
|
18
|
+
limit: 120,
|
|
19
|
+
windowMs: 60_000,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`observe` records requests that would exceed the policy but does not reject them. Review `rate_limit.would_reject` events and Redis health, then deliberately enable enforcement:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
config.onions.interceptor['a-ratelimit:rateLimit'] = {
|
|
28
|
+
enable: true,
|
|
29
|
+
rateLimit: {
|
|
30
|
+
mode: 'enforce',
|
|
31
|
+
limit: 120,
|
|
32
|
+
windowMs: 60_000,
|
|
33
|
+
key: 'identity',
|
|
34
|
+
headers: true,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use instance configuration when tenants require different policy values. Each limiter key always includes the active instance, so one instance never consumes another instance’s quota.
|
|
40
|
+
|
|
41
|
+
## Route policies
|
|
42
|
+
|
|
43
|
+
Use `Core.rateLimit(...)` on a controller for its baseline policy, or on an action for a stricter policy, an intentional shared bucket, or an explicit exemption:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Core } from 'vona-module-a-core';
|
|
47
|
+
|
|
48
|
+
@Core.rateLimit({
|
|
49
|
+
enable: true,
|
|
50
|
+
rateLimit: {
|
|
51
|
+
mode: 'enforce',
|
|
52
|
+
limit: 5,
|
|
53
|
+
windowMs: 60_000,
|
|
54
|
+
name: 'password-reset',
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
@Web.post('request-reset')
|
|
58
|
+
@Passport.public()
|
|
59
|
+
async requestReset() {}
|
|
60
|
+
|
|
61
|
+
@Core.rateLimit({ enable: false })
|
|
62
|
+
@Web.get('health')
|
|
63
|
+
@Passport.public()
|
|
64
|
+
health() {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`enable`, matching, and ordering remain outer interceptor options. The nested `rateLimit` object contains the quota policy, so partial controller/action overrides inherit the remaining policy fields through normal Vona onion merging. Interceptor options use normal Vona precedence: application config, active-instance config, controller/action decorators, then controlled dynamic onion overrides used by tests. A controller/action can enable a narrower policy while the global interceptor is disabled, or set `enable: false` as a reviewed exemption after global activation. Do not put this policy in another middleware’s options: only `a-ratelimit:rateLimit` consumes the `rateLimit` object at the post-Passport interceptor stage.
|
|
68
|
+
|
|
69
|
+
## Identity and Redis storage
|
|
70
|
+
|
|
71
|
+
The default `key: 'identity'` combines the proxy-trusted `ctx.ip` with the authenticated subject. Anonymous callers therefore receive an IP-scoped budget. Use `key: 'ip'` for an explicitly IP-only policy or `key: 'subject'` when a stable subject budget must follow a user across addresses.
|
|
72
|
+
|
|
73
|
+
Do not parse `X-Forwarded-For` in application code. Vona resolves `ctx.ip` according to its configured proxy trust settings. The trusted reverse proxy must replace client-supplied forwarding headers before forwarding a request.
|
|
74
|
+
|
|
75
|
+
The limiter always uses the dedicated `limiter` Redis client; this infrastructure choice is not a route-policy field. It registers one atomic Lua fixed-window counter command during module startup and invokes that command for each request. Redis keys contain the instance, policy, normalized route, window start, and a SHA-256 digest of the identity; raw IP addresses and subject IDs are not stored in key text.
|
|
76
|
+
|
|
77
|
+
Fixed windows provide one low-cost atomic operation per request. A limit can burst around a window boundary; choose lower limits or shorter windows for especially sensitive actions.
|
|
78
|
+
|
|
79
|
+
## HTTP response contract
|
|
80
|
+
|
|
81
|
+
In enforce mode, evaluated requests expose RFC 9333 fields when `headers` is enabled:
|
|
82
|
+
|
|
83
|
+
- `RateLimit-Limit`
|
|
84
|
+
- `RateLimit-Remaining`
|
|
85
|
+
- `RateLimit-Reset` (seconds until the window resets)
|
|
86
|
+
|
|
87
|
+
A rejected request returns Vona’s standard HTTP `429 Too Many Requests` response and also includes `Retry-After`. The rejected request does not reach body parsing or controller logic.
|
|
88
|
+
|
|
89
|
+
If the limiter Redis operation fails during enforce mode, Cabloy returns `503`, not `429`, and logs `rate_limit.redis_error`. This closed behavior preserves admission protection rather than silently accepting unbounded public traffic. During observe mode, a Redis failure is logged but does not turn the observation rollout into an outage.
|
|
90
|
+
|
|
91
|
+
## Operations
|
|
92
|
+
|
|
93
|
+
The primary structured events are `rate_limit.would_reject`, `rate_limit.rejected`, and `rate_limit.redis_error`. They contain only route/policy/instance and error-class information; do not add raw identities, full URLs, or Redis keys to logs or metric labels.
|
|
94
|
+
|
|
95
|
+
The `limiter` client may initially use the same Redis deployment as other Vona clients, but it is separately configured so production can isolate its endpoint, timeout, and capacity. Ensure Redis remains available before globally setting `enable: true` with `rateLimit.mode: 'enforce'`.
|
|
96
|
+
|
|
97
|
+
See [Controller AOP Guide](/backend/controller-aop-guide), [Config Guide](/backend/config-guide), [Redis Guide](/backend/redis-guide), and [Multi-Instance and Instance Resolution](/backend/multi-instance-and-instance-resolution).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Telemetry Guide
|
|
2
|
+
|
|
3
|
+
Vona can export backend distributed traces through the optional `a-telemetry` module. It is disabled by default and keeps Winston as the application logging API.
|
|
4
|
+
|
|
5
|
+
## Enable tracing
|
|
6
|
+
|
|
7
|
+
Configure the following deployment environment variables:
|
|
8
|
+
|
|
9
|
+
```ini
|
|
10
|
+
TELEMETRY_ENABLED=true
|
|
11
|
+
TELEMETRY_SERVICE_NAME=cabloy-basic
|
|
12
|
+
TELEMETRY_OTLP_HTTP_URL=https://collector.example.com/v1/traces
|
|
13
|
+
TELEMETRY_OTLP_HTTP_HEADERS=authorization=Bearer%20token
|
|
14
|
+
TELEMETRY_SAMPLING_ROOT_RATIO=0.1
|
|
15
|
+
|
|
16
|
+
# Optional: continue trusted internal HTTP traces only
|
|
17
|
+
TELEMETRY_INGRESS_TRUSTED_PROXY_CIDRS=10.0.0.0/8
|
|
18
|
+
TELEMETRY_INGRESS_INTERNAL_HEADER=x-vona-telemetry-ingress
|
|
19
|
+
TELEMETRY_INGRESS_INTERNAL_HEADER_VALUE=internal
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The module exports traces through OTLP/HTTP protobuf. Use an OpenTelemetry Collector as the stable integration boundary; the Collector may export to Tempo, Jaeger, or a managed observability platform.
|
|
23
|
+
|
|
24
|
+
`TELEMETRY_SAMPLING_ROOT_RATIO` is the root sampling ratio. Begin with a low value such as `0.01` to `0.1`, then increase only after measuring exporter drops, CPU, memory, and trace volume.
|
|
25
|
+
|
|
26
|
+
## Propagation
|
|
27
|
+
|
|
28
|
+
The module uses W3C Trace Context:
|
|
29
|
+
|
|
30
|
+
- public HTTP ingress ignores caller-supplied `traceparent` and `tracestate`, so local root sampling always applies
|
|
31
|
+
- a trusted internal ingress may continue W3C trace context only when its direct socket peer matches `TELEMETRY_INGRESS_TRUSTED_PROXY_CIDRS` and the protected classification header has the configured value
|
|
32
|
+
- outgoing queue jobs and Redis Broadcast messages carry a versioned technical trace carrier
|
|
33
|
+
- queue and Broadcast consumers create child spans in a new Vona context
|
|
34
|
+
- internal `performAction(...)` calls create an internal child span
|
|
35
|
+
|
|
36
|
+
The trusted-CIDR list defaults to empty. Keep it empty unless a controlled reverse proxy or gateway is responsible for classifying internal traffic. The application evaluates the direct socket peer, not `ctx.innerAccess`, `ctx.ip`, or generic proxy settings. A trusted proxy must remove any client-provided copy of the classification header and overwrite it with its own decision before forwarding the request. When public and internal traffic use the same proxy, CIDR matching alone is insufficient; the protected header is required as the second trust condition.
|
|
37
|
+
|
|
38
|
+
Vona emits `x-request-id` for HTTP requests. It is a request diagnostic identifier and is different from OpenTelemetry `trace_id` and `span_id`.
|
|
39
|
+
|
|
40
|
+
Existing domain `correlationId` values remain business or idempotency identifiers. Do not replace them with trace IDs or automatically attach raw business IDs to span attributes.
|
|
41
|
+
|
|
42
|
+
## Logging correlation
|
|
43
|
+
|
|
44
|
+
When an active telemetry span is present, existing Vona log entries receive these technical fields:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
request_id
|
|
48
|
+
trace_id
|
|
49
|
+
span_id
|
|
50
|
+
trace_flags
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Continue to use `$logger` and `$loggerChild(...)`. Do not create request-specific cached loggers.
|
|
54
|
+
|
|
55
|
+
## Custom module spans
|
|
56
|
+
|
|
57
|
+
Use the global telemetry facade from a container-managed bean when adding a bounded custom operation:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
return await this.bean.telemetry.withNamedSpan('payment.validate', async () => {
|
|
61
|
+
return await this._validatePayment();
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All facade methods are safe to call without checking `enabled`. When telemetry is disabled, `startSpan(...)` returns a non-recording span, `withSpan(...)` directly invokes its callback, and `recordException(...)` does nothing; no trace is created, activated, propagated, or exported.
|
|
66
|
+
|
|
67
|
+
`withNamedSpan(...)` is the preferred API: it creates an active child span, records thrown errors, and always attempts to end the span. For an operation whose lifecycle must be controlled manually, use `startSpan(...)`, `withSpan(...)`, and `recordException(...)` without an `enabled` branch:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const span = this.bean.telemetry.startSpan('payment.validate');
|
|
71
|
+
try {
|
|
72
|
+
return await this.bean.telemetry.withSpan(span, () => this._validatePayment());
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.bean.telemetry.recordException(span, error);
|
|
75
|
+
throw error;
|
|
76
|
+
} finally {
|
|
77
|
+
span.end();
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The facade intentionally does not expose carrier propagation, HTTP ingress trust, HTTP span handling, or provider lifecycle. Use a stable, bounded operation name and follow the privacy and cardinality rules below.
|
|
82
|
+
|
|
83
|
+
## Privacy and cardinality
|
|
84
|
+
|
|
85
|
+
The built-in spans use HTTP method, route templates, status codes, module/controller/action names, queue names, Broadcast names, and retry counts. They do not record request or response bodies, cookies, authorization headers, raw query values, user IDs, tenant names, or business document IDs.
|
|
86
|
+
|
|
87
|
+
Only add business attributes after an explicit privacy, tenancy, cardinality, and retention review. Prefer bounded operation categories over opaque identifiers.
|
|
88
|
+
|
|
89
|
+
## Operations
|
|
90
|
+
|
|
91
|
+
Every Vona worker owns and flushes its own exporter. Export failures and shutdown timeouts must not fail user requests. Configure Collector-side tail sampling for error or slow-trace retention instead of trying to force sampling in application code.
|
|
92
|
+
|
|
93
|
+
Browser telemetry, WebSocket message tracing, database instrumentation, and third-party SDK instrumentation are intentionally separate follow-up work.
|
|
@@ -128,7 +128,50 @@ If the next question becomes specifically about explicit DTOs, inferred DTO help
|
|
|
128
128
|
|
|
129
129
|
If the next question becomes specifically about how those controller, DTO, and entity surfaces become emitted backend contract, continue with [Backend Contract Emission Source Reading Map](/backend/backend-contract-emission-source-reading-map).
|
|
130
130
|
|
|
131
|
-
## 3.
|
|
131
|
+
## 3. Controller request pipeline, onion configuration, and rate limiting
|
|
132
|
+
|
|
133
|
+
Use this path when you are asking questions like:
|
|
134
|
+
|
|
135
|
+
- what runs before route matching, after route matching, or after Passport authentication?
|
|
136
|
+
- should this concern be system/global/local middleware, guard, interceptor, pipe, or filter?
|
|
137
|
+
- why does a controller/action override preserve some global onion options?
|
|
138
|
+
- why does `@Core.rateLimit(...)` configure a global interceptor instead of adding a local one?
|
|
139
|
+
|
|
140
|
+
### Read the docs first
|
|
141
|
+
|
|
142
|
+
- [AOP Overview](/backend/aop-overview)
|
|
143
|
+
- [Controller AOP Guide](/backend/controller-aop-guide)
|
|
144
|
+
- [Controller Guide](/backend/controller-guide)
|
|
145
|
+
- [Rate Limit Guide](/backend/rate-limit-guide) when the concern is request admission
|
|
146
|
+
|
|
147
|
+
### Then read source in this order
|
|
148
|
+
|
|
149
|
+
1. `vona/src/suite-vendor/a-vona/modules/a-web/src/main.ts`
|
|
150
|
+
2. `vona/src/suite-vendor/a-vona/modules/a-web/src/bean/bean.router.ts`
|
|
151
|
+
3. `vona/src/suite-vendor/a-vona/modules/a-web/src/lib/middleware/middlewareGuard.ts`
|
|
152
|
+
4. `vona/src/suite-vendor/a-vona/modules/a-web/src/lib/middleware/middlewareInterceptor.ts`
|
|
153
|
+
5. `vona/src/suite-vendor/a-vona/modules/a-web/src/lib/middleware/middlewarePipe.ts`
|
|
154
|
+
6. `vona/src/suite-vendor/a-vona/modules/a-onion/src/service/onion_.ts`
|
|
155
|
+
7. `vona/src/suite-vendor/a-vona/modules/a-aspect/src/lib/use/useOnionBase.ts`
|
|
156
|
+
8. `vona/src/suite-vendor/a-vona/modules/a-aspect/src/lib/use/useOnionGlobalBase.ts`
|
|
157
|
+
9. `vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/bean/interceptor.rateLimit.ts` when tracing rate-limit policy consumption
|
|
158
|
+
|
|
159
|
+
### What each file clarifies
|
|
160
|
+
|
|
161
|
+
- `main.ts` proves that system middleware is composed before router lookup.
|
|
162
|
+
- `bean.router.ts` establishes the matched-route sequence: global middleware, guard, interceptor, pipe, local middleware, then action.
|
|
163
|
+
- The guard, interceptor, and pipe middleware files show the fixed middle stages independently.
|
|
164
|
+
- `onion_.ts` proves global/local onion composition and the effective deep merge of defaults, configuration, route metadata, and dynamic overrides.
|
|
165
|
+
- `useOnionBase.ts` shows how a local use decorator joins the local execution chain; `useOnionGlobalBase.ts` shows how a global use decorator writes route options for an already-global onion.
|
|
166
|
+
- `interceptor.rateLimit.ts` is the concrete post-Passport policy consumer and shows why `rateLimit` is owned by that interceptor’s options.
|
|
167
|
+
|
|
168
|
+
For exception behavior only, continue to `a-error/src/config/config.ts` and `a-aspectutils/src/service/filter.ts`. Do not treat filters as part of the normal successful request path.
|
|
169
|
+
|
|
170
|
+
### Stop condition
|
|
171
|
+
|
|
172
|
+
Do not infer placement from an aspect/decorator name alone. Prove both the outer route stage in `bean.router.ts` and the inner global/local composition and option merge in `onion_.ts` before changing request-path behavior.
|
|
173
|
+
|
|
174
|
+
## 4. A compact reading strategy
|
|
132
175
|
|
|
133
176
|
When in doubt, use this order:
|
|
134
177
|
|
|
@@ -361,6 +361,8 @@ Use a resource-owner model when:
|
|
|
361
361
|
|
|
362
362
|
This pattern is especially strong for admin-style, resource-driven UIs.
|
|
363
363
|
|
|
364
|
+
When the same persisted domain also has a genuinely different customer self-service contract, keep this owner for the Admin Resource boundary and use a dedicated model for the distinct self-service state. See [Admin Resource and Web Self-Service](/fullstack/admin-resource-and-web-self-service) for the cross-stack decision rule and Commerce Order specimen.
|
|
365
|
+
|
|
364
366
|
## When not to use this pattern
|
|
365
367
|
|
|
366
368
|
Do not reach for this pattern by default when:
|
|
@@ -34,6 +34,8 @@ For browser work that must wait for the initial SSR handoff, register `this.$ssr
|
|
|
34
34
|
|
|
35
35
|
This lifecycle applies only to the initial client hydration of SSR HTML. It is not an SPA-startup or later client-navigation readiness signal. Do not render a client-ready marker into server HTML; add it from an `onHydrated(...)` callback when browser-visible evidence is required.
|
|
36
36
|
|
|
37
|
+
For a render-time branch, use `this.$ssr.isRuntimeSsrHydrated`. It is reactive and is `false` during server rendering and the browser's initial hydration render, then `true` after that hydration completes. It is also immediately `true` for SPA startup and later client navigation. Use it to retain the same neutral shell before hydration and begin private or browser-only queries and UI afterward. It indicates only SSR-hydration lifecycle readiness, not completed route admission, authentication, or data loading.
|
|
38
|
+
|
|
37
39
|
### SEO meta
|
|
38
40
|
|
|
39
41
|
SSR also supports flexible SEO metadata handling.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Admin Resource and Web Self-Service
|
|
2
|
+
|
|
3
|
+
A business domain often has two valid audiences:
|
|
4
|
+
|
|
5
|
+
- **Admin** users perform permission-controlled operational work, usually through schema-driven resource pages.
|
|
6
|
+
- **Web** users perform self-service work on data that belongs to them, usually through purpose-built pages.
|
|
7
|
+
|
|
8
|
+
Those audiences should usually share the same domain, persistence model, and lifecycle rules. They should not automatically share one HTTP projection, one authorization rule, or one frontend page architecture.
|
|
9
|
+
|
|
10
|
+
This guide explains the reusable Cabloy pattern:
|
|
11
|
+
|
|
12
|
+
> Keep one business resource boundary, but expose separate Admin Resource and Web self-service contracts when their authority, scope, projection, or experience differs.
|
|
13
|
+
|
|
14
|
+
The pattern is Common-first and applies to both Cabloy Basic and Cabloy Start. The Commerce Order implementation used below is a **Cabloy Basic specimen**; detect the active edition before relying on its UI, site, or build details.
|
|
15
|
+
|
|
16
|
+
## Where this guide fits
|
|
17
|
+
|
|
18
|
+
This page connects several mechanisms that have their own deeper documentation:
|
|
19
|
+
|
|
20
|
+
- [Backend Resource/Module Contract Chain](/backend/backend-resource-module-contract-chain) explains backend resource layers.
|
|
21
|
+
- [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern) explains Admin resource-state ownership.
|
|
22
|
+
- [Contract Loop Playbook](/fullstack/contract-loop-playbook) explains generated contract direction and recovery.
|
|
23
|
+
- [SSR Review Checklist](/frontend/ssr-review-checklist) explains server-render and hydration review boundaries.
|
|
24
|
+
|
|
25
|
+
Use this page when the design question is broader:
|
|
26
|
+
|
|
27
|
+
> Should Admin operators and Web users consume one resource as one API/page, or as separate audience-specific surfaces?
|
|
28
|
+
|
|
29
|
+
## The shortest accurate model
|
|
30
|
+
|
|
31
|
+
Treat these as different decisions:
|
|
32
|
+
|
|
33
|
+
| Concern | Usually shared | Usually audience-specific |
|
|
34
|
+
| ------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------- |
|
|
35
|
+
| Business identity and lifecycle | Entity, model, domain service, state transitions | — |
|
|
36
|
+
| Authorization | Common authentication infrastructure | Admin role/action guards; Web owner scope |
|
|
37
|
+
| HTTP contract | Controller/module ownership and generated-contract workflow | Operation names, request DTOs, response DTOs |
|
|
38
|
+
| Query semantics | Instance scope and domain persistence | Operational query capability; customer-visible predicates |
|
|
39
|
+
| Frontend state | Generated API contract and domain terminology | Admin resource owner; Web self-service model |
|
|
40
|
+
| User interface | Shared frontend framework and routing system | `presetResource` Admin page; purpose-built Web page |
|
|
41
|
+
|
|
42
|
+
Do **not** duplicate a persistence model merely because Admin and Web read the domain differently. Conversely, do **not** force a customer experience through the Admin Resource contract merely because both consumers refer to the same rows.
|
|
43
|
+
|
|
44
|
+
A useful default is:
|
|
45
|
+
|
|
46
|
+
- **Admin:** preserve the conventional Resource actions `select` and `view`; add `create`, `update`, and `delete` only when the operators truly own those mutations.
|
|
47
|
+
- **Web:** use explicit self-service names such as `mine` and `viewMine`; the service derives ownership from the authenticated user.
|
|
48
|
+
|
|
49
|
+
This makes the contract self-describing. An unqualified `view` remains the operational Resource action, while `viewMine` communicates a different authority and query scope.
|
|
50
|
+
|
|
51
|
+
## Specimen: one Order domain, two read contracts
|
|
52
|
+
|
|
53
|
+
The Cabloy Basic Commerce Order controller is a read-only Admin Resource plus a customer self-service surface:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Order domain
|
|
57
|
+
├── Admin Resource
|
|
58
|
+
│ ├── GET /order → select
|
|
59
|
+
│ └── GET /order/:id → view
|
|
60
|
+
└── Web self-service
|
|
61
|
+
├── GET /order/mine → mine
|
|
62
|
+
└── GET /order/viewMine/:id → viewMine
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See the controller in [order.ts](https://github.com/cabloy/cabloy/blob/main/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts).
|
|
66
|
+
|
|
67
|
+
The controller uses `@Resource()` so the ordinary `select` and `view` actions participate in the Admin Resource contract. They use `@Passport.systemAdmin()` because they are operational reads. The `mine` and `viewMine` actions remain separate customer-facing operations; they do not inherit the Admin action guard.
|
|
68
|
+
|
|
69
|
+
A read-only Resource is a first-class shape. Registering `select` and `view` does not require pretending that operators may create, update, or delete an immutable business record such as an Order.
|
|
70
|
+
|
|
71
|
+
### Navigation is not authorization
|
|
72
|
+
|
|
73
|
+
An Admin SSR menu can point to `presetResource` and declare a `systemAdmin` role. That controls navigation disclosure and selects the generic Resource UI. It does **not** authorize the API action.
|
|
74
|
+
|
|
75
|
+
Likewise, a protected Web route can provide browser admission and redirect behavior, but it does not replace backend owner scope or controller authorization.
|
|
76
|
+
|
|
77
|
+
Keep these boundaries separate:
|
|
78
|
+
|
|
79
|
+
1. menu and route metadata decide whether and how a user reaches a page;
|
|
80
|
+
2. controller guards decide whether a caller may invoke an operation;
|
|
81
|
+
3. service/model query scope decides which records that operation can observe.
|
|
82
|
+
|
|
83
|
+
For menu behavior, see [Menu Guide](/backend/menu-guide). For route admission, see [Navigation Guards Guide](/frontend/navigation-guards-guide).
|
|
84
|
+
|
|
85
|
+
## Make DTOs audience-specific projections
|
|
86
|
+
|
|
87
|
+
An entity is a persistence shape, not a promise to every API consumer. Use operation-specific DTOs for each audience.
|
|
88
|
+
|
|
89
|
+
In the Order specimen, the Admin Resource uses a query/list/view family:
|
|
90
|
+
|
|
91
|
+
- `DtoOrderSelectReq`
|
|
92
|
+
- `DtoOrderSelectResItem`
|
|
93
|
+
- `DtoOrderSelectRes`
|
|
94
|
+
- `DtoOrderView`
|
|
95
|
+
|
|
96
|
+
These contracts can expose the operational projection and the metadata that generic Admin list and entry pages require.
|
|
97
|
+
|
|
98
|
+
The customer surface uses a deliberately narrow family:
|
|
99
|
+
|
|
100
|
+
- `DtoOrderMineReq`
|
|
101
|
+
- `DtoOrderMineRes`
|
|
102
|
+
- `DtoOrderSummary`
|
|
103
|
+
- `DtoOrderDetail`
|
|
104
|
+
|
|
105
|
+
For example, the customer summary has only the stable list information needed by the customer experience: identity, customer-visible state, currency, payable total, and creation time. It does not expose `userId`, instance information, correlation identifiers, or Admin page metadata.
|
|
106
|
+
|
|
107
|
+
The detail projection can include customer-relevant immutable snapshots, such as the purchase address and order lines, without becoming an alias for the entire persisted row.
|
|
108
|
+
|
|
109
|
+
### Keep request authority on the server
|
|
110
|
+
|
|
111
|
+
A self-service request DTO may expose useful page controls such as a visible state filter, date filter, sort, `pageNo`, and `pageSize`. It must not let the caller supply the authoritative owner identity or tenant/instance boundary.
|
|
112
|
+
|
|
113
|
+
In the Order specimen, `DtoOrderMineReq` derives from `$Dto.queryPage(...)`, but it only permits customer-safe filters. The service obtains the user id from the current Passport and adds it to the query itself.
|
|
114
|
+
|
|
115
|
+
This distinction is essential:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Web request: permitted filter and paging intent
|
|
119
|
+
Service: authoritative owner and visible-state predicates
|
|
120
|
+
Model/ORM: normal active-instance scope
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
See [DTO Guide](/backend/dto-guide) for operation DTO design and [Multi-Instance and Instance Resolution](/backend/multi-instance-and-instance-resolution) for the normal instance boundary.
|
|
124
|
+
|
|
125
|
+
## Put ownership and visibility inside the paged query
|
|
126
|
+
|
|
127
|
+
Self-service visibility is a query rule, not a presentation cleanup step.
|
|
128
|
+
|
|
129
|
+
The Order service uses `selectAndCount(...)` to apply all of these before the database calculates the count or page:
|
|
130
|
+
|
|
131
|
+
- the owner derived from `currentUser`;
|
|
132
|
+
- the customer-visible order states;
|
|
133
|
+
- permitted caller filters;
|
|
134
|
+
- deterministic default ordering.
|
|
135
|
+
|
|
136
|
+
Conceptually:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const result = await model.order.selectAndCount({
|
|
140
|
+
...params,
|
|
141
|
+
where: {
|
|
142
|
+
...params?.where,
|
|
143
|
+
userId: currentUser.id,
|
|
144
|
+
state: customerVisibleStates,
|
|
145
|
+
},
|
|
146
|
+
orders: params?.orders ?? [['id', 'desc']],
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The order of responsibility matters:
|
|
151
|
+
|
|
152
|
+
1. accept only the DTO's permitted filtering intent;
|
|
153
|
+
2. overwrite authoritative constraints such as owner and allowed states;
|
|
154
|
+
3. let the scoped model execute count, ordering, offset, and limit together;
|
|
155
|
+
4. map the result to the Web projection.
|
|
156
|
+
|
|
157
|
+
Do not load a broad result, paginate it, and then remove foreign or unavailable rows in memory. That produces incorrect `total` and `pageCount`, can make pages unexpectedly empty, and risks exposing data before filtering.
|
|
158
|
+
|
|
159
|
+
For a single record, include the owner in the lookup itself. A foreign record should normally be absent from the caller's scoped result; do not use a cross-instance probe merely to change absence into a distinguishable authorization response.
|
|
160
|
+
|
|
161
|
+
See [ORM Select Guide](/backend/orm-select-guide) for query and projection behavior.
|
|
162
|
+
|
|
163
|
+
### Pagination is one contract across layers
|
|
164
|
+
|
|
165
|
+
A list is not genuinely paginated until all of these agree:
|
|
166
|
+
|
|
167
|
+
1. the request DTO derives from `$Dto.queryPage(...)`;
|
|
168
|
+
2. the response DTO derives from `$Dto.listAndCount(...)`;
|
|
169
|
+
3. the service applies audience scope before `selectAndCount(...)`;
|
|
170
|
+
4. the generated SDK describes the list-and-count result;
|
|
171
|
+
5. the frontend model includes the varying query input in its query key;
|
|
172
|
+
6. the page consumes `list`, `pageNo`, `pageSize`, `total`, and `pageCount` consistently.
|
|
173
|
+
|
|
174
|
+
The Order Web page demonstrates the final step by requesting `{ pageNo, pageSize }`, rendering `data.list`, and changing pages from the returned `pageCount`.
|
|
175
|
+
|
|
176
|
+
## Admin frontend: let the Resource owner stay the owner
|
|
177
|
+
|
|
178
|
+
For an operational Admin surface, the generic Resource infrastructure is usually the right page architecture.
|
|
179
|
+
|
|
180
|
+
The Order specimen has an SSR menu entry whose `presetResource` target identifies `commerce-trade:order`. Generic Admin Resource pages then consume the backend's Resource metadata, permission surface, schemas, table behavior, and read actions.
|
|
181
|
+
|
|
182
|
+
The module-level `ModelOrder` is intentionally thin:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
@Use({ beanFullName: 'rest-resource.model.resource' })
|
|
186
|
+
protected get $$modelResource(): ModelResource {
|
|
187
|
+
return usePrepareArg('commerce-trade:order', true);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
select(query?: Record<string, unknown>) {
|
|
191
|
+
return this.$$modelResource.select(query);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This is not a second Admin cache tree. `ModelResource` remains the selector-scoped owner of resource bootstrap, permissions, schemas, query state, and invalidation policy. The thin facade is useful only when it gives the module a clearer semantic boundary while preserving the generic Resource surface.
|
|
196
|
+
|
|
197
|
+
Use this path when the Admin experience is mainly:
|
|
198
|
+
|
|
199
|
+
- schema-driven list, filter, view, form, or action behavior;
|
|
200
|
+
- conventional Resource permissions and metadata;
|
|
201
|
+
- operational work that benefits from generic page infrastructure.
|
|
202
|
+
|
|
203
|
+
For the underlying owner pattern, read [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern), [Using `ModelResource` in Your Module](/frontend/model-resource-usage-guide), and [Resource Model Best Practices and Anti-Patterns](/frontend/model-resource-best-practices).
|
|
204
|
+
|
|
205
|
+
## Web frontend: use a dedicated self-service model when semantics diverge
|
|
206
|
+
|
|
207
|
+
A customer order page is not a smaller Admin table. It has different API names, customer-safe projections, owner scope, interaction flow, and presentation requirements.
|
|
208
|
+
|
|
209
|
+
The Order specimen therefore uses `ModelOrderMine` for `mine` and `viewMine`, alongside purpose-built orders-list and order-detail page controllers. The dedicated model owns query state for those self-service operations:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
mine(query) {
|
|
213
|
+
return this.$useStateData({
|
|
214
|
+
queryKey: ['mine', query],
|
|
215
|
+
queryFn: () => this.scope.api.commerceTradeOrder.mine({ query }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This does not violate the Admin Resource owner rule. The two model boundaries own different state domains:
|
|
221
|
+
|
|
222
|
+
| Model | Owns |
|
|
223
|
+
| ------------------------------------ | ------------------------------------------------------------------------ |
|
|
224
|
+
| Admin `ModelOrder` → `ModelResource` | Admin Resource `select`/`view`, schemas, permissions, generic page state |
|
|
225
|
+
| Web `ModelOrderMine` | Customer `mine`/`viewMine` queries and customer-page state |
|
|
226
|
+
|
|
227
|
+
Use a dedicated Web model and pages when the self-service contract is genuinely different. Do not create a parallel cache owner merely because an Admin endpoint needs a small domain-specific helper; keep such Admin helpers as thin facades over the existing Resource owner instead.
|
|
228
|
+
|
|
229
|
+
For model-owned remote state and query keys, see [Model State Guide](/frontend/model-state-guide).
|
|
230
|
+
|
|
231
|
+
## Private Web SSR needs explicit admission and hydration boundaries
|
|
232
|
+
|
|
233
|
+
Private customer data must not appear in server HTML when the SSR path cannot establish the customer's authenticated state. The Order specimen keeps a neutral shell through server render and hydration-time initial render, then begins private queries only at an explicit client-side boundary.
|
|
234
|
+
|
|
235
|
+
The required layers work together:
|
|
236
|
+
|
|
237
|
+
1. **Route admission** uses the appropriate protected-route metadata for browser navigation.
|
|
238
|
+
2. **Backend contract enforcement** uses controller guards and service-level owner scope as the authoritative data boundary.
|
|
239
|
+
3. **Model gating** avoids creating the private query until the browser runtime and authenticated Passport are available.
|
|
240
|
+
4. **Page gating** keeps the same neutral shell until the page reaches its post-hydration admission boundary.
|
|
241
|
+
5. **Query behavior** may use `disableSuspenseOnInit: true` where appropriate, but that flag only disables the initialization-time suspense kick.
|
|
242
|
+
|
|
243
|
+
Do not treat `disableSuspenseOnInit` as proof that a query cannot be created, fetched, or affect hydration. It is not an authorization mechanism or a substitute for an explicit hydration boundary.
|
|
244
|
+
|
|
245
|
+
For the full review criteria, see [SSR Review Checklist](/frontend/ssr-review-checklist), [SSR Init Data](/frontend/ssr-init-data), and [`$useStateData` Best Practices](/frontend/use-state-data-best-practices).
|
|
246
|
+
|
|
247
|
+
## Follow the forward contract loop
|
|
248
|
+
|
|
249
|
+
Changing audience-specific backend contracts is a forward-chain change.
|
|
250
|
+
|
|
251
|
+
Use this order:
|
|
252
|
+
|
|
253
|
+
1. change Vona controller and DTO contract truth first;
|
|
254
|
+
2. inspect the emitted OpenAPI surface, including every Admin and Web operation;
|
|
255
|
+
3. keep the Zova module's operation ownership explicit;
|
|
256
|
+
4. regenerate generated SDK, schema, and type consumers;
|
|
257
|
+
5. adapt thin Admin facades or dedicated Web models to the generated API;
|
|
258
|
+
6. do not hand-edit generated API clients or types.
|
|
259
|
+
|
|
260
|
+
The Order module owns its four operations—`select`, `view`, `mine`, and `viewMine`—through its OpenAPI configuration. That is the appropriate place to constrain the generated module contract slice.
|
|
261
|
+
|
|
262
|
+
Read [Backend OpenAPI to Frontend SDK](/fullstack/openapi-to-sdk) for the forward-chain bridge and [Contract Loop Playbook](/fullstack/contract-loop-playbook) for generation, consumer drift, reverse-chain, and local dependency-drift decisions.
|
|
263
|
+
|
|
264
|
+
## Verification checklist
|
|
265
|
+
|
|
266
|
+
Review this pattern at the boundaries where an accidental merge would matter:
|
|
267
|
+
|
|
268
|
+
- [ ] Admin Resource permissions expose exactly the operational actions intended; a read-only resource does not accidentally gain mutation actions.
|
|
269
|
+
- [ ] Admin `select` and `view` enforce their controller authorization independently of menu visibility.
|
|
270
|
+
- [ ] Web list and detail derive ownership server-side and treat foreign or cross-instance records as absent.
|
|
271
|
+
- [ ] Web visibility predicates are applied before count, ordering, and pagination.
|
|
272
|
+
- [ ] Web request and response projections contain no owner, tenant, or operational-only data.
|
|
273
|
+
- [ ] Generated contract output contains every intended Admin and Web operation, and frontend consumers use it without manual generated-file edits.
|
|
274
|
+
- [ ] The Admin menu reaches the intended `presetResource` entry and generic Resource page.
|
|
275
|
+
- [ ] Web SSR and hydration render the same neutral private-data shell until admission; post-hydration behavior is tested separately.
|
|
276
|
+
- [ ] Where the domain has a meaningful customer journey, targeted browser coverage proves the authenticated list/detail flow as well as access denial.
|
|
277
|
+
|
|
278
|
+
Use [Unit Testing](/backend/unit-testing) for backend test conventions and [SSR Review Checklist](/frontend/ssr-review-checklist) for SSR-specific checks.
|
|
279
|
+
|
|
280
|
+
## Anti-patterns
|
|
281
|
+
|
|
282
|
+
Avoid these shortcuts:
|
|
283
|
+
|
|
284
|
+
### One broad endpoint with role-dependent response shapes
|
|
285
|
+
|
|
286
|
+
Do not make `view(id)` return a full operational row for an Admin and a reduced customer row for someone else. The contract, authorization, cache semantics, and generated types become ambiguous. Use distinct operations and DTOs.
|
|
287
|
+
|
|
288
|
+
### Post-pagination ownership filtering
|
|
289
|
+
|
|
290
|
+
Do not query broad rows, page them, and then filter for owner or state in memory. Scope the database query first.
|
|
291
|
+
|
|
292
|
+
### Navigation as authorization
|
|
293
|
+
|
|
294
|
+
Do not rely on `@SsrMenu` roles or frontend `requiresAuth` to secure an API. They affect navigation and admission only; backend operations must enforce authority and data scope themselves.
|
|
295
|
+
|
|
296
|
+
### Generic Admin page as a customer experience
|
|
297
|
+
|
|
298
|
+
Do not use `presetResource` just because a customer page happens to list resource rows. Customer-oriented layout, detail, pagination, and actions often deserve purpose-built pages.
|
|
299
|
+
|
|
300
|
+
### Competing resource cache ownership
|
|
301
|
+
|
|
302
|
+
Do not create another Admin resource model/cache owner when the custom Admin operation still belongs to the same Resource boundary. Reuse `ModelResource` through a thin facade. Create a separate model only for a genuinely distinct self-service state domain.
|
|
303
|
+
|
|
304
|
+
### Hydration flags as privacy controls
|
|
305
|
+
|
|
306
|
+
Do not assume a suspense or rendering flag prevents private data loading. Use explicit server, browser, authentication, and post-hydration boundaries.
|
|
307
|
+
|
|
308
|
+
### Hand-patched generated contracts
|
|
309
|
+
|
|
310
|
+
Do not fix a changed backend operation by editing generated frontend API or type files. Repair contract truth and regenerate the owned SDK slice.
|
|
311
|
+
|
|
312
|
+
## Decision rule
|
|
313
|
+
|
|
314
|
+
When one business resource serves Admin and Web users:
|
|
315
|
+
|
|
316
|
+
> Share domain persistence and lifecycle logic. Split API contracts, DTO projections, query authority, frontend state ownership, and page architecture wherever the audiences have different authority or experience.
|
|
317
|
+
|
|
318
|
+
Use Admin `select`/`view` with the generic Resource owner for conventional operations. Use explicit Web self-service operations and dedicated Web state/pages when customer scope and UX diverge.
|
|
319
|
+
|
|
320
|
+
## Read next
|
|
321
|
+
|
|
322
|
+
- [Backend Resource/Module Contract Chain](/backend/backend-resource-module-contract-chain)
|
|
323
|
+
- [Menu Guide](/backend/menu-guide)
|
|
324
|
+
- [ORM Select Guide](/backend/orm-select-guide)
|
|
325
|
+
- [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
|
|
326
|
+
- [Resource Model Best Practices and Anti-Patterns](/frontend/model-resource-best-practices)
|
|
327
|
+
- [SSR Review Checklist](/frontend/ssr-review-checklist)
|
|
328
|
+
- [Backend OpenAPI to Frontend SDK](/fullstack/openapi-to-sdk)
|
|
329
|
+
- [Contract Loop Playbook](/fullstack/contract-loop-playbook)
|
|
@@ -226,7 +226,7 @@ That means:
|
|
|
226
226
|
- use **thin model facades** as semantic wrappers over generated APIs
|
|
227
227
|
- **reuse the existing resource-owner** when the custom API still belongs to an existing resource
|
|
228
228
|
|
|
229
|
-
For resource-bound custom endpoints, prefer `rest-resource.model.resource` as the state owner. A module-local model may still exist, but it should remain a semantic facade instead of becoming a second cache owner.
|
|
229
|
+
For resource-bound custom endpoints, prefer `rest-resource.model.resource` as the state owner. A module-local model may still exist, but it should remain a semantic facade instead of becoming a second cache owner. When Admin Resource consumption and Web self-service consumption have genuinely different API, scope, projection, or SSR semantics, see [Admin Resource and Web Self-Service](/fullstack/admin-resource-and-web-self-service) for the boundary between Admin owner reuse and a dedicated Web model.
|
|
230
230
|
|
|
231
231
|
See the downstream pattern in `.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md`.
|
|
232
232
|
|