@oj-module/cli 0.2.54 → 0.2.56
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.
- package/devkit/SKILL.md +3 -3
- package/devkit/manual.md +59 -52
- package/package.json +2 -2
- package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/layout.test.ts +30 -1
- package/src/__tests__/templates-schema-columns.test.ts +99 -0
- package/src/alias-paths.ts +11 -12
- package/src/builtin.ts +10 -3
- package/src/contract/__tests__/emit-client.test.ts +4 -4
- package/src/contract/__tests__/market-client.integration.test.ts +2 -2
- package/src/contract/__tests__/run-api.test.ts +2 -2
- package/src/contract/check.ts +6 -2
- package/src/contract/emit-client.ts +10 -3
- package/src/contract/run.ts +10 -2
- package/src/init.ts +19 -11
- package/src/layout.ts +72 -16
- package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
- package/src/market/__tests__/bootstrap.test.ts +1 -1
- package/src/site.ts +1 -1
- package/templates/README.md +30 -20
- package/templates/init.include.yaml +19 -11
- package/templates/openapi.json +95 -86
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
- package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
- package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
- package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +5 -2
- package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +5 -2
- package/templates/src/app/web/notification/pages/center/index.tsx +1 -1
- package/templates/src/app/web/notification/routes.ts +1 -1
- package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +1 -1
- package/templates/src/app/web/personal-center/routes.ts +1 -1
- package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
- package/templates/src/app/web/system/locales/en-US.json +2 -3
- package/templates/src/app/web/system/locales/zh-CN.json +2 -3
- package/templates/src/app/web/system/pages/departments/index.tsx +2 -2
- package/templates/src/app/web/system/pages/login-methods/index.tsx +1 -1
- package/templates/src/app/web/system/pages/oplog/index.tsx +44 -19
- package/templates/src/app/web/system/pages/roles/index.tsx +5 -3
- package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
- package/templates/src/app/web/system/pages/settings/index.tsx +2 -2
- package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
- package/templates/src/app/web/system/pages/users/index.tsx +25 -17
- package/templates/src/app/web/system/pages/users/roles.tsx +1 -1
- package/templates/src/app/web/system/routes.ts +1 -1
- package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +5 -2
- package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
- package/templates/tests/auth-ldap.spec.ts +2 -2
- package/templates/tests/auth-session.spec.ts +3 -3
- package/templates/tests/auth-throttle.spec.ts +1 -1
- package/templates/tests/fixture.ts +6 -4
- package/templates/tests/guard.spec.ts +1 -1
- package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
- package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
- package/templates/tests/lifecycle/installer.spec.ts +10 -10
- package/templates/tests/lifecycle/market-api.spec.ts +14 -14
- package/templates/tests/lifecycle/migrate.spec.ts +6 -6
- package/templates/tests/lifecycle/preflight.spec.ts +3 -3
- package/templates/tests/lifecycle/register.spec.ts +1 -1
- package/templates/tests/lifecycle/semver.spec.ts +1 -1
- package/templates/tests/lifecycle/sha384.spec.ts +1 -1
- package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
- package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
- package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
- package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
- package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
- package/templates/tests/lifecycle/trust.spec.ts +5 -5
- package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
- package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
- package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
- package/templates/tests/org-departments.spec.ts +10 -6
- package/templates/tests/org-read.spec.ts +7 -7
- package/templates/tests/query-utils.spec.ts +1 -1
- package/templates/tests/rbac-audit.spec.ts +1 -1
- package/templates/tests/rbac-contacts.spec.ts +2 -2
- package/templates/tests/rbac-guard.spec.ts +1 -1
- package/templates/tests/rbac-roles.spec.ts +10 -2
- package/templates/tests/rbac-seed.spec.ts +1 -1
- package/templates/tests/rbac-settings.spec.ts +77 -7
- package/templates/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/tests/rbac-tables.spec.ts +29 -0
- package/templates/tests/rbac-users.spec.ts +55 -3
- package/templates/tests/roles-page.spec.ts +40 -0
- package/templates/tests/seed-role-accounts.spec.ts +1 -1
- package/templates/tests/tenant-access-cache.spec.ts +1 -1
- package/templates/tests/tenant-forgery.spec.ts +5 -5
- package/templates/tests/tenant-isolation.spec.ts +5 -4
- package/templates/tests/username-namespace.spec.ts +3 -3
- package/templates/tests/users-page.spec.ts +39 -0
- package/templates/tests/wecom-admin.spec.ts +6 -6
- package/templates/tests/wecom-client-timeout.spec.ts +1 -1
- package/templates/tests/wecom-connector.spec.ts +9 -9
- package/templates/tests/wecom-corp.spec.ts +9 -9
- package/templates/tests/wecom-crypt.spec.ts +1 -1
- package/templates/tests/wecom-endpoints.spec.ts +2 -2
- package/templates/tests/wecom-landing.spec.ts +1 -1
- package/templates/tests/wecom-org-sync.spec.ts +7 -7
- package/templates/tests/wecom-sync.spec.ts +3 -3
- package/templates/tsconfig.paths.json +11 -11
- package/templates/vitest.config.ts +2 -2
- package/templates/web.config.ts +5 -2
- package/vendor/host-versions.json +3 -3
- package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
- /package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +0 -0
- /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
- /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/api.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/del/api.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
- /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
- /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/roles/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/settings/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/password/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/roles/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/get-async-routes/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/hello/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
- /package/templates/src/{web/api → app/api/web}/user-info/api.ts +0 -0
- /package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +0 -0
- /package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +0 -0
- /package/templates/src/app/web/home/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/notification/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/personal-center/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/system/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/{web → app/web}/web/__client/api.schemas.ts +0 -0
package/devkit/SKILL.md
CHANGED
|
@@ -103,7 +103,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
|
|
|
103
103
|
|
|
104
104
|
## 企业微信(接通真实套件 / 自建应用)
|
|
105
105
|
|
|
106
|
-
模板已内置企微登录(`src/
|
|
106
|
+
模板已内置企微登录(`src/app/api/auth/wecom/` + 登录页「企业微信」页签 + `/system/wecom` 后台)。
|
|
107
107
|
两种来源、两本手册,**按客户形态选**:
|
|
108
108
|
|
|
109
109
|
| 客户形态 | 读 | 凭据 |
|
|
@@ -132,7 +132,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
|
|
|
132
132
|
- **整页跳转拼 API base**:`window.location.assign(resolveApiBase() + wecomAuthorizePath(corp))`;
|
|
133
133
|
拼站点 base 会被 SPA 兜底页吞掉(页面原地重载、一个请求都不发,控制台无报错)。
|
|
134
134
|
- **通讯录同步是「组织架构底座 + 企微适配器」,不是企微专属功能**:模板的
|
|
135
|
-
`src/
|
|
135
|
+
`src/app/api/auth/wecom/{sync,contacts}.ts` 只负责「取快照 → 喂给领域层」,模型与读面在 `_platform`
|
|
136
136
|
(手册 §14)。要按部门算人、要选人、要给组织架构接 LDAP,一律读 §14,不要在企微代码里加分支。
|
|
137
137
|
同步/事件增量与回调解密是**两条独立纪律**:回调必须回 `success`,而事件处理失败**也照样回 `success`**
|
|
138
138
|
(重推补不回已判死的失败,兜底是下一轮全量同步)。
|
|
@@ -187,7 +187,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
|
|
|
187
187
|
| 登录 401 且 msg 不是 `invalid credentials` | `/auth/*` 不在 `anonymous_paths` | 加进工程根 `config.yaml`,重启 |
|
|
188
188
|
| WS 连接 401(「文件在却连不上」) | 模块写了 `ws.ts`,但其 `/ws` 路由不在 `anonymous_paths`(oj v0.1.30+ WS 握手也过鉴权) | 匿名 WS 加进 `anonymous_paths`;受保护 WS 带 Bearer/Cookie 是正当用法。`ojm api --check` 会对此 warn |
|
|
189
189
|
| 登出/回登录页落空,或登录后落错误边界 | 工程缺 `login` / `home` 模块 | 在 `web.config.ts` 保留两者 |
|
|
190
|
-
| 通知铃 404 `no route matched`(`/api/notifications`) | 前端 `notification` 模块没加载 / 没注册 `notificationsApi`,回落到内置只读兜底;脚手架不再提供 root 级 `/api/notifications` | 保留 `src/app/web/notification/entry.ts` 的登记 + onInit 注册四方法;后端由 `src/
|
|
190
|
+
| 通知铃 404 `no route matched`(`/api/notifications`) | 前端 `notification` 模块没加载 / 没注册 `notificationsApi`,回落到内置只读兜底;脚手架不再提供 root 级 `/api/notifications` | 保留 `src/app/web/notification/entry.ts` 的登记 + onInit 注册四方法;后端由 `src/app/api/notification` 出 `/api/notification/*` |
|
|
191
191
|
| `oj serve` 启动即退,报 `config uses removed server.* site keys (v0.1.58): api_prefix` | 用了 oj v0.1.58 起删除的旧站点键(`api_prefix`/`app_path`/`app_prefix`/`static_sites`/`app_spa_fallback`) | 改 `server.api_prefix: /api` 为顶层 `mounts:`(module-root 写 `{prefix: "/api", api: "api/dist"}`;历史布局写 `api: "src"`、源码交给 `-d`);迁移完整 YAML 由报错给出 |
|
|
192
192
|
| 改完 `mounts:` 仍全 404 | `api:` 指向目录不对(相对 config 所在目录) | module-root 指向产物目录 `"api/dist"`、历史布局指向源码树 `"src"`;`-c` 指向工程根 `config.yaml`(ojm dev/preview 已内置)。module-root 模板**故意不写 api 挂载**(源码交给 `--module-root src`、产物交给 `--api-path api/dist`,同时出现会报 module declared in multiple places) |
|
|
193
193
|
| `oj serve` 启动即退,报 `module '_shared' missing manifest.yaml` | 把共享后端代码放到了扫描根顶层(`src/_shared/`)——oj 把扫描根下的顶层目录都当模块;历史布局是 `api/src/_shared/` | 嵌进所属模块:`src/<模块>/api/_shared/`(历史布局 `api/src/<模块>/_shared/`),用 `#<本模块>/...` 别名 |
|
package/devkit/manual.md
CHANGED
|
@@ -42,22 +42,25 @@
|
|
|
42
42
|
│ ├── tsconfig.json # 后端 typecheck 工程(extends 上两者,exclude */web)
|
|
43
43
|
│ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠,见 §11)
|
|
44
44
|
│ ├── styles.css
|
|
45
|
-
│ ├──
|
|
46
|
-
│ │ ├── api/ #
|
|
47
|
-
│ │
|
|
48
|
-
│ ├──
|
|
49
|
-
│ │ ├──
|
|
50
|
-
│ │ ├──
|
|
51
|
-
│ │ ├──
|
|
52
|
-
│ │
|
|
53
|
-
│ │
|
|
54
|
-
│ │
|
|
45
|
+
│ ├── app/ # 整个应用(后端模块 + 前端聚合模块)
|
|
46
|
+
│ │ ├── api/ # 后端模块(模块名 = URL 前缀,各自仍是独立 oj 模块)
|
|
47
|
+
│ │ │ ├── _platform/ # users 表 / rbac / 市场编排 / 生命周期 + _shared/
|
|
48
|
+
│ │ │ ├── auth/ # login / refresh / logout / ldap-login
|
|
49
|
+
│ │ │ ├── system/ # 用户 / 角色 / 权限 / 设置 / oplog / login-methods
|
|
50
|
+
│ │ │ ├── notification/ # 通知互动(表 + push + del)
|
|
51
|
+
│ │ │ ├── personal-center/ # 头像上传
|
|
52
|
+
│ │ │ └── web/ # root 级 /api/web(hello / user-info / get-async-routes)
|
|
53
|
+
│ │ └── web/ # 前端**多根聚合模块**(一个 entry 多个根)
|
|
54
|
+
│ │ ├── entry.ts # routes 聚合 + i18nNamespaces + 生命周期
|
|
55
|
+
│ │ ├── home/ # /home(routes.ts + pages + locales + __client)
|
|
56
|
+
│ │ ├── login/ # /login、/change-password
|
|
57
|
+
│ │ ├── personal-center/ # /personal-center/my-profile
|
|
58
|
+
│ │ ├── notification/ # /notification-center
|
|
59
|
+
│ │ ├── system/ # /system/*(根上 handle.roles = 系统管理员)
|
|
60
|
+
│ │ ├── auth/ # 仅 __client(无页面)
|
|
61
|
+
│ │ ├── _platform/ # 仅 __client(oj-market 引用)
|
|
62
|
+
│ │ └── web/ # 仅 __client
|
|
55
63
|
│ ├── oj-market/web/ # 解决方案市场(独立模块,删目录即下线)
|
|
56
|
-
│ ├── auth/api/ # login / refresh / logout / ldap-login
|
|
57
|
-
│ ├── system/api/ # 用户 / 角色 / 权限 / 设置 / oplog / login-methods(后端)
|
|
58
|
-
│ ├── notification/api/ # 通知互动(表 + push + del)
|
|
59
|
-
│ ├── personal-center/api/ # 头像上传
|
|
60
|
-
│ └── web/{api,web}/ # root 级 /api/web(hello / user-info / get-async-routes 在 api 侧)
|
|
61
64
|
├── dist/api/ # 后端产物(release 根:<name>-<ver>/ + manifests.yaml + versions.json)—构建后才有
|
|
62
65
|
├── dist/web/ # 站点产物(shell + modules/ + modules.json)—构建后才有
|
|
63
66
|
├── bin/ # oj 二进制 + plugins/ + devkit/(不入库)
|
|
@@ -72,10 +75,13 @@
|
|
|
72
75
|
`resolveCertDir()` 单点派生,你不需要自己拼路径)。
|
|
73
76
|
- **产物有两个根**:后端 `dist/api`(oj 扁平 release,`<name>-<ver>/` 与
|
|
74
77
|
`manifests.yaml` 同层),站点 `dist/web`(`modules/` + `modules.json`)。
|
|
75
|
-
- **`src/`
|
|
76
|
-
|
|
78
|
+
- **`src/` 下的顶层目录必须是一个模块**(`api/manifest.yaml` 或 `web/entry.ts`):
|
|
79
|
+
脚本 / 共享库请放在 `src/` 之外。应用容器 `src/app` 是唯一例外——它的 `api/<name>/`
|
|
80
|
+
子目录各自是独立模块(模块名 = 子目录名 = URL 前缀,oj 的 `--module-root` 递归发现)。
|
|
81
|
+
- **前端 client 入口是 `__client/index.ts`**(0.2.54 起,旧名 `api.ts` 会被 oj dev
|
|
82
|
+
当路由扫描)。页面导入写 `from "../__client"`。
|
|
77
83
|
|
|
78
|
-
> 模板里还有一批 demo-only 模块(`src/
|
|
84
|
+
> 模板里还有一批 demo-only 模块(`src/app/api/home`、`src/{about,access,exception,
|
|
79
85
|
> outside,route-nest}/web`)仅供模板自演示,**不进脚手架**;其 `web.config.ts` 条目带
|
|
80
86
|
> `// #region demo-only` 标记,init 拷贝时整段剥离。新布局的清单是「扫描 + 覆盖」并集,
|
|
81
87
|
> 注释掉不会让模块消失——要「不构建」必须写 `enabled: false`。
|
|
@@ -100,7 +106,7 @@
|
|
|
100
106
|
> **更早建好的 dev 库里仍是旧口令**——拿 `aA123456` 登不上时先怀疑这个,别去查登录链路。
|
|
101
107
|
> 要统一:删 `db.sqlite` 重跑 `oj migrate`(seed 随服务启动重放),或直接
|
|
102
108
|
> `UPDATE users SET password_hash = '<aA123456 的哈希>' WHERE username IN ('superadmin','admin','editor','viewer');`。
|
|
103
|
-
> 哈希现成值从 `src/
|
|
109
|
+
> 哈希现成值从 `src/app/api/_platform/seed.sql` 里 grep `$2y$10$` 即可。
|
|
104
110
|
|
|
105
111
|
端口:前端 devServer **5174**,oj 后端 **9778**
|
|
106
112
|
(以工程根 `config.yaml` 的 `server.port` 为准)。
|
|
@@ -154,9 +160,9 @@
|
|
|
154
160
|
src/<模块>/api/<路径...>/api.ts → /api/<模块>/<路径...>
|
|
155
161
|
```
|
|
156
162
|
|
|
157
|
-
`src/
|
|
163
|
+
`src/app/api/web/hello/api.ts` → `GET|POST /api/web/hello`。
|
|
158
164
|
|
|
159
|
-
注意中间的 `api` 是**布局分隔符,不是路由段**(`src/
|
|
165
|
+
注意中间的 `api` 是**布局分隔符,不是路由段**(`src/app/api/web/hello` ↔ `/api/web/hello`,
|
|
160
166
|
不是 `/api/web/api/hello`)——`ojm` 的契约扫描按「模块的 api 侧目录」逐模块进行,
|
|
161
167
|
不会把 `src` 整棵当扫描根(历史布局同理:`api/src/web/hello/api.ts`)。
|
|
162
168
|
|
|
@@ -217,13 +223,14 @@ src/<模块>/api/<路径...>/api.ts → /api/<模块>/<路径...>
|
|
|
217
223
|
|
|
218
224
|
| 文件位置 | 到 `_platform/_shared` 的写法 |
|
|
219
225
|
|---|---|
|
|
220
|
-
| `src/
|
|
221
|
-
| `src/
|
|
222
|
-
| `src/
|
|
223
|
-
| `src/
|
|
224
|
-
| `src/
|
|
225
|
-
|
|
226
|
-
> 历史布局把 `src/<m>/api` 换成 `api/src/<m>`
|
|
226
|
+
| `src/app/api/system/<x>/api.ts` | `../../_platform/_shared/...` |
|
|
227
|
+
| `src/app/api/system/<x>/<y>/api.ts` | `../../../_platform/_shared/...` |
|
|
228
|
+
| `src/app/api/auth/<z>/api.ts` | `../../_platform/_shared/...` |
|
|
229
|
+
| `src/app/api/_platform/<x>/api.ts` | `../_shared/...` |
|
|
230
|
+
| `src/app/api/_platform/<x>/<y>/api.ts` | `../../_shared/...` |
|
|
231
|
+
|
|
232
|
+
> 历史布局把 `src/<m>/api` 换成 `api/src/<m>` 后同构,深度不变;内置模块收进
|
|
233
|
+
> `src/app/api/<m>` 后比原 `src/<m>/api` **少一层**(上表已按新布局校准)。
|
|
227
234
|
3. **模块级可变状态不跨请求存活。** oj **每请求重建 handler 模块实例**(实测:模块级
|
|
228
235
|
计数器连打三次恒为初值)——`let` / `Map` / `Set` 级状态一律视为**单请求作用域**。
|
|
229
236
|
需要跨请求的状态(限速、节流、去重窗口、判权缓存)**必须落库**或走 oj 持久面。
|
|
@@ -287,7 +294,7 @@ src/<模块名>/web/
|
|
|
287
294
|
> 就是忘了登记,结果模块安静地没加载、按钮安静地灰着,**全程没有一个报错**。
|
|
288
295
|
|
|
289
296
|
**建档顺序很重要**(有接口的模块):先写 handler 的 `.schema` → 跑 `oj openapi` + `ojm api` → 再写 `entry.ts`。
|
|
290
|
-
生成前 `import { createXxxClient } from "./__client
|
|
297
|
+
生成前 `import { createXxxClient } from "./__client"` 必然 ts 报红,
|
|
291
298
|
那不是你写错了,是产物还没生成。完整流水线见 §3.4。
|
|
292
299
|
|
|
293
300
|
```ts
|
|
@@ -316,7 +323,7 @@ import { BookOutlined } from "@ant-design/icons";
|
|
|
316
323
|
import { defineModule } from "@oj-module/runtime";
|
|
317
324
|
import { createElement, lazy } from "react";
|
|
318
325
|
|
|
319
|
-
import { createBooksClient } from "./__client
|
|
326
|
+
import { createBooksClient } from "./__client";
|
|
320
327
|
|
|
321
328
|
const Books = lazy(() => import("./pages/index"));
|
|
322
329
|
|
|
@@ -483,7 +490,7 @@ const roles = useUserStore(s => s.roles);
|
|
|
483
490
|
|
|
484
491
|
**写(改密/改角色/改设置)→ 走契约 API client,不碰框架内部**
|
|
485
492
|
|
|
486
|
-
- 框架已把 `auth/*`、`system/*`、`web/*` 等端点经 OpenAPI 契约定义,并从 `@oj-module/runtime` 导出类型化 fetch(如 `api/rbac` 的 `fetchChangePassword`、`fetchRbacUpdateUserRoles`,`api/user` 的 `fetchUserInfo`)。要调框架内置写接口,直接复用这些导出。**但更推荐模块 own 自己的契约**:在**自己模块的 handler 动词**上写 `.schema`,跑 `oj openapi` + `ojm api` 生成 `src/<模块>/web/__client/
|
|
493
|
+
- 框架已把 `auth/*`、`system/*`、`web/*` 等端点经 OpenAPI 契约定义,并从 `@oj-module/runtime` 导出类型化 fetch(如 `api/rbac` 的 `fetchChangePassword`、`fetchRbacUpdateUserRoles`,`api/user` 的 `fetchUserInfo`)。要调框架内置写接口,直接复用这些导出。**但更推荐模块 own 自己的契约**:在**自己模块的 handler 动词**上写 `.schema`,跑 `oj openapi` + `ojm api` 生成 `src/<模块>/web/__client/index.ts`,模块经自己的 client 调框架端点——接口由契约(versioned OpenAPI)对齐,而非把框架源文件打进来。
|
|
487
494
|
- 底层都走同一个 `request` 单例 + 契约 URL;模块侧请求被 `defineModule` 的 `ctx` 收敛到登记过的 `apiPrefix`,越界 / `../` 穿越 / 逐请求 prefix 覆盖均被拒(请求边界见 §2.6 + `tests/runtime/scoped-request.test.ts`)。
|
|
488
495
|
|
|
489
496
|
**场景:模块记录自己的操作日志(写端)**
|
|
@@ -537,7 +544,7 @@ oj openapi -c config.yaml --module-root src -b /api -o openapi.json → OpenA
|
|
|
537
544
|
ojm api → irFromOpenapi → emitZodFromJsonSchema / emitClient
|
|
538
545
|
│
|
|
539
546
|
▼
|
|
540
|
-
src/<模块>/web/__client/{
|
|
547
|
+
src/<模块>/web/__client/{index.ts, api.schemas.ts}(+ stub / mock)
|
|
541
548
|
```
|
|
542
549
|
|
|
543
550
|
`openapi.json` 即权威文档源,`ojm api --docs` 直接聚合它渲染文档站。`.schema` / `.response`
|
|
@@ -556,7 +563,7 @@ src/<模块>/web/__client/{api.ts, api.schemas.ts}(+ stub / mock)
|
|
|
556
563
|
|
|
557
564
|
| 工程形态 | 权威源 | 产物流向 |
|
|
558
565
|
|---|---|---|
|
|
559
|
-
| **module-root**(`ojm init` 产物,推荐) | 后端 `src/<模块>/api/**/api.ts` 上的 `.schema` | `src/<模块>/web/__client/{
|
|
566
|
+
| **module-root**(`ojm init` 产物,推荐) | 后端 `src/<模块>/api/**/api.ts` 上的 `.schema` | `src/<模块>/web/__client/{index.ts, api.schemas.ts}`(由 `ojm api` 消费 `openapi.json` 生成);stub → `src/<模块>/api/<路径>/api.ts`(未实现 handler 时补骨架) |
|
|
560
567
|
| **uni-dev**(历史布局) | 后端 `api/src/<模块>/**/api.ts` 上的 `.schema` | `web/src/<模块>/__client/…`;stub → `api/src/<模块>/<路径>/api.ts` |
|
|
561
568
|
| 纯前端 | 仅消费他人 `openapi.json` | `__client/` 全部落前端侧,无 stub |
|
|
562
569
|
|
|
@@ -569,7 +576,7 @@ src/<模块>/web/__client/{api.ts, api.schemas.ts}(+ stub / mock)
|
|
|
569
576
|
以目录名作 `MODULE_PREFIX`(跨模块认领护栏的对比基准),只改一边会让 dev 控制台报「正在认领
|
|
570
577
|
别人的 client」。与旧 `apiPrefix` 字面相等约束同源,只是不再手写字段。
|
|
571
578
|
|
|
572
|
-
> 工程里有两个 "web":前端侧 `src/web/web/`,与后端 `src/
|
|
579
|
+
> 工程里有两个 "web":前端侧 `src/app/web/web/`,与后端 `src/app/api/web/`(提供 user-info 等内置接口)。
|
|
573
580
|
> 豁免清单里的 `"modules": ["web"]` 指的是**后者**。
|
|
574
581
|
|
|
575
582
|
### 3.3 handler `.schema`(入参)+ `.response`(出参)写法(JSON Schema 子集)
|
|
@@ -636,7 +643,7 @@ minLength / maxLength / pattern / enum / nullable / minItems / maxItems / defaul
|
|
|
636
643
|
**上传文件(multipart/form-data)**:当前 `ojm api` 的 openapi 流水线**不生成** form 通道的
|
|
637
644
|
前端 client(文件部件是二进制,不进 JSON Schema)。惯例走 antd `Upload` **直连**后端
|
|
638
645
|
(不经 JSON client),handler 用 `http.files` / `http.file(i)` 读字节,仅平级声明
|
|
639
|
-
`.response`(见 `src/personal-center/
|
|
646
|
+
`.response`(见 `src/app/api/personal-center/upload/api.ts` 范本)。
|
|
640
647
|
|
|
641
648
|
**不要手写 `content-type`**:运行时(ky)见到 `FormData` 会自己补 `multipart/form-data;
|
|
642
649
|
boundary=…`,手写反而丢 boundary。
|
|
@@ -646,7 +653,7 @@ boundary=…`,手写反而丢 boundary。
|
|
|
646
653
|
|
|
647
654
|
### 3.4 生成物使用契约
|
|
648
655
|
|
|
649
|
-
生成的 `src/<模块>/web/__client/
|
|
656
|
+
生成的 `src/<模块>/web/__client/index.ts` 导出:
|
|
650
657
|
|
|
651
658
|
- 裸请求函数(`fetchNotifications`、`markRead`…)——**直接 import 调用**;
|
|
652
659
|
- `API_PREFIX` 常量——契约前缀的唯一真源;
|
|
@@ -672,7 +679,7 @@ const booksClient = createBooksClient(ctx);
|
|
|
672
679
|
> 目录却不在」只有一种解释:那个模块的 `__client/` 从没生成过(模板里的
|
|
673
680
|
> personal-center 上传走 antd Upload 直连,不经过 JSON client,故没人 import 它的 `__client`)。
|
|
674
681
|
> 你在自己工程跑一次 `ojm api`,它就会出现。
|
|
675
|
-
> 判断某模块**要不要用** client 的标准只有一个:代码里有没有人 import `./__client
|
|
682
|
+
> 判断某模块**要不要用** client 的标准只有一个:代码里有没有人 import `./__client`。
|
|
676
683
|
> 有就在 onInit 里 `create<Module>Client(ctx)` 一次;没有就什么都不写,
|
|
677
684
|
> 生成了但没人 import 也不报错。
|
|
678
685
|
|
|
@@ -683,7 +690,7 @@ const booksClient = createBooksClient(ctx);
|
|
|
683
690
|
|
|
684
691
|
### 3.5 生成物勿手改
|
|
685
692
|
|
|
686
|
-
`__client/
|
|
693
|
+
`__client/index.ts`、`__client/api.schemas.ts` **重跑即覆盖**(eslint 已忽略这些路径)。
|
|
687
694
|
改需求 = 改 handler 的 `.schema`(或路由),重跑 `oj openapi` + `ojm api`。
|
|
688
695
|
|
|
689
696
|
**唯一例外:stub。** `src/<模块>/api/**/api.ts` 的 stub 文件头带指纹
|
|
@@ -832,7 +839,7 @@ export default { get };
|
|
|
832
839
|
3. **生成规范**:`oj openapi -c config.yaml --module-root src -b /api -o openapi.json`
|
|
833
840
|
(历史布局用 `-d api/src`;项目级单文件、含全部模块;组级 `.schema`/`.response` 已由
|
|
834
841
|
oj 内省期合并)。
|
|
835
|
-
4. **生成 client**:`ojm api` → `src/<模块>/web/__client/{
|
|
842
|
+
4. **生成 client**:`ojm api` → `src/<模块>/web/__client/{index.ts, api.schemas.ts}`
|
|
836
843
|
(历史布局 `web/src/<模块>/__client/`)。
|
|
837
844
|
5. **改前端消费点**:函数名从旧人类命名(`fetchNotifications`)改为
|
|
838
845
|
`camelCase(operationId)`(`notificationNotificationsGet`)——用「逐字节比对」识别,
|
|
@@ -904,9 +911,9 @@ Provider 就是:「这块别用框架自带的了,换成我的实现。」
|
|
|
904
911
|
|
|
905
912
|
### 4.4 案例:接管通知功能(标准四文件)
|
|
906
913
|
|
|
907
|
-
**① handler `.schema`**(`src/
|
|
914
|
+
**① handler `.schema`**(`src/app/api/notification/api.ts` 上各动词挂的 `.schema`)——见 §3.3 与 §3.4。
|
|
908
915
|
|
|
909
|
-
**② handler**(`src/
|
|
916
|
+
**② handler**(`src/app/api/notification/api.ts`,模块根嵌套组——组名 = 路径段,oj v0.1.64):
|
|
910
917
|
|
|
911
918
|
```ts
|
|
912
919
|
const notifications = {
|
|
@@ -939,7 +946,7 @@ export default { notifications, /* push, "push-broadcast" 等同理 */ };
|
|
|
939
946
|
```ts
|
|
940
947
|
import type { NotificationsApiProvider } from "@oj-module/runtime";
|
|
941
948
|
import { defineModule } from "@oj-module/runtime";
|
|
942
|
-
import { createNotificationClient } from "./__client
|
|
949
|
+
import { createNotificationClient } from "./__client";
|
|
943
950
|
|
|
944
951
|
export default defineModule({
|
|
945
952
|
name: "notification",
|
|
@@ -964,7 +971,7 @@ export default defineModule({
|
|
|
964
971
|
**⑤ 跨模块发射原语(任何后端模块都能推通知)**
|
|
965
972
|
|
|
966
973
|
通知不是「只有通知模块自己用」的功能——下单、审核、@我等其它模块也需要给某个人推一条提醒。
|
|
967
|
-
把发射逻辑收敛成 `src/
|
|
974
|
+
把发射逻辑收敛成 `src/app/api/notification/_shared/notify.ts` 里的纯函数原语,其它模块**别名导入
|
|
968
975
|
直接调用,无需走 HTTP**:
|
|
969
976
|
|
|
970
977
|
```ts
|
|
@@ -997,7 +1004,7 @@ export default {
|
|
|
997
1004
|
不带 `node_modules`——dev 能跑、release 或装到站点后必炸。后端共享代码放本模块
|
|
998
1005
|
`api/_shared/`,跨模块走 `#/<模块名>/_shared/...` 别名;类型与 runtime 的
|
|
999
1006
|
`@oj-module/runtime/notify` 保持同形即可(共享契约类型单一真源在 runtime)。
|
|
1000
|
-
- **`_shared` 必须嵌进所属模块的 api 侧**(`src/
|
|
1007
|
+
- **`_shared` 必须嵌进所属模块的 api 侧**(`src/app/api/notification/_shared/`):
|
|
1001
1008
|
`src/` 下每个目录都得是模块(`api/manifest.yaml` 或 `web/entry.ts`),`src/_shared` 会被
|
|
1002
1009
|
`ojm dev/build` 判成坏目录;放进模块内则是「无 `api.ts` 的纯工具目录」,不产生路由。
|
|
1003
1010
|
(历史布局同理:`api/src/notification/_shared/`,而 `api/src/_shared` 会被 oj 当成缺
|
|
@@ -1563,7 +1570,7 @@ export default async (path: string, html: string) => {
|
|
|
1563
1570
|
| 登录 401 且 msg 不是 `invalid credentials` | `/auth/*` 不在 `anonymous_paths` | 加进工程根 `config.yaml`,重启 |
|
|
1564
1571
|
| 登出/回登录页空白或 404 | 工程缺 `login` 模块(shell 无内置登录回落) | 在 `web.config.ts` 保留 `login` |
|
|
1565
1572
|
| 登录后 / 点 logo 落 React Router 错误边界 | 工程缺 `home` 模块 | 在 `web.config.ts` 保留 `home`(尽量靠前) |
|
|
1566
|
-
| 通知铃 404 `no route matched`(`/api/notifications`) | 前端 `notification` 模块没加载 / 没注册 `notificationsApi`,回落到内置只读兜底——而脚手架不再提供 root 级 `/api/notifications` 模块 | 保留 `web.config.ts` 的 `notification` 登记,onInit 里注册四方法(模板 `src/app/web/notification/entry.ts`);后端由 `src/
|
|
1573
|
+
| 通知铃 404 `no route matched`(`/api/notifications`) | 前端 `notification` 模块没加载 / 没注册 `notificationsApi`,回落到内置只读兜底——而脚手架不再提供 root 级 `/api/notifications` 模块 | 保留 `web.config.ts` 的 `notification` 登记,onInit 里注册四方法(模板 `src/app/web/notification/entry.ts`);后端由 `src/app/api/notification` 提供 `/api/notification/*` |
|
|
1567
1574
|
| `@types/react` / `typescript` 装成 `*` | 宿主 versions.json 未列 | 安装后按实际版本钉死(`*` 不可复现) |
|
|
1568
1575
|
| 迁移账本落后、服务拒绝启动 | release `verify` 闸 | 先 `ojm migrate -d api/dist`(透传自动补 `-c`) |
|
|
1569
1576
|
| `seed.sql` 没生效 / 语法错 | 按 `;` 朴素切分,注释里有分号 | 去掉注释里的分号字面量 |
|
|
@@ -1590,11 +1597,11 @@ export default async (path: string, html: string) => {
|
|
|
1590
1597
|
| 接管通知 provider(四方法全量) | `src/app/web/notification/entry.ts` |
|
|
1591
1598
|
| 上传 provider(action + headers 方法) | `src/app/web/personal-center/entry.ts` |
|
|
1592
1599
|
| 无页面模块(`routes: []`) | 同 `src/app/web/notification/entry.ts` |
|
|
1593
|
-
| handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `src/
|
|
1594
|
-
| handler 写法(嵌套组列表 / 写入 / 403 归属) | `src/
|
|
1595
|
-
| 统一异常出口 `respond()`(service 抛 `Error & {status}` → 信封 `json.fail`) | 模板 `src/
|
|
1596
|
-
| 生成物长什么样(含工厂) | `src/app/web/notification/__client/
|
|
1597
|
-
| 建表三件套 | `src/
|
|
1600
|
+
| handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `src/app/api/notification/api.ts` |
|
|
1601
|
+
| handler 写法(嵌套组列表 / 写入 / 403 归属) | `src/app/api/notification/api.ts`(模块根单文件,`read`/`read-all`/`clear` 子组) |
|
|
1602
|
+
| 统一异常出口 `respond()`(service 抛 `Error & {status}` → 信封 `json.fail`) | 模板 `src/app/api/_platform/_shared/respond.ts` + 各 handler 用法(§1.7-1) |
|
|
1603
|
+
| 生成物长什么样(含工厂) | `src/app/web/notification/__client/index.ts` |
|
|
1604
|
+
| 建表三件套 | `src/app/api/notification/{schema.yaml,manifest.yaml,migrations/}` |
|
|
1598
1605
|
| 全局类型(`window.$message` 等) | 工程根 `env.d.ts` |
|
|
1599
1606
|
|
|
1600
1607
|
另有:
|
|
@@ -2016,7 +2023,7 @@ ojm trust revoke <key_id> # 吊销:阻断新装/升级,不动已装
|
|
|
2016
2023
|
|
|
2017
2024
|
### 12.3 第一方 bootstrap key(首装免 TOFU)
|
|
2018
2025
|
|
|
2019
|
-
平台模板内置第一方发布方**公钥**(`src/
|
|
2026
|
+
平台模板内置第一方发布方**公钥**(`src/app/api/_platform/market/bootstrap.ts` 的
|
|
2020
2027
|
`BOOTSTRAP_KEY`):信任库文件首次初始化时自动播种一次——**market 方案首装免 TOFU 直接过
|
|
2021
2028
|
验签**。发布时序(平台发行人执行):`ojm keygen` → 签 oj-market 首发包 → 公钥 + key_id
|
|
2022
2029
|
回填 `BOOTSTRAP_KEY` 随模板发布。播种纪律:库已初始化(含空库)不播种——运维 `revoke`
|
|
@@ -2232,7 +2239,7 @@ async function post() {
|
|
|
2232
2239
|
所以前端消费组织架构的唯一正路是「借壳」:
|
|
2233
2240
|
|
|
2234
2241
|
1. 在**自己模块**的 handler 里调 `resolveReadFace` + 需要的那几个读函数(§14.2);
|
|
2235
|
-
2. 跑 `oj openapi` + `ojm api`,生成本模块的 `src/<模块>/web/__client/
|
|
2242
|
+
2. 跑 `oj openapi` + `ojm api`,生成本模块的 `src/<模块>/web/__client/index.ts`;
|
|
2236
2243
|
3. 页面用本模块 client 调。
|
|
2237
2244
|
|
|
2238
2245
|
多一层 handler 换来三件事:接口形状由**你自己的契约**决定(只出页面要的字段)、框架内部实现不进你的
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oj-module/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.56",
|
|
5
5
|
"description": "React Antd 模块工程命令行工具(含预构建宿主 shell-dist)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@faker-js/faker": "^10.4.0",
|
|
52
|
-
"@oj-module/runtime": "0.2.
|
|
52
|
+
"@oj-module/runtime": "0.2.56",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"cac": "^6.7.14",
|
|
@@ -276,9 +276,30 @@ function buildShellChildren() {
|
|
|
276
276
|
* 路由表;随后 SPA 登录(不刷新页面)菜单回来了,但 /system/* 仍不在路由表里,
|
|
277
277
|
* 直达恒落 catch-all 404「返回首页」,须硬刷新才恢复。
|
|
278
278
|
*/
|
|
279
|
+
/**
|
|
280
|
+
* 宿主路由根(Boot 建表与登录后重登记共用同一形状,children 按当前角色快照生成)。
|
|
281
|
+
*
|
|
282
|
+
* 路由根只渲染容器(Outlet),整站 chrome 由模块自带的 ContainerLayout 提供;
|
|
283
|
+
* 宿主不叠加自己的 Layout chrome,避免「宿主侧栏 + 模块侧栏」双层嵌套。
|
|
284
|
+
* element 里同时挂 LayoutEffects(暗色类/动态标题/NProgress,与 App 链同源,偏差 4)
|
|
285
|
+
* 与页面级 ErrorBoundary(模块路由不经 runtime LayoutRoot,缺它时渲染抛错整页白屏)。
|
|
286
|
+
*/
|
|
287
|
+
function buildShellRoot() {
|
|
288
|
+
return {
|
|
289
|
+
path: "/",
|
|
290
|
+
id: SHELL_ROOT_ID,
|
|
291
|
+
element: /* @__PURE__ */ jsxs(ErrorBoundary, {
|
|
292
|
+
FallbackComponent: PageError,
|
|
293
|
+
children: [/* @__PURE__ */ jsx(LayoutEffects, {}), /* @__PURE__ */ jsx(Outlet, {})]
|
|
294
|
+
}),
|
|
295
|
+
children: buildShellChildren()
|
|
296
|
+
};
|
|
297
|
+
}
|
|
279
298
|
function repatchShellRoutes() {
|
|
280
299
|
if (!shellRouter) return;
|
|
281
|
-
shellRouter.
|
|
300
|
+
const internal = shellRouter._internalSetRoutes;
|
|
301
|
+
if (internal) internal.call(shellRouter, [buildShellRoot()]);
|
|
302
|
+
else shellRouter.patchRoutes(SHELL_ROOT_ID, buildShellChildren());
|
|
282
303
|
const { pathname, search } = shellRouter.state.location;
|
|
283
304
|
shellRouter.navigate(`${pathname}${search}`, { replace: true }).catch(() => {});
|
|
284
305
|
}
|
|
@@ -313,7 +334,7 @@ function Boot() {
|
|
|
313
334
|
(async () => {
|
|
314
335
|
try {
|
|
315
336
|
if (typeof navigator !== "undefined" && /wxwork/i.test(navigator.userAgent) && location.pathname.endsWith("/login") && !new URLSearchParams(location.search).get("wecom_ticket")) {
|
|
316
|
-
window.location.assign(resolveApiBase()
|
|
337
|
+
window.location.assign(`${resolveApiBase()}/auth/wecom/login`);
|
|
317
338
|
return;
|
|
318
339
|
}
|
|
319
340
|
ensureI18n();
|
|
@@ -348,15 +369,7 @@ function Boot() {
|
|
|
348
369
|
await hydrateUser();
|
|
349
370
|
await loadAll(manifest);
|
|
350
371
|
if (cancelled) return;
|
|
351
|
-
shellRouter = createBrowserRouter([{
|
|
352
|
-
path: "/",
|
|
353
|
-
id: SHELL_ROOT_ID,
|
|
354
|
-
element: /* @__PURE__ */ jsxs(ErrorBoundary, {
|
|
355
|
-
FallbackComponent: PageError,
|
|
356
|
-
children: [/* @__PURE__ */ jsx(LayoutEffects, {}), /* @__PURE__ */ jsx(Outlet, {})]
|
|
357
|
-
}),
|
|
358
|
-
children: buildShellChildren()
|
|
359
|
-
}], {
|
|
372
|
+
shellRouter = createBrowserRouter([buildShellRoot()], {
|
|
360
373
|
/**
|
|
361
374
|
* 子路径部署(oj `mounts: [{ prefix: /oj, web: dist, spa: true }]`):
|
|
362
375
|
* 不设 basename 时 react-router 看到的是含前缀的 `/oj/wiki/...`,
|
|
@@ -304,10 +304,10 @@ function getAppInfo() {
|
|
|
304
304
|
return {
|
|
305
305
|
"pkg": {
|
|
306
306
|
"name": "@oj-module/runtime",
|
|
307
|
-
"version": "0.2.
|
|
307
|
+
"version": "0.2.56",
|
|
308
308
|
"license": "MIT"
|
|
309
309
|
},
|
|
310
|
-
"lastBuildTime": "2026-11-02
|
|
310
|
+
"lastBuildTime": "2026-11-02 23:15:22"
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
var init_get_app_info = __esmMin((() => {}));
|
package/shell-dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="/favicon.ico" />
|
|
7
7
|
<title>React Antd Admin · Shell</title>
|
|
8
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-
|
|
9
|
-
<script type="importmap" nonce="
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-oa6e5O+MEhBiegxvbtPppw=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
|
|
9
|
+
<script type="importmap" nonce="oa6e5O+MEhBiegxvbtPppw==">{
|
|
10
10
|
"imports": {
|
|
11
11
|
"react": "/assets/react.js",
|
|
12
12
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
.boot-skeleton:after{content:"";width:18px;height:18px;margin-left:8px;border:2px solid #d9d9d9;border-top-color:#1677ff;border-radius:50%;animation:bs-spin .8s linear infinite}
|
|
131
131
|
@keyframes bs-spin{to{transform:rotate(360deg)}}
|
|
132
132
|
</style>
|
|
133
|
-
<script nonce="
|
|
133
|
+
<script nonce="oa6e5O+MEhBiegxvbtPppw==" data-ojm-entry="/assets/index-CImwskAz.js">(function(){try{var ua=(navigator.userAgent||"").toLowerCase(),p=location.pathname;while(p.length>1&&p.charAt(p.length-1)==="/")p=p.slice(0,-1);if(ua.indexOf("wxwork")>=0&&p.slice(-6)==="/login"&&!new URLSearchParams(location.search).get("wecom_ticket")){var a=(typeof window.__OJ_API_BASE__==="string"&&window.__OJ_API_BASE__.trim())||"/api";while(a.length>1&&a.charAt(a.length-1)==="/")a=a.slice(0,-1);location.replace(a+"/auth/wecom/login");return}}catch(e){}var s=document.createElement("script");s.type="module";s.src=document.currentScript.getAttribute("data-ojm-entry");document.head.appendChild(s);})();</script>
|
|
134
134
|
</head>
|
|
135
135
|
<body>
|
|
136
136
|
<div id="root"><div class="boot-skeleton">加载中…</div></div>
|
package/shell-dist/versions.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"react-router": "7.18.2",
|
|
8
8
|
"react-router/dom": "7.18.2",
|
|
9
9
|
"@tanstack/react-query": "5.102.8",
|
|
10
|
-
"@oj-module/runtime": "0.2.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.56",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.56",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.56",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -111,7 +111,7 @@ describe("resolveLayout / 历史布局", () => {
|
|
|
111
111
|
it("web/<name>/entry.ts 识别为 flat,web 产物落 build/(框架根仓约定不变),api 产物归 dist/api", () => {
|
|
112
112
|
const root = newRoot();
|
|
113
113
|
writeFile(root, "web/home/entry.ts", "export default {};\n");
|
|
114
|
-
writeFile(root, "web/home/__client/
|
|
114
|
+
writeFile(root, "web/home/__client/index.ts", "export {};\n");
|
|
115
115
|
|
|
116
116
|
const layout = resolveLayout(root);
|
|
117
117
|
expect(layout.kind).toBe("flat");
|
|
@@ -177,3 +177,32 @@ describe("路径取值函数", () => {
|
|
|
177
177
|
expect(webSrcOf(layout, "wiki")).toBe(join(layout.modulesSrc, "wiki", "web"));
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 聚合容器:src/app 同时有 web 侧(多根前端模块)与 api 下多个后端模块。
|
|
183
|
+
* 模块身份取 api 下那层目录名(= URL 前缀),与平铺 src/<name>/api 并存且互不干扰。
|
|
184
|
+
*/
|
|
185
|
+
describe("resolveLayout / 聚合容器(src/app/api/<name>)", () => {
|
|
186
|
+
it("api 子目录各自成模块,web 侧仍是 app 模块", () => {
|
|
187
|
+
const root = newRoot();
|
|
188
|
+
writeFile(root, "src/app/api/system/manifest.yaml", "name: system\nversion: 0.1.0\n");
|
|
189
|
+
writeFile(root, "src/app/api/system/users/api.ts", "export default { get() {} };\n");
|
|
190
|
+
writeFile(root, "src/app/api/notification/manifest.yaml", "name: notification\nversion: 0.1.0\n");
|
|
191
|
+
writeFile(root, "src/app/web/entry.ts", "export default {};\n");
|
|
192
|
+
const layout = resolveLayout(root);
|
|
193
|
+
const mods = scanModules(layout);
|
|
194
|
+
|
|
195
|
+
expect(mods.map(m => m.name)).toEqual(["app", "notification", "system"]);
|
|
196
|
+
expect(mods.find(m => m.name === "system")?.apiSrc).toBe(join(root, "src/app/api/system"));
|
|
197
|
+
expect(mods.find(m => m.name === "system")?.webSrc).toBeUndefined();
|
|
198
|
+
expect(apiSrcOf(layout, "system")).toBe(join(root, "src/app/api/system"));
|
|
199
|
+
expect(webSrcOf(layout, "app")).toBe(join(root, "src/app/web"));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("同名后端模块重复(旧 src/system/api + 新 src/app/api/system)→ 明确报错", () => {
|
|
203
|
+
const root = newRoot();
|
|
204
|
+
writeFile(root, "src/system/api/manifest.yaml", "name: system\nversion: 0.1.0\n");
|
|
205
|
+
writeFile(root, "src/app/api/system/manifest.yaml", "name: system\nversion: 0.1.0\n");
|
|
206
|
+
expect(() => resolveLayout(root)).toThrowError(/模块名重复:system/);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { parse } from "yaml";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* BDD:模板后端「迁移落库的列」必须在所属模块 schema.yaml 的列白名单里声明。
|
|
9
|
+
*
|
|
10
|
+
* 为什么要这条:oj 的 SQL 构造器是二进制内的 Rust 实现,select/insert/update 的列会**先**按
|
|
11
|
+
* schema.yaml 校验,列在库里但不在白名单时,SQL 还没发就被拒成
|
|
12
|
+
* `unknown column 'x' in <site>`(site 随语句位置变:join on / insert values / update sets …)。
|
|
13
|
+
* 线上表现是端点 500、日志里没有 SQL,看起来像「列不存在」,实际是声明漏了。
|
|
14
|
+
*
|
|
15
|
+
* 真实事故(2026-11):_platform 0017 加了 users.employee_no,schema.yaml 漏声明,
|
|
16
|
+
* /system/departments(选人列表依赖 /system/users)整页 500。
|
|
17
|
+
*
|
|
18
|
+
* 校验面取 `ALTER TABLE … ADD COLUMN`:已有表新增列的唯一入口,也是最容易「只写迁移、
|
|
19
|
+
* 忘了补声明」的一类。整表声明由 t-2(租户定性)与表数快照兜底。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../templates");
|
|
23
|
+
const srcRoot = join(templatesRoot, "src");
|
|
24
|
+
|
|
25
|
+
interface ColumnDecl {
|
|
26
|
+
type?: string
|
|
27
|
+
[key: string]: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SchemaDoc {
|
|
31
|
+
tables?: Record<string, { columns?: Record<string, ColumnDecl> }>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `ALTER TABLE <表> ADD [COLUMN] <列>`(忽略方言专属文件,sqlite 基础件是唯一真相) */
|
|
35
|
+
const ADD_COLUMN_RE = /ALTER\s+TABLE\s+[`"']?(\w+)[`"']?\s+ADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi;
|
|
36
|
+
|
|
37
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
38
|
+
for (const entry of readdirSync(dir)) {
|
|
39
|
+
const full = join(dir, entry);
|
|
40
|
+
if (statSync(full).isDirectory())
|
|
41
|
+
walk(full, out);
|
|
42
|
+
else
|
|
43
|
+
out.push(full);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 表名 → 声明的列集合(跨模块同名表由 templates-tenant-scope 的 S002 断言拦,这里取并集) */
|
|
49
|
+
function loadDeclaredColumns(files: string[]): Map<string, Set<string>> {
|
|
50
|
+
const declared = new Map<string, Set<string>>();
|
|
51
|
+
for (const file of files.filter(f => f.endsWith("schema.yaml"))) {
|
|
52
|
+
const doc = parse(readFileSync(file, "utf-8")) as SchemaDoc | null;
|
|
53
|
+
for (const [table, decl] of Object.entries(doc?.tables ?? {}))
|
|
54
|
+
declared.set(table, new Set(Object.keys(decl.columns ?? {})));
|
|
55
|
+
}
|
|
56
|
+
return declared;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 基础迁移文件:`migrations/*.sql`,排除 `<名>.mysql.sql` / `<名>.pg.sql` */
|
|
60
|
+
function isBaseMigration(file: string): boolean {
|
|
61
|
+
return /[/\\]migrations[/\\][^/\\]+\.sql$/.test(file) && !/\.(?:mysql|pg)\.sql$/.test(file);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("templates 后端 schema.yaml 列白名单", () => {
|
|
65
|
+
it("s-1 迁移里 ADD COLUMN 的列都在 schema.yaml 有声明(漏声明 = 构造器拒查)", () => {
|
|
66
|
+
const files = walk(srcRoot);
|
|
67
|
+
const declared = loadDeclaredColumns(files);
|
|
68
|
+
const violations: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const file of files.filter(isBaseMigration)) {
|
|
71
|
+
for (const m of readFileSync(file, "utf-8").matchAll(ADD_COLUMN_RE)) {
|
|
72
|
+
const [, table, column] = m;
|
|
73
|
+
const where = relative(templatesRoot, file);
|
|
74
|
+
const cols = declared.get(table);
|
|
75
|
+
if (!cols) {
|
|
76
|
+
violations.push(`${where}: 表 ${table} 未在任何 schema.yaml 声明`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!cols.has(column)) {
|
|
80
|
+
violations.push(
|
|
81
|
+
`${where}: ${table}.${column} 未在 schema.yaml 声明`
|
|
82
|
+
+ `(构造器会拒为 unknown column '${column}' in on '${table}';补到 owning 模块的 tables.${table}.columns)`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(violations).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("s-2 回归锚点:0017 三方言迁移齐备,users.employee_no 已声明为 text", () => {
|
|
92
|
+
const migDir = join(srcRoot, "app", "api", "_platform", "migrations");
|
|
93
|
+
for (const file of ["0017__users_employee_no.sql", "0017__users_employee_no.mysql.sql", "0017__users_employee_no.pg.sql"])
|
|
94
|
+
expect(existsSync(join(migDir, file)), `缺迁移 ${file}`).toBe(true);
|
|
95
|
+
|
|
96
|
+
const schema = parse(readFileSync(join(srcRoot, "app", "api", "_platform", "schema.yaml"), "utf-8")) as SchemaDoc;
|
|
97
|
+
expect(schema.tables?.users?.columns?.employee_no).toMatchObject({ type: "text" });
|
|
98
|
+
});
|
|
99
|
+
});
|