Compare commits
9 Commits
release/1.
...
release/1.
| Author | SHA1 | Date | |
|---|---|---|---|
| 29730f8400 | |||
| 41c21dd500 | |||
| 120d2c4620 | |||
| 8ca879500d | |||
| 6c649647a1 | |||
| 8e4c9d6a6f | |||
| d3ab948cf6 | |||
| aab5cccdd1 | |||
| 7d8ce0b24c |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"moduleCode": "demo",
|
"moduleCode": "demo",
|
||||||
"candidateVersion": "1.0.7-rc.8",
|
"candidateVersion": "1.0.0-rc.1",
|
||||||
"buildRevision": 8,
|
"buildRevision": 1,
|
||||||
"buildChecksum": "c5cf8784ce5fab9d7b175474b57a9ce87283c2ba011c2c0564f29f1afac97a14",
|
"buildChecksum": "07cb8511791fc96f65d6353e47a1744e448158f2f766a4110d539806801c596c",
|
||||||
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
"branchName": "release/1.0.7-rc.8"
|
"branchName": "release/1.0.0-rc.1"
|
||||||
}
|
}
|
||||||
|
|||||||
5
bootstrap.js
vendored
5
bootstrap.js
vendored
@@ -11,11 +11,10 @@ function loadGeneratedSaasModule() {
|
|||||||
return require('./dist/saas/generated').GeneratedSaasModule;
|
return require('./dist/saas/generated').GeneratedSaasModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start(options = {}) {
|
async function start() {
|
||||||
const session = await prepareSaasServiceHost({
|
await prepareSaasServiceHost({
|
||||||
generated: loadGeneratedSaasModule(),
|
generated: loadGeneratedSaasModule(),
|
||||||
});
|
});
|
||||||
if (options.verifySession) await options.verifySession(session);
|
|
||||||
requireExactReleaseEnvironment();
|
requireExactReleaseEnvironment();
|
||||||
return Bootstrap.configure({
|
return Bootstrap.configure({
|
||||||
baseDir: path.join(__dirname, 'dist'),
|
baseDir: path.join(__dirname, 'dist'),
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -10,19 +10,17 @@
|
|||||||
"build": "rimraf dist && mwtsc --cleanOutDir",
|
"build": "rimraf dist && mwtsc --cleanOutDir",
|
||||||
"publish": "cool saas-model finalize",
|
"publish": "cool saas-model finalize",
|
||||||
"start": "node bootstrap.js",
|
"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",
|
"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",
|
"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": {
|
"coolSaas": {
|
||||||
"lifecycle": {
|
"lifecycle": {
|
||||||
"dev": "pnpm run build && pnpm run start:local",
|
"dev": "pnpm run build && pnpm run start:local",
|
||||||
"test": "pnpm run typecheck && pnpm run test:stable && pnpm run test:business",
|
"test": "pnpm run typecheck",
|
||||||
"build": "pnpm run build",
|
"build": "pnpm run build"
|
||||||
"artifactDir": "dist"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export class Goods extends SaasTableModel<GoodsRow> {
|
|||||||
static readonly definition = defineSaasTableModel({
|
static readonly definition = defineSaasTableModel({
|
||||||
"kind": "table",
|
"kind": "table",
|
||||||
"moduleCode": "demo",
|
"moduleCode": "demo",
|
||||||
"revision": 8,
|
"revision": 1,
|
||||||
"schemaVersion": 8,
|
"schemaVersion": 1,
|
||||||
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
"tableCode": "goods",
|
"tableCode": "goods",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
|
|||||||
export const GeneratedSaasModule = defineGeneratedSaasModule({
|
export const GeneratedSaasModule = defineGeneratedSaasModule({
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"moduleCode": "demo",
|
"moduleCode": "demo",
|
||||||
"revision": 8,
|
"revision": 1,
|
||||||
"schemaVersion": 8,
|
"schemaVersion": 1,
|
||||||
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
|
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
|
||||||
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"
|
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifestVersion": 1,
|
"manifestVersion": 1,
|
||||||
"moduleCode": "demo",
|
"moduleCode": "demo",
|
||||||
"revision": 8,
|
"revision": 1,
|
||||||
"schemaChecksum": null,
|
"schemaChecksum": null,
|
||||||
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
"actionManifestChecksum": null,
|
"actionManifestChecksum": null,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"moduleCode": "demo",
|
"moduleCode": "demo",
|
||||||
"revision": 8,
|
"revision": 1,
|
||||||
"releaseVersion": null,
|
"releaseVersion": null,
|
||||||
"schemaVersion": 8,
|
"schemaVersion": 1,
|
||||||
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
"schemaChecksum": null,
|
"schemaChecksum": null,
|
||||||
"actionManifestChecksum": null,
|
"actionManifestChecksum": null,
|
||||||
|
|||||||
@@ -44,21 +44,6 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
|
|||||||
throw new SaasBusinessReject('STOCK_COMMAND_REQUIRED','现有商品库存只能通过受管库存命令变更');
|
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)
|
@SaasAction(Actions.GoodsBatchInfo)
|
||||||
|
|||||||
@@ -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'));
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user