From 9607eb092aff299a313afe142445761bedb4c36e Mon Sep 17 00:00:00 2001 From: viktorsperling Date: Tue, 7 Apr 2026 15:21:11 +0200 Subject: [PATCH 1/2] feat(dts-generator): add bindTree declarations and tests to TypedJSONModel --- .../src/resources/typed-json-model.d.ts | 26 ++++++++ test-packages/typed-json-model/package.json | 2 +- test-packages/typed-json-model/tsconfig.json | 3 +- .../typed-json-model/webapp/model/model.ts | 35 +++++++++++ .../webapp/model/test/cases/generalCases.ts | 36 +++++++++++ .../typed-json-model/webapp/model/typing.ts | 35 +++++++++++ yarn.lock | 63 +++++-------------- 7 files changed, 149 insertions(+), 51 deletions(-) create mode 100644 test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts diff --git a/packages/dts-generator/src/resources/typed-json-model.d.ts b/packages/dts-generator/src/resources/typed-json-model.d.ts index 5d33d33..09d97a9 100644 --- a/packages/dts-generator/src/resources/typed-json-model.d.ts +++ b/packages/dts-generator/src/resources/typed-json-model.d.ts @@ -1,5 +1,10 @@ +import { AbsoluteTreeBindingPath } from "../../../../test-packages/typed-json-model/webapp/model/typing"; + declare module "sap/ui/model/json/TypedJSONModel" { import JSONModel from "sap/ui/model/json/JSONModel"; + import JSONTreeBinding from "sap/ui/model/json/JSONTreeBinding"; + import Filter from "sap/ui/model/Filter"; + import Sorter from "sap/ui/model/Sorter"; import TypedJSONContext from "sap/ui/model/json/TypedJSONContext"; import Context from "sap/ui/model/Context"; @@ -18,6 +23,27 @@ declare module "sap/ui/model/json/TypedJSONModel" { fnCallBack?: Function, bReload?: boolean, ): TypedJSONContext; + + // Overload for absolute paths + bindTree>( + sPath: Path, + oContext?: undefined, + aFilters?: Filter | Filter[], + mParameters?: object, + aSorters?: Sorter | Sorter[], + ): JSONTreeBinding; + // Overload for relative paths + bindTree< + Path extends RelativeTreeBindingPath, + Root extends AbsoluteBindingPath, + >( + sPath: Path, + oContext: TypedJSONContext, + aFilters?: Filter | Filter[], + mParameters?: object, + aSorters?: Sorter | Sorter[], + ): JSONTreeBinding; + getData(): Data; getProperty>( sPath: Path, diff --git a/test-packages/typed-json-model/package.json b/test-packages/typed-json-model/package.json index 290cf06..dfc18d1 100644 --- a/test-packages/typed-json-model/package.json +++ b/test-packages/typed-json-model/package.json @@ -16,7 +16,7 @@ "ci": "npm run lint && npm run ui5lint && npm run ts-typecheck && npm run test" }, "devDependencies": { - "@types/openui5": "1.136.0", + "@openui5/types": "^1.146.0", "@ui5/cli": "^4.0.30", "@ui5/linter": "^1.20.2", "eslint": "^9.37.0", diff --git a/test-packages/typed-json-model/tsconfig.json b/test-packages/typed-json-model/tsconfig.json index eca45e6..182cca5 100644 --- a/test-packages/typed-json-model/tsconfig.json +++ b/test-packages/typed-json-model/tsconfig.json @@ -11,7 +11,8 @@ "baseUrl": "./", "paths": {}, "composite": true, - "outDir": "./dist" + "outDir": "./dist", + "types": ["@openui5/types"] }, "include": ["./webapp/**/*"], "exclude": ["./**/*.mjs", "./webapp/**/test/**"] diff --git a/test-packages/typed-json-model/webapp/model/model.ts b/test-packages/typed-json-model/webapp/model/model.ts index a3b70ad..ebe57db 100644 --- a/test-packages/typed-json-model/webapp/model/model.ts +++ b/test-packages/typed-json-model/webapp/model/model.ts @@ -2,10 +2,15 @@ import Context from "sap/ui/model/Context"; import JSONModel from "sap/ui/model/json/JSONModel"; import { AbsoluteBindingPath, + AbsoluteTreeBindingPath, PropertyByAbsoluteBindingPath, PropertyByRelativeBindingPath, RelativeBindingPath, + RelativeTreeBindingPath, } from "./typing"; +import Filter from "sap/ui/model/Filter"; +import Sorter from "sap/ui/model/Sorter"; +import JSONTreeBinding from "sap/ui/model/json/JSONTreeBinding"; export class TypedJSONContext> extends Context { constructor(oModel: TypedJSONModel, sPath: Root) { @@ -39,6 +44,36 @@ export class TypedJSONModel extends JSONModel { return super.createBindingContext(sPath, oContext, mParameters, fnCallBack, bReload) as TypedJSONContext; } + // Overload for absolute paths + bindTree>( + sPath: Path, + oContext?: undefined, + aFilters?: Filter | Filter[], + mParameters?: object, + aSorters?: Sorter | Sorter[], + ): JSONTreeBinding; + // Overload for relative paths + bindTree, Root extends AbsoluteBindingPath>( + sPath: Path, + oContext: TypedJSONContext, + aFilters?: Filter | Filter[], + mParameters?: object, + aSorters?: Sorter | Sorter[], + ): JSONTreeBinding; + // Implementation + bindTree< + Path extends AbsoluteTreeBindingPath | RelativeTreeBindingPath, + Root extends AbsoluteBindingPath, + >( + sPath: Path, + oContext?: TypedJSONContext, + aFilters?: Filter | Filter[], + mParameters?: object, + aSorters?: Sorter | Sorter[], + ): JSONTreeBinding { + return super.bindTree(sPath, oContext, aFilters, mParameters, aSorters); + } + getData(): Data { return super.getData() as Data; } diff --git a/test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts b/test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts new file mode 100644 index 0000000..0ce5b0c --- /dev/null +++ b/test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts @@ -0,0 +1,36 @@ +/** + * @file Various general test cases to test the TypedJSONModel for APIs which always return the same type, + * regardless of the provided path (e.g. getObject, getPath, etc.) + */ + +import { TypedJSONModel } from "../../model"; +import JSONTreeBinding from "sap/ui/model/json/JSONTreeBinding"; + +/*********************************************************************************************************************** + * bindTree - Absolute cases + **********************************************************************************************************************/ + +const data = { root: { array: [1, 2, 3], nested: { value: "test" }, number: 1, string: "foo" } }; +const model0 = new TypedJSONModel(data); + +/** @expect ok */ let jsonTreeBindingAbsolute: JSONTreeBinding = model0.bindTree("/root/array"); +/** @expect ok */ jsonTreeBindingAbsolute = model0.bindTree("/root/nested"); +/** @expect ts2345 */ jsonTreeBindingAbsolute = model0.bindTree("/root/number"); +/** @expect ts2345 */ jsonTreeBindingAbsolute = model0.bindTree("/root/string"); +/** @expect ts2345 */ jsonTreeBindingAbsolute = model0.bindTree("/root/nonExisting"); +/** @expect ts2345 */ jsonTreeBindingAbsolute = model0.bindTree("/root/array/0"); +/** @expect ts2345 */ jsonTreeBindingAbsolute = model0.bindTree("/root/nested/value"); + +/*********************************************************************************************************************** + * bindTree - Relative cases + **********************************************************************************************************************/ + +const context = model0.createBindingContext("/root"); + +/** @expect ok */ let jsonTreeBindingRelative: JSONTreeBinding = model0.bindTree("array", context); +/** @expect ok */ jsonTreeBindingRelative = model0.bindTree("nested", context); +/** @expect ts2769 */ jsonTreeBindingRelative = model0.bindTree("number", context); +/** @expect ts2769 */ jsonTreeBindingRelative = model0.bindTree("string", context); +/** @expect ts2769 */ jsonTreeBindingRelative = model0.bindTree("nonExisting", context); +/** @expect ts2769 */ jsonTreeBindingRelative = model0.bindTree("array/0", context); +/** @expect ts2769 */ jsonTreeBindingRelative = model0.bindTree("nested/value", context); diff --git a/test-packages/typed-json-model/webapp/model/typing.ts b/test-packages/typed-json-model/webapp/model/typing.ts index ba6d7f9..a9b9030 100644 --- a/test-packages/typed-json-model/webapp/model/typing.ts +++ b/test-packages/typed-json-model/webapp/model/typing.ts @@ -28,6 +28,25 @@ export type AbsoluteBindingPath = : // if T is not of type object: never; +/** + * Valid absolute binding path for underlying `Array` or `object` types. + * + * @example + * type SalesOrder = { id: string, items: string[], parameters: { weight: number } }; + * type PathInObject = PathInJSONModel; // "/id" | "/items" | "/parameters" + * let path: PathInObject = "/items"; // ok + * path = "/parameters"; // ok + * path = "/id"; // error + * path = "/items/0"; // error, since an element in the array is a string + */ +export type AbsoluteTreeBindingPath = { + [Path in AbsoluteBindingPath]: PropertyByAbsoluteBindingPath extends Array + ? Path + : PropertyByAbsoluteBindingPath extends object + ? Path + : never; +}[AbsoluteBindingPath]; + /** * Valid relative binding path in a JSONModel. * The root of the path is defined by the given root string. @@ -87,6 +106,22 @@ export type PropertyByRelativeBindingPath< RelativePath extends string, > = PropertyByAbsoluteBindingPath; +/** + * Valid relative binding path for underlying `Array` or `object` types. + * The root of the path is defined by the given root string. + * + * @example + * type SalesOrder = { buyer: { id: string, items: string[], parameters: { weight: number } } }; + * type PathRelativeToSalesOrder = RelativeTreeBindingPath; // "items" | "parameters" + */ +export type RelativeTreeBindingPath> = { + [Path in RelativeBindingPath]: PropertyByRelativeBindingPath extends Array + ? Path + : PropertyByRelativeBindingPath extends object + ? Path + : never; +}[RelativeBindingPath]; + /*********************************************************************************************************************** * Helper types to split the types above into separate parts * to make it easier to read and understand. diff --git a/yarn.lock b/yarn.lock index 6d38843..dc503b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2361,6 +2361,14 @@ dependencies: "@octokit/openapi-types" "^24.2.0" +"@openui5/types@^1.146.0": + version "1.146.0" + resolved "https://registry.yarnpkg.com/@openui5/types/-/types-1.146.0.tgz#f3f035bc0f4e25f23acfeba888b927347fb3b336" + integrity sha512-6jzQ54BIpOQEL8+46u2WsaR9gFcjyEaKphUlAS82Pt60OOW8p2xpmTknFRRZDgBmP4IbZZob9nLb82MNGMRTSw== + dependencies: + "@types/jquery" "3.5.13" + "@types/qunit" "2.5.4" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2811,14 +2819,6 @@ "@types/jquery" "~3.5.13" "@types/qunit" "^2.5.4" -"@types/openui5@1.136.0": - version "1.136.0" - resolved "https://registry.yarnpkg.com/@types/openui5/-/openui5-1.136.0.tgz#9cada8f12d5d03d03f4975553cb763da2e7fa104" - integrity sha512-gdjK8/bYKsdZUiinARbYW+B6sbAVo0B4KLbHMs/noBzx2wfUoHOb9DiS+lpBpjtfD28NyiluReCp2R4kp7fceA== - dependencies: - "@types/jquery" "~3.5.13" - "@types/qunit" "^2.5.4" - "@types/qunit@2.5.4": version "2.5.4" resolved "https://registry.yarnpkg.com/@types/qunit/-/qunit-2.5.4.tgz#0518940acc6013259a8619a1ec34ce0e4ff8d1c4" @@ -3008,7 +3008,7 @@ yargs "^17.7.2" "@ui5/dts-generator@link:packages/dts-generator": - version "3.9.1" + version "3.10.1" dependencies: "@definitelytyped/dtslint" latest "@definitelytyped/eslint-plugin" latest @@ -10948,7 +10948,7 @@ string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10966,15 +10966,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -11062,7 +11053,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11076,13 +11067,6 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -11561,7 +11545,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -"typescript-5.3@npm:typescript@~5.3.0-0": +"typescript-5.3@npm:typescript@~5.3.0-0", typescript@5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== @@ -11591,7 +11575,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== -"typescript-5.9@npm:typescript@~5.9.0-0": +"typescript-5.9@npm:typescript@~5.9.0-0", typescript@5.9.3, "typescript@>=3 < 6", typescript@^5.9.3: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== @@ -11611,21 +11595,11 @@ typescript-eslint@^8.46.1: "@typescript-eslint/typescript-estree" "8.46.1" "@typescript-eslint/utils" "8.46.1" -typescript@5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== - typescript@5.8.2: version "5.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== -typescript@5.9.3, "typescript@>=3 < 6", typescript@^5.9.3: - version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" @@ -12075,7 +12049,7 @@ workerpool@^9.2.0, workerpool@^9.3.4: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12093,15 +12067,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 1fc777eeb261edca1ca35744668edf6a32895dcd Mon Sep 17 00:00:00 2001 From: viktorsperling Date: Wed, 8 Apr 2026 17:38:38 +0200 Subject: [PATCH 2/2] chore(dts-generator): review comments --- .../webapp/model/test/cases/{generalCases.ts => general.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test-packages/typed-json-model/webapp/model/test/cases/{generalCases.ts => general.ts} (100%) diff --git a/test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts b/test-packages/typed-json-model/webapp/model/test/cases/general.ts similarity index 100% rename from test-packages/typed-json-model/webapp/model/test/cases/generalCases.ts rename to test-packages/typed-json-model/webapp/model/test/cases/general.ts