@contember/engine-server 0.12.0-alpha.9 → 1.0.0-alpha.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.
Files changed (62) hide show
  1. package/alpine.dockerfile +2 -2
  2. package/debian.dockerfile +1 -1
  3. package/dist/src/MasterContainer.d.ts +4 -1
  4. package/dist/src/MasterContainer.d.ts.map +1 -1
  5. package/dist/src/MasterContainer.js +60 -31
  6. package/dist/src/MasterContainer.js.map +1 -1
  7. package/dist/src/bootstrap/Initializer.d.ts +4 -6
  8. package/dist/src/bootstrap/Initializer.d.ts.map +1 -1
  9. package/dist/src/bootstrap/Initializer.js +10 -11
  10. package/dist/src/bootstrap/Initializer.js.map +1 -1
  11. package/dist/src/config/config.d.ts +43 -22
  12. package/dist/src/config/config.d.ts.map +1 -1
  13. package/dist/src/config/config.js +135 -241
  14. package/dist/src/config/config.js.map +1 -1
  15. package/dist/src/http/ShowMetricsMiddleware.js +1 -1
  16. package/dist/src/http/ShowMetricsMiddleware.js.map +1 -1
  17. package/dist/src/http/index.d.ts +0 -1
  18. package/dist/src/http/index.d.ts.map +1 -1
  19. package/dist/src/http/index.js +0 -1
  20. package/dist/src/http/index.js.map +1 -1
  21. package/dist/src/project/ProjectContainer.d.ts +1 -1
  22. package/dist/src/project/ProjectContainer.d.ts.map +1 -1
  23. package/dist/src/project/ProjectContainerResolver.d.ts +13 -10
  24. package/dist/src/project/ProjectContainerResolver.d.ts.map +1 -1
  25. package/dist/src/project/ProjectContainerResolver.js +48 -33
  26. package/dist/src/project/ProjectContainerResolver.js.map +1 -1
  27. package/dist/src/project/ProjectGroupState.d.ts +18 -0
  28. package/dist/src/project/ProjectGroupState.d.ts.map +1 -0
  29. package/dist/src/project/ProjectGroupState.js +30 -0
  30. package/dist/src/project/ProjectGroupState.js.map +1 -0
  31. package/dist/src/project/ProjectInitializer.d.ts +4 -10
  32. package/dist/src/project/ProjectInitializer.d.ts.map +1 -1
  33. package/dist/src/project/ProjectInitializer.js +5 -19
  34. package/dist/src/project/ProjectInitializer.js.map +1 -1
  35. package/dist/src/project/ProjectSchemaResolver.d.ts +5 -5
  36. package/dist/src/project/ProjectSchemaResolver.d.ts.map +1 -1
  37. package/dist/src/project/ProjectSchemaResolver.js +4 -4
  38. package/dist/src/project/ProjectSchemaResolver.js.map +1 -1
  39. package/dist/src/start.js +22 -14
  40. package/dist/src/start.js.map +1 -1
  41. package/dist/src/tsconfig.tsbuildinfo +1 -1
  42. package/dist/src/utils/cluster.js +1 -1
  43. package/dist/src/utils/cluster.js.map +1 -1
  44. package/dist/tests/cases/http.js +12 -11
  45. package/dist/tests/cases/http.js.map +1 -1
  46. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +21 -21
  48. package/src/MasterContainer.ts +106 -55
  49. package/src/bootstrap/Initializer.ts +10 -17
  50. package/src/config/config.ts +159 -289
  51. package/src/http/index.ts +0 -1
  52. package/src/project/ProjectContainerResolver.ts +58 -35
  53. package/src/project/ProjectGroupState.ts +33 -0
  54. package/src/project/ProjectInitializer.ts +9 -23
  55. package/src/project/ProjectSchemaResolver.ts +12 -7
  56. package/src/start.ts +13 -6
  57. package/tests/cases/http.ts +7 -2
  58. package/dist/src/http/RootMiddleware.d.ts +0 -9
  59. package/dist/src/http/RootMiddleware.d.ts.map +0 -1
  60. package/dist/src/http/RootMiddleware.js +0 -34
  61. package/dist/src/http/RootMiddleware.js.map +0 -1
  62. package/src/http/RootMiddleware.ts +0 -54
@@ -5,244 +5,167 @@ import {
5
5
  resolveParameters,
6
6
  UndefinedParameterError,
7
7
  } from '@contember/config-loader'
8
- import { DatabaseCredentials } from '@contember/database'
9
- import { tuple, upperCaseFirst } from '../utils'
10
- import { MailerOptions, TenantCredentials } from '@contember/engine-tenant-api'
8
+ import { upperCaseFirst } from '../utils'
11
9
  import { ConfigProcessor } from '@contember/engine-plugins'
12
- import {
13
- isObject,
14
- typeConfigError,
15
- hasStringProperty,
16
- hasNumberProperty,
17
- hasBooleanProperty,
18
- } from '@contember/engine-common'
19
- import { ProjectConfig, ProjectConfigResolver, StageConfig } from '@contember/engine-http'
10
+ import { Typesafe } from '@contember/engine-common'
11
+ import { ProjectConfigResolver } from '@contember/engine-http'
12
+ import { MailerOptions } from '@contember/engine-tenant-api'
20
13
 
21
- export interface TenantConfig {
22
- db: DatabaseCredentials
23
- mailer: MailerOptions
24
- credentials: TenantCredentials
25
- secrets?: {
26
- encryptionKey: string
27
- }
28
- }
14
+ export type Config = ReturnType<typeof configSchema>
15
+ export type TenantConfig = Config['tenant']
29
16
 
30
- export interface Config {
31
- tenant: TenantConfig
32
- server: {
33
- port: number
34
- monitoringPort: number
35
- workerCount?: number | string
36
- http: {
37
- requestBodySize?: string
38
- }
39
- logging: {
40
- sentry?: {
41
- dsn: string
42
- }
43
- }
44
- }
45
- }
17
+ const dbCredentialsSchema = Typesafe.intersection(
18
+ Typesafe.object({
19
+ host: Typesafe.string,
20
+ port: Typesafe.number,
21
+ user: Typesafe.string,
22
+ password: Typesafe.string,
23
+ database: Typesafe.string,
24
+ }),
25
+ Typesafe.partial({
26
+ ssl: Typesafe.boolean,
27
+ }),
28
+ )
46
29
 
47
- function checkDatabaseCredentials(json: unknown, path: string): DatabaseCredentials {
48
- if (!isObject(json)) {
49
- return typeConfigError(path, json, 'object')
50
- }
51
- if (!hasStringProperty(json, 'host')) {
52
- return typeConfigError(path + '.host', json.host, 'string')
53
- }
54
- if (!hasNumberProperty(json, 'port')) {
55
- return typeConfigError(path + '.port', json.port, 'number')
56
- }
57
- if (!hasStringProperty(json, 'user')) {
58
- return typeConfigError(path + '.user', json.user, 'string')
59
- }
60
- if (!hasStringProperty(json, 'password')) {
61
- return typeConfigError(path + '.password', json.password, 'string')
62
- }
63
- if (!hasStringProperty(json, 'database')) {
64
- return typeConfigError(path + '.database', json.database, 'string')
65
- }
66
- if (json.ssl !== undefined && !hasBooleanProperty(json, 'ssl')) {
67
- return typeConfigError(path + '.ssl', json.ssl, 'boolean')
68
- }
69
- return json
70
- }
71
-
72
- function checkMailerParameters(json: unknown, path: string): MailerOptions {
73
- if (!isObject(json)) {
74
- return typeConfigError(path, json, 'object')
75
- }
76
- const values = Object.fromEntries(Object.entries(json).filter(([, it]) => it !== undefined))
77
- if ('from' in values && !hasStringProperty(values, 'from')) {
78
- return typeConfigError(path + '.from', values.from, 'string')
79
- }
80
- if ('host' in values && !hasStringProperty(values, 'host')) {
81
- return typeConfigError(path + '.host', values.host, 'string')
82
- }
83
- if ('port' in values && !hasNumberProperty(values, 'port')) {
84
- return typeConfigError(path + '.port', values.port, 'number')
85
- }
86
- if ('user' in values && !hasStringProperty(json, 'user')) {
87
- return typeConfigError(path + '.user', json.user, 'string')
88
- }
89
- if ('password' in values && !hasStringProperty(json, 'password')) {
90
- return typeConfigError(path + '.password', json.password, 'string')
91
- }
92
- return {
93
- ...values,
94
- ...('user' in values && 'password' in values
95
- ? { auth: { user: String(values.user), pass: String(values.password) } }
96
- : {}),
97
- }
98
- }
99
-
100
- function checkTenantCredentials(json: unknown, path: string): TenantCredentials {
101
- if (!isObject(json)) {
102
- return typeConfigError(path, json, 'object')
103
- }
104
- const values = Object.fromEntries(Object.entries(json).filter(([, it]) => it !== undefined))
105
- if ('rootToken' in values && !hasStringProperty(values, 'rootToken')) {
106
- return typeConfigError(path + '.rootToken', values.rootToken, 'string')
107
- }
108
- if ('rootPassword' in values && !hasStringProperty(values, 'rootPassword')) {
109
- return typeConfigError(path + '.rootPassword', values.rootPassword, 'string')
110
- }
111
- if ('rootEmail' in values && !hasStringProperty(values, 'rootEmail')) {
112
- return typeConfigError(path + '.rootEmail', values.rootEmail, 'string')
113
- }
114
- if ('loginToken' in values && !hasStringProperty(values, 'loginToken')) {
115
- return typeConfigError(path + '.loginToken', values.loginToken, 'string')
116
- }
117
-
118
- return values
119
- }
120
-
121
- function checkTenantSecrets(json: unknown, path: string): TenantConfig['secrets'] {
122
- if (!isObject(json)) {
123
- return typeConfigError(path, json, 'object')
124
- }
125
- if (json.encryptionKey === undefined) {
126
- return undefined
127
- }
128
- if (!hasStringProperty(json, 'encryptionKey')) {
129
- return typeConfigError(path + '.encryptionKey', json.encryptionKey, 'string')
130
- }
131
- return {
132
- encryptionKey: json.encryptionKey,
133
- }
134
- }
135
- function checkTenantStructure(json: unknown): Config['tenant'] {
136
- if (!isObject(json)) {
137
- return typeConfigError('tenant', json, 'object')
138
- }
139
- return {
140
- db: checkDatabaseCredentials(json.db, 'tenant.db'),
141
- mailer: checkMailerParameters(json.mailer, 'tenant.mailer'),
142
- credentials: checkTenantCredentials(json.credentials, 'tenant.credentials'),
143
- secrets: checkTenantSecrets(json.secrets, 'tenant.secrets'),
144
- }
145
- }
146
-
147
- function checkStageStructure(json: unknown, slug: string, path: string): StageConfig {
148
- json = json || {}
149
- if (!isObject(json)) {
150
- return typeConfigError(path, json, 'object')
151
- }
152
-
153
- if (json.name && !hasStringProperty(json, 'name')) {
154
- return typeConfigError(path + '.name', json.name, 'string')
155
- }
156
- return { name: upperCaseFirst(slug), ...json, slug }
157
- }
158
-
159
- function checkProjectStructure(json: unknown, slug: string, path: string): ProjectConfig {
160
- if (!isObject(json)) {
161
- return typeConfigError(path, json, 'object')
162
- }
163
-
164
- if (json.name && !hasStringProperty(json, 'name')) {
165
- return typeConfigError(path + '.name', json.name, 'string')
166
- }
167
- if (!isObject(json.stages)) {
168
- return typeConfigError(`${path}.stages`, json.stages, 'object')
169
- }
170
- const stages = Object.entries(json.stages).map(([slug, value]) =>
171
- checkStageStructure(value, slug, `${path}.stages.${slug}`),
172
- )
173
- return {
174
- name: upperCaseFirst(slug).replace(/-/g, ' '),
175
- directory: `${slug}/api`,
176
- ...json,
177
- slug,
178
- stages: stages,
179
- db: checkDatabaseCredentials(json.db, `${path}.db`),
180
- }
181
- }
182
-
183
- function checkServerStructure(json: unknown): Config['server'] {
184
- if (!isObject(json)) {
185
- return typeConfigError('server', json, 'object')
186
- }
187
- if (!hasNumberProperty(json, 'port')) {
188
- return typeConfigError('server.port', json.port, 'number')
189
- }
190
- if (!hasNumberProperty(json, 'monitoringPort')) {
191
- return typeConfigError('server.monitoringPort', json.monitoringPort, 'number')
192
- }
193
- return {
194
- ...json,
195
- logging: checkLoggingStructure(json.logging),
196
- http: checkHttpStructure(json.http),
197
- }
198
- }
199
-
200
- function checkLoggingStructure(json: unknown): Config['server']['logging'] {
201
- if (!json) {
202
- return {}
203
- }
204
- if (!isObject(json)) {
205
- return typeConfigError('logging', json, 'object')
206
- }
207
- let sentry: Config['server']['logging']['sentry'] = undefined
208
- if (json.sentry) {
209
- if (!isObject(json.sentry)) {
210
- return typeConfigError('logging.sentry', json.sentry, 'object')
211
- }
212
- if (json.sentry.dsn) {
213
- if (!hasStringProperty(json.sentry, 'dsn')) {
214
- return typeConfigError('logging.sentry.dsn', json.sentry.dsn, 'string')
30
+ const tenantConfigSchema = Typesafe.intersection(
31
+ Typesafe.object({
32
+ db: dbCredentialsSchema,
33
+ mailer: Typesafe.transform(Typesafe.partial({
34
+ from: Typesafe.string,
35
+ host: Typesafe.string,
36
+ port: Typesafe.number,
37
+ user: Typesafe.string,
38
+ password: Typesafe.string,
39
+ }), (value, input): MailerOptions => {
40
+ return {
41
+ ...(input as MailerOptions),
42
+ ...value,
43
+ ...(value.user ? { auth: { user: value.user, pass: value.password } } : {}),
215
44
  }
216
- sentry = { dsn: json.sentry.dsn }
217
- }
218
- }
45
+ }),
46
+ credentials: Typesafe.partial({
47
+ rootToken: Typesafe.string,
48
+ rootTokenHash: Typesafe.string,
49
+ rootPassword: Typesafe.string,
50
+ rootEmail: Typesafe.string,
51
+ loginToken: Typesafe.string,
52
+ }),
53
+ }),
54
+ Typesafe.object({
55
+ secrets: Typesafe.partial({
56
+ encryptionKey: Typesafe.string,
57
+ }),
58
+ }),
59
+ )
60
+ const serverConfigSchema = Typesafe.intersection(
61
+ Typesafe.object({
62
+ port: Typesafe.number,
63
+ monitoringPort: Typesafe.number,
64
+ http: Typesafe.partial({
65
+ requestBodySize: Typesafe.string,
66
+ }),
67
+ logging: Typesafe.union(
68
+ val => Typesafe.valueAt(val, ['sentry', 'dsn']) === undefined ? {} : Typesafe.fail([]),
69
+ Typesafe.partial({
70
+ sentry: Typesafe.object({
71
+ dsn: Typesafe.string,
72
+ }),
73
+ }),
74
+ ),
75
+ }),
76
+ Typesafe.partial({
77
+ workerCount: Typesafe.union(Typesafe.number, Typesafe.string),
78
+ projectGroup: (val, path) => Typesafe.valueAt(val, ['domainMapping']) === undefined ? undefined : Typesafe.object({
79
+ domainMapping: Typesafe.string,
80
+ })(val, path),
81
+ }),
82
+ )
83
+ const configSchema = Typesafe.object({
84
+ tenant: tenantConfigSchema,
85
+ server: serverConfigSchema,
86
+ })
219
87
 
220
- return { sentry }
221
- }
88
+ const stageConfig = Typesafe.map(Typesafe.object({
89
+ name: Typesafe.union(
90
+ Typesafe.string,
91
+ (_, path = []) => upperCaseFirst(String(path[path.length - 2])),
92
+ ),
93
+ slug: (_, path = []) => String(path[path.length - 2]),
94
+ }))
222
95
 
223
- function checkHttpStructure(json: unknown): Config['server']['http'] {
224
- if (!json) {
225
- return {}
226
- }
227
- if (!isObject(json)) {
228
- return typeConfigError('http', json, 'object')
229
- }
230
- return json
231
- }
96
+ const projectConfigSchema = Typesafe.object({
97
+ name: Typesafe.union(
98
+ Typesafe.string,
99
+ (_, path = []) => upperCaseFirst(String(path[path.length - 2])).replace(/-/g, ' '),
100
+ ),
101
+ slug: (_, path = []) => String(path[path.length - 2]),
102
+ stages: (input, path = []) => Object.values(stageConfig(input, path)),
103
+ db: dbCredentialsSchema,
104
+ })
232
105
 
233
- function checkConfigStructure(json: unknown): Config {
234
- if (!isObject(json)) {
235
- return typeConfigError('', json, 'object')
236
- }
237
- return {
238
- ...json,
239
- tenant: checkTenantStructure(json.tenant),
240
- server: checkServerStructure(json.server),
241
- }
106
+ const defaultTemplate: any = {
107
+ tenant: {
108
+ db: {
109
+ host: `%tenant.env.DB_HOST%`,
110
+ port: `%tenant.env.DB_PORT::number%`,
111
+ user: `%tenant.env.DB_USER%`,
112
+ password: `%tenant.env.DB_PASSWORD%`,
113
+ database: `%tenant.env.DB_NAME%`,
114
+ ssl: `%?tenant.env.DB_SSL::bool%`,
115
+ },
116
+ mailer: {
117
+ from: '%?tenant.env.MAILER_FROM%',
118
+ host: '%?tenant.env.MAILER_HOST::string%',
119
+ port: '%?tenant.env.MAILER_PORT::number%',
120
+ secure: '%?tenant.env.MAILER_SECURE::bool%',
121
+ user: '%?tenant.env.MAILER_USER%',
122
+ password: '%?tenant.env.MAILER_PASSWORD%',
123
+ },
124
+ credentials: {
125
+ rootEmail: '%?env.CONTEMBER_ROOT_EMAIL%',
126
+ rootToken: '%?env.CONTEMBER_ROOT_TOKEN%',
127
+ rootPassword: '%?env.CONTEMBER_ROOT_PASSWORD%',
128
+ loginToken: '%?env.CONTEMBER_LOGIN_TOKEN%',
129
+ },
130
+ secrets: {
131
+ encryptionKey: '%?env.CONTEMBER_ENCRYPTION_KEY%',
132
+ },
133
+ },
134
+ projectDefaults: {
135
+ db: {
136
+ host: `%project.env.DB_HOST%`,
137
+ port: `%project.env.DB_PORT::number%`,
138
+ user: `%project.env.DB_USER%`,
139
+ password: `%project.secret.db.password||project.env.DB_PASSWORD%`,
140
+ ssl: `%?project.env.DB_SSL::bool%`,
141
+ database: `%project.env.DB_NAME||project.slug%`,
142
+ },
143
+ },
144
+ server: {
145
+ port: '%env.CONTEMBER_PORT::number%',
146
+ monitoringPort: '%env.CONTEMBER_MONITORING_PORT::number%',
147
+ workerCount: '%?env.CONTEMBER_WORKER_COUNT::string%',
148
+ http: {
149
+ requestBodySize: '%?env.CONTEMBER_HTTP_REQUEST_BODY_SIZE::string%',
150
+ },
151
+ projectGroup: {
152
+ domainMapping: '%?env.CONTEMBER_PROJECT_GROUP_DOMAIN_MAPPING%',
153
+ },
154
+ logging: {
155
+ sentry: {
156
+ dsn: '%?env.SENTRY_DSN%',
157
+ },
158
+ },
159
+ },
242
160
  }
243
161
 
162
+
244
163
  const projectNameToEnvName = (projectName: string): string => {
245
- return projectName.toUpperCase().replace(/-/g, '_')
164
+ const envName = projectName.toUpperCase().replace(/-/g, '_')
165
+ if (envName === 'TENANT') {
166
+ throw new Error('Forbidden project name')
167
+ }
168
+ return envName
246
169
  }
247
170
 
248
171
  export type ConfigSource = { data: string; type: 'file' | 'json' | 'yaml' }
@@ -265,59 +188,6 @@ export async function readConfig(
265
188
  ...Object.fromEntries(Object.entries(process.env).filter((it): it is [string, string] => it[1] !== undefined)),
266
189
  }
267
190
 
268
- const defaultTemplate: any = {
269
- tenant: {
270
- db: {
271
- host: `%tenant.env.DB_HOST%`,
272
- port: `%tenant.env.DB_PORT::number%`,
273
- user: `%tenant.env.DB_USER%`,
274
- password: `%tenant.env.DB_PASSWORD%`,
275
- database: `%tenant.env.DB_NAME%`,
276
- ssl: `%?tenant.env.DB_SSL::bool%`,
277
- },
278
- mailer: {
279
- from: '%?tenant.env.MAILER_FROM%',
280
- host: '%?tenant.env.MAILER_HOST::string%',
281
- port: '%?tenant.env.MAILER_PORT::number%',
282
- secure: '%?tenant.env.MAILER_SECURE::bool%',
283
- user: '%?tenant.env.MAILER_USER%',
284
- password: '%?tenant.env.MAILER_PASSWORD%',
285
- },
286
- credentials: {
287
- rootEmail: '%?env.CONTEMBER_ROOT_EMAIL%',
288
- rootToken: '%?env.CONTEMBER_ROOT_TOKEN%',
289
- rootPassword: '%?env.CONTEMBER_ROOT_PASSWORD%',
290
- loginToken: '%?env.CONTEMBER_LOGIN_TOKEN%',
291
- },
292
- secrets: {
293
- encryptionKey: '%?env.CONTEMBER_ENCRYPTION_KEY%',
294
- },
295
- },
296
- projectDefaults: {
297
- db: {
298
- host: `%project.env.DB_HOST%`,
299
- port: `%project.env.DB_PORT::number%`,
300
- user: `%project.env.DB_USER%`,
301
- password: `%project.secret.db.password||project.env.DB_PASSWORD%`,
302
- ssl: `%?project.env.DB_SSL::bool%`,
303
- database: `%project.env.DB_NAME||project.slug%`,
304
- },
305
- },
306
- server: {
307
- port: '%env.CONTEMBER_PORT::number%',
308
- monitoringPort: '%env.CONTEMBER_MONITORING_PORT::number%',
309
- workerCount: '%?env.CONTEMBER_WORKER_COUNT::string%',
310
- http: {
311
- requestBodySize: '%?env.CONTEMBER_HTTP_REQUEST_BODY_SIZE::string%',
312
- },
313
- logging: {
314
- sentry: {
315
- dsn: '%?env.SENTRY_DSN%',
316
- },
317
- },
318
- },
319
- }
320
-
321
191
  const template = configProcessors.reduce(
322
192
  (tpl, processor) => processor.prepareConfigTemplate?.(tpl, { env }) || tpl,
323
193
  defaultTemplate,
@@ -345,10 +215,7 @@ export async function readConfig(
345
215
  return parametersResolver(parts, path, dataResolver)
346
216
  })
347
217
 
348
- const baseConfig = configProcessors.reduce<Config>(
349
- (config, processor) => processor.processConfig?.<any>(config, { env }) || config,
350
- checkConfigStructure(config),
351
- )
218
+ const baseConfig = configSchema(config)
352
219
  return {
353
220
  config: baseConfig,
354
221
  projectConfigResolver: (slug, additionalConfig, secrets) => {
@@ -358,7 +225,7 @@ export async function readConfig(
358
225
  additionalConfig,
359
226
  )
360
227
  if (!mergedConfig.stages) {
361
- mergedConfig.stages = { live: { base: null } }
228
+ mergedConfig.stages = { live: {} }
362
229
  }
363
230
  const resolvedConfig = resolveParameters(mergedConfig, (parts, path, dataResolver) => {
364
231
  if (parts[0] === 'project') {
@@ -383,12 +250,15 @@ export async function readConfig(
383
250
  }
384
251
  throw new UndefinedParameterError(`Parameter "${parts.join('.')}" not found.`)
385
252
  })
386
- const projectConfig = checkProjectStructure(resolvedConfig, slug, 'project')
387
253
 
388
- return configProcessors.reduce(
389
- (config, processor) => processor.processProjectConfig?.(slug, config) ?? config,
390
- projectConfig,
254
+ const projectConfigSchemaWithPlugins = configProcessors.reduce(
255
+ (schema, current) => (
256
+ current.getProjectConfigSchema ? Typesafe.intersection(schema, current.getProjectConfigSchema(slug)) : schema
257
+ ),
258
+ projectConfigSchema,
391
259
  )
260
+ const projectConfig = projectConfigSchemaWithPlugins(resolvedConfig, ['project', slug])
261
+ return projectConfig
392
262
  },
393
263
  }
394
264
  }
package/src/http/index.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from './CollectHttpMetricsMiddelware'
2
- export * from './RootMiddleware'
3
2
  export * from './ShowMetricsMiddleware'
@@ -1,23 +1,28 @@
1
1
  import { ProjectContainerFactory } from './ProjectContainer'
2
2
  import { ProjectConfigResolver, ProjectContainer } from '@contember/engine-http'
3
3
  import { ProjectManager, ProjectWithSecrets } from '@contember/engine-tenant-api'
4
+ import { ProjectGroup } from '@contember/engine-tenant-api'
5
+ import { ProjectInitializer as SystemProjectInitializer } from '@contember/engine-system-api'
6
+ import { ProjectGroupState } from './ProjectGroupState'
7
+ import { Logger } from '@contember/engine-common'
4
8
 
5
9
  export class ProjectContainerResolver {
6
- private containers = new Map<string, { container: ProjectContainer; cleanups: (() => void)[]; timestamp: Date }>()
7
- private aliasMapping = new Map<string, string>()
10
+ private projectGroupStates = new WeakMap<ProjectGroup, ProjectGroupState>()
8
11
 
9
12
  public readonly onCreate: ((container: ProjectContainer) => void | (() => void))[] = []
13
+
10
14
  constructor(
11
- private projectContainerFactory: ProjectContainerFactory,
12
- private projectConfigResolver: ProjectConfigResolver,
13
- private projectManager: ProjectManager,
15
+ private readonly projectContainerFactory: ProjectContainerFactory,
16
+ private readonly projectConfigResolver: ProjectConfigResolver,
17
+ private readonly projectManager: ProjectManager,
18
+ private readonly systemProjectInitializer: SystemProjectInitializer,
14
19
  ) {}
15
20
 
16
- public async getAllProjectContainers(): Promise<ProjectContainer[]> {
17
- const projects = await this.projectManager.getProjects()
21
+ public async getAllProjectContainers(projectGroup: ProjectGroup): Promise<ProjectContainer[]> {
22
+ const projects = await this.projectManager.getProjects(projectGroup.database)
18
23
  return await Promise.all(
19
24
  projects.map(async it => {
20
- const container = await this.getProjectContainer(it.slug)
25
+ const container = await this.getProjectContainer(projectGroup, it.slug)
21
26
  if (!container) {
22
27
  throw new Error('should not happen')
23
28
  }
@@ -26,55 +31,64 @@ export class ProjectContainerResolver {
26
31
  )
27
32
  }
28
33
 
29
- public async getProjectContainer(slug: string, alias: boolean = false): Promise<ProjectContainer | undefined> {
30
- const realSlug = this.aliasMapping.get(slug)
34
+ public async getProjectContainer(projectGroup: ProjectGroup, slug: string, alias: boolean = false): Promise<ProjectContainer | undefined> {
35
+ let groupState = this.getProjectGroupState(projectGroup)
36
+
37
+ const realSlug = groupState.resolveAlias(slug)
31
38
  if (realSlug) {
32
39
  slug = realSlug
33
40
  }
34
- const existing = this.containers.get(slug)
41
+ const existing = groupState.getContainer(slug)
35
42
  if (existing) {
36
- const state = await this.projectManager.getProjectState(slug, existing.timestamp)
43
+ const existingAwaited = await existing
44
+ const state = await this.projectManager.getProjectState(projectGroup.database, slug, existingAwaited.timestamp)
37
45
  if (state === 'valid') {
38
- return existing.container
46
+ return (await existingAwaited).container
39
47
  }
40
- this.destroyContainer(slug)
48
+ await this.destroyContainer(projectGroup, slug)
41
49
  if (state === 'not_found') {
42
50
  return undefined
43
51
  }
44
52
  }
45
- const project = await this.projectManager.getProjectWithSecretsBySlug(slug, alias)
53
+ const project = await this.projectManager.getProjectWithSecretsBySlug(projectGroup.database, slug, alias)
46
54
  if (!project) {
47
55
  return undefined
48
56
  }
49
- const container = this.createProjectContainer(project)
57
+ const container = await this.createProjectContainer(projectGroup, project)
50
58
  if (slug !== project.slug) {
51
- this.aliasMapping.set(slug, project.slug)
59
+ groupState.setAlias(project.slug, slug)
52
60
  }
53
61
  return container
54
62
  }
55
63
 
56
- public createProjectContainer(project: ProjectWithSecrets): ProjectContainer {
64
+ public async createProjectContainer(projectGroup: ProjectGroup, project: ProjectWithSecrets): Promise<ProjectContainer> {
57
65
  const projectConfig = this.projectConfigResolver(project.slug, project.config, project.secrets)
58
- const existing = this.containers.get(project.slug)
59
- if (existing) {
60
- return existing.container
61
- }
62
- const projectContainer = this.projectContainerFactory.createContainer(projectConfig)
63
- const cleanups = this.onCreate.map(it => it(projectContainer) || (() => null))
64
- this.containers.set(project.slug, {
65
- container: projectContainer,
66
- cleanups,
67
- timestamp: project.updatedAt,
68
- })
69
- return projectContainer
66
+ const groupState = this.getProjectGroupState(projectGroup)
67
+ return (await groupState.fetchContainer(project.slug, async slug => {
68
+ const projectContainer = this.projectContainerFactory.createContainer(projectConfig)
69
+ await this.systemProjectInitializer.initialize(
70
+ projectContainer.systemDatabaseContextFactory,
71
+ projectContainer.project,
72
+ // eslint-disable-next-line no-console
73
+ new Logger(console.log),
74
+ )
75
+ const cleanups = this.onCreate.map(it => it(projectContainer) || (() => null))
76
+ return {
77
+ container: projectContainer,
78
+ cleanups,
79
+ timestamp: project.updatedAt,
80
+ }
81
+ })).container
70
82
  }
71
83
 
72
- public destroyContainer(slug: string): void {
73
- const existing = this.containers.get(slug)
84
+ public async destroyContainer(projectGroup: ProjectGroup, slug: string): Promise<void> {
85
+ const groupState = this.getProjectGroupState(projectGroup)
86
+ const existing = groupState.getContainer(slug)
74
87
  if (existing) {
75
- existing.cleanups.forEach(it => it())
76
- existing.container.project.alias?.forEach(it => this.aliasMapping.delete(it))
77
- this.containers.delete(slug)
88
+ const existingAwaited = await existing
89
+ existingAwaited.cleanups.forEach(it => it())
90
+ existingAwaited.container.project.alias?.forEach(it => groupState.removeAlias(it))
91
+ groupState.removeContainer(slug)
78
92
  }
79
93
  process.nextTick(() => {
80
94
  if (global.gc) {
@@ -82,4 +96,13 @@ export class ProjectContainerResolver {
82
96
  }
83
97
  })
84
98
  }
99
+
100
+ private getProjectGroupState(projectGroup: ProjectGroup): ProjectGroupState {
101
+ let groupState = this.projectGroupStates.get(projectGroup)
102
+ if (!groupState) {
103
+ groupState = new ProjectGroupState()
104
+ this.projectGroupStates.set(projectGroup, new ProjectGroupState())
105
+ }
106
+ return groupState
107
+ }
85
108
  }
@@ -0,0 +1,33 @@
1
+ import { ProjectContainer } from '@contember/engine-http'
2
+ import { PromiseMap } from '@contember/engine-common'
3
+
4
+ type ContainerWithMeta = { container: ProjectContainer; cleanups: (() => void)[]; timestamp: Date }
5
+
6
+ export class ProjectGroupState {
7
+ private containers = new PromiseMap<string, ContainerWithMeta>()
8
+ private aliasMapping = new Map<string, string>()
9
+
10
+ public resolveAlias(slug: string): string | undefined {
11
+ return this.aliasMapping.get(slug)
12
+ }
13
+
14
+ public setAlias(slug: string, alias: string): void {
15
+ this.aliasMapping.set(alias, slug)
16
+ }
17
+
18
+ public removeAlias(alias: string): void {
19
+ this.aliasMapping.delete(alias)
20
+ }
21
+
22
+ public getContainer(slug: string): Promise<ContainerWithMeta> | undefined {
23
+ return this.containers.get(slug)
24
+ }
25
+
26
+ public async fetchContainer(slug: string, factory: (slug: string) => Promise<ContainerWithMeta>): Promise<ContainerWithMeta> {
27
+ return await this.containers.fetch(slug, factory)
28
+ }
29
+
30
+ public removeContainer(slug: string): void {
31
+ this.containers.delete(slug)
32
+ }
33
+ }