cabloy 5.1.194 → 5.1.196

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 (91) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/agent-governance.yml +1 -0
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +13 -24
  5. package/package.json +2 -1
  6. package/repo-agent-governance/managed-assets.json +138 -33
  7. package/repo-agent-governance/scripts/pack-check.mjs +17 -0
  8. package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -0
  9. package/repo-agent-governance/skills/cabloy-domain-planning/SKILL.md +5 -3
  10. package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +13 -8
  11. package/repo-agent-governance/skills/cabloy-spec-execution/evals/evals.json +108 -12
  12. package/repo-agent-governance/skills/cabloy-spec-execution/evals/files/scenarios.json +84 -0
  13. package/repo-agent-governance/skills/cabloy-spec-execution/evals/protocol.md +9 -0
  14. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +21 -5
  15. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +5 -3
  16. package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +78 -168
  17. package/repo-agent-governance/skills/cabloy-spec-generation/evals/evals.json +165 -17
  18. package/repo-agent-governance/skills/cabloy-spec-generation/evals/files/scenarios.json +114 -0
  19. package/repo-agent-governance/skills/cabloy-spec-generation/evals/protocol.md +44 -0
  20. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +145 -0
  21. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-aware-discovery.md +55 -57
  22. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +6 -4
  23. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +12 -8
  24. package/repo-agent-governance/tests/governance.test.mjs +10 -1
  25. package/repo-agent-governance/tests/spec-audit.test.mjs +255 -0
  26. package/repo-agent-governance/tools/spec-audit/audit.mjs +427 -0
  27. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +97 -408
  28. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +462 -1
  29. package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +561 -0
  30. package/repo-docs/.vitepress/config.mjs +54 -29
  31. package/repo-docs/ai/playbook-spec-execution.md +10 -4
  32. package/repo-docs/ai/playbook-spec-generation.md +40 -3
  33. package/repo-docs/ai/skills.md +3 -3
  34. package/repo-docs/backend/controller-aop-guide.md +10 -0
  35. package/repo-docs/backend/field-indexes.md +25 -0
  36. package/repo-docs/blogs/cabloy-fullstack-resource-addressing/index.md +1 -1
  37. package/repo-docs/fullstack/contract-loop-playbook.md +1 -1
  38. package/repo-docs/fullstack/development-history.md +24 -0
  39. package/repo-docs/fullstack/introduction.md +25 -156
  40. package/repo-docs/fullstack/quickstart.md +1 -1
  41. package/repo-docs/fullstack/ssr-entry-modes.md +52 -0
  42. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +3 -1
  43. package/repo-docs/fullstack/tutorial-5-backend-contract-sharing.md +13 -7
  44. package/repo-docs/fullstack/tutorial-6-one-contract-four-uses.md +11 -15
  45. package/repo-docs/fullstack/tutorials-overview.md +2 -2
  46. package/repo-docs/index.md +23 -45
  47. package/repo-docs/public/cabloy.png +0 -0
  48. package/repo-docs/public/cabloy.svg +3 -0
  49. package/repo-docs/public/favicon.svg +3 -0
  50. package/repo-docs/reference/repo-scripts.md +6 -1
  51. package/repo-e2e/specs/home-user-account.spec.ts +311 -207
  52. package/scripts/bootstrapAgentGovernance.mjs +1 -0
  53. package/scripts/upgrade.ts +1 -0
  54. package/vona/packages-cli/cli/package.json +1 -1
  55. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/2-meta.index.ts +4 -10
  56. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/2-meta.index.ts +4 -10
  57. package/vona/packages-cli/cli-set-api/package.json +8 -2
  58. package/vona/packages-cli/cli-set-api/src/index.ts +1 -0
  59. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +9 -12
  60. package/vona/packages-cli/cli-set-api/src/lib/mergeMetaIndex.ts +121 -0
  61. package/vona/packages-cli/cli-set-api/test/indexSnippets.test.ts +41 -0
  62. package/vona/packages-cli/cli-set-api/test/mergeMetaIndex.test.ts +78 -0
  63. package/vona/packages-vona/vona/package.json +1 -1
  64. package/vona/pnpm-lock.yaml +6 -6
  65. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +25 -4
  66. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuUniqueness.test.ts +161 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +21 -20
  68. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +165 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +16 -13
  70. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/promotionIndexes.test.ts +82 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +23 -21
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/test/tradeIndexes.test.ts +86 -0
  73. package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +379 -376
  74. package/vona/src/suite/a-home/modules/home-user/src/controller/passportTest.ts +60 -0
  75. package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +164 -1
  76. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  77. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  78. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  79. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +35 -26
  80. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  81. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  82. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  83. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  84. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  85. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionFiber_.ts +6 -2
  86. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transaction_.ts +4 -1
  87. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/package.json +1 -1
  88. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/src/lib/columns.ts +3 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  91. /package/repo-docs/{.vitepress/public → public}/CNAME +0 -0
@@ -6,26 +6,27 @@ import { $tableColumns } from 'vona-module-a-ormutils';
6
6
 
7
7
  @Meta<IMetaOptionsIndex>({
8
8
  indexes: {
9
- ...$tableColumns('commercePaymentAttempt', 'orderId'),
10
- ...$tableColumns('commercePaymentAttempt', 'userId'),
11
- ...$tableColumns('commercePaymentAttempt', 'state'),
12
- ...$tableColumns('commercePaymentAttempt', 'correlationId'),
13
- ...$tableColumns('commercePaymentAudit', 'paymentAttemptId'),
14
- ...$tableColumns('commercePaymentAudit', 'providerEventId'),
15
- ...$tableColumns('commercePaymentAudit', 'orderId'),
16
- ...$tableColumns('commercePaymentAudit', 'idempotencyKey'),
17
- ...$tableColumns('commercePaymentRefundRequest', 'orderId'),
18
- ...$tableColumns('commercePaymentRefundRequest', 'userId'),
19
- ...$tableColumns('commercePaymentRefundRequest', 'state'),
20
- ...$tableColumns('commercePaymentRefundRequest', 'correlationId'),
21
- ...$tableColumns('commercePaymentRefundAttempt', 'refundRequestId'),
22
- ...$tableColumns('commercePaymentRefundAttempt', 'orderId'),
23
- ...$tableColumns('commercePaymentRefundAttempt', 'state'),
24
- ...$tableColumns('commercePaymentRefundAudit', 'refundRequestId'),
25
- ...$tableColumns('commercePaymentRefundAudit', 'refundAttemptId'),
26
- ...$tableColumns('commercePaymentRefundAudit', 'orderId'),
27
- ...$tableColumns('commercePaymentRefundAudit', 'idempotencyKey'),
28
- ...$tableColumns('commercePaymentRefundAudit', 'correlationId'),
9
+ ...$tableColumns('commercePaymentAttempt', ['orderId', 'userId', 'state', 'correlationId']),
10
+ ...$tableColumns('commercePaymentAudit', [
11
+ 'paymentAttemptId',
12
+ 'providerEventId',
13
+ 'orderId',
14
+ 'idempotencyKey',
15
+ ]),
16
+ ...$tableColumns('commercePaymentRefundRequest', [
17
+ 'orderId',
18
+ 'userId',
19
+ 'state',
20
+ 'correlationId',
21
+ ]),
22
+ ...$tableColumns('commercePaymentRefundAttempt', ['refundRequestId', 'orderId', 'state']),
23
+ ...$tableColumns('commercePaymentRefundAudit', [
24
+ 'refundRequestId',
25
+ 'refundAttemptId',
26
+ 'orderId',
27
+ 'idempotencyKey',
28
+ 'correlationId',
29
+ ]),
29
30
  },
30
31
  })
31
32
  export class MetaIndex extends BeanBase {}
@@ -0,0 +1,165 @@
1
+ import type { IMetaOptionsIndex } from 'vona-module-a-index';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { mkdtemp, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { describe, it } from 'node:test';
9
+ import { appResource } from 'vona';
10
+ import { app } from 'vona-mock';
11
+ import { ServiceDatabaseAsyncLocalStorage, ServiceDatabaseClient } from 'vona-module-a-orm';
12
+
13
+ const expectedIndexes = {
14
+ commercePaymentAttempt: ['orderId', 'userId', 'state', 'correlationId'],
15
+ commercePaymentAudit: ['paymentAttemptId', 'providerEventId', 'orderId', 'idempotencyKey'],
16
+ commercePaymentRefundRequest: ['orderId', 'userId', 'state', 'correlationId'],
17
+ commercePaymentRefundAttempt: ['refundRequestId', 'orderId', 'state'],
18
+ commercePaymentRefundAudit: [
19
+ 'refundRequestId',
20
+ 'refundAttemptId',
21
+ 'orderId',
22
+ 'idempotencyKey',
23
+ 'correlationId',
24
+ ],
25
+ };
26
+
27
+ describe('paymentIndexes.test.ts', { concurrency: false }, () => {
28
+ it('registers all payment indexes without overwriting table keys', () => {
29
+ const options = appResource.getBean('commerce-payment.meta.index')
30
+ ?.options as IMetaOptionsIndex;
31
+ assert.ok(options);
32
+ assert.deepEqual(options.indexes, expectedIndexes);
33
+ });
34
+
35
+ it('creates non-unique single-column indexes and searches payment audit without a scan', async t => {
36
+ await app.bean.executor.mockCtx(async () => {
37
+ if (app.bean.database.current.dialectName !== 'better-sqlite3') {
38
+ t.skip('SQLite index introspection and query plan');
39
+ return;
40
+ }
41
+ const db = app.bean.database.current.connection;
42
+ for (const [table, fields] of Object.entries(expectedIndexes)) {
43
+ const indexList = (await db.raw(`PRAGMA index_list("${table}")`)) as Array<{
44
+ name: string;
45
+ unique: number;
46
+ }>;
47
+ for (const field of fields) {
48
+ const name = `idx_${table}_${field}`;
49
+ const index = indexList.find(item => item.name === name);
50
+ assert.ok(index, `missing ${name}`);
51
+ assert.equal(index.unique, 0, `${name} must not be unique`);
52
+ const columns = (await db.raw(`PRAGMA index_xinfo("${name}")`)) as Array<{
53
+ name: string;
54
+ key: number;
55
+ }>;
56
+ assert.deepEqual(
57
+ columns.filter(column => column.key === 1).map(column => column.name),
58
+ [field],
59
+ );
60
+ }
61
+ }
62
+
63
+ for (const eventField of ['providerEventId', 'idempotencyKey']) {
64
+ const queryPlan = (await db.raw(
65
+ `EXPLAIN QUERY PLAN SELECT id FROM commercePaymentAudit WHERE iid = ? AND deleted = ? AND paymentAttemptId = ? AND "${eventField}" = ?`,
66
+ [1, 0, 1, 'event'],
67
+ )) as Array<{ detail: string }>;
68
+ assert.ok(
69
+ queryPlan.some(step =>
70
+ /SEARCH commercePaymentAudit USING INDEX idx_commercePaymentAudit_/.test(step.detail),
71
+ ),
72
+ `${eventField}: ${JSON.stringify(queryPlan)}`,
73
+ );
74
+ assert.ok(
75
+ queryPlan.every(step => !/SCAN commercePaymentAudit/.test(step.detail)),
76
+ `${eventField}: ${JSON.stringify(queryPlan)}`,
77
+ );
78
+ }
79
+ });
80
+ });
81
+
82
+ it('recovers missing indexes on an isolated copy of an existing database without changing its data', async t => {
83
+ await app.bean.executor.mockCtx(async () => {
84
+ if (app.bean.database.current.dialectName !== 'better-sqlite3') {
85
+ t.skip('SQLite-only independent database copy');
86
+ return;
87
+ }
88
+ const directory = await mkdtemp(path.join(tmpdir(), 'commerce-index-recovery-'));
89
+ const filename = path.join(directory, 'existing.db');
90
+ const source = app.bean.database.current;
91
+ let client: ServiceDatabaseClient | undefined;
92
+ try {
93
+ await source.connection.raw('VACUUM main INTO ?', [filename]);
94
+ const selector = `index-recovery-${randomUUID()}`;
95
+ client = app.bean._newBean(ServiceDatabaseClient, selector, {
96
+ client: 'better-sqlite3',
97
+ connection: { filename },
98
+ useNullAsDefault: true,
99
+ });
100
+ const isolated = client.db;
101
+ await app.bean._getBean(ServiceDatabaseAsyncLocalStorage).run(isolated, async () => {
102
+ const db = isolated.connection;
103
+ const table = 'commercePaymentAudit';
104
+ const missingIndex = `idx_${table}_providerEventId`;
105
+ const retainedIndex = `idx_${table}_idempotencyKey`;
106
+ const marker = `index-recovery-${randomUUID()}`;
107
+ await db(table).insert({
108
+ iid: 1,
109
+ paymentAttemptId: 1,
110
+ orderId: 1,
111
+ userId: 1,
112
+ provider: 'test',
113
+ providerEventId: marker,
114
+ outcome: 'failed',
115
+ fromAttemptState: 'created',
116
+ toOrderState: 'cancelled',
117
+ idempotencyKey: marker,
118
+ correlationId: marker,
119
+ reason: 'test',
120
+ processedAt: new Date(),
121
+ });
122
+ const versionsBefore = await db('aVersion')
123
+ .where({ module: 'commerce-payment' })
124
+ .select('version');
125
+ await db.schema.alterTable(table, builder =>
126
+ builder.dropIndex('providerEventId', missingIndex),
127
+ );
128
+ const indexesBefore = (await db.raw(`PRAGMA index_list("${table}")`)) as Array<{
129
+ name: string;
130
+ }>;
131
+ assert.equal(
132
+ indexesBefore.some(item => item.name === missingIndex),
133
+ false,
134
+ );
135
+ assert.ok(indexesBefore.some(item => item.name === retainedIndex));
136
+
137
+ const update = async () => {
138
+ await app.bean._getBean('a-index.meta.version').update({ version: -1 });
139
+ };
140
+ for (let pass = 0; pass < 2; pass++) {
141
+ await update();
142
+ const indexes = (await db.raw(`PRAGMA index_list("${table}")`)) as Array<{
143
+ name: string;
144
+ }>;
145
+ assert.equal(indexes.filter(item => item.name === missingIndex).length, 1);
146
+ assert.equal(indexes.filter(item => item.name === retainedIndex).length, 1);
147
+ const rows = await db(table).where({ providerEventId: marker, idempotencyKey: marker });
148
+ assert.equal(rows.length, 1);
149
+ assert.equal(rows[0].paymentAttemptId, 1);
150
+ assert.deepEqual(
151
+ await db('aVersion').where({ module: 'commerce-payment' }).select('version'),
152
+ versionsBefore,
153
+ );
154
+ }
155
+ });
156
+ } finally {
157
+ try {
158
+ if (client) await client.connection.destroy();
159
+ } finally {
160
+ await rm(directory, { recursive: true, force: true });
161
+ }
162
+ }
163
+ });
164
+ });
165
+ });
@@ -6,19 +6,22 @@ import { $tableColumns } from 'vona-module-a-ormutils';
6
6
 
7
7
  @Meta<IMetaOptionsIndex>({
8
8
  indexes: {
9
- ...$tableColumns('commercePromotionCouponTemplate', 'state'),
10
- ...$tableColumns('commercePromotionCouponTemplate', 'validUntil'),
11
- ...$tableColumns('commercePromotionCouponGrant', 'templateId'),
12
- ...$tableColumns('commercePromotionCouponGrant', 'userId'),
13
- ...$tableColumns('commercePromotionCouponGrant', 'state'),
14
- ...$tableColumns('commercePromotionCouponGrant', 'validUntilSnapshot'),
15
- ...$tableColumns('commercePromotionCouponGrant', 'reservationOrderId'),
16
- ...$tableColumns('commercePromotionCouponGrant', 'reservationCorrelationId'),
17
- ...$tableColumns('commercePromotionCouponGrant', 'redeemedOrderId'),
18
- ...$tableColumns('commercePromotionCouponAudit', 'couponGrantId'),
19
- ...$tableColumns('commercePromotionCouponAudit', 'templateId'),
20
- ...$tableColumns('commercePromotionCouponAudit', 'orderId'),
21
- ...$tableColumns('commercePromotionCouponAudit', 'correlationId'),
9
+ ...$tableColumns('commercePromotionCouponTemplate', ['state', 'validUntil']),
10
+ ...$tableColumns('commercePromotionCouponGrant', [
11
+ 'templateId',
12
+ 'userId',
13
+ 'state',
14
+ 'validUntilSnapshot',
15
+ 'reservationOrderId',
16
+ 'reservationCorrelationId',
17
+ 'redeemedOrderId',
18
+ ]),
19
+ ...$tableColumns('commercePromotionCouponAudit', [
20
+ 'couponGrantId',
21
+ 'templateId',
22
+ 'orderId',
23
+ 'correlationId',
24
+ ]),
22
25
  },
23
26
  })
24
27
  export class MetaIndex extends BeanBase {}
@@ -0,0 +1,82 @@
1
+ import type { IMetaOptionsIndex } from 'vona-module-a-index';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { describe, it } from 'node:test';
5
+ import { appResource } from 'vona';
6
+ import { app } from 'vona-mock';
7
+
8
+ const expectedIndexes = {
9
+ commercePromotionCouponTemplate: ['state', 'validUntil'],
10
+ commercePromotionCouponGrant: [
11
+ 'templateId',
12
+ 'userId',
13
+ 'state',
14
+ 'validUntilSnapshot',
15
+ 'reservationOrderId',
16
+ 'reservationCorrelationId',
17
+ 'redeemedOrderId',
18
+ ],
19
+ commercePromotionCouponAudit: ['couponGrantId', 'templateId', 'orderId', 'correlationId'],
20
+ };
21
+
22
+ describe('promotionIndexes.test.ts', { concurrency: false }, () => {
23
+ it('registers every coupon index without overwriting table keys', () => {
24
+ const options = appResource.getBean('commerce-promotion.meta.index')
25
+ ?.options as IMetaOptionsIndex;
26
+ assert.ok(options);
27
+ assert.deepEqual(options.indexes, expectedIndexes);
28
+ });
29
+
30
+ it('creates non-unique single-column coupon indexes', async () => {
31
+ await app.bean.executor.mockCtx(async () => {
32
+ const database = app.bean.database.current;
33
+ const db = database.connection;
34
+ for (const [table, fields] of Object.entries(expectedIndexes)) {
35
+ const indexes = await database.dialect.fetchIndexes(db.schema, table);
36
+ for (const field of fields) {
37
+ const name = `idx_${table}_${field}`;
38
+ assert.ok(
39
+ indexes.some(index => index.indexName === name),
40
+ `missing ${name}`,
41
+ );
42
+ if (database.dialectName === 'better-sqlite3') {
43
+ const details = (await db.raw(`PRAGMA index_list("${table}")`)) as Array<{
44
+ name: string;
45
+ unique: number;
46
+ }>;
47
+ assert.equal(details.find(index => index.name === name)?.unique, 0);
48
+ const columns = (await db.raw(`PRAGMA index_xinfo("${name}")`)) as Array<{
49
+ name: string;
50
+ key: number;
51
+ }>;
52
+ assert.deepEqual(
53
+ columns.filter(column => column.key === 1).map(column => column.name),
54
+ [field],
55
+ );
56
+ } else if (database.dialectName === 'mysql2') {
57
+ const [details] = (await db.raw(`SHOW INDEX FROM \`${table}\``)) as [
58
+ Array<{
59
+ Key_name: string;
60
+ Non_unique: number;
61
+ Seq_in_index: number;
62
+ Column_name: string;
63
+ }>,
64
+ ];
65
+ const columns = details.filter(index => index.Key_name === name);
66
+ assert.ok(columns.length, `missing ${name}`);
67
+ assert.ok(
68
+ columns.every(index => index.Non_unique === 1),
69
+ `${name} must not be unique`,
70
+ );
71
+ assert.deepEqual(
72
+ columns
73
+ .sort((a, b) => a.Seq_in_index - b.Seq_in_index)
74
+ .map(index => index.Column_name),
75
+ [field],
76
+ );
77
+ }
78
+ }
79
+ }
80
+ });
81
+ });
82
+ });
@@ -7,28 +7,30 @@ import { $tableColumns } from 'vona-module-a-ormutils';
7
7
  @Meta<IMetaOptionsIndex>({
8
8
  indexes: {
9
9
  ...$tableColumns('commerceTradeCart', 'userId'),
10
- ...$tableColumns('commerceTradeCartItem', 'cartId'),
11
- ...$tableColumns('commerceTradeCartItem', 'skuId'),
12
- ...$tableColumns('commerceTradeOrder', 'userId'),
13
- ...$tableColumns('commerceTradeOrder', 'correlationId'),
14
- ...$tableColumns('commerceTradeOrder', 'state'),
15
- ...$tableColumns('commerceTradeOrder', 'reservationExpiresAt'),
16
- ...$tableColumns('commerceTradeOrderLine', 'orderId'),
17
- ...$tableColumns('commerceTradeShipment', 'orderId'),
18
- ...$tableColumns('commerceTradeShipment', 'correlationId'),
19
- ...$tableColumns('commerceTradeOrderAudit', 'orderId'),
20
- ...$tableColumns('commerceTradeOrderAudit', 'correlationId'),
21
- ...$tableColumns('commerceTradeOrderLine', 'skuId'),
22
- ...$tableColumns('commerceTradeStockAudit', 'stockBalanceId'),
23
- ...$tableColumns('commerceTradeStockAudit', 'skuId'),
24
- ...$tableColumns('commerceTradeStockAudit', 'stockReservationId'),
25
- ...$tableColumns('commerceTradeStockAudit', 'correlationId'),
10
+ ...$tableColumns('commerceTradeCartItem', ['cartId', 'skuId']),
11
+ ...$tableColumns('commerceTradeOrder', [
12
+ 'userId',
13
+ 'correlationId',
14
+ 'state',
15
+ 'reservationExpiresAt',
16
+ ]),
17
+ ...$tableColumns('commerceTradeOrderLine', ['orderId', 'skuId']),
18
+ ...$tableColumns('commerceTradeShipment', ['orderId', 'correlationId']),
19
+ ...$tableColumns('commerceTradeOrderAudit', ['orderId', 'correlationId']),
20
+ ...$tableColumns('commerceTradeStockAudit', [
21
+ 'stockBalanceId',
22
+ 'skuId',
23
+ 'stockReservationId',
24
+ 'correlationId',
25
+ ]),
26
26
  ...$tableColumns('commerceTradeStockBalance', 'skuId'),
27
- ...$tableColumns('commerceTradeStockReservation', 'stockBalanceId'),
28
- ...$tableColumns('commerceTradeStockReservation', 'skuId'),
29
- ...$tableColumns('commerceTradeStockReservation', 'orderLineId'),
30
- ...$tableColumns('commerceTradeStockReservation', 'correlationId'),
31
- ...$tableColumns('commerceTradeStockReservation', 'state'),
27
+ ...$tableColumns('commerceTradeStockReservation', [
28
+ 'stockBalanceId',
29
+ 'skuId',
30
+ 'orderLineId',
31
+ 'correlationId',
32
+ 'state',
33
+ ]),
32
34
  },
33
35
  })
34
36
  export class MetaIndex extends BeanBase {}
@@ -0,0 +1,86 @@
1
+ import type { IMetaOptionsIndex } from 'vona-module-a-index';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { describe, it } from 'node:test';
5
+ import { appResource } from 'vona';
6
+ import { app } from 'vona-mock';
7
+
8
+ const expectedIndexes = {
9
+ commerceTradeCart: 'userId',
10
+ commerceTradeCartItem: ['cartId', 'skuId'],
11
+ commerceTradeOrder: ['userId', 'correlationId', 'state', 'reservationExpiresAt'],
12
+ commerceTradeOrderLine: ['orderId', 'skuId'],
13
+ commerceTradeShipment: ['orderId', 'correlationId'],
14
+ commerceTradeOrderAudit: ['orderId', 'correlationId'],
15
+ commerceTradeStockAudit: ['stockBalanceId', 'skuId', 'stockReservationId', 'correlationId'],
16
+ commerceTradeStockBalance: 'skuId',
17
+ commerceTradeStockReservation: [
18
+ 'stockBalanceId',
19
+ 'skuId',
20
+ 'orderLineId',
21
+ 'correlationId',
22
+ 'state',
23
+ ],
24
+ };
25
+
26
+ describe('tradeIndexes.test.ts', { concurrency: false }, () => {
27
+ it('registers every trade index without overwriting table keys', () => {
28
+ const options = appResource.getBean('commerce-trade.meta.index')?.options as IMetaOptionsIndex;
29
+ assert.ok(options);
30
+ assert.deepEqual(options.indexes, expectedIndexes);
31
+ });
32
+
33
+ it('creates non-unique single-column trade indexes', async () => {
34
+ await app.bean.executor.mockCtx(async () => {
35
+ const database = app.bean.database.current;
36
+ const db = database.connection;
37
+ for (const [table, value] of Object.entries(expectedIndexes)) {
38
+ const fields = Array.isArray(value) ? value : [value];
39
+ const indexes = await database.dialect.fetchIndexes(db.schema, table);
40
+ for (const field of fields) {
41
+ const name = `idx_${table}_${field}`;
42
+ assert.ok(
43
+ indexes.some(index => index.indexName === name),
44
+ `missing ${name}`,
45
+ );
46
+ if (database.dialectName === 'better-sqlite3') {
47
+ const details = (await db.raw(`PRAGMA index_list("${table}")`)) as Array<{
48
+ name: string;
49
+ unique: number;
50
+ }>;
51
+ assert.equal(details.find(index => index.name === name)?.unique, 0);
52
+ const columns = (await db.raw(`PRAGMA index_xinfo("${name}")`)) as Array<{
53
+ name: string;
54
+ key: number;
55
+ }>;
56
+ assert.deepEqual(
57
+ columns.filter(column => column.key === 1).map(column => column.name),
58
+ [field],
59
+ );
60
+ } else if (database.dialectName === 'mysql2') {
61
+ const [details] = (await db.raw(`SHOW INDEX FROM \`${table}\``)) as [
62
+ Array<{
63
+ Key_name: string;
64
+ Non_unique: number;
65
+ Seq_in_index: number;
66
+ Column_name: string;
67
+ }>,
68
+ ];
69
+ const columns = details.filter(index => index.Key_name === name);
70
+ assert.ok(columns.length, `missing ${name}`);
71
+ assert.ok(
72
+ columns.every(index => index.Non_unique === 1),
73
+ `${name} must not be unique`,
74
+ );
75
+ assert.deepEqual(
76
+ columns
77
+ .sort((a, b) => a.Seq_in_index - b.Seq_in_index)
78
+ .map(index => index.Column_name),
79
+ [field],
80
+ );
81
+ }
82
+ }
83
+ }
84
+ });
85
+ });
86
+ });