New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@cap-js/cds-typer

Package Overview
Dependencies
Maintainers
0
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cap-js/cds-typer - npm Package Compare versions

Comparing version 0.31.0 to 0.32.0

17

CHANGELOG.md

@@ -7,3 +7,3 @@ # Changelog

### Added
### Added
### Changed

@@ -15,2 +15,17 @@ ### Deprecated

## [0.32.0] - 2025-01-14
### Added
- dedicated classes for inline compositions
- dedicated text-classes for entities with `localized` elements
### Changed
- prefixed builtin types like `Promise` and `Record` with `globalThis.`, to allow using names of builtin types for entities without collisions
- bumped peer-dependency to `@cap-js/cds-types` to `>=0.9`
### Deprecated
### Removed
### Fixed
- referencing another entity's property of type `cds.String` in an enum will now properly quote the generated values
### Security
## [0.31.0] - 2024-12-16

@@ -17,0 +32,0 @@ ### Fixed

6

lib/components/enum.js

@@ -70,3 +70,3 @@ const { normalise } = require('./identifier')

* will fall back to the key if no value is provided.
* @param {import('../typedefs').resolver.EnumCSN} enumCsn - the CSN type describing the enum
* @param {import('../typedefs').resolver.EnumCSN & { resolvedType?: string }} enumCsn - the CSN type describing the enum
* @param {{unwrapVals: boolean} | {}} options - if `unwrapVals` is passed,

@@ -85,6 +85,6 @@ * then the CSN structure `{val:x}` is flattened to just `x`.

*/
const csnToEnumPairs = ({enum: enm, type}, options = {}) => {
const csnToEnumPairs = ({enum: enm, type, resolvedType}, options = {}) => {
const actualOptions = {...{unwrapVals: true}, ...options}
return Object.entries(enm).map(([k, v]) => {
const val = enumVal(k, v.val, type)
const val = enumVal(k, v.val, resolvedType ?? type) // if type is a ref, prefer the resolvedType to catch references to cds.Strings
return [k, (actualOptions.unwrapVals ? val : { val })]

@@ -91,0 +91,0 @@ })

module.exports = {
empty: 'Record<never, never>'
empty: 'globalThis.Record<never, never>'
}
const { LOG } = require('./logging')
const { annotations } = require('./util')

@@ -296,2 +297,26 @@ const DRAFT_ENABLED_ANNO = '@odata.draft.enabled'

/**
* Clears "correct" singular/plural annotations from inferred model
* copies the ones from the xtended model.
*
* This is done to prevent potential duplicate class names because of annotation propagation.
* @param {{inferred: CSN, xtended: CSN}} csn - CSN models
*/
function propagateInflectionAnnotations(csn) {
const singularAnno = annotations.singular[0]
const pluralAnno = annotations.plural[0]
for (const [name, def] of Object.entries(csn.inferred.definitions)) {
const xtendedDef = csn.xtended.definitions[name]
// we keep the annotations from definition specific to the inferred model (e.g. inline compositions)
if (!xtendedDef) continue
// clear annotations from inferred definition
if (Object.hasOwn(def, singularAnno)) delete def[singularAnno]
if (Object.hasOwn(def, pluralAnno)) delete def[pluralAnno]
// transfer annotation from xtended if existing
if (Object.hasOwn(xtendedDef, singularAnno)) def[singularAnno] = xtendedDef[singularAnno]
if (Object.hasOwn(xtendedDef, pluralAnno)) def[pluralAnno] = xtendedDef[pluralAnno]
}
}
/**
* @param {EntityCSN} entity - the entity

@@ -315,2 +340,21 @@ */

/**
* Heuristic way of looking up a reference type.
* We currently only support up to two segments,
* the first referring to the entity, a possible second
* referring to an element of the entity.
* @param {CSN} csn - CSN
* @param {string[]} ref - reference
* @returns {EntityCSN}
*/
function lookUpRefType (csn, ref) {
if (ref.length > 2) throw new Error(`Unsupported reference type ${ref.join('.')} with ${ref.length} segments. Please report this error.`)
/** @type {EntityCSN | undefined} */
let result = csn.definitions[ref[0]] // entity
if (ref.length === 1) return result
result = result?.elements?.[ref[1]] // property
if (!result) throw new Error(`Failed to look up reference type ${ref.join('.')}`)
return result
}
module.exports = {

@@ -331,3 +375,5 @@ collectDraftEnabledEntities,

propagateForeignKeys,
isCsnAny
propagateInflectionAnnotations,
isCsnAny,
lookUpRefType
}

@@ -260,2 +260,3 @@ 'use strict'

buffer.closed = false
buffer.namespace = name
buffer.add(`export namespace ${name} {`)

@@ -290,2 +291,4 @@ buffer.indent()

* @param {[string, string][]} kvs - list of key-value pairs
* @param {Buffer} [buffer] - if buffer is of subnamespace the enum will be added there,
* otherwise to the inline enums of the file
* @param {string[]} doc - the enum docs

@@ -315,12 +318,28 @@ * If given, the enum is considered to be an inline definition of an enum.

*/
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, doc=[]) {
addInlineEnum(entityCleanName, entityFqName, propertyName, kvs, buffer, doc=[]) {
const namespacedEntity = [buffer?.namespace, entityCleanName].filter(Boolean).join('.')
this.enums.data.push({
name: `${entityCleanName}.${propertyName}`,
name: `${namespacedEntity}.${propertyName}`,
property: propertyName,
kvs,
fq: `${entityCleanName}.${propertyName}`
fq: `${namespacedEntity}.${propertyName}`
})
const entityProxy = this.entityProxies[entityCleanName] ?? (this.entityProxies[entityCleanName] = [])
const entityProxy = this.entityProxies[namespacedEntity] ?? (this.entityProxies[namespacedEntity] = [])
entityProxy.push(propertyName)
printEnum(this.inlineEnums.buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
// REVISIT: find a better way to do this???
const printEnumToBuffer = (/** @type {Buffer} */buffer) => printEnum(buffer, propertyToInlineEnumName(entityCleanName, propertyName), kvs, {export: false}, doc)
if (buffer?.namespace) {
const tempBuffer = new Buffer()
// we want to put the enums on class level
tempBuffer.indent()
printEnumToBuffer(tempBuffer)
// we want to write the enums at the beginning of the namespace
const [first,...rest] = buffer.parts
buffer.parts = [first, ...tempBuffer.parts, ...rest]
} else {
printEnumToBuffer(this.inlineEnums.buffer)
}
}

@@ -496,8 +515,11 @@

singularRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: true }${customPropsStr} })`,
pluralRhs: `createEntityProxy(['${namespace}', '${original}'])`,
pluralRhs: `createEntityProxy(['${namespace}', '${original}'], { target: { is_singular: false }})`,
}
} else {
// standard entity: csn.Books
// inline entity: csn['Books.texts']
const csnAccess = original.includes('.') ? `csn['${original}']` : `csn.${original}`
return {
singularRhs: `{ is_singular: true, __proto__: csn.${original} }`,
pluralRhs: `csn.${original}`
singularRhs: `{ is_singular: true, __proto__: ${csnAccess} }`,
pluralRhs: csnAccess
}

@@ -596,2 +618,7 @@ }

this.closed = false
/**
* Required for inline enums of inline compositions or text entities
* @type {string | undefined}
*/
this.namespace = undefined
}

@@ -598,0 +625,0 @@

@@ -87,3 +87,3 @@ /**

printDeconstructedImport (imports, from) {
return `import { ${imports.join(', ')} } from '${from}'`
return `import { ${imports.join(', ')} } from '${from}/index.js'`
}

@@ -93,3 +93,5 @@

printExport (name, value) {
return `export const ${name} = ${value}`
return name.includes('.')
? `${name} = ${value}`
: `export const ${name} = ${value}`
}

@@ -96,0 +98,0 @@

@@ -106,3 +106,3 @@ 'use strict'

*/
const createPromiseOf = t => `Promise<${t}>`
const createPromiseOf = t => `globalThis.Promise<${t}>`

@@ -109,0 +109,0 @@ /**

@@ -64,2 +64,18 @@ const { isType } = require('../csn')

/** @type {Set<string> | undefined} */
#inheritedElements
/** @returns set of inherited elements (e.g. ID of aspect cuid) */
get inheritedElements() {
if (this.#inheritedElements) return this.#inheritedElements
this.#inheritedElements = new Set()
for (const parentName of this.csn.includes ?? []) {
const parent = this.#repository.getByFq(parentName)
for (const element of Object.keys(parent?.csn?.elements ?? {})) {
this.#inheritedElements.add(element)
}
}
return this.#inheritedElements
}
/** @returns the **inferred** csn for this entity. */

@@ -66,0 +82,0 @@ get csn () {

@@ -27,3 +27,3 @@ 'use strict'

class Resolver {
get csn() { return this.visitor.csn.inferred }
get csn() { return this.visitor.csn }

@@ -169,7 +169,9 @@ /** @param {Visitor} visitor - the visitor */

const defs = this.visitor.csn.inferred.definitions
const defs = this.visitor.csn.definitions
// check if name is already an entity, then we do not have a property access, but a nested entity
if (defs[p]?.kind === 'entity') return []
// assume parts to contain [Namespace, Service, Entity1, Entity2, Entity3, property1, property2]
/** @type {string} */
// @ts-expect-error - nope, we know there is at least one element
let qualifier = parts.shift()
let qualifier = /** @type {string} */ (parts.shift())
// find first entity from left (Entity1)

@@ -245,2 +247,4 @@ while ((!defs[qualifier] || !isEntity(defs[qualifier])) && parts.length) {

plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName
// remove leading entity name
if (plural.includes('.')) plural = last(plural)
singular = util.getSingularAnnotation(typeInfo.csn) ?? util.singular4(typeInfo.csn, true) // util.singular4(typeInfo.csn, true) // can not use `plural` to honor possible @singular annotation

@@ -317,14 +321,2 @@

// FIXME: super hack!!
// Inflection currently does not retain the scope of the entity.
// But we can't just fix it in inflection(...), as that would break several other things
// So we bandaid-fix it back here, as it is the least intrusive place -- but this should get fixed asap!
if (target.type) {
const untangled = this.visitor.entityRepository.getByFqOrThrow(target.type)
const scope = untangled.scope.join('.')
if (scope && !singular.startsWith(scope)) {
singular = `${scope}.${singular}`
}
}
typeName = cardinality > 1

@@ -377,4 +369,10 @@ ? toMany(plural)

if (target && !typeInfo.isDeepRequire) {
const { propertyAccess } = this.visitor.entityRepository.getByFq(target) ?? {}
if (propertyAccess?.length) {
const { propertyAccess, scope } = this.visitor.entityRepository.getByFq(target) ?? {}
if (scope?.length) {
// update inflections with proper prefix, e.g. Books.text, Books.texts
typeInfo.inflection = {
singular: [...scope, typeInfo.inflection?.singular].join('.'),
plural: [...scope, typeInfo.inflection?.plural].join('.')
}
} else if (propertyAccess?.length) {
const element = target.slice(0, -propertyAccess.join('.').length - 1)

@@ -460,2 +458,3 @@ const access = this.visitor.inlineDeclarationResolver.getTypeLookup(propertyAccess)

/** @type {TypeResolveInfo} */
const result = {

@@ -578,3 +577,3 @@ isBuiltin: false, // will be rectified in the corresponding handlers, if needed

resolveTypeName(t, into) {
const result = into ?? {}
const result = into ?? /** @type {TypeResolveInfo} */({})
const path = t.split('.')

@@ -581,0 +580,0 @@ const builtin = this.builtinResolver.resolveBuiltin(path)

@@ -16,3 +16,3 @@ export module resolver {

elements?: { [key: string]: EntityCSN }
key?: string // custom!!
key?: boolean // custom!!
keys?: { [key:string]: any }

@@ -29,2 +29,4 @@ kind: string,

name: string,
'@singular'?: string,
'@plural'?: string,
'@odata.draft.enabled'?: boolean // custom!

@@ -51,3 +53,4 @@ _unresolved?: boolean

export type EnumCSN = EntityCSN & {
enum: {[key:name]: string}
enum: {[key:name]: string},
resolvedType?: string // custom property! When .type points to a ref, the visitor will resolve the ref into this property
}

@@ -54,0 +57,0 @@

@@ -18,6 +18,6 @@ /** @typedef { import('./typedefs').util.Annotations} Annotations */

const annotations = {
const annotations = /** @type {const} */ ({
singular: ['@singular'],
plural: ['@plural'],
}
})

@@ -24,0 +24,0 @@ /**

'use strict'
const util = require('./util')
const { isView, isUnresolved, propagateForeignKeys, collectDraftEnabledEntities, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum, isEntity } = require('./csn')
const { propagateForeignKeys, propagateInflectionAnnotations, collectDraftEnabledEntities, isDraftEnabled, isType, getMaxCardinality, isViewOrProjection, isEnum, isEntity, lookUpRefType } = require('./csn')
// eslint-disable-next-line no-unused-vars

@@ -47,8 +45,9 @@ const { SourceFile, FileRepository, Buffer, Path } = require('./file')

constructor(csn) {
propagateForeignKeys(csn.xtended)
propagateForeignKeys(csn.inferred)
// has to be executed on the inferred model as autoexposed entities are not included in the xtended csn
propagateInflectionAnnotations(csn)
collectDraftEnabledEntities(csn.inferred)
this.csn = csn
// xtendend csn not required after this point -> continue with inferred
this.csn = csn.inferred
/** @type {Context[]} **/

@@ -78,38 +77,5 @@ this.contexts = []

visitDefinitions() {
for (const [name, entity] of Object.entries(this.csn.xtended.definitions)) {
if (isView(entity)) {
this.visitEntity(name, this.csn.inferred.definitions[name])
} else if (isProjection(entity) || !isUnresolved(entity)) {
this.visitEntity(name, entity)
} else {
LOG.warn(`Skipping unresolved entity: ${name}`)
}
for (const [name, entity] of Object.entries(this.csn.definitions)) {
this.visitEntity(name, entity)
}
// FIXME: optimise
// We are currently working with two flavours of CSN:
// xtended, as it is as close as possible to an OOP class hierarchy
// inferred, as it contains information missing in xtended
// This is less than optimal and has to be revisited at some point!
const handledKeys = new Set(Object.keys(this.csn.xtended.definitions))
// we are looking for autoexposed entities in services
const missing = Object.entries(this.csn.inferred.definitions).filter(([key]) => !key.endsWith('.texts') &&!handledKeys.has(key))
for (const [name, entity] of missing) {
// instead of using the definition from inferred CSN, we refer to the projected entity from xtended CSN instead.
// The latter contains the CSN fixes (propagated foreign keys, etc) and none of the localised fields we don't handle yet.
if (entity.projection) {
const targetName = entity.projection.from.ref[0]
// FIXME: references to types of entity properties may be missing from xtendend flavour (see #103)
// this should be revisted once we settle on a single flavour.
const target = this.csn.xtended.definitions[targetName] ?? this.csn.inferred.definitions[targetName]
if (target.kind !== 'type') {
// skip if the target is a property, like in:
// books: Association to many Author.books ...
// as this would result in a type definition that
// name-clashes with the actual declaration of Author
this.visitEntity(name, target)
}
} else {
LOG.error(`Expecting an autoexposed projection within a service. Skipping ${name}`)
}
}
}

@@ -124,11 +90,4 @@

#keys(fq) {
// FIXME: this is actually pretty bad, as not only have to propagate keys through
// both flavours of CSN (see constructor), but we are now also collecting them from
// both flavours and deduplicating them.
// xtended contains keys that have been inherited from parents
// inferred contains keys from queried entities (thing `entity Foo as select from Bar`, where Bar has keys)
// So we currently need them both.
return Object.entries({
...this.csn.inferred.definitions[fq]?.keys ?? {},
...this.csn.xtended.definitions[fq]?.keys ?? {}
...this.csn.definitions[fq]?.keys ?? {}
})

@@ -239,3 +198,3 @@ }

// types are not inflected, so don't change those to singular
const csn = this.csn.inferred.definitions[fq]
const csn = this.csn.definitions[fq]
const ident = isType(csn)

@@ -270,2 +229,3 @@ ? clean

const inheritedElements = !isViewOrProjection(entity) ? info.inheritedElements : null
this.contexts.push({ entity: fq })

@@ -282,6 +242,3 @@

for (let [ename, element] of Object.entries(entity.elements ?? [])) {
if (element.target && /\.texts?/.test(element.target)) {
LOG.warn(`referring to .texts property in ${fq}. This is currently not supported and will be ignored.`)
continue
}
if (inheritedElements?.has(ename)) continue
this.visitElement({name: ename, element, file, buffer, resolverOptions})

@@ -301,3 +258,4 @@

const kelement = Object.assign(Object.create(originalKeyElement), {
isRefNotNull: !!element.notNull || !!element.key
isRefNotNull: !!element.notNull || !!element.key,
key: element.key
})

@@ -311,3 +269,3 @@ this.visitElement({name: foreignKey, element: kelement, file, buffer, resolverOptions})

// store inline enums for later handling, as they have to go into one common "static elements" wrapper
if (isInlineEnumType(element, this.csn.xtended)) {
if (isInlineEnumType(element, this.csn)) {
enums.push(element)

@@ -325,3 +283,6 @@ }

}))
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), eDoc)
if (typeof e?.type !== 'string' && e?.type?.ref) {
e.resolvedType = /** @type {string} */(lookUpRefType(this.csn, e.type.ref)?.type)
}
file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}), buffer, eDoc)
}

@@ -369,3 +330,3 @@

const info = this.entityRepository.getByFqOrThrow(fq)
const { namespace: ns, entityName: clean, inflection } = info
const { namespace: ns, entityName: clean, inflection, scope } = info
const file = this.fileRepository.getNamespaceFile(ns)

@@ -386,3 +347,3 @@ let { singular, plural } = inflection

const namespacedSingular = `${ns.asNamespace()}.${singular}`
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.xtended.definitions) {
if (!isType(entity) && namespacedSingular !== fq && namespacedSingular in this.csn.definitions) {
LOG.error(

@@ -400,17 +361,12 @@ `Derived singular '${singular}' for your entity '${fq}', already exists. The resulting types will be erronous. Consider using '@singular:'/ '@plural:' annotations in your model or move the offending declarations into different namespaces to resolve this collision.`

// we can't just use "singular" here, as it may have the subnamespace removed:
// "Books.text" is just "text" in "singular". Within the inflected exports we need
// to have Books.texts = Books.text, so we derive the singular once more without cutting off the ns.
// Directly deriving it from the plural makes sure we retain any parent namespaces of kind "entity",
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
// edge case: @singular annotation present. singular4 will take care of that.
file.addInflection(util.singular4(entity, true), plural, clean)
if (scope?.length > 0) {
/** @param {string} n - name of entity */
const scoped = n => [...scope, n].join('.')
file.addInflection(scoped(singular), scoped(plural), scoped(clean))
} else {
file.addInflection(singular, plural, clean)
}
// in case of projections `entity` is empty -> retrieve from inferred csn where the actual properties are rolled out
const target = isProjection(entity) || isView(entity)
? this.csn.inferred.definitions[fq]
: entity
this.#aspectify(fq, entity, buffer, { cleanName: singular })
this.#aspectify(fq, target, buffer, { cleanName: singular })
buffer.add(overrideNameProperty(singular, entity.name))

@@ -531,3 +487,3 @@ buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)

} else {
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.definitions[type?.type])
// alias

@@ -534,0 +490,0 @@ file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)

{
"name": "@cap-js/cds-typer",
"version": "0.31.0",
"version": "0.32.0",
"description": "Generates .ts files for a CDS model to receive code completion in VS Code",

@@ -45,3 +45,3 @@ "main": "index.js",

"peerDependencies": {
"@cap-js/cds-types": ">=0.6.4",
"@cap-js/cds-types": ">=0.9",
"@sap/cds": ">=8"

@@ -48,0 +48,0 @@ },

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc