feat: implement demo candidate 1.0.0-rc.1
This commit is contained in:
8
.cool-saas-candidate.json
Normal file
8
.cool-saas-candidate.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"moduleCode": "demo",
|
||||||
|
"candidateVersion": "1.0.0-rc.1",
|
||||||
|
"buildRevision": 1,
|
||||||
|
"buildChecksum": "07cb8511791fc96f65d6353e47a1744e448158f2f766a4110d539806801c596c",
|
||||||
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
|
"branchName": "release/1.0.0-rc.1"
|
||||||
|
}
|
||||||
6
.cool-saas-service.json
Normal file
6
.cool-saas-service.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"kind": "cool-saas-microservice",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"moduleCode": "demo",
|
||||||
|
"generatedRoot": "src/saas/generated"
|
||||||
|
}
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
logs/
|
||||||
|
dist/
|
||||||
|
dist-*/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.tmp-*
|
||||||
5
.npmrc
Normal file
5
.npmrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@cool-midway:registry=https://npm.blendercg.art/
|
||||||
|
@g1sir8b:registry=https://npm.blendercg.art/
|
||||||
|
//npm.blendercg.art/:_authToken=${NODE_AUTH_TOKEN}
|
||||||
|
always-auth=true
|
||||||
|
confirmModulesPurge=false
|
||||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# COOL SaaS 模块微服务起始项目
|
||||||
|
|
||||||
|
这是一个可复制到 `cool-midway-biz/microservices/<service-name>` 的干净起始项目。
|
||||||
|
它只包含托管 SaaS 模块微服务运行所需的基础结构,不包含 Demo 商品业务、Pilot、
|
||||||
|
Examples、测试 Fixture 或任何既有模块的生成契约。
|
||||||
|
|
||||||
|
## 初始化
|
||||||
|
|
||||||
|
1. 将本目录的项目文件复制到 `cool-midway-biz/microservices/<service-name>`,不要复制模板
|
||||||
|
仓库自身的 `.git`;scaffold 工具也必须移除模板 origin,再由业务项目重新初始化。
|
||||||
|
2. 修改 `package.json` 中的包名,并按需修改
|
||||||
|
`src/config/app.default.ts` 中的本地默认端口 `7101`。
|
||||||
|
3. 在开发环境的模块管理页输入基础版本(例如 `1.0.0`)并锁定 Candidate;平台会分配
|
||||||
|
Build Revision、`1.0.0-rc.<revision>` 候选版本和同名 release 分支。
|
||||||
|
4. 安装依赖后,用平台给出的精确 Revision 生成并启动开发实例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm install
|
||||||
|
pnpm exec cool saas-model generate --module <module-code> --revision <build-revision>
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
首次生成会初始化 Git(如果需要)、要求配置 `origin`,并切换到平台指定的
|
||||||
|
`release/<candidate-version>` 分支。CLI 会在项目根目录写入可提交的
|
||||||
|
`.cool-saas-candidate.json`;其中只有模块、Revision、版本、分支与 checksum,不包含
|
||||||
|
控制面 token、Runtime ticket 或其他 Secret。
|
||||||
|
|
||||||
|
`src/config/app.default.ts` 会直接导入 `src/saas/generated`。因此全新模板在首次生成
|
||||||
|
完成前不能编译,这是为了避免用占位契约冒充平台锁定的 Build Revision。
|
||||||
|
|
||||||
|
`start:local` 会显式开启仅限 `local/test` 的 Runtime bootstrap fallback;本地控制面
|
||||||
|
启动命令也必须开启同一个 flag。两端未另设 token 时只使用仓库内公开的开发常量,
|
||||||
|
生产环境始终拒绝这条路径。
|
||||||
|
|
||||||
|
## CLI 生命周期
|
||||||
|
|
||||||
|
在微服务根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm exec cool saas-model generate `
|
||||||
|
--module <module-code> `
|
||||||
|
--revision <build-revision>
|
||||||
|
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
管理端的“生成到本地”仍只负责选择目录并写入生成文件,不会初始化 Git、修改 remote
|
||||||
|
或切换分支;完整 Candidate 开发流程应使用上面的 CLI generate 命令建立这些 Git
|
||||||
|
约束。
|
||||||
|
|
||||||
|
使用默认输出目录时,CLI 会写入 `src/saas/generated/<module-code>`,并自动维护
|
||||||
|
`src/saas/generated/index.ts` 稳定导出入口。
|
||||||
|
|
||||||
|
生成后可随时执行 `pnpm exec cool saas-model verify` 检查当前分支、候选锁、生成文件与
|
||||||
|
控制面的精确 Candidate 是否一致。`pnpm dev` 会先执行同一校验,再调用
|
||||||
|
`package.json` 中 `coolSaas.lifecycle.dev` 的非递归命令构建并启动原有开发宿主;因此
|
||||||
|
配置里不要写回 `pnpm dev`。项目接入自己的 watch/reload 工具后,可将该命令替换为对应
|
||||||
|
的开发启动命令。
|
||||||
|
|
||||||
|
CLI 生成的 `models.ts`、`contracts.ts`、`saas-actions.ts`、`saas-events.ts`、
|
||||||
|
`module.ts`、`index.ts` 和锁文件属于同一个不可变 Build,请整体保留且不要手工修改。
|
||||||
|
|
||||||
|
## 固化 Candidate
|
||||||
|
|
||||||
|
开发联调完成后,先提交并将当前 release 分支推送到 `origin`,然后执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm run publish
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本只调用 `cool saas-model finalize`。CLI 从 `package.json` 的 `coolSaas.lifecycle`
|
||||||
|
读取并依次执行 `test` 与 `build`(本模板分别为 typecheck 和原有 build),确认工作区仍然
|
||||||
|
干净且当前提交已推送后,创建并推送 `v<candidate-version>` Tag,最后向控制面幂等提交
|
||||||
|
freeze 证据。生命周期子命令不得调用 `pnpm dev`、`pnpm publish` 或再次调用
|
||||||
|
`cool saas-model dev/finalize`,以免递归。
|
||||||
|
|
||||||
|
freeze 成功后,原来的 development 实例会因身份不再匹配而停止参与路由。再次执行
|
||||||
|
`pnpm dev`,CLI 会读取候选的 Frozen Artifact,只允许从完全一致且干净的 Git commit
|
||||||
|
以 `runtimeMode=frozen` 启动。请用这个固化实例完成最终验收;验收通过后才能在管理端
|
||||||
|
晋升 Stable。`accepted` 状态也可以用同一命令重启精确固化实例,但已晋升、拒绝、替代
|
||||||
|
或放弃的候选会被拒绝。
|
||||||
|
|
||||||
|
控制面地址与凭证继续使用 `COOL_SAAS_CONTROL_PLANE_URL`、`COOL_SAAS_TOKEN`,或 CLI 的
|
||||||
|
`--endpoint`、`--token` 参数。CLI token 只用于校验/freeze 请求,不会写入候选锁,也
|
||||||
|
不会传给测试、构建或开发宿主子进程。freeze 调用
|
||||||
|
`/internal/saas/module_release/freeze`,只发送 Bearer 凭证,不依赖浏览器 Cookie。
|
||||||
|
|
||||||
|
## 依赖与发布
|
||||||
|
|
||||||
|
`@cool-midway/core`、`@cool-midway/module-runtime` 和 `@cool-midway/rpc` 使用
|
||||||
|
`https://npm.blendercg.art/` 中已发布的精确版本。安装前通过环境变量提供私服凭证,
|
||||||
|
不要把 token 写入仓库:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:NODE_AUTH_TOKEN='<registry-token>'
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
本地环境可以使用 Runtime 提供的开发配置。应用开发者在部署描述中只需要关注:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
PORT=7101
|
||||||
|
SAAS_CONTROL_PLANE_URL=https://control-plane.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
`PORT` 是进程在容器内监听的健康检查端口,不是 Action RPC 的入口。同一镜像的所有
|
||||||
|
副本都可以监听 `7101`。宿主机可分别映射为 `17101:7101`、`17102:7101`,也可以完全
|
||||||
|
不发布端口;无论选择哪种方式,容器内配置都保持不变。
|
||||||
|
|
||||||
|
仓库当前网关与模块宿主默认连接 Redis `6397`。部署环境如果使用其他 Redis 服务端口,
|
||||||
|
由编排器同时覆盖双方的 `REDIS_PORT`,不能只修改一侧。独立模块宿主默认采用
|
||||||
|
`RPC_RUNTIME_MODE=all`,即同一进程同时接收 Action 并运行托管 Outbox/Inbox Worker;
|
||||||
|
只有明确拆成 `rpc` 与 `worker` 两类副本时才覆盖该值,不能用兼容旧名称 `server`
|
||||||
|
冒充完整托管宿主。
|
||||||
|
|
||||||
|
生产环境由受信部署编排器在应用配置之外完成以下工作:
|
||||||
|
|
||||||
|
- 签发绑定精确 Build/Release 的 bootstrap ticket,并注入 `BUILD_SHA`、Redis 和
|
||||||
|
RPC HMAC 材料;Release 身份及摘要由控制面权威响应回填,不要求手工填写。
|
||||||
|
- 为每个副本分别挂载短期、一次性且绑定模块/Build/Release/制品的
|
||||||
|
`SAAS_RUNTIME_BOOTSTRAP_TOKEN_FILE`,不能让两个副本共用一个票据。票据签发结果会
|
||||||
|
同时返回并绑定该副本的 `instanceId`。
|
||||||
|
- 为每个副本提供独立、可写的 `SAAS_RUNTIME_BOOTSTRAP_STATE_FILE`;其中的实例 UUID、
|
||||||
|
Runtime Node ID、租约及 fencing 状态由 Runtime 管理,开发者不要手工填写。
|
||||||
|
- 使用票据签发结果中的 `instanceId` 生成对应的 `SAAS_TENANT_CREDENTIALS_FILE`,再与
|
||||||
|
票据一起挂载给该副本。源码和 Compose 文件都不能保存 Secret 值。
|
||||||
|
|
||||||
|
生产及其他严格环境不接受手工配置 `RPC_INSTANCE_ID`、`SAAS_RUNTIME_NODE_ID` 和
|
||||||
|
`SAAS_SERVICE_TOKEN_FILE` 来绕过 bootstrap。进程只能通过一次性 ticket 建立新会话,
|
||||||
|
或恢复 Runtime 已验真且仍有效的独占 state;静态三元组仅作为 local/test 的迁移兼容,
|
||||||
|
不能用于部署。
|
||||||
|
|
||||||
|
`bootstrap.js` 会先执行 Runtime pre-bootstrap,再校验精确 Release 环境,最后启动
|
||||||
|
Midway。`GET /health/` 读取 `rpcHostReadiness` 的真实快照;bootstrap、Node ID 租约或
|
||||||
|
RPC 注册任一未就绪时都会返回 HTTP `503`,只有全部就绪才返回 `200`。
|
||||||
|
|
||||||
|
仓库根目录的 `docker-compose.microservices.yml` 是同镜像双副本示例。两个副本容器内
|
||||||
|
均使用 `7101`,并分别映射到宿主机 `17101`、`17102`,每个副本有独立的 bootstrap
|
||||||
|
ticket、tenant credential Secret 和 Runtime state volume。生产环境若只需容器网络
|
||||||
|
内的 readiness probe,可以用部署 override 清空 `ports`,Action RPC 不受影响:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
demo-stable-a:
|
||||||
|
ports: []
|
||||||
|
demo-stable-b:
|
||||||
|
ports: []
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose 顶层 Secret 的 `${..._FILE}` 是编排器生成的宿主机文件路径,不是应用配置。
|
||||||
|
这些源文件在 Linux 上必须限制为服务账户私有权限(例如 `0600`),且不能提交到
|
||||||
|
仓库。生产平台可等价替换为 Kubernetes/Swarm external Secret,容器内的固定挂载路径
|
||||||
|
和 `*_FILE` 环境变量无需改变。
|
||||||
|
|
||||||
|
`SAAS_CONTROL_PLANE_URL` 必须是容器自身能够访问的地址;容器中的 `127.0.0.1` 指向
|
||||||
|
容器本身,不能用来表示宿主机或另一个控制面容器。生产环境必须使用 HTTPS。
|
||||||
36
bootstrap.js
vendored
Normal file
36
bootstrap.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Bootstrap } = require('@midwayjs/bootstrap');
|
||||||
|
const { prepareSaasServiceHost } = require('@cool-midway/rpc/saas-host');
|
||||||
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
requireExactReleaseEnvironment,
|
||||||
|
} = require('./release-environment');
|
||||||
|
|
||||||
|
function loadGeneratedSaasModule() {
|
||||||
|
return require('./dist/saas/generated').GeneratedSaasModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await prepareSaasServiceHost({
|
||||||
|
generated: loadGeneratedSaasModule(),
|
||||||
|
});
|
||||||
|
requireExactReleaseEnvironment();
|
||||||
|
return Bootstrap.configure({
|
||||||
|
baseDir: path.join(__dirname, 'dist'),
|
||||||
|
imports: [require('./dist/configuration')],
|
||||||
|
}).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
start().catch(error => {
|
||||||
|
process.stderr.write(`${error?.stack || error?.message || error}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadGeneratedSaasModule,
|
||||||
|
requireExactReleaseEnvironment,
|
||||||
|
start,
|
||||||
|
};
|
||||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@tenant/saas-module-service",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "COOL SaaS 托管模块微服务",
|
||||||
|
"main": "dist/configuration.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cool saas-model dev",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"build": "rimraf dist && mwtsc --cleanOutDir",
|
||||||
|
"publish": "cool saas-model finalize",
|
||||||
|
"start": "node bootstrap.js",
|
||||||
|
"start:local": "cross-env NODE_ENV=local SAAS_RUNTIME_BOOTSTRAP_LOCAL_FALLBACK_ENABLED=true node bootstrap.js"
|
||||||
|
},
|
||||||
|
"coolSaas": {
|
||||||
|
"lifecycle": {
|
||||||
|
"dev": "pnpm run build && pnpm run start:local",
|
||||||
|
"test": "pnpm run typecheck",
|
||||||
|
"build": "pnpm run build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cool-midway/module-runtime": "9.0.2",
|
||||||
|
"@cool-midway/rpc": "9.0.2",
|
||||||
|
"@midwayjs/bootstrap": "^3.20.3",
|
||||||
|
"@midwayjs/core": "^3.20.3",
|
||||||
|
"@midwayjs/koa": "^3.20.3",
|
||||||
|
"@midwayjs/logger": "^3.4.2",
|
||||||
|
"@midwayjs/validate": "^3.20.3",
|
||||||
|
"mysql2": "^3.12.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"typeorm": "npm:@cool-midway/typeorm@0.3.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cool-midway/core": "8.0.11",
|
||||||
|
"@types/node": "22.10.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"mwtsc": "^1.16.0",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "~5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
4961
pnpm-lock.yaml
generated
Normal file
4961
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
195
release-environment.js
Normal file
195
release-environment.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { statSync } = require('fs');
|
||||||
|
|
||||||
|
function requireRuntimeFile(environment, field, minimumSize, maximumSize) {
|
||||||
|
const file = String(environment[field] || '').trim();
|
||||||
|
if (!path.isAbsolute(file)) {
|
||||||
|
throw new Error(`${field} must be absolute`);
|
||||||
|
}
|
||||||
|
const stat = statSync(file, { throwIfNoEntry: false });
|
||||||
|
if (!stat?.isFile() || stat.size < minimumSize || stat.size > maximumSize) {
|
||||||
|
throw new Error(`${field} is unavailable or invalid`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireExactReleaseEnvironment(environment = process.env) {
|
||||||
|
const runtimeEnvironment = String(environment.NODE_ENV || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const localOrTest = ['local', 'test'].includes(runtimeEnvironment);
|
||||||
|
const required =
|
||||||
|
runtimeEnvironment === 'prod' ||
|
||||||
|
runtimeEnvironment === 'production' ||
|
||||||
|
environment.RPC_REQUIRE_EXACT_RELEASE === 'true';
|
||||||
|
if (!required) return;
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
'RPC_SERVICE_VERSION',
|
||||||
|
'RPC_MIN_SCHEMA_VERSION',
|
||||||
|
'RPC_MAX_SCHEMA_VERSION',
|
||||||
|
'RPC_RELEASE_ID',
|
||||||
|
'RPC_RELEASE_VERSION',
|
||||||
|
'RPC_SCHEMA_CHECKSUM',
|
||||||
|
'RPC_ACTION_MANIFEST_CHECKSUM',
|
||||||
|
'RPC_RUNTIME_CONTRACT_CHECKSUM',
|
||||||
|
'RPC_IMPLEMENTATION_MANIFEST_CHECKSUM',
|
||||||
|
'RPC_ARTIFACT_DIGEST',
|
||||||
|
'BUILD_SHA',
|
||||||
|
'SAAS_CONTROL_PLANE_URL',
|
||||||
|
'SAAS_TENANT_CREDENTIALS_FILE',
|
||||||
|
];
|
||||||
|
const missing = fields.filter(
|
||||||
|
field => !String(environment[field] || '').trim()
|
||||||
|
);
|
||||||
|
if (missing.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Exact SaaS Release environment is incomplete: ${missing.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrapTicketFile = String(
|
||||||
|
environment.SAAS_RUNTIME_BOOTSTRAP_TOKEN_FILE || ''
|
||||||
|
).trim();
|
||||||
|
const bootstrapStateFile = String(
|
||||||
|
environment.SAAS_RUNTIME_BOOTSTRAP_STATE_FILE || ''
|
||||||
|
).trim();
|
||||||
|
const configuredIdentityReady = [
|
||||||
|
'RPC_INSTANCE_ID',
|
||||||
|
'SAAS_RUNTIME_NODE_ID',
|
||||||
|
'SAAS_SERVICE_TOKEN_FILE',
|
||||||
|
].every(field => String(environment[field] || '').trim());
|
||||||
|
if (
|
||||||
|
String(environment.SAAS_RUNTIME_BOOTSTRAP_TOKEN || '').trim() &&
|
||||||
|
['prod', 'production'].includes(runtimeEnvironment)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Production SaaS runtime bootstrap tokens must be mounted as a file'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!bootstrapTicketFile &&
|
||||||
|
!bootstrapStateFile &&
|
||||||
|
!(localOrTest && configuredIdentityReady)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Strict SaaS runtime identity requires an authenticated orchestrator bootstrap ticket/state; configured RPC_INSTANCE_ID and SAAS_RUNTIME_NODE_ID are local/test migration compatibility only'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
environment.RPC_MODULE_CODE &&
|
||||||
|
!/^[a-z][a-z0-9_]{0,62}$/.test(environment.RPC_MODULE_CODE)
|
||||||
|
) {
|
||||||
|
throw new Error('RPC_MODULE_CODE is invalid');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
environment.RPC_INSTANCE_ID &&
|
||||||
|
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
||||||
|
environment.RPC_INSTANCE_ID
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('RPC_INSTANCE_ID must be a UUID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseId = Number(environment.RPC_RELEASE_ID);
|
||||||
|
const minSchema = Number(environment.RPC_MIN_SCHEMA_VERSION);
|
||||||
|
const maxSchema = Number(environment.RPC_MAX_SCHEMA_VERSION);
|
||||||
|
const runtimeNodeId = environment.SAAS_RUNTIME_NODE_ID
|
||||||
|
? Number(environment.SAAS_RUNTIME_NODE_ID)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
!Number.isSafeInteger(releaseId) ||
|
||||||
|
releaseId < 1 ||
|
||||||
|
!Number.isSafeInteger(minSchema) ||
|
||||||
|
minSchema < 1 ||
|
||||||
|
minSchema !== maxSchema ||
|
||||||
|
(runtimeNodeId !== undefined &&
|
||||||
|
(!Number.isSafeInteger(runtimeNodeId) ||
|
||||||
|
runtimeNodeId < 0 ||
|
||||||
|
runtimeNodeId > 65_535))
|
||||||
|
) {
|
||||||
|
throw new Error('Exact SaaS Release numeric identity is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of [
|
||||||
|
'RPC_SCHEMA_CHECKSUM',
|
||||||
|
'RPC_ACTION_MANIFEST_CHECKSUM',
|
||||||
|
'RPC_RUNTIME_CONTRACT_CHECKSUM',
|
||||||
|
'RPC_IMPLEMENTATION_MANIFEST_CHECKSUM',
|
||||||
|
]) {
|
||||||
|
if (!/^[a-f0-9]{64}$/.test(environment[field])) {
|
||||||
|
throw new Error(`${field} must be a SHA-256 checksum`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!/^sha256:[a-f0-9]{64}$/.test(environment.RPC_ARTIFACT_DIGEST)) {
|
||||||
|
throw new Error('RPC_ARTIFACT_DIGEST must be a sha256 digest');
|
||||||
|
}
|
||||||
|
if (!/^[0-9A-Za-z._-]{6,128}$/.test(environment.BUILD_SHA)) {
|
||||||
|
throw new Error('BUILD_SHA is invalid');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!/^\d+\.\d+\.\d+(?:-rc\.\d+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
||||||
|
environment.RPC_SERVICE_VERSION
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('RPC_SERVICE_VERSION is invalid');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!/^[0-9A-Za-z][0-9A-Za-z.+_-]{0,95}$/.test(
|
||||||
|
environment.RPC_RELEASE_VERSION
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('RPC_RELEASE_VERSION is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
let controlPlane;
|
||||||
|
try {
|
||||||
|
controlPlane = new URL(environment.SAAS_CONTROL_PLANE_URL);
|
||||||
|
} catch {
|
||||||
|
throw new Error('SAAS_CONTROL_PLANE_URL is invalid');
|
||||||
|
}
|
||||||
|
if (!['http:', 'https:'].includes(controlPlane.protocol)) {
|
||||||
|
throw new Error('SAAS_CONTROL_PLANE_URL is invalid');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['prod', 'production'].includes(runtimeEnvironment) &&
|
||||||
|
controlPlane.protocol !== 'https:'
|
||||||
|
) {
|
||||||
|
throw new Error('Production SAAS_CONTROL_PLANE_URL must use HTTPS');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bootstrapTicketFile) {
|
||||||
|
requireRuntimeFile(
|
||||||
|
environment,
|
||||||
|
'SAAS_RUNTIME_BOOTSTRAP_TOKEN_FILE',
|
||||||
|
32,
|
||||||
|
16_384
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (bootstrapStateFile) {
|
||||||
|
requireRuntimeFile(
|
||||||
|
environment,
|
||||||
|
'SAAS_RUNTIME_BOOTSTRAP_STATE_FILE',
|
||||||
|
2,
|
||||||
|
1024 * 1024
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (environment.SAAS_SERVICE_TOKEN_FILE) {
|
||||||
|
requireRuntimeFile(
|
||||||
|
environment,
|
||||||
|
'SAAS_SERVICE_TOKEN_FILE',
|
||||||
|
32,
|
||||||
|
16_384
|
||||||
|
);
|
||||||
|
}
|
||||||
|
requireRuntimeFile(
|
||||||
|
environment,
|
||||||
|
'SAAS_TENANT_CREDENTIALS_FILE',
|
||||||
|
2,
|
||||||
|
1024 * 1024
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { requireExactReleaseEnvironment };
|
||||||
14
src/config/app.default.ts
Normal file
14
src/config/app.default.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { MidwayConfig } from '@midwayjs/core';
|
||||||
|
import { defineSaasServiceHostConfig } from '@cool-midway/rpc/saas-host';
|
||||||
|
import { GeneratedSaasModule } from '../saas/generated';
|
||||||
|
|
||||||
|
const host = defineSaasServiceHostConfig({
|
||||||
|
generated: GeneratedSaasModule,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...host,
|
||||||
|
keys: 'saas-module-service-local-v1',
|
||||||
|
koa: { port: Number(process.env.PORT || 7101) },
|
||||||
|
asyncContextManager: { enable: true },
|
||||||
|
} as MidwayConfig;
|
||||||
12
src/configuration.ts
Normal file
12
src/configuration.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Configuration } from '@midwayjs/core';
|
||||||
|
import * as koa from '@midwayjs/koa';
|
||||||
|
import * as validate from '@midwayjs/validate';
|
||||||
|
import * as rpc from '@cool-midway/rpc';
|
||||||
|
import * as saasHost from '@cool-midway/rpc/saas-host';
|
||||||
|
import * as DefaultConfig from './config/app.default';
|
||||||
|
|
||||||
|
@Configuration({
|
||||||
|
imports: [koa, validate, rpc, saasHost],
|
||||||
|
importConfigs: [{ default: DefaultConfig }],
|
||||||
|
})
|
||||||
|
export class SaasModuleServiceConfiguration {}
|
||||||
90
src/controller/health.ts
Normal file
90
src/controller/health.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Config, Controller, Get, Inject } from '@midwayjs/core';
|
||||||
|
import type { Context } from '@midwayjs/koa';
|
||||||
|
import type {
|
||||||
|
RpcHostReadiness,
|
||||||
|
RpcHostReadinessSnapshot,
|
||||||
|
} from '@cool-midway/rpc/saas-host';
|
||||||
|
|
||||||
|
@Controller('/health')
|
||||||
|
export class SaasModuleHealthController {
|
||||||
|
@Inject()
|
||||||
|
ctx: Context;
|
||||||
|
|
||||||
|
@Config('cool.rpc.moduleCode')
|
||||||
|
moduleCode: string;
|
||||||
|
|
||||||
|
@Config('cool.rpc.serviceVersion')
|
||||||
|
serviceVersion: string;
|
||||||
|
|
||||||
|
@Config('cool.rpc.minSchemaVersion')
|
||||||
|
minSchemaVersion: number;
|
||||||
|
|
||||||
|
@Config('cool.rpc.maxSchemaVersion')
|
||||||
|
maxSchemaVersion: number;
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
async health() {
|
||||||
|
const readiness = await this.resolveReadiness();
|
||||||
|
if (!readiness.ready) this.ctx.status = 503;
|
||||||
|
return {
|
||||||
|
...readiness,
|
||||||
|
moduleCode: this.moduleCode,
|
||||||
|
serviceVersion: this.serviceVersion,
|
||||||
|
schemaRange: [this.minSchemaVersion, this.maxSchemaVersion],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveReadiness(): Promise<RpcHostReadinessSnapshot> {
|
||||||
|
try {
|
||||||
|
const container = this.ctx?.requestContext;
|
||||||
|
if (!container) {
|
||||||
|
return this.unavailable('RPC_HOST_READINESS_PROVIDER_MISSING');
|
||||||
|
}
|
||||||
|
const readiness = await container.getAsync<RpcHostReadiness>(
|
||||||
|
'rpcHostReadiness'
|
||||||
|
);
|
||||||
|
if (!readiness) {
|
||||||
|
return this.unavailable('RPC_HOST_READINESS_PROVIDER_MISSING');
|
||||||
|
}
|
||||||
|
const snapshot = readiness?.snapshot?.();
|
||||||
|
return this.isReadinessSnapshot(snapshot)
|
||||||
|
? snapshot
|
||||||
|
: this.unavailable('RPC_HOST_READINESS_SNAPSHOT_INVALID');
|
||||||
|
} catch {
|
||||||
|
return this.unavailable('RPC_HOST_READINESS_UNAVAILABLE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isReadinessSnapshot(
|
||||||
|
snapshot: RpcHostReadinessSnapshot | undefined
|
||||||
|
): snapshot is RpcHostReadinessSnapshot {
|
||||||
|
const checkStates = ['pending', 'ok', 'error'];
|
||||||
|
return Boolean(
|
||||||
|
snapshot &&
|
||||||
|
typeof snapshot.ready === 'boolean' &&
|
||||||
|
['starting', 'ready', 'degraded', 'draining', 'stopped'].includes(
|
||||||
|
snapshot.status
|
||||||
|
) &&
|
||||||
|
snapshot.ready === (snapshot.status === 'ready') &&
|
||||||
|
Number.isFinite(snapshot.checkedAt) &&
|
||||||
|
snapshot.checks &&
|
||||||
|
checkStates.includes(snapshot.checks.bootstrap) &&
|
||||||
|
checkStates.includes(snapshot.checks.lease) &&
|
||||||
|
checkStates.includes(snapshot.checks.registry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unavailable(reason: string): RpcHostReadinessSnapshot {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
status: 'degraded',
|
||||||
|
reason,
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
checks: {
|
||||||
|
bootstrap: 'error',
|
||||||
|
lease: 'error',
|
||||||
|
registry: 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/saas/generated/.gitkeep
Normal file
1
src/saas/generated/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1104
src/saas/generated/demo/contracts.ts
Normal file
1104
src/saas/generated/demo/contracts.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
src/saas/generated/demo/index.ts
Normal file
5
src/saas/generated/demo/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './models';
|
||||||
|
export * from './contracts';
|
||||||
|
export * from './saas-actions';
|
||||||
|
export * from './saas-events';
|
||||||
|
export * from './module';
|
||||||
78
src/saas/generated/demo/models.ts
Normal file
78
src/saas/generated/demo/models.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Generated by cool saas-model generate. DO NOT EDIT.
|
||||||
|
import {
|
||||||
|
SaasReadModel,
|
||||||
|
SaasTableModel,
|
||||||
|
defineSaasReadModel,
|
||||||
|
defineSaasTableModel
|
||||||
|
} from '@cool-midway/module-runtime';
|
||||||
|
|
||||||
|
export interface GoodsRow extends Record<string, unknown> {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
stock: number;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Goods extends SaasTableModel<GoodsRow> {
|
||||||
|
static readonly definition = defineSaasTableModel({
|
||||||
|
"kind": "table",
|
||||||
|
"moduleCode": "demo",
|
||||||
|
"revision": 1,
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"code": "id",
|
||||||
|
"type": "bigint",
|
||||||
|
"nullable": false,
|
||||||
|
"primary": true,
|
||||||
|
"readable": true,
|
||||||
|
"writable": false,
|
||||||
|
"filterable": true,
|
||||||
|
"sortable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title",
|
||||||
|
"type": "varchar",
|
||||||
|
"nullable": false,
|
||||||
|
"primary": false,
|
||||||
|
"readable": true,
|
||||||
|
"writable": true,
|
||||||
|
"filterable": true,
|
||||||
|
"sortable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "price",
|
||||||
|
"type": "decimal",
|
||||||
|
"nullable": false,
|
||||||
|
"primary": false,
|
||||||
|
"readable": true,
|
||||||
|
"writable": true,
|
||||||
|
"filterable": true,
|
||||||
|
"sortable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "stock",
|
||||||
|
"type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"primary": false,
|
||||||
|
"readable": true,
|
||||||
|
"writable": true,
|
||||||
|
"filterable": true,
|
||||||
|
"sortable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "status",
|
||||||
|
"type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"primary": false,
|
||||||
|
"readable": true,
|
||||||
|
"writable": true,
|
||||||
|
"filterable": true,
|
||||||
|
"sortable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
12
src/saas/generated/demo/module.ts
Normal file
12
src/saas/generated/demo/module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Generated by cool saas-model generate. DO NOT EDIT.
|
||||||
|
import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
|
||||||
|
|
||||||
|
export const GeneratedSaasModule = defineGeneratedSaasModule({
|
||||||
|
"formatVersion": 2,
|
||||||
|
"moduleCode": "demo",
|
||||||
|
"revision": 1,
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
|
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
|
||||||
|
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"
|
||||||
|
});
|
||||||
2
src/saas/generated/demo/saas-actions.ts
Normal file
2
src/saas/generated/demo/saas-actions.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Generated by cool saas-model generate. DO NOT EDIT.
|
||||||
|
export const SaasActions = {} as const;
|
||||||
2
src/saas/generated/demo/saas-events.ts
Normal file
2
src/saas/generated/demo/saas-events.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Generated by cool saas-model generate. DO NOT EDIT.
|
||||||
|
export const SaasEvents = {} as const;
|
||||||
957
src/saas/generated/demo/saas-implementation.expected.json
Normal file
957
src/saas/generated/demo/saas-implementation.expected.json
Normal file
@@ -0,0 +1,957 @@
|
|||||||
|
{
|
||||||
|
"manifestVersion": 1,
|
||||||
|
"moduleCode": "demo",
|
||||||
|
"revision": 1,
|
||||||
|
"schemaChecksum": null,
|
||||||
|
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
|
||||||
|
"actionManifestChecksum": null,
|
||||||
|
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
|
||||||
|
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236",
|
||||||
|
"actions": [
|
||||||
|
"GoodsAdd",
|
||||||
|
"GoodsBatchInfo",
|
||||||
|
"GoodsDelete",
|
||||||
|
"GoodsInfo",
|
||||||
|
"GoodsList",
|
||||||
|
"GoodsPage",
|
||||||
|
"GoodsReleaseStock",
|
||||||
|
"GoodsReserveStock",
|
||||||
|
"GoodsUpdate"
|
||||||
|
],
|
||||||
|
"eventHandlers": [
|
||||||
|
"RuntimeSaasSagaCompensationCompleted",
|
||||||
|
"RuntimeSaasSagaCompensationFailed",
|
||||||
|
"RuntimeSaasSagaStepCompleted",
|
||||||
|
"RuntimeSaasSagaStepFailed"
|
||||||
|
],
|
||||||
|
"runtimeManagedEventHandlers": [
|
||||||
|
"RuntimeSaasSagaCompensationCompleted",
|
||||||
|
"RuntimeSaasSagaCompensationFailed",
|
||||||
|
"RuntimeSaasSagaStepCompleted",
|
||||||
|
"RuntimeSaasSagaStepFailed"
|
||||||
|
],
|
||||||
|
"runtimeManagedActions": [
|
||||||
|
"GoodsAdd",
|
||||||
|
"GoodsDelete",
|
||||||
|
"GoodsInfo",
|
||||||
|
"GoodsList",
|
||||||
|
"GoodsPage",
|
||||||
|
"GoodsUpdate"
|
||||||
|
],
|
||||||
|
"crudDescriptors": [
|
||||||
|
{
|
||||||
|
"exportName": "Goods",
|
||||||
|
"tableCode": "goods",
|
||||||
|
"operations": [
|
||||||
|
"add",
|
||||||
|
"delete",
|
||||||
|
"update",
|
||||||
|
"info",
|
||||||
|
"list",
|
||||||
|
"page"
|
||||||
|
],
|
||||||
|
"descriptorChecksum": "22211efee07c5c1aada51d508d9c053e0d7388ad6fc0da12c95a4d1de2e3ca53",
|
||||||
|
"actions": {
|
||||||
|
"add": "GoodsAdd",
|
||||||
|
"delete": "GoodsDelete",
|
||||||
|
"update": "GoodsUpdate",
|
||||||
|
"info": "GoodsInfo",
|
||||||
|
"list": "GoodsList",
|
||||||
|
"page": "GoodsPage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actionDescriptors": [
|
||||||
|
{
|
||||||
|
"name": "goods.add",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"mutationKind": "insert",
|
||||||
|
"access": "write",
|
||||||
|
"idempotent": true,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"price"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputChecksum": "c2984884419729416e6cd81e4cddc6d82486b6dde2d9de0ca17533f69f55816a",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputChecksum": "bb691ac7eaa19bb231f9da14a80848262965255fb37726ab155fb9804c9a55b1",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:add"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"stock",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"select": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"aggregateStream": "goods",
|
||||||
|
"sequenceCompleteness": "complete",
|
||||||
|
"idempotencyRequired": true,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.created",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
|
||||||
|
"operation": "upsert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.deleted",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
|
||||||
|
"operation": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.updated",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
|
||||||
|
"operation": "upsert"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.batchInfo",
|
||||||
|
"origin": "business",
|
||||||
|
"implementation": "business",
|
||||||
|
"access": "read",
|
||||||
|
"idempotent": false,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputChecksum": "b2e0c5fd68adc75e2687c6a1ce01b8270240b42df6511efd6d922ea3242bc264",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"list"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "module",
|
||||||
|
"allowedModules": [
|
||||||
|
"order"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tables": [],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.delete",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"mutationKind": "delete",
|
||||||
|
"access": "write",
|
||||||
|
"idempotent": true,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputChecksum": "f7411322cb1460ead79eaa8267854f576188c583bfdc621d9cb6b5097991e865",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"affected": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"affected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputChecksum": "5aa7994f78677fd557c2611cb26b6a1c4518cf4d6ccefa189a5c36af9f229716",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:delete"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"select": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"aggregateStream": "goods",
|
||||||
|
"sequenceCompleteness": "complete",
|
||||||
|
"idempotencyRequired": true,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.created",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
|
||||||
|
"operation": "upsert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.deleted",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
|
||||||
|
"operation": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.updated",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
|
||||||
|
"operation": "upsert"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.info",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"access": "read",
|
||||||
|
"idempotent": false,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputChecksum": "f7411322cb1460ead79eaa8267854f576188c583bfdc621d9cb6b5097991e865",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputChecksum": "bb691ac7eaa19bb231f9da14a80848262965255fb37726ab155fb9804c9a55b1",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:info"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"stock",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"select": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.list",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"access": "read",
|
||||||
|
"idempotent": false,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"where": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"field": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"asc",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"field",
|
||||||
|
"direction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keyword": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 200
|
||||||
|
},
|
||||||
|
"take": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"inputChecksum": "68074fd84863ef796fc3c99fc87d36c740f4a5bad7134a02b090f7cef6bde883",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputChecksum": "2182ab26245d80a9a40216cce7cadbe8217ea12813f1cf536b7a59ba0a4ed4ae",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:list"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"stock",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"select": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.page",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"access": "read",
|
||||||
|
"idempotent": false,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"where": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"field": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"asc",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"field",
|
||||||
|
"direction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keyword": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 200
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"inputChecksum": "65eb38a87c2692fde220c350f971d8cc91c9ccc28e7bc41221092edaba042a4b",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"stock",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"page": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 200
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"page",
|
||||||
|
"size",
|
||||||
|
"total"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"list",
|
||||||
|
"pagination"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputChecksum": "15ee81575d900276aaa2437afa5ee6f41321e5b00ab58706322e5d5c0809bf8d",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:page"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"stock",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"select": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.releaseStock",
|
||||||
|
"origin": "business",
|
||||||
|
"implementation": "business",
|
||||||
|
"access": "write",
|
||||||
|
"idempotent": true,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"goodsId",
|
||||||
|
"quantity",
|
||||||
|
"orderNo"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"goodsId": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"orderNo": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputChecksum": "c363d259b5108d069650a2554309eb3eb165d98723596595958e00acf8410abe",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"goodsId",
|
||||||
|
"currentStock"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"goodsId": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"currentStock": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "module",
|
||||||
|
"allowedModules": [
|
||||||
|
"order"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tables": [],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"aggregateStream": "goods",
|
||||||
|
"sequenceCompleteness": "complete",
|
||||||
|
"idempotencyRequired": true,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.stockReserved",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "4d8a47d64f7d9fa43e0253228d05a2bb28db7efe3181e96d58dbff2163b9b752",
|
||||||
|
"operation": "upsert"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.reserveStock",
|
||||||
|
"origin": "business",
|
||||||
|
"implementation": "business",
|
||||||
|
"access": "write",
|
||||||
|
"idempotent": true,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"goodsId",
|
||||||
|
"quantity"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"goodsId": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputChecksum": "6fbfa4d2324c4c0bdc621cbfd256353284e27495f59f47231b7d897b82a4e7b6",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"goodsId",
|
||||||
|
"remainingStock"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"goodsId": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"remainingStock": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "module",
|
||||||
|
"allowedModules": [
|
||||||
|
"order"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tables": [],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"aggregateStream": "goods",
|
||||||
|
"sequenceCompleteness": "complete",
|
||||||
|
"idempotencyRequired": true,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.stockReserved",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "4d8a47d64f7d9fa43e0253228d05a2bb28db7efe3181e96d58dbff2163b9b752",
|
||||||
|
"operation": "upsert"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goods.update",
|
||||||
|
"origin": "table_crud",
|
||||||
|
"implementation": "runtime_managed",
|
||||||
|
"mutationKind": "update",
|
||||||
|
"access": "write",
|
||||||
|
"idempotent": true,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?[0-9]+$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"minProperties": 2
|
||||||
|
},
|
||||||
|
"inputChecksum": "ced49f501c83dd1dce9725c913a1761566828004c851779a5db85ddd2bb9123c",
|
||||||
|
"outputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"affected": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"affected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputChecksum": "5aa7994f78677fd557c2611cb26b6a1c4518cf4d6ccefa189a5c36af9f229716",
|
||||||
|
"callerPolicy": {
|
||||||
|
"kind": "permission",
|
||||||
|
"permission": "demo:goods:update"
|
||||||
|
},
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"fields": [
|
||||||
|
"id",
|
||||||
|
"price",
|
||||||
|
"status",
|
||||||
|
"stock",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"select": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readModels": [],
|
||||||
|
"writes": [
|
||||||
|
{
|
||||||
|
"tableCode": "goods",
|
||||||
|
"aggregateStream": "goods",
|
||||||
|
"sequenceCompleteness": "complete",
|
||||||
|
"idempotencyRequired": true,
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.created",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
|
||||||
|
"operation": "upsert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.deleted",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
|
||||||
|
"operation": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "demo.goods.updated",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
|
||||||
|
"operation": "upsert"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eventHandlerDescriptors": [
|
||||||
|
{
|
||||||
|
"eventType": "saas.saga.compensation.completed",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "2919921c79a7795d6408b202d74aa0fb4b12ec56e1cec7de65eb95bbeeef2c5e",
|
||||||
|
"tables": [
|
||||||
|
"__saas_rpc_saga",
|
||||||
|
"__saas_rpc_saga_step"
|
||||||
|
],
|
||||||
|
"maxAttempts": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "saas.saga.compensation.failed",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "b7c7313c221e395c42e45e9480205a673108bdc395430bec892a9dafa39807f3",
|
||||||
|
"tables": [
|
||||||
|
"__saas_rpc_saga",
|
||||||
|
"__saas_rpc_saga_step"
|
||||||
|
],
|
||||||
|
"maxAttempts": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "saas.saga.step.completed",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "9d3acfdac4e5713672063f7e9f9b5b0b09487c59238d89dbe8d7145c6a09f1bd",
|
||||||
|
"tables": [
|
||||||
|
"__saas_rpc_saga",
|
||||||
|
"__saas_rpc_saga_step"
|
||||||
|
],
|
||||||
|
"maxAttempts": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "saas.saga.step.failed",
|
||||||
|
"eventVersion": 1,
|
||||||
|
"eventContractChecksum": "8dd17e8104b78fa3ece7375402782dee38e121a87c91185658633fabaac8eba0",
|
||||||
|
"tables": [
|
||||||
|
"__saas_rpc_saga",
|
||||||
|
"__saas_rpc_saga_step"
|
||||||
|
],
|
||||||
|
"maxAttempts": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2044
src/saas/generated/demo/saas-schema.lock.json
Normal file
2044
src/saas/generated/demo/saas-schema.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
1
src/saas/generated/index.ts
Normal file
1
src/saas/generated/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './demo';
|
||||||
1
src/service/.gitkeep
Normal file
1
src/service/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
118
src/service/goods.ts
Normal file
118
src/service/goods.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import {
|
||||||
|
InjectSaasCrud,
|
||||||
|
SaasAction,
|
||||||
|
SaasActionContext,
|
||||||
|
SaasBusinessReject,
|
||||||
|
SaasCrudCaller,
|
||||||
|
SaasCrudService,
|
||||||
|
SaasCrudServiceBase,
|
||||||
|
} from '@cool-midway/module-runtime';
|
||||||
|
import {
|
||||||
|
Actions,
|
||||||
|
Crud,
|
||||||
|
Events,
|
||||||
|
type GoodsBatchInfoInput,
|
||||||
|
type GoodsBatchInfoOutput,
|
||||||
|
type GoodsInfoOutput,
|
||||||
|
type GoodsReleaseStockInput,
|
||||||
|
type GoodsReleaseStockOutput,
|
||||||
|
type GoodsReserveStockInput,
|
||||||
|
type GoodsReserveStockOutput,
|
||||||
|
} from '../saas/generated';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@SaasCrudService(Crud.Goods)
|
||||||
|
export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
|
||||||
|
@InjectSaasCrud(Crud.Goods)
|
||||||
|
private readonly goods!: SaasCrudCaller<typeof Crud.Goods>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private readonly saasActionContext!: SaasActionContext;
|
||||||
|
|
||||||
|
@SaasAction(Actions.GoodsBatchInfo)
|
||||||
|
async batchInfo(
|
||||||
|
input: GoodsBatchInfoInput
|
||||||
|
): Promise<GoodsBatchInfoOutput> {
|
||||||
|
const list: GoodsInfoOutput[] = [];
|
||||||
|
for (const id of input.ids) {
|
||||||
|
const goods = await this.findGoods(id);
|
||||||
|
if (goods) list.push(goods);
|
||||||
|
}
|
||||||
|
return { list };
|
||||||
|
}
|
||||||
|
|
||||||
|
@SaasAction(Actions.GoodsReserveStock)
|
||||||
|
async reserveStock(
|
||||||
|
input: GoodsReserveStockInput
|
||||||
|
): Promise<GoodsReserveStockOutput> {
|
||||||
|
const goods = await this.requireGoods(input.goodsId);
|
||||||
|
if (goods.status !== 1) {
|
||||||
|
throw new SaasBusinessReject(
|
||||||
|
'GOODS_UNAVAILABLE',
|
||||||
|
`商品 ${input.goodsId} 当前不可售`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (goods.stock < input.quantity) {
|
||||||
|
throw new SaasBusinessReject(
|
||||||
|
'INSUFFICIENT_STOCK',
|
||||||
|
`商品 ${input.goodsId} 库存不足`,
|
||||||
|
{ available: goods.stock, requested: input.quantity }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingStock = goods.stock - input.quantity;
|
||||||
|
this.saasActionContext.raise(Events.DemoGoodsStockReserved, {
|
||||||
|
goodsId: input.goodsId,
|
||||||
|
quantity: input.quantity,
|
||||||
|
remainingStock,
|
||||||
|
});
|
||||||
|
await this.updateStock(input.goodsId, remainingStock);
|
||||||
|
return { goodsId: input.goodsId, remainingStock };
|
||||||
|
}
|
||||||
|
|
||||||
|
@SaasAction(Actions.GoodsReleaseStock)
|
||||||
|
async releaseStock(
|
||||||
|
input: GoodsReleaseStockInput
|
||||||
|
): Promise<GoodsReleaseStockOutput> {
|
||||||
|
const goods = await this.requireGoods(input.goodsId);
|
||||||
|
const currentStock = goods.stock + input.quantity;
|
||||||
|
|
||||||
|
this.saasActionContext.raise(Events.DemoGoodsStockReserved, {
|
||||||
|
goodsId: input.goodsId,
|
||||||
|
quantity: input.quantity,
|
||||||
|
remainingStock: currentStock,
|
||||||
|
});
|
||||||
|
await this.updateStock(input.goodsId, currentStock);
|
||||||
|
return { goodsId: input.goodsId, currentStock };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findGoods(goodsId: number): Promise<GoodsInfoOutput | null> {
|
||||||
|
const rows = await this.goods.list({
|
||||||
|
where: { id: String(goodsId) },
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requireGoods(goodsId: number): Promise<GoodsInfoOutput> {
|
||||||
|
const goods = await this.findGoods(goodsId);
|
||||||
|
if (!goods) {
|
||||||
|
throw new SaasBusinessReject(
|
||||||
|
'GOODS_NOT_FOUND',
|
||||||
|
`商品 ${goodsId} 不存在`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return goods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateStock(goodsId: number, stock: number): Promise<void> {
|
||||||
|
const result = await this.goods.update({ id: String(goodsId), stock });
|
||||||
|
if (result.affected !== 1) {
|
||||||
|
throw new SaasBusinessReject(
|
||||||
|
'GOODS_STOCK_UPDATE_FAILED',
|
||||||
|
`商品 ${goodsId} 库存更新失败`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "dist-*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user