@backstage/backend-defaults 0.2.19-next.0 → 0.3.0-next.2

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.
@@ -3,6 +3,21 @@
3
3
  var backendCommon = require('@backstage/backend-common');
4
4
  var backendPluginApi = require('@backstage/backend-plugin-api');
5
5
  var config = require('@backstage/config');
6
+ var errors = require('@backstage/errors');
7
+ var knexFactory = require('knex');
8
+ var lodash = require('lodash');
9
+ var limiterFactory = require('p-limit');
10
+ var yn = require('yn');
11
+ var backendDevUtils = require('@backstage/backend-dev-utils');
12
+ var fsExtra = require('fs-extra');
13
+ var path = require('path');
14
+
15
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
16
+
17
+ var knexFactory__default = /*#__PURE__*/_interopDefaultCompat(knexFactory);
18
+ var limiterFactory__default = /*#__PURE__*/_interopDefaultCompat(limiterFactory);
19
+ var yn__default = /*#__PURE__*/_interopDefaultCompat(yn);
20
+ var path__default = /*#__PURE__*/_interopDefaultCompat(path);
6
21
 
7
22
  const databaseServiceFactory = backendPluginApi.createServiceFactory({
8
23
  service: backendPluginApi.coreServices.database,
@@ -28,5 +43,1051 @@ const databaseServiceFactory = backendPluginApi.createServiceFactory({
28
43
  }
29
44
  });
30
45
 
46
+ function defaultNameOverride(name) {
47
+ return {
48
+ connection: {
49
+ database: name
50
+ }
51
+ };
52
+ }
53
+
54
+ function mergeDatabaseConfig(config, ...overrides) {
55
+ return lodash.merge({}, config, ...overrides);
56
+ }
57
+
58
+ const ddlLimiter$1 = limiterFactory__default.default(1);
59
+ function createMysqlDatabaseClient(dbConfig, overrides) {
60
+ const knexConfig = buildMysqlDatabaseConfig(dbConfig, overrides);
61
+ const database = knexFactory__default.default(knexConfig);
62
+ return database;
63
+ }
64
+ function buildMysqlDatabaseConfig(dbConfig, overrides) {
65
+ return mergeDatabaseConfig(
66
+ dbConfig.get(),
67
+ {
68
+ connection: getMysqlConnectionConfig(dbConfig, !!overrides),
69
+ useNullAsDefault: true
70
+ },
71
+ overrides
72
+ );
73
+ }
74
+ function getMysqlConnectionConfig(dbConfig, parseConnectionString) {
75
+ const connection = dbConfig.get("connection");
76
+ const isConnectionString = typeof connection === "string" || connection instanceof String;
77
+ const autoParse = typeof parseConnectionString !== "boolean";
78
+ const shouldParseConnectionString = autoParse ? isConnectionString : parseConnectionString && isConnectionString;
79
+ return shouldParseConnectionString ? parseMysqlConnectionString(connection) : connection;
80
+ }
81
+ function parseMysqlConnectionString(connectionString) {
82
+ try {
83
+ const {
84
+ protocol,
85
+ username,
86
+ password,
87
+ port,
88
+ hostname,
89
+ pathname,
90
+ searchParams
91
+ } = new URL(connectionString);
92
+ if (protocol !== "mysql:") {
93
+ throw new Error(`Unknown protocol ${protocol}`);
94
+ } else if (!username || !password) {
95
+ throw new Error(`Missing username/password`);
96
+ } else if (!pathname.match(/^\/[^/]+$/)) {
97
+ throw new Error(`Expected single path segment`);
98
+ }
99
+ const result = {
100
+ user: username,
101
+ password,
102
+ host: hostname,
103
+ port: Number(port || 3306),
104
+ database: decodeURIComponent(pathname.substring(1))
105
+ };
106
+ const ssl = searchParams.get("ssl");
107
+ if (ssl) {
108
+ result.ssl = ssl;
109
+ }
110
+ const debug = searchParams.get("debug");
111
+ if (debug) {
112
+ result.debug = yn__default.default(debug);
113
+ }
114
+ return result;
115
+ } catch (e) {
116
+ throw new errors.InputError(
117
+ `Error while parsing MySQL connection string, ${e}`,
118
+ e
119
+ );
120
+ }
121
+ }
122
+ async function ensureMysqlDatabaseExists(dbConfig, ...databases) {
123
+ const admin = createMysqlDatabaseClient(dbConfig, {
124
+ connection: {
125
+ database: null
126
+ },
127
+ pool: {
128
+ min: 0,
129
+ acquireTimeoutMillis: 1e4
130
+ }
131
+ });
132
+ try {
133
+ const ensureDatabase = async (database) => {
134
+ await admin.raw(`CREATE DATABASE IF NOT EXISTS ??`, [database]);
135
+ };
136
+ await Promise.all(
137
+ databases.map(async (database) => {
138
+ let lastErr = void 0;
139
+ for (let i = 0; i < 3; i++) {
140
+ try {
141
+ return await ddlLimiter$1(() => ensureDatabase(database));
142
+ } catch (err) {
143
+ lastErr = err;
144
+ }
145
+ await new Promise((resolve) => setTimeout(resolve, 100));
146
+ }
147
+ throw lastErr;
148
+ })
149
+ );
150
+ } finally {
151
+ await admin.destroy();
152
+ }
153
+ }
154
+ async function dropMysqlDatabase(dbConfig, ...databases) {
155
+ const admin = createMysqlDatabaseClient(dbConfig, {
156
+ connection: {
157
+ database: null
158
+ },
159
+ pool: {
160
+ min: 0,
161
+ acquireTimeoutMillis: 1e4
162
+ }
163
+ });
164
+ try {
165
+ const dropDatabase = async (database) => {
166
+ await admin.raw(`DROP DATABASE ??`, [database]);
167
+ };
168
+ await Promise.all(
169
+ databases.map(async (database) => {
170
+ return await ddlLimiter$1(() => dropDatabase(database));
171
+ })
172
+ );
173
+ } finally {
174
+ await admin.destroy();
175
+ }
176
+ }
177
+ const mysqlConnector = Object.freeze({
178
+ createClient: createMysqlDatabaseClient,
179
+ ensureDatabaseExists: ensureMysqlDatabaseExists,
180
+ createNameOverride: defaultNameOverride,
181
+ parseConnectionString: parseMysqlConnectionString,
182
+ dropDatabase: dropMysqlDatabase
183
+ });
184
+ function pluginPath$3(pluginId) {
185
+ return `plugin.${pluginId}`;
186
+ }
187
+ function normalizeConnection$2(connection, client) {
188
+ if (typeof connection === "undefined" || connection === null) {
189
+ return {};
190
+ }
191
+ return typeof connection === "string" || connection instanceof String ? mysqlConnector.parseConnectionString(connection, client) : connection;
192
+ }
193
+ function createNameOverride$2(client, name) {
194
+ try {
195
+ return mysqlConnector.createNameOverride(name);
196
+ } catch (e) {
197
+ throw new errors.InputError(
198
+ `Unable to create database name override for '${client}' connector`,
199
+ e
200
+ );
201
+ }
202
+ }
203
+ class MysqlConnector {
204
+ constructor(config, prefix) {
205
+ this.config = config;
206
+ this.prefix = prefix;
207
+ }
208
+ async getClient(pluginId, deps) {
209
+ const pluginConfig = new config.ConfigReader(
210
+ this.getConfigForPlugin(pluginId)
211
+ );
212
+ const databaseName = this.getDatabaseName(pluginId);
213
+ if (databaseName && this.getEnsureExistsConfig(pluginId)) {
214
+ try {
215
+ await mysqlConnector.ensureDatabaseExists(pluginConfig, databaseName);
216
+ } catch (error) {
217
+ throw new Error(
218
+ `Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`
219
+ );
220
+ }
221
+ }
222
+ const pluginDivisionMode = this.getPluginDivisionModeConfig();
223
+ if (pluginDivisionMode !== "database") {
224
+ throw new Error(
225
+ `The MySQL driver does not support plugin division mode '${pluginDivisionMode}'`
226
+ );
227
+ }
228
+ const databaseClientOverrides = mergeDatabaseConfig(
229
+ {},
230
+ this.getDatabaseOverrides(pluginId)
231
+ );
232
+ const client = mysqlConnector.createClient(
233
+ pluginConfig,
234
+ databaseClientOverrides,
235
+ deps
236
+ );
237
+ return client;
238
+ }
239
+ async dropDatabase(...databaseNames) {
240
+ return await dropMysqlDatabase(this.config, ...databaseNames);
241
+ }
242
+ /**
243
+ * Provides the canonical database name for a given plugin.
244
+ *
245
+ * This method provides the effective database name which is determined using
246
+ * global and plugin specific database config. If no explicit database name,
247
+ * this method will provide a generated name which is the pluginId prefixed
248
+ * with 'backstage_plugin_'.
249
+ *
250
+ * @param pluginId - Lookup the database name for given plugin
251
+ * @returns String representing the plugin's database name
252
+ */
253
+ getDatabaseName(pluginId) {
254
+ const connection = this.getConnectionConfig(pluginId);
255
+ const databaseName = connection?.database;
256
+ return databaseName ?? `${this.prefix}${pluginId}`;
257
+ }
258
+ /**
259
+ * Provides the client type which should be used for a given plugin.
260
+ *
261
+ * The client type is determined by plugin specific config if present.
262
+ * Otherwise the base client is used as the fallback.
263
+ *
264
+ * @param pluginId - Plugin to get the client type for
265
+ * @returns Object with client type returned as `client` and boolean
266
+ * representing whether or not the client was overridden as
267
+ * `overridden`
268
+ */
269
+ getClientType(pluginId) {
270
+ const pluginClient = this.config.getOptionalString(
271
+ `${pluginPath$3(pluginId)}.client`
272
+ );
273
+ const baseClient = this.config.getString("client");
274
+ const client = pluginClient ?? baseClient;
275
+ return {
276
+ client,
277
+ overridden: client !== baseClient
278
+ };
279
+ }
280
+ getRoleConfig(pluginId) {
281
+ return this.config.getOptionalString(`${pluginPath$3(pluginId)}.role`) ?? this.config.getOptionalString("role");
282
+ }
283
+ /**
284
+ * Provides the knexConfig which should be used for a given plugin.
285
+ *
286
+ * @param pluginId - Plugin to get the knexConfig for
287
+ * @returns The merged knexConfig value or undefined if it isn't specified
288
+ */
289
+ getAdditionalKnexConfig(pluginId) {
290
+ const pluginConfig = this.config.getOptionalConfig(`${pluginPath$3(pluginId)}.knexConfig`)?.get();
291
+ const baseConfig = this.config.getOptionalConfig("knexConfig")?.get();
292
+ return lodash.merge(baseConfig, pluginConfig);
293
+ }
294
+ getEnsureExistsConfig(pluginId) {
295
+ const baseConfig = this.config.getOptionalBoolean("ensureExists") ?? true;
296
+ return this.config.getOptionalBoolean(`${pluginPath$3(pluginId)}.ensureExists`) ?? baseConfig;
297
+ }
298
+ getPluginDivisionModeConfig() {
299
+ return this.config.getOptionalString("pluginDivisionMode") ?? "database";
300
+ }
301
+ /**
302
+ * Provides a Knex connection plugin config by combining base and plugin
303
+ * config.
304
+ *
305
+ * This method provides a baseConfig for a plugin database connector. If the
306
+ * client type has not been overridden, the global connection config will be
307
+ * included with plugin specific config as the base. Values from the plugin
308
+ * connection take precedence over the base. Base database name is omitted
309
+ * unless `pluginDivisionMode` is set to `schema`.
310
+ */
311
+ getConnectionConfig(pluginId) {
312
+ const { client, overridden } = this.getClientType(pluginId);
313
+ let baseConnection = normalizeConnection$2(
314
+ this.config.get("connection"),
315
+ this.config.getString("client")
316
+ );
317
+ if (this.getPluginDivisionModeConfig() !== "schema") {
318
+ baseConnection = lodash.omit(baseConnection, "database");
319
+ }
320
+ const connection = normalizeConnection$2(
321
+ this.config.getOptional(`${pluginPath$3(pluginId)}.connection`),
322
+ client
323
+ );
324
+ return {
325
+ // include base connection if client type has not been overridden
326
+ ...overridden ? {} : baseConnection,
327
+ ...connection
328
+ };
329
+ }
330
+ /**
331
+ * Provides a Knex database config for a given plugin.
332
+ *
333
+ * This method provides a Knex configuration object along with the plugin's
334
+ * client type.
335
+ *
336
+ * @param pluginId - The plugin that the database config should correspond with
337
+ */
338
+ getConfigForPlugin(pluginId) {
339
+ const { client } = this.getClientType(pluginId);
340
+ const role = this.getRoleConfig(pluginId);
341
+ return {
342
+ ...this.getAdditionalKnexConfig(pluginId),
343
+ client,
344
+ connection: this.getConnectionConfig(pluginId),
345
+ ...role && { role }
346
+ };
347
+ }
348
+ /**
349
+ * Provides a partial `Knex.Config`• database name override for a given plugin.
350
+ *
351
+ * @param pluginId - Target plugin to get database name override
352
+ * @returns Partial `Knex.Config` with database name override
353
+ */
354
+ getDatabaseOverrides(pluginId) {
355
+ const databaseName = this.getDatabaseName(pluginId);
356
+ return databaseName ? createNameOverride$2(this.getClientType(pluginId).client, databaseName) : {};
357
+ }
358
+ }
359
+
360
+ function defaultSchemaOverride(name) {
361
+ return {
362
+ searchPath: [name]
363
+ };
364
+ }
365
+
366
+ const ddlLimiter = limiterFactory__default.default(1);
367
+ function createPgDatabaseClient(dbConfig, overrides) {
368
+ const knexConfig = buildPgDatabaseConfig(dbConfig, overrides);
369
+ const database = knexFactory__default.default(knexConfig);
370
+ const role = dbConfig.getOptionalString("role");
371
+ if (role) {
372
+ database.client.pool.on(
373
+ "createSuccess",
374
+ async (_event, pgClient) => {
375
+ await pgClient.query(`SET ROLE ${role}`);
376
+ }
377
+ );
378
+ }
379
+ return database;
380
+ }
381
+ function buildPgDatabaseConfig(dbConfig, overrides) {
382
+ return mergeDatabaseConfig(
383
+ dbConfig.get(),
384
+ {
385
+ connection: getPgConnectionConfig(dbConfig, !!overrides),
386
+ useNullAsDefault: true
387
+ },
388
+ overrides
389
+ );
390
+ }
391
+ function getPgConnectionConfig(dbConfig, parseConnectionString) {
392
+ const connection = dbConfig.get("connection");
393
+ const isConnectionString = typeof connection === "string" || connection instanceof String;
394
+ const autoParse = typeof parseConnectionString !== "boolean";
395
+ const shouldParseConnectionString = autoParse ? isConnectionString : parseConnectionString && isConnectionString;
396
+ return shouldParseConnectionString ? parsePgConnectionString(connection) : connection;
397
+ }
398
+ function parsePgConnectionString(connectionString) {
399
+ const parse = requirePgConnectionString();
400
+ return parse(connectionString);
401
+ }
402
+ function requirePgConnectionString() {
403
+ try {
404
+ return require("pg-connection-string").parse;
405
+ } catch (e) {
406
+ throw new errors.ForwardedError("Postgres: Install 'pg-connection-string'", e);
407
+ }
408
+ }
409
+ async function ensurePgDatabaseExists(dbConfig, ...databases) {
410
+ const admin = createPgDatabaseClient(dbConfig, {
411
+ connection: {
412
+ database: "postgres"
413
+ },
414
+ pool: {
415
+ min: 0,
416
+ acquireTimeoutMillis: 1e4
417
+ }
418
+ });
419
+ try {
420
+ const ensureDatabase = async (database) => {
421
+ const result = await admin.from("pg_database").where("datname", database).count();
422
+ if (parseInt(result[0].count, 10) > 0) {
423
+ return;
424
+ }
425
+ await admin.raw(`CREATE DATABASE ??`, [database]);
426
+ };
427
+ await Promise.all(
428
+ databases.map(async (database) => {
429
+ let lastErr = void 0;
430
+ for (let i = 0; i < 3; i++) {
431
+ try {
432
+ return await ddlLimiter(() => ensureDatabase(database));
433
+ } catch (err) {
434
+ lastErr = err;
435
+ }
436
+ await new Promise((resolve) => setTimeout(resolve, 100));
437
+ }
438
+ throw lastErr;
439
+ })
440
+ );
441
+ } finally {
442
+ await admin.destroy();
443
+ }
444
+ }
445
+ async function ensurePgSchemaExists(dbConfig, ...schemas) {
446
+ const admin = createPgDatabaseClient(dbConfig);
447
+ const role = dbConfig.getOptionalString("role");
448
+ try {
449
+ const ensureSchema = async (database) => {
450
+ if (role) {
451
+ await admin.raw(`CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ??`, [
452
+ database,
453
+ role
454
+ ]);
455
+ } else {
456
+ await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [database]);
457
+ }
458
+ };
459
+ await Promise.all(
460
+ schemas.map((database) => ddlLimiter(() => ensureSchema(database)))
461
+ );
462
+ } finally {
463
+ await admin.destroy();
464
+ }
465
+ }
466
+ async function dropPgDatabase(dbConfig, ...databases) {
467
+ const admin = createPgDatabaseClient(dbConfig);
468
+ try {
469
+ await Promise.all(
470
+ databases.map(async (database) => {
471
+ await ddlLimiter(() => admin.raw(`DROP DATABASE ??`, [database]));
472
+ })
473
+ );
474
+ } finally {
475
+ await admin.destroy();
476
+ }
477
+ }
478
+ const pgConnector = Object.freeze({
479
+ createClient: createPgDatabaseClient,
480
+ ensureDatabaseExists: ensurePgDatabaseExists,
481
+ ensureSchemaExists: ensurePgSchemaExists,
482
+ createNameOverride: defaultNameOverride,
483
+ createSchemaOverride: defaultSchemaOverride,
484
+ parseConnectionString: parsePgConnectionString,
485
+ dropDatabase: dropPgDatabase
486
+ });
487
+ function pluginPath$2(pluginId) {
488
+ return `plugin.${pluginId}`;
489
+ }
490
+ function normalizeConnection$1(connection, client) {
491
+ if (typeof connection === "undefined" || connection === null) {
492
+ return {};
493
+ }
494
+ return typeof connection === "string" || connection instanceof String ? pgConnector.parseConnectionString(connection, client) : connection;
495
+ }
496
+ function createSchemaOverride(client, name) {
497
+ try {
498
+ return pgConnector.createSchemaOverride?.(name);
499
+ } catch (e) {
500
+ throw new errors.InputError(
501
+ `Unable to create database schema override for '${client}' connector`,
502
+ e
503
+ );
504
+ }
505
+ }
506
+ function createNameOverride$1(client, name) {
507
+ try {
508
+ return pgConnector.createNameOverride(name);
509
+ } catch (e) {
510
+ throw new errors.InputError(
511
+ `Unable to create database name override for '${client}' connector`,
512
+ e
513
+ );
514
+ }
515
+ }
516
+ class PgConnector {
517
+ constructor(config, prefix) {
518
+ this.config = config;
519
+ this.prefix = prefix;
520
+ }
521
+ async getClient(pluginId, deps) {
522
+ const pluginConfig = new config.ConfigReader(
523
+ this.getConfigForPlugin(pluginId)
524
+ );
525
+ const databaseName = this.getDatabaseName(pluginId);
526
+ if (databaseName && this.getEnsureExistsConfig(pluginId)) {
527
+ try {
528
+ await pgConnector.ensureDatabaseExists(pluginConfig, databaseName);
529
+ } catch (error) {
530
+ throw new Error(
531
+ `Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`
532
+ );
533
+ }
534
+ }
535
+ let schemaOverrides;
536
+ if (this.getPluginDivisionModeConfig() === "schema") {
537
+ schemaOverrides = this.getSchemaOverrides(pluginId);
538
+ if (this.getEnsureSchemaExistsConfig(pluginId) || this.getEnsureExistsConfig(pluginId)) {
539
+ try {
540
+ await pgConnector.ensureSchemaExists(pluginConfig, pluginId);
541
+ } catch (error) {
542
+ throw new Error(
543
+ `Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`
544
+ );
545
+ }
546
+ }
547
+ }
548
+ const databaseClientOverrides = mergeDatabaseConfig(
549
+ {},
550
+ this.getDatabaseOverrides(pluginId),
551
+ schemaOverrides
552
+ );
553
+ const client = pgConnector.createClient(
554
+ pluginConfig,
555
+ databaseClientOverrides,
556
+ deps
557
+ );
558
+ return client;
559
+ }
560
+ async dropDatabase(...databaseNames) {
561
+ return await dropPgDatabase(this.config, ...databaseNames);
562
+ }
563
+ /**
564
+ * Provides the canonical database name for a given plugin.
565
+ *
566
+ * This method provides the effective database name which is determined using global
567
+ * and plugin specific database config. If no explicit database name is configured
568
+ * and `pluginDivisionMode` is not `schema`, this method will provide a generated name
569
+ * which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
570
+ * `schema`, it will fallback to using the default database for the knex instance.
571
+ *
572
+ * @param pluginId - Lookup the database name for given plugin
573
+ * @returns String representing the plugin's database name
574
+ */
575
+ getDatabaseName(pluginId) {
576
+ const connection = this.getConnectionConfig(pluginId);
577
+ const databaseName = connection?.database;
578
+ if (this.getPluginDivisionModeConfig() === "schema") {
579
+ return databaseName;
580
+ }
581
+ return databaseName ?? `${this.prefix}${pluginId}`;
582
+ }
583
+ /**
584
+ * Provides the client type which should be used for a given plugin.
585
+ *
586
+ * The client type is determined by plugin specific config if present.
587
+ * Otherwise the base client is used as the fallback.
588
+ *
589
+ * @param pluginId - Plugin to get the client type for
590
+ * @returns Object with client type returned as `client` and boolean
591
+ * representing whether or not the client was overridden as
592
+ * `overridden`
593
+ */
594
+ getClientType(pluginId) {
595
+ const pluginClient = this.config.getOptionalString(
596
+ `${pluginPath$2(pluginId)}.client`
597
+ );
598
+ const baseClient = this.config.getString("client");
599
+ const client = pluginClient ?? baseClient;
600
+ return {
601
+ client,
602
+ overridden: client !== baseClient
603
+ };
604
+ }
605
+ getRoleConfig(pluginId) {
606
+ return this.config.getOptionalString(`${pluginPath$2(pluginId)}.role`) ?? this.config.getOptionalString("role");
607
+ }
608
+ /**
609
+ * Provides the knexConfig which should be used for a given plugin.
610
+ *
611
+ * @param pluginId - Plugin to get the knexConfig for
612
+ * @returns The merged knexConfig value or undefined if it isn't specified
613
+ */
614
+ getAdditionalKnexConfig(pluginId) {
615
+ const pluginConfig = this.config.getOptionalConfig(`${pluginPath$2(pluginId)}.knexConfig`)?.get();
616
+ const baseConfig = this.config.getOptionalConfig("knexConfig")?.get();
617
+ return lodash.merge(baseConfig, pluginConfig);
618
+ }
619
+ getEnsureExistsConfig(pluginId) {
620
+ const baseConfig = this.config.getOptionalBoolean("ensureExists") ?? true;
621
+ return this.config.getOptionalBoolean(`${pluginPath$2(pluginId)}.ensureExists`) ?? baseConfig;
622
+ }
623
+ getEnsureSchemaExistsConfig(pluginId) {
624
+ const baseConfig = this.config.getOptionalBoolean("ensureSchemaExists") ?? false;
625
+ return this.config.getOptionalBoolean(
626
+ `${pluginPath$2(pluginId)}.getEnsureSchemaExistsConfig`
627
+ ) ?? baseConfig;
628
+ }
629
+ getPluginDivisionModeConfig() {
630
+ return this.config.getOptionalString("pluginDivisionMode") ?? "database";
631
+ }
632
+ /**
633
+ * Provides a Knex connection plugin config by combining base and plugin
634
+ * config.
635
+ *
636
+ * This method provides a baseConfig for a plugin database connector. If the
637
+ * client type has not been overridden, the global connection config will be
638
+ * included with plugin specific config as the base. Values from the plugin
639
+ * connection take precedence over the base. Base database name is omitted
640
+ * unless `pluginDivisionMode` is set to `schema`.
641
+ */
642
+ getConnectionConfig(pluginId) {
643
+ const { client, overridden } = this.getClientType(pluginId);
644
+ let baseConnection = normalizeConnection$1(
645
+ this.config.get("connection"),
646
+ this.config.getString("client")
647
+ );
648
+ if (this.getPluginDivisionModeConfig() !== "schema") {
649
+ baseConnection = lodash.omit(baseConnection, "database");
650
+ }
651
+ const connection = normalizeConnection$1(
652
+ this.config.getOptional(`${pluginPath$2(pluginId)}.connection`),
653
+ client
654
+ );
655
+ baseConnection.application_name ||= `backstage_plugin_${pluginId}`;
656
+ return {
657
+ // include base connection if client type has not been overridden
658
+ ...overridden ? {} : baseConnection,
659
+ ...connection
660
+ };
661
+ }
662
+ /**
663
+ * Provides a Knex database config for a given plugin.
664
+ *
665
+ * This method provides a Knex configuration object along with the plugin's
666
+ * client type.
667
+ *
668
+ * @param pluginId - The plugin that the database config should correspond with
669
+ */
670
+ getConfigForPlugin(pluginId) {
671
+ const { client } = this.getClientType(pluginId);
672
+ const role = this.getRoleConfig(pluginId);
673
+ return {
674
+ ...this.getAdditionalKnexConfig(pluginId),
675
+ client,
676
+ connection: this.getConnectionConfig(pluginId),
677
+ ...role && { role }
678
+ };
679
+ }
680
+ /**
681
+ * Provides a partial `Knex.Config` database schema override for a given
682
+ * plugin.
683
+ *
684
+ * @param pluginId - Target plugin to get database schema override
685
+ * @returns Partial `Knex.Config` with database schema override
686
+ */
687
+ getSchemaOverrides(pluginId) {
688
+ return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
689
+ }
690
+ /**
691
+ * Provides a partial `Knex.Config`• database name override for a given plugin.
692
+ *
693
+ * @param pluginId - Target plugin to get database name override
694
+ * @returns Partial `Knex.Config` with database name override
695
+ */
696
+ getDatabaseOverrides(pluginId) {
697
+ const databaseName = this.getDatabaseName(pluginId);
698
+ return databaseName ? createNameOverride$1(this.getClientType(pluginId).client, databaseName) : {};
699
+ }
700
+ }
701
+
702
+ function createSqliteDatabaseClient(dbConfig, overrides, deps) {
703
+ const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides);
704
+ const connConfig = knexConfig.connection;
705
+ const filename = connConfig.filename ?? ":memory:";
706
+ if (filename !== ":memory:") {
707
+ const directory = path__default.default.dirname(filename);
708
+ fsExtra.ensureDirSync(directory);
709
+ }
710
+ let database;
711
+ if (deps && filename === ":memory:") {
712
+ const devStore = backendDevUtils.DevDataStore.get();
713
+ if (devStore) {
714
+ const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`;
715
+ const connectionLoader = async () => {
716
+ const { data: seedData } = await devStore.load(dataKey);
717
+ return {
718
+ ...knexConfig.connection,
719
+ filename: seedData ?? ":memory:"
720
+ };
721
+ };
722
+ database = knexFactory__default.default({
723
+ ...knexConfig,
724
+ connection: Object.assign(connectionLoader, {
725
+ // This is a workaround for the knex SQLite driver always warning when using a config loader
726
+ filename: ":memory:"
727
+ })
728
+ });
729
+ deps.lifecycle.addShutdownHook(async () => {
730
+ const connection = await database.client.acquireConnection();
731
+ const data = connection.serialize();
732
+ await devStore.save(dataKey, data);
733
+ });
734
+ } else {
735
+ database = knexFactory__default.default(knexConfig);
736
+ }
737
+ } else {
738
+ database = knexFactory__default.default(knexConfig);
739
+ }
740
+ database.client.pool.on("createSuccess", (_eventId, resource) => {
741
+ resource.run("PRAGMA foreign_keys = ON", () => {
742
+ });
743
+ });
744
+ return database;
745
+ }
746
+ function buildSqliteDatabaseConfig(dbConfig, overrides) {
747
+ const baseConfig = dbConfig.get();
748
+ if (typeof baseConfig.connection === "string") {
749
+ baseConfig.connection = { filename: baseConfig.connection };
750
+ }
751
+ if (overrides && typeof overrides.connection === "string") {
752
+ overrides.connection = { filename: overrides.connection };
753
+ }
754
+ const config = mergeDatabaseConfig(
755
+ {
756
+ connection: {}
757
+ },
758
+ baseConfig,
759
+ {
760
+ useNullAsDefault: true
761
+ },
762
+ overrides
763
+ );
764
+ return config;
765
+ }
766
+ function createSqliteNameOverride(name) {
767
+ return {
768
+ connection: parseSqliteConnectionString(name)
769
+ };
770
+ }
771
+ function parseSqliteConnectionString(name) {
772
+ return {
773
+ filename: name
774
+ };
775
+ }
776
+ const sqliteConnector = Object.freeze({
777
+ createClient: createSqliteDatabaseClient,
778
+ createNameOverride: createSqliteNameOverride,
779
+ parseConnectionString: parseSqliteConnectionString
780
+ });
781
+ function pluginPath$1(pluginId) {
782
+ return `plugin.${pluginId}`;
783
+ }
784
+ function normalizeConnection(connection, client) {
785
+ if (typeof connection === "undefined" || connection === null) {
786
+ return {};
787
+ }
788
+ return typeof connection === "string" || connection instanceof String ? sqliteConnector.parseConnectionString(connection, client) : connection;
789
+ }
790
+ function createNameOverride(client, name) {
791
+ try {
792
+ return sqliteConnector.createNameOverride(name);
793
+ } catch (e) {
794
+ throw new errors.InputError(
795
+ `Unable to create database name override for '${client}' connector`,
796
+ e
797
+ );
798
+ }
799
+ }
800
+ class Sqlite3Connector {
801
+ constructor(config) {
802
+ this.config = config;
803
+ }
804
+ async getClient(pluginId, deps) {
805
+ const pluginConfig = new config.ConfigReader(
806
+ this.getConfigForPlugin(pluginId)
807
+ );
808
+ const pluginDivisionMode = this.getPluginDivisionModeConfig();
809
+ if (pluginDivisionMode !== "database") {
810
+ throw new Error(
811
+ `The SQLite driver does not suppoert plugin division mode '${pluginDivisionMode}'`
812
+ );
813
+ }
814
+ const databaseClientOverrides = mergeDatabaseConfig(
815
+ {},
816
+ this.getDatabaseOverrides(pluginId)
817
+ );
818
+ const client = sqliteConnector.createClient(
819
+ pluginConfig,
820
+ databaseClientOverrides,
821
+ deps
822
+ );
823
+ return client;
824
+ }
825
+ async dropDatabase(..._databaseNames) {
826
+ }
827
+ /**
828
+ * Provides the canonical database name for a given plugin.
829
+ *
830
+ * This method provides the effective database name which is determined using global
831
+ * and plugin specific database config. If no explicit database name is configured
832
+ * and `pluginDivisionMode` is not `schema`, this method will provide a generated name
833
+ * which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
834
+ * `schema`, it will fallback to using the default database for the knex instance.
835
+ *
836
+ * @param pluginId - Lookup the database name for given plugin
837
+ * @returns String representing the plugin's database name
838
+ */
839
+ getDatabaseName(pluginId) {
840
+ const connection = this.getConnectionConfig(pluginId);
841
+ const sqliteFilename = connection.filename;
842
+ if (sqliteFilename === ":memory:") {
843
+ return sqliteFilename;
844
+ }
845
+ const sqliteDirectory = connection.directory ?? ".";
846
+ return path__default.default.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`);
847
+ }
848
+ /**
849
+ * Provides the client type which should be used for a given plugin.
850
+ *
851
+ * The client type is determined by plugin specific config if present.
852
+ * Otherwise the base client is used as the fallback.
853
+ *
854
+ * @param pluginId - Plugin to get the client type for
855
+ * @returns Object with client type returned as `client` and boolean
856
+ * representing whether or not the client was overridden as
857
+ * `overridden`
858
+ */
859
+ getClientType(pluginId) {
860
+ const pluginClient = this.config.getOptionalString(
861
+ `${pluginPath$1(pluginId)}.client`
862
+ );
863
+ const baseClient = this.config.getString("client");
864
+ const client = pluginClient ?? baseClient;
865
+ return {
866
+ client,
867
+ overridden: client !== baseClient
868
+ };
869
+ }
870
+ getRoleConfig(pluginId) {
871
+ return this.config.getOptionalString(`${pluginPath$1(pluginId)}.role`) ?? this.config.getOptionalString("role");
872
+ }
873
+ /**
874
+ * Provides the knexConfig which should be used for a given plugin.
875
+ *
876
+ * @param pluginId - Plugin to get the knexConfig for
877
+ * @returns The merged knexConfig value or undefined if it isn't specified
878
+ */
879
+ getAdditionalKnexConfig(pluginId) {
880
+ const pluginConfig = this.config.getOptionalConfig(`${pluginPath$1(pluginId)}.knexConfig`)?.get();
881
+ const baseConfig = this.config.getOptionalConfig("knexConfig")?.get();
882
+ return lodash.merge(baseConfig, pluginConfig);
883
+ }
884
+ getPluginDivisionModeConfig() {
885
+ return this.config.getOptionalString("pluginDivisionMode") ?? "database";
886
+ }
887
+ /**
888
+ * Provides a Knex connection plugin config by combining base and plugin
889
+ * config.
890
+ *
891
+ * This method provides a baseConfig for a plugin database connector. If the
892
+ * client type has not been overridden, the global connection config will be
893
+ * included with plugin specific config as the base. Values from the plugin
894
+ * connection take precedence over the base. Base database name is omitted for
895
+ * all supported databases excluding SQLite unless `pluginDivisionMode` is set
896
+ * to `schema`.
897
+ */
898
+ getConnectionConfig(pluginId) {
899
+ const { client, overridden } = this.getClientType(pluginId);
900
+ let baseConnection = normalizeConnection(
901
+ this.config.get("connection"),
902
+ this.config.getString("client")
903
+ );
904
+ if (client.includes("sqlite3") && "filename" in baseConnection && baseConnection.filename !== ":memory:") {
905
+ throw new Error(
906
+ "`connection.filename` is not supported for the base sqlite connection. Prefer `connection.directory` or provide a filename for the plugin connection instead."
907
+ );
908
+ }
909
+ if (this.getPluginDivisionModeConfig() !== "schema") {
910
+ baseConnection = lodash.omit(baseConnection, "database");
911
+ }
912
+ const connection = normalizeConnection(
913
+ this.config.getOptional(`${pluginPath$1(pluginId)}.connection`),
914
+ client
915
+ );
916
+ return {
917
+ // include base connection if client type has not been overridden
918
+ ...overridden ? {} : baseConnection,
919
+ ...connection
920
+ };
921
+ }
922
+ /**
923
+ * Provides a Knex database config for a given plugin.
924
+ *
925
+ * This method provides a Knex configuration object along with the plugin's
926
+ * client type.
927
+ *
928
+ * @param pluginId - The plugin that the database config should correspond with
929
+ */
930
+ getConfigForPlugin(pluginId) {
931
+ const { client } = this.getClientType(pluginId);
932
+ const role = this.getRoleConfig(pluginId);
933
+ return {
934
+ ...this.getAdditionalKnexConfig(pluginId),
935
+ client,
936
+ connection: this.getConnectionConfig(pluginId),
937
+ ...role && { role }
938
+ };
939
+ }
940
+ /**
941
+ * Provides a partial `Knex.Config`• database name override for a given plugin.
942
+ *
943
+ * @param pluginId - Target plugin to get database name override
944
+ * @returns Partial `Knex.Config` with database name override
945
+ */
946
+ getDatabaseOverrides(pluginId) {
947
+ const databaseName = this.getDatabaseName(pluginId);
948
+ return databaseName ? createNameOverride(this.getClientType(pluginId).client, databaseName) : {};
949
+ }
950
+ }
951
+
952
+ function pluginPath(pluginId) {
953
+ return `plugin.${pluginId}`;
954
+ }
955
+ class DatabaseManagerImpl {
956
+ constructor(config, connectors, options, databaseCache = /* @__PURE__ */ new Map()) {
957
+ this.config = config;
958
+ this.connectors = connectors;
959
+ this.options = options;
960
+ this.databaseCache = databaseCache;
961
+ }
962
+ /**
963
+ * Generates a PluginDatabaseManager for consumption by plugins.
964
+ *
965
+ * @param pluginId - The plugin that the database manager should be created for. Plugin names
966
+ * should be unique as they are used to look up database config overrides under
967
+ * `backend.database.plugin`.
968
+ */
969
+ forPlugin(pluginId, deps) {
970
+ const client = this.getClientType(pluginId).client;
971
+ const connector = this.connectors[client];
972
+ if (!connector) {
973
+ throw new Error(
974
+ `Unsupported database client type '${client}' specified for plugin '${pluginId}'`
975
+ );
976
+ }
977
+ const getClient = () => this.getDatabase(pluginId, connector, deps);
978
+ const migrations = { skip: false, ...this.options?.migrations };
979
+ return { getClient, migrations };
980
+ }
981
+ /**
982
+ * Provides the client type which should be used for a given plugin.
983
+ *
984
+ * The client type is determined by plugin specific config if present.
985
+ * Otherwise the base client is used as the fallback.
986
+ *
987
+ * @param pluginId - Plugin to get the client type for
988
+ * @returns Object with client type returned as `client` and boolean
989
+ * representing whether or not the client was overridden as
990
+ * `overridden`
991
+ */
992
+ getClientType(pluginId) {
993
+ const pluginClient = this.config.getOptionalString(
994
+ `${pluginPath(pluginId)}.client`
995
+ );
996
+ const baseClient = this.config.getString("client");
997
+ const client = pluginClient ?? baseClient;
998
+ return {
999
+ client,
1000
+ overridden: client !== baseClient
1001
+ };
1002
+ }
1003
+ /**
1004
+ * Provides a scoped Knex client for a plugin as per application config.
1005
+ *
1006
+ * @param pluginId - Plugin to get a Knex client for
1007
+ * @returns Promise which resolves to a scoped Knex database client for a
1008
+ * plugin
1009
+ */
1010
+ async getDatabase(pluginId, connector, deps) {
1011
+ if (this.databaseCache.has(pluginId)) {
1012
+ return this.databaseCache.get(pluginId);
1013
+ }
1014
+ const clientPromise = connector.getClient(pluginId, deps);
1015
+ this.databaseCache.set(pluginId, clientPromise);
1016
+ if (process.env.NODE_ENV !== "test") {
1017
+ clientPromise.then((client) => this.startKeepaliveLoop(pluginId, client));
1018
+ }
1019
+ return clientPromise;
1020
+ }
1021
+ startKeepaliveLoop(pluginId, client) {
1022
+ let lastKeepaliveFailed = false;
1023
+ setInterval(() => {
1024
+ client?.raw("select 1").then(
1025
+ () => {
1026
+ lastKeepaliveFailed = false;
1027
+ },
1028
+ (error) => {
1029
+ if (!lastKeepaliveFailed) {
1030
+ lastKeepaliveFailed = true;
1031
+ this.options?.logger?.warn(
1032
+ `Database keepalive failed for plugin ${pluginId}, ${errors.stringifyError(
1033
+ error
1034
+ )}`
1035
+ );
1036
+ }
1037
+ }
1038
+ );
1039
+ }, 60 * 1e3);
1040
+ }
1041
+ }
1042
+ class DatabaseManager {
1043
+ constructor(impl) {
1044
+ this.impl = impl;
1045
+ }
1046
+ /**
1047
+ * Creates a {@link DatabaseManager} from `backend.database` config.
1048
+ *
1049
+ * @param config - The loaded application configuration.
1050
+ * @param options - An optional configuration object.
1051
+ */
1052
+ static fromConfig(config, options) {
1053
+ const databaseConfig = config.getConfig("backend.database");
1054
+ const prefix = databaseConfig.getOptionalString("prefix") || "backstage_plugin_";
1055
+ return new DatabaseManager(
1056
+ new DatabaseManagerImpl(
1057
+ databaseConfig,
1058
+ {
1059
+ pg: new PgConnector(databaseConfig, prefix),
1060
+ sqlite3: new Sqlite3Connector(databaseConfig),
1061
+ "better-sqlite3": new Sqlite3Connector(databaseConfig),
1062
+ mysql: new MysqlConnector(databaseConfig, prefix),
1063
+ mysql2: new MysqlConnector(databaseConfig, prefix)
1064
+ },
1065
+ options
1066
+ )
1067
+ );
1068
+ }
1069
+ /**
1070
+ * Generates a PluginDatabaseManager for consumption by plugins.
1071
+ *
1072
+ * @param pluginId - The plugin that the database manager should be created for. Plugin names
1073
+ * should be unique as they are used to look up database config overrides under
1074
+ * `backend.database.plugin`.
1075
+ */
1076
+ forPlugin(pluginId, deps) {
1077
+ return this.impl.forPlugin(pluginId, deps);
1078
+ }
1079
+ }
1080
+ async function dropDatabase(dbConfig, ...databaseNames) {
1081
+ const client = dbConfig.getString("client");
1082
+ const prefix = dbConfig.getOptionalString("prefix") || "backstage_plugin_";
1083
+ if (client === "pg") {
1084
+ await new PgConnector(dbConfig, prefix).dropDatabase(...databaseNames);
1085
+ } else if (client === "mysql" || client === "mysql2") {
1086
+ await new MysqlConnector(dbConfig, prefix).dropDatabase(...databaseNames);
1087
+ }
1088
+ }
1089
+
1090
+ exports.DatabaseManager = DatabaseManager;
31
1091
  exports.databaseServiceFactory = databaseServiceFactory;
1092
+ exports.dropDatabase = dropDatabase;
32
1093
  //# sourceMappingURL=database.cjs.js.map