@cap-js/cds-typer 0.38.0 → 0.39.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/lib/cli.js +5 -1
- package/lib/components/class.js +1 -1
- package/lib/components/enum.js +3 -3
- package/lib/components/identifier.js +183 -7
- package/lib/components/inline.js +15 -6
- package/lib/file.js +37 -30
- package/lib/printers/wrappers.js +12 -2
- package/lib/resolution/entity.js +6 -6
- package/lib/resolution/resolver.js +229 -74
- package/lib/typedefs.d.ts +12 -7
- package/lib/util.js +4 -2
- package/lib/visitor.js +135 -62
- package/package.json +6 -1
package/lib/cli.js
CHANGED
|
@@ -213,7 +213,11 @@ const flags = enrichFlagSchema({
|
|
|
213
213
|
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.`,
|
|
214
214
|
allowed: ['none', 'blake2s256'],
|
|
215
215
|
default: 'none'
|
|
216
|
-
}
|
|
216
|
+
},
|
|
217
|
+
brandedPrimitiveTypes: parameterTypes.boolean({
|
|
218
|
+
desc: `If set to true, generated primitive types will be branded to prevent accidental mixing of types with the same underlying primitive.${EOL}E.g., type CustomerID = string & { __brand: 'CustomerID' }`,
|
|
219
|
+
default: 'false'
|
|
220
|
+
})
|
|
217
221
|
})
|
|
218
222
|
|
|
219
223
|
const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
|
package/lib/components/class.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Create a class member with given modifiers in the right order.
|
|
2
|
+
* Create a class member with given modifiers in the right order and returns the resulting string.
|
|
3
3
|
* @param {object} options - options
|
|
4
4
|
* @param {string} options.name - the name of the member
|
|
5
5
|
* @param {string} [options.type] - the type of the member
|
package/lib/components/enum.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { enquote } = require('./identifier')
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts all unique values from a list of enum key-value pairs.
|
|
@@ -57,7 +57,7 @@ function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
|
57
57
|
buffer.add('// enum')
|
|
58
58
|
if (opts.export) buffer.add(doc)
|
|
59
59
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
60
|
-
kvs.forEach(([k, v]) => { buffer.add(`${
|
|
60
|
+
kvs.forEach(([k, v]) => { buffer.add(`${enquote(k)}: ${v},`) })
|
|
61
61
|
, '} as const;')
|
|
62
62
|
buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`)
|
|
63
63
|
buffer.blankLine()
|
|
@@ -126,7 +126,7 @@ const isInlineEnumType = (element, csn) => element.enum
|
|
|
126
126
|
*/
|
|
127
127
|
const stringifyEnumImplementation = (name, kvs, jsp) => jsp.printExport(
|
|
128
128
|
name,
|
|
129
|
-
`{ ${kvs.map(([k,v]) => `${
|
|
129
|
+
`{ ${kvs.map(([k,v]) => `${enquote(k)}: ${v}`).join(', ')} }`,
|
|
130
130
|
{ coalesce: true })
|
|
131
131
|
|
|
132
132
|
|
|
@@ -1,12 +1,84 @@
|
|
|
1
|
+
const { createHash } = require('node:crypto')
|
|
2
|
+
const { LOG } = require('../logging')
|
|
3
|
+
|
|
4
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
|
|
5
|
+
const JS_RESERVED = new Set([
|
|
6
|
+
'abstract',
|
|
7
|
+
'arguments',
|
|
8
|
+
'await',
|
|
9
|
+
'boolean',
|
|
10
|
+
'break',
|
|
11
|
+
'byte',
|
|
12
|
+
'case',
|
|
13
|
+
'catch',
|
|
14
|
+
'char',
|
|
15
|
+
'class',
|
|
16
|
+
'const',
|
|
17
|
+
'continue',
|
|
18
|
+
'debugger',
|
|
19
|
+
'default',
|
|
20
|
+
'delete',
|
|
21
|
+
'do',
|
|
22
|
+
'double',
|
|
23
|
+
'else',
|
|
24
|
+
'enum',
|
|
25
|
+
'eval',
|
|
26
|
+
'export',
|
|
27
|
+
'extends',
|
|
28
|
+
'false',
|
|
29
|
+
'final',
|
|
30
|
+
'finally',
|
|
31
|
+
'float',
|
|
32
|
+
'for',
|
|
33
|
+
'function',
|
|
34
|
+
'goto',
|
|
35
|
+
'if',
|
|
36
|
+
'implements',
|
|
37
|
+
'import',
|
|
38
|
+
'in',
|
|
39
|
+
'instanceof',
|
|
40
|
+
'int',
|
|
41
|
+
'interface',
|
|
42
|
+
'let',
|
|
43
|
+
'long',
|
|
44
|
+
'native',
|
|
45
|
+
'new',
|
|
46
|
+
'null',
|
|
47
|
+
'object',
|
|
48
|
+
'package',
|
|
49
|
+
'private',
|
|
50
|
+
'protected',
|
|
51
|
+
'public',
|
|
52
|
+
'return',
|
|
53
|
+
'short',
|
|
54
|
+
'static',
|
|
55
|
+
'super',
|
|
56
|
+
'switch',
|
|
57
|
+
'synchronized',
|
|
58
|
+
'this',
|
|
59
|
+
'throw',
|
|
60
|
+
'throws',
|
|
61
|
+
'transient',
|
|
62
|
+
'true',
|
|
63
|
+
'try',
|
|
64
|
+
'typeof',
|
|
65
|
+
'var',
|
|
66
|
+
'void',
|
|
67
|
+
'volatile',
|
|
68
|
+
'while',
|
|
69
|
+
'with',
|
|
70
|
+
'yield',
|
|
71
|
+
])
|
|
72
|
+
|
|
1
73
|
const isValidIdent = /^[_$a-zA-Z][$\w]*$/
|
|
2
74
|
|
|
3
75
|
/**
|
|
4
|
-
*
|
|
76
|
+
* Enquotes an identifier to a valid JavaScript identifier.
|
|
5
77
|
* I.e. either the identifier itself or a quoted string.
|
|
6
|
-
* @param {string} ident - the identifier to
|
|
7
|
-
* @returns {string} the
|
|
78
|
+
* @param {string} ident - the identifier to enquote
|
|
79
|
+
* @returns {string} the enquoted identifier
|
|
8
80
|
*/
|
|
9
|
-
const
|
|
81
|
+
const enquote = ident => ident && !isValidIdent.test(ident)
|
|
10
82
|
? `"${ident}"`
|
|
11
83
|
: ident
|
|
12
84
|
|
|
@@ -17,7 +89,111 @@ const normalise = ident => ident && !isValidIdent.test(ident)
|
|
|
17
89
|
*/
|
|
18
90
|
const last = ident => ident.split('.').at(-1) ?? ident
|
|
19
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Normalises the name of a service or entity.
|
|
94
|
+
* This function is suited to normalise class names.
|
|
95
|
+
* To handle properties with exotic characters, see {@link enquote}.
|
|
96
|
+
* @param {string} name - name of the entity or fq thereof.
|
|
97
|
+
*/
|
|
98
|
+
const normalise = name => {
|
|
99
|
+
const namespace = name.split('.').slice(0, -1)
|
|
100
|
+
const unqualified = /** @type {string} */ (name.split('.').at(-1))
|
|
101
|
+
let normalised = unqualified.match(/^[a-zA-Z]+\w*$/)
|
|
102
|
+
? unqualified
|
|
103
|
+
: `__${unqualified.replaceAll(/[^a-zA-Z0-9]/g, '_')}`
|
|
104
|
+
if (/^_+$/.test(normalised)) {
|
|
105
|
+
// very rare case when the entire name consists of non-alphanumeric characters (Kanji, etc)
|
|
106
|
+
// That would leave us with just underscores,
|
|
107
|
+
// which has high potential of colliding with other such names
|
|
108
|
+
// -> fall back to hash. Hashes still bear a small risk of collision, but much lower.
|
|
109
|
+
// Prepend underscore to avoid problems with hashes starting with a digit.
|
|
110
|
+
normalised = '_' + createHash('md5').update(name).digest('hex')
|
|
111
|
+
}
|
|
112
|
+
if (JS_RESERVED.has(normalised)) {
|
|
113
|
+
normalised = `__${name}`
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
original: name,
|
|
117
|
+
unqualified,
|
|
118
|
+
normalised: [...namespace, normalised].filter(Boolean).join('.'),
|
|
119
|
+
wasNormalised: unqualified !== normalised
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class Identifier {
|
|
124
|
+
/** @type {string} */
|
|
125
|
+
plain
|
|
126
|
+
/** @type {string[]} */
|
|
127
|
+
scope = []
|
|
128
|
+
/** @type {Identifier | null} */
|
|
129
|
+
#from = null
|
|
130
|
+
/** @type {Identifier | undefined} */
|
|
131
|
+
#normalised
|
|
132
|
+
/** @type {boolean} */
|
|
133
|
+
#sealed = false
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @returns whether normalisation was applied. This does not mean the identifier has changed!
|
|
137
|
+
* If it did not require normalisation, this will be true regardless.
|
|
138
|
+
* {@link Identifier#isChangedFromNormalisation} tells whether the identifier actually changed.
|
|
139
|
+
*/
|
|
140
|
+
get isNormalised () {
|
|
141
|
+
return this.#sealed || Boolean(this.#from)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @returns whether the identifier actually changed due to normalisation.
|
|
146
|
+
*/
|
|
147
|
+
get isChangedFromNormalisation () {
|
|
148
|
+
return this.isNormalised && this.#from !== null && this.plain !== this.#from.plain
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @returns {Identifier}
|
|
153
|
+
*/
|
|
154
|
+
get normalised () {
|
|
155
|
+
return this.isNormalised
|
|
156
|
+
? this
|
|
157
|
+
: this.#normalised ??= new Identifier(normalise(this.plain).normalised, this.scope, this)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get scoped () {
|
|
161
|
+
return [...this.scope, this.plain].join('.')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string | Identifier} name - the inflected name
|
|
166
|
+
* @param {string[]} [scope] - the scope of the identifier
|
|
167
|
+
* @param {Identifier | null} [from] - the original inflection if this is a normalised one
|
|
168
|
+
* @param {boolean} [sealed] - whether the identifier should be sealed to prevent further normalisation (e.g., for inline declarations).
|
|
169
|
+
* Setting this to true also prevents automatic scope splitting for identifiers with dots.
|
|
170
|
+
* @example
|
|
171
|
+
* ```js
|
|
172
|
+
* new Identifier('Books.texts') // name: 'texts', scope: ['Books']
|
|
173
|
+
* new Identifier('text', ['Books']) // name: 'text', scope: ['Books']
|
|
174
|
+
* new Identifier(new Identifier('Books.text'), ['Books']) // name: 'text', scope: ['Books'] -> as explicit scope is passed, the original name is retained as is!
|
|
175
|
+
* new Identifier('{x: Books.text}', null, true) // name: '{x: Books.text}', scope: [] -> as sealed is true, the original name is retained as is, and no scope splitting is applied, even though there are dots in the name!
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
constructor (name, scope = [], from = null, sealed = false) {
|
|
179
|
+
this.plain = typeof name === 'string' ? name : name.plain
|
|
180
|
+
this.scope = scope
|
|
181
|
+
this.#from = from
|
|
182
|
+
this.#sealed = sealed
|
|
183
|
+
if (from && name !== from.plain) {
|
|
184
|
+
LOG.debug(`Identifier '${from.plain}' was normalised to '${name}'.`)
|
|
185
|
+
}
|
|
186
|
+
if (this.plain.includes('.') && !this.scope.length && !sealed) {
|
|
187
|
+
const parts = this.plain.split('.')
|
|
188
|
+
this.plain = /** @type {string} */ (parts.at(-1))
|
|
189
|
+
this.scope = parts.slice(0, -1)
|
|
190
|
+
LOG.debug(`Identifier with dots without explicitly specified scope detected. Split into scope '${this.scope.join('.')}' and name '${this.plain}'.`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
20
195
|
module.exports = {
|
|
21
|
-
|
|
22
|
-
last
|
|
23
|
-
|
|
196
|
+
enquote,
|
|
197
|
+
last,
|
|
198
|
+
Identifier,
|
|
199
|
+
}
|
package/lib/components/inline.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { configuration } = require('../config')
|
|
2
2
|
const { SourceFile, Buffer } = require('../file')
|
|
3
|
-
const {
|
|
3
|
+
const { enquote } = require('./identifier')
|
|
4
4
|
const { docify } = require('../printers/wrappers')
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
|
|
@@ -88,7 +88,13 @@ class InlineDeclarationResolver {
|
|
|
88
88
|
const type = this.visitor.resolver.resolveAndRequire(element, file, resolverOptions)
|
|
89
89
|
this.depth--
|
|
90
90
|
if (this.depth === 0) {
|
|
91
|
-
|
|
91
|
+
// For builtin types (Composition, Association, etc.), just print the property directly
|
|
92
|
+
// since the typeName is already fully resolved and doesn't need further flattening
|
|
93
|
+
if (type.typeInfo.isBuiltin) {
|
|
94
|
+
buffer.add(`${this.stringifyModifiers(modifiers)}${enquote(name)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`)
|
|
95
|
+
} else {
|
|
96
|
+
this.printInlineType({fq: name, type, buffer, modifiers})
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
return type
|
|
94
100
|
}
|
|
@@ -193,7 +199,7 @@ class FlatInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
193
199
|
? Object.entries(type.typeInfo.structuredType).map(
|
|
194
200
|
([k,v]) => this.flatten({prefix: `${this.prefix(prefix)}${k}`, type: v, modifiers}) // for flat we pass the modifiers!
|
|
195
201
|
).flat()
|
|
196
|
-
: [`${this.stringifyModifiers(modifiers)}${
|
|
202
|
+
: [`${this.stringifyModifiers(modifiers)}${enquote(prefix)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}`]
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
/**
|
|
@@ -254,8 +260,11 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
254
260
|
this.printDepth++
|
|
255
261
|
const lineEnding = this.printDepth > 1 ? ',' : statementEnd
|
|
256
262
|
if (type.typeInfo.structuredType) {
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
// When fq is empty, we're generating a bare struct type without a property name
|
|
264
|
+
// (used for inline declarations in compositions/associations)
|
|
265
|
+
buffer.add(fq
|
|
266
|
+
? `${this.stringifyModifiers(modifiers)}${enquote(fq)}${this.getPropertyTypeSeparator()} {`
|
|
267
|
+
: '{')
|
|
259
268
|
buffer.indent()
|
|
260
269
|
for (const [n, t] of Object.entries(type.typeInfo.structuredType)) {
|
|
261
270
|
this.flatten({fq: n, type: t, buffer})
|
|
@@ -263,7 +272,7 @@ class StructuredInlineDeclarationResolver extends InlineDeclarationResolver {
|
|
|
263
272
|
buffer.outdent()
|
|
264
273
|
buffer.add(`}${this.getPropertyDatatype(type, '')}${lineEnding}`)
|
|
265
274
|
} else {
|
|
266
|
-
buffer.add(`${this.stringifyModifiers(modifiers)}${
|
|
275
|
+
buffer.add(`${this.stringifyModifiers(modifiers)}${enquote(fq)}${this.getPropertyTypeSeparator()} ${this.getPropertyDatatype(type)}${lineEnding}`)
|
|
267
276
|
}
|
|
268
277
|
this.printDepth--
|
|
269
278
|
return buffer
|
package/lib/file.js
CHANGED
|
@@ -4,10 +4,10 @@ const fs = require('fs').promises
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { readFileSync } = require('fs')
|
|
6
6
|
const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = require('./components/enum')
|
|
7
|
-
const {
|
|
7
|
+
const { enquote } = require('./components/identifier')
|
|
8
8
|
const { empty } = require('./components/typescript')
|
|
9
9
|
const { proxyAccessFunction } = require('./components/javascript')
|
|
10
|
-
const { createObjectOf, stringIdent } = require('./printers/wrappers')
|
|
10
|
+
const { createObjectOf, stringIdent, docify } = require('./printers/wrappers')
|
|
11
11
|
const { configuration } = require('./config')
|
|
12
12
|
const { CJSPrinter, ESMPrinter } = require('./printers/javascript')
|
|
13
13
|
|
|
@@ -142,7 +142,7 @@ class SourceFile extends File {
|
|
|
142
142
|
this.classNames = {} // for .js file
|
|
143
143
|
/** @type {{[key: string]: any}} */
|
|
144
144
|
this.typeNames = {}
|
|
145
|
-
/** @type {
|
|
145
|
+
/** @type {{singular: string, plural: string, original: string, isNormalised: boolean}[]} */
|
|
146
146
|
this.inflections = []
|
|
147
147
|
/** @type {{ buffer: Buffer, names: string[]}} */
|
|
148
148
|
this.services = { buffer: new Buffer(), names: [] }
|
|
@@ -190,21 +190,21 @@ class SourceFile extends File {
|
|
|
190
190
|
*/
|
|
191
191
|
static stringifyLambda({name, parameters=[], returns='any', kind, initialiser, self='never', isStatic=false, callStyles={positional:true, named:true}, doc}) {
|
|
192
192
|
let docStr = doc?.length ? doc.join('\n')+'\n' : ''
|
|
193
|
-
const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${
|
|
193
|
+
const parameterTypes = parameters.map(({name, modifier, type, doc}) => `${doc?'\n'+doc:''}${enquote(name)}${modifier}: ${type}`).join(', ')
|
|
194
194
|
const parameterTypeAsObject = parameterTypes.length
|
|
195
195
|
? createObjectOf(parameterTypes)
|
|
196
196
|
: empty
|
|
197
197
|
const callableSignatures = []
|
|
198
198
|
if (callStyles.positional) {
|
|
199
|
-
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${
|
|
199
|
+
const paramTypesPositional = parameters.map(({name, type, doc}) => `${doc?'\n'+doc:''}${enquote(name)}: ${type}`).join(', ') // must not include ? modifiers
|
|
200
200
|
callableSignatures.push('// positional',`${docStr}(${paramTypesPositional}): ${returns}`) // docs shows up on action consumer side: `.action(...)`
|
|
201
201
|
}
|
|
202
202
|
if (callStyles.named) {
|
|
203
|
-
const parameterNames = createObjectOf(parameters.map(({name}) =>
|
|
203
|
+
const parameterNames = createObjectOf(parameters.map(({name}) => enquote(name)).join(', '))
|
|
204
204
|
callableSignatures.push('// named',`${docStr}(${parameterNames}: ${parameterTypeAsObject}): ${returns}`)
|
|
205
205
|
}
|
|
206
206
|
if (callableSignatures.length === 0) throw new Error('At least one call style must be specified')
|
|
207
|
-
let prefix = name ? `${
|
|
207
|
+
let prefix = name ? `${enquote(name)}: `: ''
|
|
208
208
|
if (prefix && isStatic) {
|
|
209
209
|
prefix = `static ${prefix}`
|
|
210
210
|
}
|
|
@@ -222,13 +222,15 @@ class SourceFile extends File {
|
|
|
222
222
|
* Adds a pair of singular and plural inflection.
|
|
223
223
|
* These are later used to generate the singular -> plural
|
|
224
224
|
* aliases in the index.js file.
|
|
225
|
-
* @param {
|
|
226
|
-
* @param {string}
|
|
227
|
-
* @param {string}
|
|
225
|
+
* @param {object} options - options
|
|
226
|
+
* @param {string} options.singular - singular type without namespace.
|
|
227
|
+
* @param {string} options.plural - plural type without namespace
|
|
228
|
+
* @param {string} options.original - original entity name without namespace.
|
|
228
229
|
* In many cases this will be the same as plural.
|
|
230
|
+
* @param {boolean} [options.isNormalised] - whether the passed names are already normalised.
|
|
229
231
|
*/
|
|
230
|
-
addInflection(singular, plural, original) {
|
|
231
|
-
this.inflections.push(
|
|
232
|
+
addInflection({singular, plural, original, isNormalised = false}) {
|
|
233
|
+
this.inflections.push({singular, plural, original, isNormalised})
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
/**
|
|
@@ -239,12 +241,22 @@ class SourceFile extends File {
|
|
|
239
241
|
* @param {'function' | 'action'} kind - kind of the node
|
|
240
242
|
* @param {string[]} doc - documentation for the function
|
|
241
243
|
* @param {{positional?: boolean, named?: boolean}} callStyles - how the operation can be called
|
|
244
|
+
* @param {string} [originalName] - the original name before normalization, if different
|
|
242
245
|
*/
|
|
243
|
-
addOperation(name, parameters, returns, kind, doc, callStyles) {
|
|
246
|
+
addOperation(name, parameters, returns, kind, doc, callStyles, originalName) {
|
|
244
247
|
// this.operations.buffer.add(`// ${kind}`)
|
|
245
248
|
if (doc) this.operations.buffer.add(doc.join('\n')) // docs shows up on action provider side: `.on(action,...)`
|
|
246
249
|
const [opener, content, closer] = SourceFile.stringifyLambda({name, parameters, returns, kind, doc, callStyles})
|
|
247
250
|
this.operations.buffer.addIndentedBlock(`export declare const ${opener}`, content, closer)
|
|
251
|
+
// Add alias export if normalized name differs from original
|
|
252
|
+
if (originalName && originalName !== name) {
|
|
253
|
+
this.operations.buffer.add(docify([
|
|
254
|
+
` This ${kind} can be accessed via:`,
|
|
255
|
+
` - Named import: \`import { ${name} } from '...'\``,
|
|
256
|
+
` - Aliased import: \`import { "${originalName}" as MyAlias } from '...'\`\n`
|
|
257
|
+
]))
|
|
258
|
+
this.operations.buffer.add(`export { ${name} as "${originalName}" }`)
|
|
259
|
+
}
|
|
248
260
|
this.operations.names.push(name)
|
|
249
261
|
}
|
|
250
262
|
|
|
@@ -326,9 +338,8 @@ class SourceFile extends File {
|
|
|
326
338
|
entityProxy.push(propertyName)
|
|
327
339
|
|
|
328
340
|
// REVISIT: find a better way to do this???
|
|
329
|
-
const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
|
|
330
|
-
|
|
331
341
|
if (buffer?.namespace) {
|
|
342
|
+
const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
|
|
332
343
|
const tempBuffer = new Buffer()
|
|
333
344
|
// we want to put the enums on class level
|
|
334
345
|
tempBuffer.indent()
|
|
@@ -338,6 +349,9 @@ class SourceFile extends File {
|
|
|
338
349
|
const [first,...rest] = buffer.parts
|
|
339
350
|
buffer.parts = [first, ...tempBuffer.parts, ...rest]
|
|
340
351
|
} else {
|
|
352
|
+
// top-level inline enums must be exported so they can be referenced cross-namespace
|
|
353
|
+
// (e.g. when a service projects an entity whose key has an inline enum type)
|
|
354
|
+
const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: true}, doc)
|
|
341
355
|
printEnumToBuffer(this.inlineEnums.buffer)
|
|
342
356
|
}
|
|
343
357
|
}
|
|
@@ -550,8 +564,8 @@ class SourceFile extends File {
|
|
|
550
564
|
// sorting the entries based on the number of dots in their singular.
|
|
551
565
|
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
552
566
|
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
553
|
-
.sort((
|
|
554
|
-
.flatMap((
|
|
567
|
+
.sort(({singular:a}, {singular:b}) => a.split('.').length - b.split('.').length)
|
|
568
|
+
.flatMap(({singular, plural, original, isNormalised}) => {
|
|
555
569
|
const { singularRhs, pluralRhs } = this.#getEntityExportsRhs(singular, original)
|
|
556
570
|
|
|
557
571
|
const exports = [
|
|
@@ -565,8 +579,9 @@ class SourceFile extends File {
|
|
|
565
579
|
// FIXME: we currently produce at most 3 entries.
|
|
566
580
|
// This could be an issue when the user re-used the original name in a @singular/@plural annotation.
|
|
567
581
|
// Seems unlikely, but we have to eliminate the original entry if users start running into this.
|
|
568
|
-
if (singular !== original) {
|
|
582
|
+
if (singular !== original && !isNormalised) {
|
|
569
583
|
// do not do the is_singular spiel if the original name is used for the plural
|
|
584
|
+
// or if we had to normalise the name, which would imply an invalid identifier we can not use anyway
|
|
570
585
|
const rhs = plural === original ? pluralRhs : singularRhs
|
|
571
586
|
exports.push(jsp.printExport(original, rhs))
|
|
572
587
|
}
|
|
@@ -592,21 +607,13 @@ class Buffer {
|
|
|
592
607
|
* @param {string} indentation - indentation to use (two spaces by default)
|
|
593
608
|
*/
|
|
594
609
|
constructor(indentation = ' ') {
|
|
595
|
-
/**
|
|
596
|
-
* @type {string[]}
|
|
597
|
-
*/
|
|
610
|
+
/** @type {string[]} */
|
|
598
611
|
this.parts = []
|
|
599
|
-
/**
|
|
600
|
-
* @type {string}
|
|
601
|
-
*/
|
|
612
|
+
/** @type {string} */
|
|
602
613
|
this.indentation = indentation
|
|
603
|
-
/**
|
|
604
|
-
* @type {string}
|
|
605
|
-
*/
|
|
614
|
+
/** @type {string} */
|
|
606
615
|
this.currentIndent = ''
|
|
607
|
-
/**
|
|
608
|
-
* @type {boolean}
|
|
609
|
-
*/
|
|
616
|
+
/** @type {boolean} */
|
|
610
617
|
this.closed = false
|
|
611
618
|
/**
|
|
612
619
|
* Required for inline enums of inline compositions or text entities
|
package/lib/printers/wrappers.js
CHANGED
|
@@ -105,6 +105,12 @@ const createIntersectionOf = (...types) => types.join(' & ')
|
|
|
105
105
|
*/
|
|
106
106
|
const createPromiseOf = t => `globalThis.Promise<${t}>`
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Creates a branded type.
|
|
110
|
+
* @param {string} t - the type to brand.
|
|
111
|
+
*/
|
|
112
|
+
const createBrandedType = t => `{ __brand: '${t}' }`
|
|
113
|
+
|
|
108
114
|
/**
|
|
109
115
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
110
116
|
* @param {string} t - the singular type name.
|
|
@@ -123,13 +129,16 @@ const unkey = t => `${base}.Unkey<${t}>`
|
|
|
123
129
|
|
|
124
130
|
/**
|
|
125
131
|
* Puts a passed string in docstring format.
|
|
126
|
-
* @param {string | undefined} doc - raw string to docify.
|
|
132
|
+
* @param {string | string[] | undefined} doc - raw string to docify.
|
|
133
|
+
* Passing a string may contain linebreaks, which are used to split the doc into multiple lines.
|
|
134
|
+
* Alternatively, an array of strings can be passed, where each string is a line in the doc.
|
|
127
135
|
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
128
136
|
* concatenated to be properly indented by `buffer.add(...)`.
|
|
129
137
|
*/
|
|
130
138
|
const docify = doc => {
|
|
131
139
|
if (!doc) return []
|
|
132
|
-
const lines = doc.split(/\r?\n/)
|
|
140
|
+
const lines = (Array.isArray(doc) ? doc : doc.split(/\r?\n/))
|
|
141
|
+
.map(l => l.trim().replaceAll('*/', '*\\/')) // mask any */ with *\/
|
|
133
142
|
if (lines.length === 1) return [`/** ${lines[0]} */`] // one-line doc
|
|
134
143
|
return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
|
|
135
144
|
}
|
|
@@ -143,6 +152,7 @@ const stringIdent = s => `'${s}'`
|
|
|
143
152
|
|
|
144
153
|
module.exports = {
|
|
145
154
|
createArrayOf,
|
|
155
|
+
createBrandedType,
|
|
146
156
|
createDraftOf,
|
|
147
157
|
createDraftsOf,
|
|
148
158
|
createKey,
|
package/lib/resolution/entity.js
CHANGED
|
@@ -46,7 +46,7 @@ class EntityInfo {
|
|
|
46
46
|
*/
|
|
47
47
|
propertyAccess
|
|
48
48
|
|
|
49
|
-
/** @type {
|
|
49
|
+
/** @type {import('./resolver').Inflection | undefined} */
|
|
50
50
|
#inflection
|
|
51
51
|
|
|
52
52
|
/** @type {import('./resolver').Resolver} */
|
|
@@ -150,22 +150,22 @@ class EntityRepository {
|
|
|
150
150
|
#resolver
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* @param {string} fq - fully qualified name of the entity
|
|
153
|
+
* @param {import('../components/identifier').Identifier |string} fq - fully qualified name of the entity
|
|
154
154
|
* @returns {EntityInfo | null}
|
|
155
155
|
*/
|
|
156
156
|
getByFq (fq) {
|
|
157
|
+
if (typeof fq !== 'string') fq = fq.plain
|
|
157
158
|
if (this.#cache[fq] !== undefined) return this.#cache[fq]
|
|
158
|
-
this.#cache[fq] = this.#resolver.isPartOfModel(fq)
|
|
159
|
+
return this.#cache[fq] = this.#resolver.isPartOfModel(fq)
|
|
159
160
|
? new EntityInfo(fq, this, this.#resolver)
|
|
160
161
|
: null
|
|
161
|
-
return this.#cache[fq]
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
165
|
* Convenience for getByFq when you are 100% sure the entity exists.
|
|
166
166
|
* Serves to eliminate cumbersome null-handling where you know it's not necessary.
|
|
167
167
|
* For example when fq is derived from a reference to another entity.
|
|
168
|
-
* @param {string} fq - fully qualified name of the entity
|
|
168
|
+
* @param {import('../components/identifier').Identifier | string} fq - fully qualified name of the entity
|
|
169
169
|
* @returns {EntityInfo}
|
|
170
170
|
*/
|
|
171
171
|
getByFqOrThrow(fq) {
|
|
@@ -195,7 +195,7 @@ class EntityRepository {
|
|
|
195
195
|
function asIdentifier ({info, wrapper = undefined, relative = undefined}) {
|
|
196
196
|
const name = isType(info.csn)
|
|
197
197
|
? info.entityName
|
|
198
|
-
: info.inflection.singular
|
|
198
|
+
: info.inflection.singular?.plain
|
|
199
199
|
|
|
200
200
|
const wrapped = typeof wrapper === 'function'
|
|
201
201
|
? wrapper(name)
|