@sap/cds-dk 4.6.3 → 4.7.3
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 +57 -0
- package/bin/add.js +2 -1
- package/bin/bind.js +121 -48
- package/bin/cds.js +8 -0
- package/bin/env.js +0 -2
- package/bin/repl.js +5 -2
- package/bin/watch.js +10 -7
- package/lib/bind/bindingResolver.js +26 -14
- package/lib/bind/bindingUtil.js +144 -0
- package/lib/bind/cfKeyProvider.js +2 -80
- package/lib/bind/kubernetesCredentialsProvider.js +157 -0
- package/lib/bind/userCdsrcJsonStorageProvider.js +7 -7
- package/lib/client/activate.js +8 -10
- package/lib/import/index.js +3 -2
- package/lib/import/internalDoc/edm2cdsTypeMapping.md +38 -0
- package/lib/import/utils/MultiMap.js +1 -0
- package/lib/import/v2parser.js +96 -33
- package/lib/import/v4parser.js +57 -18
- package/lib/index.js +6 -5
- package/lib/init/constants.js +2 -2
- package/lib/init/index.js +1 -0
- package/lib/init/template/data/index.js +18 -6
- package/lib/init/template/hana/files/common/undeploy.json +5 -0
- package/lib/init/template/hana/files/java/package.json +3 -0
- package/lib/init/template/hana/index.js +5 -5
- package/lib/init/template/lint/index.js +17 -1
- package/lib/init/template/mta/files/mta.yaml.hbs +12 -5
- package/lib/init/util/fsUtil.js +1 -1
- package/lib/init/util/mvnArchetypeUtil.js +3 -1
- package/lib/lint/Linter.js +1 -2
- package/lib/lint/io.js +3 -12
- package/lib/watch/node-watch.js +46 -20
- package/npm-shrinkwrap.json +10625 -5060
- package/package.json +3 -3
|
@@ -8,18 +8,6 @@ const { URL } = require('url');
|
|
|
8
8
|
const execAsync = util.promisify(cp.exec);
|
|
9
9
|
const IS_WIN = os.platform() === 'win32';
|
|
10
10
|
|
|
11
|
-
const KindToRequiresNameMap = {
|
|
12
|
-
xsuaa: 'uaa',
|
|
13
|
-
hana: 'db',
|
|
14
|
-
destinations: 'destinations',
|
|
15
|
-
connectivity: 'connectivity',
|
|
16
|
-
'audit-log': 'audit-log',
|
|
17
|
-
'enterprise-messaging': 'messaging',
|
|
18
|
-
'enterprise-messaging-amqp': 'messaging',
|
|
19
|
-
'enterprise-messaging-http': 'messaging',
|
|
20
|
-
'enterprise-messaging-shared': 'messaging'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
11
|
class CFKeyProvider {
|
|
24
12
|
|
|
25
13
|
constructor() {
|
|
@@ -69,7 +57,6 @@ class CFKeyProvider {
|
|
|
69
57
|
|
|
70
58
|
async getCfTargetFromCli() {
|
|
71
59
|
const result = await this._cfRead('target');
|
|
72
|
-
|
|
73
60
|
return {
|
|
74
61
|
apiEndpoint: this.extract(result, /^api endpoint:\s+(.*)$/i),
|
|
75
62
|
user: this.extract(result, /^user:\s+(.*)$/i),
|
|
@@ -142,7 +129,7 @@ class CFKeyProvider {
|
|
|
142
129
|
name = name || `${instance}:${key}`;
|
|
143
130
|
|
|
144
131
|
if (apiEndpoint && apiEndpoint !== target.apiEndpoint) {
|
|
145
|
-
throw new Error(`Current Cloud Foundry api endpoint "${target.apiEndpoint}"
|
|
132
|
+
throw new Error(`Current Cloud Foundry api endpoint "${target.apiEndpoint}" different than api endpoint "${apiEndpoint}" for service binding "${name}".`);
|
|
146
133
|
}
|
|
147
134
|
|
|
148
135
|
let spaceGuid;
|
|
@@ -182,6 +169,7 @@ class CFKeyProvider {
|
|
|
182
169
|
vcap: {
|
|
183
170
|
label: offeringObj.name,
|
|
184
171
|
plan: planObj.name,
|
|
172
|
+
tags: offeringObj.tags
|
|
185
173
|
},
|
|
186
174
|
resolved: true,
|
|
187
175
|
},
|
|
@@ -190,72 +178,6 @@ class CFKeyProvider {
|
|
|
190
178
|
|
|
191
179
|
return resolvedBinding;
|
|
192
180
|
}
|
|
193
|
-
|
|
194
|
-
getRequiresNameFromKind(service, kind) {
|
|
195
|
-
const requiresName = KindToRequiresNameMap[kind];
|
|
196
|
-
if (!requiresName) throw new Error(`Unknown CDS service name for service "${service}". Please specify as argument for "cds bind".`)
|
|
197
|
-
return requiresName;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Determine kind value for services based on VCAP binding
|
|
202
|
-
* information in the configuration.
|
|
203
|
-
*/
|
|
204
|
-
determineKinds(services) {
|
|
205
|
-
const vcapServices = {}
|
|
206
|
-
|
|
207
|
-
const getIndex = service => {
|
|
208
|
-
const { credentials } = service;
|
|
209
|
-
return credentials && credentials.binding && credentials.binding.index;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Build VCAP_SERVICES
|
|
213
|
-
for (let index = 0; index<services.length; index++) {
|
|
214
|
-
const service = services[index];
|
|
215
|
-
const { label, name, tags } = service.binding.vcap;
|
|
216
|
-
if (!vcapServices[label]) vcapServices[label] = [];
|
|
217
|
-
vcapServices[label].push({ credentials: { binding: { index } }, label, name, tags });
|
|
218
|
-
|
|
219
|
-
service.kindCandidates = [ ]
|
|
220
|
-
service.nameCandidates = [ ]
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Auto configure services based on VCAP_SERVICES
|
|
224
|
-
const cds = require('@sap/cds/lib');
|
|
225
|
-
const VCAP_SERVICES = process.env.VCAP_SERVICES;
|
|
226
|
-
try {
|
|
227
|
-
process.env.VCAP_SERVICES = JSON.stringify(vcapServices);
|
|
228
|
-
const env = cds.env.for('cds', '.');
|
|
229
|
-
|
|
230
|
-
// Extract "kind" from auto configured services
|
|
231
|
-
for (const kind of Object.keys(env.requires)) {
|
|
232
|
-
const requireService = env.requires[kind];
|
|
233
|
-
const superKind = requireService.kind;
|
|
234
|
-
const index = getIndex(requireService);
|
|
235
|
-
if (!Number.isInteger(index)) continue;
|
|
236
|
-
|
|
237
|
-
const superRequireService = env.requires[superKind];
|
|
238
|
-
if (superRequireService) {
|
|
239
|
-
const superIndex = getIndex(superRequireService);
|
|
240
|
-
if (index === superIndex) continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
services[index].kindCandidates.push(kind);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Check if one "kind" per service was found
|
|
247
|
-
for (const service of services) {
|
|
248
|
-
if (service.kindCandidates.length !== 1) {
|
|
249
|
-
throw new Error(`Unknown kind for service "${service.binding.instance}". Please specify with "--kind" option.`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
service.kind = service.kindCandidates[0];
|
|
253
|
-
delete service.kindCandidates;
|
|
254
|
-
}
|
|
255
|
-
} finally {
|
|
256
|
-
process.env.VCAP_SERVICES = VCAP_SERVICES;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
181
|
}
|
|
260
182
|
|
|
261
183
|
class BatchRequest {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const cp = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const util = require('util');
|
|
4
|
+
const yaml = require("@sap/cds-foss")("yaml");
|
|
5
|
+
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
|
|
9
|
+
const execFile = util.promisify(cp.execFile);
|
|
10
|
+
const IS_WIN = os.platform() === 'win32';
|
|
11
|
+
|
|
12
|
+
class KubernetesCredentialsProvider {
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.logger = console;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setLogger(logger) {
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async _kubectlRead(...args) {
|
|
23
|
+
this.logger.debug(`_kubectlRead: kubectl ${args.join(" ")}`);
|
|
24
|
+
|
|
25
|
+
const result = await execFile("kubectl", args, {
|
|
26
|
+
shell: IS_WIN,
|
|
27
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return result.stdout.toString().trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async _kubectlConfig() {
|
|
35
|
+
const configString = await this._kubectlRead('config', 'view', '--raw');
|
|
36
|
+
const config = yaml.parse(configString);
|
|
37
|
+
|
|
38
|
+
const currentContextName = config["current-context"];
|
|
39
|
+
if (!currentContextName) throw new Error(`No current context in kubectl config.`);
|
|
40
|
+
const currentContextEntry = config.contexts && config.contexts.find( entry => entry.name === currentContextName && entry.context && entry.context.cluster && entry.context.user );
|
|
41
|
+
if (!currentContextEntry) throw new Error(`Current context not found in kubectl config.`);
|
|
42
|
+
const currentContext = currentContextEntry.context;
|
|
43
|
+
const clusterEntry = config.clusters && config.clusters.find( entry => entry.name === currentContext.cluster && entry.cluster && entry.cluster.server );
|
|
44
|
+
if (!currentContextEntry) throw new Error(`Cluster ${currentContext.cluster} not found in kubectl config.`);
|
|
45
|
+
const cluster = clusterEntry.cluster;
|
|
46
|
+
const userEntry = config.users && config.users.find( entry => entry.name === currentContext.user && entry.user && entry.user.token );
|
|
47
|
+
if (!userEntry) throw new Error(`User ${currentContext.user} not found in kubectl config.`);
|
|
48
|
+
const user = userEntry.user;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
url: cluster.server,
|
|
52
|
+
namespace: currentContext.namespace || 'default',
|
|
53
|
+
token: user.token
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async init() {
|
|
58
|
+
this.config = await this._kubectlConfig();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _req(urlPath) {
|
|
62
|
+
|
|
63
|
+
const url = new URL(urlPath, this.config.url);
|
|
64
|
+
this.logger.debug(`_req: ${url.toString()}`);
|
|
65
|
+
|
|
66
|
+
const result = await axios.get(url.toString(), {
|
|
67
|
+
validateStatus: status => status === 200 || status === 404,
|
|
68
|
+
headers: {
|
|
69
|
+
authorization: `Bearer ${this.config.token}`
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.status === 200) {
|
|
74
|
+
return result.data;
|
|
75
|
+
} else {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async resolve(name, binding) {
|
|
82
|
+
if (!this.initPromise) {
|
|
83
|
+
this.initPromise = this.init();
|
|
84
|
+
}
|
|
85
|
+
await this.initPromise;
|
|
86
|
+
|
|
87
|
+
let {cluster, namespace, name: bindingName, secret } = binding;
|
|
88
|
+
let instance;
|
|
89
|
+
let secretOrBindingResource = 'Secret';
|
|
90
|
+
|
|
91
|
+
if (!namespace) namespace = this.config.namespace;
|
|
92
|
+
if (cluster && cluster !== this.config.url) throw new Error(`Current Kubernetes cluster "${this.config.url}" different than Kubernetes cluster "${cluster}" for service binding "${name}".`);
|
|
93
|
+
|
|
94
|
+
if (bindingName) {
|
|
95
|
+
const bindingResource = await this._req(`/apis/servicecatalog.k8s.io/v1beta1/namespaces/${encodeURIComponent(namespace)}/servicebindings/${encodeURIComponent(bindingName)}`);
|
|
96
|
+
if (bindingResource === null) {
|
|
97
|
+
// Fallback to secret instead of service binding;
|
|
98
|
+
secret = bindingName;
|
|
99
|
+
bindingName = undefined;
|
|
100
|
+
secretOrBindingResource = 'Service Binding or Secret';
|
|
101
|
+
} else {
|
|
102
|
+
secret = bindingResource.spec.secretName || throwError();
|
|
103
|
+
instance = bindingResource.spec.instanceRef && bindingResource.spec.instanceRef.name || throwError();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const secretResource = await this._req(`/api/v1/namespaces/${encodeURIComponent(namespace)}/secrets/${encodeURIComponent(secret)}`);
|
|
108
|
+
if (secretResource === null) throwError(`${secretOrBindingResource} '${secret}' not found.`);
|
|
109
|
+
|
|
110
|
+
const credentials = {};
|
|
111
|
+
for (const name in secretResource.data) {
|
|
112
|
+
let value = secretResource.data[name];
|
|
113
|
+
value = Buffer.from(value, "base64").toString(); // secret values are base64 encoded
|
|
114
|
+
// TODO: TESTIT
|
|
115
|
+
if (value && value[0] === '{') try { value = JSON.parse(value); } catch(e) {/* ignored */} // secret values may contain JSON objects
|
|
116
|
+
credentials[name] = value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolvedBinding = {
|
|
120
|
+
binding: {
|
|
121
|
+
type: "k8s",
|
|
122
|
+
name: bindingName,
|
|
123
|
+
cluster: this.config.url,
|
|
124
|
+
instance,
|
|
125
|
+
namespace,
|
|
126
|
+
secret: secret,
|
|
127
|
+
resolved: true,
|
|
128
|
+
},
|
|
129
|
+
credentials
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if (instance) {
|
|
134
|
+
const serviceInstanceResource = await this._req(`/apis/servicecatalog.k8s.io/v1beta1/namespaces/${encodeURIComponent(namespace)}/serviceinstances/${encodeURIComponent(instance)}`);
|
|
135
|
+
|
|
136
|
+
resolvedBinding.binding.vcap = {
|
|
137
|
+
label: serviceInstanceResource.spec.clusterServiceClassExternalName,
|
|
138
|
+
plan: serviceInstanceResource.spec.clusterServicePlanExternalName
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return resolvedBinding;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
*
|
|
150
|
+
* @param {string} msg
|
|
151
|
+
* @returns {never}
|
|
152
|
+
*/
|
|
153
|
+
function throwError(msg = 'Internal Error') {
|
|
154
|
+
throw new Error(msg);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = new KubernetesCredentialsProvider();
|
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
|
|
5
5
|
const { fse } = require('@sap/cds-foss');
|
|
6
|
-
const { highlight
|
|
6
|
+
const { highlight } = require('../util/term');
|
|
7
7
|
|
|
8
8
|
const commandUtil = require('../init/util/commandUtil');
|
|
9
9
|
|
|
@@ -108,19 +108,19 @@ ${file}
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
for (const service of services) {
|
|
111
|
-
const { name, kind, binding, credentials } = service;
|
|
111
|
+
const { name, kind, binding, credentials, vcap } = service;
|
|
112
112
|
const requireService = section[name] = section[name] || {};
|
|
113
113
|
|
|
114
|
-
Object.assign(requireService, {
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
Object.assign(requireService, { binding, credentials });
|
|
115
|
+
if (kind) requireService.kind = kind;
|
|
116
|
+
if (vcap) requireService.vcap = vcap;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
await this.writeJsonFile(jsonPath, jsonObject, options.logger);
|
|
120
120
|
if (options.profile) {
|
|
121
|
-
options.logger.log(`Saving bindings to ${highlight(jsonPath)} in profile ${highlight(options.profile)}
|
|
121
|
+
options.logger.log(`Saving bindings to ${highlight(jsonPath)} in profile ${highlight(options.profile)}`);
|
|
122
122
|
} else {
|
|
123
|
-
options.logger.log(`Saving bindings to ${highlight(jsonPath)}
|
|
123
|
+
options.logger.log(`Saving bindings to ${highlight(jsonPath)}`);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
package/lib/client/activate.js
CHANGED
|
@@ -166,15 +166,14 @@ module.exports = class Activate extends BaseCommand {
|
|
|
166
166
|
|
|
167
167
|
activationUrl = Activate.getMtxApiUrl(params.get('appUrl'), COMMAND_URL_SUFFIX);
|
|
168
168
|
const requestOptions = Object.assign({
|
|
169
|
-
headers: {}
|
|
170
|
-
data: {
|
|
171
|
-
extension: [...allFiles],
|
|
172
|
-
undeployExtension: params.get('undeploy')
|
|
173
|
-
}
|
|
169
|
+
headers: {}
|
|
174
170
|
}, params.get('reqAuth'));
|
|
175
171
|
requestOptions.headers['Content-Type'] = 'application/json';
|
|
176
172
|
|
|
177
|
-
return axios.post(activationUrl,
|
|
173
|
+
return axios.post(activationUrl, {
|
|
174
|
+
extension: [...allFiles],
|
|
175
|
+
undeployExtension: params.get('undeploy')
|
|
176
|
+
}, requestOptions);
|
|
178
177
|
})
|
|
179
178
|
.then(response => {
|
|
180
179
|
if (response === null) {
|
|
@@ -223,13 +222,12 @@ module.exports = class Activate extends BaseCommand {
|
|
|
223
222
|
const customFileUrl = Activate.getMtxApiUrl(params.get('appUrl'), Activate.UPDATE_CUSTOM_FILE_URL_SUFFIX);
|
|
224
223
|
const requestOptions = Object.assign({
|
|
225
224
|
headers: {},
|
|
226
|
-
data: {
|
|
227
|
-
content: customFilesSerialized
|
|
228
|
-
}
|
|
229
225
|
}, params.get('reqAuth'));
|
|
230
226
|
requestOptions.headers['Content-Type'] = 'application/json';
|
|
231
227
|
|
|
232
|
-
await axios.post(customFileUrl,
|
|
228
|
+
await axios.post(customFileUrl, {
|
|
229
|
+
content: customFilesSerialized
|
|
230
|
+
}, requestOptions);
|
|
233
231
|
|
|
234
232
|
Logger.log('Custom file upload finished');
|
|
235
233
|
}
|
package/lib/import/index.js
CHANGED
|
@@ -130,12 +130,13 @@ function _convertEDMXWithMultipleSchemas(edmx, multipleSchemas, namespace) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async function edmx2csn(edmx, filename, context) {
|
|
133
|
-
const result = edmx.match(/EntityType="([
|
|
133
|
+
const result = edmx.match(/EntityType="([^"]+)/);
|
|
134
134
|
const multipleSchemas = /(\s*<\/Schema>\s*<Schema [^>]*>)\s*/m;
|
|
135
135
|
if (!result) {
|
|
136
136
|
throw new Error(messages.MISSING_ENTITIES);
|
|
137
137
|
}
|
|
138
|
-
const
|
|
138
|
+
const index = result[1].lastIndexOf(".");
|
|
139
|
+
const namespace = (index > 0 ) ? result[1].substring(0, index) : result[1];
|
|
139
140
|
|
|
140
141
|
if (multipleSchemas.test(edmx)) {
|
|
141
142
|
edmx = _convertEDMXWithMultipleSchemas(edmx, multipleSchemas, namespace);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# OData V2 or V4 EDM Datatype mapping to CAP CDS
|
|
2
|
+
|
|
3
|
+
oData V2/V4 | CAP CDS | Comments (if any) |
|
|
4
|
+
------------|----------| ----------|
|
|
5
|
+
Edm.Guid |cds.UUID | |
|
|
6
|
+
Edm.Boolean |cds.Boolean| |
|
|
7
|
+
Edm.Int16 |cds.Integer| |
|
|
8
|
+
Edm.Int32 |cds.Integer| |
|
|
9
|
+
Edm.Int64 | cds.Integer64| |
|
|
10
|
+
Edm.Decimal | cds.DecimalFloat| |
|
|
11
|
+
Edm.Decimal Precision > 0 | cds.Decimal| |
|
|
12
|
+
Edm.Single | cds.Double @odata.type: “Edm.Single”| |
|
|
13
|
+
Edm.Double | cds.Double| |
|
|
14
|
+
Edm.Double Precision > 0 |cds.Double| |
|
|
15
|
+
Edm.String | cds.LargeString| |
|
|
16
|
+
Edm.String MaxLength > 0 |cds.String| |
|
|
17
|
+
Edm.Binary MaxLength > 0 | cds.Binary | |
|
|
18
|
+
Edm.Binary | cds.LargeBinary | |
|
|
19
|
+
Edm.Byte |cds.Integer| |
|
|
20
|
+
Edm.Byte MaxLength > 0 |cds.Binary| |
|
|
21
|
+
Edm.SByte |cds.Integer| |
|
|
22
|
+
Edm.Stream | cds.LargeBinary @odata.type: “Edm.Stream” | |
|
|
23
|
+
Edm.DateTimeOffset Precision : Microsecond | cds.Timestamp, @odata.type: “Edm.DateTimeOffset”, @odata.precision:<> | The value of precision from odata will be mapped to @odata.precision |
|
|
24
|
+
Edm.DateTimeOffset Precision : Second | cds.DateTime, @odata.type: “Edm.DateTimeOffset”, @odata.precision:0| |
|
|
25
|
+
| |
|
|
26
|
+
**Below attributes are applicable to V2 only** |
|
|
27
|
+
| |
|
|
28
|
+
Edm.DateTime Precision : Microsecond | cds.Timestamp, @odata.type: “Edm.DateTime”, @odata.precision:<> | The value of precision from odata will be mapped to @odata.precision | |
|
|
29
|
+
Edm.DateTime Precision : Second | cds.DateTime, @odata.type: “Edm.DateTime”, @odata.precision:0| |
|
|
30
|
+
Edm.Time |cds.Time| |
|
|
31
|
+
Edm.DateTime sap:display-format="Date" |cds.Date| |
|
|
32
|
+
Edm.DateTimeOffset sap:display-format="Date" |cds.Date||
|
|
33
|
+
**Below attributes are applicable to V4 only** |
|
|
34
|
+
| |
|
|
35
|
+
Edm.TimeOfDay |cds.Time||
|
|
36
|
+
Edm.Date |cds.Date||
|
|
37
|
+
|
|
38
|
+
|