@cap-js/cds-typer 0.33.1 → 0.35.0
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/cds-plugin.js +15 -3
- package/lib/boilerplate/tsBoilerplate.ts +95 -0
- package/lib/cli.js +8 -3
- package/lib/compile.js +47 -4
- package/lib/components/basedefs.js +20 -77
- package/lib/file.js +1 -1
- package/lib/printers/javascript.js +21 -0
- package/lib/resolution/resolver.js +18 -2
- package/lib/typedefs.d.ts +2 -0
- package/lib/visitor.js +6 -2
- package/package.json +15 -7
package/cds-plugin.js
CHANGED
|
@@ -5,9 +5,12 @@ const util = require('util')
|
|
|
5
5
|
const exec = util.promisify(require('child_process').exec)
|
|
6
6
|
const typer = require('./lib/compile')
|
|
7
7
|
const { fs, path } = cds.utils
|
|
8
|
+
const { configuration } = require('./lib/config')
|
|
9
|
+
|
|
8
10
|
const DEBUG = cds.debug('cli|build')
|
|
9
11
|
const BUILD_CONFIG = 'tsconfig.cdsbuild.json'
|
|
10
|
-
const
|
|
12
|
+
const DEFAULT_MODEL_DIRECTORY_NAME = '@cds-models'
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Check if the project is a TypeScript project by looking for a dependency on TypeScript.
|
|
@@ -54,6 +57,15 @@ const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
|
54
57
|
|
|
55
58
|
// IIFE to be able to return early
|
|
56
59
|
;(() => {
|
|
60
|
+
if (cds.watched) {
|
|
61
|
+
if (fs.existsSync(cds.env.typer.output_directory)) return
|
|
62
|
+
DEBUG?.('>> start cds-typer before cds watch')
|
|
63
|
+
module.exports = typer.compileFromFile('*')
|
|
64
|
+
.then(() => DEBUG?.('<< end cds-typer before cds watch'))
|
|
65
|
+
.catch(e => DEBUG?.(e))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// FIXME: remove once cds7 has been phased out
|
|
58
70
|
if (!cds?.version || cds.version < '8.0.0') {
|
|
59
71
|
DEBUG?.('typescript build task requires @sap/cds-dk version >= 8.0.0, skipping registration')
|
|
@@ -94,7 +106,7 @@ const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
|
94
106
|
} catch {
|
|
95
107
|
DEBUG?.('tsconfig.json not found, not parsable, or inconclusive. Using default model directory name')
|
|
96
108
|
}
|
|
97
|
-
return
|
|
109
|
+
return DEFAULT_MODEL_DIRECTORY_NAME
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
init() {
|
|
@@ -151,4 +163,4 @@ const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
|
151
163
|
this.#copyCleanModel(buildDirCdsModels)
|
|
152
164
|
}
|
|
153
165
|
})
|
|
154
|
-
})()
|
|
166
|
+
})()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import cds from '@sap/cds'
|
|
2
|
+
import { type } from '@sap/cds'
|
|
3
|
+
|
|
4
|
+
export type ElementsOf<T> = {[name in keyof Required<T>]: type }
|
|
5
|
+
|
|
6
|
+
export namespace Association {
|
|
7
|
+
export type to <T> = T;
|
|
8
|
+
export namespace to {
|
|
9
|
+
export type many <T extends readonly any[]> = T;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export namespace Composition {
|
|
14
|
+
export type of <T> = T;
|
|
15
|
+
export namespace of {
|
|
16
|
+
export type many <T extends readonly any[]> = T;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Entity {
|
|
21
|
+
static data<T extends Entity> (this:T, _input:Object) : T {
|
|
22
|
+
return {} as T // mock
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type EntitySet<T> = T[] & {
|
|
27
|
+
data (input:object[]) : T[]
|
|
28
|
+
data (input:object) : T
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type DraftEntity<T> = T & {
|
|
32
|
+
IsActiveEntity?: boolean | null
|
|
33
|
+
HasActiveEntity?: boolean | null
|
|
34
|
+
HasDraftEntity?: boolean | null
|
|
35
|
+
DraftAdministrativeData_DraftUUID?: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
|
|
39
|
+
export type DraftsOf<T> = typeof Array<DraftEntity<T>>
|
|
40
|
+
|
|
41
|
+
export type DeepRequired<T> = {
|
|
42
|
+
[K in keyof T]: DeepRequired<Unkey<T[K]>>
|
|
43
|
+
} & Exclude<Required<T>, null>;
|
|
44
|
+
|
|
45
|
+
const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
|
|
46
|
+
export type Key<T> = T & {[key]?: true}
|
|
47
|
+
|
|
48
|
+
export type KeysOf<T> = {
|
|
49
|
+
[K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Unkey<T> = T extends Key<infer U> ? U : T
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Dates and timestamps are strings during runtime, so cds-typer represents them as such.
|
|
56
|
+
*/
|
|
57
|
+
export type CdsDate = `${number}${number}${number}${number}-${number}${number}-${number}${number}`;
|
|
58
|
+
/**
|
|
59
|
+
* @see {@link CdsDate}
|
|
60
|
+
*/
|
|
61
|
+
export type CdsDateTime = string;
|
|
62
|
+
/**
|
|
63
|
+
* @see {@link CdsDate}
|
|
64
|
+
*/
|
|
65
|
+
export type CdsTime = `${number}${number}:${number}${number}:${number}${number}`;
|
|
66
|
+
/**
|
|
67
|
+
* @see {@link CdsDate}
|
|
68
|
+
*/
|
|
69
|
+
export type CdsTimestamp = string;
|
|
70
|
+
|
|
71
|
+
export type CdsMap = { [key: string]: unknown };
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export const createEntityProxy = function (fqParts: any, opts = {}) {
|
|
75
|
+
const { target, customProps } = { target: {}, customProps: [], ...opts }
|
|
76
|
+
const fq = fqParts.filter((p: any) => !!p).join('.')
|
|
77
|
+
return new Proxy(target, {
|
|
78
|
+
get: function (target:any, prop:any) {
|
|
79
|
+
if (cds.entities) {
|
|
80
|
+
target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
|
|
81
|
+
// overwrite/simplify getter after cds.entities is accessible
|
|
82
|
+
this.get = (target, prop) => target[prop]
|
|
83
|
+
return target[prop]
|
|
84
|
+
}
|
|
85
|
+
// we already know the name so we skip the cds.entities proxy access
|
|
86
|
+
if (prop === 'name') return fq
|
|
87
|
+
// custom properties access on 'target' as well as cached _entity property access goes here
|
|
88
|
+
if (Object.hasOwn(target, prop)) return target[prop]
|
|
89
|
+
// inline enums have to be caught here for first time access, as they do not exist on the entity
|
|
90
|
+
if (customProps.includes(prop as never)) return target[prop]
|
|
91
|
+
// last but not least we pass the property access to cds.entities
|
|
92
|
+
throw new Error(`Property ${prop} does not exist on entity '${fq}' or cds.entities is not yet defined. Ensure the CDS runtime is fully booted before accessing properties.`)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -174,7 +174,7 @@ const flags = enrichFlagSchema({
|
|
|
174
174
|
},
|
|
175
175
|
useEntitiesProxy: parameterTypes.boolean({
|
|
176
176
|
desc: `If set to true the 'cds.entities' exports in the generated 'index.js'${EOL}files will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.${EOL}${EOL}WARNING: entity properties can still only be accessed after${EOL}'cds.entities' has been loaded`,
|
|
177
|
-
default: '
|
|
177
|
+
default: 'true'
|
|
178
178
|
}),
|
|
179
179
|
version: {
|
|
180
180
|
desc: 'Prints the version of this tool.'
|
|
@@ -185,7 +185,7 @@ const flags = enrichFlagSchema({
|
|
|
185
185
|
default: 'flat'
|
|
186
186
|
},
|
|
187
187
|
propertiesOptional: parameterTypes.boolean({
|
|
188
|
-
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
|
|
188
|
+
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).${EOL}Setting it to false makes properties non-optional instead (a: T).`,
|
|
189
189
|
default: 'true'
|
|
190
190
|
}),
|
|
191
191
|
IEEE754Compatible: parameterTypes.boolean({
|
|
@@ -204,7 +204,12 @@ const flags = enrichFlagSchema({
|
|
|
204
204
|
buildTask: parameterTypes.boolean({
|
|
205
205
|
desc: `If set to true, the typescript build task will not be registered/ executed.${EOL}This value must be set in your project configuration.${EOL}Passing it as parameter to the cds-typer CLI has no effect.`,
|
|
206
206
|
default: 'true'
|
|
207
|
-
})
|
|
207
|
+
}),
|
|
208
|
+
cache: {
|
|
209
|
+
desc: `How to cache typer runs.${EOL}none: fully run cds-typer whenever it is called${EOL}blake2s256: only run if the blake2s256-hash of the model has changed. Hash is stored in a file between runs.`,
|
|
210
|
+
allowed: ['none', 'blake2s256'],
|
|
211
|
+
default: 'none'
|
|
212
|
+
}
|
|
208
213
|
})
|
|
209
214
|
|
|
210
215
|
const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
|
package/lib/compile.js
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
|
-
const { normalize } = require('path')
|
|
4
|
+
const { normalize, join } = require('path')
|
|
5
5
|
const cds = require(require.resolve('@sap/cds', { paths: [process.cwd(), __dirname] }))
|
|
6
6
|
const util = require('./util')
|
|
7
7
|
const { writeout } = require('./file')
|
|
8
8
|
const { Visitor } = require('./visitor')
|
|
9
9
|
const { LOG, setLevel } = require('./logging')
|
|
10
10
|
const { configuration } = require('./config')
|
|
11
|
+
const cdsTyperVersion = process.env.npm_package_version
|
|
12
|
+
?? require('../package.json').version
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {JSON} csn - the model
|
|
16
|
+
* @param {string} hashFile - path to the hash file
|
|
17
|
+
* @returns {[boolean, string]}
|
|
18
|
+
*/
|
|
19
|
+
const checkHash = (csn, hashFile) => {
|
|
20
|
+
const crypto = require('crypto')
|
|
21
|
+
const currentHash = crypto
|
|
22
|
+
.createHash('blake2s256')
|
|
23
|
+
.update(JSON.stringify({'cds-typer-version': cdsTyperVersion, csn}))
|
|
24
|
+
.digest('hex')
|
|
25
|
+
if (fs.existsSync(hashFile)) {
|
|
26
|
+
const oldHash = fs.readFileSync(hashFile, 'utf8')
|
|
27
|
+
if (oldHash === currentHash) {
|
|
28
|
+
return [false, currentHash]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [true, currentHash]
|
|
32
|
+
}
|
|
11
33
|
|
|
12
34
|
/**
|
|
13
35
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
@@ -39,11 +61,11 @@ const writeJsConfig = path => {
|
|
|
39
61
|
* @param {{xtended: import('./typedefs').resolver.CSN, inferred: import('./typedefs').resolver.CSN}} csn - csn tuple
|
|
40
62
|
*/
|
|
41
63
|
const compileFromCSN = async csn => {
|
|
42
|
-
|
|
43
64
|
setLevel(configuration.logLevel)
|
|
44
65
|
if (configuration.jsConfigPath) {
|
|
45
66
|
writeJsConfig(configuration.jsConfigPath)
|
|
46
67
|
}
|
|
68
|
+
|
|
47
69
|
return writeout(
|
|
48
70
|
configuration.outputDirectory,
|
|
49
71
|
Object.values(new Visitor(csn).getWriteoutFiles())
|
|
@@ -55,10 +77,31 @@ const compileFromCSN = async csn => {
|
|
|
55
77
|
* @param {string | string[]} inputFile - path to input .cds file
|
|
56
78
|
*/
|
|
57
79
|
const compileFromFile = async inputFile => {
|
|
80
|
+
const hashFile = join(configuration.outputDirectory, '.hash')
|
|
58
81
|
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
|
|
59
|
-
const
|
|
82
|
+
const xtendedPlain = await cds.load(paths, { docs: true, flavor: 'xtended' })
|
|
83
|
+
let modelHash
|
|
84
|
+
|
|
85
|
+
if (configuration.cache === 'blake2s256') {
|
|
86
|
+
const [shouldCompile, hash] = checkHash(xtendedPlain, hashFile)
|
|
87
|
+
if (!shouldCompile) {
|
|
88
|
+
LOG.info('Model has not changed. Skipping generation.')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
modelHash = hash
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const xtended = await cds.linked(xtendedPlain)
|
|
60
95
|
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
|
|
61
|
-
|
|
96
|
+
const result = await compileFromCSN({xtended, inferred})
|
|
97
|
+
|
|
98
|
+
if (modelHash) {
|
|
99
|
+
// write hash as last thing before returning,
|
|
100
|
+
// so that we don't write it if an error occurs,
|
|
101
|
+
// which would block the next run although the model has changed
|
|
102
|
+
fs.writeFileSync(hashFile, modelHash)
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
module.exports = {
|
|
@@ -1,88 +1,31 @@
|
|
|
1
1
|
const { SourceFile } = require('../file')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** @type {string} */
|
|
6
|
+
let tsBoilerplate
|
|
7
|
+
/** @type {SourceFile} */
|
|
8
|
+
let baseDefinitions
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
* such as Associations and Compositions.
|
|
11
|
-
* @type {SourceFile}
|
|
11
|
+
* @returns {string}
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { type } from '@sap/cds'
|
|
17
|
-
|
|
18
|
-
export type ElementsOf<T> = {[name in keyof Required<T>]: type }
|
|
19
|
-
|
|
20
|
-
export namespace Association {
|
|
21
|
-
export type to <T> = T;
|
|
22
|
-
export namespace to {
|
|
23
|
-
export type many <T extends readonly any[]> = T;
|
|
13
|
+
function getTsBoilerplate () {
|
|
14
|
+
if (!tsBoilerplate) {
|
|
15
|
+
tsBoilerplate = fs.readFileSync(path.join(__filename, '..', '..', 'boilerplate/tsBoilerplate.ts'), 'utf8')
|
|
24
16
|
}
|
|
17
|
+
return tsBoilerplate
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
export namespace Composition {
|
|
28
|
-
export type of <T> = T;
|
|
29
|
-
export namespace of {
|
|
30
|
-
export type many <T extends readonly any[]> = T;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class Entity {
|
|
35
|
-
static data<T extends Entity> (this:T, _input:Object) : T {
|
|
36
|
-
return {} as T // mock
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type EntitySet<T> = T[] & {
|
|
41
|
-
data (input:object[]) : T[]
|
|
42
|
-
data (input:object) : T
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export type DraftEntity<T> = T & {
|
|
46
|
-
IsActiveEntity?: boolean | null
|
|
47
|
-
HasActiveEntity?: boolean | null
|
|
48
|
-
HasDraftEntity?: boolean | null
|
|
49
|
-
DraftAdministrativeData_DraftUUID?: string | null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type DraftOf<T> = { new(...args: any[]): DraftEntity<T> }
|
|
53
|
-
export type DraftsOf<T> = typeof Array<DraftEntity<T>>
|
|
54
|
-
|
|
55
|
-
export type DeepRequired<T> = {
|
|
56
|
-
[K in keyof T]: DeepRequired<Unkey<T[K]>>
|
|
57
|
-
} & Exclude<Required<T>, null>;
|
|
58
|
-
|
|
59
|
-
const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
|
|
60
|
-
export type Key<T> = T & {[key]?: true}
|
|
61
|
-
|
|
62
|
-
export type KeysOf<T> = {
|
|
63
|
-
[K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type Unkey<T> = T extends Key<infer U> ? U : T
|
|
67
|
-
|
|
68
20
|
/**
|
|
69
|
-
*
|
|
21
|
+
* @returns {SourceFile}
|
|
70
22
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
79
|
-
export type CdsTime = ${timeRegex};
|
|
80
|
-
/**
|
|
81
|
-
* @see {@link CdsDate}
|
|
82
|
-
*/
|
|
83
|
-
export type CdsTimestamp = string;
|
|
84
|
-
|
|
85
|
-
export type CdsMap = { [key: string]: unknown };
|
|
86
|
-
`)
|
|
23
|
+
function getBaseDefinitions () {
|
|
24
|
+
if (!baseDefinitions) {
|
|
25
|
+
baseDefinitions = new SourceFile('_')
|
|
26
|
+
baseDefinitions.addPreamble(getTsBoilerplate())
|
|
27
|
+
}
|
|
28
|
+
return baseDefinitions
|
|
29
|
+
}
|
|
87
30
|
|
|
88
|
-
module.exports = {
|
|
31
|
+
module.exports = { getBaseDefinitions }
|
package/lib/file.js
CHANGED
|
@@ -471,7 +471,7 @@ class SourceFile extends File {
|
|
|
471
471
|
const boilerplate = [AUTO_GEN_NOTE]
|
|
472
472
|
if (configuration.useEntitiesProxy) {
|
|
473
473
|
if (namespace === '_') {
|
|
474
|
-
boilerplate.push(jsp.
|
|
474
|
+
boilerplate.push(jsp.printDefaultImport('cds', '@sap/cds'), this.#getEntityProxyFunctionExport())
|
|
475
475
|
} else {
|
|
476
476
|
boilerplate.push(
|
|
477
477
|
jsp.printDeconstructedImport(
|
|
@@ -33,6 +33,17 @@ class JavaScriptPrinter {
|
|
|
33
33
|
throw Error('not implemented')
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @abstract
|
|
38
|
+
* @param {string} alias - what the import should be known as within the importing file
|
|
39
|
+
* @param {string} from - the package/ location to import from
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
// eslint-disable-next-line no-unused-vars
|
|
43
|
+
printDefaultImport (alias, from) {
|
|
44
|
+
throw Error('not implemented')
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
/**
|
|
37
48
|
* @abstract
|
|
38
49
|
* @param {string[]} imports - the deconstructed elements
|
|
@@ -83,6 +94,11 @@ class ESMPrinter extends JavaScriptPrinter {
|
|
|
83
94
|
return `import * as ${alias} from '${from}'`
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
/** @type {JavaScriptPrinter['printDefaultImport']} */
|
|
98
|
+
printDefaultImport (alias, from) {
|
|
99
|
+
return `import ${alias} from '${from}'`
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
/** @type {JavaScriptPrinter['printDeconstructedImport']} */
|
|
87
103
|
printDeconstructedImport (imports, from) {
|
|
88
104
|
return `import { ${imports.join(', ')} } from '${from}/index.js'`
|
|
@@ -115,6 +131,11 @@ class CJSPrinter extends JavaScriptPrinter {
|
|
|
115
131
|
return `const ${alias} = require('${from}')`
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
/** @type {JavaScriptPrinter['printDefaultImport']} */
|
|
135
|
+
printDefaultImport (alias, from) {
|
|
136
|
+
return `const ${alias} = require('${from}')`
|
|
137
|
+
}
|
|
138
|
+
|
|
118
139
|
/** @type {JavaScriptPrinter['printDeconstructedImport']} */
|
|
119
140
|
printDeconstructedImport (imports, from) {
|
|
120
141
|
return `const { ${imports.join(', ')} } = require('${from}')`
|
|
@@ -8,13 +8,15 @@ const { StructuredInlineDeclarationResolver } = require('../components/inline')
|
|
|
8
8
|
const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
|
|
9
9
|
const { isReferenceType } = require('../components/reference')
|
|
10
10
|
const { isEntity, getMaxCardinality } = require('../csn')
|
|
11
|
-
const {
|
|
11
|
+
const { getBaseDefinitions } = require('../components/basedefs')
|
|
12
12
|
const { BuiltinResolver } = require('./builtin')
|
|
13
13
|
const { LOG } = require('../logging')
|
|
14
14
|
const { last } = require('../components/identifier')
|
|
15
15
|
const { getPropertyModifiers } = require('../components/property')
|
|
16
16
|
const { configuration } = require('../config')
|
|
17
17
|
|
|
18
|
+
const baseDefinitions = getBaseDefinitions()
|
|
19
|
+
|
|
18
20
|
/** @typedef {import('../visitor').Visitor} Visitor */
|
|
19
21
|
/** @typedef {import('../typedefs').resolver.CSN} CSN */
|
|
20
22
|
/** @typedef {import('../typedefs').resolver.EntityCSN} EntityCSN */
|
|
@@ -68,6 +70,14 @@ class Resolver {
|
|
|
68
70
|
return type.items ? !type.items.notNull : !type.notNull
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {EntityCSN} type - a CSN type
|
|
75
|
+
* @returns {boolean} whether the type has the @mandatory annotation
|
|
76
|
+
*/
|
|
77
|
+
isMandatory(type) {
|
|
78
|
+
return type['@mandatory'] === true
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
/**
|
|
72
82
|
* Returns all libraries that have been referenced at least once.
|
|
73
83
|
* @returns {Library[]}
|
|
@@ -475,6 +485,11 @@ class Resolver {
|
|
|
475
485
|
: element?.key || element?.notNull || cardinality > 1,
|
|
476
486
|
}
|
|
477
487
|
|
|
488
|
+
// parameters with @mandatory are always not null
|
|
489
|
+
// as of today, it is not clear if this also applies to other fields annotated with @mandatory.
|
|
490
|
+
result.isNotNull ||= element.kind === 'param' && this.isMandatory(element)
|
|
491
|
+
|
|
492
|
+
|
|
478
493
|
if (element?.type === undefined) {
|
|
479
494
|
// "fallback" type "empty object". May be overriden via #resolveInlineDeclarationType
|
|
480
495
|
// later on with an inline declaration
|
|
@@ -521,9 +536,10 @@ class Resolver {
|
|
|
521
536
|
result.isBuiltin = true
|
|
522
537
|
this.resolveType(element.items, file)
|
|
523
538
|
//delete element.items
|
|
524
|
-
} else if (element?.elements && (options?.forceInlineStructs || !element?.type)) {
|
|
539
|
+
} else if (!result.isBuiltin && element?.elements && (options?.forceInlineStructs || !element?.type)) {
|
|
525
540
|
// explicitly skip named type definitions, which have elements too, but should not be considered inline declarations
|
|
526
541
|
// if the resolver option `forceInlineStructs` is `true`, named types in elements will be converted to inline
|
|
542
|
+
// Skipping isBuiltin will skip cds.Map, which has elements
|
|
527
543
|
this.#resolveInlineDeclarationType(element.elements, result, file, options)
|
|
528
544
|
}
|
|
529
545
|
|
package/lib/typedefs.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export module resolver {
|
|
|
27
27
|
name: string,
|
|
28
28
|
'@singular'?: string,
|
|
29
29
|
'@plural'?: string,
|
|
30
|
+
'@mandatory'?: boolean,
|
|
30
31
|
'@odata.draft.enabled'?: boolean // custom!
|
|
31
32
|
_unresolved?: boolean
|
|
32
33
|
isRefNotNull?: boolean // custom!
|
|
@@ -188,6 +189,7 @@ export module config {
|
|
|
188
189
|
|
|
189
190
|
export type Configuration = {
|
|
190
191
|
outputDirectory: string,
|
|
192
|
+
cache: 'none' | 'blake2s256'
|
|
191
193
|
logLevel: number,
|
|
192
194
|
/**
|
|
193
195
|
* `useEntitiesProxy = true` will wrap the `module.exports.<entityName>` in `Proxy` objects
|
package/lib/visitor.js
CHANGED
|
@@ -10,7 +10,7 @@ const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf,
|
|
|
10
10
|
const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
|
|
11
11
|
const { isReferenceType } = require('./components/reference')
|
|
12
12
|
const { empty } = require('./components/typescript')
|
|
13
|
-
const {
|
|
13
|
+
const { getBaseDefinitions } = require('./components/basedefs')
|
|
14
14
|
const { EntityRepository, asIdentifier } = require('./resolution/entity')
|
|
15
15
|
const { last } = require('./components/identifier')
|
|
16
16
|
const { getPropertyModifiers } = require('./components/property')
|
|
@@ -18,6 +18,8 @@ const { configuration } = require('./config')
|
|
|
18
18
|
const { createMember } = require('./components/class')
|
|
19
19
|
const { overrideNameProperty } = require('./printers/javascript')
|
|
20
20
|
|
|
21
|
+
const baseDefinitions = getBaseDefinitions()
|
|
22
|
+
|
|
21
23
|
/** @typedef {import('./file').File} File */
|
|
22
24
|
/** @typedef {import('./typedefs').visitor.Context} Context */
|
|
23
25
|
/** @typedef {import('./typedefs').visitor.Inflection} Inflection */
|
|
@@ -403,7 +405,9 @@ class Visitor {
|
|
|
403
405
|
.filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
|
|
404
406
|
.map(([name, type]) => ({
|
|
405
407
|
name,
|
|
406
|
-
modifier: this.resolver.isOptional(type)
|
|
408
|
+
modifier: this.resolver.isOptional(type) && !this.resolver.isMandatory(type)
|
|
409
|
+
? '?'
|
|
410
|
+
: '',
|
|
407
411
|
type: this.#stringifyFunctionParamType(type, file),
|
|
408
412
|
doc: docify(type.doc).join('\n'),
|
|
409
413
|
}))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/cds-typer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": "github:cap-js/cds-typer",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"test:unit": "node test/testRunner.js ./test/unit ./test/unit/setup.mjs",
|
|
17
17
|
"test:integration": "node test/testRunner.js ./test/integration ./test/integration/setup.mjs",
|
|
18
18
|
"test:smoke": "node test/testRunner.js ./test/smoke ./test/smoke/setup.mjs",
|
|
19
|
-
"test:all": "npm run test:smoke && npm run test:unit",
|
|
20
|
-
"test": "npm run test:
|
|
19
|
+
"test:all": "npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
20
|
+
"test": "npm run test:all",
|
|
21
21
|
"lint": "npx eslint .",
|
|
22
22
|
"lint:fix": "npx eslint . --fix",
|
|
23
23
|
"cli": "node lib/cli.js",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@cap-js/cds-types": "^0",
|
|
48
|
-
"@sap/cds": "^8",
|
|
49
48
|
"@stylistic/eslint-plugin-js": "^4.2.0",
|
|
50
49
|
"acorn": "^8.10.0",
|
|
51
50
|
"eslint": "^9",
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"inline_declarations": "flat",
|
|
59
58
|
"target_module_type": "auto",
|
|
60
59
|
"properties_optional": true,
|
|
61
|
-
"use_entities_proxy":
|
|
60
|
+
"use_entities_proxy": true,
|
|
62
61
|
"build_task": true
|
|
63
62
|
},
|
|
64
63
|
"schema": {
|
|
@@ -71,6 +70,15 @@
|
|
|
71
70
|
"type": "object",
|
|
72
71
|
"description": "Configuration for CDS Typer",
|
|
73
72
|
"properties": {
|
|
73
|
+
"cache": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "How to cache typer runs.\nnone: fully run cds-typer whenever it is called\nblake2s256: only run if the blake2s256-hash of the model has changed. Hash is stored in a file between runs.",
|
|
76
|
+
"enum": [
|
|
77
|
+
"none",
|
|
78
|
+
"blake2s256"
|
|
79
|
+
],
|
|
80
|
+
"default": "none"
|
|
81
|
+
},
|
|
74
82
|
"output_directory": {
|
|
75
83
|
"type": "string",
|
|
76
84
|
"description": "Root directory to write the generated files to.",
|
|
@@ -101,7 +109,7 @@
|
|
|
101
109
|
"use_entities_proxy": {
|
|
102
110
|
"type": "boolean",
|
|
103
111
|
"description": "If set to true the 'cds.entities' exports in the generated 'index.js'\nfiles will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.\n\nWARNING: entity properties can still only be accessed after\n'cds.entities' has been loaded",
|
|
104
|
-
"default":
|
|
112
|
+
"default": true
|
|
105
113
|
},
|
|
106
114
|
"inline_declarations": {
|
|
107
115
|
"type": "string",
|
|
@@ -114,7 +122,7 @@
|
|
|
114
122
|
},
|
|
115
123
|
"properties_optional": {
|
|
116
124
|
"type": "boolean",
|
|
117
|
-
"description": "If set to true, properties in entities are\nalways generated as optional (a?: T).",
|
|
125
|
+
"description": "If set to true, properties in entities are\nalways generated as optional (a?: T).\nSetting it to false makes properties non-optional instead (a: T).",
|
|
118
126
|
"default": true
|
|
119
127
|
},
|
|
120
128
|
"ieee754compatible": {
|