@sap-ux/create 1.1.3 → 1.2.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/README.md CHANGED
@@ -416,7 +416,7 @@ Command group for updating saved resources. A subcommand is required.
416
416
 
417
417
  Usage: `npx --yes @sap-ux/create@latest update [subcommand] [options]`
418
418
 
419
- The available subcommands are: `system`
419
+ The available subcommands are: `system` and `service-metadata`
420
420
 
421
421
 
422
422
  --------------------------------
@@ -445,6 +445,28 @@ Options:
445
445
 
446
446
  --------------------------------
447
447
 
448
+ ## [`update service-metadata`](#update-service-metadata)
449
+
450
+ Refresh the local OData service metadata.xml from the live backend for a Fiori application.
451
+ Also fetches value-help (external) service metadata when available.
452
+
453
+ Only supported for Fiori applications connected to an EDMX backend. CAP applications are not supported,
454
+ as their service metadata is generated locally from the CDS model rather than fetched from a backend.
455
+
456
+ Example:
457
+
458
+ `npx --yes @sap-ux/create@latest update service-metadata /path/to/my-fiori-app`
459
+
460
+ `npx --yes @sap-ux/create@latest update service-metadata /path/to/my-fiori-app --simulate`
461
+
462
+ Options:
463
+ - `--service <name>` - Name of the data source in manifest.json (defaults to mainService or first service)
464
+ - `--no-value-help` - Skip fetching value-help (external) service metadata
465
+ - `-s, --simulate` - Simulate only. Do not write. Also sets `--verbose`.
466
+ - `-v, --verbose` - Show verbose information.
467
+
468
+ --------------------------------
469
+
448
470
  ## [`change`](#change)
449
471
 
450
472
  Command group for changing existing SAP Fiori applications. A subcommand is required.
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { addSystemUpdateCommand } from './system.js';
3
+ import { addServiceUpdateCommand } from './service.js';
3
4
  /**
4
5
  * Return 'create-fiori update *' commands. Commands include also the handler action.
5
6
  *
@@ -8,6 +9,7 @@ import { addSystemUpdateCommand } from './system.js';
8
9
  export function getUpdateCommands() {
9
10
  const updateCommands = new Command('update');
10
11
  addSystemUpdateCommand(updateCommands);
12
+ addServiceUpdateCommand(updateCommands);
11
13
  return updateCommands;
12
14
  }
13
15
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Add the "update service-metadata" subcommand to a passed command.
4
+ * Refreshes the local OData service metadata and value-help service metadata from the live backend.
5
+ *
6
+ * @param cmd - commander command to attach the service-metadata subcommand to
7
+ */
8
+ export declare function addServiceUpdateCommand(cmd: Command): void;
9
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1,244 @@
1
+ import { AbapCloudEnvironment, AbapServiceProvider, createForAbapOnCloud, createForDestination, ODataVersion as CatalogODataVersion, TlsPatch } from '@sap-ux/axios-extension';
2
+ import { isAppStudio, WebIDEUsage } from '@sap-ux/btp-utils';
3
+ import { getExternalServiceReferences, OdataVersion, ServiceType, update as updateOdataService } from '@sap-ux/odata-service-writer';
4
+ import { createApplicationAccess, FileName } from '@sap-ux/project-access';
5
+ import { BackendSystemKey, getService } from '@sap-ux/store';
6
+ import { UI5Config } from '@sap-ux/ui5-config';
7
+ import { create as createStore } from 'mem-fs';
8
+ import { create as createEditor } from 'mem-fs-editor';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { join, resolve } from 'node:path';
11
+ import { getLogger, setLogLevelVerbose, traceChanges } from '../../tracing/index.js';
12
+ import { validateBasePath } from '../../validation/index.js';
13
+ /**
14
+ * Creates an AbapServiceProvider for the given backend config.
15
+ * Returns null and logs an error if the required config is missing.
16
+ *
17
+ * @param backendConfig
18
+ * @param logger
19
+ */
20
+ async function buildProvider(backendConfig, logger) {
21
+ if (isAppStudio()) {
22
+ if (!backendConfig.destination) {
23
+ logger.error(`No destination found in '${FileName.Ui5Yaml}'. Add a 'destination' entry to the backend configuration to connect in SAP Business Application Studio.`);
24
+ return null;
25
+ }
26
+ const destination = { Name: backendConfig.destination, WebIDEUsage: WebIDEUsage.ODATA_ABAP };
27
+ return createForDestination({}, destination);
28
+ }
29
+ const backendUrl = backendConfig.connectPath
30
+ ? new URL(backendConfig.connectPath, backendConfig.url).href
31
+ : backendConfig.url;
32
+ const clientSuffix = backendConfig.client ? ` (client ${backendConfig.client})` : '';
33
+ const systemService = await getService({ entityName: 'system' });
34
+ if (!systemService) {
35
+ logger.error(`A stored connection configuration for backend system '${backendUrl}'${clientSuffix} cannot be found. Use 'npx @sap-ux/create@latest add system' to create a matching entry.`);
36
+ return null;
37
+ }
38
+ const system = await systemService.read(new BackendSystemKey({ url: backendUrl, client: backendConfig.client }));
39
+ if (!system) {
40
+ logger.error(`No stored system found for URL '${backendUrl}'${clientSuffix}. Run 'sap-ux add system' first.`);
41
+ return null;
42
+ }
43
+ const providerConfig = {
44
+ baseURL: backendConfig.url,
45
+ params: { 'sap-client': backendConfig.client },
46
+ ...(system.username ? { auth: { username: system.username, password: system.password ?? '' } } : {})
47
+ };
48
+ if (TlsPatch.isPatchRequired(providerConfig.baseURL ?? '')) {
49
+ TlsPatch.apply();
50
+ }
51
+ if (system.authenticationType === 'reentranceTicket' || system.serviceKeys) {
52
+ return createForAbapOnCloud({
53
+ environment: AbapCloudEnvironment.EmbeddedSteampunk,
54
+ url: backendConfig.url
55
+ });
56
+ }
57
+ return new AbapServiceProvider(providerConfig);
58
+ }
59
+ /**
60
+ * Fetches external (value-help) services when requested.
61
+ * Returns undefined if not requested, none found, or the fetch fails.
62
+ *
63
+ * @param provider
64
+ * @param servicePath
65
+ * @param metadataXml
66
+ * @param fetchExternal
67
+ * @param logger
68
+ */
69
+ async function fetchExternalServicesIfRequested(provider, servicePath, metadataXml, fetchExternal, logger) {
70
+ if (!fetchExternal) {
71
+ return undefined;
72
+ }
73
+ const references = getExternalServiceReferences(servicePath, metadataXml);
74
+ if (references.length === 0) {
75
+ logger.debug('No external service references found in metadata');
76
+ return undefined;
77
+ }
78
+ logger.info(`Fetching ${references.length} external service(s)...`);
79
+ try {
80
+ const externalServices = await provider.fetchExternalServices(references);
81
+ logger.debug(`Fetched ${externalServices.length} external service(s)`);
82
+ return externalServices;
83
+ }
84
+ catch (error) {
85
+ logger.warn(`Could not fetch external service metadata: ${error.message}. Continuing without external services.`);
86
+ logger.debug(error);
87
+ return undefined;
88
+ }
89
+ }
90
+ /**
91
+ * Fetches the service's remote annotations from the backend catalog.
92
+ * Returns undefined when none are available or the fetch fails, so that a metadata refresh
93
+ * never deletes the existing local annotation files (see odata-service-writer guard).
94
+ *
95
+ * @param provider
96
+ * @param servicePath
97
+ * @param odataVersion
98
+ * @param logger
99
+ */
100
+ async function fetchAnnotations(provider, servicePath, odataVersion, logger) {
101
+ // OData V4 embeds annotations in the metadata; the catalog returns none.
102
+ if (odataVersion === OdataVersion.v4) {
103
+ return undefined;
104
+ }
105
+ try {
106
+ const annotations = await provider.catalog(CatalogODataVersion.v2).getAnnotations({ path: servicePath });
107
+ if (annotations.length === 0) {
108
+ logger.debug('No remote annotations found for service');
109
+ return undefined;
110
+ }
111
+ logger.debug(`Fetched ${annotations.length} annotation source(s)`);
112
+ return annotations.map((annotation) => ({
113
+ technicalName: annotation.TechnicalName,
114
+ xml: annotation.Definitions
115
+ }));
116
+ }
117
+ catch (error) {
118
+ logger.warn(`Could not fetch annotations: ${error.message}. Existing local annotation files will be left untouched.`);
119
+ logger.debug(error);
120
+ return undefined;
121
+ }
122
+ }
123
+ /**
124
+ * Add the "update service-metadata" subcommand to a passed command.
125
+ * Refreshes the local OData service metadata and value-help service metadata from the live backend.
126
+ *
127
+ * @param cmd - commander command to attach the service-metadata subcommand to
128
+ */
129
+ export function addServiceUpdateCommand(cmd) {
130
+ cmd.command('service-metadata <appPath>')
131
+ .description(`Refresh the local OData service metadata.xml from the live backend for a Fiori application.
132
+ Also fetches value-help (external) service metadata when available.
133
+
134
+ Only supported for Fiori applications connected to an EDMX backend. CAP applications are not supported,
135
+ as their service metadata is generated locally from the CDS model rather than fetched from a backend.
136
+
137
+ Example:
138
+ \`npx --yes @sap-ux/create@latest update service-metadata /path/to/my-fiori-app\`
139
+ \`npx --yes @sap-ux/create@latest update service-metadata /path/to/my-fiori-app --simulate\``)
140
+ .option('--service <name>', 'Name of the data source in manifest.json (defaults to mainService or first service)')
141
+ .option('--no-value-help', 'Skip fetching value-help (external) service metadata')
142
+ .option('-s, --simulate', 'Simulate only. Do not write. Also sets `--verbose`.')
143
+ .option('-v, --verbose', 'Show verbose information.')
144
+ .action(async (appPath, options) => {
145
+ if (options.verbose || options.simulate) {
146
+ setLogLevelVerbose();
147
+ }
148
+ await updateService(resolve(appPath), options.valueHelp !== false, !!options.simulate, options.service);
149
+ });
150
+ }
151
+ /**
152
+ * Refreshes the local OData service metadata for a Fiori application.
153
+ *
154
+ * @param appPath - absolute path to the Fiori application root
155
+ * @param fetchExternalServiceMetadata - whether to also fetch value-help service metadata
156
+ * @param simulate - dry run; trace changes but do not write to disk
157
+ * @param serviceNameOpt - name of the data source in manifest (defaults to mainService or first service)
158
+ */
159
+ async function updateService(appPath, fetchExternalServiceMetadata, simulate, serviceNameOpt) {
160
+ const logger = getLogger();
161
+ try {
162
+ logger.debug(`Called update service-metadata for path '${appPath}'`);
163
+ await validateBasePath(appPath);
164
+ const appAccess = await createApplicationAccess(appPath);
165
+ if (appAccess.projectType !== 'EDMXBackend') {
166
+ logger.error('Update service-metadata is only supported for Fiori applications connected to an EDMX backend');
167
+ return;
168
+ }
169
+ const serviceName = serviceNameOpt ?? appAccess.app.mainService ?? Object.keys(appAccess.app.services)[0];
170
+ if (!serviceName) {
171
+ logger.error(`No OData service found in manifest for app at '${appPath}'`);
172
+ return;
173
+ }
174
+ if (serviceNameOpt && !appAccess.app.services[serviceNameOpt]) {
175
+ logger.error(`Service '${serviceNameOpt}' not found in manifest. Available: ${Object.keys(appAccess.app.services).join(', ')}`);
176
+ return;
177
+ }
178
+ const servicePath = appAccess.app.services[serviceName]?.uri;
179
+ if (!servicePath) {
180
+ logger.error(`Service '${serviceName}' has no URI defined in manifest`);
181
+ return;
182
+ }
183
+ const manifest = await appAccess.readManifest();
184
+ const odataVersionRaw = manifest['sap.app']?.dataSources?.[serviceName]?.settings?.odataVersion;
185
+ const odataVersion = odataVersionRaw?.startsWith('4') ? OdataVersion.v4 : OdataVersion.v2;
186
+ logger.debug(`Service '${serviceName}' at path '${servicePath}' (OData ${odataVersion})`);
187
+ const ui5YamlContent = await readFile(join(appPath, FileName.Ui5Yaml), 'utf-8');
188
+ const ui5Config = await UI5Config.newInstance(ui5YamlContent);
189
+ const backendConfig = ui5Config.getBackendConfigsFromFioriToolsProxyMiddleware()[0];
190
+ if (!backendConfig) {
191
+ logger.error(`No backend configuration found in '${FileName.Ui5Yaml}' for app at '${appPath}'`);
192
+ return;
193
+ }
194
+ const provider = await buildProvider(backendConfig, logger);
195
+ if (!provider) {
196
+ return;
197
+ }
198
+ logger.info(`Fetching metadata for service '${serviceName}'...`);
199
+ let metadataXml;
200
+ try {
201
+ metadataXml = await provider.service(servicePath).metadata();
202
+ }
203
+ catch (error) {
204
+ if (isAppStudio() && backendConfig.destination) {
205
+ throw new Error(`The service metadata is returning an error. Please check that the destination '${backendConfig.destination}' exists and the service is accessible.`);
206
+ }
207
+ throw error;
208
+ }
209
+ logger.debug(`Received metadata for service '${serviceName}'`);
210
+ const externalServices = await fetchExternalServicesIfRequested(provider, servicePath, metadataXml, fetchExternalServiceMetadata, logger);
211
+ const annotations = await fetchAnnotations(provider, servicePath, odataVersion, logger);
212
+ const serviceData = {
213
+ name: serviceName,
214
+ path: servicePath,
215
+ version: odataVersion,
216
+ type: ServiceType.EDMX,
217
+ metadata: metadataXml,
218
+ annotations,
219
+ externalServices,
220
+ previewSettings: {
221
+ path: backendConfig.path,
222
+ url: backendConfig.url,
223
+ client: backendConfig.client,
224
+ destination: backendConfig.destination,
225
+ connectPath: backendConfig.connectPath
226
+ }
227
+ };
228
+ const memStore = createStore();
229
+ const fs = createEditor(memStore);
230
+ // updateMiddlewares = false: refresh metadata.xml, annotation files and manifest dataSources only.
231
+ // The ui5*.yaml middlewares are intentionally left untouched by a metadata re-sync.
232
+ await updateOdataService(appPath, serviceData, fs, false);
233
+ await traceChanges(fs);
234
+ if (!simulate) {
235
+ await new Promise((resolve, reject) => fs.commit((err) => (err ? reject(err) : resolve())));
236
+ logger.info('Metadata updated.');
237
+ }
238
+ }
239
+ catch (error) {
240
+ logger.error(error.message);
241
+ logger.debug(error);
242
+ }
243
+ }
244
+ //# sourceMappingURL=service.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/create",
3
3
  "description": "SAP Fiori tools module to add or remove features",
4
- "version": "1.1.3",
4
+ "version": "1.2.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -33,23 +33,23 @@
33
33
  "mem-fs": "2.1.0",
34
34
  "mem-fs-editor": "9.4.0",
35
35
  "prompts": "2.4.2",
36
- "@sap-ux/abap-deploy-config-inquirer": "2.0.34",
36
+ "@sap-ux/abap-deploy-config-inquirer": "2.0.36",
37
37
  "@sap-ux/abap-deploy-config-writer": "1.0.20",
38
- "@sap-ux/adp-tooling": "1.0.40",
38
+ "@sap-ux/adp-tooling": "1.0.42",
39
39
  "@sap-ux/btp-utils": "2.0.6",
40
40
  "@sap-ux/app-config-writer": "1.1.15",
41
41
  "@sap-ux/cap-config-writer": "1.0.28",
42
- "@sap-ux/inquirer-common": "1.0.29",
42
+ "@sap-ux/inquirer-common": "1.0.31",
43
43
  "@sap-ux/logger": "1.0.3",
44
44
  "@sap-ux/mockserver-config-writer": "1.0.15",
45
- "@sap-ux/odata-service-writer": "1.0.17",
46
- "@sap-ux/preview-middleware": "1.1.1",
45
+ "@sap-ux/odata-service-writer": "1.0.18",
46
+ "@sap-ux/preview-middleware": "1.1.3",
47
47
  "@sap-ux/project-access": "2.1.10",
48
48
  "@sap-ux/project-input-validator": "1.0.15",
49
49
  "@sap-ux/system-access": "1.0.10",
50
50
  "@sap-ux/store": "2.0.6",
51
51
  "@sap-ux/ui5-config": "1.0.7",
52
- "@sap-ux/flp-config-inquirer": "1.0.40",
52
+ "@sap-ux/flp-config-inquirer": "1.0.42",
53
53
  "@sap-ux/nodejs-utils": "1.0.9",
54
54
  "@sap-ux/axios-extension": "2.0.8"
55
55
  },
@@ -60,7 +60,7 @@
60
60
  "@types/mem-fs": "1.1.2",
61
61
  "@types/mem-fs-editor": "7.0.1",
62
62
  "@types/prompts": "2.4.9",
63
- "@sap-ux/inquirer-common": "1.0.29",
63
+ "@sap-ux/inquirer-common": "1.0.31",
64
64
  "@sap-ux/store": "2.0.6"
65
65
  },
66
66
  "scripts": {