Skip to content

Commit 0573f46

Browse files
authored
feat(dts-generator): add TypedJSONModel (#516)
Add the type definitions for the TypedJSONModel and adapt the generator to insert them to the core d.ts file.
1 parent 81aa3c2 commit 0573f46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+6182
-58
lines changed

.prettierignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnoth
1313
packages/ts-interface-generator/src/test/samples/sampleManagedObject/SampleAnotherManagedObject.ts
1414
packages/ts-interface-generator/src/test/samples/sampleWebComponent/SampleWebComponent.gen.d.ts
1515
packages/ts-interface-generator/src/test/testcases/
16-
test-packages
16+
test-packages/openui5-snapshot-test

packages/dts-generator/docs/TECHNICAL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,5 @@ This is where the actual TypeScript code is produced:
9999

100100
Tweaks the dts file content:
101101

102-
- for sap.ui.core: prepends a long preamble with the jQuery object (will be merged by TypeScript with the original jQuery types) and the functions attached to it
102+
- for sap.ui.core: prepends a long preamble with the jQuery object (will be merged by TypeScript with the original jQuery types) and the functions attached to it; also insert the TypedJSONModel typings - if possible close to the JSONModel
103103
- for sap.ui.export: rewrites the namespace "export" because it is a reserved name

packages/dts-generator/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"lodash.combinations": "18.11.1",
3434
"node-fetch": "^3.3.2",
3535
"prettier": "3.6.0",
36+
"semver": "^7.7.2",
3637
"resolve": "^1.22.10",
3738
"sanitize-html": "2.17.0",
3839
"strip-json-comments": "^5.0.2",
@@ -50,12 +51,12 @@
5051
},
5152
"scripts": {
5253
"clean": "del-cli -f dist",
53-
"copy-files": "copyfiles -V -u 1 \"src/**/*.json\" \"src/**/core-preamble.d.ts\" \"src/**/dtslintConfig/.npm___ignore\" \"src/**/dtslintConfig/openui5-tests.ts\" \"src/**/dtslintConfig/.eslintrc.json\" \"src/**/dtslintConfig/forDefinitelyTypedDir/.eslintrc.cjs\" \"src/**/api-json.d.ts\" dist/",
54+
"copy-files": "copyfiles -V -u 1 \"src/**/*.json\" \"src/**/core-preamble.d.ts\" \"src/**/typed-json-model.d.ts\" \"src/**/dtslintConfig/.npm___ignore\" \"src/**/dtslintConfig/openui5-tests.ts\" \"src/**/dtslintConfig/.eslintrc.json\" \"src/**/dtslintConfig/forDefinitelyTypedDir/.eslintrc.cjs\" \"src/**/api-json.d.ts\" dist/",
5455
"prebuild": "npm-run-all clean copy-files",
5556
"build": "tsc",
5657
"postbuild": "npm-run-all build-api-types clean-implementation-types",
5758
"build-api-types": "api-extractor run --local --verbose",
58-
"clean-implementation-types": "del-cli -f \"dist/**/*.d.ts.map\" \"dist/**/*.d.ts\" \"!dist/**/index.d.ts\" \"!dist/**/core-preamble.d.ts\"",
59+
"clean-implementation-types": "del-cli -f \"dist/**/*.d.ts.map\" \"dist/**/*.d.ts\" \"!dist/**/index.d.ts\" \"!dist/**/core-preamble.d.ts\" \"!dist/**/typed-json-model.d.ts\"",
5960
"ci": "npm-run-all test:*",
6061
"test:apis": "tsc ./src/types/api-json.d.ts ./src/types/ast.d.ts ./src/types/ui5-logger-types.d.ts",
6162
"prewatch": "npm-run-all clean copy-files",

packages/dts-generator/src/generate-from-objects.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,10 @@ export async function generateFromObjects(config: GenerateFromObjectsConfig) {
210210

211211
// Phase 7 - postProcess:
212212
// Tweak d.ts text by e.g. prepending a hardcoded preamble to the sap.ui.core d.ts file
213-
await postProcess(dtsResult, { generateGlobals }); // modifies dtsResult.dtsText
213+
await postProcess(dtsResult, {
214+
generateGlobals,
215+
ui5Version: apiObject.version,
216+
}); // modifies dtsResult.dtsText
214217

215218
return dtsResult;
216219
}

packages/dts-generator/src/phases/post-process.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const log = getLogger("@ui5/dts-generator/post-process");
33
import * as fs from "fs";
44
import prettier from "prettier";
55
const { format } = prettier;
6+
import semver from "semver";
67

78
export async function postProcess(
89
dtsResult: { library: string; dtsText: string },
9-
options: { generateGlobals?: boolean },
10+
options: { generateGlobals?: boolean; ui5Version?: string } = {},
1011
) {
1112
switch (dtsResult.library) {
1213
case "sap.ui.core":
@@ -29,6 +30,44 @@ export async function postProcess(
2930
);
3031
}
3132

33+
// add the TypedJSONModel - but only if we do not generate globals and the UI5 version is >=1.140.0
34+
if (
35+
!options.generateGlobals &&
36+
options.ui5Version &&
37+
(semver.gte(options.ui5Version, "1.140.0") ||
38+
options.ui5Version === "1.140.0-SNAPSHOT")
39+
) {
40+
const typedJsonModel = fs
41+
.readFileSync(
42+
new URL("../resources/typed-json-model.d.ts", import.meta.url),
43+
)
44+
.toString();
45+
46+
// the position right after JSONModel
47+
let pos = dtsResult.dtsText.indexOf(
48+
'declare module "sap/ui/model/json/JSONPropertyBinding" {',
49+
);
50+
// or at least before JSONModel
51+
if (pos === -1) {
52+
pos = dtsResult.dtsText.indexOf(
53+
'declare module "sap/ui/model/json/JSONModel" {',
54+
);
55+
}
56+
if (pos > -1) {
57+
// insert the typedJsonModel after/before the JSONModel module declaration in case we found it
58+
dtsResult.dtsText = await reformat(
59+
dtsResult.dtsText.slice(0, pos) +
60+
typedJsonModel +
61+
"\n" +
62+
dtsResult.dtsText.slice(pos),
63+
);
64+
} else {
65+
// otherwise just append it to the end of the dtsText
66+
dtsResult.dtsText += typedJsonModel;
67+
}
68+
}
69+
70+
// prepend preamble
3271
dtsResult.dtsText = await reformat(preamble + dtsResult.dtsText);
3372
break;
3473

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
declare module "sap/ui/model/json/TypedJSONModel" {
2+
import JSONModel from "sap/ui/model/json/JSONModel";
3+
import TypedJSONContext from "sap/ui/model/json/TypedJSONContext";
4+
import Context from "sap/ui/model/Context";
5+
6+
/**
7+
* TypedJSONModel is a subclass of JSONModel that provides type-safe access to the model data. It is only available when using UI5 with TypeScript.
8+
*
9+
* @since 1.140.0
10+
*/
11+
export default class TypedJSONModel<Data extends object> extends JSONModel {
12+
constructor(oData?: Data, bObserve?: boolean);
13+
createBindingContext<Path extends AbsoluteBindingPath<Data>>(
14+
sPath: Path,
15+
oContext?: Context,
16+
mParameters?: object,
17+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
18+
fnCallBack?: Function,
19+
bReload?: boolean,
20+
): TypedJSONContext<Data, Path>;
21+
getData(): Data;
22+
getProperty<Path extends AbsoluteBindingPath<Data>>(
23+
sPath: Path,
24+
): PropertyByAbsoluteBindingPath<Data, Path>;
25+
getProperty<
26+
Path extends RelativeBindingPath<Data, Root>,
27+
Root extends AbsoluteBindingPath<Data>,
28+
>(
29+
sPath: Path,
30+
oContext: TypedJSONContext<Data, Root>,
31+
): PropertyByRelativeBindingPath<Data, Root, Path>;
32+
33+
setData(oData: Data, bMerge?: boolean): void;
34+
35+
// setProperty with AbsoluteBindingPath (context === undefined),
36+
// PLEASE NOTE: the parameter is still necessary so
37+
// the bAsyncUpdate parameter can also be used with absolute paths.
38+
setProperty<Path extends AbsoluteBindingPath<Data>>(
39+
sPath: Path,
40+
oValue: PropertyByAbsoluteBindingPath<Data, Path>,
41+
oContext?: undefined,
42+
bAsyncUpdate?: boolean,
43+
): boolean;
44+
setProperty<
45+
Path extends RelativeBindingPath<Data, Root>,
46+
Root extends AbsoluteBindingPath<Data>,
47+
>(
48+
sPath: Path,
49+
oValue: PropertyByRelativeBindingPath<Data, Root, Path>,
50+
oContext: TypedJSONContext<Data, Root>,
51+
bAsyncUpdate?: boolean,
52+
): boolean;
53+
}
54+
55+
/**
56+
* Valid absolute binding in a JSONModel with the underlying type `Type`.
57+
* Counterpart to {@link PropertyByAbsoluteBindingPath}
58+
* @example
59+
* type Person = { name: string, id: number };
60+
* type PathInPerson = PathInJSONModel<Person>; // "/name" | "/id"
61+
* let path: PathInPerson = "/name"; // ok
62+
* path = "/firstName"; // error
63+
*/
64+
export type AbsoluteBindingPath<Type> =
65+
Type extends Array<unknown>
66+
? // if Type is an array:
67+
| `/${number}` // /0 -> first element of array
68+
| `/${number}${AbsoluteBindingPath<Type[number]>}` // /0/{NestedPath}
69+
: // if Type is not an array:
70+
Type extends object
71+
?
72+
| {
73+
[Key in keyof Type]: Type[Key] extends Array<unknown>
74+
? // Type[Key] is an array:
75+
| `/${string & Key}/${number}` // items/0 -> elem of array
76+
// path can end there or:
77+
| `/${string & Key}/${number}${AbsoluteBindingPath<Type[Key][number]>}` // items/0/{NestedPath}
78+
: // Type[Key] is NOT an array:
79+
`/${string & Key}${AbsoluteBindingPath<Type[Key]>}`;
80+
}[keyof Type]
81+
| `/${string & PropertiesOf<Type>}` // /items/0/id -> last part of path
82+
: // if T is not of type object:
83+
never;
84+
85+
/**
86+
* Valid relative binding path in a JSONModel.
87+
* The root of the path is defined by the given root string.
88+
*
89+
* @example
90+
* type PersonWrapper = { person: { name: string, id: number } };
91+
* type PathRelativeToPerson = RelativeBindingPath<PersonWrapper, "/person">; // "name" | "id"
92+
*/
93+
export type RelativeBindingPath<
94+
Type,
95+
Root extends AbsoluteBindingPath<Type>,
96+
> =
97+
AbsoluteBindingPath<TypeAtPath<Type, Root>> extends `/${infer Rest}`
98+
? Rest
99+
: never;
100+
101+
/**
102+
* The type of a property in a JSONModel identified by the given path.
103+
* Counterpart to {@link AbsoluteBindingPath}.
104+
* @example
105+
* type Person = { name: string, id: number };
106+
* type PersonName = PropertyInJSONModel<Person, "/name">; // string
107+
* const name: PersonName = "John"; // ok
108+
*/
109+
export type PropertyByAbsoluteBindingPath<
110+
Type,
111+
Path extends string,
112+
> = Path extends `/${number}`
113+
? Type extends Array<infer U>
114+
? U
115+
: never
116+
: Path extends `/${number}${infer Rest}`
117+
? Type extends Array<infer U>
118+
? PropertyByAbsoluteBindingPath<U, Rest>
119+
: never
120+
: Path extends `/${infer Key}/${number}/${infer Rest}`
121+
? Key extends keyof Type
122+
? FromArrayWithSubPath<Type, Key, Rest>
123+
: never
124+
: Path extends `/${infer Key}/${number}`
125+
? Key extends keyof Type
126+
? FromArrayElement<Type, Key>
127+
: never
128+
: Path extends `/${infer Key}/${infer Rest}`
129+
? Key extends keyof Type
130+
? FromNestedProperty<Type, Key, Rest>
131+
: never
132+
: Path extends `/${infer Key}`
133+
? Key extends keyof Type
134+
? FromTopLevelProperty<Type, Key>
135+
: never
136+
: never;
137+
138+
/**
139+
* The type of a property in a JSONModel identified by the given relative path and root.
140+
* Counterpart to {@link RelativeBindingPath}.
141+
* @example
142+
* type PersonWrapper = { person: { name: string, id: number } };
143+
* type PersonName = PropertyByRelativeBindingPath<PersonWrapper, "/person", "name">;
144+
* const name: PersonName = "John"; // ok
145+
*/
146+
export type PropertyByRelativeBindingPath<
147+
Type,
148+
Root extends string,
149+
RelativePath extends string,
150+
> = PropertyByAbsoluteBindingPath<Type, `${Root}/${RelativePath}`>;
151+
152+
/***********************************************************************************************************************
153+
* Helper types to split the types above into separate parts
154+
* to make it easier to read and understand.
155+
/**********************************************************************************************************************/
156+
157+
/**
158+
* Helper type to handle paths that point to an array with a subpath.
159+
* @example const path = "/orders/0/items"
160+
*/
161+
type FromArrayWithSubPath<Type, Key extends keyof Type, Rest extends string> =
162+
Type[Key] extends Array<infer U>
163+
? PropertyByAbsoluteBindingPath<U, `/${Rest}`>
164+
: never;
165+
166+
/**
167+
* Helper type to handle paths that point to an array element.
168+
* @example const path = "/orders/0"
169+
*/
170+
type FromArrayElement<Type, Key extends keyof Type> =
171+
Type[Key] extends Array<infer U> ? U : never;
172+
173+
/**
174+
* Helper type to handle paths that point to a nested property.
175+
* @example const path = "/customer/address/street"
176+
*/
177+
type FromNestedProperty<
178+
Type,
179+
Key extends keyof Type,
180+
Rest extends string,
181+
> = PropertyByAbsoluteBindingPath<Type[Key], `/${Rest}`>;
182+
183+
/**
184+
* Helper type to handle paths that point to a top-level property.
185+
* @example const path = "/customer"
186+
*/
187+
type FromTopLevelProperty<Type, Key extends keyof Type> = Type[Key];
188+
189+
/**
190+
* Helper type to navigate along a nested path.
191+
* Navigates from the root type along the path to determine the sub-type.
192+
*/
193+
type TypeAtPath<
194+
Type,
195+
Path extends string,
196+
> = Path extends `/${infer Key}/${infer Rest}`
197+
? Key extends keyof Type
198+
? TypeAtPath<Type[Key], `/${Rest}`>
199+
: never
200+
: Path extends `/${infer Key}`
201+
? Key extends keyof Type
202+
? Type[Key]
203+
: never
204+
: never;
205+
206+
/**
207+
* Helper type to extract the names of the properties of a given type.
208+
* Excludes properties that are of the type `Function` or `symbol`.
209+
* @example
210+
* type Person = { name: string, id: number };
211+
* type PersonProperties = PropertiesOf<Person>; // "name" | "id"
212+
* let property: PersonProperties = "name"; // ok
213+
* property = "firstName"; // error
214+
*/
215+
type PropertiesOf<Type> = {
216+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
217+
[Key in keyof Type]: Type[Key] extends Function
218+
? never
219+
: Type[Key] extends symbol
220+
? never
221+
: Key;
222+
}[keyof Type];
223+
224+
export {}; // this prevents the non-exported types like PropertiesOf from being visible for applications
225+
}
226+
227+
declare module "sap/ui/model/json/TypedJSONContext" {
228+
import Context from "sap/ui/model/Context";
229+
import TypedJSONModel from "sap/ui/model/json/TypedJSONModel";
230+
import {
231+
AbsoluteBindingPath,
232+
RelativeBindingPath,
233+
PropertyByRelativeBindingPath,
234+
} from "sap/ui/model/json/TypedJSONModel";
235+
236+
/**
237+
* TypedJSONContext is a subclass of Context that provides type-safe access to the model data. It is only available when using UI5 with TypeScript.
238+
*
239+
* @since 1.140.0
240+
*/
241+
export default class TypedJSONContext<
242+
Data extends object,
243+
Root extends AbsoluteBindingPath<Data>,
244+
> extends Context {
245+
constructor(oModel: TypedJSONModel<Data>, sPath: Root);
246+
247+
getModel(): TypedJSONModel<Data>;
248+
249+
getProperty<P extends RelativeBindingPath<Data, Root>>(
250+
sPath: P extends RelativeBindingPath<Data, Root> ? P : never,
251+
): PropertyByRelativeBindingPath<Data, Root, P>;
252+
}
253+
}

packages/dts-generator/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"./node_modules/**/*",
1818
"./src/checkDtslint/dtslintConfig/openui5-tests.ts",
1919
"./src/checkDtslint/dtslintConfig/tsconfig.json",
20-
"./src/resources/core-preamble.d.ts"
20+
"./src/resources/core-preamble.d.ts",
21+
"./src/resources/typed-json-model.d.ts"
2122
]
2223
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"ignore": ["**/*.d.ts"],
3+
"presets": [
4+
[
5+
"@babel/preset-env",
6+
{
7+
"targets": "defaults"
8+
}
9+
],
10+
"transform-ui5",
11+
"@babel/preset-typescript"
12+
],
13+
"sourceMaps": true
14+
}

0 commit comments

Comments
 (0)