From d3d9c9e70b948987aa57e3781bdc35847b1af7a5 Mon Sep 17 00:00:00 2001 From: meta-d Date: Thu, 25 Jan 2024 16:51:22 +0800 Subject: [PATCH] feat: odata --- .github/workflows/publish-npm-cap-odata.yml | 2 +- btp-cap-monorepo/README.md | 20 +- btp-cap-monorepo/README_zh.md | 22 +- .../apps/launchpad/src/app/app.config.ts | 2 +- .../src/app/core/services/flp.service.ts | 2 +- .../src/app/core/services/menus.service.ts | 2 +- .../src/app/demo/elements/OData.svc.ts | 12 +- .../app/pages/admin/risk/risk.component.ts | 2 +- .../{stores/btp => pages/admin/risk}/risk.ts | 0 .../src/app/stores/btp/app-store.btp.ts | 2 +- .../src/app/stores/{ => btp}/auth.ts | 7 +- .../launchpad/src/app/stores/btp/index.ts | 2 + .../apps/launchpad/src/app/stores/index.ts | 8 +- .../src/app/stores/notification_srv.ts | 4 +- .../src/app/stores/{s4 => s4h}/ESH_SEARCH.ts | 2 +- .../src/app/stores/{s4 => s4h}/INTEROP.ts | 0 .../app/stores/{s4 => s4h}/app-store.s4.ts | 4 +- .../src/app/stores/s4h/authorization.ts | 34 +++ .../launchpad/src/app/stores/s4h/index.ts | 3 + .../apps/launchpad/src/environments/types.ts | 2 +- btp-cap-monorepo/package.json | 9 +- btp-cap-monorepo/packages/odata/README.md | 219 +++++++++++++++- btp-cap-monorepo/packages/odata/package.json | 11 +- btp-cap-monorepo/packages/odata/src/index.ts | 3 +- .../packages/odata/src/lib/filter.ts | 3 +- .../packages/odata/src/lib/helpers.ts | 134 ++++++++++ .../packages/odata/src/lib/odata.ts | 186 ++++++------- .../packages/odata/src/lib/types.ts | 244 ++++++++++-------- docs/README.md | 1 + docs/btp/GetStarted.md | 11 + 30 files changed, 706 insertions(+), 247 deletions(-) rename btp-cap-monorepo/apps/launchpad/src/app/{stores/btp => pages/admin/risk}/risk.ts (100%) rename btp-cap-monorepo/apps/launchpad/src/app/stores/{ => btp}/auth.ts (79%) create mode 100644 btp-cap-monorepo/apps/launchpad/src/app/stores/btp/index.ts rename btp-cap-monorepo/apps/launchpad/src/app/stores/{s4 => s4h}/ESH_SEARCH.ts (92%) rename btp-cap-monorepo/apps/launchpad/src/app/stores/{s4 => s4h}/INTEROP.ts (100%) rename btp-cap-monorepo/apps/launchpad/src/app/stores/{s4 => s4h}/app-store.s4.ts (95%) create mode 100644 btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/authorization.ts create mode 100644 btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/index.ts create mode 100644 btp-cap-monorepo/packages/odata/src/lib/helpers.ts create mode 100644 docs/btp/GetStarted.md diff --git a/.github/workflows/publish-npm-cap-odata.yml b/.github/workflows/publish-npm-cap-odata.yml index be5cc40..e6ea6a6 100644 --- a/.github/workflows/publish-npm-cap-odata.yml +++ b/.github/workflows/publish-npm-cap-odata.yml @@ -28,7 +28,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: yarn working-directory: ./btp-cap-monorepo - - run: yarn nx build odata + - run: yarn b:odata working-directory: ./btp-cap-monorepo - run: npm publish --access public --tag latest working-directory: ./btp-cap-monorepo/dist/packages/odata diff --git a/btp-cap-monorepo/README.md b/btp-cap-monorepo/README.md index 85ef189..5e5ab2c 100644 --- a/btp-cap-monorepo/README.md +++ b/btp-cap-monorepo/README.md @@ -36,8 +36,8 @@ This is a template for building SAP BTP and Fiori apps with [Angular](https://an - `yarn start:btp:sandbox` run btp app in sandbox environment. - `yarn ar` run approuter in url *http://localhost:5000/*. - `yarn sb` run storybook to preview components in url *http://localhost:4400/*. -- `yarn start:s4:mock` Start launchpad app for S4 system environment. Open in *http://localhost:4200/*. -- `yarn start:s4:live` Start launchpad app for live S4 system environment, Open in *http://localhost:4200/*. +- `yarn start:s4h:mock` Start launchpad app for s4h system environment. Open in *http://localhost:4200/*. +- `yarn start:s4h:live` Start launchpad app for live s4h system environment, Open in *http://localhost:4200/*. ## 🛫 Start the Project @@ -53,9 +53,9 @@ If you first run this BTP project, run `yarn deploy:btp:local` to deploy db mode To start the development server run `yarn start` or run `nx serve launchpad` and `yarn --cwd caps/app-store w-sandbox` separately. Open your browser and navigate to http://localhost:4200/. Happy coding! -### Start S4 App +### Start s4h App -Run `yarn start:s4:live` or `yarn start:s4:mock` to start the application for S4 system. +Run `yarn start:s4h:live` or `yarn start:s4h:mock` to start the application for s4h system. ### Environments @@ -63,7 +63,7 @@ The application has two environments, `development` and `production`. The defaul The features in environment are: * **production** - enable production mode, disable debug log, and others. -* **platform** - **S4** | **BTP** | **LOCAL** +* **platform** - **S4H** | **BTP** | **LOCAL** * **enableFiori** - enable load all Fiori apps in SAP system as menus in this application. * **enableNotification** - enable notification service in S4HANA system. * **enableWaterMark** - enable water mark on page of the application. @@ -87,8 +87,8 @@ You can execute the following npm scripts to preview the application: * **start** - starts the application (btp). * **start:btp** - starts the application for btp. -* **start:s4:live** - starts the application for S4 system with live service. -* **start:s4:mock** - starts the application for S4 system with mock data. +* **start:s4h:live** - starts the application for s4h system with live service. +* **start:s4h:mock** - starts the application for s4h system with mock data. ### 📡 Use Live Data @@ -167,14 +167,14 @@ For BTP platform, you can disable * Base url -The deployed application needs to be opened in a non-root path, so you need to configure the base url when building the app. Replace `your_project_name` with the name of the BSP application in command `yarn b:s4:app`. +The deployed application needs to be opened in a non-root path, so you need to configure the base url when building the app. Replace `your_project_name` with the name of the BSP application in command `yarn b:s4h:app`. ```javascript { - "b:s4:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", + "b:s4h:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", } ``` * Deploy -Run `yarn d:s4` to build and deploy to s4 system, other details ref to [Deploying to ABAP](../docs/Deploy.md#deploying-to-abap). \ No newline at end of file +Run `yarn d:s4h` to build and deploy to s4h system, other details ref to [Deploying to ABAP](../docs/Deploy.md#deploying-to-abap). \ No newline at end of file diff --git a/btp-cap-monorepo/README_zh.md b/btp-cap-monorepo/README_zh.md index e52a207..18b8984 100644 --- a/btp-cap-monorepo/README_zh.md +++ b/btp-cap-monorepo/README_zh.md @@ -35,8 +35,8 @@ - `yarn start:btp:sandbox` 在沙盒环境中运行 btp 应用。 - `yarn ar` 运行 approuter 链接为 *http://localhost:5000/*. - `yarn sb` 运行 storybook 预览组件,链接为 *http://localhost:4400/*. -- `yarn start:s4:mock` 启动 S4 系统环境的启动应用,链接为 *http://localhost:4200/*. -- `yarn start:s4:live` 启动连接在线 S4 系统的启动应用,链接为 *http://localhost:4200/*. +- `yarn start:s4h:mock` 启动 S4 系统环境的启动应用,链接为 *http://localhost:4200/*. +- `yarn start:s4h:live` 启动连接在线 S4 系统的启动应用,链接为 *http://localhost:4200/*. - ## 🛫 启动应用程序! @@ -54,7 +54,7 @@ ### 启动 S4 应用 -运行 `yarn start:s4:live` 或 `yarn start:s4:mock` 以启动适用于 S4 系统的应用程序。 +运行 `yarn start:s4h:live` 或 `yarn start:s4h:mock` 以启动适用于 S4 系统的应用程序。 ### 环境配置 @@ -63,7 +63,7 @@ 环境的特性有: * **production** - 启用生产模式,禁用调试日志等。 -* **platform** - **S4** | **BTP** | **LOCAL** +* **platform** - **S4H** | **BTP** | **LOCAL** * **enableFiori** - 启用所有 Fiori 应用作为此应用程序中的菜单的加载。 * **enableNotification** - 启用 S4HANA 系统中的通知服务。 * **enableWaterMark** - 在应用程序的页面上启用水印。 @@ -87,12 +87,12 @@ * **start** - 启动应用程序(btp)。 * **start:btp** - 为 btp 启动应用程序。 -* **start:s4:live** - 为带有实时服务的 S4 系统启动应用程序。 -* **start:s4:mock** - 为带有模拟数据的 S4 系统启动应用程序。 +* **start:s4h:live** - 为带有实时服务的 S4 系统启动应用程序。 +* **start:s4h:mock** - 为带有模拟数据的 S4 系统启动应用程序。 ### 📡 使用在线数据 -当运行 `yarn start:s4:live` 本地开发应用并调用实时的 OData 服务,你需要配置代理将请求转发给 ABAP 服务器。 +当运行 `yarn start:s4h:live` 本地开发应用并调用实时的 OData 服务,你需要配置代理将请求转发给 ABAP 服务器。 这里是配置文件 *src/proxy.conf.json*, 所有请求以 `/sap/` 开头的都会被转发到 **target** 服务器,并且授权账号信息 **auth** 已经被配置。 @@ -110,7 +110,7 @@ ### 📋 使用模拟数据 -当使用`yarn start:s4:mock` 来运行应用程序和模拟数据服务器来模拟 OData 端点时,您可以在不连接到实时 OData 服务的情况下使用应用程序,并即时生成模拟数据。 +当使用`yarn start:s4h:mock` 来运行应用程序和模拟数据服务器来模拟 OData 端点时,您可以在不连接到实时 OData 服务的情况下使用应用程序,并即时生成模拟数据。 ### 添加新 OData 的模拟数据 @@ -167,14 +167,14 @@ default: { * 基础 URL -部署的应用程序需要在非根路径中打开,因此在构建应用程序时需要配置基础 URL。在命令 `yarn b:s4:app` 中,将 `your_project_name` 替换为 BSP 应用程序的路径名称。 +部署的应用程序需要在非根路径中打开,因此在构建应用程序时需要配置基础 URL。在命令 `yarn b:s4h:app` 中,将 `your_project_name` 替换为 BSP 应用程序的路径名称。 ```javascript { - "b:s4:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", + "b:s4h:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", } ``` * 部署 -运行 `yarn d:s4` 以构建并部署到S4系统,其他详细信息请参考[部署到ABAP](../docs/Deploy.md#deploying-to-abap)。 +运行 `yarn d:s4h` 以构建并部署到S4系统,其他详细信息请参考[部署到ABAP](../docs/Deploy.md#deploying-to-abap)。 diff --git a/btp-cap-monorepo/apps/launchpad/src/app/app.config.ts b/btp-cap-monorepo/apps/launchpad/src/app/app.config.ts index 050535f..ce780e3 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/app.config.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/app.config.ts @@ -71,7 +71,7 @@ export const appConfig: ApplicationConfig = { CookieService, { provide: APP_STORE_TOKEN, - useClass: environment.platform === 'S4' ? S4AppStoreService : BTPAppStoreService + useClass: environment.platform === 'S4H' ? S4AppStoreService : BTPAppStoreService }, ...APPINIT_PROVIDES, ZngPageTitleStrategy, diff --git a/btp-cap-monorepo/apps/launchpad/src/app/core/services/flp.service.ts b/btp-cap-monorepo/apps/launchpad/src/app/core/services/flp.service.ts index fdee093..36ffe60 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/core/services/flp.service.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/core/services/flp.service.ts @@ -94,7 +94,7 @@ export class FioriLaunchpadService { }) constructor() { - if (environment.platform === 'S4' && environment.enableFiori) { + if (environment.platform === 'S4H' && environment.enableFiori) { this.loadFLPMenus().then() this.loadCookie() const localPageSets = this.localStorage.getItem(SAPFioriPageSetsName) diff --git a/btp-cap-monorepo/apps/launchpad/src/app/core/services/menus.service.ts b/btp-cap-monorepo/apps/launchpad/src/app/core/services/menus.service.ts index 869e0de..2cbb143 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/core/services/menus.service.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/core/services/menus.service.ts @@ -63,7 +63,7 @@ export class MenusService { readonly flpMenus = this.flpService.routes readonly flpLoading = computed(() => { - return environment.platform === 'S4' && environment.enableFiori && !this.flpMenus() + return environment.platform === 'S4H' && environment.enableFiori && !this.flpMenus() }) readonly menus = computed(() => { return [...this.tAppMenus(), ...(this.flpMenus() ?? [])] diff --git a/btp-cap-monorepo/apps/launchpad/src/app/demo/elements/OData.svc.ts b/btp-cap-monorepo/apps/launchpad/src/app/demo/elements/OData.svc.ts index 7dd0d4c..255c735 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/demo/elements/OData.svc.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/demo/elements/OData.svc.ts @@ -1,4 +1,4 @@ -import { ODataQueryOptions, StoreStatus, defineODataStore } from '@metad/cap-odata' +import { Keys, ODataQueryOptions, StoreStatus, defineODataStore } from '@metad/cap-odata' const demoODataStore = defineODataStore('OData.svc', { base: '/odata.org/V3/OData/' @@ -21,6 +21,11 @@ export async function queryProducts(options?: ODataQueryOptions) { return result } +export async function readProduct(keys: Keys, options?: ODataQueryOptions) { + const { read } = useDemoODataStore() + return await read('Products', {"ID": 8}, options) +} + export async function helpProductCategory(options?: ODataQueryOptions) { const { query } = useDemoODataStore() const result = await query('Categories', options) @@ -39,6 +44,11 @@ export async function helpProductDetails(options?: ODataQueryOptions) { return result } +export async function cloneCatalog() { + const { functionImport } = useDemoODataStore() + return await functionImport('CloneCatalog', {sourceId: 1, targetId: 2, title: 'test'}) +} + export type ProductType = { ID: string Name: string diff --git a/btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.component.ts b/btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.component.ts index 70c2296..31b1f9a 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.component.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.component.ts @@ -5,7 +5,7 @@ import { toSignal } from '@angular/core/rxjs-interop' import { FormsModule } from '@angular/forms' import { EMPTY, catchError, from, map, tap } from 'rxjs' import { ZngAntdModule } from '../../../core/shared.module' -import { queryRisks } from '../../../stores' +import { queryRisks } from './risk' @Component({ standalone: true, diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/risk.ts b/btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.ts similarity index 100% rename from btp-cap-monorepo/apps/launchpad/src/app/stores/btp/risk.ts rename to btp-cap-monorepo/apps/launchpad/src/app/pages/admin/risk/risk.ts diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/app-store.btp.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/app-store.btp.ts index eb58d2a..b0505c4 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/app-store.btp.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/app-store.btp.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { NGXLogger } from 'ngx-logger' import { BehaviorSubject, map } from 'rxjs' -import { getCurrentUser } from '../auth' +import { getCurrentUser } from './auth' import { upsertPersonalization, readPersonalization } from './personalization' import { IAppStore, AppStoreState, DefaultPersonalization, PersContainerId, PersonalizationType, PersContainer } from '../app' diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/auth.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/auth.ts similarity index 79% rename from btp-cap-monorepo/apps/launchpad/src/app/stores/auth.ts rename to btp-cap-monorepo/apps/launchpad/src/app/stores/btp/auth.ts index 2af7993..25b33a3 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/auth.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/auth.ts @@ -1,5 +1,10 @@ import { StoreStatus, defineODataStore } from '@metad/cap-odata' +type ODataUserType = { + ID: string + Name: string +} + const authStore = defineODataStore('auth', { base: '/api' }) @@ -14,5 +19,5 @@ export const useAuthStore = () => { export async function getCurrentUser() { const { functionImport } = useAuthStore() - return await functionImport('current') + return await functionImport('current') } diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/index.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/index.ts new file mode 100644 index 0000000..afd4a36 --- /dev/null +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/btp/index.ts @@ -0,0 +1,2 @@ +export * from './app-store.btp' +export * from './personalization' \ No newline at end of file diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/index.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/index.ts index ace249f..5dc26bb 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/index.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/index.ts @@ -1,9 +1,7 @@ export * from './epm' -export * from './auth' +export * from './btp/auth' export * from './app' export * from './PAGE_BUILDER_PERS' export * from './notification_srv' -export * from './btp/risk' -export * from './btp/app-store.btp' -export * from './s4/app-store.s4' -export * from './s4/INTEROP' \ No newline at end of file +export * from './btp/' +export * from './s4h/' \ No newline at end of file diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/notification_srv.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/notification_srv.ts index 842054b..3fda088 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/notification_srv.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/notification_srv.ts @@ -58,9 +58,9 @@ export const useNotificationStore = () => { export async function getBadgeNumber(): Promise { const { functionImport } = notificationStore - const result = await functionImport('GetBadgeNumber') + const result = await functionImport<{value: number}>('GetBadgeNumber') - return result.value as number + return result.value } export async function resetBadgeNumber() { diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4/ESH_SEARCH.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/ESH_SEARCH.ts similarity index 92% rename from btp-cap-monorepo/apps/launchpad/src/app/stores/s4/ESH_SEARCH.ts rename to btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/ESH_SEARCH.ts index 59d639c..52d3219 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4/ESH_SEARCH.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/ESH_SEARCH.ts @@ -10,7 +10,7 @@ export const useESHSearchStore = () => { return eshStore } -export type UserType = { +export type ODataUserType = { Id: string Name: string } diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4/INTEROP.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/INTEROP.ts similarity index 100% rename from btp-cap-monorepo/apps/launchpad/src/app/stores/s4/INTEROP.ts rename to btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/INTEROP.ts diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4/app-store.s4.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/app-store.s4.ts similarity index 95% rename from btp-cap-monorepo/apps/launchpad/src/app/stores/s4/app-store.s4.ts rename to btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/app-store.s4.ts index a356ca1..f2ba99d 100644 --- a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4/app-store.s4.ts +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/app-store.s4.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { NGXLogger } from 'ngx-logger' import { BehaviorSubject, map } from 'rxjs' -import { UserType, useESHSearchStore } from './ESH_SEARCH' +import { ODataUserType, useESHSearchStore } from './ESH_SEARCH' import { PersContainerType, useINTEROPStore } from './INTEROP' import { IAppStore, AppStoreState, DefaultPersonalization, PersContainerId, PersonalizationType } from '../app' @@ -29,7 +29,7 @@ export class S4AppStoreService implements IAppStore { async refreshUser() { const { read } = useESHSearchStore() - const user = await read('Users', { Id: '' }) + const user = await read('Users', { Id: '' }) .then((result) => { return { id: result.Id, diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/authorization.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/authorization.ts new file mode 100644 index 0000000..3257749 --- /dev/null +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/authorization.ts @@ -0,0 +1,34 @@ +import { StoreStatus, defineODataStore } from '@metad/cap-odata' + +/** + * Define the private store for the authorization OData service. + */ +const authStore = defineODataStore('ZNG_AUTHORIZATION_SRV') + +/** + * Export a hook to access the authorization store. + * - If the store is not yet initialized, it will be initialized. + * - If the store is in error state, it will be reinitialized. + * + * @returns store for odata service + */ +export const useAuthStore = () => { + const { store, init } = authStore + if (store.value.status === StoreStatus.init || store.value.status === StoreStatus.error) { + init() + } + return authStore +} + +/** + * Deconstruct actions from the store using the hook + * + * @returns + */ +export async function checkAuthorization() { + const { save } = useAuthStore() + + const result = await save('ActionSet', {}) + + return result +} \ No newline at end of file diff --git a/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/index.ts b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/index.ts new file mode 100644 index 0000000..29c8abf --- /dev/null +++ b/btp-cap-monorepo/apps/launchpad/src/app/stores/s4h/index.ts @@ -0,0 +1,3 @@ +export * from './app-store.s4' +export * from './ESH_SEARCH' +export * from './INTEROP' \ No newline at end of file diff --git a/btp-cap-monorepo/apps/launchpad/src/environments/types.ts b/btp-cap-monorepo/apps/launchpad/src/environments/types.ts index dd9bd28..5462747 100644 --- a/btp-cap-monorepo/apps/launchpad/src/environments/types.ts +++ b/btp-cap-monorepo/apps/launchpad/src/environments/types.ts @@ -8,7 +8,7 @@ export type IEnvironment = { /** * 此 Web 应用所适用的服务器平台 */ - platform: 'S4' | 'BTP' | 'LOCAL' + platform: 'S4H' | 'BTP' | 'LOCAL' /** * 启用兼容 S4 系统 Fiori Apps 功能 */ diff --git a/btp-cap-monorepo/package.json b/btp-cap-monorepo/package.json index 03967f3..a2fa601 100644 --- a/btp-cap-monorepo/package.json +++ b/btp-cap-monorepo/package.json @@ -7,17 +7,18 @@ "start:btp": "concurrently \"nx serve launchpad\" \"yarn --cwd caps/app-store w\"", "start:btp:sandbox": "concurrently \"nx serve launchpad\" \"yarn --cwd caps/app-store w-sandbox\"", "deploy:btp:local": "yarn --cwd caps/app-store deploy:local", - "start:s4:mock": "concurrently \"nx serve launchpad -c mock\" \"node apps/launchpad/src/mock/index.mjs\"", - "start:s4:live": "nx serve launchpad", + "start:s4h:mock": "concurrently \"nx serve launchpad -c mock\" \"node apps/launchpad/src/mock/index.mjs\"", + "start:s4h:live": "nx serve launchpad", "ar": "yarn bapp && yarn --cwd caps/app-store approuter", "sb": "yarn nx run launchpad:storybook", + "b:odata": "rm -rf dist/packages/odata/ && yarn nx build odata && cp packages/odata/README.md dist/packages/odata/", "b:btp:app": "nx build launchpad -- --base-href /blp/ && rm -rf ./caps/app-store/app/blp/ && mv ./dist/apps/launchpad/browser/ ./caps/app-store/app/blp/", "b:btp": "yarn b:btp:app && yarn --cwd caps/app-store build", "d:btp": "yarn --cwd caps/app-store deploy", - "b:s4:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", + "b:s4h:app": "nx build launchpad -- --base-href /sap/bc/ui5_ui5/sap/your_project_name/", "archive": "cd dist/apps/launchpad/browser && npx bestzip ../../../../archive.zip * .Ui5RepositoryBinaryFiles", "d:fiori": "fiori deploy --archive-path ./archive.zip --config ui5-deploy.yaml --yes", - "d:s4": "yarn b:s4:app && yarn archive && yarn d:fiori && rimraf ./archive.zip", + "d:s4h": "yarn b:s4h:app && yarn archive && yarn d:fiori && rimraf ./archive.zip", "b:performance": "yarn nx build --stats-json --sourceMap=true launchpad", "stats": "npx webpack-bundle-analyzer ./dist/apps/launchpad/stats.json" }, diff --git a/btp-cap-monorepo/packages/odata/README.md b/btp-cap-monorepo/packages/odata/README.md index b1d661f..e8c5cfc 100644 --- a/btp-cap-monorepo/packages/odata/README.md +++ b/btp-cap-monorepo/packages/odata/README.md @@ -1,3 +1,218 @@ -# OData +# Metad CAP OData -CAP OData package. \ No newline at end of file +This package provides a TypeScript library for interacting with OData services. It includes utilities for initializing and managing an OData store, querying entities, updating data, and more. + +## Installation + +You can install this package using npm: + +```bash +npm i @metad/cap-odata +``` +or yarn: + +```bash +yarn add @metad/cap-odata` +``` + +## Usage + +### Importing + +```typescript +import { ODataStore, StoreStatus, EntityType, ODataQueryOptions, Keys, OrderEnum } from '@metad/cap-odata'; +``` + +### Example + +You can define an OData store using the `defineODataStore` function. This function takes the name of the OData service as a parameter and returns an OData store instance. + +Then you can create a hook to access the store, in the hook you can initialize the store if it is not yet initialized or in error state. + +Using the hook you can access the store actions and perform operations on it. + +```ts +/** + * Define the private store for the authorization OData service. + */ +const authStore = defineODataStore('ZNG_AUTHORIZATION_SRV') + +/** + * Export a hook to access the authorization store. + * - If the store is not yet initialized, it will be initialized. + * - If the store is in error state, it will be reinitialized. + * + * @returns store for odata service + */ +export const useAuthStore = () => { + const { store, init } = authStore + if (store.value.status === StoreStatus.init || store.value.status === StoreStatus.error) { + init() + } + return authStore +} + +/** + * Deconstruct actions from the store using the hook + * + * @returns + */ +export async function checkAuthorization() { + const { save } = useAuthStore() + + const result = await save('ActionSet', {...}) + + return result +} +``` + +### Actions in Store + +- `init`: Initializes the OData store by fetching the metadata file and parsing it as a schema. + +#### Selectors + +- `select`: Selects a portion of the store state using a selector function. +- `selectEntityType`: Selects the entity type for a given entity. + +#### Entity Operations + +- `read`: Reads an entity using keys. +- `save`: Creates an entity. +- `query`: Queries entities using ODataQueryOptions. +- `update`: Updates an entity. +- `count`: Counts entities using ODataQueryOptions. +- `remove`: Removes an entity using keys. + +#### Function and Action Imports + +- `functionImport`: Calls a function import. +- `actionImport`: Calls an action import with optional data. + +#### CSRF Token + +- `setXCsrfToken`: Sets the X-Csrf-Token into the store. + +## API + +### defineODataStore + +Defines an OData store using the name of the OData service. + +```ts +const store = defineODataStore('ZNG_AUTHORIZATION_SRV') +``` + +With different options, base url: +```ts +const store = defineODataStore('OData.svc', { + base: 'https://services.odata.org/odata.org/V3/OData/' +}) +``` + +### select + +Selects a portion of the store state using a selector function. + +### query + +Query entities using filter string: +```ts +const { query } = useStore() +const result = await query('Products', { + $filter: 'Supplier/ID eq 0', + $orderby: 'Price desc', + $top: 10, + $expand: ['ProductDetail', 'Categories', 'Supplier'] +}) +``` + +Query entities using filter object: +```ts +import { Filter, FilterOperator, OrderEnum } from '@metad/cap-odata' + +const { query } = useStore() +const result = await query('Products', { + $filter: [ + { + path: 'Supplier/ID', + operator: FilterOperator.eq, + value: [0, 1] + } + ], + $orderby: [ + { + name: 'Price', + order: OrderEnum.desc + } + ], +}) +``` + +### read + +Reads an entity using keys: +```ts +const { read } = useStore() +const product = await read('Products', {ID: 8}, { + $expand: ['ProductDetail', 'Categories', 'Supplier'] +}) +``` + +### save + +Creates an entity: +```ts +const { save } = useStore() +const product = await save('Products', { + Name: 'New Product', + Price: 49.99, + Category: 'Electronics' +}) +``` + +### update + +Updates an entity: +```ts +const { update } = useStore() +const product = await update('Products', {ID: 8}, { + ID: 8, + Name: 'New Product', + Price: 49.99, + Category: 'Electronics' +}) +``` + +### remove + +Delete en entity: +```ts +const { remove } = useStore() +const product = await remove('Products', {ID: 8}) +``` + +### functionImport + +Calls a function import: +```ts +const { functionImport } = useStore() +const result = await functionImport('FunctionImport') +``` + +### actionImport + +Calls an action import with optional data: +```ts +const { actionImport } = useStore() +const result = await actionImport('ActionImport', data) +``` + +## 🛡️ License + +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. + +## 💌 Contact Us + +- For business inquiries: +- [Metad Platform @ Twitter](https://twitter.com/CloudMtda) \ No newline at end of file diff --git a/btp-cap-monorepo/packages/odata/package.json b/btp-cap-monorepo/packages/odata/package.json index 3b0143f..40c8d95 100644 --- a/btp-cap-monorepo/packages/odata/package.json +++ b/btp-cap-monorepo/packages/odata/package.json @@ -1,12 +1,21 @@ { "name": "@metad/cap-odata", "version": "0.0.19", + "homepage": "https://mtda.cloud/en/sap", + "repository": { + "type": "git", + "url": "git+https://github.com/meta-d/sap-fiori-templates.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/meta-d/sap-fiori-templates/issues" + }, "dependencies": {}, "main": "./index.js", "module": "./index.mjs", "typings": "./index.d.ts", "peerDependencies": { - "rxjs": "~7.8.0", + "rxjs": "^7.8.0", "xml-js": "^1.6.11", "query-string": "^8.1.0", "date-fns": "^3.0.4" diff --git a/btp-cap-monorepo/packages/odata/src/index.ts b/btp-cap-monorepo/packages/odata/src/index.ts index 3db4360..1cdaaf0 100644 --- a/btp-cap-monorepo/packages/odata/src/index.ts +++ b/btp-cap-monorepo/packages/odata/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/odata'; export * from './lib/types'; -export * from './lib/utils'; \ No newline at end of file +export * from './lib/utils'; +export * from './lib/helpers'; \ No newline at end of file diff --git a/btp-cap-monorepo/packages/odata/src/lib/filter.ts b/btp-cap-monorepo/packages/odata/src/lib/filter.ts index 1de8900..013be75 100644 --- a/btp-cap-monorepo/packages/odata/src/lib/filter.ts +++ b/btp-cap-monorepo/packages/odata/src/lib/filter.ts @@ -1,4 +1,5 @@ -import { Filter, FilterOperator, entityKeyValue } from './types' +import { entityKeyValue } from './helpers' +import { Filter, FilterOperator } from './types' export function filterString(filter: Filter): string { const value = filter.value diff --git a/btp-cap-monorepo/packages/odata/src/lib/helpers.ts b/btp-cap-monorepo/packages/odata/src/lib/helpers.ts new file mode 100644 index 0000000..d4a95b6 --- /dev/null +++ b/btp-cap-monorepo/packages/odata/src/lib/helpers.ts @@ -0,0 +1,134 @@ +import { format, isDate } from 'date-fns' +import { isPlainObject } from './utils' +import { isString } from './utils/isString' +import { EntityType, Keys, ODataError, uuidRegex } from './types' + +export function entityKeyValue(value: number | string | boolean | Date | null): string { + if (isString(value)) { + if (uuidRegex.test(value)) { + return `${value}` + } + return `'${encodeURIComponent(value)}'` + } else if (isDate(value)) { + return `datetime'${format(value as Date, "yyyy-MM-dd'T'HH:mm:ss")}'` + } else { + return `${value}` + } +} + +export function KeysParameters(keys: Keys) { + if (isPlainObject(keys)) { + return `(${Object.keys(keys) + .map((key) => `${key}=${entityKeyValue((>keys)[key])}`) + .join(',')})` + } else { + return `(${entityKeyValue(keys)})` + } +} + +export function getEntityName(entity: string | { name: string }): string { + return typeof entity === 'string' ? entity : entity.name.split('.').pop()! +} + +/** + * + * @param entityType EntityType for the entity + * @param item + * @returns + */ +export function mapEdmEntity>(entityType: EntityType, item: Record) { + return Object.keys(item).reduce((acc, key: keyof T) => { + const property = entityType.Property.find((prop) => prop['@'].Name === key) + switch (property?.['@'].Type) { + case 'Edm.Time': + acc[key] = parseEdmTime(item[key]) as T[keyof T] + break + case 'Edm.DateTime': + acc[key] = parseEdmDateTime(item[key]) as T[keyof T] + break + case 'Edm.Decimal': + acc[key] = Number(item[key]) as T[keyof T] + break + default: + acc[key] = item[key] + } + return acc + }, {} as T) +} + +/** + * Convert EDM.Time in OData to humman format + * + * Example: + * + * ```typescript +// Example usage +const edmTimeValue = 'PT12H30M45S'; +const convertedString = parseEdmTime(edmTimeValue); +console.log(convertedString); // Output: '12:30:45' + * ``` + * @param edmTime is in the format 'PTxxHxxMxxS' + * @returns + */ +export function parseEdmTime(edmTime: string) { + // Extract hours, minutes, and seconds + const hours = edmTime.slice(2, 4) + const minutes = edmTime.slice(5, 7) + const seconds = edmTime.slice(8, 10) + + // Construct the string representation + const timeString = `${hours}:${minutes}:${seconds}` + + return timeString +} + +/** + * Parse Edm.DateTime value to Date + * + * @param value The format is "/Date(1689206400000)/" + * @returns + */ +export function parseEdmDateTime(value: string) { + const mg = value.match(/\d+/) + if (mg) { + // 提取时间戳部分 + const timestamp = parseInt(mg[0], 10) + // 使用 parse 函数将时间戳转换为 Date 对象 + const dateObject = new Date(timestamp) + return dateObject + } + return null +} + +export function getErrorMessage(err: any) { + if (isString(err) && /^\{"error"\:\{/.test(err)) { + const error = JSON.parse(err) + + return error.error?.message?.value + } + + return err +} + +export async function throwODataError(response: Response) { + throw { + code: response.status, + error: getODataErrorMessage(await response.text()) + } as ODataError +} + +/** + * Get error message from any error object of odata + * + * @param err + * @returns + */ +export function getODataErrorMessage(err: unknown): string { + if (isString(err) && /^\{"error":\{/.test(err)) { + const error = JSON.parse(err) + + return error.error?.message?.value + } + + return err as string +} diff --git a/btp-cap-monorepo/packages/odata/src/lib/odata.ts b/btp-cap-monorepo/packages/odata/src/lib/odata.ts index 5dcb633..57386b0 100644 --- a/btp-cap-monorepo/packages/odata/src/lib/odata.ts +++ b/btp-cap-monorepo/packages/odata/src/lib/odata.ts @@ -2,27 +2,22 @@ import queryString from 'query-string' import { BehaviorSubject, map } from 'rxjs' import * as convert from 'xml-js' import { filterString } from './filter' +import { KeysParameters, entityKeyValue, getEntityName, getErrorMessage, throwODataError } from './helpers' import { FilterOperator, Keys, - KeysParameters, - ODataError, ODataQueryOptions, + ODataStore, OrderEnum, - entityKeyValue, - getEntityName, - getErrorMessage + StoreStatus, + XCsrfTokenFetch, + XCsrfTokenName } from './types' import { isString } from './utils/isString' -export enum StoreStatus { - init, - initializing, - loaded, - error, - complete -} - +/** + * OData config type + */ export interface ODataConfig { xCsrfToken: string | null environment: string @@ -35,6 +30,11 @@ const _config$ = new BehaviorSubject({ isMockData: false }) +/** + * Update odata global configuration + * + * @param value odata config + */ export function updateODataConfig(value: Partial) { _config$.next({ ..._config$.value, @@ -42,19 +42,35 @@ export function updateODataConfig(value: Partial) { }) } +/** + * Is high version (3 or 4) of odata + * + * @param response Response of odata call + * @returns boolean + */ export function isHighVersion(response: Response) { - return response.headers.get('Odata-Version')?.startsWith('4') || response.headers.get('Dataserviceversion')?.startsWith('3') + return ( + response.headers.get('Odata-Version')?.startsWith('4') || + response.headers.get('Dataserviceversion')?.startsWith('3') + ) } +/** + * Define store for an odata service. + * + * @param service + * @param options + * @returns + */ export function defineODataStore( service: string, options: { base: string version?: string | null } = { - base: '/sap/opu/odata/sap', + base: '/sap/opu/odata/sap' } -) { +): ODataStore { const { base, version } = options const store = new BehaviorSubject<{ status: StoreStatus; Schema: any }>({ @@ -66,7 +82,11 @@ export function defineODataStore( const baseUrl = `${removeLastSlash(base)}/${service}${version && !_config$.value.isMockData ? `/${version}` : ''}` const metadata = async () => { - const response = await fetch(`${baseUrl}/$metadata`) + const response = await fetch(`${baseUrl}/$metadata`, { + headers: { + [XCsrfTokenName]: XCsrfTokenFetch + } + }) const token = response.headers.get('X-Csrf-Token') if (token) { setXCsrfToken(token) @@ -74,7 +94,7 @@ export function defineODataStore( if (response.ok) { const text = await response.text() - + const _metadata: any = convert.xml2js(text, { compact: true, attributesKey: '@' }) return _metadata['edmx:Edmx']['edmx:DataServices']['Schema'] @@ -84,7 +104,7 @@ export function defineODataStore( error: getErrorMessage(await response.text()) } } - + const init = () => { store.next({ ...store.value, @@ -99,8 +119,7 @@ export function defineODataStore( status: StoreStatus.loaded }) }) - .catch((err) => { - // console.error(err) + .catch((err: any) => { store.next({ ...store.value, status: StoreStatus.error @@ -109,7 +128,7 @@ export function defineODataStore( }) } - const select = (selector: (state: any) => any) => { + const select = (selector: (state: { status: StoreStatus; Schema: any }) => T) => { return store.pipe(map(selector)) } @@ -125,40 +144,15 @@ export function defineODataStore( }) } - const read = async ( - entity: string, - keys: Keys, - options?: ODataQueryOptions - ): Promise => { + const read = async (entity: string, keys: Keys, options?: ODataQueryOptions): Promise => { const queryObj = constructQuery(options) const qString = queryString.stringify(queryObj) - // const query = new URLSearchParams() - // if ($filter) { - // query.append( - // '$filter', - // Object.keys($filter).reduce((acc, key) => { - // if (acc) { - // acc += ' and ' + key + ' eq ' + $filter[key] - // } else { - // acc = key + ' eq ' + $filter[key] - // } - // return acc - // }, '') - // ) - // } - - // if ($expand) { - // query.append('$expand', isString($expand) ? $expand : $expand.join(',')) - // } - let url = `${baseUrl}/${entity}` if (keys) { url += KeysParameters(keys) } - // const queryString = query.toString() - return fetch(`${url}${qString ? '?' + qString : ''}`, { method: 'get', headers: { @@ -180,12 +174,12 @@ export function defineODataStore( return result.d } } - + await throwODataError(response) }) } - const save = async (entitySet: string, body: any): Promise => { + const save = async (entitySet: string, body: T, options?: ODataQueryOptions): Promise => { const url = `${baseUrl}/${entitySet}` const reqOptions = { @@ -193,7 +187,8 @@ export function defineODataStore( headers: { 'Content-Type': 'application/json', Accept: 'application/json', - 'X-Csrf-Token': _config$.value.xCsrfToken ?? '' + 'X-Csrf-Token': _config$.value.xCsrfToken ?? '', + ...(options?.headers ?? {}) }, body: JSON.stringify(body) } @@ -206,12 +201,12 @@ export function defineODataStore( return result.d } } - + await throwODataError(response) }) } - const update = async (entitySet: string, keys: Keys, body: any): Promise => { + const update = async (entitySet: string, keys: Keys, body: T, options?: ODataQueryOptions): Promise => { let url = `${baseUrl}/${entitySet}` if (keys) { url += KeysParameters(keys) @@ -221,7 +216,8 @@ export function defineODataStore( headers: { 'Content-Type': 'application/json', Accept: 'application/json', - 'X-Csrf-Token': _config$.value.xCsrfToken ?? '' + 'X-Csrf-Token': _config$.value.xCsrfToken ?? '', + ...(options?.headers ?? {}) }, body: JSON.stringify(body) } @@ -234,7 +230,7 @@ export function defineODataStore( return result.d } } - + await throwODataError(response) }) } @@ -243,7 +239,6 @@ export function defineODataStore( const entitySet = getEntityName(entity) const queryObj = constructQuery(options) const qString = queryString.stringify(queryObj) - // const qString = queryObj.toString() const url = `${baseUrl}/${entitySet}` return fetch(`${url}${qString ? '?' + qString : ''}`, { @@ -276,10 +271,11 @@ export function defineODataStore( * * @param name Function Import Name */ - const functionImport = async (name: string) => { - const url = `${baseUrl}/${name}()` + const functionImport = async (name: string, params?: Record): Promise => { + // @todo how to call functionImport with params? + const url = `${baseUrl}/${name}${params ? KeysParameters(params) : '()'}` const reqOptions = { - method: 'GET', + method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' @@ -294,7 +290,7 @@ export function defineODataStore( }) } - const actionImport = async (name: string, body?: any): Promise => { + const actionImport = async (name: string, data?: any): Promise => { const url = `${baseUrl}/${name}` const reqOptions: RequestInit = { method: 'POST', @@ -303,7 +299,7 @@ export function defineODataStore( Accept: 'application/json', 'X-Csrf-Token': _config$.value.xCsrfToken ?? '' }, - body: body ? JSON.stringify(body) : null + body: data ? JSON.stringify(data) : null } return fetch(url, reqOptions).then(async (response) => { if (response.ok) { @@ -319,11 +315,11 @@ export function defineODataStore( } const count = async (entitySet: string, options?: ODataQueryOptions): Promise => { - // const queryObj = constructQuery(options) - // const qString = queryString.stringify(queryObj) + const queryObj = constructQuery(options) + const qs = queryString.stringify(queryObj) const url = `${baseUrl}/${entitySet}/$count` - return fetch(`${url}${queryString ? '?' + queryString : ''}`, { + return fetch(`${url}${qs ? '?' + qs : ''}`, { method: 'get', headers: { 'Content-Type': 'application/json', @@ -334,7 +330,29 @@ export function defineODataStore( if (response.ok) { return await response.json() } - + + await throwODataError(response) + }) + } + + const remove = async (entitySet: string, keys: Keys, options?: ODataQueryOptions): Promise => { + let url = `${baseUrl}/${entitySet}` + if (keys) { + url += KeysParameters(keys) + } + + return fetch(url, { + method: 'delete', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(options?.headers ?? {}) + } + }).then(async (response) => { + if (response.ok) { + return + } + await throwODataError(response) }) } @@ -349,9 +367,10 @@ export function defineODataStore( query, save, update, + count, + remove, functionImport, actionImport, - count, setXCsrfToken } } @@ -363,9 +382,13 @@ export function defineODataStore( * @returns */ export function constructQuery(options?: ODataQueryOptions) { - const { $filter, $expand, $orderby, $skip, $top } = options ?? {} + const { $select, $filter, $expand, $orderby, $skip, $top } = options ?? {} const query = {} as Record + if ($select) { + query['$select'] = isString($select) ? $select : $select.join(',') + } + // Filters to query string if ($filter) { if (Array.isArray($filter)) { @@ -377,6 +400,8 @@ export function constructQuery(options?: ODataQueryOptions) { return _filterString } }, '') + } else if (isString($filter)) { + query['$filter'] = $filter } else { query['$filter'] = Object.keys($filter).reduce((acc, key) => { const filterString = `${key} ${FilterOperator.eq} ${entityKeyValue($filter[key])}` @@ -394,7 +419,9 @@ export function constructQuery(options?: ODataQueryOptions) { } if ($orderby) { - query['$orderby'] = $orderby.map(({ name, order }) => `${name} ${order ?? OrderEnum.asc}`).join(',') + query['$orderby'] = isString($orderby) + ? $orderby + : $orderby.map(({ name, order }) => `${name} ${order ?? OrderEnum.asc}`).join(',') } if ($skip != null) { @@ -413,26 +440,3 @@ function removeLastSlash(url: string) { } return url } - -async function throwODataError(response: Response) { - throw { - code: response.status, - error: getODataErrorMessage(await response.text()) - } as ODataError -} - -/** - * Get error message from any error object of odata - * - * @param err - * @returns - */ -export function getODataErrorMessage(err: unknown): string { - if (isString(err) && /^\{"error":\{/.test(err)) { - const error = JSON.parse(err) - - return error.error?.message?.value - } - - return err as string -} \ No newline at end of file diff --git a/btp-cap-monorepo/packages/odata/src/lib/types.ts b/btp-cap-monorepo/packages/odata/src/lib/types.ts index ca8b3fd..2f8639e 100644 --- a/btp-cap-monorepo/packages/odata/src/lib/types.ts +++ b/btp-cap-monorepo/packages/odata/src/lib/types.ts @@ -1,6 +1,4 @@ -import { format, isDate } from 'date-fns' -import { isPlainObject } from './utils' -import { isString } from './utils/isString' +import { BehaviorSubject, Observable } from 'rxjs' export const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ export enum OrderEnum { @@ -28,17 +26,19 @@ export type ODataError = { export interface ODataQueryOptions { headers?: Record - $filter?: + $select?: string | string[] + $filter?: string | { [key: string]: ValueOfKey } | Filter[] $expand?: string | string[] - $orderby?: { - name: string - order?: OrderEnum | null - }[] + $orderby?: string + | { + name: string + order?: OrderEnum | null + }[] $skip?: number $top?: number @@ -50,59 +50,6 @@ export const XCsrfTokenFetch = 'Fetch' export type ValueOfKey = number | string | boolean | null | Date export type Keys = ValueOfKey | Record -export function entityKeyValue(value: number | string | boolean | Date | null): string { - if (isString(value)) { - if (uuidRegex.test(value)) { - return `${value}` - } - return `'${encodeURIComponent(value)}'` - } else if (isDate(value)) { - return `datetime'${format(value as Date, "yyyy-MM-dd'T'HH:mm:ss")}'` - } else { - return `${value}` - } -} - -export function KeysParameters(keys: Keys) { - if (isPlainObject(keys)) { - return `(${Object.keys(keys) - .map((key) => `${key}=${entityKeyValue((>keys)[key])}`) - .join(',')})` - } else { - return `(${entityKeyValue(keys)})` - } -} - -export function getEntityName(entity: string | { name: string }): string { - return typeof entity === 'string' ? entity : entity.name.split('.').pop()! -} - -/** - * - * @param entityType EntityType for the entity - * @param item - * @returns - */ -export function mapEdmEntity>(entityType: EntityType, item: Record) { - return Object.keys(item).reduce((acc, key: keyof T) => { - const property = entityType.Property.find((prop) => prop['@'].Name === key) - switch (property?.['@'].Type) { - case 'Edm.Time': - acc[key] = parseEdmTime(item[key]) as T[keyof T] - break - case 'Edm.DateTime': - acc[key] = parseEdmDateTime(item[key]) as T[keyof T] - break - case 'Edm.Decimal': - acc[key] = Number(item[key]) as T[keyof T] - break - default: - acc[key] = item[key] - } - return acc - }, {} as T) -} - export type Property = { '@': { Name: string @@ -114,55 +61,138 @@ export type EntityType = { } /** - * Convert EDM.Time in OData to humman format - * - * Example: - * - * ```typescript -// Example usage -const edmTimeValue = 'PT12H30M45S'; -const convertedString = parseEdmTime(edmTimeValue); -console.log(convertedString); // Output: '12:30:45' - * ``` - * @param edmTime is in the format 'PTxxHxxMxxS' - * @returns + * The status type of an odata store */ -export function parseEdmTime(edmTime: string) { - // Extract hours, minutes, and seconds - const hours = edmTime.slice(2, 4) - const minutes = edmTime.slice(5, 7) - const seconds = edmTime.slice(8, 10) - - // Construct the string representation - const timeString = `${hours}:${minutes}:${seconds}` - - return timeString +export enum StoreStatus { + init, + initializing, + loaded, + error, + complete } /** - * Parse Edm.DateTime value to Date - * - * @param value The format is "/Date(1689206400000)/" - * @returns + * The store type of odata service */ -export function parseEdmDateTime(value: string) { - const mg = value.match(/\d+/) - if (mg) { - // 提取时间戳部分 - const timestamp = parseInt(mg[0], 10) - // 使用 parse 函数将时间戳转换为 Date 对象 - const dateObject = new Date(timestamp) - return dateObject - } - return null +export type ODataStore = { + /** + * The store of odata service + */ + store: BehaviorSubject<{ status: StoreStatus; Schema: any }> + /** + * Initialize the store: + * - Fetch metadata file + * - Parse metadata as schema + */ + init: () => void + /** + * Selector for odata store + * + * @param selector + * @returns + */ + select: (selector: (state: { status: StoreStatus; Schema: any }) => T) => Observable + /** + * Selector for entity type + * + * @param entity + * @returns + */ + selectEntityType: (entity: string) => Observable + + /** + * Get the entity type + * + * @param entity + * @returns + */ + entityType: (entity: string) => EntityType + + /** + * Read an entity using keys + * + * @param entitySet + * @param keys + * @param options + * @returns + */ + read: (entitySet: string, keys: Keys, options?: ODataQueryOptions) => Promise + + /** + * Create an entity. + * + * Examples: + * ```ts + * const result = await create('EntitySet', {name: 'Metad', age: 18}) + * ``` + * + * @param entitySet + * @param data + * @param options + * @returns + */ + save: (entitySet: string, data: T, options?: ODataQueryOptions) => Promise + + /** + * Query entities using ODataQueryOptions + * + * @param entitySet + * @param options + * @returns + */ + query: (entitySet: string | { name: string }, options?: ODataQueryOptions) => Promise + + /** + * Update an entity + * + * @param entitySet + * @param keys + * @param data + * @param options + * @returns + */ + update: (entitySet: string, keys: Keys, data: T, options?: ODataQueryOptions) => Promise + + /** + * Count entities using ODataQueryOptions + * + * @param entitySet + * @param options + * @returns + */ + count: (entitySet: string, options?: ODataQueryOptions) => Promise + + /** + * Remove an entity using keys + * + * @param entitySet + * @param keys + * @param options + * @returns + */ + remove: (entitySet: string, keys: Keys, options?: ODataQueryOptions) => Promise + + /** + * Call a function import + * + * @param functionName + * @returns + */ + functionImport: (functionName: string, params?: Record) => Promise + + /** + * Call an action import with data + * + * @param actionName + * @param data + * @returns + */ + actionImport: (actionName: string, data?: unknown) => Promise + /** + * Set the X-Csrf-Token into the store + * + * @param token + * @returns + */ + setXCsrfToken: (token: string) => void } - -export function getErrorMessage(err: any) { - if (isString(err) && /^\{"error"\:\{/.test(err)) { - const error = JSON.parse(err) - - return error.error?.message?.value - } - - return err -} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 688d467..11c790d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,7 @@ This document lists various technical documents involved in this project, includ ## BTP +- [🚀 Get Started](./btp/GetStarted.md) - [CAP](./btp/CAP.md) - [Cloud SDK](./btp/Cloud-SDK.md) - [Consume Remote Services](./btp/ConsumeRemoteServices.md) diff --git a/docs/btp/GetStarted.md b/docs/btp/GetStarted.md new file mode 100644 index 0000000..6d96b9c --- /dev/null +++ b/docs/btp/GetStarted.md @@ -0,0 +1,11 @@ +# 🚀 Get Started + +## 📦 Installation + +```bash +// npm install +yarn install +``` + +## Environments +