@contember/engine-server 1.1.0-alpha.1 → 1.1.0-alpha.4

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 (93) hide show
  1. package/alpine.dockerfile +3 -1
  2. package/debian.dockerfile +3 -1
  3. package/dist/src/MasterContainer.d.ts +104 -8
  4. package/dist/src/MasterContainer.d.ts.map +1 -1
  5. package/dist/src/MasterContainer.js +65 -84
  6. package/dist/src/MasterContainer.js.map +1 -1
  7. package/dist/src/bootstrap/Initializer.d.ts +3 -12
  8. package/dist/src/bootstrap/Initializer.d.ts.map +1 -1
  9. package/dist/src/bootstrap/Initializer.js +8 -24
  10. package/dist/src/bootstrap/Initializer.js.map +1 -1
  11. package/dist/src/config/config.d.ts +7 -49
  12. package/dist/src/config/config.d.ts.map +1 -1
  13. package/dist/src/config/config.js +11 -188
  14. package/dist/src/config/config.js.map +1 -1
  15. package/dist/src/config/configSchema.d.ts +84 -0
  16. package/dist/src/config/configSchema.d.ts.map +1 -0
  17. package/dist/src/config/configSchema.js +80 -0
  18. package/dist/src/config/configSchema.js.map +1 -0
  19. package/dist/src/config/configTemplate.d.ts +2 -0
  20. package/dist/src/config/configTemplate.d.ts.map +1 -0
  21. package/dist/src/config/configTemplate.js +62 -0
  22. package/dist/src/config/configTemplate.js.map +1 -0
  23. package/dist/src/config/projectConfigResolver.d.ts +8 -0
  24. package/dist/src/config/projectConfigResolver.d.ts.map +1 -0
  25. package/dist/src/config/projectConfigResolver.js +63 -0
  26. package/dist/src/config/projectConfigResolver.js.map +1 -0
  27. package/dist/src/config/tenantConfigResolver.d.ts +8 -0
  28. package/dist/src/config/tenantConfigResolver.d.ts.map +1 -0
  29. package/dist/src/config/tenantConfigResolver.js +31 -0
  30. package/dist/src/config/tenantConfigResolver.js.map +1 -0
  31. package/dist/src/http/CollectHttpMetricsMiddelware.d.ts +2 -2
  32. package/dist/src/http/CollectHttpMetricsMiddelware.d.ts.map +1 -1
  33. package/dist/src/http/CollectHttpMetricsMiddelware.js +6 -5
  34. package/dist/src/http/CollectHttpMetricsMiddelware.js.map +1 -1
  35. package/dist/src/project/ProjectContainer.d.ts +14 -16
  36. package/dist/src/project/ProjectContainer.d.ts.map +1 -1
  37. package/dist/src/project/ProjectContainer.js +17 -12
  38. package/dist/src/project/ProjectContainer.js.map +1 -1
  39. package/dist/src/project/ProjectContainerResolver.d.ts +12 -10
  40. package/dist/src/project/ProjectContainerResolver.d.ts.map +1 -1
  41. package/dist/src/project/ProjectContainerResolver.js +26 -33
  42. package/dist/src/project/ProjectContainerResolver.js.map +1 -1
  43. package/dist/src/project/{ProjectGroupState.d.ts → ProjectContainerStore.d.ts} +2 -2
  44. package/dist/src/project/ProjectContainerStore.d.ts.map +1 -0
  45. package/dist/src/project/{ProjectGroupState.js → ProjectContainerStore.js} +4 -4
  46. package/dist/src/project/ProjectContainerStore.js.map +1 -0
  47. package/dist/src/project/ProjectInitializer.d.ts +3 -3
  48. package/dist/src/project/ProjectInitializer.d.ts.map +1 -1
  49. package/dist/src/project/ProjectInitializer.js +4 -4
  50. package/dist/src/project/ProjectInitializer.js.map +1 -1
  51. package/dist/src/project/ProjectSchemaResolver.d.ts +3 -3
  52. package/dist/src/project/ProjectSchemaResolver.d.ts.map +1 -1
  53. package/dist/src/project/ProjectSchemaResolver.js +5 -5
  54. package/dist/src/project/ProjectSchemaResolver.js.map +1 -1
  55. package/dist/src/projectGroup/ProjectGroupContainer.d.ts +23 -0
  56. package/dist/src/projectGroup/ProjectGroupContainer.d.ts.map +1 -0
  57. package/dist/src/projectGroup/ProjectGroupContainer.js +64 -0
  58. package/dist/src/projectGroup/ProjectGroupContainer.js.map +1 -0
  59. package/dist/src/projectGroup/ProjectGroupContainerResolver.d.ts +12 -0
  60. package/dist/src/projectGroup/ProjectGroupContainerResolver.d.ts.map +1 -0
  61. package/dist/src/projectGroup/ProjectGroupContainerResolver.js +36 -0
  62. package/dist/src/projectGroup/ProjectGroupContainerResolver.js.map +1 -0
  63. package/dist/src/start.js +9 -8
  64. package/dist/src/start.js.map +1 -1
  65. package/dist/src/tsconfig.tsbuildinfo +1 -1
  66. package/dist/src/utils/dbMetrics.d.ts +6 -2
  67. package/dist/src/utils/dbMetrics.d.ts.map +1 -1
  68. package/dist/src/utils/dbMetrics.js +13 -16
  69. package/dist/src/utils/dbMetrics.js.map +1 -1
  70. package/dist/tests/cases/http.js +30 -19
  71. package/dist/tests/cases/http.js.map +1 -1
  72. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +22 -21
  74. package/src/MasterContainer.ts +145 -204
  75. package/src/bootstrap/Initializer.ts +9 -34
  76. package/src/config/config.ts +23 -234
  77. package/src/config/configSchema.ts +114 -0
  78. package/src/config/configTemplate.ts +58 -0
  79. package/src/config/projectConfigResolver.ts +83 -0
  80. package/src/config/tenantConfigResolver.ts +41 -0
  81. package/src/http/CollectHttpMetricsMiddelware.ts +6 -5
  82. package/src/project/ProjectContainer.ts +25 -18
  83. package/src/project/ProjectContainerResolver.ts +29 -37
  84. package/src/project/{ProjectGroupState.ts → ProjectContainerStore.ts} +1 -1
  85. package/src/project/ProjectInitializer.ts +5 -9
  86. package/src/project/ProjectSchemaResolver.ts +6 -10
  87. package/src/projectGroup/ProjectGroupContainer.ts +120 -0
  88. package/src/projectGroup/ProjectGroupContainerResolver.ts +47 -0
  89. package/src/start.ts +9 -8
  90. package/src/utils/dbMetrics.ts +19 -20
  91. package/tests/cases/http.ts +35 -22
  92. package/dist/src/project/ProjectGroupState.d.ts.map +0 -1
  93. package/dist/src/project/ProjectGroupState.js.map +0 -1
@@ -1,188 +1,30 @@
1
- import {
2
- ConfigLoader,
3
- createObjectParametersResolver,
4
- Merger,
5
- resolveParameters,
6
- UndefinedParameterError,
7
- } from '@contember/config-loader'
8
- import { upperCaseFirst } from '../utils'
1
+ import { ConfigLoader, createObjectParametersResolver, Merger, resolveParameters } from '@contember/config-loader'
9
2
  import { ConfigProcessor } from '@contember/engine-plugins'
10
- import { Typesafe } from '@contember/engine-common'
11
- import { ProjectConfigResolver } from '@contember/engine-http'
12
- import { MailerOptions } from '@contember/engine-tenant-api'
3
+ import { serverConfigSchema, tenantConfigSchema } from './configSchema'
4
+ import { configTemplate } from './configTemplate'
5
+ import { createProjectConfigResolver, ProjectConfigResolver } from './projectConfigResolver'
6
+ import { createTenantConfigResolver, TenantConfigResolver } from './tenantConfigResolver'
13
7
 
14
- export type Config = ReturnType<typeof configSchema>
15
- export type TenantConfig = Config['tenant']
16
-
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
- )
29
-
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 } } : {}),
44
- }
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
- })
87
-
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
- }))
95
-
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
- })
105
-
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
- rootTokenHash: '%?env.CONTEMBER_ROOT_TOKEN_HASH%',
128
- rootPassword: '%?env.CONTEMBER_ROOT_PASSWORD%',
129
- loginToken: '%?env.CONTEMBER_LOGIN_TOKEN%',
130
- },
131
- secrets: {
132
- encryptionKey: '%?env.CONTEMBER_ENCRYPTION_KEY%',
133
- },
134
- },
135
- projectDefaults: {
136
- db: {
137
- host: `%project.env.DB_HOST%`,
138
- port: `%project.env.DB_PORT::number%`,
139
- user: `%project.env.DB_USER%`,
140
- password: `%project.secret.db.password||project.env.DB_PASSWORD%`,
141
- ssl: `%?project.env.DB_SSL::bool%`,
142
- database: `%project.env.DB_NAME||project.slug%`,
143
- },
144
- },
145
- server: {
146
- port: '%env.CONTEMBER_PORT::number%',
147
- monitoringPort: '%env.CONTEMBER_MONITORING_PORT::number%',
148
- workerCount: '%?env.CONTEMBER_WORKER_COUNT::string%',
149
- http: {
150
- requestBodySize: '%?env.CONTEMBER_HTTP_REQUEST_BODY_SIZE::string%',
151
- },
152
- projectGroup: {
153
- domainMapping: '%?env.CONTEMBER_PROJECT_GROUP_DOMAIN_MAPPING%',
154
- },
155
- logging: {
156
- sentry: {
157
- dsn: '%?env.SENTRY_DSN%',
158
- },
159
- },
160
- },
161
- }
162
-
163
-
164
- const projectNameToEnvName = (projectName: string): string => {
165
- const envName = projectName.toUpperCase().replace(/-/g, '_')
166
- if (envName === 'TENANT') {
167
- throw new Error('Forbidden project name')
168
- }
169
- return envName
170
- }
8
+ export type ServerConfig = ReturnType<typeof serverConfigSchema>
9
+ export type TenantConfig = ReturnType<typeof tenantConfigSchema>
171
10
 
172
11
  export type ConfigSource = { data: string; type: 'file' | 'json' | 'yaml' }
173
12
 
13
+ type Env = Record<string, string>
14
+
174
15
  export async function readConfig(
175
16
  configSources: ConfigSource[],
176
17
  configProcessors: ConfigProcessor[] = [],
177
18
  ): Promise<{
178
- config: Config
19
+ serverConfig: ServerConfig
179
20
  projectConfigResolver: ProjectConfigResolver
21
+ tenantConfigResolver: TenantConfigResolver
180
22
  }> {
181
23
  const loader = new ConfigLoader()
182
24
  const configs = await Promise.all(
183
25
  configSources.map(it => (it.type === 'file' ? loader.load(it.data) : loader.loadString(it.data, it.type))),
184
26
  )
185
- const env: Record<string, string> = {
27
+ const env: Env = {
186
28
  ...configProcessors.reduce((acc, curr) => ({ ...acc, ...(curr.getDefaultEnv?.() || {}) }), {
187
29
  DEFAULT_DB_PORT: '5432',
188
30
  }),
@@ -191,75 +33,22 @@ export async function readConfig(
191
33
 
192
34
  const template = configProcessors.reduce(
193
35
  (tpl, processor) => processor.prepareConfigTemplate?.(tpl, { env }) || tpl,
194
- defaultTemplate,
36
+ configTemplate,
195
37
  )
196
38
 
197
- let { projectDefaults, ...config } = Merger.merge(template, ...configs)
39
+ const rawConfig = Merger.merge(template, ...configs)
198
40
 
199
41
  const parametersResolver = createObjectParametersResolver({ env })
200
- config = resolveParameters(config, (parts, path, dataResolver) => {
201
- if (parts[0] === 'tenant') {
202
- if (path[0] !== 'tenant') {
203
- throw new Error(`Invalid use of ${parts.join('.')} variable in path ${path.join('.')}.`)
204
- }
205
- if (parts[1] === 'env') {
206
- const envName = parts[2]
207
- const envValue = env['TENANT_' + envName] || env['DEFAULT_' + envName]
208
- if (envValue === undefined) {
209
- throw new UndefinedParameterError(`ENV variable "${'TENANT_' + envName}" not found.`)
210
- }
211
- return envValue
212
- }
213
- throw new UndefinedParameterError(`Parameter "${parts.join('.')}" not found.`)
214
- }
215
-
216
- return parametersResolver(parts, path, dataResolver)
217
- })
42
+ const config = resolveParameters(rawConfig.server, parametersResolver)
218
43
 
219
- const baseConfig = configSchema(config)
44
+ const serverConfig = serverConfigSchema(config)
220
45
  return {
221
- config: baseConfig,
222
- projectConfigResolver: (slug, additionalConfig, secrets) => {
223
- const mergedConfig = Merger.merge(
224
- projectDefaults as any,
225
- (config?.projects as any)?.[slug] as any,
226
- additionalConfig,
227
- )
228
- if (!mergedConfig.stages) {
229
- mergedConfig.stages = { live: {} }
230
- }
231
- const resolvedConfig = resolveParameters(mergedConfig, (parts, path, dataResolver) => {
232
- if (parts[0] === 'project') {
233
- if (parts[1] === 'env') {
234
- const envName = parts[2]
235
- const projectEnvName = projectNameToEnvName(slug)
236
- const envValue = env[projectEnvName + '_' + envName] || env['DEFAULT_' + envName]
237
- if (envValue === undefined) {
238
- throw new UndefinedParameterError(`ENV variable "${projectEnvName + '_' + envName}" not found.`)
239
- }
240
- return envValue
241
- } else if (parts[1] === 'slug') {
242
- return slug
243
- } else if (parts[1] === 'secret') {
244
- const key = parts.slice(2).join('.')
245
- const value = secrets[key]
246
- if (value === undefined) {
247
- throw new UndefinedParameterError(`Project secret ${parts[2]} not found`)
248
- }
249
- return value
250
- }
251
- }
252
- throw new UndefinedParameterError(`Parameter "${parts.join('.')}" not found.`)
253
- })
254
-
255
- const projectConfigSchemaWithPlugins = configProcessors.reduce(
256
- (schema, current) => (
257
- current.getProjectConfigSchema ? Typesafe.intersection(schema, current.getProjectConfigSchema(slug)) : schema
258
- ),
259
- projectConfigSchema,
260
- )
261
- const projectConfig = projectConfigSchemaWithPlugins(resolvedConfig, ['project', slug])
262
- return projectConfig
263
- },
46
+ serverConfig,
47
+ projectConfigResolver: createProjectConfigResolver(env, rawConfig, configProcessors),
48
+ tenantConfigResolver: createTenantConfigResolver(env, rawConfig.tenant),
264
49
  }
265
50
  }
51
+
52
+
53
+
54
+
@@ -0,0 +1,114 @@
1
+ import { Typesafe } from '@contember/engine-common'
2
+ import { MailerOptions } from '@contember/engine-tenant-api'
3
+ import { upperCaseFirst } from '../utils'
4
+
5
+
6
+ export const dbCredentialsSchema = Typesafe.intersection(
7
+ Typesafe.object({
8
+ host: Typesafe.string,
9
+ port: Typesafe.number,
10
+ user: Typesafe.string,
11
+ password: Typesafe.string,
12
+ database: Typesafe.string,
13
+ }),
14
+ Typesafe.partial({
15
+ ssl: Typesafe.boolean,
16
+ }),
17
+ )
18
+
19
+ export const tenantConfigSchema = Typesafe.intersection(
20
+ Typesafe.object({
21
+ db: dbCredentialsSchema,
22
+ mailer: Typesafe.transform(Typesafe.partial({
23
+ from: Typesafe.string,
24
+ host: Typesafe.string,
25
+ port: Typesafe.number,
26
+ user: Typesafe.string,
27
+ password: Typesafe.string,
28
+ }), (value, input): MailerOptions => {
29
+ return {
30
+ ...(input as MailerOptions),
31
+ ...value,
32
+ ...(value.user ? { auth: { user: value.user, pass: value.password } } : {}),
33
+ }
34
+ }),
35
+ credentials: Typesafe.partial({
36
+ rootToken: Typesafe.string,
37
+ rootTokenHash: Typesafe.string,
38
+ rootPassword: Typesafe.string,
39
+ rootEmail: Typesafe.string,
40
+ loginToken: Typesafe.string,
41
+ }),
42
+ }),
43
+ Typesafe.object({
44
+ secrets: Typesafe.partial({
45
+ encryptionKey: Typesafe.string,
46
+ }),
47
+ }),
48
+ )
49
+ export const serverConfigSchema = Typesafe.intersection(
50
+ Typesafe.object({
51
+ port: Typesafe.number,
52
+ monitoringPort: Typesafe.number,
53
+ http: Typesafe.partial({
54
+ requestBodySize: Typesafe.string,
55
+ }),
56
+ logging: Typesafe.union(
57
+ val => Typesafe.valueAt(val, ['sentry', 'dsn']) === undefined ? {} : Typesafe.fail([]),
58
+ Typesafe.partial({
59
+ sentry: Typesafe.object({
60
+ dsn: Typesafe.string,
61
+ }),
62
+ }),
63
+ ),
64
+ }),
65
+ Typesafe.partial({
66
+ workerCount: Typesafe.union(Typesafe.number, Typesafe.string),
67
+ projectGroup: (val, path) => Typesafe.valueAt(val, ['domainMapping']) === undefined
68
+ ? undefined
69
+ : Typesafe.intersection(
70
+ Typesafe.object({
71
+ domainMapping: Typesafe.string,
72
+ }),
73
+ Typesafe.partial({
74
+ configHeader: Typesafe.string,
75
+ configEncryptionKey: Typesafe.string,
76
+ }),
77
+ )(val, path),
78
+ }),
79
+ )
80
+
81
+ export const stageConfig = Typesafe.map(
82
+ Typesafe.intersection(
83
+ Typesafe.object({
84
+ name: Typesafe.union(
85
+ Typesafe.string,
86
+ (_, path = []) => upperCaseFirst(String(path[path.length - 2])),
87
+ ),
88
+ slug: (_, path = []) => String(path[path.length - 2]),
89
+ }),
90
+ Typesafe.partial({
91
+ schema: Typesafe.string,
92
+ }),
93
+ ),
94
+ )
95
+
96
+ export 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: Typesafe.intersection(
104
+ Typesafe.union(
105
+ Typesafe.object({
106
+ useTenantDb: Typesafe.literal(true),
107
+ }),
108
+ dbCredentialsSchema,
109
+ ),
110
+ Typesafe.partial({
111
+ systemSchema: Typesafe.string,
112
+ }),
113
+ ),
114
+ })
@@ -0,0 +1,58 @@
1
+ export const configTemplate: any = {
2
+ tenant: {
3
+ db: {
4
+ host: `%tenant.env.DB_HOST%`,
5
+ port: `%tenant.env.DB_PORT::number%`,
6
+ user: `%tenant.env.DB_USER%`,
7
+ password: `%tenant.env.DB_PASSWORD%`,
8
+ database: `%tenant.env.DB_NAME%`,
9
+ ssl: `%?tenant.env.DB_SSL::bool%`,
10
+ },
11
+ mailer: {
12
+ from: '%?tenant.env.MAILER_FROM%',
13
+ host: '%?tenant.env.MAILER_HOST::string%',
14
+ port: '%?tenant.env.MAILER_PORT::number%',
15
+ secure: '%?tenant.env.MAILER_SECURE::bool%',
16
+ user: '%?tenant.env.MAILER_USER%',
17
+ password: '%?tenant.env.MAILER_PASSWORD%',
18
+ },
19
+ credentials: {
20
+ rootEmail: '%?env.CONTEMBER_ROOT_EMAIL%',
21
+ rootToken: '%?env.CONTEMBER_ROOT_TOKEN%',
22
+ rootTokenHash: '%?env.CONTEMBER_ROOT_TOKEN_HASH%',
23
+ rootPassword: '%?env.CONTEMBER_ROOT_PASSWORD%',
24
+ loginToken: '%?env.CONTEMBER_LOGIN_TOKEN%',
25
+ },
26
+ secrets: {
27
+ encryptionKey: '%?env.CONTEMBER_ENCRYPTION_KEY%',
28
+ },
29
+ },
30
+ projectDefaults: {
31
+ db: {
32
+ host: `%?project.env.DB_HOST%`,
33
+ port: `%?project.env.DB_PORT::number%`,
34
+ user: `%?project.env.DB_USER%`,
35
+ password: `%?project.secret.db.password||project.env.DB_PASSWORD%`,
36
+ database: `%?project.env.DB_NAME||project.slug%`,
37
+ ssl: `%?project.env.DB_SSL::bool%`,
38
+ },
39
+ },
40
+ server: {
41
+ port: '%env.CONTEMBER_PORT::number%',
42
+ monitoringPort: '%env.CONTEMBER_MONITORING_PORT::number%',
43
+ workerCount: '%?env.CONTEMBER_WORKER_COUNT::string%',
44
+ http: {
45
+ requestBodySize: '%?env.CONTEMBER_HTTP_REQUEST_BODY_SIZE::string%',
46
+ },
47
+ projectGroup: {
48
+ domainMapping: '%?env.CONTEMBER_PROJECT_GROUP_DOMAIN_MAPPING%',
49
+ configHeader: '%?env.CONTEMBER_PROJECT_GROUP_CONFIG_HEADER%',
50
+ configEncryptionKey: '%?env.CONTEMBER_PROJECT_GROUP_CONFIG_ENCRYPTION_KEY%',
51
+ },
52
+ logging: {
53
+ sentry: {
54
+ dsn: '%?env.SENTRY_DSN%',
55
+ },
56
+ },
57
+ },
58
+ }
@@ -0,0 +1,83 @@
1
+ import { ProjectConfig, ProjectSecrets } from '@contember/engine-http'
2
+ import { ConfigProcessor } from '@contember/engine-plugins'
3
+ import { Merger, ParametersResolver, resolveParameters, UndefinedParameterError } from '@contember/config-loader'
4
+ import { Typesafe } from '@contember/engine-common'
5
+ import { projectConfigSchema } from './configSchema'
6
+ import { TenantConfig } from './config'
7
+
8
+ export type ProjectConfigResolver = (
9
+ slug: string,
10
+ additionalConfig: any,
11
+ secrets: ProjectSecrets,
12
+ tenantConfig: TenantConfig,
13
+ ) => ProjectConfig
14
+
15
+ type Env = Record<string, string>
16
+
17
+ export const createProjectConfigResolver = (env: Env, config: any, configProcessors: ConfigProcessor[]): ProjectConfigResolver =>
18
+ (slug, additionalConfig, secrets, tenantConfig): ProjectConfig => {
19
+ const mergedConfig = Merger.merge(
20
+ config.projectDefaults as any,
21
+ (config?.projects as any)?.[slug] as any,
22
+ additionalConfig,
23
+ )
24
+ if (!mergedConfig.stages) {
25
+ mergedConfig.stages = { live: {} }
26
+ }
27
+ const resolvedConfig = resolveParameters(mergedConfig, createProjectParametersResolver(slug, env, secrets))
28
+
29
+ const projectConfigSchemaWithPlugins = configProcessors.reduce(
30
+ (schema, current) => (
31
+ current.getProjectConfigSchema ? Typesafe.intersection(schema, current.getProjectConfigSchema(slug)) : schema
32
+ ),
33
+ projectConfigSchema,
34
+ )
35
+ const projectConfig = projectConfigSchemaWithPlugins(resolvedConfig, ['project', slug])
36
+ if (!('host' in projectConfig.db)) {
37
+ return {
38
+ ...projectConfig,
39
+ db: {
40
+ ...tenantConfig.db,
41
+ ...projectConfig.db,
42
+ },
43
+ }
44
+ }
45
+ return {
46
+ ...projectConfig,
47
+ db: projectConfig.db,
48
+ }
49
+ }
50
+
51
+
52
+ const createProjectParametersResolver = (slug: string, env: Env, secrets: ProjectSecrets): ParametersResolver =>
53
+ parts => {
54
+ if (parts[0] === 'project') {
55
+ if (parts[1] === 'env') {
56
+ const envName = parts[2]
57
+ const projectEnvName = projectNameToEnvName(slug)
58
+ const envValue = env[projectEnvName + '_' + envName] || env['DEFAULT_' + envName]
59
+ if (envValue === undefined) {
60
+ throw new UndefinedParameterError(`ENV variable "${projectEnvName + '_' + envName}" not found.`)
61
+ }
62
+ return envValue
63
+ } else if (parts[1] === 'slug') {
64
+ return slug
65
+ } else if (parts[1] === 'secret') {
66
+ const key = parts.slice(2).join('.')
67
+ const value = secrets[key]
68
+ if (value === undefined) {
69
+ throw new UndefinedParameterError(`Project secret ${parts[2]} not found`)
70
+ }
71
+ return value
72
+ }
73
+ }
74
+ throw new UndefinedParameterError(`Parameter "${parts.join('.')}" not found.`)
75
+ }
76
+
77
+ const projectNameToEnvName = (projectName: string): string => {
78
+ const envName = projectName.toUpperCase().replace(/-/g, '_')
79
+ if (envName === 'TENANT') {
80
+ throw new Error('Forbidden project name')
81
+ }
82
+ return envName
83
+ }
@@ -0,0 +1,41 @@
1
+ import { TenantConfig } from './config'
2
+ import {
3
+ createObjectParametersResolver,
4
+ Merger,
5
+ ParametersResolver,
6
+ resolveParameters,
7
+ UndefinedParameterError,
8
+ } from '@contember/config-loader'
9
+ import { tenantConfigSchema } from './configSchema'
10
+
11
+ export type TenantConfigResolver = (slug: string | undefined, additionalConfig: any) => TenantConfig
12
+
13
+ type Env = Record<string, string>
14
+
15
+ export const createTenantConfigResolver = (env: Env, tenantConfig: any): TenantConfigResolver =>
16
+ (slug, additionalConfig) => {
17
+ const mergedConfig = Merger.merge(tenantConfig, additionalConfig)
18
+ const parametersResolver = createObjectParametersResolver({ env })
19
+ const tenantParametersResolver = createTenantParametersResolver(env)
20
+ const resolvedConfig = resolveParameters(mergedConfig, (parts, path, dataResolver) => {
21
+ if (parts[0] === 'tenant') {
22
+ return tenantParametersResolver(parts, path, dataResolver)
23
+ }
24
+ return parametersResolver(parts, path, dataResolver)
25
+ })
26
+ return tenantConfigSchema(resolvedConfig)
27
+ }
28
+
29
+
30
+ export const createTenantParametersResolver = (env: Env): ParametersResolver =>
31
+ parts => {
32
+ if (parts[1] === 'env') {
33
+ const envName = parts[2]
34
+ const envValue = env['TENANT_' + envName] || env['DEFAULT_' + envName]
35
+ if (envValue === undefined) {
36
+ throw new UndefinedParameterError(`ENV variable "${'TENANT_' + envName}" not found.`)
37
+ }
38
+ return envValue
39
+ }
40
+ throw new UndefinedParameterError(`Parameter "${parts.join('.')}" not found.`)
41
+ }
@@ -1,15 +1,15 @@
1
- import { KoaMiddleware, ModuleInfoMiddlewareState, ProjectResolveMiddlewareState } from '@contember/engine-http'
1
+ import { KoaMiddleware, ModuleInfoMiddlewareState, ProjectInfoMiddlewareState } from '@contember/engine-http'
2
2
  import prom from 'prom-client'
3
3
  import { GraphQLKoaState } from '@contember/engine-http'
4
4
 
5
5
  export const createColllectHttpMetricsMiddleware = (
6
6
  registry: prom.Registry,
7
- ): KoaMiddleware<Partial<ProjectResolveMiddlewareState & ModuleInfoMiddlewareState & GraphQLKoaState>> => {
7
+ ): KoaMiddleware<Partial<ProjectInfoMiddlewareState & ModuleInfoMiddlewareState & GraphQLKoaState>> => {
8
8
  const requestSummary = new prom.Histogram({
9
9
  name: 'contember_http_duration_seconds',
10
- help: 'Incoming HTTP requests statistics by http_method (OPTIONS requests are ignored), http_code, contember_project (or "unknown" for undefined project), contember_module (system, tenant, content, unknown) and graphql_operation (query, mutation, unknown)',
10
+ help: 'Incoming HTTP requests statistics by http_method (OPTIONS requests are ignored), http_code, contember_project (or "unknown" for undefined project), contember_project_group (or "unknown" for undefined project group), contember_module (system, tenant, content, unknown) and graphql_operation (query, mutation, unknown)',
11
11
  registers: [registry],
12
- labelNames: ['http_method', 'http_code', 'contember_project', 'contember_module', 'graphql_operation'],
12
+ labelNames: ['http_method', 'http_code', 'contember_project', 'contember_project_group', 'contember_module', 'graphql_operation'],
13
13
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
14
14
  })
15
15
 
@@ -21,7 +21,8 @@ export const createColllectHttpMetricsMiddleware = (
21
21
  const labels = {
22
22
  http_method: ctx.method,
23
23
  http_code: ctx.status,
24
- contember_project: ctx.state.project?.slug || 'unknown',
24
+ contember_project: ctx.state.project || 'unknown',
25
+ contember_project_group: ctx.state.projectGroup || 'unknown',
25
26
  contember_module: ctx.state.module || 'unknown',
26
27
  graphql_operation: ctx.state.graphql?.operationName || 'unknown',
27
28
  }