@oj-module/cli 0.2.53 → 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 +4 -4
- package/devkit/manual.md +79 -65
- package/package.json +2 -2
- package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
- package/shell-dist/assets/runtime.js +7 -4
- 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/__tests__/templates-tenant-scope.test.ts +3 -2
- package/src/alias-paths.ts +11 -12
- package/src/builtin.ts +10 -3
- package/src/config.ts +9 -0
- package/src/contract/__tests__/client-path.test.ts +30 -0
- package/src/contract/__tests__/emit-client.test.ts +20 -2
- package/src/contract/__tests__/handler-scan.test.ts +23 -0
- 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 +11 -7
- package/src/contract/client-path.ts +30 -0
- package/src/contract/emit-client.ts +27 -6
- package/src/contract/handler-scan.ts +40 -3
- package/src/contract/run.ts +15 -5
- package/src/init.ts +29 -3
- 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 +49 -43
- package/templates/init.include.yaml +31 -19
- package/templates/openapi.json +1488 -120
- 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/{_platform/api → app/api/_platform}/contacts/api.ts +29 -1
- package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +25 -1
- 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/{_platform/api → app/api/_platform}/solutions/api.ts +60 -1
- 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/{auth/api → app/api/auth}/api.ts +59 -0
- package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +37 -1
- package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +23 -0
- package/templates/src/{notification/api → app/api/notification}/api.ts +17 -0
- package/templates/src/{notification/api → app/api/notification}/del/api.ts +2 -0
- package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
- package/templates/src/{system/api → app/api/system}/roles/api.ts +3 -0
- package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +3 -0
- package/templates/src/{system/api → app/api/system}/settings/api.ts +3 -0
- package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +3 -0
- package/templates/src/{system/api → app/api/system}/users/password/api.ts +3 -0
- package/templates/src/{system/api → app/api/system}/users/roles/api.ts +3 -0
- package/templates/src/app/api/web/get-async-routes/api.ts +23 -0
- package/templates/src/app/api/web/hello/api.ts +22 -0
- package/templates/src/{web/api → app/api/web}/user-info/api.ts +21 -1
- package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +99 -35
- package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +108 -61
- package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +133 -13
- package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +134 -33
- package/templates/src/app/web/entry.ts +77 -0
- package/templates/src/{home/web/__client/api.ts → app/web/home/__client/index.ts} +5 -2
- package/templates/src/app/web/home/routes.ts +41 -0
- package/templates/src/app/web/login/routes.ts +71 -0
- package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
- package/templates/src/{notification/web/__client/api.ts → app/web/notification/__client/index.ts} +67 -20
- package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +1 -1
- package/templates/src/app/web/notification/routes.ts +70 -0
- package/templates/src/{personal-center/web/__client/api.ts → app/web/personal-center/__client/index.ts} +5 -2
- package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +1 -1
- package/templates/src/app/web/personal-center/routes.ts +49 -0
- package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +29 -22
- package/templates/src/{system/web/__client/api.ts → app/web/system/__client/index.ts} +103 -58
- package/templates/src/{system/web → app/web/system}/locales/en-US.json +2 -3
- package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +2 -3
- package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +2 -2
- package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +1 -1
- package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +44 -19
- package/templates/src/{system/web → 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/{system/web → 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/{system/web → app/web/system}/pages/users/index.tsx +23 -15
- package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +1 -1
- package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +12 -3
- package/templates/src/{system/web/entry.ts → app/web/system/routes.ts} +25 -36
- package/templates/src/app/web/web/__client/api.schemas.ts +36 -0
- package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +38 -11
- 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 +24 -15
- package/vendor/host-versions.json +3 -3
- package/templates/src/home/web/entry.ts +0 -49
- package/templates/src/login/web/entry.ts +0 -79
- package/templates/src/notification/web/entry.ts +0 -84
- package/templates/src/personal-center/web/entry.ts +0 -60
- package/templates/src/web/api/get-async-routes/api.ts +0 -8
- package/templates/src/web/api/hello/api.ts +0 -10
- package/templates/src/web/web/__client/api.schemas.ts +0 -8
- 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}/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/{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}/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/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}/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}/users/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
- /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
- /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
- /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
- /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
- /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
- /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/pages/login.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
- /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
- /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
|
@@ -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
|
+
});
|
|
@@ -115,7 +115,7 @@ describe("templates 租户定性(A 租户表 / B 共享表)", () => {
|
|
|
115
115
|
expect(unknown).toEqual([]);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it("t-4
|
|
118
|
+
it("t-4 27 张表的 A/B 分组与方案 v3 §5.2 一致(新增表须先定性)", () => {
|
|
119
119
|
const { tables } = loadSchemaTables();
|
|
120
120
|
const shared = [...tables].filter(([, d]) => d.tenant === false).map(([n]) => n).sort();
|
|
121
121
|
// Phase 0 落地 21 张(11 A + 10 B);Phase 1 加企微三表,全部 B 组(D9:查它们时必然还没有租户上下文)
|
|
@@ -124,7 +124,8 @@ describe("templates 租户定性(A 租户表 / B 共享表)", () => {
|
|
|
124
124
|
// auth/0002 加 wecom_sync_job / wecom_sync_lock 两张,同属 A 组:同步只在调用方自己的租户内跑
|
|
125
125
|
// (D7 不接受 corpId 参数),故不进 shared_allow——跨租户读元数据那条另走显式 tenant_id 的裸 SQL。
|
|
126
126
|
// 改这张表清单 = 改租户隔离边界,必须同步 config.yaml 的 tenant.shared_allow(t-2/t-3 会把关)。
|
|
127
|
-
|
|
127
|
+
// 2026-11-02 demo 模块下线(ecc7b35e)删掉 demo/api/schema.yaml 的 todos 表,28 → 27。
|
|
128
|
+
expect(tables.size).toBe(27);
|
|
128
129
|
expect(shared).toEqual([
|
|
129
130
|
"_oj_migrations",
|
|
130
131
|
"market_history",
|
package/src/alias-paths.ts
CHANGED
|
@@ -20,18 +20,18 @@ import type { ProjectLayout } from "./layout";
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { parse } from "yaml";
|
|
23
|
-
import { tryResolveLayout } from "./layout";
|
|
23
|
+
import { scanModules, tryResolveLayout } from "./layout";
|
|
24
24
|
|
|
25
25
|
/** 生成物文件名(工程根)。`src/tsconfig.json` 以 `../tsconfig.paths.json` 引用 */
|
|
26
26
|
export const ALIAS_PATHS_FILE = "tsconfig.paths.json";
|
|
27
27
|
|
|
28
28
|
/** 生成物头注释:tsconfig 允许注释,且这是唯一会被 `ojm` 覆写的 tsconfig */
|
|
29
29
|
const HEADER = [
|
|
30
|
-
"// 由 ojm 生成:ojm init / ojm build / ojm dev
|
|
30
|
+
"// 由 ojm 生成:ojm init / ojm build / ojm dev 会按模块清单重写本文件,请勿手改。",
|
|
31
31
|
"//",
|
|
32
|
-
"// oj 别名 `#/<模块名>/…`
|
|
33
|
-
"// paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*`
|
|
34
|
-
"// 以 TS5062 拒掉),故按模块逐条枚举。",
|
|
32
|
+
"// oj 别名 `#/<模块名>/…` 锚在目标模块根(src/<模块名>/api 或 src/app/api/<模块名>)。",
|
|
33
|
+
"// tsconfig 的 paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*`",
|
|
34
|
+
"// 会被 TS 以 TS5062 拒掉),故按模块逐条枚举。",
|
|
35
35
|
].join("\n");
|
|
36
36
|
|
|
37
37
|
/** 读模块根 manifest 的 `name`(oj 的模块身份);读不到/格式坏时回落到目录名 */
|
|
@@ -58,14 +58,13 @@ export function aliasPathEntries(layout: ProjectLayout): Record<string, string[]
|
|
|
58
58
|
if (layout.kind !== "module-root")
|
|
59
59
|
return {};
|
|
60
60
|
const entries: Record<string, string[]> = {};
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// 以扫描结果为准:既覆盖平铺 src/<name>/api,也覆盖聚合容器 src/app/api/<name>
|
|
62
|
+
for (const mod of scanModules(layout)) {
|
|
63
|
+
if (!mod.apiSrc)
|
|
63
64
|
continue;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const name = manifestName(apiDir, mod.name);
|
|
68
|
-
entries[`#/${name}/*`] = [`./${path.posix.join("src", mod.name, "api", "*")}`];
|
|
65
|
+
const name = manifestName(mod.apiSrc, mod.name);
|
|
66
|
+
const rel = path.relative(layout.projectRoot, mod.apiSrc).split(path.sep).join("/");
|
|
67
|
+
entries[`#/${name}/*`] = [`./${rel}/*`];
|
|
69
68
|
}
|
|
70
69
|
return entries;
|
|
71
70
|
}
|
package/src/builtin.ts
CHANGED
|
@@ -64,9 +64,16 @@ export function readBuiltinModules(projectRoot: string): Set<string> {
|
|
|
64
64
|
export function deriveBuiltinModules(include: Iterable<string>): Set<string> {
|
|
65
65
|
const mods = new Set<string>();
|
|
66
66
|
for (const p of include) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
// 聚合容器下的后端模块:src/app/api/<name>(模块名取 api 的下一段,不是容器名)
|
|
68
|
+
const nested = /^src\/[^/]+\/api\/([^/]+)(?:\/|$)/.exec(p)?.[1];
|
|
69
|
+
if (nested) {
|
|
70
|
+
mods.add(nested);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// 平铺后端 src/<name>/api 与前端 src/<name>/web/...
|
|
74
|
+
const flat = /^src\/([^/]+)\/(?:api|web)(?:\/|$)/.exec(p)?.[1];
|
|
75
|
+
if (flat)
|
|
76
|
+
mods.add(flat);
|
|
70
77
|
}
|
|
71
78
|
return mods;
|
|
72
79
|
}
|
package/src/config.ts
CHANGED
|
@@ -48,6 +48,15 @@ export interface ModulesConfig {
|
|
|
48
48
|
* `(path, html) => string | { html, cacheControl? } | Promise<…>`)。
|
|
49
49
|
*/
|
|
50
50
|
htmlTransform?: string
|
|
51
|
+
/**
|
|
52
|
+
* 生成 client 的归属模块:`api 前缀 → 承载其 __client 的 web 模块`。
|
|
53
|
+
*
|
|
54
|
+
* 缺省(不配)= 同名模块,即 `src/<api 前缀>/web/__client`(历史行为,零漂移)。
|
|
55
|
+
* 多根聚合模块把多个业务域的页面/client 收进同一个 web 模块时配它:生成物落
|
|
56
|
+
* `src/<归属模块>/web/<api 前缀>/__client`,页面相对导入 `../__client` 不变。
|
|
57
|
+
* 只影响 `ojm api` 的落盘位置与生成物里的认领护栏,不影响路由与后端。
|
|
58
|
+
*/
|
|
59
|
+
clientOwners?: Record<string, string>
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
const DEFAULT_CONFIG: Required<Pick<ModulesConfig, "baseUrl" | "siteBase">> = {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProjectLayout } from "../../layout";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { clientDirOf } from "../client-path";
|
|
5
|
+
|
|
6
|
+
const layout: ProjectLayout = {
|
|
7
|
+
kind: "module-root",
|
|
8
|
+
projectRoot: "/proj",
|
|
9
|
+
modulesSrc: "/proj/src",
|
|
10
|
+
distDir: "/proj/dist/web",
|
|
11
|
+
apiDist: "/proj/dist/api",
|
|
12
|
+
watchTarget: "/proj/src",
|
|
13
|
+
moduleRoot: "/proj/src",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("clientDirOf(client 落盘目录)", () => {
|
|
17
|
+
it("缺省 = 同名模块 src/<模块>/web/__client(历史口径零漂移)", () => {
|
|
18
|
+
expect(clientDirOf(layout, "/proj", "system")).toBe(join("/proj/src/system/web", "__client"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("clientOwners 指定归属 → src/<归属>/web/<api 前缀>/__client", () => {
|
|
22
|
+
expect(clientDirOf(layout, "/proj", "system", { system: "app" }))
|
|
23
|
+
.toBe(join("/proj/src/app/web/system", "__client"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("历史布局(layout=null)同样支持归属", () => {
|
|
27
|
+
expect(clientDirOf(null, "/proj", "system", { system: "app" }))
|
|
28
|
+
.toBe(join("/proj/web/src/app/system", "__client"));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -72,7 +72,7 @@ function emit() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
describe("emitClient (module target)", () => {
|
|
75
|
-
const { "
|
|
75
|
+
const { "index.ts": apiTs, "api.schemas.ts": schemasTs } = emit();
|
|
76
76
|
|
|
77
77
|
it("aPI_PREFIX / MODULE_PREFIX 用 JSON 字符串字面量(注入安全)", () => {
|
|
78
78
|
expect(apiTs).toContain("export const API_PREFIX = \"/wiki\";");
|
|
@@ -114,7 +114,7 @@ describe("emitClient (module target)", () => {
|
|
|
114
114
|
describe("emitClient 注入安全:模块名含特殊字符不破坏源码", () => {
|
|
115
115
|
it("module 名含 -/$ 时 MODULE_PREFIX/工厂名仍走字符串字面量(API_PREFIX 取 IR wirePrefix)", () => {
|
|
116
116
|
const ir = irFromOpenapi(DOC, { base: "/v1/api" });
|
|
117
|
-
const { "
|
|
117
|
+
const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "weird-name" });
|
|
118
118
|
// module 选项只影响 MODULE_PREFIX 与 create<Module>Client 名
|
|
119
119
|
expect(apiTs).toContain("export const MODULE_PREFIX = \"/weird-name\";");
|
|
120
120
|
expect(apiTs).toContain("export function createWeirdNameClient(");
|
|
@@ -125,6 +125,24 @@ describe("emitClient 注入安全:模块名含特殊字符不破坏源码", ()
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
describe("emitClient 多根聚合归属(owner)", () => {
|
|
129
|
+
it("owner 与 module 不同 → 落 MODULE_OWNER,护栏对比归属模块;MODULE_PREFIX 仍是 api 前缀", () => {
|
|
130
|
+
const ir = irFromOpenapi(DOC, { base: "/v1/api" });
|
|
131
|
+
const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "app" });
|
|
132
|
+
expect(apiTs).toContain("export const MODULE_PREFIX = \"/system\";");
|
|
133
|
+
expect(apiTs).toContain("export const MODULE_OWNER = \"/app\";");
|
|
134
|
+
expect(apiTs).toContain("ctx.module.name !== MODULE_OWNER.slice(1)");
|
|
135
|
+
expect(apiTs).not.toContain("ctx.module.name !== MODULE_PREFIX.slice(1)");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("owner 缺省 = module → 生成物与历史逐字一致(不出现 MODULE_OWNER)", () => {
|
|
139
|
+
const ir = irFromOpenapi(DOC, { base: "/v1/api" });
|
|
140
|
+
const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "system" });
|
|
141
|
+
expect(apiTs).not.toContain("MODULE_OWNER");
|
|
142
|
+
expect(apiTs).toContain("ctx.module.name !== MODULE_PREFIX.slice(1)");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
128
146
|
describe("emitClient 空 IR 报错", () => {
|
|
129
147
|
it("iR 为空抛错(无端点无需生成)", () => {
|
|
130
148
|
expect(() => emitClient([], { target: "module", module: "x" })).toThrow();
|
|
@@ -181,3 +181,26 @@ describe("handler-scan · 对账键与契约侧同形(参数路由精确命中
|
|
|
181
181
|
expect(pathKey(row.method, row.full)).toBe(pathKey("DELETE", "wiki/{id}"));
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
describe("handler-scan · 工厂返回本地对象(const api = { … }; return api)", () => {
|
|
186
|
+
it("回归:聚合工厂 return 标识符时仍展开动词与嵌套组(solutions/api.ts 形态)", () => {
|
|
187
|
+
const rows = scan("api.ts", `
|
|
188
|
+
import type { Env } from "./types";
|
|
189
|
+
export function createApi(env: Env) {
|
|
190
|
+
\tconst routes = { async get() {} };
|
|
191
|
+
\tconst verify = {
|
|
192
|
+
\t\tasync post() {},
|
|
193
|
+
\t\tlast: { async get() {} },
|
|
194
|
+
\t};
|
|
195
|
+
\tconst api = { routes, verify };
|
|
196
|
+
\treturn api;
|
|
197
|
+
}
|
|
198
|
+
export default createApi({} as Env);
|
|
199
|
+
`);
|
|
200
|
+
expect(rows.map(r => pathKey(r.method, handlerAbs(r))).sort()).toEqual([
|
|
201
|
+
"GET /_platform/market/routes",
|
|
202
|
+
"GET /_platform/market/verify/last",
|
|
203
|
+
"POST /_platform/market/verify",
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ScopedRequestLike 真源在 runtime contract(生成物 __client/
|
|
1
|
+
// ScopedRequestLike 真源在 runtime contract(生成物 __client/index.ts 也从这里 import,
|
|
2
2
|
// 自身不再 re-export——旧写法 type import 自 __client 会在再生后 TS2305)。
|
|
3
3
|
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
|
9
9
|
// 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
|
|
10
10
|
// 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
-
import { bindRequest } from "../../../templates/src/
|
|
12
|
+
import { bindRequest } from "../../../templates/src/app/web/_platform/__client";
|
|
13
13
|
import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
|
|
14
14
|
|
|
15
15
|
// index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
|
|
@@ -46,9 +46,9 @@ describe("runApi 消费 openapi.json(新口径 Task #4)", () => {
|
|
|
46
46
|
const result = await runApi({ cwd: dir, openapi: "openapi.json" });
|
|
47
47
|
expect(result.contracts).toBe(2);
|
|
48
48
|
|
|
49
|
-
const wikiApi = join(dir, "web/src/wiki/__client/
|
|
49
|
+
const wikiApi = join(dir, "web/src/wiki/__client/index.ts");
|
|
50
50
|
const wikiSchemas = join(dir, "web/src/wiki/__client/api.schemas.ts");
|
|
51
|
-
const userApi = join(dir, "web/src/user/__client/
|
|
51
|
+
const userApi = join(dir, "web/src/user/__client/index.ts");
|
|
52
52
|
expect(existsSync(wikiApi)).toBe(true);
|
|
53
53
|
expect(existsSync(wikiSchemas)).toBe(true);
|
|
54
54
|
expect(existsSync(userApi)).toBe(true);
|
package/src/contract/check.ts
CHANGED
|
@@ -4,8 +4,9 @@ import type { IrEndpoint } from "./ir";
|
|
|
4
4
|
import type { OpenApiDoc } from "./openapi-ir";
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
7
|
-
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout
|
|
7
|
+
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout } from "../layout";
|
|
8
8
|
import { readOjAnonymousPaths } from "../oj-config";
|
|
9
|
+
import { clientDirOf, loadClientOwners } from "./client-path";
|
|
9
10
|
import { emitClient } from "./emit-client";
|
|
10
11
|
import { planStubWrites } from "./emit-stub";
|
|
11
12
|
import { handlerAbs, implementedRouteKeys, pathKey, scanHandlerFile, VERB_OF, walkApiFiles } from "./handler-scan";
|
|
@@ -306,18 +307,21 @@ async function checkFromOpenapi(openapiPath: string, cwd: string, _exemption: Ap
|
|
|
306
307
|
const violations: CheckViolation[] = [];
|
|
307
308
|
const layout = tryResolveLayout(cwd);
|
|
308
309
|
// 契约工具的扫描单元是「模块 api 根」而不是整棵源码树:module-root 布局里
|
|
309
|
-
// `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/
|
|
310
|
+
// `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/index.ts` 不能被当 handler。
|
|
310
311
|
const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
|
|
311
|
-
const
|
|
312
|
-
join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
|
|
312
|
+
const owners = await loadClientOwners(layout?.projectRoot ?? cwd);
|
|
313
313
|
for (const [module, eps] of groupIrByModule(ir)) {
|
|
314
|
-
const client = emitClient(eps, { target: "module", module });
|
|
315
|
-
const clientDir = clientDirOf(module);
|
|
316
|
-
for (const [file, content] of [["
|
|
314
|
+
const client = emitClient(eps, { target: "module", module, owner: owners[module] });
|
|
315
|
+
const clientDir = clientDirOf(layout, cwd, module, owners);
|
|
316
|
+
for (const [file, content] of [["index.ts", client["index.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
|
|
317
317
|
const p = join(clientDir, file);
|
|
318
318
|
if (!existsSync(p) || readFileSync(p, "utf8") !== content)
|
|
319
319
|
violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 生成物过期或缺失:${relative(cwd, p)}——请重跑 ojm api 使其与 openapi.json 同步。`, filePath: p });
|
|
320
320
|
}
|
|
321
|
+
// 旧生成物(0.2.54 前叫 api.ts):不删会被 oj dev 当路由扫描并因 import 逃逸报错
|
|
322
|
+
const legacy = join(clientDir, "api.ts");
|
|
323
|
+
if (existsSync(legacy))
|
|
324
|
+
violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 旧生成物仍在:${relative(cwd, legacy)}——入口已改为 index.ts,请重跑 ojm api(会删除旧文件)。`, filePath: legacy });
|
|
321
325
|
}
|
|
322
326
|
const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
|
|
323
327
|
if (existsSync(apiSrcDir)) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProjectLayout } from "../layout";
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadSiteConfig } from "../config";
|
|
5
|
+
import { webSrcOf } from "../layout";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 生成 client 的落盘目录(`ojm api` 写、`ojm api --check` 核对,同一真源)。
|
|
9
|
+
*
|
|
10
|
+
* 缺省口径不变:`<project>/src/<api 前缀>/web/__client`(历史布局 `web/src/<模块>/__client`)。
|
|
11
|
+
* 多根聚合模块(web.config `clientOwners`)把多个业务域的 client 收进同一个 web 模块:
|
|
12
|
+
* 落 `<project>/src/<归属模块>/web/<api 前缀>/__client`——页面与该目录一起迁移,
|
|
13
|
+
* 页面里的相对导入 `../../__client` 不用改。
|
|
14
|
+
*/
|
|
15
|
+
export function clientDirOf(
|
|
16
|
+
layout: ProjectLayout | null,
|
|
17
|
+
cwd: string,
|
|
18
|
+
apiModule: string,
|
|
19
|
+
owners: Record<string, string> = {},
|
|
20
|
+
): string {
|
|
21
|
+
const owner = owners[apiModule] ?? apiModule;
|
|
22
|
+
const base = layout ? webSrcOf(layout, owner) : path.join(cwd, "web/src", owner);
|
|
23
|
+
return owner === apiModule ? path.join(base, "__client") : path.join(base, apiModule, "__client");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 读 web.config.ts 的 `clientOwners`(缺文件 / 未配置 = 空表,口径回落同名模块) */
|
|
27
|
+
export async function loadClientOwners(projectRoot: string): Promise<Record<string, string>> {
|
|
28
|
+
const config = await loadSiteConfig(projectRoot);
|
|
29
|
+
return config.clientOwners ?? {};
|
|
30
|
+
}
|
|
@@ -229,8 +229,8 @@ function emitEndpoint(ep: IrEndpoint): string {
|
|
|
229
229
|
}`;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
function emitPrelude(target: "module" | "internal", module?: string, endpointNames: string[] = [], wirePrefix?: string): string {
|
|
233
|
-
|
|
232
|
+
function emitPrelude(target: "module" | "internal", module?: string, endpointNames: string[] = [], wirePrefix?: string, owner?: string): string {
|
|
233
|
+
let binding = target === "module"
|
|
234
234
|
? `let req: ScopedRequestLike | undefined;
|
|
235
235
|
|
|
236
236
|
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
@@ -275,6 +275,20 @@ function ensureReq(): ScopedRequestLike {
|
|
|
275
275
|
return request;
|
|
276
276
|
}`;
|
|
277
277
|
|
|
278
|
+
// 多根聚合模块:client 落在归属模块目录下,认领护栏的对比基准随之改为归属模块名。
|
|
279
|
+
// 缺省(未配 clientOwners)owner === module,生成物与历史逐字一致(快照不变)。
|
|
280
|
+
if (target === "module" && owner && owner !== module) {
|
|
281
|
+
binding = binding
|
|
282
|
+
.replace(
|
|
283
|
+
`export const MODULE_PREFIX = ${JSON.stringify(`/${module}`)};`,
|
|
284
|
+
`export const MODULE_PREFIX = ${JSON.stringify(`/${module}`)};
|
|
285
|
+
|
|
286
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
287
|
+
export const MODULE_OWNER = ${JSON.stringify(`/${owner}`)};`,
|
|
288
|
+
)
|
|
289
|
+
.replaceAll("MODULE_PREFIX.slice(1)", "MODULE_OWNER.slice(1)");
|
|
290
|
+
}
|
|
291
|
+
|
|
278
292
|
const ctxImport = target === "module"
|
|
279
293
|
? "import type { ModuleContext } from \"@oj-module/runtime\";\n"
|
|
280
294
|
: "";
|
|
@@ -343,8 +357,15 @@ ${entries.join("\n")}
|
|
|
343
357
|
`;
|
|
344
358
|
}
|
|
345
359
|
|
|
346
|
-
/**
|
|
347
|
-
|
|
360
|
+
/**
|
|
361
|
+
* 发射双产物:index.ts(类型 + 请求函数 + 工厂)与 api.schemas.ts(DEV 校验 schema)。
|
|
362
|
+
*
|
|
363
|
+
* 入口文件名是 `index.ts` 而非 `api.ts`:oj dev 的 `--module-root` 扫描会把模块目录下
|
|
364
|
+
* **任何 `api.ts`** 当路由文件导入,聚合模块(src/app 同时有 api/ 与 web/)里的
|
|
365
|
+
* `web/<root>/__client/index.ts` 会被误扫(import 逃逸 project root 直接报错)。
|
|
366
|
+
* `index.ts` 不在扫描名单内,且 `from "../__client"` 的导入更短。
|
|
367
|
+
*/
|
|
368
|
+
export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "internal", module?: string, /** client 归属模块(多根聚合);缺省 = module */ owner?: string }): { "index.ts": string, "api.schemas.ts": string } {
|
|
348
369
|
if (ir.length === 0)
|
|
349
370
|
throw new Error("[ojm-api] client 发射失败:IR 为空——openapi.json 里没有任何端点(operationId + tags),无需生成;请检查 \`oj openapi\` 产物。");
|
|
350
371
|
if (opts.target === "module" && !opts.module) {
|
|
@@ -362,7 +383,7 @@ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "interna
|
|
|
362
383
|
+ "——register.apiPrefix 一模块一值,urlPrefix 映射须整模块一致(不同前缀请拆模块)。",
|
|
363
384
|
);
|
|
364
385
|
}
|
|
365
|
-
const sections: string[] = [emitPrelude(opts.target, opts.module, ir.map(ep => ep.name), wirePrefixes.size === 1 ? wirePrefixes.values().next().value : undefined)];
|
|
386
|
+
const sections: string[] = [emitPrelude(opts.target, opts.module, ir.map(ep => ep.name), wirePrefixes.size === 1 ? wirePrefixes.values().next().value : undefined, opts.owner)];
|
|
366
387
|
for (const ep of ir) {
|
|
367
388
|
const types = emitTypes(ep);
|
|
368
389
|
if (types.length)
|
|
@@ -372,7 +393,7 @@ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "interna
|
|
|
372
393
|
sections.push(emitEndpoint(ep));
|
|
373
394
|
}
|
|
374
395
|
return {
|
|
375
|
-
"
|
|
396
|
+
"index.ts": `${sections.join("\n\n")}\n`,
|
|
376
397
|
"api.schemas.ts": emitSchemas(ir, opts.target),
|
|
377
398
|
};
|
|
378
399
|
}
|
|
@@ -161,7 +161,23 @@ function defaultExportObject(sf: ts.SourceFile, decls: Map<string, ts.ObjectLite
|
|
|
161
161
|
* 旧逻辑直接 dottedNameOf(call.expression) 对 CallExpression 返回 undefined → 扫成 0 路由,
|
|
162
162
|
* 致使 ojm api --check 把 market 全族端点判为「未实现 / stub 缺失」。此处穿透一层取被调函数名。
|
|
163
163
|
*/
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* 取函数体内 `const <name> = { … }` 的对象字面量初始化器(工厂 return 标识符时用)。
|
|
166
|
+
* 只认直接声明,不追闭包/别名——够用即可,追深了会把无关对象当路由组。
|
|
167
|
+
*/
|
|
168
|
+
function objectLiteralOf(body: ts.Block, name: string): ts.ObjectLiteralExpression | undefined {
|
|
169
|
+
for (const st of body.statements) {
|
|
170
|
+
if (!ts.isVariableStatement(st))
|
|
171
|
+
continue;
|
|
172
|
+
for (const d of st.declarationList.declarations) {
|
|
173
|
+
if (ts.isIdentifier(d.name) && d.name.text === name && d.initializer && ts.isObjectLiteralExpression(d.initializer))
|
|
174
|
+
return d.initializer;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression, decls: Map<string, ts.ObjectLiteralExpression>): ts.ObjectLiteralExpression[] {
|
|
165
181
|
// `export default createXxxApi(marketEnv())`:`call.expression` 为带实参的调用表达式,
|
|
166
182
|
// 函数名在其 `.expression`;`export default obj.createXxxApi(marketEnv())` 同样穿透。
|
|
167
183
|
const target = ts.isCallExpression(call.expression) ? call.expression.expression : call.expression;
|
|
@@ -171,10 +187,31 @@ function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression): ts.Ob
|
|
|
171
187
|
for (const stmt of sf.statements) {
|
|
172
188
|
if (!ts.isFunctionDeclaration(stmt) || stmt.name?.text !== callee || stmt.body === undefined)
|
|
173
189
|
continue;
|
|
190
|
+
// 工厂体内的局部对象声明(`const routes = {…}`)合并进声明表:
|
|
191
|
+
// return 的对象里用简写属性引用它们时,walk 才能解析到组。
|
|
192
|
+
for (const st of stmt.body.statements) {
|
|
193
|
+
if (!ts.isVariableStatement(st))
|
|
194
|
+
continue;
|
|
195
|
+
for (const d of st.declarationList.declarations) {
|
|
196
|
+
if (ts.isIdentifier(d.name) && d.initializer && ts.isObjectLiteralExpression(d.initializer))
|
|
197
|
+
decls.set(d.name.text, d.initializer);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
174
200
|
const out: ts.ObjectLiteralExpression[] = [];
|
|
175
201
|
for (const st of stmt.body.statements) {
|
|
176
|
-
if (ts.isReturnStatement(st)
|
|
202
|
+
if (!ts.isReturnStatement(st) || st.expression === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
if (ts.isObjectLiteralExpression(st.expression)) {
|
|
177
205
|
out.push(st.expression);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// `const api = { routes, verify }; … return api;` 形态(聚合工厂):
|
|
209
|
+
// 返回标识符时回溯同函数体内的 object literal 初始化器。
|
|
210
|
+
if (ts.isIdentifier(st.expression)) {
|
|
211
|
+
const local = objectLiteralOf(stmt.body, st.expression.text);
|
|
212
|
+
if (local)
|
|
213
|
+
out.push(local);
|
|
214
|
+
}
|
|
178
215
|
}
|
|
179
216
|
return out;
|
|
180
217
|
}
|
|
@@ -317,7 +354,7 @@ export function scanHandlerFile(filePath: string, apiDir: string, moduleName = "
|
|
|
317
354
|
? expr
|
|
318
355
|
: (ts.isIdentifier(expr) ? callDecls.get(expr.text) : undefined);
|
|
319
356
|
if (callExpr) {
|
|
320
|
-
for (const obj of factoryReturnObjects(sf, callExpr))
|
|
357
|
+
for (const obj of factoryReturnObjects(sf, callExpr, decls))
|
|
321
358
|
walk(obj, "", "", 1);
|
|
322
359
|
}
|
|
323
360
|
}
|
package/src/contract/run.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { OjSchemaDiag } from "./emit-oj-schema";
|
|
|
2
2
|
import type { IrEndpoint } from "./ir";
|
|
3
3
|
import type { OpenApiDoc } from "./openapi-ir";
|
|
4
4
|
import { execFile as execFileCb } from "node:child_process";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { basename, dirname, join } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { stringify as stringifyYaml } from "yaml";
|
|
11
|
-
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, scanModules, tryResolveLayout
|
|
11
|
+
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, scanModules, tryResolveLayout } from "../layout";
|
|
12
|
+
import { clientDirOf, loadClientOwners } from "./client-path";
|
|
12
13
|
import { emitClient } from "./emit-client";
|
|
13
14
|
import { applyStubWrites, planStubWrites } from "./emit-stub";
|
|
14
15
|
import { implementedRouteKeys } from "./handler-scan";
|
|
@@ -83,12 +84,21 @@ async function runFromOpenapi(openapiPath: string, cwd: string, result: RunResul
|
|
|
83
84
|
const ir = irFromOpenapi(doc, { rawPaths });
|
|
84
85
|
const byModule = groupIrByModule(ir);
|
|
85
86
|
const layout = tryResolveLayout(cwd);
|
|
87
|
+
const owners = await loadClientOwners(layout?.projectRoot ?? cwd);
|
|
86
88
|
result.contracts += byModule.size;
|
|
87
89
|
for (const [module, eps] of byModule) {
|
|
88
|
-
const client = emitClient(eps, { target: "module", module });
|
|
89
|
-
const clientDir =
|
|
90
|
-
writeIfChanged(join(clientDir, "
|
|
90
|
+
const client = emitClient(eps, { target: "module", module, owner: owners[module] });
|
|
91
|
+
const clientDir = clientDirOf(layout, cwd, module, owners);
|
|
92
|
+
writeIfChanged(join(clientDir, "index.ts"), client["index.ts"], result);
|
|
91
93
|
writeIfChanged(join(clientDir, "api.schemas.ts"), client["api.schemas.ts"], result);
|
|
94
|
+
// 迁移:0.2.54 起入口文件从 api.ts 改为 index.ts。旧文件必须删——oj dev 的
|
|
95
|
+
// --module-root 扫描会把模块目录下任何 api.ts 当路由导入(聚合模块的
|
|
96
|
+
// web/<root>/__client/index.ts 会直接报 import 逃逸)。
|
|
97
|
+
const legacy = join(clientDir, "api.ts");
|
|
98
|
+
if (existsSync(legacy)) {
|
|
99
|
+
rmSync(legacy);
|
|
100
|
+
result.written.push(legacy);
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
// stub:oj 目录镜像树(人碰过的文件永不写——plan 阶段已按指纹判好)。
|
|
94
104
|
// 已实现键全量扫一遍传入:嵌套组(模块根单文件)实现的端点不再生成镜像 stub。
|