@nocobase/app-plugin-releases 0.1.0-beta.13 → 0.1.0-beta.15

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +19 -13
  3. package/dist/client/components/environment-form.d.ts +1 -1
  4. package/dist/client/components/environment-form.d.ts.map +1 -1
  5. package/dist/client/components/environment-form.js +8 -1
  6. package/dist/client/components/environment-form.js.map +1 -1
  7. package/dist/client/components/variable-alerts.d.ts.map +1 -1
  8. package/dist/client/components/variable-alerts.js +2 -11
  9. package/dist/client/components/variable-alerts.js.map +1 -1
  10. package/dist/client/locales/de-DE.d.ts.map +1 -1
  11. package/dist/client/locales/de-DE.js +4 -1
  12. package/dist/client/locales/de-DE.js.map +1 -1
  13. package/dist/client/locales/en-US.d.ts +7 -1
  14. package/dist/client/locales/en-US.d.ts.map +1 -1
  15. package/dist/client/locales/en-US.js +4 -1
  16. package/dist/client/locales/en-US.js.map +1 -1
  17. package/dist/client/locales/es-ES.d.ts.map +1 -1
  18. package/dist/client/locales/es-ES.js +4 -1
  19. package/dist/client/locales/es-ES.js.map +1 -1
  20. package/dist/client/locales/fr-FR.d.ts.map +1 -1
  21. package/dist/client/locales/fr-FR.js +4 -1
  22. package/dist/client/locales/fr-FR.js.map +1 -1
  23. package/dist/client/locales/id-ID.d.ts.map +1 -1
  24. package/dist/client/locales/id-ID.js +4 -1
  25. package/dist/client/locales/id-ID.js.map +1 -1
  26. package/dist/client/locales/ja-JP.d.ts.map +1 -1
  27. package/dist/client/locales/ja-JP.js +4 -1
  28. package/dist/client/locales/ja-JP.js.map +1 -1
  29. package/dist/client/locales/pt-BR.d.ts.map +1 -1
  30. package/dist/client/locales/pt-BR.js +4 -1
  31. package/dist/client/locales/pt-BR.js.map +1 -1
  32. package/dist/client/locales/vi-VN.d.ts.map +1 -1
  33. package/dist/client/locales/vi-VN.js +4 -1
  34. package/dist/client/locales/vi-VN.js.map +1 -1
  35. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  36. package/dist/client/locales/zh-CN.js +4 -1
  37. package/dist/client/locales/zh-CN.js.map +1 -1
  38. package/dist/client/locales/zh-TW.d.ts.map +1 -1
  39. package/dist/client/locales/zh-TW.js +4 -1
  40. package/dist/client/locales/zh-TW.js.map +1 -1
  41. package/dist/client/pages/environment-page.js +1 -1
  42. package/dist/client/pages/environment-page.js.map +1 -1
  43. package/dist/client/pages/environments-page.d.ts +8 -3
  44. package/dist/client/pages/environments-page.d.ts.map +1 -1
  45. package/dist/client/pages/environments-page.js +16 -4
  46. package/dist/client/pages/environments-page.js.map +1 -1
  47. package/dist/client/pages.d.ts +4 -1
  48. package/dist/client/pages.d.ts.map +1 -1
  49. package/dist/client/pages.js +1 -0
  50. package/dist/client/pages.js.map +1 -1
  51. package/dist/database/migrations/.manifest.json +4 -0
  52. package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts +8 -0
  53. package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts.map +1 -0
  54. package/dist/database/migrations/202611020001_rel_environment_purpose.js +30 -0
  55. package/dist/database/migrations/202611020001_rel_environment_purpose.js.map +1 -0
  56. package/dist/server/routes/api.d.ts.map +1 -1
  57. package/dist/server/routes/api.js +55 -6
  58. package/dist/server/routes/api.js.map +1 -1
  59. package/dist/server/routes/schemas.d.ts +3 -1
  60. package/dist/server/routes/schemas.d.ts.map +1 -1
  61. package/dist/server/routes/schemas.js +21 -3
  62. package/dist/server/routes/schemas.js.map +1 -1
  63. package/dist/server/services/environments.d.ts +2 -2
  64. package/dist/server/services/environments.d.ts.map +1 -1
  65. package/dist/server/services/environments.js +21 -1
  66. package/dist/server/services/environments.js.map +1 -1
  67. package/dist/server/services/releases.d.ts +30 -6
  68. package/dist/server/services/releases.d.ts.map +1 -1
  69. package/dist/server/services/releases.js +77 -51
  70. package/dist/server/services/releases.js.map +1 -1
  71. package/dist/shared/releases.d.ts +15 -1
  72. package/dist/shared/releases.d.ts.map +1 -1
  73. package/dist/shared/releases.js +5 -0
  74. package/dist/shared/releases.js.map +1 -1
  75. package/package.json +6 -6
@@ -26,6 +26,11 @@ import { decryptText, encryptText } from './secrets.js';
26
26
  import { DEFAULT_INITIAL_ADMIN, INITIAL_ADMIN_PATHS, PUBLIC_ORIGIN_PATH, SAMPLE_DATA_PATH, appVariableView, assertVariableName, assertVariableValue, configValueAt, decodeVariablesManifest, generateVariable, hasRealValue, isSecretName, parseVariablesManifest, randomPassword, resolveVariables, } from './variables.js';
27
27
  import { APP_ID_PATTERN, assertAppId, assertPagination, normalizeLabels, normalizeName, normalizeRuntimePolicy, releaseArtifactKey, } from './validation.js';
28
28
  const DEFAULT_STARTUP_WAIT_MS = 5_000;
29
+ /** `<appId>-<version>.tar.gz`, with anything but letters, digits, `.`, `_` and `-` replaced. */
30
+ export function archiveFileName(appId, version) {
31
+ const safe = (text) => text.replace(/[^A-Za-z0-9._-]+/gu, '_').replace(/^\.+/u, '') || 'release';
32
+ return `${safe(appId)}-${safe(version)}.tar.gz`;
33
+ }
29
34
  export class ReleasesService {
30
35
  options;
31
36
  diagnostic;
@@ -118,7 +123,7 @@ export class ReleasesService {
118
123
  : {}),
119
124
  };
120
125
  }
121
- async createApp(caller, input) {
126
+ async createApp(caller, input, options = {}) {
122
127
  this.options.guard.requireCreate(caller);
123
128
  if (!input || typeof input.id !== 'string')
124
129
  throw new ReleasesError('An App ID is required.', 'INVALID_APP_ID', 'INVALID_ARGUMENT');
@@ -147,28 +152,32 @@ export class ReleasesService {
147
152
  if (await this.findApp(id))
148
153
  throw new ReleasesError('Application ID is unavailable. Choose a different ID.', 'APP_EXISTS', 'ALREADY_EXISTS');
149
154
  await this.assertEnvironmentCapacity(environment);
150
- await this.query()
151
- .insertInto('relApps')
152
- .values({
153
- id,
154
- environmentId: environment.id,
155
- name,
156
- description: typeof input.description === 'string' && input.description.trim()
157
- ? input.description.trim()
158
- : null,
159
- currentDeploymentId: null,
160
- enabled: false,
161
- activation: policy.activation,
162
- idleStopMinutes: policy.idleStopMinutes,
163
- dormantAfterHours: policy.dormantAfterHours,
164
- labels: JSON.stringify(labels),
165
- previewOf,
166
- createdBy: caller.userId,
167
- createdVia: caller.kind,
168
- createdAt: now,
169
- updatedAt: now,
170
- })
171
- .execute();
155
+ await this.options.database.transaction(async (connection) => {
156
+ await connection.query
157
+ .insertInto('relApps')
158
+ .values({
159
+ id,
160
+ environmentId: environment.id,
161
+ name,
162
+ description: typeof input.description === 'string' &&
163
+ input.description.trim()
164
+ ? input.description.trim()
165
+ : null,
166
+ currentDeploymentId: null,
167
+ enabled: false,
168
+ activation: policy.activation,
169
+ idleStopMinutes: policy.idleStopMinutes,
170
+ dormantAfterHours: policy.dormantAfterHours,
171
+ labels: JSON.stringify(labels),
172
+ previewOf,
173
+ createdBy: caller.userId,
174
+ createdVia: caller.kind,
175
+ createdAt: now,
176
+ updatedAt: now,
177
+ })
178
+ .execute();
179
+ await options.initialize?.(connection, id);
180
+ });
172
181
  });
173
182
  }
174
183
  catch (error) {
@@ -271,6 +280,48 @@ export class ReleasesService {
271
280
  const images = await this.artifacts.imagesOf([release.id]);
272
281
  return releaseView(release, images.get(release.id) ?? []);
273
282
  }
283
+ /**
284
+ * Several Apps' summaries in one batched read (one runtime status call per environment), for the assembling
285
+ * application: no caller is checked. Unknown ids are left out; the rest keep the order asked.
286
+ */
287
+ async summarizeApps(appIds) {
288
+ const ids = [...new Set(appIds)];
289
+ if (ids.length === 0)
290
+ return [];
291
+ const rows = await this.query()
292
+ .selectFrom('relApps')
293
+ .selectAll()
294
+ .where('id', 'in', ids)
295
+ .execute();
296
+ const byId = new Map(rows.map(decodeApp).map((app) => [app.id, app]));
297
+ const apps = ids.flatMap((id) => {
298
+ const app = byId.get(id);
299
+ return app ? [app] : [];
300
+ });
301
+ return this.summarize(apps);
302
+ }
303
+ /**
304
+ * A release's archive, to download: `view` on the App. An image release has none (`RELEASE_HAS_NO_ARCHIVE`), and an
305
+ * archive missing from the disk is `RELEASE_ARCHIVE_NOT_FOUND`. The stream is opened last, so a refusal never touches
306
+ * the disk.
307
+ */
308
+ async openArchive(caller, appId, releaseId) {
309
+ const app = await this.requireApp(appId);
310
+ await this.options.guard.requireApp(caller, 'view', app);
311
+ const release = await this.requireRelease(appId, releaseId);
312
+ const key = release.artifactKey;
313
+ if (!key)
314
+ throw new ReleasesError('This release is a set of images and has no archive to download.', 'RELEASE_HAS_NO_ARCHIVE', 'FAILED_PRECONDITION');
315
+ // A local disk opens its file lazily, so a missing archive would fail only after the response headers went out.
316
+ if (!(await this.disk.exists(key)))
317
+ throw notFound('Archive', 'RELEASE_ARCHIVE_NOT_FOUND', releaseId);
318
+ return {
319
+ stream: await this.disk.getStream(key),
320
+ size: release.size,
321
+ checksum: release.checksum,
322
+ fileName: archiveFileName(app.id, release.version),
323
+ };
324
+ }
274
325
  /**
275
326
  * Registers an image CI built and pushed, by digest, as a release of `version` (an image release, which Docker
276
327
  * environments pull). It needs `upload` on the App, as uploading an archive does. The same digest again answers its
@@ -930,7 +981,6 @@ export class ReleasesService {
930
981
  * not report every App as stopped or a finished deployment as still running.
931
982
  */
932
983
  async restore() {
933
- await this.clearExpiredInitialAdmins().catch((error) => this.diagnostic.warn('Failed to clear expired first administrators', error));
934
984
  const interrupted = (await this.query()
935
985
  .selectFrom('relDeployments')
936
986
  .selectAll()
@@ -1201,15 +1251,14 @@ export class ReleasesService {
1201
1251
  }
1202
1252
  /**
1203
1253
  * The first administrator the App's first deployment generated, for a person who may deploy it: until someone
1204
- * says it is saved, and for 24 hours (a preview App's until the App goes).
1254
+ * says it is saved or the App is removed.
1205
1255
  */
1206
1256
  async readInitialAdmin(caller, appId) {
1207
1257
  const app = await this.requireApp(appId);
1208
1258
  await this.requireInitialAdminReader(caller, app);
1209
- await this.clearExpiredInitialAdmins();
1210
1259
  const found = await this.initialAdminOf(app);
1211
1260
  if (!found)
1212
- throw new ReleasesError('This App has no first administrator to show: it was saved, it expired, or the application did not generate one.', 'INITIAL_ADMIN_UNAVAILABLE', 'NOT_FOUND');
1261
+ throw new ReleasesError('This App has no first administrator to show: it was saved or the application did not generate one.', 'INITIAL_ADMIN_UNAVAILABLE', 'NOT_FOUND');
1213
1262
  return found;
1214
1263
  }
1215
1264
  /** Forgets the generated first administrator once someone has saved it. */
@@ -1227,16 +1276,6 @@ export class ReleasesService {
1227
1276
  .where('initialAdmin', 'is not', null)
1228
1277
  .execute();
1229
1278
  }
1230
- /** Clears the first administrators whose 24 hours are over. */
1231
- async clearExpiredInitialAdmins(now = new Date()) {
1232
- await this.query()
1233
- .updateTable('relDeployments')
1234
- .set({ initialAdmin: null })
1235
- .where('initialAdmin', 'is not', null)
1236
- .where('initialAdminExpiresAt', 'is not', null)
1237
- .where('initialAdminExpiresAt', '<=', now)
1238
- .execute();
1239
- }
1240
1279
  async requireInitialAdminReader(caller, app) {
1241
1280
  // A password a person copies: never an agent's or a key's, whatever they may deploy.
1242
1281
  if (caller.kind !== 'human' &&
@@ -1253,18 +1292,14 @@ export class ReleasesService {
1253
1292
  .where('initialAdmin', 'is not', null)
1254
1293
  .orderBy('createdAt', 'desc')
1255
1294
  .execute();
1256
- const now = new Date();
1257
1295
  for (const deployment of rows.map(decodeDeployment)) {
1258
- if (deployment.initialAdminExpiresAt &&
1259
- deployment.initialAdminExpiresAt <= now)
1260
- continue;
1261
1296
  const admin = this.openInitialAdmin(deployment);
1262
1297
  if (!admin)
1263
1298
  continue;
1264
1299
  return {
1265
1300
  deploymentId: deployment.id,
1266
1301
  ...admin,
1267
- expiresAt: iso(deployment.initialAdminExpiresAt),
1302
+ expiresAt: null,
1268
1303
  };
1269
1304
  }
1270
1305
  return null;
@@ -1434,7 +1469,6 @@ export class ReleasesService {
1434
1469
  for (const [name, variable] of plan.oneShot)
1435
1470
  generated[name] = generateVariable(variable.generate);
1436
1471
  let initialAdmin = null;
1437
- let initialAdminExpiresAt = null;
1438
1472
  if (plan.initialAdmin) {
1439
1473
  const password = (await this.earlierInitialAdmin(app))?.password ?? randomPassword();
1440
1474
  generated[plan.initialAdmin.name] = password;
@@ -1444,10 +1478,6 @@ export class ReleasesService {
1444
1478
  password,
1445
1479
  };
1446
1480
  initialAdmin = encryptText(JSON.stringify(admin), [deploymentId], this.options.secrets, 'initial-admin');
1447
- // A preview App's goes with the preview; any other's is deleted after a day.
1448
- initialAdminExpiresAt = app.previewOf
1449
- ? null
1450
- : new Date(Date.now() + INITIAL_ADMIN_TTL_MS);
1451
1481
  }
1452
1482
  const resolved = resolveVariables({
1453
1483
  manifest: release.variables,
@@ -1465,7 +1495,6 @@ export class ReleasesService {
1465
1495
  : encryptText(JSON.stringify(resolved.env), [deploymentId], this.options.secrets, 'deployment-env'),
1466
1496
  variablesFingerprint: release.variables || names.length > 0 ? resolved.fingerprint : null,
1467
1497
  initialAdmin,
1468
- initialAdminExpiresAt,
1469
1498
  };
1470
1499
  }
1471
1500
  /**
@@ -2333,8 +2362,6 @@ export class ReleasesService {
2333
2362
  return this.options.database.connection().query;
2334
2363
  }
2335
2364
  }
2336
- // --- Variables --------------------------------------------------------------------------------------------------
2337
- const INITIAL_ADMIN_TTL_MS = 24 * 60 * 60 * 1000;
2338
2365
  /** A variable's value from the App or the environment, when one of them sets it. */
2339
2366
  function suppliedValue(context, name) {
2340
2367
  return (context.appVariables.find((variable) => variable.name === name)?.value ??
@@ -2689,7 +2716,6 @@ function decodeDeployment(row) {
2689
2716
  env: nullableString(row.env),
2690
2717
  variablesFingerprint: nullableString(row.variablesFingerprint),
2691
2718
  initialAdmin: nullableString(row.initialAdmin),
2692
- initialAdminExpiresAt: decodeOptionalDate(row.initialAdminExpiresAt),
2693
2719
  initialAdminSavedBy: nullableString(row.initialAdminSavedBy),
2694
2720
  initialAdminSavedAt: decodeOptionalDate(row.initialAdminSavedAt),
2695
2721
  createdAt: decodeDate(row.createdAt),