@cap-js/cds-typer 0.22.0 → 0.23.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/CHANGELOG.md +9 -1
- package/cds-plugin.js +4 -4
- package/lib/cli.js +19 -8
- package/lib/compile.js +8 -10
- package/lib/components/enum.js +12 -12
- package/lib/components/identifier.js +9 -1
- package/lib/components/inline.js +15 -13
- package/lib/components/reference.js +1 -1
- package/lib/components/wrappers.js +8 -8
- package/lib/csn.js +38 -25
- package/lib/file.js +34 -30
- package/lib/logging.js +12 -70
- package/lib/resolution/builtin.js +64 -0
- package/lib/resolution/entity.js +155 -0
- package/lib/{components → resolution}/resolver.js +54 -163
- package/lib/typedefs.d.ts +21 -0
- package/lib/util.js +6 -6
- package/lib/visitor.js +116 -124
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 0.
|
|
7
|
+
## Version 0.24.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.23.0 - 2024-07-04
|
|
10
|
+
### Fixed
|
|
11
|
+
- Plurals no longer have `is_singular` attached in the resulting .js files
|
|
12
|
+
- Properties are properly propagated beyond just one level of inheritance
|
|
8
13
|
|
|
9
14
|
## Version 0.22.0 - 2024-06-20
|
|
10
15
|
### Fixed
|
|
11
16
|
- Fixed a bug where keys would sometimes inconsistently become nullable
|
|
12
17
|
|
|
18
|
+
### Changed
|
|
19
|
+
- Logging now internally uses `cds.log` and pipes output into the `cds-typer` logger, which can be configured via `cds.env` in addition to explicitly passing a `--logLevel` parameter to CLI. Users now have to use the levels defined in [`cds.log.levels`](https://cap.cloud.sap/docs/node.js/cds-log#log-levels). The formerly valid levels `WARNING`, `CRITICAL`, and `NONE` are now deprecated and automatically mapped to valid levels for now.
|
|
20
|
+
|
|
13
21
|
## Version 0.21.2 - 2024-06-06
|
|
14
22
|
### Fixed
|
|
15
23
|
- The typescript build task will no longer attempt to run unless at least cds 8 is installed
|
package/cds-plugin.js
CHANGED
|
@@ -33,7 +33,7 @@ const rmDirIfExists = dir => {
|
|
|
33
33
|
* @param {string[]} exts - The extensions to remove.
|
|
34
34
|
* @returns {Promise<void>}
|
|
35
35
|
*/
|
|
36
|
-
const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
36
|
+
const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
37
37
|
? Promise.all(
|
|
38
38
|
(await readdir(dir))
|
|
39
39
|
.map(async file => {
|
|
@@ -66,7 +66,7 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
66
66
|
get #modelDirectoryName () {
|
|
67
67
|
try {
|
|
68
68
|
// expected format: { '#cds-models/*': [ './@cds-models/*/index.ts' ] }
|
|
69
|
-
// ^^^^^^^^^^^^^^^
|
|
69
|
+
// ^^^^^^^^^^^^^^^
|
|
70
70
|
// relevant part - may be changed by user
|
|
71
71
|
const config = JSON.parse(fs.readFileSync ('tsconfig.json', 'utf8'))
|
|
72
72
|
const alias = config.compilerOptions.paths['#cds-models/*'][0]
|
|
@@ -118,8 +118,8 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
118
118
|
await rmFiles(this.task.dest, ['.js', '.ts'])
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
|
-
await (buildConfigExists()
|
|
122
|
-
? this.#buildWithConfig()
|
|
121
|
+
await (buildConfigExists()
|
|
122
|
+
? this.#buildWithConfig()
|
|
123
123
|
: this.#buildWithoutConfig()
|
|
124
124
|
)
|
|
125
125
|
} catch (error) {
|
package/lib/cli.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
+
const cds = require('@sap/cds')
|
|
5
6
|
const { compileFromFile } = require('./compile')
|
|
6
7
|
const { parseCommandlineArgs } = require('./util')
|
|
7
|
-
const {
|
|
8
|
+
const { deprecated, _keyFor } = require('./logging')
|
|
8
9
|
const path = require('path')
|
|
9
10
|
const { EOL } = require('node:os')
|
|
10
11
|
|
|
@@ -21,9 +22,11 @@ const flags = {
|
|
|
21
22
|
desc: 'This text.',
|
|
22
23
|
},
|
|
23
24
|
logLevel: {
|
|
24
|
-
desc:
|
|
25
|
-
allowed: Object.keys(
|
|
26
|
-
|
|
25
|
+
desc: `Minimum log level that is printed.${EOL}The default is only used if no explicit value is passed${EOL}and there is no configuration passed via cds.env either.`,
|
|
26
|
+
allowed: Object.keys(cds.log.levels).concat(Object.keys(deprecated)),
|
|
27
|
+
allowedHint: Object.keys(cds.log.levels).join(' | '), // FIXME: remove once old levels are faded out
|
|
28
|
+
defaultHint: _keyFor(cds.log.levels.ERROR),
|
|
29
|
+
default: cds?.env?.log?.levels?.['cds-typer'] ?? _keyFor(cds.log.levels.ERROR),
|
|
27
30
|
},
|
|
28
31
|
jsConfigPath: {
|
|
29
32
|
desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
|
|
@@ -60,14 +63,16 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
60
63
|
.sort()
|
|
61
64
|
.map(([key, value]) => {
|
|
62
65
|
let s = indent(`--${key}`, ' ')
|
|
63
|
-
if (value.
|
|
66
|
+
if (value.allowedHint) {
|
|
67
|
+
s += ` ${value.allowedHint}`
|
|
68
|
+
} else if (value.allowed) {
|
|
64
69
|
s += `: <${value.allowed.join(' | ')}>`
|
|
65
70
|
} else if (value.type) {
|
|
66
71
|
s += `: <${value.type}>`
|
|
67
72
|
}
|
|
68
|
-
if (value.default) {
|
|
73
|
+
if (value.defaultHint || value.default) {
|
|
69
74
|
s += EOL
|
|
70
|
-
s += indent(`(default: ${value.default})`, ' ')
|
|
75
|
+
s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
|
|
71
76
|
}
|
|
72
77
|
s += `${EOL2}${indent(value.desc, ' ')}`
|
|
73
78
|
return s
|
|
@@ -92,10 +97,16 @@ const main = async args => {
|
|
|
92
97
|
if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
|
|
93
98
|
args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
|
|
94
99
|
}
|
|
100
|
+
const newLogLevel = deprecated[args.named.logLevel]
|
|
101
|
+
if (newLogLevel) {
|
|
102
|
+
console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
|
|
103
|
+
args.named.logLevel = newLogLevel
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
compileFromFile(args.positional, {
|
|
96
107
|
// temporary fix until rootDir is faded out
|
|
97
108
|
outputDirectory: [args.named.outputDirectory, args.named.rootDir].find(d => d !== './') ?? './',
|
|
98
|
-
logLevel:
|
|
109
|
+
logLevel: args.named.logLevel,
|
|
99
110
|
jsConfigPath: args.named.jsConfigPath,
|
|
100
111
|
inlineDeclarations: args.named.inlineDeclarations,
|
|
101
112
|
propertiesOptional: args.named.propertiesOptional === 'true',
|
package/lib/compile.js
CHANGED
|
@@ -5,21 +5,20 @@ const { normalize } = 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
|
-
const { Logger } = require('./logging')
|
|
9
8
|
const { Visitor } = require('./visitor')
|
|
9
|
+
const { LOG, setLevel } = require('./logging')
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @typedef {import('./
|
|
12
|
+
* @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
17
17
|
* Tries to merge nicely if an existing file is found.
|
|
18
18
|
* @param {string} path - filepath to jsconfig.json.
|
|
19
|
-
* @param {import('./logging').Logger} logger - logger
|
|
20
19
|
* @private
|
|
21
20
|
*/
|
|
22
|
-
const writeJsConfig =
|
|
21
|
+
const writeJsConfig = path => {
|
|
23
22
|
let values = {
|
|
24
23
|
compilerOptions: {
|
|
25
24
|
checkJs: true,
|
|
@@ -29,7 +28,7 @@ const writeJsConfig = (path, logger) => {
|
|
|
29
28
|
if (fs.existsSync(path)) {
|
|
30
29
|
const currentContents = JSON.parse(fs.readFileSync(path))
|
|
31
30
|
if (currentContents?.compilerOptions?.checkJs) {
|
|
32
|
-
|
|
31
|
+
LOG.warn(`jsconfig at location ${path} already exists. Attempting to merge.`)
|
|
33
32
|
}
|
|
34
33
|
util.deepMerge(currentContents, values)
|
|
35
34
|
values = currentContents
|
|
@@ -40,20 +39,19 @@ const writeJsConfig = (path, logger) => {
|
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
41
|
* Compiles a CSN object to Typescript types.
|
|
43
|
-
* @param {{xtended: CSN, inferred: CSN}} csn
|
|
42
|
+
* @param {{xtended: CSN, inferred: CSN}} csn - csn tuple
|
|
44
43
|
* @param {CompileParameters} parameters - path to root directory for all generated files, min log level
|
|
45
44
|
*/
|
|
46
45
|
const compileFromCSN = async (csn, parameters) => {
|
|
47
46
|
const envSettings = cds.env?.typer ?? {}
|
|
48
47
|
parameters = { ...envSettings, ...parameters }
|
|
49
|
-
|
|
50
|
-
logger.addFrom(parameters.logLevel)
|
|
48
|
+
setLevel(parameters.logLevel)
|
|
51
49
|
if (parameters.jsConfigPath) {
|
|
52
|
-
writeJsConfig(parameters.jsConfigPath
|
|
50
|
+
writeJsConfig(parameters.jsConfigPath)
|
|
53
51
|
}
|
|
54
52
|
return writeout(
|
|
55
53
|
parameters.outputDirectory,
|
|
56
|
-
Object.values(new Visitor(csn, parameters
|
|
54
|
+
Object.values(new Visitor(csn, parameters).getWriteoutFiles())
|
|
57
55
|
)
|
|
58
56
|
}
|
|
59
57
|
|
package/lib/components/enum.js
CHANGED
|
@@ -3,7 +3,7 @@ const { normalise } = require('./identifier')
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts all unique values from a list of enum key-value pairs.
|
|
5
5
|
* If the value is an object, then the `.val` property is used.
|
|
6
|
-
* @param {[string, any | {val: any}][]} kvs
|
|
6
|
+
* @param {[string, any | {val: any}][]} kvs - key value pairs
|
|
7
7
|
*/
|
|
8
8
|
const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type
|
|
9
9
|
|
|
@@ -44,10 +44,10 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
|
|
|
44
44
|
* @param {Buffer} buffer - Buffer to write into
|
|
45
45
|
* @param {string} name - local name of the enum, i.e. the name under which it should be created in the .ts file
|
|
46
46
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
47
|
-
* @param {object} options
|
|
47
|
+
* @param {object} options - options for printing the enum
|
|
48
48
|
*/
|
|
49
49
|
function printEnum(buffer, name, kvs, options = {}) {
|
|
50
|
-
const opts = {...{export: true}, ...options}
|
|
50
|
+
const opts = {...{export: true}, ...options}
|
|
51
51
|
buffer.add('// enum')
|
|
52
52
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
53
53
|
kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
|
|
@@ -60,14 +60,14 @@ function printEnum(buffer, name, kvs, options = {}) {
|
|
|
60
60
|
* Converts a CSN type describing an enum into a list of kv-pairs.
|
|
61
61
|
* Values from CSN are unwrapped from their `.val` structure and
|
|
62
62
|
* will fall back to the key if no value is provided.
|
|
63
|
-
* @param {{enum: {[key: name]: string}, type: string}} enumCsn
|
|
63
|
+
* @param {{enum: {[key: name]: string}, type: string}} enumCsn - the CSN type describing the enum
|
|
64
64
|
* @param {{unwrapVals: boolean}} options - if `unwrapVals` is passed,
|
|
65
65
|
* then the CSN structure `{val:x}` is flattened to just `x`.
|
|
66
66
|
* Retaining `val` is closer to the actual CSN structure and should be used where we want
|
|
67
67
|
* to mimic the runtime as closely as possible (inline enum types).
|
|
68
68
|
* Stripping that additional wrapper would be more readable for users.
|
|
69
69
|
* @example
|
|
70
|
-
* ```ts
|
|
70
|
+
* ```ts
|
|
71
71
|
* const csn = {enum: {X: {val: 'a'}, Y: {val: 'b'}, Z: {}}}
|
|
72
72
|
* csnToEnumPairs(csn) // -> [['X', 'a'], ['Y': 'b'], ['Z': 'Z']]
|
|
73
73
|
* csnToEnumPairs(csn, {unwrapVals: false}) // -> [['X', {val:'a'}], ['Y': {val:'b'}], ['Z':'Z']]
|
|
@@ -82,8 +82,8 @@ const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* @param {string} entity
|
|
86
|
-
* @param {string} property
|
|
85
|
+
* @param {string} entity - the entity to which the property belongs
|
|
86
|
+
* @param {string} property - the property name
|
|
87
87
|
*/
|
|
88
88
|
const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
|
|
89
89
|
|
|
@@ -91,8 +91,8 @@ const propertyToInlineEnumName = (entity, property) => `${entity}_${property}`
|
|
|
91
91
|
* A type is considered to be an inline enum, iff it has a `.enum` property
|
|
92
92
|
* _and_ its type is a CDS primitive, i.e. it is not contained in `cds.definitions`.
|
|
93
93
|
* If it is contained there, then it is a standard enum declaration that has its own name.
|
|
94
|
-
* @param {{type: string}} element
|
|
95
|
-
* @param {object} csn
|
|
94
|
+
* @param {{type: string}} element - the element to check
|
|
95
|
+
* @param {object} csn - the CSN model
|
|
96
96
|
* @returns boolean
|
|
97
97
|
*/
|
|
98
98
|
const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn.definitions)
|
|
@@ -107,12 +107,12 @@ const isInlineEnumType = (element, csn) => element.enum && !(element.type in csn
|
|
|
107
107
|
* }
|
|
108
108
|
* ```
|
|
109
109
|
* becomes
|
|
110
|
-
*
|
|
110
|
+
*
|
|
111
111
|
* ```js
|
|
112
112
|
* module.exports.Language = { DE: "German", EN: "English", FR: "FR" }
|
|
113
113
|
* ```
|
|
114
|
-
* @param {string} name
|
|
115
|
-
* @param {[string, string][]} kvs - a list of key-value pairs. Values that are falsey are replaced by
|
|
114
|
+
* @param {string} name - the enum name
|
|
115
|
+
* @param {[string, string][]} kvs - a list of key-value pairs. Values that are falsey are replaced by
|
|
116
116
|
*/
|
|
117
117
|
// ??= for inline enums. If there is some static property of that name, we don't want to override it (for example: ".actions"
|
|
118
118
|
const stringifyEnumImplementation = (name, kvs) => `module.exports.${name} ??= { ${kvs.map(([k,v]) => `${normalise(k)}: ${v}`).join(', ')} }`
|
|
@@ -10,6 +10,14 @@ const normalise = ident => ident && !isValidIdent.test(ident)
|
|
|
10
10
|
? `"${ident}"`
|
|
11
11
|
: ident
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Returns the last part of a dot-separated identifier.
|
|
15
|
+
* @param {string} ident - the identifier to extract the last part from
|
|
16
|
+
* @returns {string} the last part of the identifier
|
|
17
|
+
*/
|
|
18
|
+
const last = ident => ident.split('.').at(-1)
|
|
19
|
+
|
|
13
20
|
module.exports = {
|
|
14
|
-
normalise
|
|
21
|
+
normalise,
|
|
22
|
+
last
|
|
15
23
|
}
|
package/lib/components/inline.js
CHANGED
|
@@ -2,6 +2,8 @@ const { SourceFile, Buffer } = require('../file')
|
|
|
2
2
|
const { normalise } = require('./identifier')
|
|
3
3
|
const { docify } = require('./wrappers')
|
|
4
4
|
|
|
5
|
+
/** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Inline declarations of types can come in different flavours.
|
|
7
9
|
* The compiler can therefore be adjusted to print out one or the other
|
|
@@ -10,20 +12,20 @@ const { docify } = require('./wrappers')
|
|
|
10
12
|
*/
|
|
11
13
|
class InlineDeclarationResolver {
|
|
12
14
|
/**
|
|
13
|
-
* @param {string}
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {import('../file').Buffer} buffer
|
|
16
|
-
* @param {string} statementEnd
|
|
15
|
+
* @param {string} fq - full qualifier of the type
|
|
16
|
+
* @param {TypeResolveInfo} type - type info so far
|
|
17
|
+
* @param {import('../file').Buffer} buffer - the buffer to write into
|
|
18
|
+
* @param {string} statementEnd - statement ending character
|
|
17
19
|
* @protected
|
|
18
20
|
* @abstract
|
|
19
21
|
*/
|
|
20
22
|
// eslint-disable-next-line no-unused-vars
|
|
21
|
-
printInlineType(
|
|
23
|
+
printInlineType(fq, type, buffer, statementEnd) { /* abstract */ }
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Attempts to resolve a type that could reference another type.
|
|
25
|
-
* @param {any} items
|
|
26
|
-
* @param {
|
|
27
|
+
* @param {any} items - properties of the declaration we are resolving
|
|
28
|
+
* @param {TypeResolveInfo} into - @see Visitor.resolveType
|
|
27
29
|
* @param {SourceFile} relativeTo - file to which the resolved type should be relative to
|
|
28
30
|
* @public
|
|
29
31
|
*/
|
|
@@ -57,7 +59,7 @@ class InlineDeclarationResolver {
|
|
|
57
59
|
/**
|
|
58
60
|
* Visits a single element in an entity.
|
|
59
61
|
* @param {string} name - name of the element
|
|
60
|
-
* @param {import('
|
|
62
|
+
* @param {import('../resolution/resolver').CSN} element - CSN data belonging to the the element.
|
|
61
63
|
* @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
|
|
62
64
|
* @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
63
65
|
* @public
|
|
@@ -87,7 +89,7 @@ class InlineDeclarationResolver {
|
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
91
|
* It returns TypeScript datatype for provided TS property
|
|
90
|
-
* @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type
|
|
92
|
+
* @param {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} type - type of the property
|
|
91
93
|
* @param {string} typeName - name of the TypeScript property
|
|
92
94
|
* @returns {string} the datatype to be presented on TypeScript layer
|
|
93
95
|
* @public
|
|
@@ -96,7 +98,7 @@ class InlineDeclarationResolver {
|
|
|
96
98
|
return type.typeInfo.isNotNull ? typeName : `${typeName} | null`
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
/** @param {import('../visitor').Visitor} visitor */
|
|
101
|
+
/** @param {import('../visitor').Visitor} visitor - the visitor */
|
|
100
102
|
constructor(visitor) {
|
|
101
103
|
this.visitor = visitor
|
|
102
104
|
// type resolution might recurse. This indicator is used to determine
|
|
@@ -114,7 +116,7 @@ class InlineDeclarationResolver {
|
|
|
114
116
|
* b: number
|
|
115
117
|
* }
|
|
116
118
|
* }
|
|
117
|
-
*
|
|
119
|
+
*
|
|
118
120
|
* T['a']['b'] // number
|
|
119
121
|
* ```
|
|
120
122
|
* but especially with inline declarations, the access will differ between flattened and nested representations.
|
|
@@ -187,9 +189,9 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
187
189
|
* ```
|
|
188
190
|
*/
|
|
189
191
|
class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
190
|
-
constructor(visitor) {
|
|
192
|
+
constructor(visitor) {
|
|
191
193
|
super(visitor)
|
|
192
|
-
this.printDepth = 0
|
|
194
|
+
this.printDepth = 0
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
flatten(name, type, buffer, statementEnd = ';') {
|
|
@@ -5,35 +5,35 @@ const base = '__'
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Wraps type into association to scalar.
|
|
8
|
-
* @param {string} t - the singular type name.
|
|
8
|
+
* @param {string} t - the singular type name.
|
|
9
9
|
* @returns {string}
|
|
10
10
|
*/
|
|
11
11
|
const createToOneAssociation = t => `${base}.Association.to<${t}>`
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Wraps type into association to vector.
|
|
15
|
-
* @param {string} t - the singular type name.
|
|
15
|
+
* @param {string} t - the singular type name.
|
|
16
16
|
* @returns {string}
|
|
17
17
|
*/
|
|
18
18
|
const createToManyAssociation = t => `${base}.Association.to.many<${t}>`
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Wraps type into composition of scalar.
|
|
22
|
-
* @param {string} t - the singular type name.
|
|
22
|
+
* @param {string} t - the singular type name.
|
|
23
23
|
* @returns {string}
|
|
24
24
|
*/
|
|
25
25
|
const createCompositionOfOne = t => `${base}.Composition.of<${t}>`
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Wraps type into composition of vector.
|
|
29
|
-
* @param {string} t - the singular type name.
|
|
29
|
+
* @param {string} t - the singular type name.
|
|
30
30
|
* @returns {string}
|
|
31
31
|
*/
|
|
32
32
|
const createCompositionOfMany = t => `${base}.Composition.of.many<${t}>`
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Wraps type into an array.
|
|
36
|
-
* @param {string} t - the singular type name.
|
|
36
|
+
* @param {string} t - the singular type name.
|
|
37
37
|
* @returns {string}
|
|
38
38
|
*/
|
|
39
39
|
const createArrayOf = t => `Array<${t}>`
|
|
@@ -47,7 +47,7 @@ const createObjectOf = t => `{${t}}`
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
50
|
-
* @param {string} t - the singular type name.
|
|
50
|
+
* @param {string} t - the singular type name.
|
|
51
51
|
* @param {string?} lookup - a property lookup of the required type (`['Foo']`)
|
|
52
52
|
* @returns {string}
|
|
53
53
|
*/
|
|
@@ -59,8 +59,8 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
|
|
|
59
59
|
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
60
60
|
* concatenated to be properly indented by `buffer.add(...)`.
|
|
61
61
|
*/
|
|
62
|
-
const docify = doc => doc
|
|
63
|
-
? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
|
|
62
|
+
const docify = doc => doc
|
|
63
|
+
? ['/**'].concat(doc.split('\n').map(line => `* ${line}`)).concat(['*/'])
|
|
64
64
|
: []
|
|
65
65
|
|
|
66
66
|
module.exports = {
|
package/lib/csn.js
CHANGED
|
@@ -5,15 +5,20 @@ const annotation = '@odata.draft.enabled'
|
|
|
5
5
|
* i.e. ones that have a query, but are not a cds level projection.
|
|
6
6
|
* Those are still not expanded and we have to retrieve their definition
|
|
7
7
|
* with all properties from the inferred model.
|
|
8
|
-
* @param {any} entity
|
|
8
|
+
* @param {any} entity - the entity
|
|
9
9
|
*/
|
|
10
10
|
const isView = entity => entity.query && !entity.projection
|
|
11
11
|
|
|
12
12
|
const isProjection = entity => entity.projection
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} entity - the entity
|
|
16
|
+
* @see isView
|
|
17
|
+
* Unresolved entities have to be looked up from inferred csn.
|
|
18
|
+
*/
|
|
19
|
+
const isUnresolved = entity => entity._unresolved === true
|
|
15
20
|
|
|
16
|
-
const
|
|
21
|
+
const isCsnAny = entity => entity?.constructor?.name === 'any'
|
|
17
22
|
|
|
18
23
|
const isDraftEnabled = entity => entity['@odata.draft.enabled'] === true
|
|
19
24
|
|
|
@@ -22,13 +27,20 @@ const isType = entity => entity?.kind === 'type'
|
|
|
22
27
|
const isEntity = entity => entity?.kind === 'entity'
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @
|
|
27
|
-
*
|
|
30
|
+
* Attempts to retrieve the max cardinality of a CSN for an entity.
|
|
31
|
+
* @param {EntityCSN} element - csn of entity to retrieve cardinality for
|
|
32
|
+
* @returns {number} max cardinality of the element.
|
|
33
|
+
* If no cardinality is attached to the element, cardinality is 1.
|
|
34
|
+
* If it is set to '*', result is Infinity.
|
|
28
35
|
*/
|
|
29
|
-
const
|
|
36
|
+
const getMaxCardinality = element => {
|
|
37
|
+
const cardinality = element?.cardinality?.max ?? 1
|
|
38
|
+
return cardinality === '*' ? Infinity : parseInt(cardinality)
|
|
39
|
+
}
|
|
30
40
|
|
|
31
|
-
const
|
|
41
|
+
const getViewTarget = entity => entity.query?.SELECT?.from?.ref?.[0]
|
|
42
|
+
|
|
43
|
+
const getProjectionTarget = entity => entity.projection?.from?.ref?.[0]
|
|
32
44
|
|
|
33
45
|
class DraftUnroller {
|
|
34
46
|
/** @type {Set<string>} */
|
|
@@ -36,7 +48,7 @@ class DraftUnroller {
|
|
|
36
48
|
/** @type {{[key: string]: boolean}} */
|
|
37
49
|
#draftable = {}
|
|
38
50
|
/** @type {{[key: string]: string}} */
|
|
39
|
-
#projections
|
|
51
|
+
#projections
|
|
40
52
|
/** @type {object[]} */
|
|
41
53
|
#entities
|
|
42
54
|
#csn
|
|
@@ -56,7 +68,7 @@ class DraftUnroller {
|
|
|
56
68
|
* @param {object | string} entity - entity to set draftable annotation for.
|
|
57
69
|
* @param {boolean} value - whether the entity is draftable.
|
|
58
70
|
*/
|
|
59
|
-
#setDraftable(entity, value) {
|
|
71
|
+
#setDraftable(entity, value) {
|
|
60
72
|
if (typeof entity === 'string') entity = this.#getDefinition(entity)
|
|
61
73
|
if (!entity) return // inline definition -- not found in definitions
|
|
62
74
|
entity[annotation] = value
|
|
@@ -75,10 +87,10 @@ class DraftUnroller {
|
|
|
75
87
|
#getDraftable(entityOrName) {
|
|
76
88
|
const entity = (typeof entityOrName === 'string')
|
|
77
89
|
? this.#getDefinition(entityOrName)
|
|
78
|
-
: entityOrName
|
|
90
|
+
: entityOrName
|
|
79
91
|
// assert(typeof entity !== 'string')
|
|
80
92
|
const name = entity?.name ?? entityOrName
|
|
81
|
-
return this.#draftable[name] ??= this.#propagateInheritance(entity)
|
|
93
|
+
return this.#draftable[name] ??= this.#propagateInheritance(entity)
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/**
|
|
@@ -156,11 +168,11 @@ class DraftUnroller {
|
|
|
156
168
|
/**
|
|
157
169
|
* We are unrolling the @odata.draft.enabled annotations into related entities manually.
|
|
158
170
|
* This includes three scenarios:
|
|
159
|
-
*
|
|
171
|
+
*
|
|
160
172
|
* (a) aspects via `A: B`, where `B` is draft enabled.
|
|
161
173
|
* Note that when an entity extends two other entities of which one has drafts enabled and
|
|
162
174
|
* one has not, then the one that is later in the list of mixins "wins":
|
|
163
|
-
* @param {any} csn
|
|
175
|
+
* @param {any} csn - the entity
|
|
164
176
|
* @example
|
|
165
177
|
* ```ts
|
|
166
178
|
* @odata.draft.enabled true
|
|
@@ -170,7 +182,7 @@ class DraftUnroller {
|
|
|
170
182
|
* entity A: T,F {} // draft not enabled
|
|
171
183
|
* entity B: F,T {} // draft enabled
|
|
172
184
|
* ```
|
|
173
|
-
*
|
|
185
|
+
*
|
|
174
186
|
* (b) Draft enabled projections make the entity we project on draft enabled.
|
|
175
187
|
* @example
|
|
176
188
|
* ```ts
|
|
@@ -178,9 +190,9 @@ class DraftUnroller {
|
|
|
178
190
|
* entity A as projection on B {}
|
|
179
191
|
* entity B {} // draft enabled
|
|
180
192
|
* ```
|
|
181
|
-
*
|
|
193
|
+
*
|
|
182
194
|
* (c) Entities that are draft enabled propagate this property down through compositions:
|
|
183
|
-
*
|
|
195
|
+
*
|
|
184
196
|
* ```ts
|
|
185
197
|
* @odata.draft.enabled: true
|
|
186
198
|
* entity A {
|
|
@@ -201,7 +213,7 @@ function unrollDraftability(csn) {
|
|
|
201
213
|
*
|
|
202
214
|
* This explicit propagation is required to add foreign key relations
|
|
203
215
|
* to referring entities.
|
|
204
|
-
* @param {any} csn
|
|
216
|
+
* @param {any} csn - the entity
|
|
205
217
|
* @example
|
|
206
218
|
* ```cds
|
|
207
219
|
* entity A: cuid { key name: String; }
|
|
@@ -209,10 +221,10 @@ function unrollDraftability(csn) {
|
|
|
209
221
|
* ```
|
|
210
222
|
* must yield
|
|
211
223
|
* ```ts
|
|
212
|
-
* class A {
|
|
224
|
+
* class A {
|
|
213
225
|
* ID: UUID // inherited from cuid
|
|
214
226
|
* name: String;
|
|
215
|
-
* }
|
|
227
|
+
* }
|
|
216
228
|
* class B {
|
|
217
229
|
* ref: Association.to<A>
|
|
218
230
|
* ref_ID: UUID
|
|
@@ -235,7 +247,7 @@ function propagateForeignKeys(csn) {
|
|
|
235
247
|
const remoteKeys = Object.entries(this.associations ?? {})
|
|
236
248
|
.filter(([,{key}]) => key) // only follow associations that are keys, that way we avoid cycles
|
|
237
249
|
.flatMap(([kname, key]) => Object.entries(csn.definitions[key.target].keys)
|
|
238
|
-
.map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
|
|
250
|
+
.map(([ckname, ckey]) => [`${kname}_${ckname}`, ckey]))
|
|
239
251
|
|
|
240
252
|
this.__keys = Object.fromEntries(ownKeys
|
|
241
253
|
.concat(inheritedKeys)
|
|
@@ -251,7 +263,7 @@ function propagateForeignKeys(csn) {
|
|
|
251
263
|
|
|
252
264
|
/**
|
|
253
265
|
*
|
|
254
|
-
* @param {any} csn
|
|
266
|
+
* @param {any} csn - complete csn
|
|
255
267
|
*/
|
|
256
268
|
function amendCSN(csn) {
|
|
257
269
|
unrollDraftability(csn)
|
|
@@ -274,14 +286,15 @@ const getProjectionAliases = entity => {
|
|
|
274
286
|
return { aliases, all }
|
|
275
287
|
}
|
|
276
288
|
|
|
277
|
-
module.exports = {
|
|
278
|
-
amendCSN,
|
|
279
|
-
isView,
|
|
289
|
+
module.exports = {
|
|
290
|
+
amendCSN,
|
|
291
|
+
isView,
|
|
280
292
|
isProjection,
|
|
281
293
|
isDraftEnabled,
|
|
282
294
|
isEntity,
|
|
283
295
|
isUnresolved,
|
|
284
296
|
isType,
|
|
297
|
+
getMaxCardinality,
|
|
285
298
|
getProjectionTarget,
|
|
286
299
|
getProjectionAliases,
|
|
287
300
|
getViewTarget,
|