@open-rap/compiler 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/agent/server.d.ts +35 -0
- package/dist/agent/server.js +408 -0
- package/dist/agent/server.js.map +1 -0
- package/dist/authoring/abapgit.d.ts +38 -0
- package/dist/authoring/abapgit.js +242 -0
- package/dist/authoring/abapgit.js.map +1 -0
- package/dist/authoring/catalog.d.ts +48 -0
- package/dist/authoring/catalog.js +117 -0
- package/dist/authoring/catalog.js.map +1 -0
- package/dist/authoring/floorplan.d.ts +28 -0
- package/dist/authoring/floorplan.js +31 -0
- package/dist/authoring/floorplan.js.map +1 -0
- package/dist/authoring/generate.d.ts +57 -0
- package/dist/authoring/generate.js +630 -0
- package/dist/authoring/generate.js.map +1 -0
- package/dist/authoring/mcp.d.ts +15 -0
- package/dist/authoring/mcp.js +52 -0
- package/dist/authoring/mcp.js.map +1 -0
- package/dist/authoring/new.d.ts +35 -0
- package/dist/authoring/new.js +389 -0
- package/dist/authoring/new.js.map +1 -0
- package/dist/bdef/ast.d.ts +249 -0
- package/dist/bdef/ast.js +3 -0
- package/dist/bdef/ast.js.map +1 -0
- package/dist/bdef/lexer.d.ts +105 -0
- package/dist/bdef/lexer.js +148 -0
- package/dist/bdef/lexer.js.map +1 -0
- package/dist/bdef/parser.d.ts +22 -0
- package/dist/bdef/parser.js +1012 -0
- package/dist/bdef/parser.js.map +1 -0
- package/dist/build.d.ts +26 -0
- package/dist/build.js +62 -0
- package/dist/build.js.map +1 -0
- package/dist/cds/adapter.d.ts +91 -0
- package/dist/cds/adapter.js +381 -0
- package/dist/cds/adapter.js.map +1 -0
- package/dist/cds/expression-ast.d.ts +65 -0
- package/dist/cds/expression-ast.js +330 -0
- package/dist/cds/expression-ast.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +485 -0
- package/dist/cli.js.map +1 -0
- package/dist/dcl/parser.d.ts +59 -0
- package/dist/dcl/parser.js +338 -0
- package/dist/dcl/parser.js.map +1 -0
- package/dist/ddic/catalog.d.ts +17 -0
- package/dist/ddic/catalog.js +211 -0
- package/dist/ddic/catalog.js.map +1 -0
- package/dist/ddic/cds-types.d.ts +47 -0
- package/dist/ddic/cds-types.js +131 -0
- package/dist/ddic/cds-types.js.map +1 -0
- package/dist/ddic/table-source.d.ts +70 -0
- package/dist/ddic/table-source.js +500 -0
- package/dist/ddic/table-source.js.map +1 -0
- package/dist/ddic/xml.d.ts +65 -0
- package/dist/ddic/xml.js +201 -0
- package/dist/ddic/xml.js.map +1 -0
- package/dist/dev.d.ts +128 -0
- package/dist/dev.js +434 -0
- package/dist/dev.js.map +1 -0
- package/dist/diagnostics.d.ts +33 -0
- package/dist/diagnostics.js +53 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/gen/abap/derived-types.d.ts +26 -0
- package/dist/gen/abap/derived-types.js +323 -0
- package/dist/gen/abap/derived-types.js.map +1 -0
- package/dist/gen/abap/eml.d.ts +118 -0
- package/dist/gen/abap/eml.js +362 -0
- package/dist/gen/abap/eml.js.map +1 -0
- package/dist/gen/abap/enums.d.ts +13 -0
- package/dist/gen/abap/enums.js +74 -0
- package/dist/gen/abap/enums.js.map +1 -0
- package/dist/gen/abap/framework-http.d.ts +14 -0
- package/dist/gen/abap/framework-http.js +1049 -0
- package/dist/gen/abap/framework-http.js.map +1 -0
- package/dist/gen/abap/framework-query.d.ts +14 -0
- package/dist/gen/abap/framework-query.js +225 -0
- package/dist/gen/abap/framework-query.js.map +1 -0
- package/dist/gen/abap/framework-utils.d.ts +29 -0
- package/dist/gen/abap/framework-utils.js +3707 -0
- package/dist/gen/abap/framework-utils.js.map +1 -0
- package/dist/gen/abap/framework.d.ts +12 -0
- package/dist/gen/abap/framework.js +480 -0
- package/dist/gen/abap/framework.js.map +1 -0
- package/dist/gen/abap/generate.d.ts +40 -0
- package/dist/gen/abap/generate.js +238 -0
- package/dist/gen/abap/generate.js.map +1 -0
- package/dist/gen/abap/rewrite.d.ts +82 -0
- package/dist/gen/abap/rewrite.js +957 -0
- package/dist/gen/abap/rewrite.js.map +1 -0
- package/dist/gen/abap/shadow.d.ts +25 -0
- package/dist/gen/abap/shadow.js +175 -0
- package/dist/gen/abap/shadow.js.map +1 -0
- package/dist/gen/abap/tokens.d.ts +33 -0
- package/dist/gen/abap/tokens.js +87 -0
- package/dist/gen/abap/tokens.js.map +1 -0
- package/dist/gen/app/index.d.ts +25 -0
- package/dist/gen/app/index.js +322 -0
- package/dist/gen/app/index.js.map +1 -0
- package/dist/gen/csdl.d.ts +11 -0
- package/dist/gen/csdl.js +562 -0
- package/dist/gen/csdl.js.map +1 -0
- package/dist/gen/deploy.d.ts +12 -0
- package/dist/gen/deploy.js +53 -0
- package/dist/gen/deploy.js.map +1 -0
- package/dist/gen/docs/index.d.ts +35 -0
- package/dist/gen/docs/index.js +267 -0
- package/dist/gen/docs/index.js.map +1 -0
- package/dist/gen/docs/markdown.d.ts +23 -0
- package/dist/gen/docs/markdown.js +187 -0
- package/dist/gen/docs/markdown.js.map +1 -0
- package/dist/gen/docs/site.d.ts +17 -0
- package/dist/gen/docs/site.js +106 -0
- package/dist/gen/docs/site.js.map +1 -0
- package/dist/gen/keycloak.d.ts +21 -0
- package/dist/gen/keycloak.js +90 -0
- package/dist/gen/keycloak.js.map +1 -0
- package/dist/gen/launchpad/index.d.ts +48 -0
- package/dist/gen/launchpad/index.js +302 -0
- package/dist/gen/launchpad/index.js.map +1 -0
- package/dist/gen/launchpad/ui.d.ts +14 -0
- package/dist/gen/launchpad/ui.js +350 -0
- package/dist/gen/launchpad/ui.js.map +1 -0
- package/dist/gen/mcp.d.ts +23 -0
- package/dist/gen/mcp.js +244 -0
- package/dist/gen/mcp.js.map +1 -0
- package/dist/gen/sql-expressions.d.ts +12 -0
- package/dist/gen/sql-expressions.js +136 -0
- package/dist/gen/sql-expressions.js.map +1 -0
- package/dist/gen/sql.d.ts +38 -0
- package/dist/gen/sql.js +612 -0
- package/dist/gen/sql.js.map +1 -0
- package/dist/gen/ui/index.d.ts +7 -0
- package/dist/gen/ui/index.js +27 -0
- package/dist/gen/ui/index.js.map +1 -0
- package/dist/gen/ui/layout.d.ts +230 -0
- package/dist/gen/ui/layout.js +511 -0
- package/dist/gen/ui/layout.js.map +1 -0
- package/dist/gen/ui/static.d.ts +55 -0
- package/dist/gen/ui/static.js +2901 -0
- package/dist/gen/ui/static.js.map +1 -0
- package/dist/gen/ui/views.d.ts +10 -0
- package/dist/gen/ui/views.js +511 -0
- package/dist/gen/ui/views.js.map +1 -0
- package/dist/gen/vendor.d.ts +23 -0
- package/dist/gen/vendor.js +68 -0
- package/dist/gen/vendor.js.map +1 -0
- package/dist/gen/xml.d.ts +9 -0
- package/dist/gen/xml.js +42 -0
- package/dist/gen/xml.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/input/project.d.ts +50 -0
- package/dist/input/project.js +128 -0
- package/dist/input/project.js.map +1 -0
- package/dist/ir/access-controls.d.ts +11 -0
- package/dist/ir/access-controls.js +148 -0
- package/dist/ir/access-controls.js.map +1 -0
- package/dist/ir/authorizations.d.ts +25 -0
- package/dist/ir/authorizations.js +178 -0
- package/dist/ir/authorizations.js.map +1 -0
- package/dist/ir/behaviors.d.ts +16 -0
- package/dist/ir/behaviors.js +548 -0
- package/dist/ir/behaviors.js.map +1 -0
- package/dist/ir/builder.d.ts +20 -0
- package/dist/ir/builder.js +505 -0
- package/dist/ir/builder.js.map +1 -0
- package/dist/ir/config.d.ts +25 -0
- package/dist/ir/config.js +130 -0
- package/dist/ir/config.js.map +1 -0
- package/dist/ir/dictionary.d.ts +54 -0
- package/dist/ir/dictionary.js +328 -0
- package/dist/ir/dictionary.js.map +1 -0
- package/dist/ir/entities.d.ts +58 -0
- package/dist/ir/entities.js +676 -0
- package/dist/ir/entities.js.map +1 -0
- package/dist/ir/expressions.d.ts +33 -0
- package/dist/ir/expressions.js +327 -0
- package/dist/ir/expressions.js.map +1 -0
- package/dist/ir/handlers.d.ts +63 -0
- package/dist/ir/handlers.js +398 -0
- package/dist/ir/handlers.js.map +1 -0
- package/dist/ir/number-ranges.d.ts +24 -0
- package/dist/ir/number-ranges.js +74 -0
- package/dist/ir/number-ranges.js.map +1 -0
- package/dist/ir/seed.d.ts +23 -0
- package/dist/ir/seed.js +78 -0
- package/dist/ir/seed.js.map +1 -0
- package/dist/ir/service.d.ts +21 -0
- package/dist/ir/service.js +96 -0
- package/dist/ir/service.js.map +1 -0
- package/dist/launchpad/config.d.ts +47 -0
- package/dist/launchpad/config.js +321 -0
- package/dist/launchpad/config.js.map +1 -0
- package/dist/launchpad/workspace.d.ts +48 -0
- package/dist/launchpad/workspace.js +166 -0
- package/dist/launchpad/workspace.js.map +1 -0
- package/dist/srvd/parser.d.ts +36 -0
- package/dist/srvd/parser.js +193 -0
- package/dist/srvd/parser.js.map +1 -0
- package/dist/standard/data.d.ts +18 -0
- package/dist/standard/data.js +534 -0
- package/dist/standard/data.js.map +1 -0
- package/dist/standard/index.d.ts +36 -0
- package/dist/standard/index.js +507 -0
- package/dist/standard/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/docs/AGENTS.md +79 -0
- package/docs/ANNOTATIONS.md +76 -0
- package/docs/DECISIONS.md +1554 -0
- package/docs/DEPLOYMENT.md +113 -0
- package/docs/DIAGNOSTICS.md +116 -0
- package/docs/FEATURES.md +287 -0
- package/docs/GUIDE.md +560 -0
- package/docs/KNOWN_GAPS.md +216 -0
- package/docs/MCP.md +84 -0
- package/docs/PLAN.md +690 -0
- package/docs/README.md +148 -0
- package/docs/REMOTE_APIS.md +121 -0
- package/docs/ROADMAP.md +35 -0
- package/docs/UTILITIES.md +95 -0
- package/package.json +49 -3
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/** CDS view entities, projections and abstract entities → IR entities. */
|
|
2
|
+
import { getAnnotation, getString, mergeAnnotations, setAnnotation, sqlName, } from "@open-rap/model";
|
|
3
|
+
import { parseCds } from "../cds/adapter.js";
|
|
4
|
+
import { parseBuiltInType } from "../ddic/catalog.js";
|
|
5
|
+
import { filesOfType } from "../input/project.js";
|
|
6
|
+
import { ExpressionIssue, resolveExpression } from "./expressions.js";
|
|
7
|
+
const LAYERS = ["#CORE", "#LOCALIZATION", "#INDUSTRY", "#PARTNER", "#CUSTOMER"];
|
|
8
|
+
const SEMANTICS = [
|
|
9
|
+
["semantics.user.createdby", "createdBy"],
|
|
10
|
+
["semantics.systemdatetime.createdat", "createdAt"],
|
|
11
|
+
["semantics.user.lastchangedby", "lastChangedBy"],
|
|
12
|
+
["semantics.systemdatetime.lastchangedat", "lastChangedAt"],
|
|
13
|
+
["semantics.systemdatetime.localinstancelastchangedat", "localInstanceLastChangedAt"],
|
|
14
|
+
];
|
|
15
|
+
export class EntityResolver {
|
|
16
|
+
dictionary;
|
|
17
|
+
diagnostics;
|
|
18
|
+
parsed = new Map();
|
|
19
|
+
extensions = new Map();
|
|
20
|
+
resolved = new Map();
|
|
21
|
+
inProgress = new Set();
|
|
22
|
+
constructor(project, dictionary, diagnostics) {
|
|
23
|
+
this.dictionary = dictionary;
|
|
24
|
+
this.diagnostics = diagnostics;
|
|
25
|
+
for (const file of filesOfType(project, "DDLS").filter((f) => f.extension === "asddls")) {
|
|
26
|
+
const result = parseCds(file.fileName, file.content);
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
diagnostics.error("CDS001", result.message, { file: file.path, ...result.loc });
|
|
29
|
+
}
|
|
30
|
+
else if ("entity" in result) {
|
|
31
|
+
this.parsed.set(result.entity.name.toUpperCase(), { ast: result.entity, file });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const file of filesOfType(project, "DDLX").filter((f) => f.extension === "asddlxs")) {
|
|
35
|
+
const result = parseCds(file.fileName, file.content);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
diagnostics.error("CDS001", result.message, { file: file.path, ...result.loc });
|
|
38
|
+
}
|
|
39
|
+
else if ("extension" in result) {
|
|
40
|
+
const key = result.extension.entity.toUpperCase();
|
|
41
|
+
const list = this.extensions.get(key) ?? [];
|
|
42
|
+
list.push({ ast: result.extension, file });
|
|
43
|
+
this.extensions.set(key, list);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [entity, list] of this.extensions) {
|
|
47
|
+
if (!this.parsed.has(entity)) {
|
|
48
|
+
for (const { ast, file } of list) {
|
|
49
|
+
diagnostics.warning("CDS006", `metadata extension for unknown entity ${ast.entity}`, { file: file.path, ...ast.loc });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
has(name) {
|
|
55
|
+
return this.parsed.has(name.toUpperCase());
|
|
56
|
+
}
|
|
57
|
+
/** Names of the abstract entities of the project. */
|
|
58
|
+
abstractNames() {
|
|
59
|
+
return [...this.parsed.values()].filter((p) => p.ast.kind === "abstract").map((p) => p.ast.name);
|
|
60
|
+
}
|
|
61
|
+
/** Names of the table entities of the project (D55). */
|
|
62
|
+
tableEntityNames() {
|
|
63
|
+
return [...this.parsed.values()].filter((p) => p.ast.kind === "table").map((p) => p.ast.name);
|
|
64
|
+
}
|
|
65
|
+
/** Resolves an entity with its diagnostics in `sink` (an entity the BO may not need). */
|
|
66
|
+
resolveInto(name, sink) {
|
|
67
|
+
const previous = this.diagnostics;
|
|
68
|
+
this.diagnostics = sink;
|
|
69
|
+
try {
|
|
70
|
+
return this.resolve(name);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
this.diagnostics = previous;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ast(name) {
|
|
77
|
+
return this.parsed.get(name.toUpperCase());
|
|
78
|
+
}
|
|
79
|
+
all() {
|
|
80
|
+
return [...this.resolved.values()].filter((e) => e !== undefined);
|
|
81
|
+
}
|
|
82
|
+
/** Resolves an entity (and the entities it depends on); undefined when it cannot be built. */
|
|
83
|
+
resolve(name) {
|
|
84
|
+
const key = name.toUpperCase();
|
|
85
|
+
if (this.resolved.has(key)) {
|
|
86
|
+
return this.resolved.get(key);
|
|
87
|
+
}
|
|
88
|
+
const parsed = this.parsed.get(key);
|
|
89
|
+
if (parsed === undefined || this.inProgress.has(key)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
this.inProgress.add(key);
|
|
93
|
+
let entity;
|
|
94
|
+
switch (parsed.ast.kind) {
|
|
95
|
+
case "view":
|
|
96
|
+
entity = this.view(parsed);
|
|
97
|
+
break;
|
|
98
|
+
case "projection":
|
|
99
|
+
entity = this.projection(parsed);
|
|
100
|
+
break;
|
|
101
|
+
case "abstract":
|
|
102
|
+
entity = this.abstract(parsed);
|
|
103
|
+
break;
|
|
104
|
+
case "custom":
|
|
105
|
+
entity = this.custom(parsed);
|
|
106
|
+
break;
|
|
107
|
+
case "table":
|
|
108
|
+
entity = this.tableEntity(parsed);
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
this.diagnostics.error("CDS002", `${parsed.ast.name}: ${parsed.ast.statement} is not supported`, { file: parsed.file.path, ...parsed.ast.loc });
|
|
112
|
+
}
|
|
113
|
+
this.inProgress.delete(key);
|
|
114
|
+
this.resolved.set(key, entity);
|
|
115
|
+
return entity;
|
|
116
|
+
}
|
|
117
|
+
// -------------------------------------------------------------------------------------------
|
|
118
|
+
loc(file, loc) {
|
|
119
|
+
return { file: file.path, line: loc.line, column: loc.column };
|
|
120
|
+
}
|
|
121
|
+
entityAnnotations(parsed) {
|
|
122
|
+
const own = parsed.ast.annotations;
|
|
123
|
+
const extensions = this.applicableExtensions(parsed);
|
|
124
|
+
const headers = extensions.map((x) => withoutLayer(x.ast.annotations));
|
|
125
|
+
return mergeAnnotations(own, ...headers);
|
|
126
|
+
}
|
|
127
|
+
applicableExtensions(parsed) {
|
|
128
|
+
const list = this.extensions.get(parsed.ast.name.toUpperCase()) ?? [];
|
|
129
|
+
if (list.length === 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (getAnnotation(parsed.ast.annotations, "Metadata.allowExtensions") !== true) {
|
|
133
|
+
for (const { ast, file } of list) {
|
|
134
|
+
this.diagnostics.warning("CDS007", `metadata extension of ${parsed.ast.name} ignored: the entity does not declare @Metadata.allowExtensions: true`, { file: file.path, ...ast.loc });
|
|
135
|
+
}
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
return [...list].sort((a, b) => LAYERS.indexOf(a.ast.layer) - LAYERS.indexOf(b.ast.layer));
|
|
139
|
+
}
|
|
140
|
+
elementExtensionAnnotations(parsed, element) {
|
|
141
|
+
return this.applicableExtensions(parsed).flatMap((x) => x.ast.elements.filter((e) => e.name.toUpperCase() === element.toUpperCase()).map((e) => e.annotations));
|
|
142
|
+
}
|
|
143
|
+
checkExtensionElements(parsed, entity) {
|
|
144
|
+
for (const { ast, file } of this.applicableExtensions(parsed)) {
|
|
145
|
+
for (const element of ast.elements) {
|
|
146
|
+
const known = entity.fields.some((f) => f.name.toUpperCase() === element.name.toUpperCase()) ||
|
|
147
|
+
entity.associations.some((a) => a.name.toUpperCase() === element.name.toUpperCase());
|
|
148
|
+
if (!known) {
|
|
149
|
+
this.diagnostics.warning("CDS008", `metadata extension annotates unknown element ${entity.name}.${element.name}`, {
|
|
150
|
+
file: file.path,
|
|
151
|
+
...element.loc,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
fieldFromElement(parsed, element, type, inherited) {
|
|
158
|
+
const own = element.annotations;
|
|
159
|
+
const annotations = mergeAnnotations(inherited, own, ...this.elementExtensionAnnotations(parsed, element.name));
|
|
160
|
+
const field = { name: element.name, type: { ...type }, key: element.key, annotations, src: this.loc(parsed.file, element.loc) };
|
|
161
|
+
for (const [path, semantics] of SEMANTICS) {
|
|
162
|
+
if (getAnnotation(annotations, path) === true) {
|
|
163
|
+
field.semantics = semantics;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// an admin time stamp typed DEC(15) / DEC(21,7) is a time stamp
|
|
167
|
+
if (field.semantics !== undefined && /At$/.test(field.semantics) && (type.kind === "dec" || type.kind === "timestampl")) {
|
|
168
|
+
field.type.kind = type.decimals === 7 || type.length === 21 ? "timestampl" : type.kind === "dec" ? "timestamp" : type.kind;
|
|
169
|
+
}
|
|
170
|
+
const currency = getString(annotations, "Semantics.amount.currencyCode");
|
|
171
|
+
if (currency !== undefined) {
|
|
172
|
+
field.currencyCode = currency;
|
|
173
|
+
}
|
|
174
|
+
const unit = getString(annotations, "Semantics.quantity.unitOfMeasure");
|
|
175
|
+
if (unit !== undefined) {
|
|
176
|
+
field.unitOfMeasure = unit;
|
|
177
|
+
}
|
|
178
|
+
return field;
|
|
179
|
+
}
|
|
180
|
+
// -------------------------------------------------------------------------------------------
|
|
181
|
+
// calculated elements (D67)
|
|
182
|
+
/** The fields in the order of the select list: the plain ones, and the calculated elements resolved. */
|
|
183
|
+
withCalculatedFields(parsed, slots, scope) {
|
|
184
|
+
const done = new Map();
|
|
185
|
+
const active = new Set();
|
|
186
|
+
const isPlain = (slot) => "type" in slot;
|
|
187
|
+
const resolve = (element) => {
|
|
188
|
+
if (done.has(element)) {
|
|
189
|
+
return done.get(element);
|
|
190
|
+
}
|
|
191
|
+
if (active.has(element)) {
|
|
192
|
+
throw new ExpressionIssue("CDS022", `circular reference through $projection.${element.name}`);
|
|
193
|
+
}
|
|
194
|
+
active.add(element);
|
|
195
|
+
try {
|
|
196
|
+
const field = this.calculatedField(parsed, element, { ...scope, dictionary: this.dictionary, element: elementOf });
|
|
197
|
+
done.set(element, field);
|
|
198
|
+
return field;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
active.delete(element);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const elementOf = (name) => {
|
|
205
|
+
const slot = slots.find((s) => s.name.toUpperCase() === name.toUpperCase());
|
|
206
|
+
if (slot === undefined) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const field = isPlain(slot) ? slot : resolve(slot);
|
|
210
|
+
if (field === undefined) {
|
|
211
|
+
throw new ExpressionIssue("CDS022", `$projection.${name} is not a valid element`);
|
|
212
|
+
}
|
|
213
|
+
return { name: field.name, type: field.type };
|
|
214
|
+
};
|
|
215
|
+
return slots.flatMap((slot) => {
|
|
216
|
+
if (isPlain(slot)) {
|
|
217
|
+
return [slot];
|
|
218
|
+
}
|
|
219
|
+
const field = resolve(slot);
|
|
220
|
+
return field === undefined ? [] : [field];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
calculatedField(parsed, element, scope) {
|
|
224
|
+
const { ast, file } = parsed;
|
|
225
|
+
const where = this.loc(file, element.loc);
|
|
226
|
+
const label = `${ast.name}.${element.name || "?"}`;
|
|
227
|
+
if (element.expr === undefined) {
|
|
228
|
+
this.diagnostics.error("CDS002", `${label}: ${element.expressionError ?? "this expression"} is not supported yet`, where);
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
if (element.name === "") {
|
|
233
|
+
throw new ExpressionIssue("CDS022", "a calculated element needs a name (AS alias)");
|
|
234
|
+
}
|
|
235
|
+
if (element.key) {
|
|
236
|
+
throw new ExpressionIssue("CDS022", "a calculated element cannot be a key element");
|
|
237
|
+
}
|
|
238
|
+
const expression = resolveExpression(element.expr, scope);
|
|
239
|
+
const field = this.fieldFromElement(parsed, element, expression.type, undefined);
|
|
240
|
+
field.expression = expression;
|
|
241
|
+
return field;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (!(error instanceof ExpressionIssue)) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
this.diagnostics.error(error.code, `${label}: ${error.message}`, where);
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// -------------------------------------------------------------------------------------------
|
|
252
|
+
// view entities
|
|
253
|
+
view(parsed) {
|
|
254
|
+
const { ast, file } = parsed;
|
|
255
|
+
if (ast.source === undefined) {
|
|
256
|
+
this.diagnostics.error("CDS003", `${ast.name}: no data source`, this.loc(file, ast.loc));
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (ast.hasJoin) {
|
|
260
|
+
this.diagnostics.error("CDS002", `${ast.name}: joins are not supported yet`, this.loc(file, ast.loc));
|
|
261
|
+
}
|
|
262
|
+
if (ast.hasWhere) {
|
|
263
|
+
this.diagnostics.warning("CDS002", `${ast.name}: the WHERE condition is not supported yet and is ignored`, this.loc(file, ast.loc));
|
|
264
|
+
}
|
|
265
|
+
if (this.parsed.get(ast.source.toUpperCase())?.ast.kind === "table") {
|
|
266
|
+
this.resolve(ast.source); // a table entity is read as its table (D55)
|
|
267
|
+
}
|
|
268
|
+
const table = this.dictionary.table(ast.source);
|
|
269
|
+
const sourceEntity = table === undefined ? this.resolve(ast.source) : undefined;
|
|
270
|
+
if (table === undefined && sourceEntity === undefined) {
|
|
271
|
+
this.diagnostics.error("CDS003", `${ast.name}: unknown data source ${ast.source} (no TABL or DDLS in the sources)`, this.loc(file, ast.loc));
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const associationNames = new Set(ast.associations.map((a) => a.alias.toUpperCase()));
|
|
275
|
+
const slots = [];
|
|
276
|
+
for (const element of ast.elements) {
|
|
277
|
+
if (element.path === undefined) {
|
|
278
|
+
slots.push(element); // a calculated element (D67), resolved once the plain ones are known
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
let path = element.path;
|
|
282
|
+
if (path.length > 1 && ast.sourceAlias !== undefined && path[0]?.toUpperCase() === ast.sourceAlias.toUpperCase()) {
|
|
283
|
+
path = path.slice(1);
|
|
284
|
+
}
|
|
285
|
+
else if (path.length > 1 && path[0]?.toUpperCase() === ast.source.toUpperCase()) {
|
|
286
|
+
path = path.slice(1);
|
|
287
|
+
}
|
|
288
|
+
if (path.length === 1 && associationNames.has((path[0] ?? "").toUpperCase())) {
|
|
289
|
+
continue; // association exposure
|
|
290
|
+
}
|
|
291
|
+
if (path.length > 1) {
|
|
292
|
+
this.diagnostics.error("CDS002", `${ast.name}.${element.name}: path expressions (${path.join(".")}) are not supported yet`, this.loc(file, element.loc));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const sourceName = path[0] ?? "";
|
|
296
|
+
if (table !== undefined) {
|
|
297
|
+
const column = table.columns.find((c) => c.name === sourceName.toLowerCase());
|
|
298
|
+
if (column === undefined) {
|
|
299
|
+
this.diagnostics.error("CDS004", `${ast.name}.${element.name}: ${table.name} has no column ${sourceName}`, this.loc(file, element.loc));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const field = this.fieldFromElement(parsed, element, column.type, undefined);
|
|
303
|
+
field.column = column.name;
|
|
304
|
+
if (column.client) {
|
|
305
|
+
this.diagnostics.warning("CDS009", `${ast.name}.${element.name}: client fields are not exposed (single tenant, decision D9)`, this.loc(file, element.loc));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
slots.push(field);
|
|
309
|
+
}
|
|
310
|
+
else if (sourceEntity !== undefined) {
|
|
311
|
+
const base = sourceEntity.fields.find((f) => f.name.toUpperCase() === sourceName.toUpperCase());
|
|
312
|
+
if (base === undefined) {
|
|
313
|
+
this.diagnostics.error("CDS004", `${ast.name}.${element.name}: ${sourceEntity.name} has no element ${sourceName}`, this.loc(file, element.loc));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const field = this.fieldFromElement(parsed, element, base.type, propagated(ast, base.annotations));
|
|
317
|
+
field.baseField = base.name;
|
|
318
|
+
slots.push(field);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const fields = this.withCalculatedFields(parsed, slots, {
|
|
322
|
+
sourceName: table?.name ?? sourceEntity?.name ?? ast.source,
|
|
323
|
+
sourcePrefixes: [ast.source, ...(ast.sourceAlias === undefined ? [] : [ast.sourceAlias])],
|
|
324
|
+
associations: associationNames,
|
|
325
|
+
column: (name) => {
|
|
326
|
+
if (table !== undefined) {
|
|
327
|
+
const column = table.columns.find((c) => c.name === name.toLowerCase() && !c.client);
|
|
328
|
+
return column === undefined ? undefined : { name: column.name, type: column.type };
|
|
329
|
+
}
|
|
330
|
+
const base = sourceEntity?.fields.find((f) => f.name.toUpperCase() === name.toUpperCase() && f.draft !== true);
|
|
331
|
+
return base === undefined ? undefined : { name: base.name, type: base.type };
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
if (table !== undefined) {
|
|
335
|
+
this.addDataElementLabels(fields, table.name);
|
|
336
|
+
}
|
|
337
|
+
const entity = {
|
|
338
|
+
name: ast.name,
|
|
339
|
+
kind: "view",
|
|
340
|
+
root: ast.root,
|
|
341
|
+
source: table !== undefined ? table.name : sourceEntity.name,
|
|
342
|
+
sqlView: sqlName(ast.name),
|
|
343
|
+
fields,
|
|
344
|
+
associations: [],
|
|
345
|
+
annotations: this.entityAnnotations(parsed),
|
|
346
|
+
src: this.loc(file, ast.loc),
|
|
347
|
+
};
|
|
348
|
+
entity.associations = this.associationsOf(parsed, entity);
|
|
349
|
+
if (fields.filter((f) => f.key).length === 0) {
|
|
350
|
+
this.diagnostics.error("CDS011", `${ast.name}: no key element`, this.loc(file, ast.loc));
|
|
351
|
+
}
|
|
352
|
+
this.checkExtensionElements(parsed, entity);
|
|
353
|
+
return entity;
|
|
354
|
+
}
|
|
355
|
+
/** The associations of a view or custom entity, ON conditions resolved. */
|
|
356
|
+
associationsOf(parsed, entity) {
|
|
357
|
+
const { ast, file } = parsed;
|
|
358
|
+
return ast.associations.map((a) => {
|
|
359
|
+
const association = {
|
|
360
|
+
name: a.alias,
|
|
361
|
+
target: this.ast(a.target)?.ast.name ?? a.target,
|
|
362
|
+
kind: a.kind,
|
|
363
|
+
cardinality: a.cardinality,
|
|
364
|
+
on: [],
|
|
365
|
+
src: this.loc(file, a.loc),
|
|
366
|
+
};
|
|
367
|
+
for (const pair of a.on) {
|
|
368
|
+
const resolvedPair = this.resolveOnPair(entity, ast, a.alias, pair.left, pair.right);
|
|
369
|
+
if (resolvedPair === undefined) {
|
|
370
|
+
this.diagnostics.error("CDS010", `${ast.name}.${a.alias}: ON condition ${pair.left.join(".")} = ${pair.right.join(".")} not supported (only equalities between an element and a target element)`, this.loc(file, a.loc));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
association.on.push(resolvedPair);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!this.has(a.target)) {
|
|
377
|
+
this.diagnostics.error("CDS005", `${ast.name}.${a.alias}: unknown target entity ${a.target}`, this.loc(file, a.loc));
|
|
378
|
+
}
|
|
379
|
+
return association;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
addDataElementLabels(fields, _table) {
|
|
383
|
+
for (const field of fields) {
|
|
384
|
+
if (getAnnotation(field.annotations, "EndUserText.label") !== undefined || field.type.dataElement === undefined) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const label = this.dictionary.dataElement(field.type.dataElement)?.label;
|
|
388
|
+
if (label !== undefined) {
|
|
389
|
+
setAnnotation(field.annotations, "EndUserText.label", label);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** `$projection.A = _X.B`, `source.col = _X.B`, `A = _X.B` (both orders). */
|
|
394
|
+
resolveOnPair(entity, ast, alias, left, right) {
|
|
395
|
+
const isTarget = (p) => p.length === 2 && (p[0] ?? "").toUpperCase() === alias.toUpperCase();
|
|
396
|
+
const [sourcePath, targetPath] = isTarget(right) ? [left, right] : isTarget(left) ? [right, left] : [undefined, undefined];
|
|
397
|
+
if (sourcePath === undefined || targetPath === undefined) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
let element;
|
|
401
|
+
const head = (sourcePath[0] ?? "").toUpperCase();
|
|
402
|
+
if (sourcePath.length === 2 && head === "$PROJECTION") {
|
|
403
|
+
element = sourcePath[1];
|
|
404
|
+
}
|
|
405
|
+
else if (sourcePath.length === 2 && (head === ast.source?.toUpperCase() || head === ast.sourceAlias?.toUpperCase())) {
|
|
406
|
+
const column = (sourcePath[1] ?? "").toLowerCase();
|
|
407
|
+
element = entity.fields.find((f) => f.column === column)?.name;
|
|
408
|
+
}
|
|
409
|
+
else if (sourcePath.length === 1) {
|
|
410
|
+
const name = (sourcePath[0] ?? "").toUpperCase();
|
|
411
|
+
element = entity.fields.find((f) => f.name.toUpperCase() === name || f.column === name.toLowerCase())?.name;
|
|
412
|
+
}
|
|
413
|
+
const field = element === undefined ? undefined : entity.fields.find((f) => f.name.toUpperCase() === element?.toUpperCase());
|
|
414
|
+
if (field === undefined) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
return { source: field.name, target: targetPath[1] ?? "" };
|
|
418
|
+
}
|
|
419
|
+
// -------------------------------------------------------------------------------------------
|
|
420
|
+
// projections
|
|
421
|
+
projection(parsed) {
|
|
422
|
+
const { ast, file } = parsed;
|
|
423
|
+
const base = ast.source === undefined ? undefined : this.resolve(ast.source);
|
|
424
|
+
if (base === undefined) {
|
|
425
|
+
this.diagnostics.error("CDS003", `${ast.name}: unknown projected entity ${ast.source ?? ""}`, this.loc(file, ast.loc));
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
const fields = [];
|
|
429
|
+
const slots = [];
|
|
430
|
+
const associations = [];
|
|
431
|
+
for (const element of ast.elements) {
|
|
432
|
+
const source = element.path?.join(".") ?? "";
|
|
433
|
+
if (element.path === undefined) {
|
|
434
|
+
slots.push(element); // a calculated element (D67)
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (element.path.length !== 1) {
|
|
438
|
+
this.diagnostics.error("CDS002", `${ast.name}.${element.name || "?"}: path expressions (${source}) are not supported yet: only elements of the projected entity can be projected`, this.loc(file, element.loc));
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const baseAssociation = base.associations.find((a) => a.name.toUpperCase() === source.toUpperCase());
|
|
442
|
+
if (baseAssociation !== undefined) {
|
|
443
|
+
const target = element.redirect?.target ?? baseAssociation.target;
|
|
444
|
+
const association = {
|
|
445
|
+
name: element.name,
|
|
446
|
+
target: this.ast(target)?.ast.name ?? target,
|
|
447
|
+
kind: baseAssociation.kind,
|
|
448
|
+
cardinality: baseAssociation.cardinality,
|
|
449
|
+
on: [],
|
|
450
|
+
baseAssociation: baseAssociation.name,
|
|
451
|
+
src: this.loc(file, element.loc),
|
|
452
|
+
};
|
|
453
|
+
if (element.redirect === undefined && baseAssociation.kind !== "association") {
|
|
454
|
+
this.diagnostics.warning("CDS012", `${ast.name}.${element.name}: a ${baseAssociation.kind === "composition" ? "composition" : "to-parent association"} should be redirected to the projection of its target`, this.loc(file, element.loc));
|
|
455
|
+
}
|
|
456
|
+
if (element.redirect !== undefined && !this.has(element.redirect.target)) {
|
|
457
|
+
this.diagnostics.error("CDS005", `${ast.name}.${element.name}: unknown redirection target ${element.redirect.target}`, this.loc(file, element.loc));
|
|
458
|
+
}
|
|
459
|
+
associations.push(association);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const baseField = base.fields.find((f) => f.name.toUpperCase() === source.toUpperCase());
|
|
463
|
+
if (baseField === undefined) {
|
|
464
|
+
this.diagnostics.error("CDS004", `${ast.name}.${element.name}: ${base.name} has no element ${source}`, this.loc(file, element.loc));
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const field = this.fieldFromElement(parsed, { ...element, key: element.key || baseField.key }, baseField.type, propagated(ast, baseField.annotations));
|
|
468
|
+
field.baseField = baseField.name;
|
|
469
|
+
if (field.semantics === undefined && baseField.semantics !== undefined) {
|
|
470
|
+
field.semantics = baseField.semantics;
|
|
471
|
+
}
|
|
472
|
+
if (baseField.currencyCode !== undefined && field.currencyCode === undefined) {
|
|
473
|
+
field.currencyCode = projectedName(ast, baseField.currencyCode);
|
|
474
|
+
}
|
|
475
|
+
if (baseField.unitOfMeasure !== undefined && field.unitOfMeasure === undefined) {
|
|
476
|
+
field.unitOfMeasure = projectedName(ast, baseField.unitOfMeasure);
|
|
477
|
+
}
|
|
478
|
+
slots.push(field);
|
|
479
|
+
}
|
|
480
|
+
fields.push(...this.withCalculatedFields(parsed, slots, {
|
|
481
|
+
sourceName: base.name,
|
|
482
|
+
sourcePrefixes: [base.name, ...(ast.sourceAlias === undefined ? [] : [ast.sourceAlias])],
|
|
483
|
+
associations: new Set(base.associations.map((a) => a.name.toUpperCase())),
|
|
484
|
+
column: (name) => {
|
|
485
|
+
const baseField = base.fields.find((f) => f.name.toUpperCase() === name.toUpperCase() && f.draft !== true);
|
|
486
|
+
return baseField === undefined ? undefined : { name: baseField.name, type: baseField.type };
|
|
487
|
+
},
|
|
488
|
+
}));
|
|
489
|
+
// ON conditions: the pairs of the base association, renamed through both projections
|
|
490
|
+
const entity = {
|
|
491
|
+
name: ast.name,
|
|
492
|
+
kind: "projection",
|
|
493
|
+
root: ast.root,
|
|
494
|
+
source: base.name,
|
|
495
|
+
sqlView: sqlName(ast.name),
|
|
496
|
+
fields,
|
|
497
|
+
associations,
|
|
498
|
+
annotations: this.entityAnnotations(parsed),
|
|
499
|
+
src: this.loc(file, ast.loc),
|
|
500
|
+
};
|
|
501
|
+
if (ast.providerContract !== undefined) {
|
|
502
|
+
entity.providerContract = ast.providerContract;
|
|
503
|
+
}
|
|
504
|
+
for (const association of associations) {
|
|
505
|
+
const baseAssociation = base.associations.find((a) => a.name === association.baseAssociation);
|
|
506
|
+
const target = this.resolveLazy(association.target);
|
|
507
|
+
for (const pair of baseAssociation?.on ?? []) {
|
|
508
|
+
const source = fields.find((f) => (f.baseField ?? f.name).toUpperCase() === pair.source.toUpperCase());
|
|
509
|
+
const targetField = target === undefined ? undefined : target.fields.find((f) => (f.baseField ?? f.name).toUpperCase() === pair.target.toUpperCase());
|
|
510
|
+
if (source === undefined || (target !== undefined && targetField === undefined)) {
|
|
511
|
+
this.diagnostics.error("CDS013", `${ast.name}.${association.name}: the projection must expose ${pair.source} and its target ${pair.target} for the association`, association.src === undefined ? undefined : { ...association.src });
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
association.on.push({ source: source.name, target: targetField?.name ?? pair.target });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (fields.filter((f) => f.key).length === 0) {
|
|
518
|
+
this.diagnostics.error("CDS011", `${ast.name}: no key element`, this.loc(file, ast.loc));
|
|
519
|
+
}
|
|
520
|
+
this.checkExtensionElements(parsed, entity);
|
|
521
|
+
return entity;
|
|
522
|
+
}
|
|
523
|
+
/** Resolves an entity unless it is being resolved (cycles through associations). */
|
|
524
|
+
resolveLazy(name) {
|
|
525
|
+
const key = name.toUpperCase();
|
|
526
|
+
if (this.inProgress.has(key)) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
return this.resolve(name);
|
|
530
|
+
}
|
|
531
|
+
// -------------------------------------------------------------------------------------------
|
|
532
|
+
// table entities (D55)
|
|
533
|
+
/**
|
|
534
|
+
* A table entity: a table of the same name, whose columns are the elements in lower case, and
|
|
535
|
+
* a view entity on it. Its elements are typed with built-in types, data elements, CDS simple
|
|
536
|
+
* types or CDS enumerated types.
|
|
537
|
+
*/
|
|
538
|
+
tableEntity(parsed) {
|
|
539
|
+
const { ast, file } = parsed;
|
|
540
|
+
if (ast.name.length > 25) {
|
|
541
|
+
this.diagnostics.error("CDS019", `${ast.name}: the name of a table entity has at most 25 characters`, this.loc(file, ast.loc));
|
|
542
|
+
}
|
|
543
|
+
const columns = [];
|
|
544
|
+
const fields = [];
|
|
545
|
+
let keysDone = false;
|
|
546
|
+
for (const element of ast.elements) {
|
|
547
|
+
const declared = element.declaredType ?? "";
|
|
548
|
+
const type = parseBuiltInType(declared) ?? this.dictionary.dataElement(declared)?.type;
|
|
549
|
+
if (type === undefined) {
|
|
550
|
+
this.diagnostics.error("CDS014", `${ast.name}.${element.name}: unknown type ${declared}`, this.loc(file, element.loc));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (element.key && keysDone) {
|
|
554
|
+
this.diagnostics.error("CDS020", `${ast.name}.${element.name}: the key elements come first, without gaps`, this.loc(file, element.loc));
|
|
555
|
+
}
|
|
556
|
+
keysDone ||= !element.key;
|
|
557
|
+
if (element.key && element.nullable === true) {
|
|
558
|
+
this.diagnostics.error("CDS020", `${ast.name}.${element.name}: a key element cannot be null`, this.loc(file, element.loc));
|
|
559
|
+
}
|
|
560
|
+
if ((type.kind === "string" || type.kind === "rawstring") && element.nullable !== true) {
|
|
561
|
+
this.diagnostics.error("CDS020", `${ast.name}.${element.name}: an element of type ${type.kind} is declared null (no default value on the database)`, this.loc(file, element.loc));
|
|
562
|
+
}
|
|
563
|
+
const column = { name: element.name.toLowerCase(), type: { ...type }, key: element.key, notNull: element.nullable !== true, client: false };
|
|
564
|
+
columns.push(column);
|
|
565
|
+
const field = this.fieldFromElement(parsed, element, type, undefined);
|
|
566
|
+
field.column = column.name;
|
|
567
|
+
fields.push(field);
|
|
568
|
+
}
|
|
569
|
+
const table = { name: sqlName(ast.name), columns, src: this.loc(file, ast.loc) };
|
|
570
|
+
const label = getString(ast.annotations, "EndUserText.label");
|
|
571
|
+
if (label !== undefined) {
|
|
572
|
+
table.description = label;
|
|
573
|
+
}
|
|
574
|
+
this.dictionary.registerEntityTable(table);
|
|
575
|
+
this.addDataElementLabels(fields, table.name);
|
|
576
|
+
const entity = {
|
|
577
|
+
name: ast.name,
|
|
578
|
+
kind: "view",
|
|
579
|
+
tableEntity: true,
|
|
580
|
+
root: ast.root,
|
|
581
|
+
source: table.name,
|
|
582
|
+
sqlView: `${sqlName(ast.name)}_entity`,
|
|
583
|
+
fields,
|
|
584
|
+
associations: [],
|
|
585
|
+
annotations: this.entityAnnotations(parsed),
|
|
586
|
+
src: this.loc(file, ast.loc),
|
|
587
|
+
};
|
|
588
|
+
entity.associations = this.associationsOf(parsed, entity);
|
|
589
|
+
if (fields.filter((f) => f.key).length === 0 && entity.associations.length > 0) {
|
|
590
|
+
this.diagnostics.error("CDS011", `${ast.name}: a table entity with associations needs key elements`, this.loc(file, ast.loc));
|
|
591
|
+
}
|
|
592
|
+
this.checkExtensionElements(parsed, entity);
|
|
593
|
+
return entity;
|
|
594
|
+
}
|
|
595
|
+
// -------------------------------------------------------------------------------------------
|
|
596
|
+
// abstract entities (action parameters)
|
|
597
|
+
/** A custom entity: typed elements, read by the class of `@ObjectModel.query.implementedBy` (D43). */
|
|
598
|
+
custom(parsed) {
|
|
599
|
+
const { ast, file } = parsed;
|
|
600
|
+
const fields = [];
|
|
601
|
+
for (const element of ast.elements) {
|
|
602
|
+
const declared = element.declaredType ?? "";
|
|
603
|
+
const type = parseBuiltInType(declared) ?? this.dictionary.dataElement(declared)?.type;
|
|
604
|
+
if (type === undefined) {
|
|
605
|
+
this.diagnostics.error("CDS014", `${ast.name}.${element.name}: unknown type ${declared}`, this.loc(file, element.loc));
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
fields.push(this.fieldFromElement(parsed, element, type, undefined));
|
|
609
|
+
}
|
|
610
|
+
const implementedBy = getAnnotation(ast.annotations, "ObjectModel.query.implementedBy");
|
|
611
|
+
const entity = {
|
|
612
|
+
name: ast.name,
|
|
613
|
+
kind: "custom",
|
|
614
|
+
root: ast.root,
|
|
615
|
+
fields,
|
|
616
|
+
associations: [],
|
|
617
|
+
annotations: this.entityAnnotations(parsed),
|
|
618
|
+
src: this.loc(file, ast.loc),
|
|
619
|
+
};
|
|
620
|
+
if (typeof implementedBy === "string" && /^ABAP:\s*\S+$/i.test(implementedBy)) {
|
|
621
|
+
entity.queryClass = implementedBy.replace(/^ABAP:\s*/i, "").toUpperCase();
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
this.diagnostics.error("CDS016", `${ast.name}: a custom entity needs @ObjectModel.query.implementedBy: 'ABAP:<query class>'`, this.loc(file, ast.loc));
|
|
625
|
+
}
|
|
626
|
+
entity.associations = this.associationsOf(parsed, entity);
|
|
627
|
+
if (fields.filter((f) => f.key).length === 0) {
|
|
628
|
+
this.diagnostics.error("CDS011", `${ast.name}: no key element`, this.loc(file, ast.loc));
|
|
629
|
+
}
|
|
630
|
+
this.checkExtensionElements(parsed, entity);
|
|
631
|
+
return entity;
|
|
632
|
+
}
|
|
633
|
+
abstract(parsed) {
|
|
634
|
+
const { ast, file } = parsed;
|
|
635
|
+
const fields = [];
|
|
636
|
+
for (const element of ast.elements) {
|
|
637
|
+
const declared = element.declaredType ?? "";
|
|
638
|
+
const type = parseBuiltInType(declared) ?? this.dictionary.dataElement(declared)?.type;
|
|
639
|
+
if (type === undefined) {
|
|
640
|
+
this.diagnostics.error("CDS014", `${ast.name}.${element.name}: unknown type ${declared}`, this.loc(file, element.loc));
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
fields.push(this.fieldFromElement(parsed, element, type, undefined));
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
name: ast.name,
|
|
647
|
+
kind: "abstract",
|
|
648
|
+
root: ast.root,
|
|
649
|
+
fields,
|
|
650
|
+
associations: [],
|
|
651
|
+
annotations: this.entityAnnotations(parsed),
|
|
652
|
+
src: this.loc(file, ast.loc),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function withoutLayer(annotations) {
|
|
657
|
+
const copy = mergeAnnotations(annotations);
|
|
658
|
+
const metadata = copy["metadata"];
|
|
659
|
+
if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
|
|
660
|
+
delete metadata["layer"];
|
|
661
|
+
if (Object.keys(metadata).length === 0) {
|
|
662
|
+
delete copy["metadata"];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return copy;
|
|
666
|
+
}
|
|
667
|
+
/** Element annotations propagate from the projected entity unless the projection opts out. */
|
|
668
|
+
function propagated(ast, annotations) {
|
|
669
|
+
return getAnnotation(ast.annotations, "Metadata.ignorePropagatedAnnotations") === true ? undefined : annotations;
|
|
670
|
+
}
|
|
671
|
+
/** The name under which a projection exposes an element of its base. */
|
|
672
|
+
function projectedName(ast, baseName) {
|
|
673
|
+
const element = ast.elements.find((e) => e.path?.length === 1 && (e.path[0] ?? "").toUpperCase() === baseName.toUpperCase());
|
|
674
|
+
return element?.name ?? baseName;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=entities.js.map
|