cabloy 5.1.126 → 5.1.127

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.
Files changed (34) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/cabloy-docs/backend/error-guide.md +17 -0
  4. package/e2e/specs/a-commerce/commerce.spec.ts +135 -4
  5. package/package.json +1 -1
  6. package/vona/packages-cli/cli/package.json +1 -1
  7. package/vona/packages-cli/cli-set-api/package.json +1 -1
  8. package/vona/packages-vona/vona/package.json +1 -1
  9. package/vona/packages-vona/vona-core/package.json +1 -1
  10. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
  11. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
  12. package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
  13. package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
  14. package/vona/packages-vona/vona-mock/package.json +1 -1
  15. package/vona/pnpm-lock.yaml +5 -5
  16. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +36 -1
  17. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  18. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  19. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
  20. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
  21. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
  22. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
  23. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
  24. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
  25. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +3 -0
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +2 -1
  27. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
  28. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  29. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  30. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +8 -4
  31. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +65 -19
  32. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +7 -6
  33. package/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
  34. package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.126
1
+ 5.1.127
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.127
4
+
5
+ ### Features
6
+
7
+ - Separate module error codes from HTTP status codes.
8
+ - Complete the commerce payment refund flow.
9
+ - Update feature functionality.
10
+
11
+ ### Bug Fixes
12
+
13
+ - Harden the commerce order detail locator.
14
+
15
+ ### Improvements
16
+
17
+ - Refactor error handling.
18
+ - Record the first phase of error status evolution.
19
+ - Define product presentation authority.
20
+ - Retain category semantic CI evidence.
21
+ - Retain semantic presentation CI evidence.
22
+ - Prepare the category presentation rollout.
23
+
3
24
  ## 5.1.126
4
25
 
5
26
  ### Features
@@ -37,6 +37,23 @@ export const errors = {
37
37
 
38
38
  A useful convention is that business error codes should stay above `1000`.
39
39
 
40
+ ### Structured declarations and HTTP status
41
+
42
+ Legacy numeric declarations remain supported. When a business error needs a stable namespaced application code and an intentional HTTP status, use a descriptor:
43
+
44
+ ```typescript
45
+ export const errors = {
46
+ ErrorTest: {
47
+ code: 1001,
48
+ status: 409,
49
+ },
50
+ } as const;
51
+ ```
52
+
53
+ The descriptor produces an application error code such as `training-student:1001` and an HTTP response status of `409`. The `code` is the stable business identity; `status` is the initial transport decision. The standard JSON response remains `{ code, message }`, with the HTTP status carried by the response envelope.
54
+
55
+ Use statuses that describe the API meaning: `404` for an absent resource, `409` for a resource-state or uniqueness conflict, and `422` for unacceptable business content. Existing final error filters may still revise the initial status for request-context policy, such as converting unauthenticated authorization failures from `403` to `401`.
56
+
40
57
  ### 2. Define the localized error messages
41
58
 
42
59
  Representative locale pattern:
@@ -535,7 +535,9 @@ test(
535
535
  await orderCard.getByRole('button', { name: 'View', exact: true }).click();
536
536
  await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
537
537
  await historyDetailResponse;
538
- await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible();
538
+ await expect(
539
+ page.getByRole('heading', { name: `Order #${checkout.orderId}`, level: 1 }),
540
+ ).toBeVisible();
539
541
  await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
540
542
  await expect(page.getByRole('alert')).toHaveCount(0);
541
543
  expect(pageErrors).toEqual([]);
@@ -705,11 +707,39 @@ test(
705
707
  const executeResponse = waitForApiResponse(
706
708
  adminPage,
707
709
  'POST',
708
- `/api/commerce/trade/order/${checkout.orderId}/refundOutcome`,
710
+ `/api/commerce/trade/order/${checkout.orderId}/executeRefund`,
709
711
  );
710
712
  await orderRow.getByRole('button', { name: 'Execute refund', exact: true }).click();
711
- expect((await executeResponse).ok()).toBeTruthy();
712
- await expect(orderRow).toContainText('refunded');
713
+ const executeResponseValue = await executeResponse;
714
+ expect(executeResponseValue.ok()).toBeTruthy();
715
+ const executeResult = (await executeResponseValue.json()).data;
716
+ const authorization = executeResponseValue.request().headers()['authorization'];
717
+ expect(authorization).toMatch(/^Bearer /);
718
+ const authHeaders = { Authorization: authorization };
719
+ expectTableIdentity(executeResult.refundAttemptId);
720
+ expectTableIdentity(executeResult.refundOperationId);
721
+ expect([
722
+ executeResult.orderState,
723
+ executeResult.refundState,
724
+ executeResult.refundAttemptState,
725
+ ]).toEqual(['refund_approved', 'approved', 'created']);
726
+ const completeRefundResponse = await adminPage.request.post(
727
+ `/api/pay/mock/payment-session/refund-operation/${executeResult.refundOperationId}/complete`,
728
+ { headers: authHeaders, data: { outcome: 'succeeded' } },
729
+ );
730
+ expect(completeRefundResponse.ok()).toBeTruthy();
731
+ await expect
732
+ .poll(
733
+ async () => {
734
+ await adminPage.reload({ waitUntil: 'load' });
735
+ const currentOrderRow = await getAdminOrderRow(adminPage, checkout.orderId);
736
+ return (await currentOrderRow.textContent()) ?? '';
737
+ },
738
+ { timeout: 40_000 },
739
+ )
740
+ .toContain('refunded');
741
+ const refundedOrderRow = await getAdminOrderRow(adminPage, checkout.orderId);
742
+ await expect(refundedOrderRow).toContainText('refunded');
713
743
  expect(adminPageErrors).toEqual([]);
714
744
  } finally {
715
745
  await adminContext.close().catch(() => {});
@@ -885,6 +915,107 @@ test(
885
915
  },
886
916
  );
887
917
 
918
+ test(
919
+ 'ATP-SPC-02: Category renders semantic Admin relation and publication controls',
920
+ { tag: ['@admin', '@flow', '@category'] },
921
+ async ({ browser }, testInfo) => {
922
+ test.setTimeout(60_000);
923
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
924
+ const parentName = `E2E Category Parent ${suffix}`;
925
+ const childName = `E2E Category Child ${suffix}`;
926
+ const adminContext = await browser.newContext();
927
+ const adminPage = await adminContext.newPage();
928
+ const adminPageErrors = collectPageErrors(adminPage);
929
+ const categoryCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory/create';
930
+ const categoryListPath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory';
931
+ const categoryActionPath = '/api/commerce/catalog/category';
932
+ let parentId: number | string | undefined;
933
+ let childId: number | string | undefined;
934
+ let headers: { Authorization: string } | undefined;
935
+ try {
936
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
937
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
938
+ const accessToken = (await adminContext.cookies()).find(
939
+ cookie => cookie.name === 'token',
940
+ )?.value;
941
+ expect(accessToken).toBeTruthy();
942
+ headers = { Authorization: `Bearer ${accessToken}` };
943
+
944
+ const parentCreateResponse = await adminPage.request.post(categoryActionPath, {
945
+ data: { name: parentName, published: false },
946
+ headers,
947
+ });
948
+ expect(parentCreateResponse.ok()).toBeTruthy();
949
+ parentId = (await parentCreateResponse.json()).data;
950
+ expectTableIdentity(parentId);
951
+
952
+ await adminPage.goto(categoryCreatePath, { waitUntil: 'load' });
953
+ const parentPublication = adminPage
954
+ .getByRole('group', { name: 'Published' })
955
+ .getByRole('combobox');
956
+ await expect(parentPublication).toBeVisible();
957
+ await expect(parentPublication.locator('option')).toHaveText([
958
+ '',
959
+ 'Unpublished',
960
+ 'Published',
961
+ ]);
962
+ await parentPublication.selectOption({ label: 'Unpublished' });
963
+ await expect(parentPublication).toHaveValue('false');
964
+
965
+ const parentPicker = adminPage
966
+ .getByRole('group', { name: 'Parent category' })
967
+ .getByRole('combobox');
968
+ await expect(parentPicker).toBeVisible();
969
+ await parentPicker.selectOption({ label: parentName });
970
+ const parentValue = await parentPicker.inputValue();
971
+ expect(parentValue).toBe(String(parentId));
972
+ const childPublication = adminPage
973
+ .getByRole('group', { name: 'Published' })
974
+ .getByRole('combobox');
975
+ await childPublication.selectOption({ label: 'Published' });
976
+ await expect(childPublication).toHaveValue('true');
977
+
978
+ const childCreateResponse = await adminPage.request.post(categoryActionPath, {
979
+ data: { name: childName, parentId: parentValue, published: true },
980
+ headers,
981
+ });
982
+ expect(childCreateResponse.ok()).toBeTruthy();
983
+ childId = (await childCreateResponse.json()).data;
984
+ expectTableIdentity(childId);
985
+
986
+ await adminPage.goto(categoryListPath, { waitUntil: 'load' });
987
+ const childRow = adminPage.getByRole('row', { name: new RegExp(childName) });
988
+ await expect(childRow).toBeVisible();
989
+ await expect(childRow.getByText(parentName, { exact: true })).toBeVisible();
990
+ await expect(childRow.getByText('Published', { exact: true })).toBeVisible();
991
+
992
+ await adminPage.goto(`${categoryListPath}/${childId}`, { waitUntil: 'load' });
993
+ await expect(
994
+ adminPage.getByRole('group', { name: 'Parent category' }).getByRole('textbox'),
995
+ ).toHaveValue(parentName);
996
+ await expect(
997
+ adminPage.getByRole('group', { name: 'Published' }).getByRole('textbox'),
998
+ ).toHaveValue('Published');
999
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1000
+ expect(adminPageErrors).toEqual([]);
1001
+ } finally {
1002
+ if (childId && headers) {
1003
+ const response = await adminPage.request.delete(`${categoryActionPath}/${childId}`, {
1004
+ headers,
1005
+ });
1006
+ expect(response.ok()).toBeTruthy();
1007
+ }
1008
+ if (parentId && headers) {
1009
+ const response = await adminPage.request.delete(`${categoryActionPath}/${parentId}`, {
1010
+ headers,
1011
+ });
1012
+ expect(response.ok()).toBeTruthy();
1013
+ }
1014
+ await adminContext.close().catch(() => {});
1015
+ }
1016
+ },
1017
+ );
1018
+
888
1019
  test(
889
1020
  'Commerce Cart: anonymous browser is redirected to login',
890
1021
  { tag: ['@web', '@cart'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.126",
3
+ "version": "5.1.127",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.136",
3
+ "version": "1.1.137",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.134",
3
+ "version": "1.1.135",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.83",
3
+ "version": "5.1.84",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.35",
3
+ "version": "5.1.36",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -21,7 +21,7 @@ export class ErrorClass extends BeanSimple {
21
21
  fail(module, code, ...args) {
22
22
  const body = this.parseFail(module, code, ...args);
23
23
 
24
- this.ctx.response.status = __calcStatus(body.code);
24
+ this.ctx.response.status = body.status;
25
25
  this.ctx.response.type = 'application/json';
26
26
  this.ctx.response.body = { code: body.code, message: body.message }; // body maybe Error
27
27
  }
@@ -32,7 +32,7 @@ export class ErrorClass extends BeanSimple {
32
32
  const err = new Error();
33
33
  err.code = body.code;
34
34
  err.message = body.message;
35
- err.status = __calcStatus(body.code);
35
+ err.status = body.status;
36
36
  throw err;
37
37
  }
38
38
 
@@ -58,9 +58,16 @@ export class ErrorClass extends BeanSimple {
58
58
 
59
59
  // convert from enum
60
60
  let text;
61
+ let status;
61
62
  if (ebError && code && typeof code === 'string') {
62
63
  text = code;
63
- code = ebError[code];
64
+ const declaration = ebError[code];
65
+ if (__isErrorDescriptor(declaration)) {
66
+ code = declaration.code;
67
+ status = declaration.status;
68
+ } else {
69
+ code = declaration;
70
+ }
64
71
  }
65
72
 
66
73
  if (code === undefined || code === null || code === '') {
@@ -75,10 +82,20 @@ export class ErrorClass extends BeanSimple {
75
82
  }
76
83
 
77
84
  code = __combineErrorCode(module, code);
78
- return { code, message };
85
+ status ??= __calcStatus(code);
86
+ return { code, status, message };
79
87
  }
80
88
  }
81
89
 
90
+ function __isErrorDescriptor(value) {
91
+ return (
92
+ value &&
93
+ typeof value === 'object' &&
94
+ typeof value.code === 'number' &&
95
+ typeof value.status === 'number'
96
+ );
97
+ }
98
+
82
99
  function __combineErrorCode(module, code) {
83
100
  if (typeof code !== 'number' || code <= 1000) return code;
84
101
  return module ? `${module}:${code}` : code;
@@ -1,4 +1,5 @@
1
1
  export interface IErrorObject {
2
2
  code: number | string;
3
+ status: number;
3
4
  message: string;
4
5
  }
@@ -29,12 +29,14 @@ export type TypeBeanScopeLocaleKeys = keyof IBeanScopeLocale;
29
29
  export interface IBeanScopeErrors {}
30
30
  export type TypeBeanScopeErrorsKeys = keyof IBeanScopeErrors;
31
31
 
32
+ type TypeErrorDeclarationCode<T> = T extends { code: infer Code } ? Code : T;
33
+
32
34
  export type TypeScopesErrorsHelper<
33
35
  ModuleName extends keyof IBeanScopeErrors,
34
36
  Errors extends IBeanScopeErrors[ModuleName],
35
37
  > = {
36
38
  // @ts-ignore: ignore
37
- [K in keyof Errors as `${ModuleName}:${Errors[K]}`]: K;
39
+ [K in keyof Errors as `${ModuleName}:${TypeErrorDeclarationCode<Errors[K]>}`]: K;
38
40
  };
39
41
  export type TypeScopesErrorCodes = TypeRecordValues<{
40
42
  [ModuleName in keyof IBeanScopeErrors]: keyof TypeScopesErrorsHelper<
@@ -4,7 +4,12 @@ import type { IModuleMain, IMonkeyModule, IMonkeySystem } from './monkey.ts';
4
4
 
5
5
  export type TypeModuleResourceLocales = Record<string, object>;
6
6
  export type TypeModuleResourceLocaleModules = Record<string, TypeModuleResourceLocales>;
7
- export type TypeModuleResourceErrors = Record<string, number>;
7
+ export interface IModuleResourceErrorDescriptor {
8
+ code: number;
9
+ status: number;
10
+ }
11
+ export type TypeModuleResourceError = number | IModuleResourceErrorDescriptor;
12
+ export type TypeModuleResourceErrors = Record<string, TypeModuleResourceError>;
8
13
  export type TypeModuleResourceErrorModules = Record<string, TypeModuleResourceErrors>;
9
14
  export type TypeModuleResourceConfig = (app: VonaApplication) => object | Promise<object>;
10
15
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.35",
3
+ "version": "6.1.36",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -1834,16 +1834,16 @@ importers:
1834
1834
  src/suite-vendor/a-pay:
1835
1835
  dependencies:
1836
1836
  vona-module-a-pay:
1837
- specifier: ^5.0.2
1837
+ specifier: ^5.0.3
1838
1838
  version: link:modules/a-pay
1839
1839
  vona-module-pay-mock:
1840
- specifier: ^5.0.2
1840
+ specifier: ^5.0.3
1841
1841
  version: link:modules/pay-mock
1842
1842
  vona-module-pay-paypal:
1843
- specifier: ^5.0.2
1843
+ specifier: ^5.0.3
1844
1844
  version: link:modules/pay-paypal
1845
1845
  vona-module-pay-stripe:
1846
- specifier: ^5.0.2
1846
+ specifier: ^5.0.3
1847
1847
  version: link:modules/pay-stripe
1848
1848
 
1849
1849
  src/suite-vendor/a-pay/modules/a-pay:
@@ -1922,7 +1922,7 @@ importers:
1922
1922
  specifier: ^5.0.8
1923
1923
  version: link:modules/test-file
1924
1924
  vona-module-test-pay:
1925
- specifier: ^5.0.1
1925
+ specifier: ^5.0.2
1926
1926
  version: link:modules/test-pay
1927
1927
 
1928
1928
  src/suite-vendor/a-test/modules/test-auth:
@@ -142,7 +142,12 @@ declare module 'vona' {
142
142
  import type { IModelGetOptions, IModelMethodOptions, IModelSelectParams, TypeModelSelectAndCount, TypeModelRelationResult, TypeModelWhere, IModelInsertOptions, TypeModelMutateRelationData, IModelDeleteOptions, IModelUpdateOptions, IModelMutateOptions, IModelSelectCountParams, IModelIncrementParams, IModelSelectAggrParams, TypeModelAggrRelationResult, IModelSelectGroupParams, TypeModelGroupRelationResult } from 'vona-module-a-orm';
143
143
  import { SymbolKeyEntity, SymbolKeyEntityMeta, SymbolKeyModelOptions } from 'vona-module-a-orm';
144
144
  declare module 'vona-module-commerce-catalog' {
145
- export interface IModelOptionsProduct {
145
+ export interface IModelOptionsCategory {
146
+ relations: {
147
+ parent: IModelRelationBelongsTo<'commerce-catalog:category', 'commerce-catalog:category', false, 'id'|'name'>;
148
+ };
149
+ }
150
+ export interface IModelOptionsProduct {
146
151
  relations: {
147
152
  category: IModelRelationBelongsTo<'commerce-catalog:product', 'commerce-catalog:category', false, '*'>;
148
153
  skus: IModelRelationHasMany<'commerce-catalog:sku', 'productId', false, '*', undefined, undefined, undefined>;
@@ -159,7 +164,17 @@ export interface IModelOptionsSku {
159
164
  [SymbolKeyEntityMeta]: EntityCategoryMeta;
160
165
  [SymbolKeyModelOptions]: IModelOptionsCategory;
161
166
  get<T extends IModelGetOptions<EntityCategory,ModelCategory>>(where: TypeModelWhere<EntityCategory>, options?: T): Promise<TypeModelRelationResult<EntityCategory, ModelCategory, T> | undefined>;
167
+ /**
168
+ * Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
169
+ * Requires an active transaction. The lock is released when that transaction completes.
170
+ * Entity and query caches are bypassed.
171
+ */
162
172
  getForUpdate<T extends IModelGetOptions<EntityCategory,ModelCategory>>(where: TypeModelWhere<EntityCategory>, options?: T): Promise<TypeModelRelationResult<EntityCategory, ModelCategory, T> | undefined>;
173
+ /**
174
+ * Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
175
+ * Requires an active transaction. The lock is released when that transaction completes.
176
+ * Entity and query caches are bypassed.
177
+ */
163
178
  getByIdForUpdate<T extends IModelGetOptions<EntityCategory,ModelCategory>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntityCategory, ModelCategory, T> | undefined>;
164
179
  mget<T extends IModelGetOptions<EntityCategory,ModelCategory>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntityCategory, ModelCategory, T>[]>;
165
180
  selectAndCount<T extends IModelSelectParams<EntityCategory,ModelCategory,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntityCategory, ModelCategory, T>>;
@@ -190,7 +205,17 @@ export interface ModelProduct {
190
205
  [SymbolKeyEntityMeta]: EntityProductMeta;
191
206
  [SymbolKeyModelOptions]: IModelOptionsProduct;
192
207
  get<T extends IModelGetOptions<EntityProduct,ModelProduct>>(where: TypeModelWhere<EntityProduct>, options?: T): Promise<TypeModelRelationResult<EntityProduct, ModelProduct, T> | undefined>;
208
+ /**
209
+ * Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
210
+ * Requires an active transaction. The lock is released when that transaction completes.
211
+ * Entity and query caches are bypassed.
212
+ */
193
213
  getForUpdate<T extends IModelGetOptions<EntityProduct,ModelProduct>>(where: TypeModelWhere<EntityProduct>, options?: T): Promise<TypeModelRelationResult<EntityProduct, ModelProduct, T> | undefined>;
214
+ /**
215
+ * Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
216
+ * Requires an active transaction. The lock is released when that transaction completes.
217
+ * Entity and query caches are bypassed.
218
+ */
194
219
  getByIdForUpdate<T extends IModelGetOptions<EntityProduct,ModelProduct>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntityProduct, ModelProduct, T> | undefined>;
195
220
  mget<T extends IModelGetOptions<EntityProduct,ModelProduct>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntityProduct, ModelProduct, T>[]>;
196
221
  selectAndCount<T extends IModelSelectParams<EntityProduct,ModelProduct,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntityProduct, ModelProduct, T>>;
@@ -217,7 +242,17 @@ export interface ModelSku {
217
242
  [SymbolKeyEntityMeta]: EntitySkuMeta;
218
243
  [SymbolKeyModelOptions]: IModelOptionsSku;
219
244
  get<T extends IModelGetOptions<EntitySku,ModelSku>>(where: TypeModelWhere<EntitySku>, options?: T): Promise<TypeModelRelationResult<EntitySku, ModelSku, T> | undefined>;
245
+ /**
246
+ * Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
247
+ * Requires an active transaction. The lock is released when that transaction completes.
248
+ * Entity and query caches are bypassed.
249
+ */
220
250
  getForUpdate<T extends IModelGetOptions<EntitySku,ModelSku>>(where: TypeModelWhere<EntitySku>, options?: T): Promise<TypeModelRelationResult<EntitySku, ModelSku, T> | undefined>;
251
+ /**
252
+ * Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
253
+ * Requires an active transaction. The lock is released when that transaction completes.
254
+ * Entity and query caches are bypassed.
255
+ */
221
256
  getByIdForUpdate<T extends IModelGetOptions<EntitySku,ModelSku>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntitySku, ModelSku, T> | undefined>;
222
257
  mget<T extends IModelGetOptions<EntitySku,ModelSku>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntitySku, ModelSku, T>[]>;
223
258
  selectAndCount<T extends IModelSelectParams<EntitySku,ModelSku,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntitySku, ModelSku, T>>;
@@ -9,6 +9,7 @@ export default {
9
9
  Product: 'Product',
10
10
  ProductTitle: 'Product title',
11
11
  Published: 'Published',
12
+ Unpublished: 'Unpublished',
12
13
  Sku: 'SKU',
13
14
  SkuAttributeName: 'SKU attribute name',
14
15
  SkuAttributeValue: 'SKU attribute value',
@@ -9,6 +9,7 @@ export default {
9
9
  Product: '商品',
10
10
  ProductTitle: '商品标题',
11
11
  Published: '已发布',
12
+ Unpublished: '未发布',
12
13
  Sku: 'SKU',
13
14
  SkuAttributeName: 'SKU 属性名',
14
15
  SkuAttributeValue: 'SKU 属性值',
@@ -46,7 +46,9 @@ export interface IDtoOptionsCategorySelectResItem extends IDecoratorDtoOptions {
46
46
  }),
47
47
  ],
48
48
  })
49
- export class DtoCategorySelectResItem extends $Dto.get(() => ModelCategory) {
49
+ export class DtoCategorySelectResItem extends $Dto.get(() => ModelCategory, {
50
+ include: { parent: true },
51
+ }) {
50
52
  @Api.field(
51
53
  v.title($locale('Operations')),
52
54
  ZovaRender.order(1, 'max'),
@@ -22,4 +22,6 @@ export interface IDtoOptionsCategoryView extends IDecoratorDtoOptions {}
22
22
  }),
23
23
  ],
24
24
  })
25
- export class DtoCategoryView extends $Dto.get(() => ModelCategory) {}
25
+ export class DtoCategoryView extends $Dto.get(() => ModelCategory, {
26
+ include: { parent: true },
27
+ }) {}
@@ -1,7 +1,7 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
  import type { IDecoratorEntityOptions } from 'vona-module-a-orm';
3
3
 
4
- import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
4
+ import { $makeMetadata, $resourceName, Api, v } from 'vona-module-a-openapiutils';
5
5
  import { Entity, EntityBase } from 'vona-module-a-orm';
6
6
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
7
7
 
@@ -9,6 +9,11 @@ import { $locale } from '../.metadata/locales.ts';
9
9
 
10
10
  export interface IEntityOptionsCategory extends IDecoratorEntityOptions {}
11
11
 
12
+ export const categoryPublicationItems = [
13
+ { value: false, title: $locale('Unpublished') },
14
+ { value: true, title: $locale('Published') },
15
+ ];
16
+
12
17
  @Entity<IEntityOptionsCategory>('commerceCatalogCategory', {
13
18
  openapi: { title: $locale('Category') },
14
19
  fields: {
@@ -41,11 +46,25 @@ export class EntityCategory extends EntityBase {
41
46
  v.title($locale('ParentCategory')),
42
47
  v.optional(),
43
48
  ZovaRender.order(2),
49
+ ZovaRender.field('basic-resource:formFieldResourcePicker', {
50
+ resource: $resourceName('commerce-catalog:category'),
51
+ relationName: 'parent',
52
+ }),
53
+ ZovaRender.cell('basic-resource:resourcePicker', {
54
+ resource: $resourceName('commerce-catalog:category'),
55
+ relationName: 'parent',
56
+ }),
44
57
  v.tableIdentity(),
45
58
  )
46
59
  parentId?: TableIdentity;
47
60
 
48
- @Api.field(v.title($locale('Published')), v.default(false), ZovaRender.order(3))
61
+ @Api.field(
62
+ v.title($locale('Published')),
63
+ v.default(false),
64
+ ZovaRender.order(3),
65
+ ZovaRender.field('basic-select:formFieldSelect', { items: categoryPublicationItems }),
66
+ ZovaRender.cell('basic-select:select', { items: categoryPublicationItems }),
67
+ )
49
68
  published: boolean;
50
69
 
51
70
  @Api.field(v.title($locale('Description')), v.optional(), ZovaRender.order(4))
@@ -1,6 +1,6 @@
1
1
  import type { IDecoratorModelOptions } from 'vona-module-a-orm';
2
2
 
3
- import { BeanModelBase, Model } from 'vona-module-a-orm';
3
+ import { $relation, BeanModelBase, Model } from 'vona-module-a-orm';
4
4
 
5
5
  import { EntityCategory } from '../entity/category.tsx';
6
6
 
@@ -8,6 +8,16 @@ export interface IModelOptionsCategory extends IDecoratorModelOptions<EntityCate
8
8
 
9
9
  @Model<IModelOptionsCategory>({
10
10
  entity: EntityCategory,
11
+ relations: {
12
+ parent: $relation.belongsTo(
13
+ 'commerce-catalog:category',
14
+ 'commerce-catalog:category',
15
+ 'parentId',
16
+ {
17
+ columns: ['id', 'name'],
18
+ },
19
+ ),
20
+ },
11
21
  cache: {
12
22
  modelsClear: 'commerce-catalog:product',
13
23
  },
@@ -19,11 +19,16 @@ export class ServiceCategory extends BeanBase {
19
19
  }
20
20
 
21
21
  async select(params?: IQueryParams<ModelCategory>): Promise<DtoCategorySelectRes> {
22
- return await this.scope.model.category.selectAndCount(params);
22
+ return await this.scope.model.category.selectAndCount({
23
+ ...params,
24
+ include: { parent: true },
25
+ });
23
26
  }
24
27
 
25
28
  async view(id: TableIdentity): Promise<DtoCategoryView | undefined> {
26
- return await this.scope.model.category.getById(id);
29
+ return await this.scope.model.category.getById(id, {
30
+ include: { parent: true },
31
+ });
27
32
  }
28
33
 
29
34
  async update(id: TableIdentity, category: DtoCategoryUpdate) {