9 Commits

11 changed files with 46 additions and 134 deletions

View File

@@ -1,8 +1,8 @@
{
"moduleCode": "demo",
"candidateVersion": "1.0.6-rc.7",
"buildRevision": 7,
"buildChecksum": "80e53dc17a552419f0298c64eb92969197677ff45321c6965488479e3dd7b46c",
"candidateVersion": "1.0.0-rc.1",
"buildRevision": 1,
"buildChecksum": "07cb8511791fc96f65d6353e47a1744e448158f2f766a4110d539806801c596c",
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"branchName": "release/1.0.6-rc.7"
"branchName": "release/1.0.0-rc.1"
}

5
bootstrap.js vendored
View File

@@ -11,11 +11,10 @@ function loadGeneratedSaasModule() {
return require('./dist/saas/generated').GeneratedSaasModule;
}
async function start(options = {}) {
const session = await prepareSaasServiceHost({
async function start() {
await prepareSaasServiceHost({
generated: loadGeneratedSaasModule(),
});
if (options.verifySession) await options.verifySession(session);
requireExactReleaseEnvironment();
return Bootstrap.configure({
baseDir: path.join(__dirname, 'dist'),

View File

@@ -10,24 +10,22 @@
"build": "rimraf dist && mwtsc --cleanOutDir",
"publish": "cool saas-model finalize",
"start": "node bootstrap.js",
"start:stable": "node start-stable.js",
"test:stable": "node --test test/start-stable.test.cjs",
"test:business": "node --test test/goods-business-input.test.cjs",
"start:local": "cross-env NODE_ENV=local SAAS_RUNTIME_BOOTSTRAP_LOCAL_FALLBACK_ENABLED=true node bootstrap.js",
"verify:packages": "node scripts/verify-local-packages.cjs",
"start:frozen": "node start-frozen.js"
"start:frozen": "node start-frozen.js",
"start:stable": "node start-stable.js",
"test:stable": "node --test test/start-stable.test.cjs"
},
"coolSaas": {
"lifecycle": {
"dev": "pnpm run build && pnpm run start:local",
"test": "pnpm run typecheck && pnpm run test:stable && pnpm run test:business",
"build": "pnpm run build",
"artifactDir": "dist"
"test": "pnpm run typecheck",
"build": "pnpm run build"
}
},
"dependencies": {
"@cool-midway/module-runtime": "9.1.0",
"@cool-midway/rpc": "9.1.1",
"@cool-midway/module-runtime": "9.1.1",
"@cool-midway/rpc": "9.1.2",
"@midwayjs/bootstrap": "^3.20.3",
"@midwayjs/core": "^3.20.3",
"@midwayjs/koa": "^3.20.3",

24
pnpm-lock.yaml generated
View File

@@ -9,11 +9,11 @@ importers:
.:
dependencies:
'@cool-midway/module-runtime':
specifier: 9.1.0
version: 9.1.0(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)
'@cool-midway/rpc':
specifier: 9.1.1
version: 9.1.1(@cool-midway/module-runtime@9.1.0(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24))(@midwayjs/validate@3.20.24)(debug@4.4.3)
version: 9.1.1(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)
'@cool-midway/rpc':
specifier: 9.1.2
version: 9.1.2(@cool-midway/module-runtime@9.1.1(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24))(@midwayjs/validate@3.20.24)(debug@4.4.3)
'@midwayjs/bootstrap':
specifier: ^3.20.3
version: 3.20.24
@@ -72,16 +72,16 @@ packages:
resolution: {integrity: sha512-Kf4x3IrWnBM5o2k3hdHhDt7ujBG7MLn5JXV4o8glKEh4pqdzgoe/HgTcEvtdpIRvy80MsnIt5ZlH1Eq54qV4uQ==}
hasBin: true
'@cool-midway/module-runtime@9.1.0':
resolution: {integrity: sha512-8pWWQpzaR28gOLuZ8aQV+RVFmuFqtIfz8VHgu4uMMDnp4Jo3qPtKonQB6AV8BUNex2CBCkyFhmrZ8D+3YVNNeA==}
'@cool-midway/module-runtime@9.1.1':
resolution: {integrity: sha512-OHJzqJX6ktnelL1J6k/7forr6TujDuLHBQ/Z21ueGiEdtvfQ3cHKjhYFEp6WwgeISECxGOxGdGyASPP3cV6jXQ==}
peerDependencies:
'@midwayjs/core': '>=3.20.0'
typeorm: '>=0.3.20'
'@cool-midway/rpc@9.1.1':
resolution: {integrity: sha512-+kGR7dLVgbjaAowf6Rfrm4niiQCoCrChdi6Dxf4+qGsLMWXffmwRg7n28p3JPpRLvjfGat2Jh3JGZ5RVC82zpg==}
'@cool-midway/rpc@9.1.2':
resolution: {integrity: sha512-WVR+zsMtojllK9LsI1Kx3VUMgEzvKLuxWVkOudyxzjLwWF4VU2/hnRN6tPJA1mBoSSJCxaSrbF0tZwbeWxcgzQ==}
peerDependencies:
'@cool-midway/module-runtime': '>=9.1.0 <10'
'@cool-midway/module-runtime': '>=9.1.1 <10'
'@midwayjs/validate': '>=3.20.0'
'@cool-midway/typeorm@0.3.20':
@@ -2493,14 +2493,14 @@ snapshots:
- supports-color
- utf-8-validate
'@cool-midway/module-runtime@9.1.0(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)':
'@cool-midway/module-runtime@9.1.1(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)':
dependencies:
'@midwayjs/core': 3.20.24
typeorm: '@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7))'
'@cool-midway/rpc@9.1.1(@cool-midway/module-runtime@9.1.0(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24))(@midwayjs/validate@3.20.24)(debug@4.4.3)':
'@cool-midway/rpc@9.1.2(@cool-midway/module-runtime@9.1.1(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24))(@midwayjs/validate@3.20.24)(debug@4.4.3)':
dependencies:
'@cool-midway/module-runtime': 9.1.0(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)
'@cool-midway/module-runtime': 9.1.1(@cool-midway/typeorm@0.3.20(ioredis@5.11.1)(mysql2@3.24.3(@types/node@22.10.7)))(@midwayjs/core@3.20.24)
'@midwayjs/validate': 3.20.24
ioredis: 5.11.1
moleculer: 0.14.36(debug@4.4.3)(ioredis@5.11.1)

View File

@@ -111,31 +111,31 @@
},
{
"name": "@cool-midway/module-runtime",
"version": "9.1.0",
"sha256": "88caa11bb651e146b877809c5dbbe43a8161844b8f8f1f04f38437dcddc4ef0b",
"integrity": "sha512-8pWWQpzaR28gOLuZ8aQV+RVFmuFqtIfz8VHgu4uMMDnp4Jo3qPtKonQB6AV8BUNex2CBCkyFhmrZ8D+3YVNNeA==",
"version": "9.1.1",
"sha256": "b4ef3ceb929ec2575f1858be5f85b5cc470e4e942b6a00fb25886874993a8144",
"integrity": "sha512-OHJzqJX6ktnelL1J6k/7forr6TujDuLHBQ/Z21ueGiEdtvfQ3cHKjhYFEp6WwgeISECxGOxGdGyASPP3cV6jXQ==",
"files": {
"dist/action.js": "86b35ec3d9d020f253cfa42f8030444da39a9742f6480c94010cc342056e0e4a",
"dist/adapter.js": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
"dist/business-reject.js": "1bea195b38fbcce97512400be8fe43f01596a03aa5ea5d6546346064c2cd1b03",
"dist/configuration.js": "fed0a7dc7e6aa6e233f8d66109589b15ccd6b0f3fdd46dc6d55bbd8c38b45311",
"dist/connection.js": "dc44aef513160623b1282b174e5bd17bc5ddeefcdaac1d328b23e061d7516f67",
"dist/consumer.js": "18189e798ccaab3a0809f2d49430ec48361f0f705645b91dd0d889210cbf38e3",
"dist/consumer.js": "2f3b54f49775eacdf977642cb0a06505fffaaff6300527fd283751ad9c7c27d0",
"dist/context.js": "eb2b3c97119f94c7f52498b591b7e63fe51b1ef8194b3b9cf6dbea6d0af555e7",
"dist/crud-host.js": "19ab92b9744f8cb671c4dcbfce288160c60e008f14a0b018f38a1c0de55aa4b8",
"dist/crud.js": "235a6ce66b54373c80bbba8ea444162ef44e8fa34863cb30be59b62b8a008043",
"dist/data.js": "e54a8d582fadefd9dc75eec049d3eee1c80fb92ed656d46d384fc48d8ae4d7d0",
"dist/error.js": "f2dced066aef06c895d00285ecf69d52203bfce1635539a4b8c88d9de2a733d3",
"dist/event-writer-migration.js": "deee4d33d7c3049f2ab9f6a9605cfea17aaf9536db7d3d1305f4903d48ff5e4f",
"dist/event-writer.js": "0c4a2f270ba868b772d83d002255cf60bf3c3f69425881121c3639184e5e17ac",
"dist/event-writer.js": "e45f62209d263b2be11a25680352c4e8dd7690b655f2f9ee007d9f1f6b8646b0",
"dist/decorator/event.js": "3801cf45836398239734611d4c58ea0261d32e870544a770e7ad667704aad143",
"dist/executor.js": "e65ec1d28ee63bfc75b56a58485640a5c2f2e4af6f60122974bd73d047ceda0b",
"dist/executor.js": "4dc904785a8bd385e98f239e1517cc33fb3e4c96807fffd1393b22ec93ff025a",
"dist/generated-module.js": "84739644067c446863ad34166602d0dcec9212661c61cbeea5bfe85a113b4c0a",
"dist/index.js": "c0de3d4fae6a2f28f5ee89519c1daf7c36dc7bcc13aeccc0372999fec4b703c9",
"dist/injected-action.js": "3fdee7be7fee4e07c41884cbc43cb23bbd1dd4ef0ffe49fee37c53f2a8d8ca97",
"dist/injected-crud.js": "5f4593c1b12a50765f4112381a0db9f732202b908f84b6f13225da7a567eba0c",
"dist/internal.js": "2d5ee68295abbaaf9a74aa7fb805cb1be70702354e1c058a2eb02fa11d1561e1",
"dist/inventory.js": "c97c12ecb113302e4d21deef52a6d6b6b477ae2e192853d664211e19f539cd74",
"dist/inventory.js": "7c6ce797200ad627dd9374e1ad70638ff11f7483c5fd98e9adef6992b1a1aee9",
"dist/json-schema.js": "ef1a378710cfc285679fdb8130750169bc09e19611a2e577ef081b44e5f2eeb0",
"dist/managed-action.js": "fb42b5f0b1992e2b2b3f5176081a2de06cac020ca3ed744f46d93f094cfe96dd",
"dist/model.js": "9e548ad45b0cac4642ae9437cbcd52a6deb5dd316de5242ce601c2765f68554a",
@@ -157,7 +157,7 @@
"dist/workflow-reply-authority.js": "90ad7bbd35bcc4a0a788d33721cdbc3ec54f5c28ebce288317a40152de50bb10",
"dist/workflow.js": "79e25c6e0dc24c6b2277c18b02bedf4fda60754947ae1917b0b9cb2eecb53af3",
"dist/write-fence.js": "95c6af20da75ca92a0149023ebbc515525ddbcd50da3e284209b77536bc2a5f2",
"package.json": "f3638ab357be8eee6b01228a7e8f24dee63fe47f31382f45000abcec18df7fd2",
"package.json": "62519527981f22831f9ade24a8c38f8541fb64ebcfdc0905386f9b2dc5fc802c",
"dist/action.d.ts": "8d0c6d6055c1acc4a6a6e70fdb83a70a81693965b20adf2afa0290245cd649cd",
"adapter.d.ts": "ffa850716087cae3e3ef5f5bb54835143d16bcb3ad42b044c0f16aba08209074",
"dist/adapter.d.ts": "f70f53cb90da78d4423f46debfea6d922068ea4f70fb063b53135d6538af1eb2",
@@ -207,13 +207,13 @@
"fileCount": 90,
"credentialScanPassed": true,
"registry": "https://npm.blendercg.art/",
"tarball": "https://npm.blendercg.art/@cool-midway/module-runtime/-/module-runtime-9.1.0.tgz"
"tarball": "https://npm.blendercg.art/@cool-midway/module-runtime/-/module-runtime-9.1.1.tgz"
},
{
"name": "@cool-midway/rpc",
"version": "9.1.1",
"sha256": "0c1e20947d373f8ec8067cf28e010352abec8438b3e5f048c1dd78470a36b033",
"integrity": "sha512-+kGR7dLVgbjaAowf6Rfrm4niiQCoCrChdi6Dxf4+qGsLMWXffmwRg7n28p3JPpRLvjfGat2Jh3JGZ5RVC82zpg==",
"version": "9.1.2",
"sha256": "624b6bfa6f6ae05bfee779acf22913492a1a3bd7e6ca36c1670da08e6de232d6",
"integrity": "sha512-WVR+zsMtojllK9LsI1Kx3VUMgEzvKLuxWVkOudyxzjLwWF4VU2/hnRN6tPJA1mBoSSJCxaSrbF0tZwbeWxcgzQ==",
"files": {
"dist/action-registry.js": "04f77c61445d47f66eaad984ee8e059a38bb0e610d2f584f5754d5a0ead105c5",
"dist/saas-host/bootstrap.js": "a53eb6f40c7a7c2427fbcd0cde78e8985f64dda15fcff1843e8957f5e7086271",
@@ -246,7 +246,7 @@
"dist/test.js": "5386504311845b9dc3fe230d3c5a3b5ab3c6d36154add15e91d32d8442c25157",
"dist/types.js": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
"dist/workflow-recovery.js": "d423e33df1cca07250036ec6b61aaa7fcddea1e1440450487da13f01b16b26f0",
"package.json": "0d1cb019ab916e80ed3e3831b6abebd0ac873e0ee6f8672300924412910fd92b",
"package.json": "42ed936eafa84ce842c19bcb62970688fa9dee3030c071fc7eff5d33424e646f",
"dist/action-registry.d.ts": "40231dd8299c0bfae1b006105d49945eb2f20a40334856db32e4b7cc2851398b",
"dist/saas-host/bootstrap.d.ts": "f639edef99770f091ac90e737a0ab2c857830241a8996171492768d108ad43f0",
"dist/caller-context.d.ts": "3f58cb5d653e2d5179996c3302dddc5c4bfdc3ff2ac501ca8d6dcedb4cb606d5",
@@ -283,7 +283,7 @@
"fileCount": 66,
"credentialScanPassed": true,
"registry": "https://npm.blendercg.art/",
"tarball": "https://npm.blendercg.art/@cool-midway/rpc/-/rpc-9.1.1.tgz"
"tarball": "https://npm.blendercg.art/@cool-midway/rpc/-/rpc-9.1.2.tgz"
}
]
}

View File

@@ -18,8 +18,8 @@ export class Goods extends SaasTableModel<GoodsRow> {
static readonly definition = defineSaasTableModel({
"kind": "table",
"moduleCode": "demo",
"revision": 7,
"schemaVersion": 7,
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"tableCode": "goods",
"fields": [

View File

@@ -4,8 +4,8 @@ import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
export const GeneratedSaasModule = defineGeneratedSaasModule({
"formatVersion": 2,
"moduleCode": "demo",
"revision": 7,
"schemaVersion": 7,
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"

View File

@@ -1,7 +1,7 @@
{
"manifestVersion": 1,
"moduleCode": "demo",
"revision": 7,
"revision": 1,
"schemaChecksum": null,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"actionManifestChecksum": null,

View File

@@ -1,9 +1,9 @@
{
"formatVersion": 2,
"moduleCode": "demo",
"revision": 7,
"revision": 1,
"releaseVersion": null,
"schemaVersion": 7,
"schemaVersion": 1,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"schemaChecksum": null,
"actionManifestChecksum": null,

View File

@@ -44,21 +44,6 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
throw new SaasBusinessReject('STOCK_COMMAND_REQUIRED','现有商品库存只能通过受管库存命令变更');
}
}
if (change.operation === 'add' || change.operation === 'update') {
const input = change.input;
if (Object.prototype.hasOwnProperty.call(input, 'title') && !input.title.trim()) {
throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品名称不能为空');
}
if (Object.prototype.hasOwnProperty.call(input, 'price') && !/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(input.price)) {
throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品价格必须是非负金额');
}
if (Object.prototype.hasOwnProperty.call(input, 'stock') && (!Number.isSafeInteger(input.stock) || input.stock < 0 || input.stock > 2147483647)) {
throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品库存必须是有效的非负整数');
}
if (Object.prototype.hasOwnProperty.call(input, 'status') && input.status !== 0 && input.status !== 1) {
throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品状态仅允许下架或上架');
}
}
}
@SaasAction(Actions.GoodsBatchInfo)

View File

@@ -1,70 +0,0 @@
'use strict';
require('reflect-metadata');
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
const { SaasBusinessReject } = require('@cool-midway/module-runtime');
const sourceRoot = path.resolve(__dirname, '../src');
const previous = require.extensions['.ts'];
let GoodsService;
try {
require.extensions['.ts'] = (module, filename) => {
assert.ok(filename.startsWith(sourceRoot + path.sep));
const compiled = ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
fileName: filename,
compilerOptions: { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS,
experimentalDecorators: true, emitDecoratorMetadata: true, esModuleInterop: true },
});
module._compile(compiled.outputText, filename);
};
({ GoodsService } = require('../src/service/goods.ts'));
} finally {
if (previous) require.extensions['.ts'] = previous;
else delete require.extensions['.ts'];
}
function fixture(context = {}) {
const service = new GoodsService();
const calls = [];
service.saasActionContext = context;
service.inventory = { assertNoActiveReservations: async id => { calls.push(id); } };
return { service, calls };
}
const rejects = code => error => error instanceof SaasBusinessReject && error.code === code;
test('new catalog records accept an explicit initial stock and nonnegative decimal price', async () => {
const { service, calls } = fixture();
await service.modifyBefore({ operation: 'add', input: { title: '验收商品', price: '12.50', stock: 20, status: 1 } });
await service.modifyBefore({ operation: 'add', input: { title: '赠品', price: '0.00', stock: 0, status: 0 } });
assert.deepEqual(calls, []);
});
test('invalid catalog values are business rejections before persistence', async t => {
const inputs = [{ title: ' ' }, { price: '-0.01' }, { stock: -1 }, { stock: 1.5 }, { stock: 2147483648 }, { status: 2 }];
for (const input of inputs) await t.test(JSON.stringify(input), async () => {
const { service } = fixture();
await assert.rejects(service.modifyBefore({ operation: 'add', input: { title: '商品', price: '1.00', ...input } }), rejects('GOODS_INPUT_INVALID'));
});
});
test('catalog edits keep managed inventory protected even with valid stock values', async () => {
const { service } = fixture();
await service.modifyBefore({ operation: 'update', input: { id: '1', title: '更新名称', price: '0.00', status: 0 } });
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', stock: 2 } }), rejects('STOCK_COMMAND_REQUIRED'));
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', price: '-1.00' } }), rejects('GOODS_INPUT_INVALID'));
});
test('managed stock updates retain the same domain validation', async () => {
const { service } = fixture({ managedCrudCaller: { kind: 'business', actionName: 'goods.reserveStock' }, workflow: { coordinatorModule: 'order' } });
await service.modifyBefore({ operation: 'update', input: { id: '1', stock: 0 } });
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', stock: -1 } }), rejects('GOODS_INPUT_INVALID'));
});
test('deletion still requires the inventory reservation lock and rejects an active reservation', async () => {
const { service, calls } = fixture();
await service.modifyBefore({ operation: 'delete', input: { id: '1' } });
assert.deepEqual(calls, ['1']);
service.inventory.assertNoActiveReservations = async () => { throw new SaasBusinessReject('STOCK_RESERVATION_CONFLICT', '仍有库存预留'); };
await assert.rejects(service.modifyBefore({ operation: 'delete', input: { id: '1' } }), rejects('STOCK_RESERVATION_CONFLICT'));
});